Skip to content

Commit

Permalink
Add cryptsetup_status table (#734)
Browse files Browse the repository at this point in the history
Simple `kolide_cryptsetup_status` as a wrapper over the `cryptsetup status` command
  • Loading branch information
directionless committed Jun 10, 2021
1 parent d4b2df8 commit 2b6f90e
Show file tree
Hide file tree
Showing 12 changed files with 366 additions and 0 deletions.
2 changes: 2 additions & 0 deletions pkg/osquery/table/platform_tables_linux.go
Expand Up @@ -4,6 +4,7 @@ package table

import (
"github.com/go-kit/kit/log"
"github.com/kolide/launcher/pkg/osquery/tables/cryptsetup"
"github.com/kolide/launcher/pkg/osquery/tables/dataflattentable"
"github.com/kolide/launcher/pkg/osquery/tables/gsettings"
osquery "github.com/kolide/osquery-go"
Expand All @@ -12,6 +13,7 @@ import (

func platformTables(client *osquery.ExtensionManagerClient, logger log.Logger, currentOsquerydBinaryPath string) []*table.Plugin {
return []*table.Plugin{
cryptsetup.TablePlugin(client, logger),
gsettings.Settings(client, logger),
gsettings.Metadata(client, logger),
dataflattentable.TablePluginExec(client, logger,
Expand Down
92 changes: 92 additions & 0 deletions pkg/osquery/tables/cryptsetup/parser.go
@@ -0,0 +1,92 @@
package cryptsetup

import (
"bufio"
"bytes"
"regexp"
"strconv"
"strings"

"github.com/davecgh/go-spew/spew"
"github.com/pkg/errors"
)

// parseStatus parses the output from `cryptsetup status`. This is a
// pretty simple key, value format, but does have a free form first
// line. It's not clear if this is going to be stable, or change
// across versions.
func parseStatus(rawdata []byte) (map[string]interface{}, error) {
var data map[string]interface{}

if len(rawdata) == 0 {
return nil, errors.New("No data")
}

scanner := bufio.NewScanner(bytes.NewReader(rawdata))
firstLine := true
for scanner.Scan() {
line := scanner.Text()
if firstLine {
var err error
data, err = parseFirstLine(line)
if err != nil {
return nil, err
}

firstLine = false
continue
}

kv := strings.SplitN(line, ": ", 2)

// blank lines, or other unexpected input can just be skipped.
if len(kv) < 2 {
continue
}

data[strings.ReplaceAll(strings.TrimSpace(kv[0]), " ", "_")] = strings.TrimSpace(kv[1])
}

return data, nil
}

// regexp for the first line of the status output.
var firstLineRegexp = regexp.MustCompile(`^(?:Device (.*) (not found))|(?:(.*?) is ([a-z]+)(?:\.| and is (in use)))`)

// parseFirstLine parses the first line of the status output. This
// appears to be a free form string indicating several pieces of
// information. It is parsed with a single regexp. (See tests for
// examples)
func parseFirstLine(line string) (map[string]interface{}, error) {
if line == "" {
return nil, errors.Errorf("Invalid first line")
}

m := firstLineRegexp.FindAllStringSubmatch(line, -1)
if len(m) != 1 {
return nil, errors.Errorf("Failed to match first line: %s", line)
}
if len(m[0]) != 6 {
spew.Dump(m)
return nil, errors.Errorf("Got %d matches. Expected 6. Failed to match first line: %s", len(m[0]), line)
}

data := make(map[string]interface{}, 3)

// check for $1 and $2 for the error condition
if m[0][1] != "" && m[0][2] != "" {
data["short_name"] = m[0][1]
data["status"] = strings.ReplaceAll(m[0][2], " ", "_")
data["mounted"] = strconv.FormatBool(false)
return data, nil
}

if m[0][3] != "" && m[0][4] != "" {
data["display_name"] = m[0][3]
data["status"] = strings.ReplaceAll(m[0][4], " ", "_")
data["mounted"] = strconv.FormatBool(m[0][5] != "")
return data, nil
}

return nil, errors.Errorf("Unknown first line: %s", line)
}
129 changes: 129 additions & 0 deletions pkg/osquery/tables/cryptsetup/parser_test.go
@@ -0,0 +1,129 @@
package cryptsetup

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

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

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

var tests = []struct {
input string
}{
{
input: "",
},
{
input: "\n\n\n\n",
},
{
input: "type: LUKS2",
},
{
input: "Hello world",
},
}

for _, tt := range tests {
t.Run("", func(t *testing.T) {
data, err := parseStatus([]byte(tt.input))
assert.Error(t, err, "parseStatus")
assert.Nil(t, data, "data is nil")

})
}

}
func TestParseStatus(t *testing.T) {
t.Parallel()

var tests = []struct {
infile string
len int
status string
mounted bool
ctype string
keysize string
key_location string
}{
{
infile: "status-active-luks1.txt",
status: "active",
mounted: true,
ctype: "LUKS1",
keysize: "512 bits",
key_location: "dm-crypt",
},
{
infile: "status-active-luks2.txt",
status: "active",
mounted: true,
ctype: "LUKS2",
keysize: "512 bits",
key_location: "keyring",
},
{
infile: "status-active-mounted.txt",
status: "active",
mounted: true,
ctype: "PLAIN",
keysize: "256 bits",
key_location: "dm-crypt",
},
{
infile: "status-active-umounted.txt",
status: "active",
ctype: "PLAIN",
keysize: "256 bits",
key_location: "dm-crypt",
},
{
infile: "status-active.txt",
status: "active",
mounted: true,
ctype: "PLAIN",
keysize: "256 bits",
key_location: "dm-crypt",
},
{
infile: "status-error.txt",
status: "not_found",
},
{
infile: "status-inactive.txt",
status: "inactive",
},
{
infile: "status-unactive.txt",
status: "inactive",
},
}

for _, tt := range tests {
t.Run(tt.infile, func(t *testing.T) {
input, err := ioutil.ReadFile(filepath.Join("testdata", tt.infile))
require.NoError(t, err, "read input file")

data, err := parseStatus(input)
require.NoError(t, err, "parseStatus")

assert.Equal(t, tt.status, data["status"], "status")
assert.Equal(t, strconv.FormatBool(tt.mounted), data["mounted"], "mounted")

// These values aren't populated in the map,
// so only check them if the test case lists
// them
if tt.ctype != "" {
assert.Equal(t, tt.ctype, data["type"], "type")
assert.Equal(t, tt.keysize, data["keysize"], "keysize")
assert.Equal(t, tt.key_location, data["key_location"], "key_location")
}
})
}
}
89 changes: 89 additions & 0 deletions pkg/osquery/tables/cryptsetup/table.go
@@ -0,0 +1,89 @@
package cryptsetup

import (
"context"
"strings"

"github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/kolide/launcher/pkg/dataflatten"
"github.com/kolide/launcher/pkg/osquery/tables/dataflattentable"
"github.com/kolide/launcher/pkg/osquery/tables/tablehelpers"
"github.com/kolide/osquery-go"
"github.com/kolide/osquery-go/plugin/table"
"github.com/pkg/errors"
)

const cryptsetupPath = "/usr/sbin/cryptsetup"

const allowedNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-/_"

type Table struct {
client *osquery.ExtensionManagerClient
logger log.Logger
name string
}

func TablePlugin(client *osquery.ExtensionManagerClient, logger log.Logger) *table.Plugin {
columns := dataflattentable.Columns(
table.TextColumn("name"),
)

t := &Table{
client: client,
logger: logger,
name: "kolide_cryptsetup_status",
}

return table.NewPlugin(t.name, columns, t.generate)
}

func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
var results []map[string]string

requestedNames := tablehelpers.GetConstraints(queryContext, "name",
tablehelpers.WithAllowedCharacters(allowedNameCharacters),
tablehelpers.WithLogger(t.logger),
)

if len(requestedNames) == 0 {
return results, errors.Errorf("The %s table requires that you specify a constraint for name", t.name)
}

for _, name := range requestedNames {
output, err := tablehelpers.Exec(ctx, t.logger, 15, []string{cryptsetupPath}, []string{"--readonly", "status", name})
if err != nil {
level.Debug(t.logger).Log("msg", "Error execing for status", "name", name, "err", err)
continue
}

status, err := parseStatus(output)
if err != nil {
level.Info(t.logger).Log("msg", "Error parsing status", "name", name, "err", err)
continue
}

for _, dataQuery := range tablehelpers.GetConstraints(queryContext, "query", tablehelpers.WithDefaults("*")) {
flatData, err := t.flattenOutput(dataQuery, status)
if err != nil {
level.Info(t.logger).Log("msg", "flatten failed", "err", err)
continue
}

rowData := map[string]string{"name": name}

results = append(results, dataflattentable.ToMap(flatData, dataQuery, rowData)...)
}
}

return results, nil
}

func (t *Table) flattenOutput(dataQuery string, status map[string]interface{}) ([]dataflatten.Row, error) {
flattenOpts := []dataflatten.FlattenOpts{
dataflatten.WithLogger(t.logger),
dataflatten.WithQuery(strings.Split(dataQuery, "/")),
}

return dataflatten.Flatten(status, flattenOpts...)
}
11 changes: 11 additions & 0 deletions pkg/osquery/tables/cryptsetup/testdata/status-active-luks1.txt
@@ -0,0 +1,11 @@
/dev/mapper/dm-crypto-luks1 is active and is in use.
type: LUKS1
cipher: aes-xts-plain64
keysize: 512 bits
key location: dm-crypt
device: /dev/sdc1
sector size: 512
offset: 4096 sectors
size: 8382464 sectors
mode: read/write

10 changes: 10 additions & 0 deletions pkg/osquery/tables/cryptsetup/testdata/status-active-luks2.txt
@@ -0,0 +1,10 @@
/dev/mapper/dm-crypto-luks2 is active and is in use.
type: LUKS2
cipher: aes-xts-plain64
keysize: 512 bits
key location: keyring
device: /dev/sdc2
sector size: 512
offset: 32768 sectors
size: 8355840 sectors
mode: read/write
10 changes: 10 additions & 0 deletions pkg/osquery/tables/cryptsetup/testdata/status-active-mounted.txt
@@ -0,0 +1,10 @@
/dev/mapper/dm-crypto-plain is active and is in use.
type: PLAIN
cipher: aes-cbc-essiv:sha256
keysize: 256 bits
key location: dm-crypt
device: /dev/sdc3
sector size: 512
offset: 0 sectors
size: 8388608 sectors
mode: read/write
10 changes: 10 additions & 0 deletions pkg/osquery/tables/cryptsetup/testdata/status-active-umounted.txt
@@ -0,0 +1,10 @@
/dev/mapper/dm-crypto-plain is active.
type: PLAIN
cipher: aes-cbc-essiv:sha256
keysize: 256 bits
key location: dm-crypt
device: /dev/sdc3
sector size: 512
offset: 0 sectors
size: 8388608 sectors
mode: read/write
10 changes: 10 additions & 0 deletions pkg/osquery/tables/cryptsetup/testdata/status-active.txt
@@ -0,0 +1,10 @@
/dev/mapper/dm-crypto-plain is active and is in use.
type: PLAIN
cipher: aes-cbc-essiv:sha256
keysize: 256 bits
key location: dm-crypt
device: /dev/sdc3
sector size: 512
offset: 0 sectors
size: 8388608 sectors
mode: read/write
1 change: 1 addition & 0 deletions pkg/osquery/tables/cryptsetup/testdata/status-error.txt
@@ -0,0 +1 @@
Device sdc3 not found
1 change: 1 addition & 0 deletions pkg/osquery/tables/cryptsetup/testdata/status-inactive.txt
@@ -0,0 +1 @@
/dev/mapper/dm-crypto-plain is inactive.
1 change: 1 addition & 0 deletions pkg/osquery/tables/cryptsetup/testdata/status-unactive.txt
@@ -0,0 +1 @@
/dev/mapper/dm-crypto-unknown is inactive.

0 comments on commit 2b6f90e

Please sign in to comment.