From 4450c0346baafdd33a5fc34d88dd4a5d088f5afa Mon Sep 17 00:00:00 2001 From: pureliture Date: Fri, 19 Jun 2026 16:27:23 +0900 Subject: [PATCH 1/2] feat(cli): env-gate scan-all verifier auto-triage (trigger-agnostic, #3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enable periodic verifier auto-triage via env, independent of how scan-all is scheduled. The scan-all runtime already runs the verifier + writes terminal dispositions under --verify-artifacts; the only gap was that it could not be turned on without an explicit CLI flag. - scan.py: --verify-artifacts is now a BooleanOptionalAction defaulting from SECURITY_SCANNER_VERIFY_ARTIFACTS (truthy=on, unset/0/false=off); --no-verify-artifacts overrides env-on. host/model/timeout/min-confidence already env-fallback via resolve_verifier_config. DEFAULT-OFF preserved. - deploy/systemd/README.md: trigger-agnostic enablement section (env table, fail-closed=needs_review semantics, exit 2 alerting). No .service edits — systemd is just one scheduler; verification rides any scan-all invocation. Scope correction (user feedback): the repo is poll-based (discover-updates → scan-worker), not PR/webhook-triggered; systemd is not essential. Per-change verification in the incremental scan-worker path is deferred as a separate follow-up. Spec under docs/workbench/specs/verifier-periodic-wiring/. Co-Authored-By: Claude Opus 4.8 --- deploy/systemd/README.md | 31 +++ .../specs/verifier-periodic-wiring/design.md | 178 ++++++++++++++++++ .../verifier-periodic-wiring/requirements.md | 175 +++++++++++++++++ src/security_scanner/cli/commands/scan.py | 19 +- tests/test_cli_scan_all_verifier_env.py | 49 +++++ 5 files changed, 450 insertions(+), 2 deletions(-) create mode 100644 docs/workbench/specs/verifier-periodic-wiring/design.md create mode 100644 docs/workbench/specs/verifier-periodic-wiring/requirements.md create mode 100644 tests/test_cli_scan_all_verifier_env.py diff --git a/deploy/systemd/README.md b/deploy/systemd/README.md index caf0fe9..34ce6bc 100644 --- a/deploy/systemd/README.md +++ b/deploy/systemd/README.md @@ -229,6 +229,37 @@ for the full schema. --- +## 5b. Optional: verifier auto-triage (Ollama) + +`scan-all` can run the Ollama verifier after scanning and record terminal +finding dispositions (false_positive / true_positive). This is **DEFAULT-OFF** +and **trigger-agnostic** — it rides whatever runs `scan-all` (this timer, cron, +or a manual run), so it needs no systemd-specific wiring. Enable it purely via +environment variables: + +| Env var | Meaning | +| --- | --- | +| `SECURITY_SCANNER_VERIFY_ARTIFACTS` | `1`/`true`/`yes`/`on` enables verification; unset/`0`/`false` keeps it off. | +| `SECURITY_SCANNER_OLLAMA_HOST` | Ollama-compatible host, e.g. `http://127.0.0.1:11434`. | +| `SECURITY_SCANNER_OLLAMA_MODEL` | Model name. | +| `SECURITY_SCANNER_OLLAMA_TIMEOUT_SECONDS` | Optional HTTP timeout (default 30). | +| `SECURITY_SCANNER_OLLAMA_MIN_CONFIDENCE` | Optional min confidence (default 0.60). | + +Notes: + +- The verifier reads only redacted metadata; raw secrets never leave the host. +- It **fails closed**: if Ollama is unreachable or low-confidence, the finding is + left `needs_review` (no disposition written) and the scan still records all + findings — verification never destructively fails a scan. A verifier failure + surfaces only as exit code `2` (alertable; see §6). +- The CLI flag `--verify-artifacts` / `--no-verify-artifacts` overrides the env + default for one-off runs. +- This is a full-sweep triage: newly detected findings are verified on the next + `scan-all` run. Per-change verification in the incremental `scan-worker` path is + out of scope here (separate follow-up). + +--- + ## 6. Exit code semantics for alerting The service uses `SuccessExitStatus=0 3`, so systemd treats exit codes `0` and diff --git a/docs/workbench/specs/verifier-periodic-wiring/design.md b/docs/workbench/specs/verifier-periodic-wiring/design.md new file mode 100644 index 0000000..732dc92 --- /dev/null +++ b/docs/workbench/specs/verifier-periodic-wiring/design.md @@ -0,0 +1,178 @@ +# design.md — 주기 scan 경로에 Ollama verifier 자동 triage 배선 (FEATURE #3) + +> **정정 (실행 중 scope 보정, 사용자 피드백):** 이 repo는 PR/webhook 트리거가 없다(코드에 없음). 변화 감지는 poll(`discover-updates` ls-remote → `SCAN_JOB` → `scan-worker`)이고 systemd는 그걸 주기 실행하는 스케줄러일 뿐이다. 따라서 본 feature의 핵심은 systemd 템플릿 편집이 아니라 **트리거-무관 env-gate**(`SECURITY_SCANNER_VERIFY_ARTIFACTS`)로 scan-all 검증을 켜는 것이다. systemd `.service` 편집은 범위에서 제외하고(어떤 스케줄러로 돌리든 동일), 활성화 절차는 `deploy/systemd/README.md`에 env 표로만 둔다. verification은 full-sweep triage(새 finding은 다음 scan-all에서 검증); 증분 `scan-worker` 즉시검증은 무거운 별도 후속으로 남긴다. + + +## 1. 개요 + +`scan-all` 런타임은 이미 `--verify-artifacts` 설정 시 verifier를 실행하고 terminal +disposition을 기록한다. 남은 격차는 주기 실행(systemd)이 이를 켜지 않는다는 점뿐이다. +본 설계는 (1) 기존 verify 플래그를 env-var 기본값으로 노출하고, (2) verifier-enabled +systemd 템플릿 변형과 README를 추가해, 주기 실행이 **public-safe / env-gated / +DEFAULT-OFF / local-first**로 자동 triage를 켜도록 한다. Ollama 다운은 런타임의 +fail-closed(=needs_review) 계약 그대로 비파괴 처리된다. 신규 런타임 동작·게이트는 없다. + +## 2. 요구사항 참조 + +- FR-1~FR-6, NFR-1~NFR-5, AC-1~AC-6 (requirements.md). +- 핵심: env→argparse-default 흡수(FR-1/FR-2/FR-4), systemd 템플릿/README(FR-5/FR-6), + fail-closed 비파괴(NFR-2), DEFAULT-OFF(FR-1). + +## 3. 접근 후보와 선택 + +### 후보 A — env-var 기본값화 + verifier-enabled systemd 변형 (선택) +- `--verify-artifacts`에 `SECURITY_SCANNER_VERIFY_ARTIFACTS` truthy 게이트를 default로 흡수. +- host/model/timeout/min-confidence/config는 이미 `resolve_verifier_config`가 env로 읽음. +- systemd는 env만 설정(EnvironmentFile=- 또는 주석 Environment=)하고 기본은 off 유지. +- 장점: 런타임 신규 코드 최소, 기존 `SECURITY_SCANNER_STORAGE_BACKEND` 컨벤션과 정합, + CLI/systemd 양쪽에서 동일하게 동작, DEFAULT-OFF 자연 보장. +- 단점: boolean env 파싱 헬퍼 1개 신설 필요(선례 없음). + +### 후보 B — verifier 전용 systemd 템플릿 파일(별도 .service)만 추가 +- env는 손대지 않고, `--verify-artifacts ...`를 inline한 두 번째 `.service` 추가. +- 장점: 런타임 코드 0 변경. +- 단점: CLI에서는 여전히 매번 플래그 수동 입력, env-convention 불일치, 템플릿 수 2배, + inline 플래그에 host/model이 노출되어 운영자 편집 부담. FR-2(명시 플래그 없이 env로 on) + 미충족. + +### 후보 C — governance gate 신설 +- 런타임에 검증 활성화 정책 게이트를 도입. +- 단점: main에 governance 런타임 모듈 부재 → 신규 기계, design 제외(no gating)와 충돌, + YAGNI 위반. **배제.** + +**선택: 후보 A.** 런타임 변경을 boolean env 헬퍼 1개로 최소화하면서 CLI·systemd 양쪽에 +일관된 env-gated 활성화를 제공하고 DEFAULT-OFF/fail-closed를 그대로 유지한다. + +## 4. 아키텍처 + +``` +[systemd .service] + Environment= / EnvironmentFile=- (SECURITY_SCANNER_VERIFY_ARTIFACTS=1, + ExecStart: uv run security-scanner scan-all OLLAMA_HOST/MODEL ...) + | + v +[cli/_args.add_scan_all_verifier_args] (신규/확장) + --verify-artifacts default <- _env_truthy(SECURITY_SCANNER_VERIFY_ARTIFACTS) + --ollama-host/... default <- None (resolve 단계에서 env fallback) + | + v +[cli/commands/scan.cmd_scan_all] (변경 없음) + verifier_config_factory=_scan_all_verifier_config_from_args(args) + | + v +[runtime/scan_all.run_scan_all] (변경 없음) + resolve verifier_config -> per-finding verify -> record disposition + transport 실패 -> needs_review (fail-closed) -> exit 2, 비파괴 + | + v +[runtime/verify_artifact.resolve_verifier_config] (변경 없음) + SECURITY_SCANNER_OLLAMA_HOST/MODEL/TIMEOUT_SECONDS/MIN_CONFIDENCE/API_KEY_ENV +``` + +검증을 켜는 권한은 **env**가 가지고, systemd 템플릿은 그 env를 설정하는 얇은 표면이다. +런타임 로직(`scan_all.py`, `verify_artifact.py`, `client.py`)은 손대지 않는다. + +## 5. 구성요소 + +### 5.1 `cli/_args.py` (또는 `cli/commands/scan.py`) — env-default 흡수 +- 신규 모듈 헬퍼 `_env_truthy(name: str) -> bool`: `os.environ.get(name)`를 읽어 + `{"1","true","yes","on"}`(소문자화)면 True, 그 외 False. (NFR-5: 최소 헬퍼) +- `--verify-artifacts`를 `action="store_true"`로 두되 `default=_env_truthy( + "SECURITY_SCANNER_VERIFY_ARTIFACTS")`로 설정. store_true는 default가 False가 보통이나 + 명시 default를 주면 env 기반 on이 가능하다(플래그 명시 시 항상 True로 override → FR-4). +- host/model/timeout/min-confidence/config 플래그는 default=None 유지(이미 resolve 단계가 + env fallback). 단 README/help에 env 이름을 문서화. +- 배치 위치: `add_storage_args`가 env를 흡수하는 것과 동일하게, scan-all 파서를 구성하는 + 곳에 두어 컨벤션 일관성 확보(NFR-4). + +### 5.2 `deploy/systemd/security-scanner-scan-all.service` (system-level) +- 기본 `ExecStart`는 현행 유지(검증 off). +- 추가: + - `EnvironmentFile=-/etc/security-scanner/verifier.env` (검증 env 주입 지점, optional) + - 주석 블록: 검증 켜기 = `Environment=SECURITY_SCANNER_VERIFY_ARTIFACTS=1`, + `Environment=SECURITY_SCANNER_OLLAMA_HOST=http://127.0.0.1:11434`, + `Environment=SECURITY_SCANNER_OLLAMA_MODEL=` 또는 대안 ExecStart + `... scan-all --verify-artifacts --ollama-host ... --ollama-model ...`. + - api_key는 `Environment=SECURITY_SCANNER_OLLAMA_API_KEY_ENV=OLLAMA_API_KEY`처럼 + **이름만** 노출하고 실제 토큰은 verifier.env/별도 env로. + +### 5.3 `deploy/systemd/user/security-scanner-scan-all.service` (user-level) +- 동일 패턴을 `%h` 기준으로: `EnvironmentFile=-%h/.config/security-scanner/verifier.env` + + 주석 Environment= 예시. 검증 기본 off. + +### 5.4 `deploy/systemd/README.md` +- 신규 절: "Enable verifier triage (optional)". + - env 표: + + | Env | 의미 | 기본값 | + | --- | --- | --- | + | `SECURITY_SCANNER_VERIFY_ARTIFACTS` | 주기 실행에서 verifier 켜기(truthy) | off | + | `SECURITY_SCANNER_OLLAMA_HOST` | Ollama 호환 endpoint | (없음, on이면 필수) | + | `SECURITY_SCANNER_OLLAMA_MODEL` | 모델 이름 | (없음, on이면 필수) | + | `SECURITY_SCANNER_OLLAMA_TIMEOUT_SECONDS` | HTTP timeout | 30.0 | + | `SECURITY_SCANNER_OLLAMA_MIN_CONFIDENCE` | fail-closed 임계 | 0.60 | + | `SECURITY_SCANNER_OLLAMA_API_KEY_ENV` | 토큰을 읽을 env '이름' | (없음) | + + - fail-closed 설명: Ollama/verifier 다운 시 모든 해당 finding은 needs_review로 떨어지고 + disposition은 쓰이지 않으며 scan은 finding/summary를 정상 기록한다. exit code 2(alertable)만 + 발생하고 파괴적 실패는 없다. + - exit code 표에 "검증 실패 포함 시 2" 명시(기존 표 재사용). + - 로컬 Ollama 전제, public-safe placeholder만 노출, 토큰은 EnvironmentFile로. + +## 6. 데이터 흐름 + +1. timer → `.service` 활성화 → systemd가 env(또는 EnvironmentFile) 로드. +2. `uv run security-scanner scan-all` 실행 → 파서가 + `SECURITY_SCANNER_VERIFY_ARTIFACTS`를 읽어 `args.verify_artifacts` 결정. +3. on이면 `_scan_all_verifier_config_from_args`가 `resolve_verifier_config` 호출 → + host/model/timeout/min_confidence/api_key_env를 env에서 채움. +4. `run_scan_all`이 fetch→scan→finding마다 `verifier.verify`→terminal verdict면 + `record_verifier_disposition`로 disposition 기록. +5. transport 실패면 `needs_review`(비파괴) + public-safe failure 항목 → exit 2. +6. notification log summary에 `verification` 요약(attempted/terminal/needs_review/ + dispositions_written/failed/failures) 기록. + +## 7. 에러 처리 + +- Ollama 다운/timeout/HTTP 오류: `OllamaChatVerifier.verify`가 `needs_review`로 변환 + (fail-closed). disposition 미기록, finding/summary 정상 기록, exit 2. (NFR-2) +- 검증 on인데 host/model 미해결: `resolve_verifier_config`가 `ValueError` → + `run_scan_all`이 `verifier_config_failure`(exit 2)로 public-safe 보고. raw 경로/디테일은 + `_public_safe_exception`으로 클래스명만 노출. +- disposition store 미가용/비-writer: 기존 `_verifier_store_unavailable_summary` / + writer-unavailable 분기로 public-safe failure 처리(변경 없음). +- truthy env 오기입(`maybe`, `2` 등): `_env_truthy`가 False로 해석 → 안전하게 off. + README에 허용 값 명시. + +## 8. 테스트 전략 + +- 단위(`tests/test_cli_scan_all.py` 확장): + - `_env_truthy` 케이스 테이블(`1/true/TRUE/yes/on` → True; 미설정/빈/`0/false/no/off/maybe` + → False). + - env-on → verifier 호출됨 + disposition 기록(monkeypatch.setenv + `SECURITY_SCANNER_VERIFY_ARTIFACTS`, `..._OLLAMA_HOST/MODEL`, OllamaChatVerifier 패치). + - 명시 `--verify-artifacts` 미지정 + env-off → verifier 미호출(기존 + `test_scan_all_default_does_not_call_verifier` 유지/보강, AC-1). + - env-on + Ollama 다운(transport가 예외) → exit 2, finding/summary 기록, 모든 해당 finding + needs_review(AC-3, NFR-2). 기존 `..._surfaces_public_safe_failures` 패턴 재사용. + - env-on + host 미설정 → `verifier_config_failure` exit 2(AC-5). 기존 + `..._verifier_config_failure_writes_fatal_and_summary` 패턴 재사용. + - 플래그 우선순위: env-off인데 `--verify-artifacts` 명시 → on(AC-4). +- 문서 검증(수동/lint): 두 `.service`와 README가 raw secret/토큰을 포함하지 않고 env 표/ + fail-closed/exit 2를 포함(AC-6). + +## 9. 마일스톤 + +- M1: `_env_truthy` 헬퍼 + `--verify-artifacts` env-default 흡수 + 단위 테스트(AC-1/2/4). +- M2: fail-closed/exit 2/config-failure 회귀 테스트 정리(AC-3/5). +- M3: system/user `.service` 템플릿에 검증 env 주석/EnvironmentFile 추가(AC-6). +- M4: README 검증 절(env 표·fail-closed·exit 2·로컬 Ollama·시크릿 취급) 추가(AC-6). + +## 10. 열린 질문 + +- `disposition_store_factory`와 `store_factory`가 같은 DynamoDB store를 두 번 생성하는 + 현행 동작이 주기 실행에서 연결 비용을 키우는가? 본 feature 범위 밖이며 별도 최적화 후보로 둔다. +- truthy env 허용 토큰 집합을 다른 향후 boolean env(있다면)와 공유할지 — 현재는 단일 사용처라 + 로컬 헬퍼로 충분(YAGNI). 사용처가 늘면 공용화 검토. +- 검증을 켠 주기 실행의 Ollama 호출 부하 상한을 timer 빈도/`timeout_seconds` 외에 추가로 둘지 — + 현재는 문서 경고로 충분, 신규 동시성/배치 제어는 범위 밖. \ No newline at end of file diff --git a/docs/workbench/specs/verifier-periodic-wiring/requirements.md b/docs/workbench/specs/verifier-periodic-wiring/requirements.md new file mode 100644 index 0000000..fb482a4 --- /dev/null +++ b/docs/workbench/specs/verifier-periodic-wiring/requirements.md @@ -0,0 +1,175 @@ +# requirements.md — 주기 scan 경로에 Ollama verifier 자동 triage 배선 (FEATURE #3) + +> **정정 (실행 중 scope 보정, 사용자 피드백):** 이 repo는 PR/webhook 트리거가 없다(코드에 없음). 변화 감지는 poll(`discover-updates` ls-remote → `SCAN_JOB` → `scan-worker`)이고 systemd는 그걸 주기 실행하는 스케줄러일 뿐이다. 따라서 본 feature의 핵심은 systemd 템플릿 편집이 아니라 **트리거-무관 env-gate**(`SECURITY_SCANNER_VERIFY_ARTIFACTS`)로 scan-all 검증을 켜는 것이다. systemd `.service` 편집은 범위에서 제외하고(어떤 스케줄러로 돌리든 동일), 활성화 절차는 `deploy/systemd/README.md`에 env 표로만 둔다. verification은 full-sweep triage(새 finding은 다음 scan-all에서 검증); 증분 `scan-worker` 즉시검증은 무거운 별도 후속으로 남긴다. + + +## 1. 배경 + +`security-scanner scan-all` 런타임은 이미 verifier 자동 triage를 완전히 지원한다. +`cmd_scan_all`(`src/security_scanner/cli/commands/scan.py`)이 `verifier_config_factory`와 +`disposition_store_factory`를 `ScanAllRequest`에 주입하고, +`run_scan_all`(`src/security_scanner/runtime/scan_all.py`)이 verifier config를 resolve해 +finding마다 verifier를 호출하고 terminal disposition을 기록하며 notification log에 +`verification` 요약을 남긴다. + +**유일한 격차**: 주기 실행을 담당하는 systemd 유닛 +(`deploy/systemd/security-scanner-scan-all.service`, +`deploy/systemd/user/security-scanner-scan-all.service`)과 +`deploy/systemd/README.md`가 검증을 전혀 켜지 않는다. 두 `.service`의 `ExecStart`는 +`scan-all --notification-log ...`만 호출하고 verifier 플래그/env를 언급하지 않는다. + +따라서 본 feature의 범위는 **신규 런타임 동작이 아니라**, 주기 실행이 검증을 +켤 수 있도록 (a) 기존 verify 플래그를 env-var 기본값으로 노출하고, +(b) verifier-enabled systemd 템플릿 변형과 README를 추가하는 것이다. +public-safe / config·env-gated / **DEFAULT-OFF** / local-first 를 모두 만족한다. + +## 2. 범위 + +### In-scope +- 기존 `scan-all` 플래그(`--verify-artifacts`, `--verifier-config`, `--ollama-host`, + `--ollama-model`, `--timeout-seconds`, `--min-confidence`)에 대한 env-var 기본값화. + 특히 `--verify-artifacts`의 boolean 게이트 env(`SECURITY_SCANNER_VERIFY_ARTIFACTS`). +- system-level / user-level systemd `.service` 템플릿에 검증을 켜는 + **opt-in, 주석/EnvironmentFile 기반** 안내 추가. +- `deploy/systemd/README.md`에 검증 활성화 절차, env 표, fail-closed 의미, + exit code 2 alerting, 로컬 Ollama 전제, 시크릿 취급 가이드 추가. +- DEFAULT-OFF 및 fail-closed 비파괴 동작을 잠그는 테스트. + +### Out-of-scope (repo 설계 제외 + YAGNI) +- target-repo PR gating / 차단. +- notification·endpoint 통합(알림 전송, webhook 등). +- 신규 governance/autopilot 런타임 게이트(main에 해당 모듈 없음). +- verifier preflight/doctor 헬스체크 신설. +- managed DynamoDB, rollout/배포 자동화. +- redaction되지 않은 raw secret 노출(이미 런타임이 redact). + +## 3. 기능 요구사항 + +- FR-1: 모든 verify 관련 env가 미설정이면 주기 실행은 **검증 없이** 동작해야 한다 + (DEFAULT-OFF). 즉 `verify_artifacts=False`이고 `verifier_config`는 `None`이다. +- FR-2: `SECURITY_SCANNER_VERIFY_ARTIFACTS`가 truthy(`1/true/yes/on`, 대소문자 무시)이면 + `scan-all`이 명시적 `--verify-artifacts` 없이도 검증을 켜야 한다. 그 외(미설정, 빈 문자열, + `0/false/no/off`)는 off. +- FR-3: 검증이 켜진 상태에서 host/model이 env(`SECURITY_SCANNER_OLLAMA_HOST`, + `SECURITY_SCANNER_OLLAMA_MODEL`) 또는 config로 제공되지 않으면 `resolve_verifier_config`가 + 기존대로 `ValueError`를 내고, `run_scan_all`이 이를 public-safe하게 + `verifier_config_failure`(exit 2)로 보고해야 한다(기존 동작 유지·회귀 방지). +- FR-4: 명시 CLI 플래그는 env보다 우선해야 한다(argparse default override 의미). +- FR-5: systemd 템플릿은 기본 ExecStart를 **검증 off**로 유지하고, 검증을 켜는 방법을 + (a) `EnvironmentFile=-` 로 가리키는 verifier env 파일, 또는 (b) 주석 처리된 inline + `Environment=` 라인 + 대안 `ExecStart ... --verify-artifacts` 예시로 제시해야 한다. +- FR-6: README는 다음을 포함해야 한다 — env 변수 표(이름/의미/기본값), 검증 켜기 절차, + fail-closed=needs_review 의미, exit code 2가 alertable임, 로컬 Ollama 전제, + api_key는 env '이름'만 노출하고 토큰은 EnvironmentFile/별도 env로만 주입. + +## 4. 비기능 요구사항 + +- NFR-1 (public-safe): 템플릿/문서/로그에 raw secret, 사설 endpoint, 토큰을 노출하지 않는다. + host/model은 public-safe placeholder(예: `http://127.0.0.1:11434`)만 보여준다. +- NFR-2 (fail-closed, 비파괴): Ollama/verifier 다운이 scan을 파괴적으로 실패시키면 안 된다. + transport 실패는 `needs_review`로 떨어지고 finding/summary 레코드는 정상 기록되어야 한다. +- NFR-3 (local-first): 검증은 로컬 Ollama 호환 endpoint 전제로만 문서화한다. +- NFR-4 (consistency): env→argparse-default 흡수는 `add_storage_args`의 + `SECURITY_SCANNER_STORAGE_BACKEND` 컨벤션과 동일한 위치/방식이어야 한다. +- NFR-5 (YAGNI): 신규 추상화·게이트·헬스체크를 추가하지 않는다. boolean env 파싱만 + 최소 헬퍼로 도입한다. + +## 5. 코드 기반 사실 (grounding) + +- `cmd_scan_all`은 `verifier_config_factory=lambda: _scan_all_verifier_config_from_args(args)`, + `disposition_store_factory=lambda: store_from_args(args)`를 이미 전달한다 + (`scan.py:482-512`). +- `_scan_all_verifier_config_from_args`는 `args.verify_artifacts`가 False면 `None`을 + 반환한다(`scan.py:468-479`). +- `resolve_verifier_config`는 `SECURITY_SCANNER_OLLAMA_HOST/MODEL/TIMEOUT_SECONDS/ + MIN_CONFIDENCE/API_KEY_ENV`를 이미 env fallback으로 읽는다(`verify_artifact.py:73-128`). +- `OllamaChatVerifier.verify`는 `TimeoutError`→`needs_review(error="timeout")`, + `OSError/URLError/HTTPError`→`needs_review(error=...)`로 fail-closed 한다 + (`client.py:69-82`). +- `_run_verifier_disposition_writes`는 `needs_review`(verdict가 terminal이 아님)이면 + disposition을 쓰지 않고 `needs_review` 카운트만 올리며, `verifier_result.error`가 있으면 + public-safe failure 항목을 추가한다 → `failed>0`이면 exit 2(`scan_all.py:354-363`, + `455-594`). +- `add_storage_args`는 `os.environ.get("SECURITY_SCANNER_STORAGE_BACKEND")`를 + argparse default로 흡수한다(`_args.py:20-30`). +- main `src/security_scanner`에 governance/autopilot 런타임 모듈은 없다(루트 `governance/`는 + stale `.worktrees/`에만 존재). +- 코드베이스에 boolean env 파싱 선례는 없다(모든 기존 env는 문자열→default). +- `test_scan_all_default_does_not_call_verifier`(`tests/test_cli_scan_all.py:384`)가 + default-off를 이미 단언한다. + +## 6. 확정 결정 (자문자답) + +### Q1. 활성화 메커니즘은 무엇으로 하나? env-default vs verifier-enabled systemd 템플릿 vs governance gate +**A.** **env-var 기본값화 + verifier-enabled systemd 변형 템플릿의 조합.** 검증을 켜는 권한은 +env가 가지고, systemd 템플릿은 그 env만 설정하는 얇은 표면이 된다. +**근거.** (1) `add_storage_args`가 `SECURITY_SCANNER_STORAGE_BACKEND`를 argparse default로 +흡수하는 컨벤션이 이미 있고, `resolve_verifier_config`가 `SECURITY_SCANNER_OLLAMA_*`를 이미 +읽으므로 런타임 신규 코드가 최소다. (2) systemd는 SCM 토큰을 `EnvironmentFile=-`로 주입하는 +패턴이 이미 있어, 동일 패턴으로 verifier env를 주입하면 일관적이다. (3) governance gate는 +main에 런타임 모듈이 없어 신규 기계가 필요 → YAGNI/범위 밖. + +### Q2. governance gate를 활성화 축으로 둘 수 있나? +**A.** 배제한다. +**근거.** `src/security_scanner`에 governance/autopilot 런타임 모듈이 존재하지 않는다(루트 +`governance/`는 stale `.worktrees/`에만 있음). 새 게이트는 신규 추상화이자 design 제외 항목 +(no gating, design-only)과 충돌한다. + +### Q3. `--verify-artifacts`(boolean)를 어떻게 env로 기본값화하나? +**A.** 작은 `_env_truthy` 헬퍼(`1/true/yes/on` 대소문자 무시)를 도입해 +`SECURITY_SCANNER_VERIFY_ARTIFACTS`를 `--verify-artifacts`의 default로 흡수한다. +미설정/빈 문자열/`0/false/no/off`는 False. +**근거.** 코드베이스에 boolean env 파싱 선례가 없고 기존 env는 전부 문자열→default였다. +argparse `store_true`는 default=False이므로, default를 env로 계산해 넘기는 최소 변경이 +`SECURITY_SCANNER_SCM_PROVIDER` 패턴과 정합한다. + +### Q4. verifier/Ollama가 다운이면 어떻게 처리하나? +**A.** 런타임 계약 그대로 **fail-closed = needs_review**. 신규 처리 코드 없음. +**근거.** `OllamaChatVerifier.verify`가 timeout/OSError/URLError/HTTPError를 모두 +`needs_review`로 변환하고, `disposition_status_for_verdict`가 needs_review에 대해 `None`을 +반환해 terminal disposition을 쓰지 않는다. finding/summary 레코드는 정상 기록되므로 +scan은 파괴적으로 실패하지 않고, `verifier_result.error`로 인해 exit 2(alertable)만 발생한다. + +### Q5. DEFAULT-OFF는 어떻게 보장/검증하나? +**A.** 모든 verify env 미설정 시 `verify_artifacts=False` → +`_scan_all_verifier_config_from_args`가 `None` → `_run_verifier_disposition_writes`가 `None` +반환. `test_scan_all_default_does_not_call_verifier`로 회귀를 잠그고, env-on 시 verifier가 +호출되는 케이스를 추가한다. +**근거.** 기존 default-off 테스트가 이미 존재하므로 회귀 안전망이 확보된다. + +### Q6. systemd 템플릿/README에 무엇을 보여줄까? +**A.** 기본 ExecStart는 verify-off 유지. 검증 켜기는 (a) `EnvironmentFile=-`이 가리키는 +verifier env 파일 또는 (b) 주석 처리된 `Environment=` + 대안 `ExecStart --verify-artifacts` +예시. README에는 env 표, 켜는 절차, fail-closed 의미, exit 2 alerting, 로컬 Ollama 전제, +api_key는 env 이름만 노출. +**근거.** public repo이므로 placeholder만 노출해야 하고, SCM 토큰 `EnvironmentFile=-` +컨벤션과 톤을 맞추면 운영자 학습 비용이 낮다. + +### Q7. api_key 노출 위험은? +**A.** 없음. 템플릿엔 `OLLAMA_API_KEY` 같은 **env 이름** placeholder만 둔다. +**근거.** `api_key_env`는 토큰이 아니라 env 변수 이름만 보관하고, `_headers`가 +`os.environ`에서 실제 토큰을 읽는다. 토큰 자체는 `EnvironmentFile`/별도 env로만 주입. + +### Q8. verifier preflight/doctor 헬스체크를 추가할까? +**A.** 배제(YAGNI). +**근거.** fail-closed가 이미 안전망이고 `doctor`에 verifier 체크 선례가 없다. 추가하면 +범위 확대와 신규 표면이 생긴다. + +### Q9. 어느 systemd 변형까지 다룰까? +**A.** system-level과 user-level 둘 다 동일 env 컨벤션으로 일관 적용. +**근거.** README가 두 flavor를 동급으로 문서화하므로 한쪽만 켜면 비대칭/혼란. + +## 7. 수용 기준 + +- AC-1: env 전무 + 플래그 전무로 `scan-all` 실행 시 verifier가 호출되지 않고 + notification log summary에 `verification`이 없다(또는 None). +- AC-2: `SECURITY_SCANNER_VERIFY_ARTIFACTS=true` + `SECURITY_SCANNER_OLLAMA_HOST/MODEL` + 설정 시, 명시 플래그 없이도 verifier가 finding마다 호출되고 terminal verdict는 disposition으로 + 기록된다. +- AC-3: 같은 상태에서 Ollama가 다운이면 scan은 finding/summary 레코드를 정상 기록하고 + exit 2를 반환하며, 모든 해당 finding은 needs_review로 카운트된다(파괴적 실패 없음). +- AC-4: `--verify-artifacts`(또는 명시 host/model)가 env와 충돌하면 CLI 플래그가 우선한다. +- AC-5: 검증 on인데 host/model 미해결이면 `verifier_config_failure`(exit 2)로 public-safe하게 + 보고된다. +- AC-6: 두 systemd `.service` 템플릿과 README가 검증 켜는 방법, env 표, fail-closed/exit 2 + 의미를 포함하며 raw secret/토큰을 노출하지 않는다. \ No newline at end of file diff --git a/src/security_scanner/cli/commands/scan.py b/src/security_scanner/cli/commands/scan.py index 10bacee..9123e78 100644 --- a/src/security_scanner/cli/commands/scan.py +++ b/src/security_scanner/cli/commands/scan.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse +import os import signal import sys import threading @@ -199,8 +200,13 @@ def register(subparsers) -> None: ) scan_all_parser.add_argument( "--verify-artifacts", - action="store_true", - help="Run the verifier and write terminal finding dispositions after scanning.", + action=argparse.BooleanOptionalAction, + default=_env_truthy(os.environ.get("SECURITY_SCANNER_VERIFY_ARTIFACTS")), + help=( + "Run the verifier and write terminal finding dispositions after " + "scanning. Defaults to SECURITY_SCANNER_VERIFY_ARTIFACTS (truthy = on; " + "unset/0/false = off). Use --no-verify-artifacts to override env-on." + ), ) scan_all_parser.add_argument( "--verifier-config", @@ -465,6 +471,15 @@ def _render_residual(repo: str, residuals: list[BranchResidual]) -> str: return "\n".join(lines) + "\n" +def _env_truthy(value: str | None) -> bool: + """Return True for truthy env strings (1/true/yes/on, case-insensitive). + + Unset, empty, and 0/false/no/off are False, so periodic verification stays + DEFAULT-OFF unless an operator explicitly opts in via env. + """ + return (value or "").strip().lower() in ("1", "true", "yes", "on") + + def _scan_all_verifier_config_from_args(args: argparse.Namespace): if not args.verify_artifacts: return None diff --git a/tests/test_cli_scan_all_verifier_env.py b/tests/test_cli_scan_all_verifier_env.py new file mode 100644 index 0000000..e9de670 --- /dev/null +++ b/tests/test_cli_scan_all_verifier_env.py @@ -0,0 +1,49 @@ +"""Env-gated --verify-artifacts default for the periodic scan path (#3).""" + +from __future__ import annotations + +from security_scanner.cli.app import build_parser +from security_scanner.cli.commands.scan import _env_truthy + +_ENV = "SECURITY_SCANNER_VERIFY_ARTIFACTS" + + +def _verify_default(monkeypatch, value: str | None) -> bool: + if value is None: + monkeypatch.delenv(_ENV, raising=False) + else: + monkeypatch.setenv(_ENV, value) + # default is computed at parser-build time, so build after setting env + args = build_parser().parse_args(["scan-all"]) + return args.verify_artifacts + + +def test_default_off_when_env_unset(monkeypatch): + assert _verify_default(monkeypatch, None) is False + + +def test_env_truthy_enables_verification(monkeypatch): + for value in ("1", "true", "YES", "On", " on "): + assert _verify_default(monkeypatch, value) is True, value + + +def test_env_falsey_stays_off(monkeypatch): + for value in ("0", "false", "no", "off", ""): + assert _verify_default(monkeypatch, value) is False, value + + +def test_no_flag_overrides_env_on(monkeypatch): + monkeypatch.setenv(_ENV, "1") + args = build_parser().parse_args(["scan-all", "--no-verify-artifacts"]) + assert args.verify_artifacts is False + + +def test_explicit_flag_on_without_env(monkeypatch): + monkeypatch.delenv(_ENV, raising=False) + args = build_parser().parse_args(["scan-all", "--verify-artifacts"]) + assert args.verify_artifacts is True + + +def test_env_truthy_helper(): + assert all(_env_truthy(v) for v in ("1", "true", "YES", "on")) + assert not any(_env_truthy(v) for v in (None, "", "0", "false", "no", "off", "x")) From 4b387b1b1c0b23aaee976d936eb24fe78032d296 Mon Sep 17 00:00:00 2001 From: pureliture Date: Fri, 19 Jun 2026 17:24:59 +0900 Subject: [PATCH 2/2] fix: address PR #40 review (verifier env docs + helper) - README: document SECURITY_SCANNER_OLLAMA_API_KEY_ENV in the verifier env table. - scan.py: simplify _env_truthy (early `if not value` instead of `(value or "")`). - design.md: correct _env_truthy signature (value:str|None, caller passes os.environ.get) and the BooleanOptionalAction default to match the impl. Co-Authored-By: Claude Opus 4.8 --- deploy/systemd/README.md | 1 + .../specs/verifier-periodic-wiring/design.md | 12 +++++++----- src/security_scanner/cli/commands/scan.py | 4 +++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/deploy/systemd/README.md b/deploy/systemd/README.md index 34ce6bc..a95f18b 100644 --- a/deploy/systemd/README.md +++ b/deploy/systemd/README.md @@ -244,6 +244,7 @@ environment variables: | `SECURITY_SCANNER_OLLAMA_MODEL` | Model name. | | `SECURITY_SCANNER_OLLAMA_TIMEOUT_SECONDS` | Optional HTTP timeout (default 30). | | `SECURITY_SCANNER_OLLAMA_MIN_CONFIDENCE` | Optional min confidence (default 0.60). | +| `SECURITY_SCANNER_OLLAMA_API_KEY_ENV` | Optional: name of the env var holding the API key (the token itself stays in that separate env var, never inline). | Notes: diff --git a/docs/workbench/specs/verifier-periodic-wiring/design.md b/docs/workbench/specs/verifier-periodic-wiring/design.md index 732dc92..2acacea 100644 --- a/docs/workbench/specs/verifier-periodic-wiring/design.md +++ b/docs/workbench/specs/verifier-periodic-wiring/design.md @@ -75,11 +75,13 @@ fail-closed(=needs_review) 계약 그대로 비파괴 처리된다. 신규 런 ## 5. 구성요소 ### 5.1 `cli/_args.py` (또는 `cli/commands/scan.py`) — env-default 흡수 -- 신규 모듈 헬퍼 `_env_truthy(name: str) -> bool`: `os.environ.get(name)`를 읽어 - `{"1","true","yes","on"}`(소문자화)면 True, 그 외 False. (NFR-5: 최소 헬퍼) -- `--verify-artifacts`를 `action="store_true"`로 두되 `default=_env_truthy( - "SECURITY_SCANNER_VERIFY_ARTIFACTS")`로 설정. store_true는 default가 False가 보통이나 - 명시 default를 주면 env 기반 on이 가능하다(플래그 명시 시 항상 True로 override → FR-4). +- 신규 모듈 헬퍼 `_env_truthy(value: str | None) -> bool`: 주어진 문자열 값이 + `{"1","true","yes","on"}`(strip+소문자화)이면 True, 미설정/빈/그 외는 False. 호출부에서 + `os.environ.get(...)`를 넘긴다(헬퍼는 env를 직접 읽지 않아 테스트가 쉽다). (NFR-5: 최소 헬퍼) +- `--verify-artifacts`를 `action=argparse.BooleanOptionalAction`로 두고 + `default=_env_truthy(os.environ.get("SECURITY_SCANNER_VERIFY_ARTIFACTS"))`로 설정. + BooleanOptionalAction은 `--verify-artifacts`/`--no-verify-artifacts` 양방향 override를 + 주므로 env-on 상태에서도 `--no-verify-artifacts`로 끌 수 있다(FR-4 양방향 충족). - host/model/timeout/min-confidence/config 플래그는 default=None 유지(이미 resolve 단계가 env fallback). 단 README/help에 env 이름을 문서화. - 배치 위치: `add_storage_args`가 env를 흡수하는 것과 동일하게, scan-all 파서를 구성하는 diff --git a/src/security_scanner/cli/commands/scan.py b/src/security_scanner/cli/commands/scan.py index 9123e78..19e835a 100644 --- a/src/security_scanner/cli/commands/scan.py +++ b/src/security_scanner/cli/commands/scan.py @@ -477,7 +477,9 @@ def _env_truthy(value: str | None) -> bool: Unset, empty, and 0/false/no/off are False, so periodic verification stays DEFAULT-OFF unless an operator explicitly opts in via env. """ - return (value or "").strip().lower() in ("1", "true", "yes", "on") + if not value: + return False + return value.strip().lower() in ("1", "true", "yes", "on") def _scan_all_verifier_config_from_args(args: argparse.Namespace):