-
Notifications
You must be signed in to change notification settings - Fork 98
/
profiles.go
164 lines (131 loc) · 4.95 KB
/
profiles.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
//+build darwin
// Package profiles provides a table wrapper around the various
// profiles options.
//
// As the returned data is a complex nested plist, this uses the
// dataflatten tooling. (See
// https://godoc.org/github.com/kolide/launcher/pkg/dataflatten)
package profiles
import (
"bytes"
"context"
"os/exec"
"strings"
"time"
"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 profilesPath = "/usr/bin/profiles"
const userAllowedCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
type Table struct {
client *osquery.ExtensionManagerClient
logger log.Logger
tableName string
}
func TablePlugin(client *osquery.ExtensionManagerClient, logger log.Logger) *table.Plugin {
// profiles options. See `man profiles`. These may not be needed,
// we use `show -all` as the default, and it probably covers
// everything.
columns := dataflattentable.Columns(
table.TextColumn("user"),
table.TextColumn("command"),
table.TextColumn("type"),
)
t := &Table{
client: client,
logger: logger,
tableName: "kolide_profiles",
}
return table.NewPlugin(t.tableName, columns, t.generate)
}
func (t *Table) generate(ctx context.Context, queryContext table.QueryContext) ([]map[string]string, error) {
var results []map[string]string
for _, command := range tablehelpers.GetConstraints(queryContext, "command", tablehelpers.WithAllowedCharacters("abcdefghijklmnopqrstuvwxyz"), tablehelpers.WithDefaults("show")) {
for _, profileType := range tablehelpers.GetConstraints(queryContext, "type", tablehelpers.WithAllowedCharacters("abcdefghijklmnopqrstuvwxyz"), tablehelpers.WithDefaults("")) {
for _, user := range tablehelpers.GetConstraints(queryContext, "user", tablehelpers.WithAllowedCharacters(userAllowedCharacters), tablehelpers.WithDefaults("_all")) {
for _, dataQuery := range tablehelpers.GetConstraints(queryContext, "query", tablehelpers.WithDefaults("*")) {
profileArgs := []string{command, "-output", "stdout-xml"}
if profileType != "" {
profileArgs = append(profileArgs, "-type", profileType)
}
// setup the command line. This table overloads the `user`
// column so one can select either:
// * All profiles merged, using the special value `_all` (this is the default)
// * The device profiles, using the special value `_device`
// * a user specific one, using the username
switch {
case user == "" || user == "_all":
profileArgs = append(profileArgs, "-all")
case user == "_device":
break
case user != "":
profileArgs = append(profileArgs, "-user", user)
default:
return nil, errors.Errorf("Unknown user argument: %s", user)
}
profilesOutput, err := t.execProfiles(ctx, profileArgs)
if err != nil {
level.Info(t.logger).Log("msg", "exec failed", "err", err)
continue
}
flattenOpts := []dataflatten.FlattenOpts{
dataflatten.WithQuery(strings.Split(dataQuery, "/")),
}
if t.logger != nil {
flattenOpts = append(flattenOpts,
dataflatten.WithLogger(level.NewFilter(t.logger, level.AllowInfo())),
)
}
flatData, err := dataflatten.Plist(profilesOutput, flattenOpts...)
if err != nil {
level.Info(t.logger).Log("msg", "flatten failed", "err", err)
continue
}
rowData := map[string]string{
"command": command,
"type": profileType,
"user": user,
}
results = append(results, dataflattentable.ToMap(flatData, dataQuery, rowData)...)
}
}
}
}
return results, nil
}
func (t *Table) flattenOutput(dataQuery string, systemOutput []byte) ([]dataflatten.Row, error) {
flattenOpts := []dataflatten.FlattenOpts{}
if dataQuery != "" {
flattenOpts = append(flattenOpts, dataflatten.WithQuery(strings.Split(dataQuery, "/")))
}
if t.logger != nil {
flattenOpts = append(flattenOpts,
dataflatten.WithLogger(level.NewFilter(t.logger, level.AllowInfo())),
)
}
return dataflatten.Plist(systemOutput, flattenOpts...)
}
func (t *Table) execProfiles(ctx context.Context, args []string) ([]byte, error) {
var stdout bytes.Buffer
var stderr bytes.Buffer
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, profilesPath, args...)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
level.Debug(t.logger).Log("msg", "calling profiles", "args", cmd.Args)
if err := cmd.Run(); err != nil {
return nil, errors.Wrapf(err, "calling profiles. Got: %s", string(stderr.Bytes()))
}
// Check for an error about root permissions
if bytes.Contains(stdout.Bytes(), []byte("requires root privileges")) {
return nil, errors.New("Requires root privileges")
}
return stdout.Bytes(), nil
}