mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 09:01:31 +00:00
fix: fail preflight when a gate tool is killed instead of reporting it clean (#633)
A tool whose child dies by signal can still exit 0 through go tool, so govulncheck printed 'ok' after being OOM-killed and preflight passed without a vulnerability scan having run. Route lint, govulncheck and nilaway through scripts/run-gate.sh, which fails on a non-zero exit or on signal-death output.
This commit is contained in:
@@ -259,7 +259,7 @@ quickinstall: install-system install-go install-aws
|
||||
|
||||
lint:
|
||||
@echo "Running golangci-lint..."
|
||||
$(_Q)golangci-lint run ./...
|
||||
$(_Q)scripts/run-gate.sh golangci-lint golangci-lint run ./...
|
||||
@echo " golangci-lint ok"
|
||||
|
||||
fix:
|
||||
@@ -267,13 +267,13 @@ fix:
|
||||
|
||||
govulncheck:
|
||||
@echo "Running govulncheck..."
|
||||
$(_Q)go tool govulncheck ./...
|
||||
$(_Q)scripts/run-gate.sh govulncheck go tool govulncheck ./...
|
||||
@echo " govulncheck ok"
|
||||
|
||||
# NilAway — advisory nil-panic analysis. Not in preflight due to false positives
|
||||
nilaway:
|
||||
@echo "Running nilaway..."
|
||||
$(_Q)go tool nilaway -include-pkgs=github.com/mulgadc/spinifex -exclude-test-files ./...
|
||||
$(_Q)scripts/run-gate.sh nilaway go tool nilaway -include-pkgs=github.com/mulgadc/spinifex -exclude-test-files ./...
|
||||
@echo " nilaway ok"
|
||||
|
||||
# Build release tarballs — use distro-ARCH for single arch, distro for both
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// Package preflightgate tests the preflight gate wrapper. It exists as a Go
|
||||
// test so `go test ./...` exercises it; the repo's shell suites are not yet
|
||||
// wired into CI.
|
||||
package preflightgate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// scriptPath resolves scripts/run-gate.sh relative to this test file, so the
|
||||
// test does not depend on the working directory.
|
||||
func scriptPath(t *testing.T) string {
|
||||
t.Helper()
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatal("runtime.Caller failed")
|
||||
}
|
||||
return filepath.Join(filepath.Dir(thisFile), "..", "..", "scripts", "run-gate.sh")
|
||||
}
|
||||
|
||||
func runGate(t *testing.T, args ...string) (string, int) {
|
||||
t.Helper()
|
||||
cmd := exec.Command(scriptPath(t), args...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
return string(out), 0
|
||||
}
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
return string(out), exitErr.ExitCode()
|
||||
}
|
||||
t.Fatalf("running run-gate.sh: %v", err)
|
||||
return "", -1
|
||||
}
|
||||
|
||||
func TestRunGate_CleanRunPasses(t *testing.T) {
|
||||
out, code := runGate(t, "govulncheck", "echo", "No vulnerabilities found.")
|
||||
if code != 0 {
|
||||
t.Fatalf("expected exit 0 for a clean run, got %d\n%s", code, out)
|
||||
}
|
||||
if !strings.Contains(out, "No vulnerabilities found.") {
|
||||
t.Errorf("tool output should be forwarded, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunGate_NonZeroExitFails(t *testing.T) {
|
||||
out, code := runGate(t, "lint", "sh", "-c", "echo 3 issues.; exit 1")
|
||||
if code == 0 {
|
||||
t.Fatalf("expected failure for a non-zero tool exit, got 0\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "lint FAILED") {
|
||||
t.Errorf("failure should name the gate, got:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// The defect this guards: a signal-killed scanner exiting 0 was reported clean,
|
||||
// so preflight passed without a vulnerability scan having run.
|
||||
func TestRunGate_SignalKilledWithZeroExitFails(t *testing.T) {
|
||||
for _, sig := range []string{"killed", "terminated", "aborted"} {
|
||||
t.Run(sig, func(t *testing.T) {
|
||||
out, code := runGate(t, "govulncheck", "sh", "-c",
|
||||
"echo 'go tool govulncheck: signal: "+sig+"'; exit 0")
|
||||
if code == 0 {
|
||||
t.Fatalf("expected failure when the tool died by signal, got 0\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "did NOT complete") {
|
||||
t.Errorf("failure should say the gate did not complete, got:\n%s", out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// "signal" appearing in ordinary findings must not fail an otherwise clean run.
|
||||
func TestRunGate_SignalWordInOutputDoesNotFail(t *testing.T) {
|
||||
out, code := runGate(t, "lint", "echo", "checked signal handling in 12 files")
|
||||
if code != 0 {
|
||||
t.Fatalf("expected exit 0, got %d\n%s", code, out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunGate_RequiresLabelAndCommand(t *testing.T) {
|
||||
out, code := runGate(t, "govulncheck")
|
||||
if code != 2 {
|
||||
t.Fatalf("expected usage exit 2, got %d\n%s", code, out)
|
||||
}
|
||||
}
|
||||
Executable
+35
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs a preflight gate and fails unless it actually completed.
|
||||
# A tool whose child is killed by a signal can still exit 0 through `go tool`,
|
||||
# which would let a security or lint gate pass without having run.
|
||||
set -uo pipefail
|
||||
|
||||
if [ "$#" -lt 2 ]; then
|
||||
echo "usage: run-gate.sh <label> <command> [args...]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
label=$1
|
||||
shift
|
||||
|
||||
output=$("$@" 2>&1)
|
||||
rc=$?
|
||||
|
||||
if [ -n "$output" ]; then
|
||||
printf '%s\n' "$output"
|
||||
fi
|
||||
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
echo " ${label} FAILED (exit ${rc})" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# A zero exit alongside a signal death means the gate never produced a verdict,
|
||||
# so it must not be reported as clean.
|
||||
if printf '%s' "$output" | grep -qE 'signal: (killed|terminated|aborted|segmentation fault)'; then
|
||||
echo " ${label} did NOT complete: the tool was killed by a signal, so this is not a clean result" >&2
|
||||
echo " re-run it with more memory available before trusting a pass" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user