test(security): satisfy tirith CI gates
This commit is contained in:
parent
e753cff0ba
commit
ca7168aa9a
3 changed files with 84 additions and 25 deletions
|
|
@ -937,11 +937,15 @@ type TirithConfig struct {
|
||||||
|
|
||||||
type ExecConfig struct {
|
type ExecConfig struct {
|
||||||
ToolConfig `envPrefix:"PICOCLAW_TOOLS_EXEC_"`
|
ToolConfig `envPrefix:"PICOCLAW_TOOLS_EXEC_"`
|
||||||
|
|
||||||
EnableDenyPatterns bool `json:"enable_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_ENABLE_DENY_PATTERNS"`
|
EnableDenyPatterns bool `json:"enable_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_ENABLE_DENY_PATTERNS"`
|
||||||
AllowRemote bool `json:"allow_remote" env:"PICOCLAW_TOOLS_EXEC_ALLOW_REMOTE"`
|
AllowRemote bool `json:"allow_remote" env:"PICOCLAW_TOOLS_EXEC_ALLOW_REMOTE"`
|
||||||
CustomDenyPatterns []string `json:"custom_deny_patterns" env:"PICOCLAW_TOOLS_EXEC_CUSTOM_DENY_PATTERNS"`
|
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"`
|
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)
|
|
||||||
|
// 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_"`
|
Tirith TirithConfig `json:"tirith" envPrefix:"PICOCLAW_TOOLS_EXEC_TIRITH_"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
// Tirith (https://github.com/sheeki03/tirith) is a terminal security tool
|
||||||
// that scans commands for content-level threats: homograph/punycode URLs,
|
// 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))
|
log.Printf("[tirith] warning: %s", tirithSummarize(jsonOutput))
|
||||||
return ""
|
return ""
|
||||||
case 3:
|
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 ""
|
return ""
|
||||||
default:
|
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 {
|
func tirithScannerFailure(cacheKey string, cfg TirithConfig, err error) string {
|
||||||
message := tirithSanitizeText(err.Error())
|
message := tirithSanitizeText(err.Error())
|
||||||
if cfg.FailOpen {
|
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 ""
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("Tirith security scan failed (fail-closed): %s", message)
|
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)
|
cacheKey = normalizeTirithBin(cacheKey)
|
||||||
key := cacheKey + "\x00" + category + "\x00" + fmt.Sprintf(format, args...)
|
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))
|
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)
|
got := tirithGuard(context.Background(), "echo second", "", cfg)
|
||||||
require.Contains(t, got, "Command blocked by Tirith")
|
require.Contains(t, got, "Command blocked by Tirith")
|
||||||
require.Contains(t, got, "later install")
|
require.Contains(t, got, "later install")
|
||||||
|
|
@ -151,7 +155,12 @@ exit 0
|
||||||
`))
|
`))
|
||||||
t.Setenv("TIRITH_ARGS_FILE", argsFile)
|
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)
|
require.Empty(t, got)
|
||||||
|
|
||||||
argsData, err := os.ReadFile(argsFile)
|
argsData, err := os.ReadFile(argsFile)
|
||||||
|
|
@ -201,7 +210,12 @@ exit 0
|
||||||
t.Setenv("GOOGLE_SAFE_BROWSING_API_KEY", "gsb-secret")
|
t.Setenv("GOOGLE_SAFE_BROWSING_API_KEY", "gsb-secret")
|
||||||
t.Setenv("ABUSECH_AUTH_KEY", "abusech-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)
|
require.Empty(t, got)
|
||||||
|
|
||||||
data, err := os.ReadFile(envFile)
|
data, err := os.ReadFile(envFile)
|
||||||
|
|
@ -238,7 +252,8 @@ exit 0
|
||||||
`))
|
`))
|
||||||
t.Setenv("TIRITH_PWD_FILE", pwdFile)
|
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)
|
require.Empty(t, got)
|
||||||
|
|
||||||
data, err := os.ReadFile(pwdFile)
|
data, err := os.ReadFile(pwdFile)
|
||||||
|
|
@ -269,8 +284,12 @@ func TestTirithGuard_ExitCodes(t *testing.T) {
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
bin := filepath.Join(t.TempDir(), "tirith")
|
bin := filepath.Join(t.TempDir(), "tirith")
|
||||||
writeExecutableFile(t, bin, fakeTirithScript(`printf '{"findings":[{"severity":"HIGH","title":"Pipe to shell"}]}'`+"\nexit "+itoa(tt.exitCode)+"\n"))
|
script := `printf '{"findings":[{"severity":"HIGH","title":"Pipe to shell"}]}'` +
|
||||||
got := tirithGuard(context.Background(), "echo hello", "", TirithConfig{Enabled: true, BinPath: bin, Timeout: 5, FailOpen: tt.failOpen})
|
"\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 {
|
if tt.wantBlock {
|
||||||
require.NotEmpty(t, got)
|
require.NotEmpty(t, got)
|
||||||
require.Contains(t, got, tt.wantText)
|
require.Contains(t, got, tt.wantText)
|
||||||
|
|
@ -287,7 +306,8 @@ func TestTirithGuard_InvalidJSONKeepsExitVerdict(t *testing.T) {
|
||||||
bin := filepath.Join(t.TempDir(), "tirith")
|
bin := filepath.Join(t.TempDir(), "tirith")
|
||||||
writeExecutableFile(t, bin, fakeTirithScript("printf 'not json'\nexit 1\n"))
|
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, "Command blocked by Tirith")
|
||||||
require.Contains(t, got, "details unavailable")
|
require.Contains(t, got, "details unavailable")
|
||||||
}
|
}
|
||||||
|
|
@ -298,14 +318,16 @@ func TestTirithGuard_TimeoutUsesFailOpen(t *testing.T) {
|
||||||
bin := filepath.Join(t.TempDir(), "tirith")
|
bin := filepath.Join(t.TempDir(), "tirith")
|
||||||
writeExecutableFile(t, bin, fakeTirithScript("sleep 2\nexit 0\n"))
|
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, "fail-closed")
|
||||||
require.Contains(t, got, "timed out")
|
require.Contains(t, got, "timed out")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTirithGuard_TimeoutKillsProcessTree(t *testing.T) {
|
func TestRunTirithCommand_CancelKillsProcessTree(t *testing.T) {
|
||||||
skipWindowsProcessTest(t)
|
skipWindowsProcessTest(t)
|
||||||
|
|
||||||
childPIDFile := filepath.Join(t.TempDir(), "child.pid")
|
childPIDFile := filepath.Join(t.TempDir(), "child.pid")
|
||||||
|
|
@ -318,8 +340,34 @@ wait
|
||||||
t.Setenv("TIRITH_CHILD_PID_FILE", childPIDFile)
|
t.Setenv("TIRITH_CHILD_PID_FILE", childPIDFile)
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
require.Empty(t, tirithGuard(context.Background(), "echo hello", "", TirithConfig{Enabled: true, BinPath: bin, Timeout: 1, FailOpen: true}))
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
require.Less(t, time.Since(start), 5*time.Second)
|
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)
|
data, err := os.ReadFile(childPIDFile)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue