Skip to content

feat(verifier): line-stable suppression via match_key (#6)#43

Merged
pureliture merged 3 commits into
mainfrom
claude/stable-suppression
Jun 19, 2026
Merged

feat(verifier): line-stable suppression via match_key (#6)#43
pureliture merged 3 commits into
mainfrom
claude/stable-suppression

Conversation

@pureliture

Copy link
Copy Markdown
Contributor

What

잔여작업 #6 — 라인 이동에 강건한 finding 억제(line-stable suppression). 이미 false-positive/resolved/ignored로 판정된 시크릿을, 단지 라인만 이동했다는 이유로 verifier(또는 사람)에게 다시 묻지 않게 한다.

Why

finding_idline_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으로 번지지 않는다.
  • items: FINDING_STATEmatchKey 투영(secret_hash 없으면 without_none이 제거).
  • store: 단말 non-blocking 상태일 때만 MATCHKEY#<mk> → disposition 포인터를 트랜잭션 이후 best-effort로 기록(_record_match_disposition_pointer), find_disposition_by_match_key 조회 추가. GSI3 신설 대신 pointer-item으로 schema/blast-radius를 최소화. 포인터 지연은 fail-safe(다시 물어봄)다.
  • runtime: 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_key
  • storage/adapters/nosql_db/items.py: FINDING_STATE.matchKey
  • storage/adapters/nosql_db/store.py: 포인터 write/lookup, _DISPOSITION_NON_BLOCKING
  • runtime/disposition_lookup.py (신규): resolve_existing_disposition
  • runtime/scan_all.py: 억제 게이트 + inherited_suppressions 요약 필드
  • docs/workbench/specs/stable-suppression/: requirements.md, design.md

Test / 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 -q702 + 신규 통과
  • 신규 파일 ruff check/ruff format --check clean
  • governance.public_safety --diff origin/main...HEAD → exit 0

Deferred (환경상 불가)

  • Tailscale Ubuntu 호스트 DynamoDB-local에서의 실제 verifier 상속 동작 확인은 런타임 환경 필요.
  • 기존 finding에 대한 match_key 백필 마이그레이션(OQ1)은 별도 후속.

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>
Comment thread src/security_scanner/runtime/disposition_lookup.py Fixed
Comment thread src/security_scanner/runtime/disposition_lookup.py Fixed
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>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/security_scanner/runtime/disposition_lookup.py Outdated
Comment thread src/security_scanner/storage/adapters/nosql_db/store.py Outdated
Comment thread src/security_scanner/storage/adapters/nosql_db/store.py Outdated
Comment thread src/security_scanner/core/finding/model.py
Comment thread src/security_scanner/runtime/scan_all.py
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>
@pureliture pureliture merged commit 81b79a1 into main Jun 19, 2026
9 checks passed
@pureliture pureliture deleted the claude/stable-suppression branch June 19, 2026 09:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants