Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add socketfilterfw parser table #1812

Merged
merged 9 commits into from
Aug 1, 2024
4 changes: 4 additions & 0 deletions ee/allowedcmd/cmd_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ func Scutil(ctx context.Context, arg ...string) (*exec.Cmd, error) {
return validatedCommand(ctx, "/usr/sbin/scutil", arg...)
}

func Socketfilterfw(ctx context.Context, arg ...string) (*exec.Cmd, error) {
return validatedCommand(ctx, "/usr/libexec/ApplicationFirewall/socketfilterfw", arg...)
}

func Softwareupdate(ctx context.Context, arg ...string) (*exec.Cmd, error) {
return validatedCommand(ctx, "/usr/sbin/softwareupdate", arg...)
}
Expand Down
125 changes: 125 additions & 0 deletions ee/tables/execparsers/socketfilterfw/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package socketfilterfw

import (
"bufio"
"io"
"regexp"
"strings"
)

var appRegex = regexp.MustCompile("(.*)(?:\\s\\(state:\\s)([0-9]+)")
var lineRegex = regexp.MustCompile("(state|block|built-in|downloaded|stealth|log mode|log option)(?:.*\\s)([0-9a-z]+)")

// socketfilterfw returns lines for each `get` argument supplied.
// The output data is in the same order as the supplied arguments.
//
// This supports parsing the list of apps and their allow state, or
// each line describes a part of the feature and what state it's in.
//
// These are not very well-formed, so I'm doing some regex magic to
// know which option the current line is, and then sanitize the state.
Micah-Kolide marked this conversation as resolved.
Show resolved Hide resolved
func socketfilterfwParse(reader io.Reader) (any, error) {
results := make([]map[string]string, 0)
row := make(map[string]string)
Micah-Kolide marked this conversation as resolved.
Show resolved Hide resolved
parseAppData := false

scanner := bufio.NewScanner(reader)
for scanner.Scan() {
line := scanner.Text()

// When parsing the app list, the first line of output is a total
// count of apps. We can break on this line to start parsing apps.
if strings.Contains(line, "Total number of apps") {
parseAppData = true
continue
}

if parseAppData {
appRow := parseAppList(line)
if appRow != nil {
results = append(results, appRow)
}

continue
}
Micah-Kolide marked this conversation as resolved.
Show resolved Hide resolved

k, v := parseLine(line)
if k != "" {
row[k] = v
}
RebeccaMahany marked this conversation as resolved.
Show resolved Hide resolved
}

if len(row) > 0 {
results = append(results, row)
}

return results, nil
}
Micah-Kolide marked this conversation as resolved.
Show resolved Hide resolved

// parseAppList parses the current line and returns the app name and
// state matches as a row of data.
func parseAppList(line string) map[string]string {
matches := appRegex.FindStringSubmatch(line)
if len(matches) != 3 {
return nil
}

return map[string]string{
"name": matches[1],
"allow_incoming_connections": sanitizeState(matches[2]),
}
}

// parseLine parse the current line and returns a feature key with the
// respective state/mode of said feature. We want all features to be a
// part of the same row of data, so we do not return this pair as a row.
func parseLine(line string) (string, string) {
matches := lineRegex.FindStringSubmatch(strings.ToLower(line))
if len(matches) != 3 {
return "", ""
}

var key string
switch matches[1] {
case "state":
key = "global_state_enabled"
case "block":
key = "block_all_enabled"
case "built-in":
key = "allow_built-in_signed_enabled"
case "downloaded":
key = "allow_downloaded_signed_enabled"
case "stealth":
key = "stealth_enabled"
case "log mode":
key = "logging_enabled"
case "log option":
key = "logging_option"
default:
return "", ""
}

return key, sanitizeState(matches[2])
}

// sanitizeState takes in a state like string and returns
// the correct boolean to create a consistent state value.
func sanitizeState(state string) string {
switch state {
// The app list state for when an app is blocking incoming connections
// is output as `4`, while `1` is the state to allow those connections.
case "0", "off", "disabled", "4":
return "0"
// When the "block all" firewall option is enabled, it doesn't
// include a state like string, which is why we match on
// the string value of "connections" for that mode.
case "1", "on", "enabled", "connections":
return "1"
case "throttled", "brief", "detail":
// The "logging option" value differs from the booleans.
// Can be one of `throttled`, `brief`, or `detail`.
return state
default:
return ""
}
}
108 changes: 108 additions & 0 deletions ee/tables/execparsers/socketfilterfw/parser_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package socketfilterfw

import (
"bytes"
_ "embed"
"testing"

"github.com/stretchr/testify/require"
)

//go:embed test-data/apps.txt
var apps []byte

//go:embed test-data/data.txt
var data []byte

//go:embed test-data/empty.txt
var empty []byte

//go:embed test-data/malformed.txt
var malformed []byte

