# DonjonSec Scanner — Fix-to-Sellable Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Reconcile the codebase to one canonical tree, clear the four P0 launch-blockers so a paid license actually works, then validate all 19 scanners against real targets — making the product genuinely sellable as advertised.

**Architecture:** No rewrite (verified sound). Targeted hardening of `lib/licensing.py`, `lib/trial_license.py`, the compliance-cap enforcement path, the intel-freshness automation (live on CT100), and the ASM offline guard; plus a real-target test harness as the permanent gate.

**Tech Stack:** Python 3.10+, pytest, FastAPI (`web/`), SQLite intel DBs, Ed25519 + ML-DSA-65 licensing, Forgejo (`192.0.2.116:3000`), Proxmox test net `vmbr1` (CT107/108/VM200/CT102-104).

**Reference:** [SPEC.md](SPEC.md) · [VERIFICATION-2026-05-28.md](VERIFICATION-2026-05-28.md)

---

## Phasing

```
Phase 1 (concrete here)   SP0 reconciliation + live verification + key location  → unblocks everything
Phase 2 (titles + AC)     P0 fixes: keys, trial, framework cap, freshness        → SELLABLE
Phase 3 (titles + AC)     P1 fixes: site reconciliation, dead router, ASM offline → CREDIBLE
Phase 4 (workflow)        SP1 19-scanner audit + SP3 journey-to-16/16            → AS ADVERTISED
```

Phase 2 steps are detailed into code **after** Phase 1 Task 1.4/1.5 reveal the live state and the real keys — not before (avoids speculative code).

---

# PHASE 1 — Reconciliation & Live Verification (fully specified)

> ⚠️ Phase 1 includes git surgery on real repos. Every destructive-ish step is preceded by a backup and is **archive-not-delete**. Run with checkpoints.

### Task 1.1: Back up all three trees (no-loss guarantee)

