Skip to content

Scale redesign #2 — Segment A: 500-repo 증분 스캐닝 엔진 (M1–M7, M9)#49

Merged
pureliture merged 12 commits into
mainfrom
claude/scale-redesign-seg-a
Jun 20, 2026
Merged

Scale redesign #2 — Segment A: 500-repo 증분 스캐닝 엔진 (M1–M7, M9)#49
pureliture merged 12 commits into
mainfrom
claude/scale-redesign-seg-a

Conversation

@pureliture

Copy link
Copy Markdown
Contributor

무엇

security-scanner를 14 repos 주간 full scan에서 500+ repo로 확장하는 재설계(#2)의 Segment A(코드 마일스톤). grill-to-spec으로 합의한 requirements.md/design.md v2를 agentic-execution으로 구현. 비차단(권고형)·대시보드 전용 파이프라인: org 자동발견 카탈로그 → 주기 증분 폴링 + 주기 full baseline → 동시 워커 → per-repo freshness → 스캐너 read API → 스케줄 알림.

마일스톤 (8/8)

  • M2 큐 하드닝 — bounded ordered dequeue(신규 GSI 없이 기존 gsi1 status축 + per-shard Limit-K), lease fence 토큰, lease-reaper, starvation backoff, skip-bug 수정
  • M5 per-repo freshness — REPO_HEALTH 속성단위 conditional UpdateItem(무클로버/무regress), 스케줄 freshness-evaluator + BREACH_COUNTER, 전역 SCAN_HEALTH 싱글톤 폐기
  • M1 카탈로그 reconcile — org 발견(거버넌스 게이트)·opt-out·커버리지, transient 가산적
  • M6 disposition — triage.verdict 파생 + verifier enum 매핑 + 필터
  • M4 폴링/baseline — ls-remote skip, baseline per-repo enqueue(priority/backpressure/rolling), cadence-overrun 알림
  • M3 워커풀 — scan-worker@ systemd 템플릿 + 타이머 + reaper/eval CLI
  • M7 read API — findings/freshness/coverage/backlog 읽기전용, Select=COUNT(no full-table Scan), localhost trust model
  • M9 알림 — pluggable AlertSink + de-dup/re-notify + 스케줄 탐지→발화

검증

  • 1026 passed, 1 skipped(실엔진 gated)
  • 설계 리뷰(블로커 4 + 적대 검증) + 통합 리뷰 2 opus(블로커 4/4 CLOSED, 7/7 seam)
  • 실 DynamoDB Local(docker 없이 Java jar): bounded dequeue/fence/REPO_HEALTH UpdateItem/Select=COUNT 실 엔진 검증 — RUN_DDB_LOCAL_SMOKE gated, ScyllaDB Alternator에서도 endpoint만 바꿔 재사용
  • CodeRabbit: 1차 6건(major 2 포함) 수정 → 재리뷰 0 findings

Segment A 밖 (게이트)

  • GATE 1 박스 부하검증: cadence·N(워커 수)·DB(dynamodb-local vs Alternator)는 배포 박스 online 후 측정 → grill-to-spec로 SoT 회귀. 코드의 cadence/N 상수는 placeholder.
  • GATE 2 거버넌스: GHAS/org repo-list 실 GET은 human-PR + autopilot stop-condition 해소 후. 현재 GovernanceGatedOrgRepoListProvider가 fail-closed.
  • GATE 3 M8 대시보드 UI: 별도 sub-project, 인간 비주얼 수용.

🤖 Generated with Claude Code

pureliture and others added 11 commits June 20, 2026 18:01
…ckoff

- skip-bug: acquire_repo_lease 실패 시 루프 break 대신 해당 job만 skip
- lease fence 토큰(RepoLease/ScanJob): complete/retry/release를 worker+fence CAS로
  reaped/느린 워커의 late write·release 거부(이중 스캔·stale completion 방지)
- lease-reaper: 만료 리스 회수 + fence 증가로 old holder 차단
- bounded ordered dequeue: 기존 gsi1 status축 per-shard Limit-K + read_list_axis_ordered
  (신규 GSI 없음, {GSI1,GSI2} 불변)
- 경합 분산(preferred shard) + starvation backoff(2회 만료부터 지수, max-lease-expiry→dead-letter)

evidence: 854 passed (+18 tests)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UX1CdtfsZ4XJmSKtY2VCWp
…EALTH 싱글톤 폐기

- REPO_HEALTH#<repo_id> 엔티티(lastSuccessfulIncrementalAt/lastSuccessfulFullScanAt)
- 워커 완료 시 속성단위 conditional UpdateItem(advance-only): incremental/baseline 필드
  상호 클로버 금지 + out-of-order regress 금지(SC-5)
- per-repo 2-임계 평가(poll_interval+margin / baseline_cadence+margin), 기존 26h=24+2 idiom 재사용
- 스케줄 freshness-evaluator + materialized BREACH_COUNTER(read O(1)) + on_breaches 훅
- 전역 SCAN_HEALTH 싱글톤을 per-repo로 폐기(scan-health 게이트·local_scan 마이그레이션)
  → 신선한 1개 repo가 stale 499개 가리는 사고 구조 차단
- ScanJob.job_type(M4 baseline 시드), coverage_gap(M1), alert sink(M9) seam 남김

evidence: 879 passed (+25 tests, incl. one_fresh_repo_does_not_mask_a_stale_repo)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UX1CdtfsZ4XJmSKtY2VCWp
- CATALOG#<repo_id> 엔티티(repo_url/included/excluded_reason/first_seen/last_reconciled)
- run_catalog_reconcile: org 목록(injectable provider)→opt-out 제외(드롭 아님)→upsert,
  transient 실패에 가산적(기존 repo 절대 드롭 안 함 = 침묵 커버리지 갭 방지)
- 커버리지 갭 = included인데 REPO_HEALTH 없는 repo 수 → M5 BREACH_COUNTER.coverage_gap 배선
- reconcile CLI: 기본 provider는 GovernanceGatedOrgRepoListProvider(실 fetch 거부)
  → 실 gh api GET은 FR-12/autopilot stop-condition 해소(GATE 2) 후 1줄 주입
- FR-12: compare-ghas/ghas_api 표면 변경 없이 유지 note(카탈로그가 공유 입력축)

evidence: 899 passed (+20 tests, incl. transient_failure_does_not_drop_existing, governance_gated_refuses_live_fetch)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UX1CdtfsZ4XJmSKtY2VCWp
… + 필터

- FINDING.disposition(verified|false_positive|unreviewed) = 기존 triage.verdict 파생 property
  (별도 저장 필드 아님 → 드리프트 0, verifier가 verdict만 쓰면 자동 반영)
- F6 어휘 충돌 해소: verified↔TRUE_POSITIVE / false_positive↔FALSE_POSITIVE /
  unreviewed↔NEEDS_REVIEW 양방향 매핑(total·bijective)
- filter_findings_by_disposition + order_findings_for_dashboard(false_positive 디엠퍼사이즈)
- FindingQueryRequest.dispositions 필터(M7 read API seam). 자동 verify는 범위 밖(verifier 트랙)
- legacy finding은 wire값 무시하고 verdict에서 재파생(forward/backward 호환)

evidence: 921 passed (+22 tests, incl. mapping bijective, verifier write→merge→disposition e2e)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UX1CdtfsZ4XJmSKtY2VCWp
…eue, 우선순위/backpressure/rolling

- discovery가 INCLUDED CATALOG 소비(수동 manifest 대체, 미포함 repo skip)
- ls-remote skip(SC-6a): 원격 ref SHA가 커서와 같으면 fetch 생략(유휴 repo), 프로브 실패는
  fail-safe로 over-fetch. incremental job에 job_type 스탬프
- bounded fetch executor + CacheRoots 격리 seam(SC-6b/c, poll mirror≠worker checkout)
- baseline = 신규 per-repo enqueue(SC-3, scan-all 재사용 아님 명시): priority 900>100,
  backpressure(backlog 초과 시 throttle), rolling 1/N 결정적 파티션
- cadence overrun을 notification-log seam으로 알림(SC-6d, 침묵 금지)

evidence: 944 passed (+23 tests). 실 fetch 동시성·N·divisor 등 수치는 박스 게이트.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UX1CdtfsZ4XJmSKtY2VCWp
…ess-eval CLI

- scan-worker@.service 인스턴스 템플릿(%i→worker-id, N 독립 프로세스, Restart=on-failure,
  SuccessExitStatus=0 2) + scan-worker.target
- 타이머 유닛(placeholder OnCalendar, GATE-1 마킹): lease-reaper/incr-poll/baseline/
  freshness-eval/catalog-reconcile(.service+.timer)
- CLI 배선: reap-expired-leases(M2 store op), freshness-eval(M5 evaluator)
- catalog-reconcile 유닛은 fail-closed(실 org GET 거부) — GATE 2까지 DO NOT ENABLE 배너
- FR-4 RepoLease 중복방지 워커 테스트(N 워커 동일 repo 동시 스캔 불가)

evidence: 975 passed (+31 tests). 배포 N프로세스·Restart·실 cadence는 박스 게이트.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UX1CdtfsZ4XJmSKtY2VCWp
…용 + SC-7 no-Scan + trust model

- read_api 쿼리층: findings(disposition 필터·redacted DTO) / freshness 롤업(BREACH_COUNTER O(1)) /
  coverage(CATALOG org N·covered M) / queue backlog
- SC-7: 대시보드 backlog를 full-table Scan 대신 Select=COUNT(status GSI 샤드 fan-out), 아이템 바디 0
- F9 trust model: 기본 127.0.0.1 bind, 비-loopback은 require_auth 없이 거부; DTO는 allowlist 투영
  + 기존 Finding redaction(secretHash만, gitleaks payload null) 재사용
- read-api CLI(스냅샷 JSON, 소켓 안 바인드). 실 HTTP serving·authn은 배포 게이트. M8이 이 DTO 소비
- get_queue_status(레거시 Scan)는 비-대시보드 경로용으로 유지, 대시보드는 새 경로

evidence: 997 passed (+22 contract tests, incl. no_raw_secret_leaks, does_not_full_table_scan, refuses_non_loopback)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UX1CdtfsZ4XJmSKtY2VCWp
- AlertSink 프로토콜 + NotificationLogAlertSink(기존 jsonl seam 재사용) + RecordingAlertSink(테스트)
- 트리거: per-repo incremental/baseline SLA breach, coverage gap, dead-letter 증가,
  queue backlog 임계, M4 cadence overrun — 각 알림이 repo_id/kind/임계/age 컨텍스트 보유
- de-dup/re-notify: (repo,kind)별 renotify 윈도 내 억제, 경과 후 재발화 → 지속 stale가
  스팸도 침묵도 아니게. 상태는 신규 ALERT_STATE 엔티티(idempotent)
- run_freshness_evaluator_with_alerts: M5 스케줄 탐지→dispatcher→sink로 "침묵 불가능" 보장
- freshness-eval CLI(M3)에 sink 배선. 실 Slack/email/webhook은 배포 게이트(1줄 seam)

evidence: 1019 passed (+22, incl. one_fresh_repo_does_not_suppress_a_different_stale_repo,
renotify_fires_again_after_window, freshness_eval_fires_alert_to_notification_log)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UX1CdtfsZ4XJmSKtY2VCWp
통합 리뷰가 잡은 in-scope 갭: M4 cadence-overrun 메커니즘이 빌드·단위테스트는 됐으나
배포 경로(discover-updates 명령)에 호출부가 없어 폴 주기 지연이 침묵하던 문제(설계가
막으려던 사고 모드) 수정.

- cmd_discover_updates에 injectable clock seam + --cadence-seconds/--notification-log
- 사이클 elapsed 측정 → evaluate_poll_cadence → alert_from_cadence_overrun → M9 sink(동일 dispatcher)
- incr-poll.service가 --cadence-seconds 300(GATE-1 placeholder) 전달
- alert_sink.py 허위 docstring 정정(실제 발화 지점 명시)

evidence: 1021 passed (+2: discover_updates_deployed_path_fires_cadence_overrun_alert / within_budget_fires_nothing)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UX1CdtfsZ4XJmSKtY2VCWp
…MOKE)