func TestParse(t *testing.T) {
t.Parallel()

var tests = []struct {
name string
input []byte
expected []map[string]string
}{
{
name: "apps",
input: apps,
expected: []map[string]string{
{
"name": "replicatord",
"allow_incoming_connections": "1",
},
{
"name": "Pop Helper.app",
"allow_incoming_connections": "0",
},
{
"name": "Google Chrome",
"allow_incoming_connections": "0",
},
{
"name": "rtadvd",
"allow_incoming_connections": "1",
},
{
"name": "com.docker.backend",
"allow_incoming_connections": "1",
},
{
"name": "sshd-keygen-wrapper",
"allow_incoming_connections": "1",
},
},
},
{
name: "data",
input: data,
expected: []map[string]string{
{
"global_state_enabled": "1",
"block_all_enabled": "0",
"allow_built-in_signed_enabled": "1",
"allow_downloaded_signed_enabled": "1",
"stealth_enabled": "0",
"logging_enabled": "1",
"logging_option": "throttled",
},
},
},
{
name: "empty input",
input: empty,
},
{
name: "malformed",
input: malformed,
expected: []map[string]string{
{
"global_state_enabled": "0",
"block_all_enabled": "1",
"allow_built-in_signed_enabled": "0",
"allow_downloaded_signed_enabled": "",
"stealth_enabled": "0",
"logging_enabled": "",
"logging_option": "throttled",
},
},
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

p := New()
result, err := p.Parse(bytes.NewReader(tt.input))
require.NoError(t, err, "unexpected error parsing input")
require.ElementsMatch(t, tt.expected, result)
})
}
}
17 changes: 17 additions & 0 deletions ee/tables/execparsers/socketfilterfw/socketfilterfw.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package socketfilterfw

import (
"io"
)

type parser struct{}

var Parser = New()

func New() parser {
return parser{}
}

func (p parser) Parse(reader io.Reader) (any, error) {
return socketfilterfwParse(reader)
}
7 changes: 7 additions & 0 deletions ee/tables/execparsers/socketfilterfw/test-data/apps.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Total number of apps = 6
replicatord (state: 1)
Pop Helper.app (state: 4)
Google Chrome (state: 4)
rtadvd (state: 1)
com.docker.backend (state: 1)
sshd-keygen-wrapper (state: 1)
7 changes: 7 additions & 0 deletions ee/tables/execparsers/socketfilterfw/test-data/data.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Firewall is enabled. (State = 1)
Firewall has block all state set to disabled.
Automatically allow built-in signed software ENABLED.
Automatically allow downloaded signed software ENABLED.
Firewall stealth mode is off
Log mode is on
Log Option is throttled
Empty file.
9 changes: 9 additions & 0 deletions ee/tables/execparsers/socketfilterfw/test-data/malformed.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Firewall is enabled. (State %#)*Q&^= 0)
Firewall is blocking all non-essential incoming connections.x^CFS.
%#UO
Automatically allow built-in signed software DISABLED.

Automatically allow downloaded signed software DISABLEDENABLED.
Firewall stealth mode is off
Log mode is onr\r\n\r\n
Log Option is throttled
3 changes: 3 additions & 0 deletions pkg/osquery/table/platform_tables_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/kolide/launcher/ee/tables/dataflattentable"
"github.com/kolide/launcher/ee/tables/execparsers/remotectl"
"github.com/kolide/launcher/ee/tables/execparsers/repcli"
"github.com/kolide/launcher/ee/tables/execparsers/socketfilterfw"
"github.com/kolide/launcher/ee/tables/execparsers/softwareupdate"
"github.com/kolide/launcher/ee/tables/filevault"
"github.com/kolide/launcher/ee/tables/firmwarepasswd"
Expand Down Expand Up @@ -123,6 +124,8 @@ func platformSpecificTables(slogger *slog.Logger, currentOsquerydBinaryPath stri
munki.MunkiReport(),
dataflattentable.TablePluginExec(slogger, "kolide_nix_upgradeable", dataflattentable.XmlType, allowedcmd.NixEnv, []string{"--query", "--installed", "-c", "--xml"}),
dataflattentable.NewExecAndParseTable(slogger, "kolide_remotectl", remotectl.Parser, allowedcmd.Remotectl, []string{`dumpstate`}),
dataflattentable.NewExecAndParseTable(slogger, "kolide_socketfilterfw", socketfilterfw.Parser, allowedcmd.Socketfilterfw, []string{"--getglobalstate", "--getblockall", "--getallowsigned", "--getstealthmode", "--getloggingmode", "--getloggingopt"}, dataflattentable.WithIncludeStderr()),
dataflattentable.NewExecAndParseTable(slogger, "kolide_socketfilterfw_apps", socketfilterfw.Parser, allowedcmd.Socketfilterfw, []string{"--listapps"}, dataflattentable.WithIncludeStderr()),
dataflattentable.NewExecAndParseTable(slogger, "kolide_softwareupdate", softwareupdate.Parser, allowedcmd.Softwareupdate, []string{`--list`, `--no-scan`}, dataflattentable.WithIncludeStderr()),
dataflattentable.NewExecAndParseTable(slogger, "kolide_softwareupdate_scan", softwareupdate.Parser, allowedcmd.Softwareupdate, []string{`--list`}, dataflattentable.WithIncludeStderr()),
dataflattentable.NewExecAndParseTable(slogger, "kolide_carbonblack_repcli_status", repcli.Parser, allowedcmd.Repcli, []string{"status"}, dataflattentable.WithIncludeStderr()),
Expand Down
Loading