diff --git a/security-review/DEPLOY-R720.md b/security-review/DEPLOY-R720.md new file mode 100644 index 0000000..24fde9e --- /dev/null +++ b/security-review/DEPLOY-R720.md @@ -0,0 +1,54 @@ +# Phase 3 — Path B deployment (R720 host + CI backstop) + +Status: **design + scripts; NOT deployed.** Needs the R720 (hostname SH-NVR, deprecated NVR, no GPU) +and Adam's hands-on involvement. Nothing here has been run against the box. See memory +`project-security-review-agent`. + +## What Phase 3 delivers +1. The orchestrator hosted on the R720 as an always-on service (the Path B execution engine). +2. A **headless agentic runner** (`run_headless.py`) so the detector fan-out + proof-or-kill verifier + can run unattended (interactive `/sh-security-review` can't run in CI/cron). +3. CI backstop (`ci/security-review.yml`) running the same `review.sh` on every PR. +4. Nightly full-repo sweep + planted canary vulns + two-model disagreement on criticals. +5. Budget/telemetry ceiling against the Max-20x **$200/mo Agent SDK credit pool** + (see memory `reference-claude-subscription-billing`). + +## The load-bearing piece to BUILD first: `run_headless.py` +The interactive skill orchestrates subagents via the Claude Code Agent tool (Max-covered). Headless, +that must become explicit API/SDK calls. `run_headless.py` should: +- take `--scope` + target dir, read the in-scope files; +- run the 6 detectors (injection/authz/secrets-crypto/iac-iam/web-client/logic) as parallel calls, + each fresh context, using the SAME prompts as `~/.claude/commands/sh-security-review.md`; +- run the proof-or-kill verifier pass; +- emit the finding schema JSON that `review.sh --agent-findings` consumes. +- **Billing:** authenticate via the **Claude Agent SDK with the subscription** to spend the $200/mo + pool before API overflow (NOT a raw key) — see the billing memory. Non-Claude cross-family tiebreak + stays on Bedrock/API. ⚠️ This code cannot be validated until it runs with real creds; do not mark + done until tested. + +## R720 host setup (Windows Server 2022, no GPU) +Recommended: run the Python orchestrator in **WSL2 or Docker** rather than native Windows Python. +1. `git clone https://github.com/amoussa1229/orchestrator` onto the box. +2. Python 3.12 + `pip install -r requirements.txt` (+ the Agent SDK). +3. Install the deterministic scanners (semgrep/gitleaks/checkov/cfn-lint/pip-audit) on the box. +4. Secrets on the box (NOT in git): Agent SDK subscription auth, Bedrock creds, Slack token, repo PATs. +5. Run as a service (NSSM on Windows, or systemd inside WSL2). Expose to the harness as an + MCP/HTTP service (mirror the existing unifi MCP pattern) OR keep it as a local scheduled job. +6. Scheduler: nightly full-repo sweep → `review.sh` → Slack report (ALARM-style, see + memory `feedback_cloudwatch_alarms`: only notify on findings, not clean runs). + +## Anti-complacency reinforcements (Phase 3) +- **Canary vulns:** keep the testbed's planted vulns in a fixture the nightly sweep also scans; if the + agent ever misses a known canary, that's a complacency signal → alert. +- **Two-model disagreement:** on confirmed criticals, run the cross-family reviewer (orchestrator + GPT-4.1) and flag disagreement for human review. +- **Budget guard:** track output tokens vs the $200/mo pool; stop/alert before overflow. + +## Open dependencies (Adam) +- R720 reachable + WSL2/Docker chosen. +- GitHub repo/org secrets: `ANTHROPIC_API_KEY` (or SDK auth), set `vars.SECURITY_REVIEW_AGENTIC=1` + once `run_headless.py` is tested. +- Decide: promote `ci/security-review.yml` into Sea-Haven-Industries/.github as a reusable workflow + (per engineering-handbook/cicd.md) vs per-repo copy. +- checkov severity filtering (it emitted 51-55 best-practice mediums on payments-dashboard — tune + before nightly or the Slack reports are noise). diff --git a/security-review/ci/security-review.yml b/security-review/ci/security-review.yml new file mode 100644 index 0000000..d52d930 --- /dev/null +++ b/security-review/ci/security-review.yml @@ -0,0 +1,61 @@ +# Sea Haven security-review CI backstop (Phase 3). +# Drop into a target repo as .github/workflows/security-review.yml, OR (preferred, per +# engineering-handbook/cicd.md) promote into Sea-Haven-Industries/.github as a reusable workflow. +# +# This is the UNBYPASSABLE deterministic backstop: local hooks can be skipped with --no-verify, +# this cannot. It runs the SAME review.sh as local. The agentic detector/verifier pass (Path B) +# is gated behind SECURITY_REVIEW_AGENTIC=1 and requires the headless orchestrator runner + +# ANTHROPIC creds (see DEPLOY-R720.md) — until that exists, CI runs the scanners-only gate. +name: security-review +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + security-review: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install scanners + run: | + python3 -m pip install --quiet pipx && python3 -m pipx ensurepath + pipx install semgrep >/dev/null + pipx install checkov >/dev/null + pipx install pip-audit >/dev/null + pip install --quiet cfn-lint + # gitleaks binary + curl -sSL https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz \ + | tar -xz -C /usr/local/bin gitleaks + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + - name: Fetch review.sh + run: | + # Pin to the orchestrator repo / a release artifact. Placeholder: vendor a copy or curl a tag. + git clone --depth 1 https://github.com/amoussa1229/orchestrator /tmp/orch + chmod +x /tmp/orch/security-review/review.sh + + - name: Run gate (scanners; agentic if enabled) + env: + SECURITY_REVIEW_AGENTIC: ${{ vars.SECURITY_REVIEW_AGENTIC }} # set to 1 once headless runner exists + ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + run: | + SCOPE="${SECURITY_REVIEW_SCOPE:-src scripts template.yaml}" + if [ "${SECURITY_REVIEW_AGENTIC:-0}" = "1" ]; then + python3 /tmp/orch/security-review/run_headless.py --scope "$SCOPE" --out /tmp/agent.json . + /tmp/orch/security-review/review.sh --scope "$SCOPE" --agent-findings /tmp/agent.json \ + --suppressions .security-review/suppressions.json --json-out /tmp/review.json . + else + /tmp/orch/security-review/review.sh --scope "$SCOPE" --scanners-only \ + --suppressions .security-review/suppressions.json --json-out /tmp/review.json . + fi + + - name: Upload findings + if: always() + uses: actions/upload-artifact@v4 + with: + name: security-review-findings + path: /tmp/review.json diff --git a/security-review/review.sh b/security-review/review.sh index 84398b9..eb713f2 100755 --- a/security-review/review.sh +++ b/security-review/review.sh @@ -106,10 +106,14 @@ if command -v checkov >/dev/null; then FC="$(echo "$RAW" | jq '[ (if type=="array" then .[] else . end).results.failed_checks // [] ] | add // []' 2>/dev/null || echo '[]')" jq -s '.[0]+.[1]' "$CKALL" <(echo "$FC") > "$CKALL.t" && mv "$CKALL.t" "$CKALL" done - NORM="$(jq '[.[] | { + # High-signal checkov checks (exposure/access/wildcard) -> high; everything else -> low (informational). + # checkov OSS rarely populates .severity, so without this every best-practice nit reads as medium and drowns signal. + CKHI='["CKV_AWS_53","CKV_AWS_54","CKV_AWS_55","CKV_AWS_56","CKV_AWS_57","CKV_AWS_20","CKV_AWS_24","CKV_AWS_25","CKV_AWS_260","CKV_AWS_1","CKV_AWS_40","CKV_AWS_49","CKV_AWS_62","CKV_AWS_70","CKV_AWS_107","CKV_AWS_108","CKV_AWS_109","CKV_AWS_110","CKV_AWS_111"]' + NORM="$(jq --argjson hi "$CKHI" '[.[] | { id: ("checkov-" + .check_id + "-" + ((.file_line_range[0]) // 0 | tostring)), title: (.check_id + ": " + (.check_name // "")), - severity: ((.severity // "MEDIUM") | ascii_downcase), + severity: (if .severity != null then (.severity|ascii_downcase) + elif (.check_id as $c | $hi | index($c)) then "high" else "low" end), cwe: "n/a", file: (.file_abs_path // .file_path), line: (.file_line_range[0] // null), category: "iac-iam", source: "checkov", status: "confirmed", data_flow: "checkov policy violation", proof: {input: "deploy template", outcome: (.check_name // "")}}]' "$CKALL")" @@ -135,6 +139,24 @@ if command -v pip-audit >/dev/null; then else echo " [pip-audit] no requirements*.txt in scope" >&2; fi else note_missing pip-audit "pipx install pip-audit"; fi +# --- npm audit (vulnerable Node deps) — runs at TARGET root if package.json present --- +if command -v npm >/dev/null; then + if [ -f "$TARGET/package.json" ]; then + RAW="$(cd "$TARGET" && npm audit --json 2>/dev/null || true)" + if [ -n "$RAW" ] && echo "$RAW" | jq -e '.vulnerabilities' >/dev/null 2>&1; then + NORM="$(echo "$RAW" | jq '[.vulnerabilities // {} | to_entries[] | .value as $v | { + id: ("npmaudit-" + $v.name), + title: ("vulnerable npm dep " + $v.name + " (" + ($v.range // "") + ")"), + severity: ($v.severity | if .=="moderate" then "medium" elif .=="critical" then "critical" elif .=="high" then "high" elif .=="low" then "low" else "info" end), + cwe: ([$v.via[]? | objects | .cwe[]?] | if length>0 then .[0] else "n/a" end), + file: "package.json", line: null, category: "secrets-crypto", source: "npm-audit", status: "confirmed", + data_flow: "known-vulnerable npm dependency", + proof: {input: "install", outcome: (([$v.via[]? | objects | .title] | join("; "))[0:140])}}]')" + add "$NORM"; echo " [npm-audit] $(echo "$NORM" | jq length) finding(s)" >&2 + else echo " [npm-audit] 0 findings / no lockfile" >&2; fi + else echo " [npm-audit] no package.json at target root" >&2; fi +else note_missing npm-audit "install Node.js"; fi + # --- Agent findings (from interactive /sh-security-review, or Path B headless later) --- if [ "$SCANNERS_ONLY" -eq 0 ] && [ -n "$AGENT_FINDINGS" ]; then [ -f "$AGENT_FINDINGS" ] || { echo "review.sh: agent-findings file not found: $AGENT_FINDINGS" >&2; exit 2; } @@ -184,10 +206,13 @@ echo "================ SECURITY REVIEW ================" echo "$SUMMARY" | jq -r '" confirmed critical: \(.confirmed_critical) confirmed high: \(.confirmed_high) medium: \(.confirmed_medium) suppressed: \(.suppressed) unverified: \(.unverified)"' echo "-------------------------------------------------" echo "$DEDUP" | jq -r ' - def order: {critical:0,high:1,medium:2,low:3,info:4,unverified:5}[.severity] // 9; - sort_by(order) | .[] - | select(.status=="confirmed") + def order: {critical:0,high:1,medium:2}[.severity] // 9; + [ .[] | select(.status=="confirmed" and (.severity|IN("critical","high","medium"))) ] | sort_by(order) | .[] | " [\(.severity|ascii_upcase)] \(.cwe // "n/a") \(.file)\(if .line then ":"+( .line|tostring) else "" end) — \(.title // .id)"' +echo "$DEDUP" | jq -r ' + ([ .[] | select(.status=="confirmed" and .severity=="low") ] | length) as $lo + | ([ .[] | select(.status=="confirmed" and .severity=="info") ] | length) as $in + | if ($lo+$in)>0 then " (+\($lo) low, +\($in) info — informational, in JSON report only)" else empty end' SUPN="$(echo "$SUMMARY" | jq '.suppressed')" if [ "$SUPN" -gt 0 ]; then echo " -- suppressed (logged) --"