박스 없이 docker도 없이 Java DynamoDB Local로 4 블로커 핵심을 실 엔진 검증(평소 suite는 skip):
- SC-1 bounded dequeue: 실 GSI1 query + Limit + conditional lease
- SC-2 fence: 실 ConditionExpression이 stale holder의 release를 거부(이중 스캔 방지)
- SC-5 REPO_HEALTH: 실 속성단위 conditional UpdateItem(incremental/baseline 무클로버, 무regress)
- SC-7 backlog: 실 Select=COUNT(full-table Scan 아님)
박스/Alternator에서도 SECURITY_SCANNER_DYNAMO_ENDPOINT만 바꿔 재사용 가능.

evidence: RUN_DDB_LOCAL_SMOKE=1 ...pytest 1 passed (실 dynamodb-local:4567); 기본 suite는 1 skipped

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UX1CdtfsZ4XJmSKtY2VCWp
- poll_fetch: ls-remote가 patterns를 cmd에 미전달 → *patterns 추가(서버측 필터 복원)
- incremental_discovery._cursor_shas_for: glob ref_patterns(DEFAULT="refs/remotes/origin/*")를
  전부 skip해 빈 커서 반환 → ls-remote skip 최적화가 평소 죽어 있던 버그. list_ref_states +
  _ref_matches_patterns(fnmatch) 클라이언트측 매칭으로 수정(기존 테스트가 concrete 패턴으로 가림)
