Skip to content

Commit b675a2a

Browse files
committed
add agent logging
1 parent 09d7e60 commit b675a2a

File tree

7 files changed

+675
-558
lines changed

7 files changed

+675
-558
lines changed

cmd/agent/executeCheck.go

Lines changed: 48 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func (a *Agent) executeCheck(ctx context.Context, checkName string, rule config.
3636
func (*Agent) readFile(checkName string, rule config.CommandRule) gitmdm.CommandOutput {
3737
start := time.Now()
3838

39-
if *debug {
39+
if *debugMode {
4040
log.Printf("[DEBUG] Reading file for check %s: %s", checkName, rule.File)
4141
}
4242

@@ -49,7 +49,7 @@ func (*Agent) readFile(checkName string, rule config.CommandRule) gitmdm.Command
4949
if err != nil {
5050
if os.IsNotExist(err) {
5151
output.FileMissing = true
52-
if *debug {
52+
if *debugMode {
5353
log.Printf("[DEBUG] File not found for check %s: %s", checkName, rule.File)
5454
}
5555
} else {
@@ -71,70 +71,62 @@ func (*Agent) readFile(checkName string, rule config.CommandRule) gitmdm.Command
7171
log.Printf("[ERROR] Failed to analyze file check: %v", err)
7272
}
7373

74-
if *debug {
74+
if *debugMode {
7575
log.Printf("[DEBUG] File read completed in %v (missing: %v, failed: %v): %s",
7676
time.Since(start), output.FileMissing, output.Failed, rule.File)
7777
}
7878

7979
return output
8080
}
8181

82+
// checkCommandAvailable verifies that a command is available to execute.
83+
func checkCommandAvailable(checkName, command string) *gitmdm.CommandOutput {
84+
if containsShellOperators(command) {
85+
return nil // Commands with shell operators need shell interpretation
86+
}
87+
88+
commandParts := strings.Fields(command)
89+
if len(commandParts) == 0 {
90+
return nil
91+
}
92+
93+
primaryCmd := commandParts[0]
94+
if isShellBuiltin(primaryCmd) || strings.Contains(primaryCmd, "/") {
95+
return nil // Shell builtins and absolute paths don't need validation
96+
}
97+
98+
// Temporarily set PATH for LookPath
99+
oldPath := os.Getenv("PATH")
100+
if err := os.Setenv("PATH", securePath()); err != nil {
101+
log.Printf("[WARN] Failed to set PATH for command check: %v", err)
102+
}
103+
_, lookupErr := exec.LookPath(primaryCmd)
104+
if err := os.Setenv("PATH", oldPath); err != nil {
105+
log.Printf("[WARN] Failed to restore PATH: %v", err)
106+
}
107+
108+
if lookupErr != nil {
109+
if *debugMode {
110+
log.Printf("[DEBUG] Command '%s' not found in PATH for check '%s', skipping", primaryCmd, checkName)
111+
}
112+
return &gitmdm.CommandOutput{
113+
Command: command,
114+
Skipped: true,
115+
FileMissing: true, // Treat missing command like missing file
116+
Stderr: fmt.Sprintf("Skipped: %s not found", primaryCmd),
117+
}
118+
}
119+
return nil
120+
}
121+
82122
// executeCommand executes a command and returns its output.
83123
func (a *Agent) executeCommand(ctx context.Context, checkName string, rule config.CommandRule) gitmdm.CommandOutput {
84124
start := time.Now()
85125
command := rule.Output
86126

87-
// Skip PATH checking if the command contains shell operators
88-
// These commands need shell interpretation and can't be validated simply
89-
if !containsShellOperators(command) {
90-
// Extract the primary command (first word) to check if it exists
91-
commandParts := strings.Fields(command)
92-
if len(commandParts) > 0 {
93-
primaryCmd := commandParts[0]
94-
95-
// Check if this is a shell builtin or special command
96-
shellBuiltins := map[string]bool{
97-
"echo": true, "test": true, "[": true, "[[": true, "if": true,
98-
"then": true, "else": true, "fi": true, "for": true, "while": true,
99-
"do": true, "done": true, "case": true, "esac": true, "function": true,
100-
"return": true, "break": true, "continue": true, "exit": true,
101-
"source": true, ".": true, "eval": true, "exec": true, "export": true,
102-
"unset": true, "shift": true, "cd": true, "pwd": true, "read": true,
103-
"readonly": true, "declare": true, "typeset": true, "local": true,
104-
"true": true, "false": true, "type": true, "command": true,
105-
// Include sudo and doas since they're commonly used
106-
"sudo": true, "doas": true,
107-
}
108-
109-
// If it's not a shell builtin and not a path, check if the command exists
110-
if !shellBuiltins[primaryCmd] && !strings.Contains(primaryCmd, "/") {
111-
// Temporarily set PATH for LookPath
112-
oldPath := os.Getenv("PATH")
113-
if err := os.Setenv("PATH", securePath()); err != nil {
114-
log.Printf("[WARN] Failed to set PATH for command check: %v", err)
115-
}
116-
_, lookupErr := exec.LookPath(primaryCmd)
117-
if err := os.Setenv("PATH", oldPath); err != nil {
118-
log.Printf("[WARN] Failed to restore PATH: %v", err)
119-
}
120-
121-
if lookupErr != nil {
122-
if *debug {
123-
log.Printf("[DEBUG] Command '%s' not found in PATH for check '%s', skipping", primaryCmd, checkName)
124-
}
125-
126-
output := gitmdm.CommandOutput{
127-
Command: command,
128-
Skipped: true,
129-
FileMissing: true, // Treat missing command like missing file
130-
Stderr: fmt.Sprintf("Skipped: %s not found", primaryCmd),
131-
}
132-
133-
// Don't analyze skipped commands
134-
return output
135-
}
136-
}
137-
}
127+
// Check if command is available
128+
if result := checkCommandAvailable(checkName, command); result != nil {
129+
return *result
138130
}
139131

140132
// Use longer timeout for software update checks (they contact remote servers)
@@ -146,7 +138,7 @@ func (a *Agent) executeCommand(ctx context.Context, checkName string, rule confi
146138
ctx, cancel := context.WithTimeout(ctx, timeout)
147139
defer cancel()
148140

149-
if *debug {
141+
if *debugMode {
150142
log.Printf("[DEBUG] Executing command: %s", command)
151143
}
152144

@@ -158,7 +150,7 @@ func (a *Agent) executeCommand(ctx context.Context, checkName string, rule confi
158150
log.Printf("[ERROR] Failed to analyze command check: %v", err)
159151
}
160152

161-
if *debug {
153+
if *debugMode {
162154
log.Printf("[DEBUG] Command completed in %v (skipped: %v, failed: %v): %s",
163155
time.Since(start), output.Skipped, output.Failed, command)
164156
}

cmd/agent/executeCommand.go

Lines changed: 99 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -48,60 +48,108 @@ func securePath() string {
4848
}
4949
}
5050

51+
// isShellBuiltin checks if a command is a shell builtin.
52+
func isShellBuiltin(cmd string) bool {
53+
shellBuiltins := map[string]bool{
54+
"echo": true, "test": true, "[": true, "[[": true, "if": true,
55+
"then": true, "else": true, "fi": true, "for": true, "while": true,
56+
"do": true, "done": true, "case": true, "esac": true, "function": true,
57+
"return": true, "break": true, "continue": true, "exit": true,
58+
"source": true, ".": true, "eval": true, "exec": true, "export": true,
59+
"unset": true, "shift": true, "cd": true, "pwd": true, "read": true,
60+
"readonly": true, "declare": true, "typeset": true, "local": true,
61+
"true": true, "false": true, "type": true, "command": true,
62+
// Include sudo and doas since they're commonly used
63+
"sudo": true, "doas": true,
64+
}
65+
return shellBuiltins[cmd]
66+
}
67+
68+
// logCommandOutput logs command output for debugging.
69+
func logCommandOutput(checkName, command, stdout, stderr string, exitCode int, duration time.Duration) {
70+
if quiet {
71+
return
72+
}
73+
74+
prefix := ""
75+
if checkName != "" {
76+
prefix = fmt.Sprintf("[%s] ", checkName)
77+
}
78+
79+
// Log non-empty stdout
80+
if stdout != "" && strings.TrimSpace(stdout) != "" {
81+
trimmed := strings.TrimSpace(stdout)
82+
if len(trimmed) > maxLogLength {
83+
trimmed = trimmed[:maxLogLength] + "..."
84+
}
85+
log.Printf("[INFO] %sstdout (%d bytes): %s", prefix, len(stdout), trimmed)
86+
}
87+
88+
// Log non-empty stderr
89+
if stderr != "" && strings.TrimSpace(stderr) != "" {
90+
trimmed := strings.TrimSpace(stderr)
91+
if len(trimmed) > maxLogLength {
92+
trimmed = trimmed[:maxLogLength] + "..."
93+
}
94+
log.Printf("[INFO] %sstderr (%d bytes): %s", prefix, len(stderr), trimmed)
95+
}
96+
97+
if *debugMode {
98+
log.Printf("[DEBUG] Command completed in %v (exit: %d, stdout: %d bytes, stderr: %d bytes): %s",
99+
duration, exitCode, len(stdout), len(stderr), command)
100+
}
101+
}
102+
103+
// validateCommand checks if a command exists in PATH.
104+
func validateCommand(checkName, command string) *gitmdm.CommandOutput {
105+
if containsShellOperators(command) {
106+
return nil // Commands with shell operators need shell interpretation
107+
}
108+
109+
commandParts := strings.Fields(command)
110+
if len(commandParts) == 0 {
111+
return nil
112+
}
113+
114+
primaryCmd := commandParts[0]
115+
if isShellBuiltin(primaryCmd) || strings.Contains(primaryCmd, "/") {
116+
return nil // Shell builtins and absolute paths don't need validation
117+
}
118+
119+
// Temporarily set PATH for LookPath
120+
oldPath := os.Getenv("PATH")
121+
if err := os.Setenv("PATH", securePath()); err != nil {
122+
log.Printf("[WARN] Failed to set PATH for command check: %v", err)
123+
}
124+
_, lookupErr := exec.LookPath(primaryCmd)
125+
if err := os.Setenv("PATH", oldPath); err != nil {
126+
log.Printf("[WARN] Failed to restore PATH: %v", err)
127+
}
128+
129+
if lookupErr != nil {
130+
if *debugMode {
131+
log.Printf("[DEBUG] Command '%s' not found in PATH for check '%s', skipping", primaryCmd, checkName)
132+
}
133+
return &gitmdm.CommandOutput{
134+
Command: command,
135+
Stdout: "",
136+
Stderr: fmt.Sprintf("Skipped: %s not found", primaryCmd),
137+
ExitCode: -2, // Special exit code for skipped
138+
}
139+
}
140+
return nil
141+
}
142+
51143
// executeCommandWithPipes executes a command and captures stdout/stderr separately.
52144
// SECURITY: Commands come from checks.yaml which must be controlled by the system admin.
53145
// We set a minimal secure PATH to prevent PATH-based attacks.
54146
// The agent runs with user privileges only.
55147
func (*Agent) executeCommandWithPipes(ctx context.Context, checkName, command string) gitmdm.CommandOutput {
56148
start := time.Now()
57149

58-
// Skip PATH checking if the command contains shell operators
59-
// These commands need shell interpretation and can't be validated simply
60-
if !containsShellOperators(command) {
61-
// Extract the primary command (first word) to check if it exists
62-
commandParts := strings.Fields(command)
63-
if len(commandParts) > 0 {
64-
primaryCmd := commandParts[0]
65-
66-
// Check if this is a shell builtin or special command
67-
shellBuiltins := map[string]bool{
68-
"echo": true, "test": true, "[": true, "[[": true, "if": true,
69-
"then": true, "else": true, "fi": true, "for": true, "while": true,
70-
"do": true, "done": true, "case": true, "esac": true, "function": true,
71-
"return": true, "break": true, "continue": true, "exit": true,
72-
"source": true, ".": true, "eval": true, "exec": true, "export": true,
73-
"unset": true, "shift": true, "cd": true, "pwd": true, "read": true,
74-
"readonly": true, "declare": true, "typeset": true, "local": true,
75-
"true": true, "false": true, "type": true, "command": true,
76-
// Include sudo and doas since they're commonly used
77-
"sudo": true, "doas": true,
78-
}
79-
80-
// If it's not a shell builtin and not a path, check if the command exists
81-
if !shellBuiltins[primaryCmd] && !strings.Contains(primaryCmd, "/") {
82-
// Temporarily set PATH for LookPath
83-
oldPath := os.Getenv("PATH")
84-
if err := os.Setenv("PATH", securePath()); err != nil {
85-
log.Printf("[WARN] Failed to set PATH for command check: %v", err)
86-
}
87-
_, lookupErr := exec.LookPath(primaryCmd)
88-
if err := os.Setenv("PATH", oldPath); err != nil {
89-
log.Printf("[WARN] Failed to restore PATH: %v", err)
90-
}
91-
92-
if lookupErr != nil {
93-
if *debug {
94-
log.Printf("[DEBUG] Command '%s' not found in PATH for check '%s', skipping", primaryCmd, checkName)
95-
}
96-
return gitmdm.CommandOutput{
97-
Command: command,
98-
Stdout: "",
99-
Stderr: fmt.Sprintf("Skipped: %s not found", primaryCmd),
100-
ExitCode: -2, // Special exit code for skipped
101-
}
102-
}
103-
}
104-
}
150+
// Validate command exists
151+
if result := validateCommand(checkName, command); result != nil {
152+
return *result
105153
}
106154

107155
// Use longer timeout for software update checks (they contact remote servers)
@@ -113,7 +161,7 @@ func (*Agent) executeCommandWithPipes(ctx context.Context, checkName, command st
113161
ctx, cancel := context.WithTimeout(ctx, timeout)
114162
defer cancel()
115163

116-
if *debug {
164+
if *debugMode {
117165
log.Printf("[DEBUG] Executing command: %s", command)
118166
}
119167

@@ -131,7 +179,7 @@ func (*Agent) executeCommandWithPipes(ctx context.Context, checkName, command st
131179

132180
// Set a secure, minimal PATH for the subprocess
133181
securePath := securePath()
134-
if *debug {
182+
if *debugMode {
135183
log.Printf("[DEBUG] Using secure PATH for %s: %s", runtime.GOOS, securePath)
136184
}
137185
cmd.Env = append(os.Environ(), "PATH="+securePath)
@@ -181,34 +229,8 @@ func (*Agent) executeCommandWithPipes(ctx context.Context, checkName, command st
181229
}
182230
}
183231

184-
// Log stdout/stderr for debugging with check name
185-
prefix := ""
186-
if checkName != "" {
187-
prefix = fmt.Sprintf("[%s] ", checkName)
188-
}
189-
190-
// Only log non-empty outputs (unless in quiet mode)
191-
if !quiet {
192-
if stdout != "" && strings.TrimSpace(stdout) != "" {
193-
trimmed := strings.TrimSpace(stdout)
194-
if len(trimmed) > maxLogLength {
195-
trimmed = trimmed[:maxLogLength] + "..."
196-
}
197-
log.Printf("[INFO] %sstdout (%d bytes): %s", prefix, len(stdout), trimmed)
198-
}
199-
if stderr != "" && strings.TrimSpace(stderr) != "" {
200-
trimmed := strings.TrimSpace(stderr)
201-
if len(trimmed) > maxLogLength {
202-
trimmed = trimmed[:maxLogLength] + "..."
203-
}
204-
log.Printf("[INFO] %sstderr (%d bytes): %s", prefix, len(stderr), trimmed)
205-
}
206-
}
207-
208-
if *debug {
209-
log.Printf("[DEBUG] Command completed in %v (exit: %d, stdout: %d bytes, stderr: %d bytes): %s",
210-
duration, exitCode, len(stdout), len(stderr), command)
211-
}
232+
// Log command output
233+
logCommandOutput(checkName, command, stdout, stderr, exitCode, duration)
212234

213235
return gitmdm.CommandOutput{
214236
Command: command,

cmd/agent/install.go

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,19 @@ func installExecutable(exePath, targetPath string) error {
7272
}
7373

7474
// Try direct copy first
75-
if err := copyFile(exePath, targetPath); err != nil {
75+
data, err := os.ReadFile(exePath)
76+
if err != nil {
77+
return fmt.Errorf("failed to read executable: %w", err)
78+
}
79+
if err := os.WriteFile(targetPath, data, 0o755); err != nil { //nolint:gosec // executable needs execute permission
7680
// Handle "text file busy" error by copying to temp and renaming
7781
if !strings.Contains(strings.ToLower(err.Error()), "text file busy") {
7882
return fmt.Errorf("failed to copy executable: %w", err)
7983
}
8084

8185
// Copy to temp file and rename
8286
tempPath := targetPath + ".new"
83-
if err := copyFile(exePath, tempPath); err != nil {
87+
if err := os.WriteFile(tempPath, data, 0o755); err != nil { //nolint:gosec // executable needs execute permission
8488
return fmt.Errorf("failed to copy executable to temp file: %w", err)
8589
}
8690
if err := os.Rename(tempPath, targetPath); err != nil {
@@ -208,15 +212,6 @@ func uninstallAgent() error {
208212
return nil
209213
}
210214

211-
// copyFile copies a file from src to dst.
212-
func copyFile(src, dst string) error {
213-
data, err := os.ReadFile(src)
214-
if err != nil {
215-
return err
216-
}
217-
return os.WriteFile(dst, data, 0o755) //nolint:gosec // executable needs execute permission
218-
}
219-
220215
// isSystemdUserAvailable checks if systemd user services are available and working.
221216
func isSystemdUserAvailable() bool {
222217
// Check if systemctl exists

0 commit comments

Comments
 (0)