Skip to content

Commit d5ffa11

Browse files
tas50claude
andauthored
⭐ Add logrotate resources (#6780)
* Add logrotate resources This will help us to improve the nginx CIS benchmark Signed-off-by: Tim Smith <tsmith84@gmail.com> * Address review: handle init path arg, add files.find depth limit - initLogrotate now properly handles the path argument by storing it and using it as the main config file path in files() - Add depth: 1 to files.find for logrotate.d to avoid recursing into subdirectories, matching logrotate's own behavior Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * 🐛 Fix cache pollution bug in logrotate init Remove the custom path implementation that caused cache pollution. The CreateResource call in initLogrotate cached a default resource then mutated customPath on it, corrupting subsequent default queries. Removed init(path? string) from .lr and the mqlLogrotateInternal struct. Custom path support can be added later following the logindefs pattern with a proper file field. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Signed-off-by: Tim Smith <tsmith84@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 382f495 commit d5ffa11

6 files changed

Lines changed: 1033 additions & 0 deletions

File tree

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// Copyright (c) Mondoo, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package resources
5+
6+
import (
7+
"errors"
8+
"fmt"
9+
"strconv"
10+
"strings"
11+
12+
"go.mondoo.com/mql/v13/llx"
13+
"go.mondoo.com/mql/v13/providers-sdk/v1/plugin"
14+
"go.mondoo.com/mql/v13/providers/os/resources/logrotate"
15+
"go.mondoo.com/mql/v13/types"
16+
)
17+
18+
const (
19+
defaultLogrotateConf = "/etc/logrotate.conf"
20+
defaultLogrotateDir = "/etc/logrotate.d"
21+
)
22+
23+
func initLogrotate(runtime *plugin.Runtime, args map[string]*llx.RawData) (map[string]*llx.RawData, plugin.Resource, error) {
24+
return args, nil, nil
25+
}
26+
27+
func (l *mqlLogrotate) id() (string, error) {
28+
return "logrotate", nil
29+
}
30+
31+
func (le *mqlLogrotateEntry) id() (string, error) {
32+
file := le.File.Data
33+
lineNum := strconv.FormatInt(le.LineNumber.Data, 10)
34+
35+
return file.Path.Data + ":" + lineNum + ":" + le.Path.Data, nil
36+
}
37+
38+
// files discovers logrotate configuration files from the main config and logrotate.d directory.
39+
func (l *mqlLogrotate) files() ([]any, error) {
40+
var allFiles []any
41+
42+
// Add main logrotate.conf
43+
mainFile, err := CreateResource(l.MqlRuntime, "file", map[string]*llx.RawData{
44+
"path": llx.StringData(defaultLogrotateConf),
45+
})
46+
if err != nil {
47+
return nil, err
48+
}
49+
f := mainFile.(*mqlFile)
50+
exists := f.GetExists()
51+
if exists.Error != nil {
52+
return nil, exists.Error
53+
}
54+
55+
if exists.Data {
56+
allFiles = append(allFiles, f)
57+
}
58+
59+
// Check logrotate.d directory
60+
dirFile, err := CreateResource(l.MqlRuntime, "file", map[string]*llx.RawData{
61+
"path": llx.StringData(defaultLogrotateDir),
62+
})
63+
if err != nil {
64+
return nil, err
65+
}
66+
dir := dirFile.(*mqlFile)
67+
dirExists := dir.GetExists()
68+
if dirExists.Error != nil {
69+
return nil, dirExists.Error
70+
}
71+
72+
if dirExists.Data {
73+
files, err := CreateResource(l.MqlRuntime, "files.find", map[string]*llx.RawData{
74+
"from": llx.StringData(defaultLogrotateDir),
75+
"type": llx.StringData("file"),
76+
"depth": llx.IntData(1),
77+
})
78+
if err != nil {
79+
return nil, err
80+
}
81+
82+
ff := files.(*mqlFilesFind)
83+
list := ff.GetList()
84+
if list.Error != nil {
85+
return nil, list.Error
86+
}
87+
88+
for i := range list.Data {
89+
file := list.Data[i].(*mqlFile)
90+
basename := file.GetBasename()
91+
if basename.Error != nil {
92+
continue
93+
}
94+
95+
// Skip common backup/temp file extensions that logrotate ignores
96+
name := basename.Data
97+
if strings.HasSuffix(name, ".bak") || strings.HasSuffix(name, ".old") ||
98+
strings.HasSuffix(name, ".rpmsave") || strings.HasSuffix(name, ".rpmorig") ||
99+
strings.HasSuffix(name, ".dpkg-old") || strings.HasSuffix(name, ".dpkg-new") ||
100+
strings.HasSuffix(name, ".dpkg-dist") || strings.HasSuffix(name, "~") {
101+
continue
102+
}
103+
104+
allFiles = append(allFiles, file)
105+
}
106+
}
107+
108+
return allFiles, nil
109+
}
110+
111+
// globalConfig parses all config files and returns the global directives.
112+
func (l *mqlLogrotate) globalConfig(files []any) (map[string]any, error) {
113+
merged := make(map[string]any)
114+
115+
for i := range files {
116+
file := files[i].(*mqlFile)
117+
content := file.GetContent()
118+
if content.Error != nil {
119+
continue
120+
}
121+
122+
global, _ := logrotate.ParseContent(file.Path.Data, content.Data)
123+
for k, v := range global {
124+
merged[k] = v
125+
}
126+
}
127+
128+
return merged, nil
129+
}
130+
131+
// entries parses all config files and returns logrotate entries.
132+
func (l *mqlLogrotate) entries(files []any) ([]any, error) {
133+
var allEntries []any
134+
var errs []error
135+
136+
for i := range files {
137+
file := files[i].(*mqlFile)
138+
139+
content := file.GetContent()
140+
if content.Error != nil {
141+
errs = append(errs, fmt.Errorf("failed to read %s: %w", file.Path.Data, content.Error))
142+
continue
143+
}
144+
145+
entries, err := parseLogrotateContent(l.MqlRuntime, file, content.Data)
146+
if err != nil {
147+
errs = append(errs, fmt.Errorf("failed to parse %s: %w", file.Path.Data, err))
148+
continue
149+
}
150+
151+
allEntries = append(allEntries, entries...)
152+
}
153+
154+
if len(errs) > 0 {
155+
return allEntries, errors.Join(errs...)
156+
}
157+
158+
return allEntries, nil
159+
}
160+
161+
func parseLogrotateContent(runtime *plugin.Runtime, file *mqlFile, content string) ([]any, error) {
162+
_, parsed := logrotate.ParseContent(file.Path.Data, content)
163+
var entries []any
164+
165+
for _, e := range parsed {
166+
configMap := make(map[string]any, len(e.Config))
167+
for k, v := range e.Config {
168+
configMap[k] = v
169+
}
170+
171+
entry, err := CreateResource(runtime, "logrotate.entry", map[string]*llx.RawData{
172+
"file": llx.ResourceData(file, "file"),
173+
"lineNumber": llx.IntData(int64(e.LineNumber)),
174+
"path": llx.StringData(e.Path),
175+
"config": llx.MapData(configMap, types.String),
176+
})
177+
if err != nil {
178+
return nil, err
179+
}
180+
181+
entries = append(entries, entry.(*mqlLogrotateEntry))
182+
}
183+
184+
return entries, nil
185+
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
// Copyright (c) Mondoo, Inc.
2+
// SPDX-License-Identifier: BUSL-1.1
3+
4+
package logrotate
5+
6+
import (
7+
"strings"
8+
)
9+
10+
// Entry represents a single logrotate block for one log path.
11+
// If a block specifies multiple paths, each path gets its own Entry
12+
// with the same config values.
13+
type Entry struct {
14+
File string
15+
LineNumber int
16+
Path string
17+
Config map[string]string
18+
}
19+
20+
// ParseContent parses the content of a logrotate configuration file and
21+
// returns the global directives and per-path entries.
22+
func ParseContent(filePath string, content string) (globalConfig map[string]string, entries []Entry) {
23+
globalConfig = make(map[string]string)
24+
lines := strings.Split(content, "\n")
25+
26+
var (
27+
inBlock bool
28+
inScript bool
29+
blockPaths []string
30+
blockLine int
31+
blockConf map[string]string
32+
)
33+
34+
for i, line := range lines {
35+
lineNum := i + 1
36+
trimmed := strings.TrimSpace(line)
37+
38+
// Skip empty lines and comments
39+
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
40+
continue
41+
}
42+
43+
// Handle endscript inside a script block
44+
if inScript {
45+
if trimmed == "endscript" {
46+
inScript = false
47+
}
48+
continue
49+
}
50+
51+
if inBlock {
52+
// Check for closing brace
53+
if trimmed == "}" {
54+
// Emit one entry per path in this block
55+
for _, p := range blockPaths {
56+
entries = append(entries, Entry{
57+
File: filePath,
58+
LineNumber: blockLine,
59+
Path: p,
60+
Config: copyMap(blockConf),
61+
})
62+
}
63+
inBlock = false
64+
blockPaths = nil
65+
blockConf = nil
66+
continue
67+
}
68+
69+
// Check for script block start
70+
if isScriptDirective(trimmed) {
71+
inScript = true
72+
continue
73+
}
74+
75+
// Parse directive inside block
76+
key, value := parseDirective(trimmed)
77+
if key != "" {
78+
blockConf[key] = value
79+
}
80+
continue
81+
}
82+
83+
// Outside any block
84+
85+
// Check for lone opening brace (paths on previous line(s), { on its own)
86+
if trimmed == "{" {
87+
blockPaths, blockLine = findPathsBackward(lines, i)
88+
if len(blockPaths) > 0 {
89+
blockConf = make(map[string]string)
90+
inBlock = true
91+
}
92+
continue
93+
}
94+
95+
// Check for block opening: paths followed by { on the same line
96+
if strings.HasSuffix(trimmed, "{") {
97+
pathsPart := strings.TrimSuffix(trimmed, "{")
98+
pathsPart = strings.TrimSpace(pathsPart)
99+
if pathsPart != "" {
100+
blockPaths = splitPaths(pathsPart)
101+
blockLine = lineNum
102+
blockConf = make(map[string]string)
103+
inBlock = true
104+
}
105+
continue
106+
}
107+
108+
// Skip include directives at global level (file discovery handles these)
109+
if strings.HasPrefix(trimmed, "include ") || strings.HasPrefix(trimmed, "include\t") {
110+
continue
111+
}
112+
113+
// Skip tabooext directives
114+
if strings.HasPrefix(trimmed, "tabooext ") || strings.HasPrefix(trimmed, "tabooext\t") {
115+
continue
116+
}
117+
118+
// Global directive
119+
key, value := parseDirective(trimmed)
120+
if key != "" {
121+
globalConfig[key] = value
122+
}
123+
}
124+
125+
return globalConfig, entries
126+
}
127+
128+
// parseDirective splits a logrotate directive into key and value.
129+
// Boolean directives (like "compress") return key with empty value.
130+
// Value directives (like "rotate 4") return key and value.
131+
func parseDirective(line string) (string, string) {
132+
// Strip inline comments
133+
if idx := strings.Index(line, "#"); idx >= 0 {
134+
line = strings.TrimSpace(line[:idx])
135+
}
136+
if line == "" {
137+
return "", ""
138+
}
139+
140+
parts := strings.Fields(line)
141+
if len(parts) == 0 {
142+
return "", ""
143+
}
144+
145+
key := parts[0]
146+
if len(parts) == 1 {
147+
return key, ""
148+
}
149+
return key, strings.Join(parts[1:], " ")
150+
}
151+
152+
// splitPaths splits a space-separated list of log file paths/globs.
153+
func splitPaths(s string) []string {
154+
fields := strings.Fields(s)
155+
paths := make([]string, 0, len(fields))
156+
for _, f := range fields {
157+
f = strings.TrimSpace(f)
158+
if f != "" {
159+
paths = append(paths, f)
160+
}
161+
}
162+
return paths
163+
}
164+
165+
// findPathsBackward scans backward from a lone "{" to find the log path(s).
166+
func findPathsBackward(lines []string, braceIdx int) ([]string, int) {
167+
for j := braceIdx - 1; j >= 0; j-- {
168+
trimmed := strings.TrimSpace(lines[j])
169+
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
170+
continue
171+
}
172+
return splitPaths(trimmed), j + 1
173+
}
174+
return nil, 0
175+
}
176+
177+
// isScriptDirective returns true if the line starts a script block.
178+
func isScriptDirective(line string) bool {
179+
for _, prefix := range []string{"prerotate", "postrotate", "firstaction", "lastaction", "preremove"} {
180+
if line == prefix || strings.HasPrefix(line, prefix+" ") {
181+
return true
182+
}
183+
}
184+
return false
185+
}
186+
187+
func copyMap(m map[string]string) map[string]string {
188+
cp := make(map[string]string, len(m))
189+
for k, v := range m {
190+
cp[k] = v
191+
}
192+
return cp
193+
}

0 commit comments

Comments
 (0)