test(security): satisfy tirith CI gates
This commit is contained in:
parent
e753cff0ba
commit
ca7168aa9a
3 changed files with 84 additions and 25 deletions
|
|
@ -936,13 +936,17 @@ type TirithConfig struct {
|
|||
}
|
||||
|
||||
type ExecConfig struct {
|
||||
ToolConfig ` envPrefix:"PICOCLAW_TOOLS_EXEC_"`
|
||||
EnableDenyPatterns bool ` json:"enable_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_ENABLE_DENY_PATTERNS"`
|
||||
AllowRemote bool ` json:"allow_remote" env:"PICOCLAW_TOOLS_EXEC_ALLOW_REMOTE"`
|
||||
CustomDenyPatterns []string ` json:"custom_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_CUSTOM_DENY_PATTERNS"`
|
||||
CustomAllowPatterns []string ` json:"custom_allow_patterns" env:"PICOCLAW_TOOLS_EXEC_CUSTOM_ALLOW_PATTERNS"`
|
||||
TimeoutSeconds int ` json:"timeout_seconds" env:"PICOCLAW_TOOLS_EXEC_TIMEOUT_SECONDS"` // 0 means use default (60s)
|
||||
Tirith TirithConfig ` json:"tirith" envPrefix:"PICOCLAW_TOOLS_EXEC_TIRITH_"`
|
||||
ToolConfig `envPrefix:"PICOCLAW_TOOLS_EXEC_"`
|
||||
|
||||
EnableDenyPatterns bool `json:"enable_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_ENABLE_DENY_PATTERNS"`
|
||||
AllowRemote bool `json:"allow_remote" env:"PICOCLAW_TOOLS_EXEC_ALLOW_REMOTE"`
|
||||
CustomDenyPatterns []string `json:"custom_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_CUSTOM_DENY_PATTERNS"`
|
||||
CustomAllowPatterns []string `json:"custom_allow_patterns" env:"PICOCLAW_TOOLS_EXEC_CUSTOM_ALLOW_PATTERNS"`
|
||||
|
||||
// 0 means use default (60s)
|
||||
TimeoutSeconds int `json:"timeout_seconds" env:"PICOCLAW_TOOLS_EXEC_TIMEOUT_SECONDS"`
|
||||
|
||||
Tirith TirithConfig `json:"tirith" envPrefix:"PICOCLAW_TOOLS_EXEC_TIRITH_"`
|
||||
}
|
||||
|
||||
type SkillsToolsConfig struct {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Tirith pre-exec security scanning.
|
||||
// Package tools integrates Tirith pre-exec security scanning.
|
||||
//
|
||||
// Tirith (https://github.com/sheeki03/tirith) is a terminal security tool
|
||||
// that scans commands for content-level threats: homograph/punycode URLs,
|
||||
|
|
@ -238,23 +238,30 @@ func tirithHandleExit(exitCode int, jsonOutput []byte, cfg TirithConfig) string
|
|||
log.Printf("[tirith] warning: %s", tirithSummarize(jsonOutput))
|
||||
return ""
|
||||
case 3:
|
||||
log.Printf("[tirith] warning acknowledgement required; allowing in non-interactive mode: %s", tirithSummarize(jsonOutput))
|
||||
log.Printf(
|
||||
"[tirith] warning acknowledgement required; allowing in non-interactive mode: %s",
|
||||
tirithSummarize(jsonOutput),
|
||||
)
|
||||
return ""
|
||||
default:
|
||||
return tirithScannerFailure(normalizeTirithBin(cfg.BinPath), cfg, fmt.Errorf("unexpected exit code %d", exitCode))
|
||||
return tirithScannerFailure(
|
||||
normalizeTirithBin(cfg.BinPath),
|
||||
cfg,
|
||||
fmt.Errorf("unexpected exit code %d", exitCode),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func tirithScannerFailure(cacheKey string, cfg TirithConfig, err error) string {
|
||||
message := tirithSanitizeText(err.Error())
|
||||
if cfg.FailOpen {
|
||||
tirithLogOnce(cacheKey, "scanner-failure", "[tirith] unavailable (fail-open): %s", message)
|
||||
tirithLogOncef(cacheKey, "scanner-failure", "[tirith] unavailable (fail-open): %s", message)
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("Tirith security scan failed (fail-closed): %s", message)
|
||||
}
|
||||
|
||||
func tirithLogOnce(cacheKey, category, format string, args ...any) {
|
||||
func tirithLogOncef(cacheKey, category, format string, args ...any) {
|
||||
cacheKey = normalizeTirithBin(cacheKey)
|
||||
key := cacheKey + "\x00" + category + "\x00" + fmt.Sprintf(format, args...)
|
||||
|
||||
|
|
|
|||
|
|
@ -133,7 +133,11 @@ func TestTirithGuard_DoesNotCacheFailedResolution(t *testing.T) {
|
|||
|
||||
require.Empty(t, tirithGuard(context.Background(), "echo first", "", cfg))
|
||||
|
||||
writeExecutableFile(t, bin, fakeTirithScript(`printf '{"findings":[{"severity":"HIGH","title":"later install"}]}'`+"\nexit 1\n"))
|
||||
writeExecutableFile(
|
||||
t,
|
||||
bin,
|
||||
fakeTirithScript(`printf '{"findings":[{"severity":"HIGH","title":"later install"}]}'`+"\nexit 1\n"),
|
||||
)
|
||||
got := tirithGuard(context.Background(), "echo second", "", cfg)
|
||||
require.Contains(t, got, "Command blocked by Tirith")
|
||||
require.Contains(t, got, "later install")
|
||||
|
|
@ -151,7 +155,12 @@ exit 0
|
|||
`))
|
||||
t.Setenv("TIRITH_ARGS_FILE", argsFile)
|
||||
|
||||
got := tirithGuard(context.Background(), "echo hello", "", TirithConfig{Enabled: true, BinPath: bin, Timeout: 5, FailOpen: false})
|
||||
got := tirithGuard(
|
||||
context.Background(),
|
||||
"echo hello",
|
||||
"",
|
||||
TirithConfig{Enabled: true, BinPath: bin, Timeout: 5, FailOpen: false},
|
||||
)
|
||||
require.Empty(t, got)
|
||||
|
||||
argsData, err := os.ReadFile(argsFile)
|
||||
|
|
@ -201,7 +210,12 @@ exit 0
|
|||
t.Setenv("GOOGLE_SAFE_BROWSING_API_KEY", "gsb-secret")
|
||||
t.Setenv("ABUSECH_AUTH_KEY", "abusech-secret")
|
||||
|
||||
got := tirithGuard(context.Background(), "echo hello", "", TirithConfig{Enabled: true, BinPath: bin, Timeout: 5, FailOpen: false})
|
||||
got := tirithGuard(
|
||||
context.Background(),
|
||||
"echo hello",
|
||||
"",
|
||||
TirithConfig{Enabled: true, BinPath: bin, Timeout: 5, FailOpen: false},
|
||||
)
|
||||
require.Empty(t, got)
|
||||
|
||||
data, err := os.ReadFile(envFile)
|
||||
|
|
@ -235,10 +249,11 @@ func TestTirithGuard_UsesCommandWorkingDirectory(t *testing.T) {
|
|||
pwd > "$TIRITH_PWD_FILE"
|
||||
printf '{"findings":[]}'
|
||||
exit 0
|
||||
`))
|
||||
`))
|
||||
t.Setenv("TIRITH_PWD_FILE", pwdFile)
|
||||
|
||||
got := tirithGuard(context.Background(), "echo hello", cwd, TirithConfig{Enabled: true, BinPath: bin, Timeout: 5, FailOpen: false})
|
||||
cfg := TirithConfig{Enabled: true, BinPath: bin, Timeout: 5, FailOpen: false}
|
||||
got := tirithGuard(context.Background(), "echo hello", cwd, cfg)
|
||||
require.Empty(t, got)
|
||||
|
||||
data, err := os.ReadFile(pwdFile)
|
||||
|
|
@ -269,8 +284,12 @@ func TestTirithGuard_ExitCodes(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
bin := filepath.Join(t.TempDir(), "tirith")
|
||||
writeExecutableFile(t, bin, fakeTirithScript(`printf '{"findings":[{"severity":"HIGH","title":"Pipe to shell"}]}'`+"\nexit "+itoa(tt.exitCode)+"\n"))
|
||||
got := tirithGuard(context.Background(), "echo hello", "", TirithConfig{Enabled: true, BinPath: bin, Timeout: 5, FailOpen: tt.failOpen})
|
||||
script := `printf '{"findings":[{"severity":"HIGH","title":"Pipe to shell"}]}'` +
|
||||
"\nexit " + itoa(tt.exitCode) + "\n"
|
||||
writeExecutableFile(t, bin, fakeTirithScript(script))
|
||||
|
||||
cfg := TirithConfig{Enabled: true, BinPath: bin, Timeout: 5, FailOpen: tt.failOpen}
|
||||
got := tirithGuard(context.Background(), "echo hello", "", cfg)
|
||||
if tt.wantBlock {
|
||||
require.NotEmpty(t, got)
|
||||
require.Contains(t, got, tt.wantText)
|
||||
|
|
@ -287,7 +306,8 @@ func TestTirithGuard_InvalidJSONKeepsExitVerdict(t *testing.T) {
|
|||
bin := filepath.Join(t.TempDir(), "tirith")
|
||||
writeExecutableFile(t, bin, fakeTirithScript("printf 'not json'\nexit 1\n"))
|
||||
|
||||
got := tirithGuard(context.Background(), "echo hello", "", TirithConfig{Enabled: true, BinPath: bin, Timeout: 5, FailOpen: false})
|
||||
cfg := TirithConfig{Enabled: true, BinPath: bin, Timeout: 5, FailOpen: false}
|
||||
got := tirithGuard(context.Background(), "echo hello", "", cfg)
|
||||
require.Contains(t, got, "Command blocked by Tirith")
|
||||
require.Contains(t, got, "details unavailable")
|
||||
}
|
||||
|
|
@ -298,14 +318,16 @@ func TestTirithGuard_TimeoutUsesFailOpen(t *testing.T) {
|
|||
bin := filepath.Join(t.TempDir(), "tirith")
|
||||
writeExecutableFile(t, bin, fakeTirithScript("sleep 2\nexit 0\n"))
|
||||
|
||||
require.Empty(t, tirithGuard(context.Background(), "echo hello", "", TirithConfig{Enabled: true, BinPath: bin, Timeout: 1, FailOpen: true}))
|
||||
cfg := TirithConfig{Enabled: true, BinPath: bin, Timeout: 1, FailOpen: true}
|
||||
require.Empty(t, tirithGuard(context.Background(), "echo hello", "", cfg))
|
||||
|
||||
got := tirithGuard(context.Background(), "echo hello", "", TirithConfig{Enabled: true, BinPath: bin, Timeout: 1, FailOpen: false})
|
||||
cfg.FailOpen = false
|
||||
got := tirithGuard(context.Background(), "echo hello", "", cfg)
|
||||
require.Contains(t, got, "fail-closed")
|
||||
require.Contains(t, got, "timed out")
|
||||
}
|
||||
|
||||
func TestTirithGuard_TimeoutKillsProcessTree(t *testing.T) {
|
||||
func TestRunTirithCommand_CancelKillsProcessTree(t *testing.T) {
|
||||
skipWindowsProcessTest(t)
|
||||
|
||||
childPIDFile := filepath.Join(t.TempDir(), "child.pid")
|
||||
|
|
@ -318,8 +340,34 @@ wait
|
|||
t.Setenv("TIRITH_CHILD_PID_FILE", childPIDFile)
|
||||
|
||||
start := time.Now()
|
||||
require.Empty(t, tirithGuard(context.Background(), "echo hello", "", TirithConfig{Enabled: true, BinPath: bin, Timeout: 1, FailOpen: true}))
|
||||
require.Less(t, time.Since(start), 5*time.Second)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
cmd := exec.Command(bin)
|
||||
cmd.Env = append(os.Environ(), "TIRITH_CHILD_PID_FILE="+childPIDFile)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
errCh <- runTirithCommand(ctx, cmd)
|
||||
}()
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
_, err := os.Stat(childPIDFile)
|
||||
return err == nil
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
|
||||
cancel()
|
||||
var runErr error
|
||||
require.Eventually(t, func() bool {
|
||||
select {
|
||||
case runErr = <-errCh:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}, 5*time.Second, 100*time.Millisecond)
|
||||
require.Error(t, runErr)
|
||||
require.Less(t, time.Since(start), 7*time.Second)
|
||||
|
||||
data, err := os.ReadFile(childPIDFile)
|
||||
require.NoError(t, err)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue