Skip to content

Commit

Permalink
Add firmwarepassword table (#620)
Browse files Browse the repository at this point in the history
Add a table based on exec'ing `/usr/sbin/firmwarepasswd`. This uses a simple exec pattern, and starts to create a model for that in the future.
  • Loading branch information
directionless authored Jun 30, 2020
1 parent 0936d6f commit 2e4c979
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 0 deletions.
2 changes: 2 additions & 0 deletions pkg/osquery/table/platform_tables_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/knightsc/system_policy/osquery/table/kextpolicy"
"github.com/knightsc/system_policy/osquery/table/legacyexec"
"github.com/kolide/launcher/pkg/osquery/tables/dataflattentable"
"github.com/kolide/launcher/pkg/osquery/tables/firmwarepasswd"
"github.com/kolide/launcher/pkg/osquery/tables/munki"
"github.com/kolide/launcher/pkg/osquery/tables/screenlock"
"github.com/kolide/launcher/pkg/osquery/tables/systemprofiler"
Expand All @@ -22,6 +23,7 @@ func platformTables(client *osquery.ExtensionManagerClient, logger log.Logger, c
Airdrop(client),
AppIcons(),
ChromeLoginKeychainInfo(client, logger),
firmwarepasswd.TablePlugin(client, logger),
GDriveSyncConfig(client, logger),
GDriveSyncHistoryInfo(client, logger),
KolideVulnerabilities(client, logger),
Expand Down
171 changes: 171 additions & 0 deletions pkg/osquery/tables/firmwarepasswd/firmwarepasswd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// firmwarepasswd is a simple wrapper around the
// `/usr/sbin/firmwarepasswd` tool. This should be considered beta at
// best. It serves a bit as a pattern for future exec work.

package firmwarepasswd

import (
"bytes"
"context"
"io/ioutil"
"os"
"os/exec"
"strings"
"time"

"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/kolide/osquery-go"
"github.com/kolide/osquery-go/plugin/table"
"github.com/pkg/errors"
)

type Table struct {
client *osquery.ExtensionManagerClient
logger log.Logger
parser *OutputParser
}

func TablePlugin(client *osquery.ExtensionManagerClient, logger log.Logger) *table.Plugin {
columns := []table.ColumnDefinition{
table.IntegerColumn("option_roms_allowed"),
table.IntegerColumn("password_enabled"),
table.TextColumn("mode"),
}

t := New(client, logger)

return table.NewPlugin("kolide_firmwarepasswd", columns, t.generate)

}

func New(client *osquery.ExtensionManagerClient, logger log.Logger) *Table {
parser := NewParser(logger,
[]Matcher{
Matcher{
Match: func(in string) bool { return strings.HasPrefix(in, "Password Enabled: ") },
KeyFunc: func(_ string) (string, error) { return "password_enabled", nil },
ValFunc: func(in string) (string, error) { return passwordValue(in) },
},
Matcher{
Match: func(in string) bool { return strings.HasPrefix(in, "Mode: ") },
KeyFunc: func(_ string) (string, error) { return "mode", nil },
ValFunc: func(in string) (string, error) { return modeValue(in) },
},
Matcher{
Match: func(in string) bool { return strings.HasPrefix(in, "Option roms ") },
KeyFunc: func(_ string) (string, error) { return "option_roms_allowed", nil },
ValFunc: func(in string) (string, error) { return optionRomValue(in) },
},
})

return &Table{
client: client,
logger: level.NewFilter(logger, level.AllowInfo()),
parser: parser,
}

}

func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
result := make(map[string]string)

for _, mode := range []string{"-check", "-mode"} {
output := new(bytes.Buffer)
if err := t.runFirmwarepasswd(ctx, mode, output); err != nil {
level.Info(t.logger).Log(
"msg", "Error running firmware password",
"command", mode,
"err", err,
)
continue
}

// Merge resulting matches
for _, row := range t.parser.Parse(output) {
for k, v := range row {
result[k] = v
}
}
}
return []map[string]string{result}, nil
}

func (t *Table) runFirmwarepasswd(ctx context.Context, subcommand string, output *bytes.Buffer) error {
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()

cmd := exec.CommandContext(ctx, "/usr/sbin/firmwarepasswd", subcommand)

dir, err := ioutil.TempDir("", "osq-firmwarepasswd")
if err != nil {
return errors.Wrap(err, "mktemp")
}
defer os.RemoveAll(dir)

if err := os.Chmod(dir, 0755); err != nil {
return errors.Wrap(err, "chmod")
}

cmd.Dir = dir

stderr := new(bytes.Buffer)
cmd.Stderr = stderr

cmd.Stdout = output

if err := cmd.Run(); err != nil {
level.Info(t.logger).Log(
"msg", "Error running firmwarepasswd",
"stderr", strings.TrimSpace(stderr.String()),
"stdout", strings.TrimSpace(output.String()),
"err", err,
)
return errors.Wrap(err, "running osquery")
}
return nil
}

func modeValue(in string) (string, error) {
components := strings.SplitN(in, ":", 2)
if len(components) < 2 {
return "", errors.Errorf("Can't tell mode from %s", in)
}

return strings.TrimSpace(strings.ToLower(components[1])), nil
}

func passwordValue(in string) (string, error) {
components := strings.SplitN(in, ":", 2)
if len(components) < 2 {
return "", errors.Errorf("Can't tell value from %s", in)
}

t, err := discernValBool(components[1])

if t {
return "1", err
}
return "0", err
}

func optionRomValue(in string) (string, error) {
switch strings.TrimPrefix(in, "Option roms ") {
case "not allowed":
return "0", nil
case "allowed":
return "1", nil
}
return "", errors.Errorf("Can't tell value from %s", in)
}

func discernValBool(in string) (bool, error) {
switch strings.TrimSpace(strings.ToLower(in)) {
case "true", "t", "1", "y", "yes":
return true, nil
case "false", "f", "0", "n", "no":
return false, nil
}

return false, errors.Errorf("Can't discern boolean from string <%s>", in)
}
69 changes: 69 additions & 0 deletions pkg/osquery/tables/firmwarepasswd/firmwarepasswd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package firmwarepasswd

import (
"bytes"
"io/ioutil"
"path/filepath"
"testing"

"github.com/go-kit/kit/log"
"github.com/stretchr/testify/require"
)

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

var tests = []struct {
input string
expected map[string]string
}{
{
input: "check-no.txt",
expected: map[string]string{"password_enabled": "0"},
},
{
input: "check-garbage.txt",
expected: map[string]string{"password_enabled": "0"},
},
{
input: "check-yes.txt",
expected: map[string]string{"password_enabled": "1"},
},
{
input: "mode-command.txt",
expected: map[string]string{
"mode": "command",
"option_roms_allowed": "0",
},
},
{
input: "mode-none.txt",
expected: map[string]string{
"mode": "none",
"option_roms_allowed": "1",
},
},
}

for _, tt := range tests {
parser := New(nil, log.NewNopLogger()).parser

t.Run(tt.input, func(t *testing.T) {
inputBytes, err := ioutil.ReadFile(filepath.Join("testdata", tt.input))
require.NoError(t, err, "read file %s", tt.input)

inputBuffer := bytes.NewBuffer(inputBytes)

result := make(map[string]string)
for _, row := range parser.Parse(inputBuffer) {
for k, v := range row {
result[k] = v
}
}

require.EqualValues(t, tt.expected, result)

})
}

}
82 changes: 82 additions & 0 deletions pkg/osquery/tables/firmwarepasswd/parser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package firmwarepasswd

