Keep audit report cleanup owned by the invoking shell

The declared 80-case trace reproduced a stopped watchdog running its inherited EXIT cleanup before classification, deleting a captured critical finding. Guard cleanup by Bash subshell ownership, including Bash 3.2, and exercise the actual hook with a deterministic child-exit control and a rejected legacy hook. Retain parent cleanup and every existing audit verdict and deadline.

Contract-Neutral: Repair captured-report lifetime only; advisory classification, strict and inherited verdicts, invocation scope, retry budgets and external permissions remain unchanged.
Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-06 15:26:35 +01:00
parent c486a2aefb
commit 440348cb14
2 changed files with 48 additions and 1 deletions
@@ -5131,3 +5131,47 @@ func TestNpmAuditNativeScopeArgumentsPreserveFindings(t *testing.T) {
}
}
}
func TestNpmAuditReportCleanupRemainsParentOwned(t *testing.T) {
runner, err := os.ReadFile(repoFile("scripts", "npm-audit-retry.sh"))
if err != nil {
t.Fatal(err)
}
hook := regexp.MustCompile(`(?m)^trap '.*' EXIT$`).FindString(string(runner))
if hook == "" {
t.Fatal("audit report cleanup hook is missing")
}
for _, tc := range []struct {
name, hook string
status int
}{
{"actual_parent_owned_hook", hook, 0},
{"legacy_child_cleanup_rejected", `trap 'rm -f "${report_file}"' EXIT`, 23},
} {
t.Run(tc.name, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "report")
// Install the exact source hook in the child explicitly to model
// the early-signal window, without timing a background process.
// It must retain the report there, and still remove it at parent exit.
script := "report_file=\"$REPORT_FIXTURE\"\nprintf retained >\"$report_file\"\n" + tc.hook +
"\n(\n" + tc.hook + "\n)\ntest -f \"$report_file\" || exit 23\n"
cmd := exec.Command("bash", "-c", script)
cmd.Env = append(os.Environ(), "REPORT_FIXTURE="+path)
output, err := cmd.CombinedOutput()
status := 0
if err != nil {
exit, ok := err.(*exec.ExitError)
if !ok {
t.Fatal(err)
}
status = exit.ExitCode()
}
if status != tc.status {
t.Fatalf("child cleanup verdict=%d want=%d: %s", status, tc.status, output)
}
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Fatalf("invoking shell did not clean up its report: %v", err)
}
})
}
}
+4 -1
View File
@@ -230,7 +230,10 @@ else:
}
report_file="$(mktemp)"
trap 'rm -f "${report_file}"' EXIT
# A watchdog stopped before its subshell initialises can run this inherited
# EXIT trap. Only the invoking shell owns cleanup: the captured verdict must
# remain available after the watchdog is stopped, on Bash 3.2 as well as 5.x.
trap 'if [ "$BASH_SUBSHELL" -eq 0 ]; then rm -f "${report_file}"; fi' EXIT
attempt=1
delay="${DELAY}"