Skip to content

Commit

Permalink
Add socketfilterfw parser table (#1812)
Browse files Browse the repository at this point in the history
  • Loading branch information
Micah-Kolide authored Aug 1, 2024
1 parent 8980c6c commit 551d945
Show file tree
Hide file tree
Showing 9 changed files with 280 additions and 0 deletions.
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.
func socketfilterfwParse(reader io.Reader) (any, error) {
results := make([]map[string]string, 0)
row := make(map[string]string)
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
}

k, v := parseLine(line)
if k != "" {
row[k] = v
}
}

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

return results, nil
}

// 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

0 comments on commit 551d945

Please sign in to comment.