- base.py ScanJob.job_type 기본값 리터럴→JOB_TYPE_INCREMENTAL 상수
- test_scan_worker 타입주석 3-tuple(fence) 정정
- systemd README 중복 섹션번호 9→10
- alert_sink._now_iso naive datetime 가드(ValueError)

evidence: 1026 passed, 1 skipped (+5 tests)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UX1CdtfsZ4XJmSKtY2VCWp
Comment thread tests/test_finding.py Fixed
Comment thread tests/test_finding.py Fixed
Comment thread tests/test_finding.py Fixed
Comment thread tests/test_finding.py Fixed
Comment thread tests/test_read_api.py Fixed
Comment thread tests/test_read_api.py Fixed

@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 implements a major scale redesign of the security scanner, transitioning from a single weekly scan-all oneshot model to a queue-based N-worker pool model. Key additions include catalog-driven incremental polling, baseline per-repo enqueuing with priority separation and backpressure, per-repo freshness evaluation (replacing the global scan health check), a pluggable alert sink with de-duplication/re-notification policies, and a cost-bounded read API snapshot query layer for the admin dashboard. The reviewer's feedback is highly constructive and identifies three key areas of improvement: resolving a potential NameError in store.py due to a missing items_to_repo_leases import, adding a timeout to the git ls-remote subprocess call to prevent network hangs, and using .resolve() in path comparisons to ensure robust cache isolation.

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.

