Skip to content

Commit 40cbc11

Browse files
authored
⭐ auditd.rules resource (#5546)
* ⭐ auditd.rules resource Signed-off-by: Dominik Richter <dominik.richter@gmail.com> * ✨ parse rule fields Signed-off-by: Dominik Richter <dominik.richter@gmail.com> * 🟢 add missing test data Signed-off-by: Dominik Richter <dominik.richter@gmail.com> * 🟢 try to fix spelling Signed-off-by: Dominik Richter <dominik.richter@gmail.com> --------- Signed-off-by: Dominik Richter <dominik.richter@gmail.com>
1 parent a14fc81 commit 40cbc11

8 files changed

Lines changed: 1270 additions & 98 deletions

File tree

providers-sdk/v1/testutils/testdata/arch.json

Lines changed: 317 additions & 53 deletions
Large diffs are not rendered by default.

providers/os/resources/auditd.go

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,17 @@ package resources
77
import (
88
"errors"
99
"fmt"
10+
"regexp"
1011
"slices"
1112
"strings"
1213
"sync"
14+
"unicode"
1315

16+
"go.mondoo.com/cnquery/v11/checksums"
1417
"go.mondoo.com/cnquery/v11/llx"
1518
"go.mondoo.com/cnquery/v11/providers-sdk/v1/plugin"
1619
"go.mondoo.com/cnquery/v11/providers/os/resources/parsers"
20+
"go.mondoo.com/cnquery/v11/types"
1721
"go.mondoo.com/cnquery/v11/utils/multierr"
1822
)
1923

@@ -135,3 +139,280 @@ var auditdDowncaseKeywords = []string{
135139
"enable_krb5",
136140
"overflow_action",
137141
}
142+
143+
type mqlAuditdRulesInternal struct {
144+
lock sync.Mutex
145+
loaded bool
146+
loadError error
147+
}
148+
149+
const defaultAuditdRules = "/etc/audit/rules.d"
150+
151+
func (s *mqlAuditdRules) id() (string, error) {
152+
return s.Path.Data, nil
153+
}
154+
155+
func (s *mqlAuditdRules) path() (string, error) {
156+
return defaultAuditdRules, nil
157+
}
158+
159+
func (s *mqlAuditdRules) load(path string) error {
160+
s.lock.Lock()
161+
defer s.lock.Unlock()
162+
if s.loaded {
163+
return s.loadError
164+
}
165+
166+
if path == "" {
167+
return errors.New("the path must be non-empty to parse auditd rules")
168+
}
169+
170+
files, err := getSortedPathFiles(s.MqlRuntime, path)
171+
if err != nil {
172+
s.Controls = plugin.TValue[[]any]{State: plugin.StateIsSet, Error: err}
173+
s.Files = plugin.TValue[[]any]{State: plugin.StateIsSet, Error: err}
174+
s.Syscalls = plugin.TValue[[]any]{State: plugin.StateIsSet, Error: err}
175+
return err
176+
}
177+
178+
var errors multierr.Errors
179+
for i := range files {
180+
file := files[i].(*mqlFile)
181+
182+
bn := file.GetBasename()
183+
if !strings.HasSuffix(bn.Data, ".rules") {
184+
continue
185+
}
186+
187+
content := file.GetContent()
188+
if content.Error != nil {
189+
return content.Error
190+
}
191+
192+
s.parse(content.Data, &errors)
193+
}
194+
195+
s.loadError = errors.Deduplicate()
196+
s.loaded = true
197+
return s.loadError
198+
}
199+
200+
func parseKeyVal(line string) (string, string, int) {
201+
runes := []rune(line)
202+
i := 0
203+
204+
// invalid prefix
205+
if line[i] != '-' {
206+
for ; i < len(runes); i++ {
207+
if unicode.IsSpace(runes[i]) {
208+
break
209+
}
210+
}
211+
for ; i < len(runes); i++ {
212+
if !unicode.IsSpace(runes[i]) {
213+
break
214+
}
215+
}
216+
return "", "", i
217+
}
218+
219+
if len(line) < 2 {
220+
return "", "", len(line)
221+
}
222+
if line[1] == '-' {
223+
i = 2
224+
} else {
225+
i = 1
226+
}
227+
228+
for ; i < len(runes); i++ {
229+
if unicode.IsSpace(runes[i]) {
230+
break
231+
}
232+
}
233+
if i == len(runes) {
234+
return line, "", i
235+
}
236+
keyend := i
237+
238+
for ; i < len(runes); i++ {
239+
if !unicode.IsSpace(runes[i]) {
240+
break
241+
}
242+
}
243+
valstart := i
244+
for ; i < len(runes); i++ {
245+
if unicode.IsSpace(runes[i]) {
246+
break
247+
}
248+
}
249+
valend := i
250+
251+
for ; i < len(runes); i++ {
252+
if !unicode.IsSpace(runes[i]) {
253+
break
254+
}
255+
}
256+
257+
return line[:keyend], line[valstart:valend], i
258+
}
259+
260+
var reOperator = regexp.MustCompile(`(=|!=|<|<=|>|>=)`)
261+
262+
func (s *mqlAuditdRules) parse(content string, errors *multierr.Errors) {
263+
s.Syscalls.State = plugin.StateIsSet
264+
s.Files.State = plugin.StateIsSet
265+
s.Controls.State = plugin.StateIsSet
266+
267+
lines := strings.Split(content, "\n")
268+
for _, rawline := range lines {
269+
line := strings.TrimSpace(rawline)
270+
if line == "" || line[0] == '#' {
271+
continue
272+
}
273+
274+
resourceName := "auditd.rule.control"
275+
args := map[string]*llx.RawData{}
276+
rawFields := []string{}
277+
syscalls := []any{}
278+
other := [][2]string{}
279+
280+
for line != "" {
281+
k, v, idx := parseKeyVal(line)
282+
line = line[idx:]
283+
284+
switch k {
285+
case "-a":
286+
resourceName = "auditd.rule.syscall"
287+
arr := strings.SplitN(v, ",", 2)
288+
args["action"] = llx.StringData(arr[0])
289+
args["list"] = llx.StringData(arr[1])
290+
291+
case "-F":
292+
rawFields = append(rawFields, v)
293+
294+
case "-w":
295+
resourceName = "auditd.rule.file"
296+
args["path"] = llx.StringData(v)
297+
298+
case "-k":
299+
args["keyname"] = llx.StringData(v)
300+
301+
case "-p":
302+
args["permissions"] = llx.StringData(v)
303+
304+
case "-S":
305+
syscalls = append(syscalls, v)
306+
307+
default:
308+
other = append(other, [2]string{k, v})
309+
}
310+
}
311+
312+
switch resourceName {
313+
case "auditd.rule.file":
314+
if _, ok := args["keyname"]; !ok {
315+
args["keyname"] = llx.StringData("")
316+
}
317+
318+
r, err := CreateResource(s.MqlRuntime, resourceName, args)
319+
if err != nil {
320+
errors.Add(err)
321+
continue
322+
}
323+
s.Files.Data = append(s.Files.Data, r)
324+
325+
case "auditd.rule.syscall":
326+
args["syscalls"] = llx.ArrayData(syscalls, types.String)
327+
328+
fields := make([]any, len(rawFields))
329+
for i, raw := range rawFields {
330+
op := reOperator.FindString(raw)
331+
if op == "" {
332+
fields[i] = map[string]any{"key": raw}
333+
continue
334+
}
335+
// it must exist according to the preceding statement
336+
idx := strings.Index(raw, op)
337+
fields[i] = map[string]any{
338+
"key": raw[0:idx],
339+
"op": raw[idx : idx+len(op)],
340+
"value": raw[idx+len(op):],
341+
}
342+
}
343+
args["fields"] = llx.ArrayData(fields, types.Dict)
344+
345+
if _, ok := args["keyname"]; !ok {
346+
args["keyname"] = llx.StringData("")
347+
}
348+
349+
r, err := CreateResource(s.MqlRuntime, resourceName, args)
350+
if err != nil {
351+
errors.Add(err)
352+
continue
353+
}
354+
s.Syscalls.Data = append(s.Syscalls.Data, r)
355+
356+
default:
357+
for io := range other {
358+
r, err := CreateResource(s.MqlRuntime, resourceName, map[string]*llx.RawData{
359+
"flag": llx.StringData(other[io][0]),
360+
"value": llx.StringData(other[io][1]),
361+
})
362+
if err != nil {
363+
errors.Add(err)
364+
continue
365+
}
366+
s.Controls.Data = append(s.Controls.Data, r)
367+
}
368+
}
369+
}
370+
}
371+
372+
func (s *mqlAuditdRules) controls(path string) ([]any, error) {
373+
return nil, s.load(path)
374+
}
375+
376+
func (s *mqlAuditdRules) files(path string) ([]any, error) {
377+
return nil, s.load(path)
378+
}
379+
380+
func (s *mqlAuditdRules) syscalls(path string) ([]any, error) {
381+
return nil, s.load(path)
382+
}
383+
384+
func (s *mqlAuditdRuleFile) id() (string, error) {
385+
var f checksums.Fast
386+
return f.
387+
Add(s.Path.Data).
388+
Add(s.Permissions.Data).
389+
Add(s.Keyname.Data).
390+
String(), nil
391+
}
392+
393+
func (s *mqlAuditdRuleControl) id() (string, error) {
394+
var f checksums.Fast
395+
return f.
396+
Add(s.Flag.Data).
397+
Add(s.Value.Data).
398+
String(), nil
399+
}
400+
401+
func (s *mqlAuditdRuleSyscall) id() (string, error) {
402+
var f checksums.Fast
403+
f = f.
404+
Add(s.Action.Data).
405+
Add(s.List.Data).
406+
Add(s.Keyname.Data)
407+
for i := range s.Syscalls.Data {
408+
f = f.Add(s.Syscalls.Data[i].(string))
409+
}
410+
for i := range s.Fields.Data {
411+
c := s.Fields.Data[i].(map[string]any)
412+
for k, v := range c {
413+
f = f.Add(k).Add(v.(string))
414+
}
415+
}
416+
417+
return f.String(), nil
418+
}

providers/os/resources/auditd_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,30 @@ func TestResource_AuditdConfig(t *testing.T) {
4545
assert.Equal(t, "/var/log/audit/AuDiT.log", res[0].Data.Value)
4646
})
4747
}
48+
49+
func TestResource_AuditdRules(t *testing.T) {
50+
t.Run("auditd rules path", func(t *testing.T) {
51+
x.TestSimple(t, []testutils.SimpleTest{
52+
{
53+
Code: "auditd.rules.path",
54+
ResultIndex: 0,
55+
Expectation: "/etc/audit/rules.d",
56+
},
57+
{
58+
Code: "auditd.rules.files.first.path",
59+
ResultIndex: 0,
60+
Expectation: "/etc/sudoers",
61+
},
62+
{
63+
Code: "auditd.rules.controls[0].flag",
64+
ResultIndex: 0,
65+
Expectation: "-D",
66+
},
67+
{
68+
Code: "auditd.rules.syscalls.where(action==\"always\" && fields.contains(key==\"path\" && value==\"/usr/bin/systemd-run\")).length",
69+
ResultIndex: 0,
70+
Expectation: int64(2),
71+
},
72+
})
73+
})
74+
}

0 commit comments

Comments
 (0)