mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-11 23:40:28 -10:00
* Bump toolchain to go1.26.4 to clear reachable stdlib CVEs go.mod pinned toolchain go1.24.13, whose bundled standard library has 9 govulncheck-reachable CVEs (crypto/tls KeyUpdate DoS, net/http2 loop, crypto/x509 x3, net, net/url, os, net/textproto). CI and release build via go-version-file: go.mod, so shipped binaries inherited them. Bumping the toolchain directive to go1.26.4 clears all nine — govulncheck ./... now reports 0 reachable (minimum that clears them is go1.25.11; go1.26.4 matches the team's local Go). The go directive stays at 1.24.2 (minimum language version). Verified: go build/vet/gofmt clean, full go test ./... green. * Add govulncheck CI gate and SHA-pin GitHub Actions - ci.yml: new 'security' job runs govulncheck as a hard gate (fails on a reachable vulnerability; passes now that the toolchain is bumped) plus an advisory deadcode step that surfaces dormant code without blocking. - Pin every GitHub Action to a full commit SHA (checkout, upload-artifact, github-script) with a version comment, across all four workflows — previously only setup-go was pinned, leaving the mutable v4/v7 tags as a supply-chain risk. * Add hooks management command (add/remove/enable/disable) Wires the previously-dormant hooks.ConfigStore (10 unreachable funcs -> 0) into the CLI: zero hooks add <id> --event <event> --command <cmd> [--name --description --matcher --arg --user --json] zero hooks remove|enable|disable <id> [--user --json] Writes the project hook config by default (<cwd>/.zero/hooks.json) or the user config with --user, reusing the store's locking + atomic writes + validation (normalizeDefinition). New hooks are enabled; state is managed via enable/disable. JSON output is secret-scrubbed via redaction. Mirrors the existing 'zero mcp add/remove/enable/disable' shape. Covered by add/remove/toggle round-trip, validation, unknown-event, and JSON-redaction tests. * Fix govulncheck CI job: pin GOTOOLCHAIN to the go.mod toolchain go run govulncheck@latest selected the toolchain from govulncheck's own go.mod (downgrading to go1.25.11), which then could not load our go1.26-requiring packages (fips140only_go1.26.go), failing the security job. Resolve GOTOOLCHAIN from go.mod's toolchain line so govulncheck and deadcode both run under go1.26.4. * Address review: harden CI checkout, pin tool versions, validate hook event CodeRabbit findings on #193: - ci.yml: persist-credentials: false on all checkout steps (smoke/performance/security) — these jobs run repo code and never push, so don't keep the token in git config. - ci.yml: pin [email protected] and [email protected].0 instead of @latest, so the security gate is deterministic and not subject to supply-chain drift. - hooks: add exported IsValidEvent/KnownEvents (single source); parseEvent reuses it, and 'zero hooks add' now rejects an invalid --event with a usage error at parse time instead of falling through to the Upsert app-error crash path.
148 lines
4.9 KiB
Go
148 lines
4.9 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Gitlawb/zero/internal/hooks"
|
|
)
|
|
|
|
func hooksManageDeps(cwd string) appDeps {
|
|
return appDeps{getwd: func() (string, error) { return cwd, nil }}
|
|
}
|
|
|
|
func projectHooks(t *testing.T, cwd string) hooks.Config {
|
|
t.Helper()
|
|
paths, err := hooks.ResolvePaths(hooks.ResolvePathOptions{Cwd: cwd})
|
|
if err != nil {
|
|
t.Fatalf("ResolvePaths: %v", err)
|
|
}
|
|
store, err := hooks.NewConfigStore(hooks.StoreOptions{ConfigPath: paths.ProjectConfigPath})
|
|
if err != nil {
|
|
t.Fatalf("NewConfigStore: %v", err)
|
|
}
|
|
config, err := store.List()
|
|
if err != nil {
|
|
t.Fatalf("List: %v", err)
|
|
}
|
|
return config
|
|
}
|
|
|
|
func TestRunHooksAddPersistsToProjectConfig(t *testing.T) {
|
|
cwd := t.TempDir()
|
|
var stdout, stderr bytes.Buffer
|
|
code := runHooksAdd([]string{"zero.preflight", "--event", "beforeTool", "--matcher", "bash", "--command", "sh", "--arg", "-c", "--arg", "echo hi"}, &stdout, &stderr, hooksManageDeps(cwd))
|
|
if code != exitSuccess {
|
|
t.Fatalf("exit = %d, want %d; stderr=%q", code, exitSuccess, stderr.String())
|
|
}
|
|
config := projectHooks(t, cwd)
|
|
if len(config.Hooks) != 1 {
|
|
t.Fatalf("hooks = %d, want 1", len(config.Hooks))
|
|
}
|
|
hook := config.Hooks[0]
|
|
if hook.ID != "zero.preflight" || hook.Event != hooks.EventBeforeTool || hook.Command != "sh" {
|
|
t.Fatalf("unexpected hook: %+v", hook)
|
|
}
|
|
if !hook.Enabled {
|
|
t.Fatalf("new hook should be enabled: %+v", hook)
|
|
}
|
|
if strings.Join(hook.Args, " ") != "-c echo hi" {
|
|
t.Fatalf("args = %v, want [-c, echo hi]", hook.Args)
|
|
}
|
|
}
|
|
|
|
func TestRunHooksAddRejectsMissingRequiredFlags(t *testing.T) {
|
|
cwd := t.TempDir()
|
|
for _, tc := range []struct {
|
|
name string
|
|
args []string
|
|
}{
|
|
{"no event", []string{"h1", "--command", "sh"}},
|
|
{"no command", []string{"h1", "--event", "beforeTool"}},
|
|
{"no id", []string{"--event", "beforeTool", "--command", "sh"}},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var stdout, stderr bytes.Buffer
|
|
if code := runHooksAdd(tc.args, &stdout, &stderr, hooksManageDeps(cwd)); code == exitSuccess {
|
|
t.Fatalf("expected non-success exit for %q", tc.name)
|
|
}
|
|
if len(projectHooks(t, cwd).Hooks) != 0 {
|
|
t.Fatalf("no hook should be written on a usage error")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunHooksAddRejectsUnknownEvent(t *testing.T) {
|
|
cwd := t.TempDir()
|
|
var stdout, stderr bytes.Buffer
|
|
if code := runHooksAdd([]string{"h1", "--event", "bogusEvent", "--command", "sh"}, &stdout, &stderr, hooksManageDeps(cwd)); code == exitSuccess {
|
|
t.Fatalf("expected non-success exit for an unknown event; stderr=%q", stderr.String())
|
|
}
|
|
if len(projectHooks(t, cwd).Hooks) != 0 {
|
|
t.Fatalf("no hook should be written when the event is invalid")
|
|
}
|
|
}
|
|
|
|
func TestRunHooksAddJSONRedactsSecretArgs(t *testing.T) {
|
|
cwd := t.TempDir()
|
|
secret := "sk-proj-" + strings.Repeat("z", 24)
|
|
var stdout, stderr bytes.Buffer
|
|
code := runHooksAdd([]string{"h1", "--event", "beforeTool", "--command", "sh", "--arg", "-c", "--arg", "echo " + secret, "--json"}, &stdout, &stderr, hooksManageDeps(cwd))
|
|
if code != exitSuccess {
|
|
t.Fatalf("exit = %d, want %d; stderr=%q", code, exitSuccess, stderr.String())
|
|
}
|
|
if strings.Contains(stdout.String(), secret) || strings.Contains(stdout.String(), "sk-proj-") {
|
|
t.Fatalf("JSON output must redact secret args, got %q", stdout.String())
|
|
}
|
|
if !strings.Contains(stdout.String(), "[REDACTED]") {
|
|
t.Fatalf("expected a redaction marker in JSON output, got %q", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestRunHooksToggleAndRemove(t *testing.T) {
|
|
cwd := t.TempDir()
|
|
deps := hooksManageDeps(cwd)
|
|
var out, errBuf bytes.Buffer
|
|
if code := runHooksAdd([]string{"h1", "--event", "afterTool", "--command", "sh"}, &out, &errBuf, deps); code != exitSuccess {
|
|
t.Fatalf("add exit = %d; stderr=%q", code, errBuf.String())
|
|
}
|
|
|
|
// disable
|
|
out.Reset()
|
|
errBuf.Reset()
|
|
if code := runHooksToggle([]string{"h1"}, &out, &errBuf, deps, true); code != exitSuccess {
|
|
t.Fatalf("disable exit = %d; stderr=%q", code, errBuf.String())
|
|
}
|
|
if projectHooks(t, cwd).Hooks[0].Enabled {
|
|
t.Fatalf("hook should be disabled after disable")
|
|
}
|
|
|
|
// enable
|
|
if code := runHooksToggle([]string{"h1"}, &out, &errBuf, deps, false); code != exitSuccess {
|
|
t.Fatalf("enable exit = %d; stderr=%q", code, errBuf.String())
|
|
}
|
|
if !projectHooks(t, cwd).Hooks[0].Enabled {
|
|
t.Fatalf("hook should be enabled after enable")
|
|
}
|
|
|
|
// toggle of an unknown id is a usage error
|
|
if code := runHooksToggle([]string{"missing"}, &out, &errBuf, deps, true); code == exitSuccess {
|
|
t.Fatalf("disabling an unknown hook should fail")
|
|
}
|
|
|
|
// remove
|
|
if code := runHooksRemove([]string{"h1"}, &out, &errBuf, deps); code != exitSuccess {
|
|
t.Fatalf("remove exit = %d; stderr=%q", code, errBuf.String())
|
|
}
|
|
if len(projectHooks(t, cwd).Hooks) != 0 {
|
|
t.Fatalf("hook should be gone after remove")
|
|
}
|
|
|
|
// removing again is a no-op success
|
|
if code := runHooksRemove([]string{"h1"}, &out, &errBuf, deps); code != exitSuccess {
|
|
t.Fatalf("removing a missing hook should succeed as a no-op, got %d", code)
|
|
}
|
|
}
|