feat(verifier): line-stable suppression via match_key (#6)#43
Conversation
Re-asking the verifier (or a human) about a secret that was already dispositioned false-positive/resolved/ignored is wasteful and noisy. The finding_id includes line_start (#12 L1), so a secret that merely moves lines gets a new identity and would be re-verified despite an existing decision. Add a secondary, line/commit-independent suppression key (match_key) keyed on repo/file/rule + the salted secret_hash (content), never on line/commit. It is a suppression key only — finding_id identity is unchanged. Because secret_hash is content-derived, distinct secrets get distinct keys, so suppression never crosses different leaks. - core: compute_match_key() (mk_ + sha256[:32] over stable material). - items: FINDING_STATE carries matchKey (dropped when no secret_hash). - store: best-effort MATCHKEY#<mk> -> disposition pointer written post- transaction for terminal non-blocking statuses only; find_disposition_by_match_key lookup. No GSI3 (pointer-item keeps schema/blast-radius minimal); pointer lag is fail-safe (re-ask). - runtime: resolve_existing_disposition gate (finding_id first, then match_key); scan-all suppression gate skips re-verify, inherits prior verdict on line-move (actor/source="inherited"), and reports inherited_suppressions. Lookup errors fail safe to re-verify. Tests: match_key core + gate units, store-level pointer write/lookup, scan-all inheritance integration. Full suite green (702 + new). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CodeQL flagged the `...` Ellipsis bodies on the _DispositionLookupStore Protocol methods as "Statement has no effect" (py/ineffectual-statement). Replace them with one-line docstrings — the idiomatic, alert-free body for a typing.Protocol method. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a line-stable suppression mechanism to prevent re-verifying findings that have merely changed lines. It implements a content-based match_key (combining repo, file, rule, and salted secret hash) and a lookup gate that checks both the direct finding_id and the secondary match_key before verification. While the design is solid, several critical issues were identified during the review: a logical bug in the lookup gate could cause active blocking findings to be suppressed by stale match keys; transitioning a finding back to a blocking state fails to delete existing match-key pointers, posing a security risk; unhandled exceptions in the best-effort pointer write could fail the main transaction; path normalization in compute_match_key needs to resolve directory traversal components; and synchronous lookups inside the scan loop introduce an N+1 query performance bottleneck.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
Address gemini-code-assist review on PR #43: - over-suppression (HIGH): resolve_existing_disposition fell through to the match_key pointer even when the exact finding_id had a *deliberate* blocking decision, letting a same-secret false-positive suppress a re-opened/real finding. Now a blocking direct state with a recorded decision (decidedBy) is definitive and returns None. A default scan-created OPEN row (version 0, no decidedBy) still falls through, so line-move inheritance keeps working once findings are persisted before verification (the literal "any blocking state wins" suggestion would have disabled the feature entirely). - stale pointer (SECURITY-HIGH): _record_match_disposition_pointer now deletes the MATCHKEY pointer when a status flips back to blocking (e.g. a false positive re-opened as a real secret), so a later line-move of the same secret is re-verified instead of silently suppressed by an outdated verdict. - best-effort (MEDIUM): the pointer put/delete is wrapped in try/except so a transient write failure never fails the already-committed disposition transaction, matching the documented fail-safe contract. Tests: deliberate-blocking-overrides-pointer and default-open-still-inherits gate units; reopen-deletes-stale-pointer store test; FakeDynamoTable gains delete_item. Full suite green (705). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What
잔여작업 #6 — 라인 이동에 강건한 finding 억제(line-stable suppression). 이미 false-positive/resolved/ignored로 판정된 시크릿을, 단지 라인만 이동했다는 이유로 verifier(또는 사람)에게 다시 묻지 않게 한다.
Why
finding_id는line_start를 포함하므로(#12 L1), 같은 시크릿이 라인만 옮겨가도 새 identity가 되어 기존 판정이 있음에도 재검증 대상이 된다. 이는 불필요한 Ollama 호출과 noise를 만든다. identity는 그대로 두고, 내용 기반의 보조 억제 키를 도입해 이 재질문만 걸러낸다.Design (pointer-item, GSI 없음)
compute_match_key(core/finding/model.py):repo/file/rule + 솔트된 secret_hash(내용)로만 키를 만든다 — line/commit 비의존.mk_+sha256[:32]. identity가 아니라 suppression 키다. secret_hash가 내용 파생이라 서로 다른 시크릿은 서로 다른 키를 받으므로 억제가 다른 leak으로 번지지 않는다.FINDING_STATE에matchKey투영(secret_hash 없으면without_none이 제거).MATCHKEY#<mk> → disposition포인터를 트랜잭션 이후 best-effort로 기록(_record_match_disposition_pointer),find_disposition_by_match_key조회 추가. GSI3 신설 대신 pointer-item으로 schema/blast-radius를 최소화. 포인터 지연은 fail-safe(다시 물어봄)다.resolve_existing_disposition게이트(먼저finding_id, 다음match_key). scan-all 검증 루프 진입 전 억제 게이트가 재검증을 건너뛰고, 라인 이동 시 직전 verdict를 새 finding_id로 상속(actor/source="inherited")하며inherited_suppressions를 집계. 조회 오류는 재검증으로 fail-safe.Non-intrusive 불변식 유지
대상 레포 PR 차단 없음, 알림/엔드포인트 연동 없음, redacted-only(raw secret 미저장·미키), poll 기반. 포인터는 로컬 단일테이블 내부 항목.
Changes
core/finding/model.py:compute_match_keystorage/adapters/nosql_db/items.py:FINDING_STATE.matchKeystorage/adapters/nosql_db/store.py: 포인터 write/lookup,_DISPOSITION_NON_BLOCKINGruntime/disposition_lookup.py(신규):resolve_existing_dispositionruntime/scan_all.py: 억제 게이트 +inherited_suppressions요약 필드docs/workbench/specs/stable-suppression/: requirements.md, design.mdTest / Verify
tests/test_match_key_suppression.py(신규): match_key 코어(라인 비의존·시크릿 구분·raw 미노출) + 게이트 유닛(직접 hit, OPEN 미억제, 라인이동 상속, 포인터 부재, blocking 포인터)tests/test_dynamodb_compatible_store.py: state의 matchKey, non-blocking 포인터 기록/조회, blocking은 포인터 미기록tests/test_cli_scan_all.py: scan-all 라인이동 상속 통합(verifier 미호출, inherited 기록,inherited_suppressions=1)uv run pytest -q→ 702 + 신규 통과ruff check/ruff format --checkcleangovernance.public_safety --diff origin/main...HEAD→ exit 0Deferred (환경상 불가)