Repository navigation
fix(redaction): fail a cut-short redaction on every sink, keep partials - #813
Conversation
🔍 Ferret Scan ResultsGate mode:
Findings Summary:
Next Steps:
🤖 This comment was automatically generated by Ferret Scan |
When coverage was cut short -- a caller's ctx done or firing, a validator budget, the CLOUD_RESOURCES size cap, an unparseable file -- every redaction path but --file returned partial or unredacted output as success. Measured on v2.5.5 (c403e27): --stdin --enable-redaction exited 0 under --fail-on-incomplete (1ns budget, a 5.8 MB log over the cap, SSN=300ms on 20,000 records) and printed 0 B in pre-commit mode; Engine.Redact returned err == nil and the input unredacted for a pre-cancelled ctx and for 1, 100 and 200 ms deadlines (0 of 6,000 findings); RedactFile copied an unparseable .pdf byte-identical to the output path, and the over-cap log with 5,518/5,518 ARN lines in clear; ScanText/ScanFile ignored their ctx (30,000/30,000 findings under a pre-cancelled one). Sinks (the --file rule: output never beyond --file's, disclosure always, exit 3 under the flag / an error wrapping ErrIncomplete): - pkg/redact: a done ctx at entry or any validator error returns the partial Result (Incomplete, IncompleteReason, also on AuditRecord) and an error wrapping the new redact.ErrIncomplete and the cause. - pkg/scan.RedactFile and the new RedactFileContext: a file in any of the three --fail-on-incomplete channels gets no passthrough copy, and an error wrapping the new scan.ErrIncomplete with RedactFileResult.Coverage (a pointer: the struct stays comparable). core.RedactFile does the same with core.ErrRedactionIncomplete; ScanFile and RedactFile share coverageOf. - --stdin: both exits go through resolveIncompleteExitCode; pre-commit mode prints a one-line notice. - examples/lambda-redact maps ErrIncomplete to "redaction incomplete"; the README snippets check err. Caller ctx: core.ScanContentCtx/ScanFileCtx/RedactFileCtx and ProcessFilesWithProgressCtx root the job contexts at the caller's (the pool's own context stays the pool's, or a cancellation would drop jobs the collector waits for); ScanText/ScanFile return (nil, ctx.Err()) at entry and the partial result with an error wrapping ctx.Err() mid-scan; /scan passes the request's. Partials: keepMatches uses execguard.IsCoverageCutShort (it tested ErrMatchBudgetExceeded alone); a kept partial is scored and annotated before it is sent; both fan-outs keep collecting for a bounded grace period after Done (100 ms RunValidators, 50 ms the nested document fan-out); the worker pool never retries a guard outcome; ScanContent uses the predicate (its chain omitted ErrValidatorPanicked and named the size cap twice) and fills NotExamined. A go/ast guard derives the family from IsCoverageCutShort and fails on any errors.Is hand copy outside internal/execguard; on main it finds 5 calls in 2 sites. records.txt, SSN,EMAIL, SSN=1s: SSN findings 0 -> 8,704 (each equal to its unbudgeted twin, metadata included), CPU 7.36-7.44 s -> 3.22-3.29 s, email_validator passes 3 -> 1. Complete runs are byte-identical: golden corpus 0 files changed, make score identical, rules generated by main still suppress every finding, kept partials included. Closes #770
b8998b3 to
d5a395c
Compare
The rebase onto main (now carrying #811 and #799) auto-merged the #765 bullet into the Security section below the Go 1.27.3 entry; it is a bug fix, so it moves under Bug Fixes, as its first entry: #813 appends its own bullet after the #773 entry, and two PRs inserting at one seam would conflict again at the second merge; distinct anchors merge in either order. corpus.go: both #811 (overlap_groups_render_once) and #765 (the two line-veto cases) appended to the case table; keep all three in that order. The metadata key census is regenerated on top of main: the only movement is +14 findings at 13 keys, which is the secrets_line_veto_triggers case; no other bucket and no existing snapshot changed, and the two new snapshots are byte-identical to the ones committed before the rebase.
|
Rebased onto
|
🔍 Ferret Scan ResultsGate mode:
Findings Summary:
Next Steps:
🤖 This comment was automatically generated by Ferret Scan |
…812) * fix(secrets): scope the line pre-check veto to benign manifest lines containsSecretIndicators, the pre-check in front of the SECRETS keyword pass, ran three whole-line substring checks before its provider-literal admits: a version or boolean after the line's first colon, an "integrity"/"checksum" key beside the bytes sha or md5, and a "url"/"resolved"/"homepage" key beside a scheme and a registry or forge host. A hit dropped the whole line, so one "url" member hid an AKIA key ID, a ghp_/glpat-/xoxb- token, a JWT or a keyword-assigned password anywhere else on it: no finding, no redacted copy, --pre-commit-mode exit 0, and the --stdin redaction gateway passed the token through. The provider-literal admits (JWT, every provider prefix, PEM markers) now run first, and the three checks are replaced by one veto, benignManifestLine, that rejects a line only when (a) it is exactly one JSON member whose value is a version under 20 bytes, a boolean, an SRI digest or a URL with no user info, query or '=', (b) no keyword stem from the lists the keyword patterns are compiled from appears anywhere on it, and (c) one of the old checks fires as well, so the veto is a subset of what was skipped before. Checking only the key, as the issue's prototype does, is unsound: in "https://api.session:8443/..." the unquoted pattern reads `session:` as an assignment. findKeywordSecrets is split into the gate and keywordCandidates so that the gate's contract (a rejected line has no candidate) is tested. Measured against v2.5.5 on identical inputs: the issue's nine triggers report what their controls report (30 of 30 draws); matrix 1 goes from 99 to 0 of 143 trigger cells reporting nothing (controls 78/78 on both); every matrix 2 cell, as low as 0/40 before, is 40/40; the redacted copies, pre-commit exits and stdin gateway are fixed. No existing finding changes type, text, line or confidence on the matrices, the reproduction, 39 public npm lockfiles or 3,691 public files that hit an old check, and main's suppression rules still suppress all of them. The veto still skips 99.1% of the lockfile lines the old checks skipped; SECRETS CPU on those lockfiles is 3-6% above v2.5.5 (with no veto at all, 2.7x). Closes #765 * test(goldencorpus): state the #765 trigger case's password length right The fifth line of secrets_line_veto_triggers carries a 41-byte password, not the 40-byte one its description named. The value stays 41 bytes: a 40-byte value beside an AKIA key ID is the shape secret scanners read as an AWS key pair. Description only; no snapshot changes. Refs #765 * docs(changelog): count all 31 seeded draws in the #765 entry The reproduction ran on seeds 1-31; the entry quoted the 30 draws of seeds 2-31. On all 31 every trigger reports what its control reports, and main lost the 40-character password on 10 of them. Refs #765 * chore(goldencorpus,changelog): rebase #765 onto main after #811 The rebase onto main (now carrying #811 and #799) auto-merged the #765 bullet into the Security section below the Go 1.27.3 entry; it is a bug fix, so it moves under Bug Fixes, as its first entry: #813 appends its own bullet after the #773 entry, and two PRs inserting at one seam would conflict again at the second merge; distinct anchors merge in either order. corpus.go: both #811 (overlap_groups_render_once) and #765 (the two line-veto cases) appended to the case table; keep all three in that order. The metadata key census is regenerated on top of main: the only movement is +14 findings at 13 keys, which is the secrets_line_veto_triggers case; no other bucket and no existing snapshot changed, and the two new snapshots are byte-identical to the ones committed before the rebase. --------- Co-authored-by: union <union@example.invalid>
🔍 Ferret Scan ResultsGate mode:
Findings Summary:
Next Steps:
🤖 This comment was automatically generated by Ferret Scan |
What
When coverage is cut short — the caller's context is done or fires, a
--validator-budgetfires,CLOUD_RESOURCESdeclines input over 5 MiB, a file does not parse — every redaction path now follows the rule--filealready followed: the output never goes beyond--file's, the cut-short is always disclosed, and it is signalled (exit 3 under--fail-on-incomplete; for a Go caller, an error wrappingErrIncompletewith the partial result alongside).For someone running ferret-scan:
--stdin --enable-redaction(the gateway) exits 3 under--fail-on-incompleteon a cut-short scan, as--filedoes; it exited 0 whatever the flag said. In pre-commit mode--stdinnow prints a one-line notice (ferret-scan: <stdin> NOT fully examined (coverage cut short) — …); it printed 0 bytes.pkg/redact.Engine.Redactreturns an error wrapping the newredact.ErrIncomplete(and the cause, soerrors.Is(err, context.DeadlineExceeded)holds) together with the partialResult(Incomplete,IncompleteReason, both also onAuditRecord). It returned the input unredacted aterr == nilfor a pre-cancelled context, for 1/100/200 ms deadlines and for a >5 MiBCLOUD_RESOURCESrequest.pkg/scan.RedactFileand the newRedactFileContextreturn an error wrapping the newscan.ErrIncompletewithRedactFileResult.Coverageset, and never write the clean-file passthrough for a file that was not fully examined. An unparseable.pdfused to come back byte-identical at the output path, aterr == nil.pkg/scan.ScanText/ScanFilehonour their context:(nil, ctx.Err())when it is done at entry; the partialResultand an error wrappingctx.Err()when it fires mid-scan. They ignored it (_ context.Context).ScanTextnow fillsNotExamined.examples/lambda-redactanswersrequest_id=…: redaction incompleteinstead of returning the request's own text asredacted; the README library snippets checkerr.Complete runs are unchanged: golden corpus 0 files changed,
make scoreidentical, unbudgeted results byte-identical main vs branch.Which of the issue's 12 items #797, #791 and #796 already covered
None completely; I checked each in
c403e27before writing code.c403e27Engine.RedactrunErrstill dropped (engine.go)RedactFilecore.RedactFilestill checked onlyUnredactedFiles, then passed through. #791 added only the strict strategy check before any write--stdinrunStdinRedactionstill returned 0 / the pre-commit code withoutresolveIncompleteExitCode; the pre-commit warning was still dropped. #791/#796 touched only strategy/profile handling thereScanContentand the worker pool still rooted oncontext.Background(),/scanpassed noneScanText(_ context.Context…),ScanFile(_ …)NotExamined, prefix once,RedactFileContextScanConfighas noValidatorBudgets" — #797 addedcore.ScanConfig.ValidatorBudgets(pkg/scan options still have none)keepMatcheserrors.Is(r.err, execguard.ErrMatchBudgetExceeded)(nowdual_path_bridge.go:1010-1012). #797 moved the routed-path, both-paths-failed, legacy and metadata branches toIsCoverageCutShort, but not this gatedefault:ScanContent's hand chain unchanged (omittingErrValidatorPanicked). #797 added ago/astderivation of the family for its own table test, which this guard reuses as a patternVerification
Measured: shared
mainbinary built fromc403e27vs this branch. Inputs from the issue's recipe, regenerated by the script below:records.txt2,177,780 B (matches the issue),infra.log5,810,529 B with 5,518 ARN lines (the issue's padding is unspecified; it is 5,734,445 B there; both are over the 5,242,880 B cap),broken.pdf,one.txt. Empty config home, absolute paths, Linux, 16 cores.gen_inputs.py
Matrix A — surfaces on cut-short inputs
CLI runs:
--enable-redaction --format json --output findings.json(the twodetectrows omit--enable-redaction). Exit: without / with--fail-on-incomplete.--file--stdin--filecopy)--file--stdinall=1ns--fileall=1ns--stdinall=1ns,--pre-commit-mode--fileall=1ns,--pre-commit-mode--stdinSSN--fileCLOUD_RESOURCES,EMAIL--fileCLOUD_RESOURCES,EMAIL--stdin--filecopy (main and branch)SSN,SSN=300ms--stdin--limit)all=1ns(detect)--stdinall=1ns,--pre-commit-mode(detect)--stdinThe stdin stderr shrinks by 17–18 B because the reason now reads
coverage cut short: context deadline exceeded(wasvalidator execution did not complete: …). The stdin JSONstatsblock (stats.files_not_examined) is not in this PR — see Deviations (U33).Library calls (
context.Background()unless noted)RedactFile(broken.pdf, SSN)err=nil, count 0, output = inputscan: redaction incomplete: …/broken.pdf: cannot parse: Text Extractor: no text extracted from .pdf: …; no output file;Coverage{Incomplete:true, NotExamined:[{…/broken.pdf, cannot parse, …}]}RedactFile(infra.log, CLOUD_RESOURCES,EMAIL)err=nil, count 1, 5,518 ARN lines in clearErrIncomplete(…: coverage cut short: validator declined oversize content: cloud-resource scan skipped, content is 5810529 bytes against a 5242880-byte cap); count 1; the partial copy is kept (e-mail masked, ARNs in clear), as--filewrites it;CoveragesetRedactFile(infra.log, CLOUD_RESOURCES)err=nil, count 0, passthrough = inputErrIncomplete; no output fileRedactFile(one.txt),RedactFile(clean.txt)err=nilerr=nil,Coverage == nil(clean passthrough unchanged)Engine.Redact(infra.log, CLOUD_RESOURCES)err=nil, 0 findings,Redacted == inputredact: redaction incomplete: validator declined oversize content: …(errors.IsErrIncomplete);Incomplete,AuditRecord().IncompletetrueEngine.Redact, 2,000 records,Background()err=nilerr=nil, 0 findings,Redacted == inputErrIncomplete+context.Canceled/DeadlineExceeded, 0 findings,Incompleteerr=nil, 0 findings,Redacted == inputErrIncomplete+DeadlineExceeded, 4,768 / 5,536 findings kept (the grace drain)err=nilScanText(infra.log, CLOUD_RESOURCES)err=nil,Incomplete,len(NotExamined)=0, reasonvalidator declined oversize content: validator declined oversize content: …err=nil(an engine guard),Incomplete,NotExamined=[{<text>, coverage cut short, …}], reasoncoverage cut short: validator declined oversize content: …(once)ScanText/ScanFile, 10,000 records, pre-cancelledIncomplete=falsenil, context.CanceledIncomplete=falseIncomplete,NotExaminedcut short, 0 findings) +scan: cut short by the caller's context: context deadline exceededCancellation leaves work running.
Engine.Redacton 20,000 lines, 50 ms deadline, 5 samples: main returns at 50 ms with 0 findings and burns 0.213–0.221 s CPU in the next 4 s; the branch returns at 150 ms (deadline +CancelGrace) with 0 findings and an error, then 0.111–0.120 s. The issue reads that post-return CPU as validators' work being thrown away; on this host it is mostly the document context analysis that runs before any validator and cannot be interrupted (AnalyzeContextonrecords.txt: 262–264 ms, measured with a throwaway probe). So the grace period recovers nothing at that size; it recovers findings when validators are what is running (the 100/200 ms rows above).Matrix C — fired budgets (
--file records.txt --format json --confidence all --limit 0, 3 interleaved rounds, 1-min load 1.8–4.5)--checksfiles_not_examinedSSN=300msSSN=1sSSN=1.5sSSN=3s/20s(does not fire)SSN=300msSSN=1sAcceptance rows, measured with
--show-matchon the branch:SSNSSN=1s8,960/8,960;SSN,EMAILSSN=1s8,448/8,448;SSN,EMAILSSN=300ms2,560/2,560. (The issue's A/B saw each partial score 20 points under its twin:records.txtis classified CSV, worth +20, and the partials were sent before that adjustment.)--debug,SSN,EMAIL,SSN=300ms:email_validatorvalidate_contentrecords main 3 → branch 1 (no budget: 1 → 1);enhanced_validator_bridgeprocess_content3 → 1.--fail-on-incompleteon the fired budget: rc 3 on both.Acceptance criteria
Engine.Redact(pre-cancelled ctx, past deadline, >5 MiBCLOUD_RESOURCES):errors.Is(err, redact.ErrIncomplete),Result.IncompleteandAuditRecord().Incompletetrue, no input bytes in reason, error or audit record (pkg/redact/incomplete_test.go);examples/lambda-redactreturns an error and an emptyResponse(handler_test.go, build tagexamples_lambda).RedactFile(unparseable.pdf; >5 MiBCLOUD_RESOURCES-only log):errors.Is(err, scan.ErrIncomplete),res.Coverage.Incomplete, no file at the output path; complete runres.Coverage == nil.all=1ns(also--pre-commit-mode) and infra.log:--stdinand--fileredaction exit 0 without the flag, 3 with it, and disclose on stderr — done. Stdin JSONstats.files_not_examined == 1— not done, waits for U33 as the issue specifies.ScanText/ScanFile: pre-cancelled →nil, context.Canceled; mid-scan →Incomplete,NotExamined[0].Cause == "coverage cut short", error wrappingctx.Err(); size-cappedScanText→ non-emptyNotExamined, the guard named once.scan.RedactFileContextexists,RedactFilekeeps its signature,RedactFileResultstays comparable (TestRedactFileResultStaysComparable:Comparable()and a map key; there is no U38 snapshot yet),/scanpasses the request's context.SSN,EMAIL, a budget that fires: non-zero SSN findings, each identical to the unbudgeted run;--debug1email_validatorrecord (was 3); exit 3 under the flag.DeadlineExceededyields N findings with confidence and metadata equal to the nil-error case (TestCutShortPartialsAreKeptAndScoredLikeTheirTwins, every member of the family that can carry matches).errors.Is(err, execguard.ErrMatchBudgetExceeded)is added outsideexecguard(below).make pr-checklist: see Gates.Tests, and that each fails on
mainTrack C tests ran in this worktree with
main'sdual_path_bridge.goandvalidator_runner.gorestored (git show c403e27:…) and two one-line constant shims so they compile. The rest ran in a pristine copy ofc403e27, as is or, where the test names API this PR adds, as a probe with the same scenario and main's API (anErrIncompletecheck becomeserr != nil).maininternal/validatorsTestCutShortPartialsAreKeptAndScoredLikeTheirTwinsIsCoverageCutShort's source (#797's helper);ErrValidatorPanickedexempt (a panic carries no matches)kept 0 of the 7 matches the validator returned with context deadline exceeded(andcontext canceled,validator declined oversize content);ErrMatchBudgetExceeded:partial confidence 60, metadata keys []vstwin confidence 80, metadata keys [confidence_adjustment context_confidence context_doctype context_domain original_confidence semantic_context validation_path]TestLatePartialAfterCancellationIsCollectedkept 0 of the 4 matches sent 10 ms after the cancellationTestValidatorThatNeverReturnsIsAbandonedWithinTheGraceinternal/parallelTestGuardOutcomeIsNotRetriedthe cut check ran 3 time(s) and its sibling 3 (in 3.149s); want 1 and 1;got 0 CUT and 1 SIB matches; want 5 and 1TestCutShortPartialsSurviveRunValidatorsRunValidators returned 0 of the 9 matches the validator made before its budget firedTestRunValidatorsCollectsALatePartialRunValidatorskept 0 of the 3 matches sent 10 ms after the cancellationTestTransientErrorIsStillRetried,TestRunValidatorsAbandonsAValidatorThatNeverReturns,TestDocumentGraceEndsInsideTheRunnerGraceinternal/execguardTestNoHandCopiesOfTheCutShortFamily(+TestTheGuardFindsWhatItIsFor,TestFamilyFromSourceMatchesThePredicate)5 errors.Is test(s) name a member of the cut-short family outside internal/execguard: internal/core/scanner.go:444:30 context.DeadlineExceeded, :444:84 context.Canceled, :447:36 …ErrMatchBudgetExceeded, :452:36 …ErrContentTooLarge, internal/validators/dual_path_bridge.go:1011:47 …ErrMatchBudgetExceeded. On the branch witherrors.Is(err, execguard.ErrMatchBudgetExceeded)injected intointernal/core: fails naming itinternal/coreTestScanContentReportsARecoveredPanicafter a recovered panic: Incomplete=false NotExamined=[]TestScanContentNamesASizeCapOnceAndInNotExaminedguard named 2 times, NotExamined=[]TestRedactFileReportsAFileItDidNotFullyExamineerr=<nil>, artifact written, identical to input: trueTestScanContentCtxKeepsWhatWasFoundWhenTheCallerCancelsMidScan,TestRedactFileCtxWithADoneContextWritesNothingpkg/redactTestRedactReportsADoneContext,…AnOversizeDecline,…AHardValidatorError,TestRedactAppliesThePartialFindingspre-cancelled: err = <nil>, Redacted == input: true;past deadline: …;err = <nil>; findings 0; Redacted == input: true;err = <nil> for a validator that failed;err = <nil>; Redacted "first [EMAIL-REDACTED] then b@example.org"pkg/scanTestScanTextAndScanFileReturnTheCtxErrorAtEntryScanText(pre-cancelled): res=true (findings 1) err=<nil>; same forScanFileTestScanTextAndScanFileReturnThePartialResultWhenTheCtxFiresMidScanerr=<nil> Incomplete=false findings=1TestAnEngineGuardIsNotTheCallersCancellationNotExamined=[] IncompleteReason="validator declined oversize content: validator declined oversize content: …"TestRedactFileReportsAFileItDidNotFullyExamineerr=<nil>, count=0, output identical to input: true; log + address:err=<nil>, count=1TestRedactFileContextWithADoneContextWritesNothing,TestWithCallerCancellation,TestCompleteRedactFileHasNoCoverage,TestRedactFileResultStaysComparablecmdTestCutShortRedactionFollowsTheFileRule(size cap andPHONE=1ns, both clock-independent)--stdin --enable-redaction rc 0, want 3 (the --file rule);budget, with flag: rc 0, want 3;pre-commit stderr has no cut-short notice (it printed 0 B)(×4);--stdin stderr does not disclose the cut-short(main's warning does not name the cause:validator declined oversize content: validator declined oversize content: …).budget, without flagpasses on mainexamples/lambda-redactTestIncompleteRedactionIsAnErrorNotARedactedResponseno error; response {Redacted:reach j7.example@example.org about 517-17-1007 … RequestID:req-770}internal/goldencorpusTestGoldenFileRedactionRefusesAFileItCouldNotParseerr = <nil>; artifact written: trueGolden corpus
UPDATE_GOLDEN=1 go test ./internal/goldencorpus/...on the branch: 0 snapshot files changed. NoFileCasewas added: the unparseable-PDF case lives ingolden_file_redact_test.goas its own test, becauseTestGoldenFileRedactionSinktreats anyRedactFileerror as a failure and everyFileCaseis fully examined (TestGoldenFileRedactionSinkpasses unchanged).TestGoldenRedact(theredact.Enginesnapshots) unchanged, which is the "complete request byte-identical" check.make score: PASS, identical on both:recall_all=1.0000 recall_hm=0.9906 prec_hm=0.9589 whole_leak=0(158 cases, 212 gated labels, 16 checks); the reports differ only in timing lines.Suppression round trip
Rules generated by main (
--generate-suppressionson the complete scan, then all enabled), applied by this branch:SSN=300ms(fires)SSN=1s(fires)Every kept partial matches its twin's rule: Tracks A and B change no finding, and Track C only adds findings on cut-short runs, each equal to its twin (above), so legacy confidence-bearing rules match too.
Gates
Head
b8998b3, rebased on nothing (basec403e27).gofmt -l .go vet ./...(whole output read)go vet -tags examples_lambda ./examples/lambda-redact/cleango test -race -count=1on every touched packagecmd,internal/{core,execguard,goldencorpus,parallel,preprocessors,validators,web},pkg/{redact,scan},examples/lambda-redact(tag): all ok, no race reported./cmd/... ./internal/... ./pkg/...)-count=1tests/integration(FERRET_TEST_MODE=true, the rest ofmake test)make buildUPDATE_GOLDEN=1make scoremake pr-checklist BASE=origin/mainonb8998b3:TestGrowthRatioMissesASparseQuadratic(internal/goldencorpus), the known host flake; test untouched. The script writes its suite and race output to the shared/tmp/_suiteand/tmp/_raceand discards the flake repeats' output, and other worktrees on this host were running it at the same time (13's empty count isgrep: /tmp/_race: No such file or directory), so I re-ran 2, 12 and 13 with the output kept in my own directory: full suite 89/89 ok; race 89/89 ok; three uncached repeats of the ten touched packages: 10/10, 10/10, and 9/10 with the one failureTestGrowthRatioMissesASparseQuadratic: … 8.73x on the cpu clock … ABOVE the 8.0 threshold. Back to back, isolated,-count=3: branch 3/3 pass (5.70x, 7.09x, 7.61x), main 3/3 pass (5.62x, 7.08x, 5.84x), 1-min load 10. Of the nine whole-packagegoldencorpusruns on this branch whose output I kept, it failed in three, always alone; it reads the CPU clock of a synthetic validator this PR does not touch.simple,format_preserving,synthetic×--file/--stdin× one.txt (complete) and infra.log (cut short, partial copy): one copy each, 0 reported values in clear in any copy or stream, rc 0 / 3 as expected;--stdinstdout byte-identical to the--filecopy forsimpleandformat_preserving(syntheticis random per run on main too).internal/web/server.gopassesrequest.Context()to the scan and changes nothing else: no template, asset, handler route or header.Deviations and open questions
resilience.ClassifyError.internal/execguardimportsinternal/resilience(SafeRunbuilds aresilience.NewPermanentError), soClassifyErrorcannot callexecguard.IsCoverageCutShortwithout an import cycle. The check sits in the only retry wrapper that runs validators,parallel.retryValidatorStrategy.Run: by identity, beforeClassifyErrorsees the error, so rewording a guard's message cannot change retry behaviour, which is the property the issue asked for. Ordinary transient errors still getMaxRetries=2(TestTransientErrorIsStillRetried).RunValidators' and both start waiting at the same cancellation, so the inner one must end first:parallel.CancelGrace100 ms,validators.DocumentCancelGrace50 ms, held together by a test (validatorscannot importparallel). The bound is ≤ 100 ms as asked. Its cost: a deadline that fires while nothing can report returns up to 100 ms later (the 150 ms row above, 0 findings either way).…Ctxvariants (core.ScanContentCtx,ScanFileCtx,RedactFileCtx,parallel.ProcessFilesWithProgressCtx), the repo's existing convention (ProcessContentCtx,ValidateContentCtx), with the old names ascontext.Background()wrappers, instead of changing the internal signatures:cmd/main.goand the 35-odd test and scorecorpus call sites keep compiling, and every path frompkg/scan,pkg/redactand/scancarries the caller's context. The CLI still roots onBackground()(no signal context; out of scope). Inside the pool the context travels on a per-call copy ofJobConfig(an unexported field), so the job-submitting goroutine stays inProcessFilesWithProgress, whichinternal/goroutineguardaudits under that name.Submitand the workers select on the pool's context; rooting it at the caller's would let a cancellation drop a queued job or a finished result while the collector waits for exactly one result per file. A job that starts after the context is done does not open its file and is recorded as cut short.ScanContent's reason is the taxonomy name once, then the error, for every member (coverage cut short: context deadline exceeded,coverage cut short: validator declined oversize content: …), rather than a label per sentinel — no wording switch, so the AST guard needs no exemption (it supports the issue's one shape, a wording switch underIsCoverageCutShort, and tests it on synthetic sources). Visible change: the--stdinwarning's reason.RedactFileContextwith a context done at entry returns a non-nil result withCoverageand an error wrappingErrIncompleteandctx.Err()(the sink contract), not(nil, ctx.Err())asScanTextdoes; it creates no output directory.Background()byScanText,ScanFileandRedactFileContext: they accepted (and ignored) nil before, so honouring the context would otherwise turn a nil into a panic.Engine.Redactalready required a non-nil context and is unchanged there.pkg/scan: an incomplete result is reported with an error only when the caller's context is done when the scan returns. A complete result is returned as complete even if the context expired after the last validator.FormatterOptionsconstructor (Stats,NotExamined,OutputToFile). The exit code and stderr parts ship alone, as the issue allows;README-Stdin.mdsays the stdin JSON does not yet carrystats. Not done (optional):ValidatorBudgetsonTextOptions/FileOptions(new public API; U38).internal/help/help.go(--validator-budgethelp: "stopped and the scan is marked incomplete" is still true;docs/configuration.mdcarries the fuller statement);THREAT_MODEL.md:39(its exit-3 claim is now true for stdin as written); the file path's pre-commit notice incmd/main.go, which says(contents unreadable)for every cause including a cut-short — the stdin notice sayscoverage cut short. Edited beyond the issue's file list because the change needed them:internal/parallel/parallel_processor.go(item 5'sProcessFiles*),pkg/redact/doc.go,examples/lambda-redact/README.md.ScanContenthard errors (not cut-shorts) stay logged-not-flagged, as before;Engine.Redactflags every non-nilrunErr, as the issue specifies.ScanConfighas hadValidatorBudgetssince fix(validators): keep findings and disclose the cut-short when a metadata-capable file is scanned without METADATA #797;validator_runner.goisinternal/parallel/, notinternal/validators/; the post-return CPU after a 50 ms deadline on 20,000 lines is mostly context analysis, not validator work (above); line numbers moved (keepMatchesis atdual_path_bridge.go:1010).--filewrites its partial copy too). A caller that must never forward such a stream needs--fail-on-incompleteand to drop stdout on exit 3;README-Stdin.mdnow says so. Withholding output on both paths would be a separate decision.Checklist
make testandmake buildpass —./internal/...in the full suite (the knowngoldencorpusflake attributed above) andtests/integrationok;make buildok## [Unreleased]→### 🐛 Bug FixesCloses #770Closes #770