bb7892c0c2
- M2: stop echoing the matched pattern name in the user-visible [BLOCKED: ...] message returned by the firewall. The pattern (and the matched secret class) still appear in the operator log, but the string sent back into the prompt is now generic. - H1: document Rule.Pattern semantics on the Rule type and pin them with a regression test. Pattern is a case-sensitive, exact substring match against the JSON-serialised tool arguments — not a glob, regex, or whitespace-insensitive match. The new test exercises both matches and the documented gotchas (double-space, case drift, tab). - H3: every code path in CommandExecutor.Execute that converts a hook failure into Allow via FailOpen now emits a WARN naming the hook and the failure mode (timeout / launch_error / parse_error), so chronic hook failure or abuse is visible in operator logs. Also tightens errcheck on permission/rule.go (Printer.Print on a strings.Builder cannot error in practice; make the intent explicit).
100 lines
2.9 KiB
Go
100 lines
2.9 KiB
Go
package permission
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"mvdan.cc/sh/v3/syntax"
|
|
)
|
|
|
|
// Action is the decision for a permission rule.
|
|
type Action string
|
|
|
|
const (
|
|
ActionAllow Action = "allow"
|
|
ActionDeny Action = "deny"
|
|
)
|
|
|
|
// Rule defines a single permission rule.
|
|
//
|
|
// Pattern matching semantics — important caveats:
|
|
//
|
|
// - Tool is matched as a filepath.Match glob ("bash", "fs.*", "*").
|
|
// - Pattern, if set, is a case-sensitive substring match against the
|
|
// JSON-serialized tool arguments. It is NOT a glob, regex, or
|
|
// structured-field match.
|
|
//
|
|
// Concretely, given `Pattern = "rm -rf"`, these match:
|
|
// - `{"command":"rm -rf /tmp"}`
|
|
// - `{"prefix":"echo rm -rf","other":"x"}` (substring appears anywhere)
|
|
//
|
|
// And these DO NOT match:
|
|
// - `{"command":"rm -rf /"}` (double space — substring is exact)
|
|
// - `{"command":"RM -RF /"}` (case differs)
|
|
// - `{"command":"rm\t-rf /"}` (tab vs space)
|
|
//
|
|
// Rule authors: write patterns that are unambiguous when serialized as JSON
|
|
// (no leading/trailing whitespace, no surrounding quotes) and remember that
|
|
// deny rules are bypass-immune — they fire before any mode check.
|
|
type Rule struct {
|
|
Tool string `toml:"tool"` // glob pattern: "bash", "fs.*", "*"
|
|
Pattern string `toml:"pattern"` // optional: case-sensitive substring on JSON args (see godoc)
|
|
Action Action `toml:"action"`
|
|
}
|
|
|
|
// Matches returns true if the rule matches the given tool name.
|
|
func (r Rule) Matches(toolName string) bool {
|
|
matched, _ := filepath.Match(r.Tool, toolName)
|
|
return matched
|
|
}
|
|
|
|
// SplitCompoundCommand decomposes a shell command into individual simple commands
|
|
// using a proper POSIX shell parser (mvdan.cc/sh). Recursively walks BinaryCmd
|
|
// nodes (&&, ||) and statement lists (;).
|
|
func SplitCompoundCommand(cmd string) []string {
|
|
reader := strings.NewReader(cmd)
|
|
parser := syntax.NewParser(syntax.KeepComments(false))
|
|
|
|
file, err := parser.Parse(reader, "")
|
|
if err != nil {
|
|
return []string{cmd}
|
|
}
|
|
|
|
var commands []string
|
|
printer := syntax.NewPrinter()
|
|
|
|
for _, stmt := range file.Stmts {
|
|
extractCommands(stmt.Cmd, printer, &commands)
|
|
}
|
|
|
|
if len(commands) == 0 {
|
|
return []string{cmd}
|
|
}
|
|
return commands
|
|
}
|
|
|
|
func extractCommands(node syntax.Command, printer *syntax.Printer, out *[]string) {
|
|
if node == nil {
|
|
return
|
|
}
|
|
// Only split on && and || (logical operators), not pipes
|
|
if bin, ok := node.(*syntax.BinaryCmd); ok {
|
|
if bin.Op == syntax.AndStmt || bin.Op == syntax.OrStmt {
|
|
if bin.X != nil {
|
|
extractCommands(bin.X.Cmd, printer, out)
|
|
}
|
|
if bin.Y != nil {
|
|
extractCommands(bin.Y.Cmd, printer, out)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
// Everything else (simple command, pipe, subshell) — print as one unit.
|
|
// Printer errors on a strings.Builder are unreachable (Builder.Write never errors).
|
|
var b strings.Builder
|
|
_ = printer.Print(&b, node)
|
|
if s := strings.TrimSpace(b.String()); s != "" {
|
|
*out = append(*out, s)
|
|
}
|
|
}
|