FilterExpression="entityType = :entity_type",
ExpressionAttributeValues={":entity_type": "REPO_LEASE"},
)
for lease in items_to_repo_leases(lease_items):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

코드에서 items_to_repo_leases 함수를 호출하고 있으나, 이 함수가 store.py에 임포트되어 있지 않거나 items.py에 정의되어 있지 않을 가능성이 높습니다. 이로 인해 NameError가 발생할 수 있으므로, 임포트를 확인하거나 리스트 컴프리헨션을 사용하여 [repo_lease_from_item(item) for item in lease_items]와 같이 변경해야 합니다.

Suggested change
for lease in items_to_repo_leases(lease_items):
for lease in [repo_lease_from_item(item) for item in lease_items]:

Comment on lines +80 to +82
result = subprocess.run(
cmd, check=True, capture_output=True, text=True
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

네트워크 작업인 git ls-remote는 원격 서버가 응답하지 않거나 인증을 요구할 때 무한히 대기(hang)할 수 있습니다. 백그라운드 폴러가 차단되는 것을 방지하기 위해 subprocess.run 호출에 적절한 timeout 설정(예: 30초)을 추가하는 것이 안전합니다.

Suggested change
result = subprocess.run(
cmd, check=True, capture_output=True, text=True
)
result = subprocess.run(
cmd, check=True, capture_output=True, text=True, timeout=30
)

worker_checkout_root: Path

def __post_init__(self) -> None:
if self.poll_mirror_root == self.worker_checkout_root:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

poll_mirror_rootworker_checkout_root가 심볼릭 링크, 상대 경로(예: .. 포함) 등으로 인해 서로 다른 경로 문자열을 가지면서도 실제로는 동일한 물리적 디렉터리를 가리킬 수 있습니다. 단순 == 비교 대신 .resolve()를 사용하여 실제 물리적 경로를 비교하는 것이 안전합니다. 또한, 상대 경로 정규화가 올바르게 수행되는지 검증하기 위해 회귀 테스트(예: dir/../targettarget으로 정규화되는지 확인)를 추가해 주세요.

Suggested change
if self.poll_mirror_root == self.worker_checkout_root:
if self.poll_mirror_root.resolve() == self.worker_checkout_root.resolve():
References
  1. When normalizing relative paths, ensure that the resolved relative path (resolving directory traversal components like ..) is returned instead of the unnormalized path, and add regression tests to verify proper normalization (e.g., dir/../target to target).

- 신규 systemd 유닛 6개: ReadWritePaths=/var/log|/var/cache 하드코딩 → LogsDirectory=
  security-scanner / CacheDirectory=security-scanner(systemd가 경로 생성·RW 부여)로 전환,
  scan-worker@ ExecStart는 --notification-log ${LOGS_DIRECTORY}/... 로. 하드코딩 /var 제거
  → governance.public_safety identifier.private-path 통과(게이트 미수정). 기존 scan-all 유닛 불변.
- CodeQL py/non-iterable-in-for-loop(오탐, enum 순회): test_finding.py 4곳 list(Verdict)/
  list(Disposition)로 래핑(동작 동일). test_read_api.py 미사용 import 3개 제거.
- test_systemd_units.py: LogsDirectory/CacheDirectory/${LOGS_DIRECTORY} 어서션으로 갱신.

evidence: 1033 passed, 1 skipped; 6 유닛 machine-local 경로 0

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UX1CdtfsZ4XJmSKtY2VCWp
@pureliture pureliture merged commit 2d607ce into main Jun 20, 2026
9 checks passed
@pureliture pureliture deleted the claude/scale-redesign-seg-a branch June 20, 2026 15:01
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