**Files:** none modified — produces bundles under `C:\Donjonsec-reconcile-backup-20260528\`

- [ ] **Step 1: Create backup dir**
Run: `mkdir -p /c/Donjonsec-reconcile-backup-20260528`

- [ ] **Step 2: Bundle each tree's full history**
```bash
git -C /c/Donjonsec bundle create /c/Donjonsec-reconcile-backup-20260528/donjonsec.bundle --all
git -C /c/Users/Cris/donjon-platform-clean bundle create /c/Donjonsec-reconcile-backup-20260528/clean.bundle --all
git -C /c/Users/Cris/donjon-platform-dev bundle create /c/Donjonsec-reconcile-backup-20260528/dev.bundle --all
```

- [ ] **Step 3: Verify bundles are valid**
Run: `for b in /c/Donjonsec-reconcile-backup-20260528/*.bundle; do echo "$b"; git bundle verify "$b" | tail -1; done`
Expected: each prints "is okay".

### Task 1.2: Diff clean vs C:\Donjonsec for unique content (loss check)

**Files:** none modified — produces `docs/plans/2026-05-28-scanner-audit/RECONCILE-DIFF.md`

- [ ] **Step 1: Compare tracked file trees**
```bash
git -C /c/Donjonsec ls-files | sort > /tmp/dj_files.txt
git -C /c/Users/Cris/donjon-platform-clean ls-files | sort > /tmp/clean_files.txt
echo "=== ONLY in C:\\Donjonsec (would be lost if clean wins) ==="; comm -23 /tmp/dj_files.txt /tmp/clean_files.txt
echo "=== ONLY in clean ==="; comm -13 /tmp/dj_files.txt /tmp/clean_files.txt
```

- [ ] **Step 2: Inspect the divergent dirs that hold local state**
```bash
diff -rq /c/Donjonsec/.claude /c/Users/Cris/donjon-platform-clean/.claude 2>/dev/null
diff -rq /c/Donjonsec/config /c/Users/Cris/donjon-platform-clean/config 2>/dev/null
```

- [ ] **Step 3: Record findings in RECONCILE-DIFF.md** — list every file unique to `C:\Donjonsec` and a keep/drop decision for each. **Gate:** if anything unique and valuable exists only in `C:\Donjonsec`, cherry-pick it into clean before proceeding.

### Task 1.3: Resolve clean's dirty working tree

**Files:** `lib/orchestrator.py`, `web/api.py`, `pyproject.toml`, `.gitignore`, 8 untracked `tools/*.py`

- [ ] **Step 1: Review each change**
```bash
git -C /c/Users/Cris/donjon-platform-clean diff --stat
git -C /c/Users/Cris/donjon-platform-clean diff lib/orchestrator.py web/api.py pyproject.toml
```

- [ ] **Step 2: Decide commit vs stash per file** — commit anything that belongs to canonical history; stash anything experimental. The 8 untracked `tools/store_*.py` / `patch_hermes_*.py` are research scripts — decide keep (commit to `tools/`) or exclude (gitignore).

- [ ] **Step 3: Commit the keep set**
```bash
git -C /c/Users/Cris/donjon-platform-clean add -A
git -C /c/Users/Cris/donjon-platform-clean commit -m "chore: resolve working tree before canonical reconciliation"
```
Expected: clean `git status`.

### Task 1.4: Live verification on CT100 (ground truth for freshness/automation)

**Files:** none — produces `docs/plans/2026-05-28-scanner-audit/CT100-VERIFY.md`

- [ ] **Step 1: Confirm cron + script executable**
Run: `ssh root@192.0.2.100 "pct exec 100 -- bash -lc 'crontab -l | grep -i intel; ls -la /opt/donjon-platform/tools/weekly-intel-update.sh'"`
Expected: a weekly cron line AND `-rwx` on the script. Record actual.

- [ ] **Step 2: Check heartbeat age**
Run: `ssh root@192.0.2.100 "pct exec 100 -- bash -lc 'ls -la /opt/donjon-platform/data/.intel-heartbeat-ok 2>/dev/null; date'"`
Expected: heartbeat mtime within last 10 days. If missing/old → automation not completing.

- [ ] **Step 3: Read true freshness gate**
Run: `ssh root@192.0.2.100 "pct exec 100 -- bash -lc 'cd /opt/donjon-platform && python3 bin/update-intel.py --check; echo EXIT=\$?'"`
Expected: EXIT=1 today (data stale). EXIT=0 would confirm the NVD age blind spot.

- [ ] **Step 4: Record live data ages**
Run: `ssh root@192.0.2.100 "pct exec 100 -- bash -lc 'cd /opt/donjon-platform && sqlite3 data/vuln_db/vuln_intel.db \"SELECT id,timestamp,status FROM update_log ORDER BY id DESC LIMIT 3;\"'"`
Record into CT100-VERIFY.md. This decides how much of SP-FRESH is code vs ops.

### Task 1.5: Locate original license signing keys (per D9/D12 — DO NOT generate new)

**Files:** none — produces a keys-location note in `CT100-VERIFY.md`

- [ ] **Step 1: Read the memory pointer** — open `~/.claude/projects/C--Users-Cris/memory/reference_license_keys.md` for documented keypair locations + signing pipeline.

- [ ] **Step 2: Search repo + infra for the private key / signing tool**
```bash
git -C /c/Users/Cris/donjon-platform-clean grep -lI "PRIVATE KEY\|ml_dsa\|ML-DSA\|ed25519\|sign_license" -- '*.py' 'tools/*'
ls /c/Users/Cris/donjon-platform-clean/tools/ | grep -i "license\|key\|sign"
```
And on CT100: `ssh root@192.0.2.100 "pct exec 100 -- bash -lc 'ls -la /opt/donjon-platform/tools/ | grep -i license; ls -la /opt/donjon-platform/data/keys 2>/dev/null'"`

- [ ] **Step 3: Verify the public key in `lib/licensing.py:79-123` against the located private key** — confirm whether the placeholder was ever replaced anywhere, and whether the original private key can sign a license the current public key verifies. **Gate:** if originals cannot be found, STOP and escalate to Cris before any keygen (never replace keys blind).

### Task 1.6: Establish canonical tree at C:\Donjonsec

**Files:** filesystem — archive + relocate

- [ ] **Step 1: Archive the old trees (rename, no delete)**
```bash
mv /c/Donjonsec /c/Donjonsec-archive-20260528
mv /c/Users/Cris/donjon-platform-dev /c/Users/Cris/donjon-platform-dev-archive-20260528
```

- [ ] **Step 2: Relocate clean to the canonical path**
```bash
mv /c/Users/Cris/donjon-platform-clean /c/Donjonsec
```

- [ ] **Step 3: Verify identity**
```bash
git -C /c/Donjonsec rev-parse --abbrev-ref HEAD   # expect: main
git -C /c/Donjonsec remote get-url origin          # expect: 192.0.2.116:3000 Forgejo
ls /c/Donjonsec/scanners/*_scanner.py | wc -l      # expect: 19
git -C /c/Donjonsec status -s                       # expect: clean
```

- [ ] **Step 4: Write decision-log entry** to `docs/plans/2026-05-28-scanner-audit/decision-log.md` (D1–D12 with the evidence table) and commit.
```bash
git -C /c/Donjonsec add docs/plans/2026-05-28-scanner-audit/
git -C /c/Donjonsec commit -m "docs: scanner-audit spec, verification findings, reconciliation decision log"
```

**Phase 1 exit gate (AC1):** one canonical tree at `C:\Donjonsec`, on `main`, Forgejo remote, clean, 19 scanners, backups + archives on disk, CT100 ground truth recorded, key location resolved (or escalated).

---

# PHASE 2 — P0 Launch-Blockers → SELLABLE (titles + acceptance criteria)

> Detailed into TDD code-steps after Phase 1. Each task: write failing test → run-red → implement → run-green → commit. Each maps to a spec AC.

### Task 2.1: Embed real license keys & prove paid licenses validate — **AC7**
- Replace placeholder public keys (`lib/licensing.py:79-123`) with the originals located in Task 1.5.
- **AC:** sign a Pro license with the original private key → `LicenseManager.get_tier()=="pro"`; tampered/unsigned license → falls closed to `community`. Test in `tests/test_licensing.py`.

### Task 2.2: Make the trial a signed, non-renewable grant — **AC7**
- `lib/trial_license.py` issues a *signed* time-boxed license; one-time guard tied to durable fingerprint, not a deletable marker.
- **AC:** activating trial twice on same fingerprint → second is refused; deleting `data/.trial-used` does NOT re-grant Pro.

### Task 2.3: Enforce the Community framework cap (3) — **AC8**
- Wire `check_feature_item`/compliance cap (`lib/licensing.py:1029`) into the scan/compliance execution path (currently zero callers).
- **AC:** Community install running compliance against >3 frameworks → blocked exactly as pricing states; Pro → unlimited.

### Task 2.4: Truthful freshness + watchdog (CT100) — **AC10**
- Replace the dashboard staleness stub (`web/api_intel.py:27-49`) with real age reporting; add the heartbeat watchdog promised by `469b475`; fix `--check` NVD age blind spot; close `weekly-intel-update.sh` `exit 0` bypasses; version-control the cron.
- **AC:** staleness indicator reflects true age; missed update flags within window; forced incremental fetches genuinely fresh CVEs (new `success` row, current window). Re-run a live refresh so data is <7 days old.

**Phase 2 exit gate:** a signed Pro license unlocks paid features on a real install; trial cannot be farmed; CE caps match the storefront; intel data fresh and honestly monitored → **product is sellable.**

---

# PHASE 3 — P1 Credibility (titles + acceptance criteria)

### Task 3.1: Reconcile site claims to verified reality — **AC3**
- Fix in `donjonsec-website`: unify the 19 scanner *names* across /features/ & /docs/ (resolve MCP+Orchestrator vs Adversary; docs table 18→19 rows); settle 30↔31 frameworks; AI providers 16↔7; trial port 9090↔5000.
- **AC:** every site number/name matches what the product demonstrably does; no internal contradiction across pages.

### Task 3.2: Fix or remove the dead license router — **AC7-adjacent**
- `web/api_license.py:39-87` calls non-existent methods → AttributeError. Either implement against `LicenseManager` or remove and route to the working `/api/v1/license/*`.
- **AC:** no `/api/license/*` endpoint throws; documented endpoints return valid JSON.

### Task 3.3: Make `DONJON_OFFLINE` comprehensive for ASM — **AC9**
- Add offline guard to `scanners/asm_scanner.py:284,686,973,1079` (skip crt.sh/DoH when offline instead of 30s timeouts).
- **AC:** full scan incl. ASM on egress-DENY host with `DONJON_OFFLINE=1` → zero outbound to public IPs (tcpdump-proven via `tools/test-airgap.py`).

---

# PHASE 4 — Audit & Drive-to-Green (workflow)

### Task 4.1: SP1 — 19-scanner functional audit (multi-agent workflow, D7) — **AC2, AC5**
- One auditor per scanner against its `vmbr1` target (mapping in SPEC §7); blocked-infra scanners (cloud/openvas) flagged 🚫 and decided per D6.
- **AC:** `STATUS_MATRIX.md` has a run-determined status for all 19.

### Task 4.2: Stand up the real-target harness + issue ledger (SP2) — **AC4, AC5**
- Harness runs from CT107 against `vmbr1`; failures auto-open `ISSUE_LOG.md` entries (mirror to Forgejo per D5). Pipeline states NEW→…→CLOSED with 3-way repro.
- **AC:** no scanner reaches CLOSED without a green real-target run + documented lesson.

### Task 4.3: SP3 — drive journey to 16/16 + suite green — **AC6**
- Iterate scanners through the pipeline, criticality-ordered (network→vuln→web→ssl→compliance first). Reconcile the real test count (replace stale "168").
- **AC:** test suite genuinely green, customer journey 16/16 against real targets, claims reconciled.

---

## Self-Review

- **Spec coverage:** AC1→Phase1; AC2/AC5→4.1/4.2; AC3→3.1; AC4→4.2; AC6→4.3; AC7→2.1/2.2/3.2; AC8→2.3; AC9→3.3; AC10→2.4. All 10 ACs mapped. G1–G9 covered. P0×4→Phase2; P1×3→Phase3; P2×2→Phase4. ✅
- **Placeholders:** Phase 1 fully concrete (exact commands/expected output). Phases 2–4 intentionally title+AC pending Phase-1 ground truth — flagged as a real dependency, not a TODO dodge. Each later task names exact files + a verifiable AC.
- **Consistency:** file:line refs match VERIFICATION doc; tier/feature names match SPEC §1A.

---

## Notes / dependencies

- **Key location (Task 1.5) is the critical-path gate for sale.** If originals are unrecoverable, Phase 2.1 changes shape (new keypair + re-sign distribution) and needs Cris sign-off (never-replace-keys rule).
- **CT100 is ground truth** for all data/automation tasks; the Windows tree is a mirror.
- **Don't commit/push to Forgejo or touch the live site** without Cris's go (publish-everywhere rule applies once approved).