import (
"bufio"
"bytes"

"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
)

type Matcher struct {
Match func(string) bool
KeyFunc func(string) (string, error)
ValFunc func(string) (string, error)
}

type OutputParser struct {
matchers []Matcher
logger log.Logger
}

func NewParser(logger log.Logger, matchers []Matcher) *OutputParser {
p := &OutputParser{
matchers: matchers,
logger: logger,
}
return p
}

// Parse looks at command output, line by line. It uses the defined Matchers to set any appropriate values
func (p *OutputParser) Parse(input *bytes.Buffer) []map[string]string {
var results []map[string]string

scanner := bufio.NewScanner(input)
for scanner.Scan() {
line := scanner.Text()
if line == "" {
continue
}

row := make(map[string]string)

// check each possible key match
for _, m := range p.matchers {
if m.Match(line) {
key, err := m.KeyFunc(line)
if err != nil {
level.Debug(p.logger).Log(
"msg", "key match failed",
"line", line,
"err", err,
)
continue
}

val, err := m.ValFunc(line)
if err != nil {
level.Debug(p.logger).Log(
"msg", "value match failed",
"line", line,
"err", err,
)
continue
}

row[key] = val
continue
}
}

if len(row) == 0 {
level.Debug(p.logger).Log("msg", "No matched keys", "line", line)
continue
}
results = append(results, row)

}
if err := scanner.Err(); err != nil {
level.Debug(p.logger).Log("msg", "scanner error", "err", err)
}
return results
}
3 changes: 3 additions & 0 deletions pkg/osquery/tables/firmwarepasswd/testdata/check-garbage.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Hello: world
Password Enabled: No
Unknown: xxx
1 change: 1 addition & 0 deletions pkg/osquery/tables/firmwarepasswd/testdata/check-no.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Password Enabled: No
1 change: 1 addition & 0 deletions pkg/osquery/tables/firmwarepasswd/testdata/check-yes.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Password Enabled: Yes
2 changes: 2 additions & 0 deletions pkg/osquery/tables/firmwarepasswd/testdata/mode-command.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Mode: command
Option roms not allowed
3 changes: 3 additions & 0 deletions pkg/osquery/tables/firmwarepasswd/testdata/mode-none.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Mode: none
Option roms allowed

0 comments on commit 2e4c979

Please sign in to comment.