Input: Design documents from /specs/076-deterministic-tool-scanner/
Prerequisites: plan.md, spec.md, research.md, data-model.md, contracts/detect-engine.md
Tests: REQUIRED. The repo constitution (Principle V) and CLAUDE.md mandate a failing _test.go before implementation. Every check ships with MUST-flag / MUST-NOT-flag contract tests.
Organization: Grouped by user story (US1–US4 from spec.md) so each is an independently testable increment.
Single Go module. New package internal/security/detect/ (engine + checks/); modified internal/security/scanner/, internal/security/patterns/, cmd/scan-eval/, specs/065-evaluation-foundation/datasets/.
Purpose: Scaffold the new package and the shared types every check depends on.
- T001 Create the
internal/security/detect/package with doc.go describing the offline, deterministic, recover-isolated contract (percontracts/detect-engine.md). - T002 [P] Add the import-guard test
internal/security/detect/imports_test.goasserting the package imports nonet,os/exec, filesystem, or HTTP/Docker client (enforces FR-001 offline guarantee). It will fail until the package exists; keep it as the standing offline gate.
Purpose: Core types, normalization, position classifier, engine skeleton, and the additive report fields. Everything below blocks all user stories.
- T003 [P] Define core types in
internal/security/detect/signal.go:Tier(TierHard/TierSoft),Signal,Checkinterface,ToolView,RegistryView(withToolsByName/ToolNamesindexes), perdata-model.md. IncludeConfidenceclamping andEvidencelength cap helpers. - T004 Add additive fields
Confidence float64andSignals []stringtoScanFindingininternal/security/scanner/types.go; writetypes_test.goasserting JSON round-trip keeps them (omitempty) and existing consumers are unaffected. - T005 [P] Write
internal/security/detect/normalize_test.go(TDD) covering NFKC, zero-width strip, lowercase, whitespace-collapse, light stemming, and the "don't disclose" vs "do not tell" equivalence; then implementnormalize.go. - T006 [P] Write
internal/security/detect/position_test.go(TDD) for the instruction-vs-example classifier (discount after "such as/e.g./example", inside quotes, in "detects/flags …" lists; keep imperative-position confidence); then implementposition.go. - T007 Implement the engine skeleton in
internal/security/detect/engine.go: registers checks, builds aRegistryViewonce per scan, runs eachCheck.Inspectunderrecover(), recordsCoverage{ChecksRun,ChecksFailed,FailedCheckIDs}; writeengine_test.goasserting determinism + totality (a panicking fake check is isolated, scan still returns) per the contract guarantees. - T008 Implement
internal/security/detect/aggregate.go: signals →ScanFindingwith tier semantics (any hard → dangerous/quarantine; soft-only severity = distinct-CheckID count 1→low/2→medium/3+→high), combined confidence (independent signals add, cap 1.0), andSignalslist; writeaggregate_test.gofor the severity ladder and consensus-raises-confidence (FR-005, FR-006, FR-010).
Goal: The three HARD checks detect hidden-Unicode, cross-server shadowing, and decode-to-shell, auto-quarantining with near-zero FP.
Independent test: Offline fixtures per attack class produce a hard finding; clean equivalents produce none (go test ./internal/security/detect/checks/...).
- T009 [P] [US1] Write
internal/security/detect/checks/unicode_hidden_test.go(MUST-flag zero-width/bidi/tag-block/PUA; escalate ≥3 classes or decoded tag-message; MUST-NOT-flag plain ASCII and ordinary accented Unicode); then implementunicode_hidden.gorunning on RAW text (FR-007). - T010 [P] [US1] Write
internal/security/detect/checks/shadowing_test.go(MUST-flag cross-server tool reference and same-name collision viaRegistryView; MUST-NOT-flag a tool referencing its own name); then implementshadowing.go. - T011 [P] [US1] Write
internal/security/detect/checks/payload_decoded_test.go(MUST-flag base64/hex that DECODES tocurl|sh/chmod/rm -rf/raw IP:port with decoded evidence; MUST-NOT-flag base64 of benign data) per FR-008; then implementpayload_decoded.go. - T012 [US1] Register the three hard checks in the engine and wire
internal/security/scanner/inprocess.gosotpa-descriptionsdelegates todetect.Engine(feeding aRegistryView, rendering findings); keep all CLI/REST/MCP entry points unchanged (FR-015). Updateinprocess_test.go/e2e_tpa_smoke_test.goexpectations to the new finding shape.
Checkpoint: US1 is a usable MVP — structural attacks are caught and quarantined offline.
Goal: The SOFT checks add recall on phrased attacks while the position classifier holds FP ≤ 5% on hard-negatives.
Independent test: Hard-negative corpus entries stay unflagged-as-dangerous; matching malicious entries are caught.
- T013 [P] [US2] Write
internal/security/detect/checks/directive_imperative_test.go(MUST-flag<IMPORTANT>/"before using this tool"/"do not tell the user"/"ignore previous instructions" and variants over NORMALIZED text; MUST-NOT-flag example-position usage) per FR-009; then implementdirective_imperative.gousing regex families + the position classifier. - T014 [P] [US2] Write
internal/security/detect/checks/capability_mismatch_test.go(MUST-flag a math/string tool that reads~/.sshor has an unexplained data-sink param like "sidenote"; MUST-NOT-flag a file tool that legitimately reads files); then implementcapability_mismatch.go(declared-vs-implied + unused-param heuristic). - T015 [P] [US2] Add a per-match confidence to
internal/security/patterns/matchers (validated card/Luhn → high; entropy-only → low) without changing existing call sites' behavior; update the patterns tests. - T016 [US2] Write
internal/security/detect/checks/embedded_secret_test.go; then implementembedded_secret.gowrappingpatterns/with confidence, register all three soft checks in the engine.
Checkpoint: US1 + US2 — full six-check detector with FP discrimination.
Goal: Corpus eval gate fails the build on recall/FP regression.
Independent test: scan-eval --gate exits non-zero when recall < 0.90 or hard-negative FP > 5%.
- T017 [P] [US3] Expand the labeled corpus in
specs/065-evaluation-foundation/datasets/with new categories (unicode_smuggling, decoded_payload, capability_mismatch, shadowing) and additional hard-negatives; author original equivalents where external licensing is unclear (FR-014). Update the dataset README + counts. - T018 [US3] Add
--gate --min-recall --max-fpmode tocmd/scan-eval/that runs the newdetect.Engineover the corpus, prints per-category recall/precision/FP/F1 JSON, and exits non-zero on breach; writecmd/scan-evaltest for the gate exit logic. - T019 [US3] Wire the gate into the existing CI test workflow (
.github/workflows/…) as a blocking stepscan-eval --gate --min-recall 0.90 --max-fp 0.05(FR-013, SC-006).
Checkpoint: reliability is enforced; recall ≥ 0.90 / FP ≤ 5% proven by the gate.
Goal: Findings expose confidence + contributing checks; risk score reflects agreement instead of dedup-collapsing it.
Independent test: a multi-signal tool yields a finding listing each check, carrying confidence, with severity rising by signal count.
- T020 [US4] Update the risk-score aggregation in
internal/security/scanner/(types.go / sarif.go scoring) so independent signals on a tool ADD to the score rather than dedup by(rule_id+location); write a scoring test proving consensus raises the score (FR-006, SC-007). - T021 [P] [US4] Surface
confidence+signalsin the CLI report (cmd/mcpproxy/security_cmd.goprintReportTable) and confirm they serialize in the REST scan report; add/update the report-rendering test.
Checkpoint: operator can see why a tool was flagged and how strongly.
- T022 [P] Document the six checks, the two-tier model, and the eval gate in
docs/features/(extend security-quarantine.md / sensitive-data-detection.md or add tool-scanner.md); note offline/no-egress guarantee. - T023 [P] Run
gofmt/goimportsandgolangci-lint run --config .github/.golangci.yml ./internal/security/... ./cmd/scan-eval/...; fix findings. - T024 Full verification:
go test -race ./internal/security/... ./cmd/scan-eval/...,./scripts/test-api-e2e.sh, and the corpus gate; confirm SC-001…SC-007 and update the spec checklist.
- Setup (T001–T002) → Foundational (T003–T008) block everything.
- US1 (T009–T012) depends only on Foundational → this is the MVP; ship first.
- US2 (T013–T016) depends on Foundational; independent of US1 except the shared engine registration (T012 before T016 wiring is cleanest, but checks themselves are parallel).
- US3 (T017–T019) depends on the engine + at least US1 checks existing to measure; corpus expansion (T017) can start in parallel with US1/US2.
- US4 (T020–T021) depends on Foundational aggregation; independent of US2/US3.
- Polish (T022–T024) last.
- T002 + T003 + T005 + T006 (different files) early.
- All six check test+impl pairs (T009, T010, T011, T013, T014, T016) are
[P]— different files, one check each. - T017 (corpus) parallel with check implementation.
- T022 + T023 parallel in polish.
MVP = Phase 1 + 2 + US1 (T001–T012) — offline detector catching the three structural attack classes, delegated into the live scanner. Then US2 (FP discrimination), US3 (CI gate proving the numbers), US4 (transparency), Polish.
- Total tasks: 24
- Per story: Setup 2, Foundational 6, US1 4, US2 4, US3 3, US4 2, Polish 3
- Parallel-marked: 12 tasks
[P] - MVP scope: T001–T012 (US1)