diff --git a/.claude/skills/deep-code-review/scripts/review_checks.sh b/.claude/skills/deep-code-review/scripts/review_checks.sh index 9732d383c..b0e6a165d 100755 --- a/.claude/skills/deep-code-review/scripts/review_checks.sh +++ b/.claude/skills/deep-code-review/scripts/review_checks.sh @@ -1171,7 +1171,8 @@ try: elif k == "nuclei": # JSONL: one finding per line; a non-JSON line is a failure, not noise rows = [] for ln in filter(None, (x.strip() for x in t.splitlines())): - d = json.loads(ln); rows.append((d["matched-at"], 0, d["template-id"], f"{d['info']['severity']}: {d['info']['name']}")) + d = json.loads(ln) + if d["info"]["severity"] != "info": rows.append((d["matched-at"], 0, d["template-id"], f"{d['info']['severity']}: {d['info']['name']}")) # info = technology fingerprint, not a finding else: # zap rows = [(i.get("uri", "."), 0, "zap-" + a["pluginid"], f"{a['riskdesc']}: {a['alert']}") for s in json.loads(t)["site"] for a in s["alerts"] for i in (a.get("instances") or [{}])[:3]] except Exception: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c9714ff9..a66097030 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -519,7 +519,7 @@ jobs: # (pinned, hash-locked where the ecosystem allows; no advisory-DB download, except govulncheck, grype, trivy and osv-scanner, which fetch their vulnerability DB online: trivy by pinned digest, the rest live) and checks each is RAN with its rule id on one canary. bundler-audit and retire need a downloaded DB or rule set, so they stay stub-only; syft is an inventory the hub never counts as ran. real-hub: runs-on: ubuntu-24.04 # pinned: the cppcheck .deb below is the noble build and its runtime deps come from noble - timeout-minutes: 25 + timeout-minutes: 35 permissions: contents: read steps: @@ -821,6 +821,18 @@ jobs: chmod +x "$RUNNER_TEMP/bin/mobsfscan" echo "$RUNNER_TEMP/bin" >> "$GITHUB_PATH" "$RUNNER_TEMP/venv-mobsfscan/bin/python" -c "import importlib.metadata as m; assert m.version('semgrep') == '1.172.0', m.version('semgrep')" + - name: Install nuclei 3.11.1 and nuclei-templates v10.5.2 (pinned sha256; the binary hash is the upstream checksums-file entry, the templates hash is of the GitHub tag archive) + run: | + set -euo pipefail + mkdir -p "$RUNNER_TEMP/bin" + cd "$RUNNER_TEMP" + curl --proto '=https' --tlsv1.2 -fsSL --retry 3 --max-time 300 -O https://github.com/projectdiscovery/nuclei/releases/download/v3.11.1/nuclei_3.11.1_linux_amd64.zip + echo "ea63d4ae232808cd7c6bc00d0142428e231fab59dae01042246097d195835ab6 nuclei_3.11.1_linux_amd64.zip" | sha256sum -c - + unzip -q nuclei_3.11.1_linux_amd64.zip nuclei -d "$RUNNER_TEMP/bin" + curl --proto '=https' --tlsv1.2 -fsSL --retry 3 --max-time 300 -o nuclei-templates.tar.gz https://github.com/projectdiscovery/nuclei-templates/archive/refs/tags/v10.5.2.tar.gz + echo "016b7abbf931d4d1a851a5a568e44510dc61691dc8a5202e3b24e701515ec574 nuclei-templates.tar.gz" | sha256sum -c - + mkdir -p "$HOME/nuclei-templates" + tar -xzf nuclei-templates.tar.gz --strip-components=1 -C "$HOME/nuclei-templates" - name: Real-tool canaries run: REQUIRE_REAL_HUB=1 bash scripts/test-review-checks-real-hub.sh diff --git a/changelog.d/hub-real-smoke-13.md b/changelog.d/hub-real-smoke-13.md new file mode 100644 index 000000000..adb4a0b91 --- /dev/null +++ b/changelog.d/hub-real-smoke-13.md @@ -0,0 +1,2 @@ +### Changed +- The real-hub CI job now runs the real nuclei (3.11.1, sha256 from the upstream checksums file) with nuclei-templates v10.5.2 (tag archive, sha256-pinned) through the hub's DAST route (REVIEW_DAST_URL) against a local python http.server canary, and asserts it ran and reported the exact rule id tech-detect with no "not run" or "NOT CHECKED" line. zap-baseline is deliberately not covered: its only route is the ghcr.io/zaproxy/zaproxy image, about 1.2 GB of compressed layers, too heavy for the per-push job. diff --git a/scripts/test-review-checks-posture-dast.sh b/scripts/test-review-checks-posture-dast.sh index 0d38cf7ee..03d2ca515 100755 --- a/scripts/test-review-checks-posture-dast.sh +++ b/scripts/test-review-checks-posture-dast.sh @@ -28,7 +28,8 @@ stub nu nuclei '[ "${HUB_MODE:-}" != crash ] || exit 9 echo "gt=${GITHUB_TOKEN:-}" >>"$HUB_LOG" case " $* " in *" -tl "*) printf "%s" "${TL_OUT-tpl1}"; exit 0 ;; esac [ -z "${EMPTY_SCAN:-}" ] || exit 0 -printf "%s\n" "{\"template-id\":\"missing-xfo\",\"info\":{\"name\":\"X-Frame-Options missing\",\"severity\":\"low\"},\"matched-at\":\"http://localhost:3000/\"}"' +printf "%s\n" "{\"template-id\":\"missing-xfo\",\"info\":{\"name\":\"X-Frame-Options missing\",\"severity\":\"low\"},\"matched-at\":\"http://localhost:3000/\"}" +printf "%s\n" "{\"template-id\":\"tech-detect\",\"info\":{\"name\":\"Wappalyzer\",\"severity\":\"info\"},\"matched-at\":\"http://localhost:3000/\"}"' stub sg semgrep 'printf "%s\n" "{\"results\":[],\"errors\":[],\"paths\":{\"scanned\":[\"a.py\"]}}"' stub zap zap-baseline.py '[ "${HUB_MODE:-}" != crash ] || exit 9 while [ $# -gt 0 ]; do [ "$1" != -J ] || j=$2; shift; done @@ -96,7 +97,7 @@ CURL_FAIL=1 REVIEW_DAST_URL=http://localhost:3000/ run "$T/nu:$T/zap"; [ ! -s "$ TL_OUT= EMPTY_SCAN=1 REVIEW_DAST_URL=http://localhost:3000/ run "$T/nu"; grep -q '^NOT CHECKED: nuclei (no templates matched' "$T/err" && row '.tool=="nuclei" and .rule=="not-checked"' && ! ran nuclei && ok "empty nuclei output, no template count: not-checked" || bad "empty nuclei" EMPTY_SCAN=1 REVIEW_DAST_URL=http://localhost:3000/ run "$T/nu"; ran nuclei && ok "templates matched, no findings: ran clean" || bad "clean nuclei" GITHUB_TOKEN=tok REVIEW_DAST_URL=http://localhost:3000/ run "$T/nu:$T/zap" -row '.tool=="nuclei" and .rule=="missing-xfo" and .file=="http://localhost:3000/"' && row '.tool=="zap" and .rule=="zap-10038"' && ran nuclei && ran zap && grep -q -- '-s info,low -type http -tags tech,exposure -etags default-login,xss,ssrf,oast,fuzz,fuzzing,takeover,cve,intrusive,dos' "$HUB_LOG" && grep -q -- ' -ni ' "$HUB_LOG" && ! grep -q 'gt=tok' "$HUB_LOG" && ok "nuclei and zap findings parsed, allowlist and exclusions, no interactsh, no GitHub token" || bad "DAST findings" +row '.tool=="nuclei" and .rule=="missing-xfo" and .file=="http://localhost:3000/"' && ! row '.tool=="nuclei" and .rule=="tech-detect"' && row '.tool=="zap" and .rule=="zap-10038"' && ran nuclei && ran zap && grep -q -- '-s info,low -type http -tags tech,exposure -etags default-login,xss,ssrf,oast,fuzz,fuzzing,takeover,cve,intrusive,dos' "$HUB_LOG" && grep -q -- ' -ni ' "$HUB_LOG" && ! grep -q 'gt=tok' "$HUB_LOG" && ok "nuclei and zap findings parsed, allowlist and exclusions, no interactsh, no GitHub token" || bad "DAST findings" grep -q -- "-g --noproxy \* -sf" "$T/curl.log" && ok "probe: curl -g --noproxy '*'" || bad "curl flags" mkdir -p "$T/dk"; stub dk docker ':' REVIEW_DAST_URL=http://localhost:3000/ run "$T/dk"; grep -q '^docker run --rm --pull=never ' "$HUB_LOG" && ok "ZAP docker run never auto-pulls the image" || bad "docker pull" diff --git a/scripts/test-review-checks-real-hub.sh b/scripts/test-review-checks-real-hub.sh index 4a6f7f36b..5a2f54716 100755 --- a/scripts/test-review-checks-real-hub.sh +++ b/scripts/test-review-checks-real-hub.sh @@ -4,6 +4,7 @@ # with its expected rule id in the JSON rows. The stub tests (scripts/test-review-checks-hub-*.sh) pin routing and failure # handling; this pins the real flags, SARIF/JSON shapes and exit codes. Not covered: syft (an inventory the hub never counts as ran), bundler-audit and retire need a # network-fetched advisory DB or rule set (they are NOT CHECKED without one), so a real run would need a download. codeql is dropped: its CLI bundle is a multi-hundred-MB download plus per-language database builds, too heavy for this per-push job. psalm is dropped: the hub's row builder reads the is_security issue field, which psalm 6.x does not emit and psalm 7.x does, and psalm 7.x's platform check needs PHP 8.3.16 or newer while the runner ships 8.3.6, so it would need a third-party PHP setup action and cannot be run here. sobelow is dropped: it needs an Erlang/OTP and Elixir toolchain the runner does not ship, with no checksum-pinned install path. licensee is dropped: the hub reports it as an inventory line, never as a scan that ran, and its dependency set includes natively built gems (rugged) that cannot be hash-locked without a Bundler lockfile. pip-audit is dropped: the hub reaches it only as a fallback for a requirements*.txt that osv-scanner and trivy gave no result for (the seeded osv DB covers requirements.txt, so it would never run here), and it has no offline advisory DB: it queries PyPI live, so there is nothing to integrity-pin. +# nuclei (the hub's DAST route) scans a local python http.server canary through REVIEW_DAST_URL; its rule id is ds-store-file (a low-severity exposure: the canary serves a .DS_Store; the hub drops info-severity fingerprints such as tech-detect). zap-baseline is dropped: the only route is the ghcr.io/zaproxy/zaproxy image, whose compressed layers total about 1.2 GB (registry manifest, checked 2026-11-13), over the per-push job's size budget. # A tool absent from PATH is skipped (exit 0) unless REQUIRE_REAL_HUB=1 (the ci.yml real-hub job sets it), then it FAILs. # Read-only on the repo; writes only under a temp dir. Fixtures are fictional canaries. set -euo pipefail @@ -11,14 +12,14 @@ ROOT=$(cd "$(dirname "$0")/.." && pwd) RC=${REVIEW_CHECKS:-$ROOT/.claude/skills/deep-code-review/scripts/review_checks.sh} fail=0; tools= bin() { case $1 in ruff-security) echo ruff ;; eslint-security) echo eslint ;; *) echo "$1" ;; esac; } # the hub tool names ruff-security and eslint-security run the ruff and eslint binaries -for t in tfsec ansible-lint rubocop cfn-lint pmd solhint hadolint bandit ruff-security actionlint zizmor gosec staticcheck kube-linter flawfinder detect-secrets gitleaks trufflehog checkov kube-score kubescape semgrep njsscan tflint kics cppcheck devskim govulncheck grype trivy osv-scanner shellcheck guarddog cargo-audit cargo-deny brakeman mobsfscan eslint-security; do +for t in tfsec ansible-lint rubocop cfn-lint pmd solhint hadolint bandit ruff-security actionlint zizmor gosec staticcheck kube-linter flawfinder detect-secrets gitleaks trufflehog checkov kube-score kubescape semgrep njsscan tflint kics cppcheck devskim govulncheck grype trivy osv-scanner shellcheck guarddog cargo-audit cargo-deny brakeman mobsfscan eslint-security nuclei; do if command -v "$(bin "$t")" >/dev/null 2>&1; then tools+=" $t" elif [ "${REQUIRE_REAL_HUB:-}" = 1 ]; then echo "FAIL - $t not installed (REQUIRE_REAL_HUB=1)"; fail=1 else echo "skip - $t not installed"; fi done [ -n "$tools" ] || { [ "$fail" = 0 ] && echo "no real hub tool installed: SKIP test-review-checks-real-hub"; exit "$fail"; } T=$(mktemp -d "${TMPDIR:-/tmp}/realhub.XXXXXX") && [ -d "$T" ] || { echo "FAIL - mktemp" >&2; exit 1; } -trap 'cd /; rm -r -- "$T"' EXIT +SP=; trap '[ -z "$SP" ] || kill "$SP" 2>/dev/null; cd /; rm -r -- "$T"' EXIT mkdir "$T/repo" && cd "$T/repo" || exit 1 G() { git -c user.email=t@example.com -c user.name=t "$@"; } G init -q . && # eslint-security: the project's own flat config (committed in the base, because the hub does not run a config the diff changes) loading the pinned security plugin @@ -80,14 +81,22 @@ case " $tools " in *" eslint-security "*) # osv-scanner scans the lockfile above (lodash 4.17.15) against the offline DB the CI step seeds into REVIEW_OSV_DB_DIR (the hub's own warm command). guarddog scans the package dir the lock newly adds, so the lock's one package is installed here (untracked, after the commit) with a fictional mining-pool payload: threat-process-cryptomining case " $tools " in *" guarddog "*) mkdir -p node_modules/lodash && printf '{"name":"lodash","version":"4.17.15"}\n' >node_modules/lodash/package.json && printf '// fictional canary\nvar pool = "stratum+tcp://pool.example.com:3333"; // xmrig\n' >node_modules/lodash/index.js ;; esac # govulncheck (vuln.go.dev), grype, trivy, osv-scanner (DB seed), guarddog, cargo-audit, cargo-deny, brakeman, mobsfscan and eslint-security (a transient crash reads as not run) are online or DB-backed: retry the hub run up to 3 times while any is not run -for i in 1 2 3; do rc=0; bash "$RC" --base base >"$T/out" 2>"$T/err" || rc=$?; grep -qE 'not run:.*\b(govulncheck|grype|trivy|osv-scanner|guarddog|cargo-audit|cargo-deny|brakeman|mobsfscan|eslint-security)\b' "$T/err" && [ "$i" -lt 3 ] || break; sleep $((i * 10)); done +# nuclei: a localhost python http.server canary (outside the repo) on a free port; its default template pass is rate-limited (~3 min), so the hub's per-tool --timeout is raised +tmo=60 +case " $tools " in *" nuclei "*) + mkdir "$T/site" && printf '