Repository navigation
feat(v2-refactor): per-validator time + match budgets (Move C) + secrets/cloudresources ctx - #110
Merged
Merged
Conversation
Contributor
🔍 Ferret Scan Results✅ No sensitive data detected in the changed files. 🤖 This comment was automatically generated by Ferret Scan |
added 5 commits
July 1, 2026 16:01
… line loops Phase 3 (slice 1): make the three audit-measured worst-offender validators reclaimable mid-scan. Each now implements execguard.ContextAwareValidator via a new ValidateContentCtx that polls the context once per line and, on deadline/cancel, returns the matches gathered so far plus ctx.Err(). The existing ValidateContent becomes a backward-compatible shim over ValidateContentCtx(context.Background(), ...), so the public contract and all output are byte-identical for the common (uncancelled) path. Because execguard.ValidateContent already dispatches to ContextAwareValidator when implemented (#98), the live scan path routes through the ctx-aware form automatically — a runaway multi-line scan is now reclaimed promptly instead of running to completion (SSN reclaim: 0.01s vs 53.68s before). New shared primitive execguard.LineLoopCancelled polls ctx.Err() every 256 iterations (and on i==0), keeping the per-line cost effectively free while bounding worst-case over-run to <256 lines after a budget fires. Scope note: this fixes runaway *multi-line* scans. The O(n^2) single-long-line case is Phase 3 slice 2 (shared LineScan primitive) and unchanged here. Tests: per-validator ctx_cancel_test.go covering (a) cancelled-context reclaim returns <500ms with context.Canceled on 500k-line input, (b) the background shim is behavior-preserving. Negative controls confirmed the reclaim tests genuinely fail without the poll (IP 5.18s, phone 16.16s, SSN 53.68s, all returning nil err). Full suite + -race green; golden corpus byte-identical (zero UPDATE_GOLDEN).
…oop validators Phase 3 (slice 3): completes the ctx-polling coverage started in #107 (SSN/IP/ phone). The remaining seven line-loop validators now implement execguard.ContextAwareValidator via a new ValidateContentCtx that polls execguard.LineLoopCancelled once per line and returns on deadline/cancel; the existing ValidateContent becomes a context.Background() shim. Because execguard.ValidateContent (from #98) dispatches to ContextAwareValidator when implemented, the live scan path routes through the ctx-aware form automatically, so a runaway multi-line scan in ANY of the 13 validators is now reclaimed promptly instead of running to the 5-minute job deadline. - Direct-loop validators (loop in ValidateContent): email, creditcard, vin (preserve finishTiming observer, record {cancelled,match_count} on cancel); passport, personname (no observer; personname returns pre-dedup partials). - Delegating validators (loop in a helper): socialmedia and intellectualproperty thread ctx into detectPatternsByLine (socialmedia also into its batch variant and the *WithErrorHandling wrapper); the loop stops early on cancel, the partial per-line matches are still clustered/reconstructed, and ValidateContentCtx surfaces ctx.Err(). Behavior-preserving: the common uncancelled path is byte-identical — the golden corpus passes with zero UPDATE_GOLDEN. Public ValidateContent contract unchanged. Tests: per-validator ctx_cancel_test.go — (a) a cancelled context reclaims a 500k-line scan in <500ms returning context.Canceled, (b) the background shim equals ValidateContentCtx(Background). socialmedia uses newConfiguredValidator so the loop actually runs (the unconfigured validator returns before it). A negative control (neutralizing LineLoopCancelled) confirmed all 7 reclaim tests genuinely fail without the poll (4-35s to completion vs <=0.02s), proving each validator's poll is wired to the live path. Full suite + -race green. Stacked on #107 (feat/v2-phase3-ctx-polling), which introduces execguard.LineLoopCancelled.
…es validators Phase 3 (ctx-teeth): extends ctx-polling to the two remaining line/loop-driven validators so a per-validator time budget (Move C, next) has teeth on 12 of 13 validators. Only METADATA stays exempt — it is not text-token-loop-driven (audit measured it safe ~0.3s) and already polls ctx per metadata item on its own path. Both now implement execguard.ContextAwareValidator via a ValidateContentCtx that returns partial matches + ctx.Err() on deadline/cancel; ValidateContent becomes a context.Background() shim, so the live scan path routes through the ctx-aware form automatically (execguard.ValidateContent dispatch from #98). - secrets: poll once per line in the line loop (SECRETS' DoS shape is many dense lines — per-line shell/entropy work). Plus an ENTRY check before the whole-content passes (context analysis + multi-line secret scan) that run ahead of the polled loop, so an already-cancelled scan bails before paying for them. - cloudresources: poll once per candidate span in the scoring loop, plus an entry check before the whole-content regex pass + containment dedup. (Independent of #108's per-line negative-keyword hoist — different lines; both can land in either order.) Behavior-preserving: golden corpus byte-identical, zero UPDATE_GOLDEN. Public ValidateContent contract unchanged. Tests: per-validator ctx_cancel_test.go — cancelled context reclaims a large scan promptly returning context.Canceled; background shim equals ValidateContentCtx. cloudresources gains the standard raceEnabled helper pair and a -race-relaxed ceiling (the pre-loop regex work exceeds 500ms under the race detector's 5-20x overhead — the entry check keeps the real path near-instant; the ctx.Err() assertion is the true guard). Negative control confirmed both reclaim tests fail without the poll (secrets 19.5s, cloudresources nil-err). Full suite + -race green. Stacked on #109 (feat/v2-phase3-ctx-polling-rest).
…d chokepoint (Move C)
Move C: give operators a per-validator execution budget — a wall-clock TimeLimit
and/or a MatchLimit — enforced at the single dispatch chokepoint
(execguard.ValidateContent). Both are opt-in; the zero value is DISABLED and the
scan is byte-identical to today (golden corpus passes with zero UPDATE_GOLDEN).
Mechanism (new internal/execguard/budget.go):
- ValidatorBudget{TimeLimit, MatchLimit}, carried on the context via
execguard.WithBudgets(ctx, map[checkName]ValidatorBudget) — no fan-out
signature churn. An empty/nil map returns the parent ctx unchanged.
- TIME: when TimeLimit>0, ValidateContent derives context.WithTimeout(ctx, limit)
as the ctx handed to the validator. Since all line-loop validators poll it
(Phase 3 / secrets+cloudresources ctx-teeth), a runaway validator self-
terminates at its OWN budget, tighter than the 5-min job deadline. context
.WithTimeout never extends a parent deadline, so the tighter bound always wins.
- MATCH: when MatchLimit>0 and a SUCCESSFUL result exceeds it, the slice is
truncated (emission order preserved) and ErrMatchBudgetExceeded is returned
alongside the kept matches. Never truncates a partial slice returned with an
error (that path already means "incomplete").
Budgets are keyed by the LOGICAL CHECK NAME ("SSN", "IP_ADDRESS"), the operator-
facing name that matches --checks — not the Go type. This required threading the
check name from Detector.SetupValidators through RegisterDocumentValidator into
the document fan-out (new namedValidator{name,v}); the chokepoint is now called
with "SSN" instead of "*ssn.Validator". Side benefit: panic/error diagnostics now
name the check, not the Go type.
Surfacing (the plumbing that makes budgets observable): the document bridge and
processDualPath previously (a) DISCARDED a validator's matches whenever it
returned any error and (b) SWALLOWED per-validator errors on the normal-
completion path. Both are fixed for budget/deadline outcomes ONLY: truncated/
partial matches are kept, and a representative budget/ctx error is propagated so
core.ScanContent sets ScanResult.Incomplete (+reason). parallel.RunValidators
likewise now preserves partial matches for these outcomes. Hard validator errors
keep the historical discard-and-log behavior. No budgets configured => none of
these errors arise => byte-identical.
Config: ContentScanConfig.ValidatorBudgets (ScanContent) and JobConfig
.ValidatorBudgets (worker pool), both attached at the ctx origin. Library-surface
only for now; no CLI flag yet (a separate UX decision).
Verified through the REAL stack: e2e tests prove a time budget reclaims a runaway
validator in ~30ms and surfaces DeadlineExceeded even though the PARENT ctx never
expired; a match budget caps 50->5 matches kept through the full nested stack;
under-cap is unmutated. A standalone program driving core.ScanContent on 200k
dense lines confirms end-to-end: match cap => 5 matches + Incomplete; time budget
=> reclaimed + Incomplete; disabled => 200000 matches, complete. New unit tests in
budget_test.go use the block-on-ctx.Done anti-flake pattern (no wall-clock races).
Full suite + -race green; golden byte-identical.
Stacked on the secrets+cloudresources ctx-teeth commit and #109.
sectoramen
force-pushed
the
feat/v2-movec-validator-budget
branch
from
July 1, 2026 20:03
2fdd0f5 to
7524437
Compare
Contributor
🔍 Ferret Scan Results✅ No sensitive data detected in the changed files. 🤖 This comment was automatically generated by Ferret Scan |
This was referenced Oct 14, 2026
rustic
added a commit
that referenced
this pull request
Oct 22, 2026
…data-capable file is scanned without METADATA (#797) * fix(validators): keep findings and disclose the cut-short when a metadata-capable file is scanned without METADATA With METADATA not selected, any coverage cut-short on the document path of an office, PDF, image, audio or video file discarded every finding for the file and the disclosure with it: exit 0 under --fail-on-incomplete, empty stderr, no SARIF notification, no redacted copy. The router gives those files a metadata item, the metadata path failed for want of a validator, the cut-short made the document path fail too, and the both-paths-failed branch returned nil with the cut-short formatted away by %v; the legacy fallback then re-ran the file, dropped its partials and returned nil. - An unselected METADATA is a no-op: ProcessMetadataContentCtx returns (nil, nil) and the dual path does not start the metadata goroutine. - Both paths failing returns every match either path kept and both errors wrapped with %w. The metadata goroutine keeps cut-short matches, as the document goroutine does. - The metadata path returns its own cut-shorts through firstBudgetError; a METADATA panic was logged and dropped even with METADATA selected. - The legacy fallback keeps cut-short partials and returns the cut-short instead of nil. - --validator-budget METADATA=<d> (and all=<d>) is honoured: both metadata dispatch sites use the check name, and execguard.LoopBudget turns the time budget into one deadline for the metadata loop, checked before each item (#110's polling exemption stands). CapMatches, shared with ValidateContent, applies a match budget to the union of items. - One predicate throughout: execguard.IsCoverageCutShort. Tests: a sentinel x METADATA on/off x plaintext/.docx table derived from IsCoverageCutShort's source with go/ast, the METADATA panic, a parent deadline mid-metadata, the METADATA budget, the legacy fallback; cmd and pkg/scan contract tests over in-test containers; a golden recall case (FileCase.ValidatorBudgets, forwarded through a new core.ScanConfig.ValidatorBudgets) and three decoys that stay byte-identical. No finding's text, line or type changes, so suppression hashes are unchanged. Affected files go from exit 0 to 3 under --fail-on-incomplete; runs with no cut-short are byte-identical. Closes #771 * fix(metadata): round the field-type boost before adding it, so arm64 and amd64 agree on confidence `match.Confidence += boost * 100` is a multiply-add the arm64 compiler fuses into one FMA with a single rounding; amd64 rounds the product first. For an office AUTHOR_INFO finding with a 0.3 boost the two differ in the last digit (55.000000000000014 on amd64, 55.00000000000001 on arm64), so a golden snapshot regenerated on Linux failed on the macos-latest (darwin/arm64) leg of #797's first CI run. The explicit float64 conversion forces the rounding the Go spec permits fusion to skip. No band or finding changes; amd64 output is unchanged (golden regeneration changes 0 files). Refs #771 * fix(validators): report a METADATA budget the last item overran, and make the cut-short tests clock-independent The metadata loop checked its budget deadline only before each item, so a deadline that fired while the LAST item was running had no later item to stop and was never reported. Most files have one metadata item, so a METADATA budget was disclosed only when it had already expired before the item started: a budget the item overran was the silently ignored budget the flag promises not to have (#115). The loop now checks once more after the last item; the item's findings are kept and the overrun is disclosed. The Windows leg of #797's first CI run failed six cmd subtests and two in-package subtests, all built on a 1ns budget being expired at the first check. That holds on a nanosecond clock (Linux, macOS), where context.WithTimeout sees the deadline already passed and cancels synchronously, and not on Windows, whose clock ticks at 0.5-15.6 ms: the same call arms a timer, and work that finishes within the tick never sees it. The repo's one existing 1ns test (TestCutShortCoverageIsTheOtherOverlapTerm) already knew this and skips. - cmd: the five document-path budget subtests use a fixture with the same ten addresses plus about 1 MB of neutral filler, so the cut validator is still polling tens of milliseconds after the timer fires on any platform (PHONE adds ~600 ms, SSN ~210 ms on the dev host, against a 15.6 ms worst-case tick). Expectations are unchanged. The METADATA-item subtest, whose one item takes microseconds and which no fixture can stretch, asserts on fine clocks and skips on Windows with the reason when the budget did not fire. - internal/validators: "expired before the first item" (1ns) is replaced by "fires during the last item" (50 ms budget, 400 ms item), which is the new behaviour and fails without the post-loop check; the legacy-dispatch test uses a MatchLimit, which needs no clock to prove the lookup name. Refs #771 * test(cmd): budget PHONE, not SSN, in the precommit-profile cut-short run so Windows observes it The precommit_profile subtest of TestMetadataCapableFileKeepsFindingsOnACutShort failed on windows-latest after eb700de with rc 0 and the ten findings intact: the budget never showed. It was the one budget run in the test on SSN rather than PHONE. On a coarse clock a 1ns budget is a timer, and the cut validator has to still be polling its context (every 256 lines) when the timer fires. SSN's pass over the 1 MB filler takes about 35 ms on the dev host (the filler holds no digit runs for it to examine): under --profile precommit it observes a 30 ms budget and misses a 40 ms one. PHONE's pass takes 400-600 ms and observes a 400 ms budget; the four PHONE=1ns runs in the same test passed on the same Windows run. The Windows tick plus the runtime's timer dispatch exceeded SSN's span. Both validators are in the built-in profile's list and neither reports anything on the fixture, so the expected outcome (rc 3, ten EMAIL findings) is unchanged. On main the new row still reproduces the issue: rc 0, 0 findings. The fixture comment now names the margin per validator so a later edit does not budget a fast one again. --------- Co-authored-by: union <union@example.invalid> Co-authored-by: Lee Myers <ichilegend@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Move C of the v2 consolidation: operators can set a per-validator execution budget — a wall-clock
TimeLimitand/or aMatchLimit— enforced at the single dispatch chokepoint. Both opt-in; zero value = disabled = byte-identical (golden passes with zeroUPDATE_GOLDEN).Two commits here
1. ctx-teeth: secrets + cloudresources cooperative cancellation
Extends ctx-polling to the last two loop-driven validators so a time budget has teeth on 12/13 (METADATA stays exempt — not token-loop-driven, already per-item ctx). Both get
ValidateContentCtx+ an entry check before their whole-content pre-loop passes.2. Move C budgets
execguard.ValidatorBudget{TimeLimit, MatchLimit}, carried on ctx viaexecguard.WithBudgets(no fan-out signature churn; empty map = parent ctx unchanged).context.WithTimeout(ctx, limit)per validator → a runaway self-terminates at its own budget (all line-loop validators poll it). Tighter of {parent, budget} wins.ErrMatchBudgetExceededwith the kept matches. Never truncates an error-partial slice.SetupValidators→RegisterDocumentValidator→ fan-out (newnamedValidator). Side benefit: panic/error logs now name the check, not*ssn.Validator.processDualPath/RunValidatorsto (a) keep truncated/partial matches on budget/deadline outcomes and (b) propagate a representative budget/ctx error soScanResult.Incompletefires. Hard validator errors keep the historical discard-and-log behavior. No budgets configured ⇒ none of these paths change ⇒ byte-identical.ContentScanConfig.ValidatorBudgets+JobConfig.ValidatorBudgets. Library-surface only; no CLI flag yet (separate UX decision).Verification
DeadlineExceededthough the parent ctx never expired; match budget caps 50→5 kept through the full nested stack; under-cap unmutated.core.ScanContent, 200k dense lines): match cap → 5 matches +Incomplete; time budget → reclaimed +Incomplete; disabled → 200000 matches, complete.ctx.Doneanti-flake pattern (no wall-clock races). Full suite +-racegreen; golden byte-identical.🤖 Generated with Claude Code