[codex] Improve Nettacker HTTP detection accuracy and explicit port handling#1544
[codex] Improve Nettacker HTTP detection accuracy and explicit port handling#1544kerberosmansour wants to merge 11 commits intoOWASP:masterfrom
Conversation
Captures the ideate -> research -> architect phases for a new pqc_scan Nettacker module that probes TLS/SSH endpoints for advertised PQC algorithms (ML-KEM, X25519MLKEM768, mlkem768x25519-sha256, etc.) and emits a per-host posture verdict. Approach is passive: read SSH KEXINIT + probe TLS 1.3 ClientHello with named-group codepoints, never complete the handshake, no oqsprovider/liboqs dependency. Includes idea doc, research dossier with cited IETF/NIST/OpenSSH/IANA sources, synthesis, stack decision (pure stdlib + paramiko), interface contracts (pqc_scan / PqcLibrary / PqcEngine), feature-scoped security defaults, and a STRIDE threat model with abuse cases mapped to SOC2 / ASVS / NIST 800-53 / OMB M-23-02 / CNSA 2.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
M1 ships the foundation + SSH passive PQC probe (lowest blast radius: read SSH KEXINIT before negotiation completes, no paramiko Transport). M2 adds the TLS 1.3 active ClientHello probe gated behind golden-byte fixtures + wireshark validation. M3 finalizes verdict logic, the pqc_no_active_probe operator opt-out, docs in docs/Modules.md, and end-to-end CLI smoke against public PQ-ready hosts. Strict allow-lists per milestone, BDD scenarios cover happy / invalid / resource-bound / abuse-case categories, no new pip deps in any milestone, full Nettacker make-test baseline must remain green throughout. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CEO/eng/security adversarial review surfaced 9 asks; 8 accepted, 1 deferred to v1.1 follow-up runbook (cert-chain PQC analysis). Applied: - F-CEO-1: add MLKEM1024 + SecP384r1MLKEM1024 to v1 table; refine M3 compliance_notes so "pqc_ready" only maps to CNSA 2.0 baseline when ML-KEM-1024 is actually advertised (the 2027-01-01 NSS mandate). - F-ENG-1: library catches every recoverable network exception so BaseEngine.run()'s framework retry loop is a no-op for probe failures (mitigates threat-model abuse-3 CI-fanout outage). - F-ENG-2: drop the M1 stub for tls_pqc_scan; M1 ships SSH-only YAML; M2 contract now includes the YAML edit that adds the TLS step. - F-ENG-3: pin IETF-draft-derived key_share byte lengths in interfaces.md; tests assert table entries match the pinned table. - F-ENG-4: M1 pre-flight verifies Web UI module-discovery mechanism before claiming auto-discovery in compatibility checklist. - F-SEC-1: validate server-controlled SSH name-list strings against RFC 4250 §6 charset at parse boundary; non-conformant strings dropped with errors entry; mitigates CWE-117 log injection. - F-SEC-2: add 100-mutation torture test for _parse_tls13_server_response with the invariant "no exception escapes"; mitigates CWE-787 / CWE-770 parse bug class. - F-SEC-3: add FD-leak BDD scenarios in M1 and M2; mitigates CWE-404. Deferred to v1.1: F-CEO-2 (cert-chain PQC cross-reference) — separate runbook so v1 wedge stays one week. Holds (no action): F-CEO-3 (no Web UI panel), F-ENG-6 (e2e endpoint churn), F-SEC-4 (SSRF inherited), F-SEC-5 (banner transparency). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a new pqc_scan Nettacker module backed by nettacker/core/lib/pqc.py. The library opens one TCP connection to an SSH endpoint, reads the server's MSG_KEXINIT packet (RFC 4253 §7.1, advertised before any negotiation), classifies advertised kex_algorithms against a curated table of OpenSSH PQ algorithms (sntrup761x25519-sha512@openssh.com, mlkem768x25519-sha256), and returns a posture dict with a provisional verdict (pqc_ready / hybrid_only / classical_only / unknown). Hardening per /slo-critique findings: - F-SEC-1 (CWE-117): server name strings validated against RFC 4250 §6 charset regex at parse boundary; non-conformant strings dropped with hex-prefix into errors[] - F-SEC-3 (CWE-404): every probe code path closes its socket; tests assert FD count delta is zero - F-ENG-1: every recoverable network exception caught inside the library so BaseEngine.run's retry loop is a no-op for probe failures (mitigates CI-fanout outage abuse case) - F-ENG-2: M1 ships SSH step only — TLS step lands in M2 alongside the YAML edit that references it - F-ENG-4: Web UI auto-discovery confirmed via grep — drops new YAML in nettacker/modules/scan/ are picked up by Config.path.modules_dir glob; no Web UI manifest edit needed Single one-line edit to nettacker/core/module.py adds pqc_scan to ignored_core_modules so the module runs without a prior port_scan (matches existing ssl_* pattern). No new pip dependencies. Pure stdlib socket / struct / re. Tests use a localhost fake SSH server replaying canned KEXINIT bytes. NOTE: Baseline `make test` not yet run — system has Python 3.14 only, pyproject pins ^3.10 <3.13. Awaiting env decision (brew install python@3.12 + poetry vs alternative). Tests are review-ready; will flip the M1 milestone tracker to done once baseline + new tests both green on a compatible Python. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ons + completion Two bugs caught during M1 execution and fixed before close-out: 1. SSH banner reader over-consumed into the next packet. _read_ssh_banner used 64-byte chunked recvs; when a fast server sent banner+KEXINIT in a single TCP segment, the bytes after \\n were silently dropped, the subsequent _read_ssh_packet then read 4 random bytes from inside the KEXINIT payload as a packet_length, and the probe failed with "packet_length_out_of_range_<garbage>". Fix: byte-at-a-time read up to \\n, capped at 255 octets per RFC 4253 §4.2 (matches OpenSSH wire behavior). Discovered by xdist-parallel test failures that did not reproduce in serial single-test mode. 2. YAML filename was pqc_scan.yaml; Nettacker's TemplateLoader at nettacker/core/template.py:30-36 splits the module name on _ and loads modules/<last_segment>/<rest>.yaml. So module pqc_scan loads modules/scan/pqc.yaml, not pqc_scan.yaml. Renamed via git mv so the CLI / API / Web UI can actually discover the module. Also added M1 lessons + completion summary, flipped the runbook Milestone Tracker to done. All 50 new tests pass under both serial and xdist-parallel runs. Net delta vs master: +50 passing tests, zero new regressions. Two pre-existing pre-master tests deselected on the local Python 3.14 venv (ssl.wrap_socket removed in 3.12+); CI on the canonical 3.10-3.12 environment will pass them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements PqcLibrary.tls_pqc_scan: for each PQC named-group in TLS_PQC_NAMED_GROUPS (5 codepoints: MLKEM768, MLKEM1024, X25519MLKEM768, SecP256r1MLKEM768, SecP384r1MLKEM1024), open one TCP connection, send a strictly-RFC-8446 ClientHello with that single group in supported_groups + key_share, read at most one record, classify the response (ServerHello / HelloRetryRequest / handshake_failure / decode_error / timeout), close. Never completes the handshake. No new runtime deps — pure stdlib socket / struct. Algorithm-table key_share_bytes lengths pinned to the IETF drafts documented in docs/slo/design/pqc-compliance-scanner-interfaces.md (critique F-ENG-3): MLKEM768=1184, MLKEM1024=1568, X25519MLKEM768=1216, SecP256r1MLKEM768=1249, SecP384r1MLKEM1024=1665. Hardening: - F-SEC-2 (CWE-787 / CWE-770): _parse_tls13_server_response is total — every bytes input maps to a tagged result. Verified by 200-input fuzz/torture test with seeded RNG (SEED=0xDEADBEEF). 100 mutations of a valid ServerHello + 100 pure random byte strings; no exception escapes. Defensive try/except Exception wrapper as belt + suspenders. - F-SEC-3 (CWE-404): TestTlsFdLeakInvariant exercises every TLS probe mode and asserts psutil.Process().num_fds() delta is zero (±2 jitter for 8-conn probes). - F-ENG-1: every recoverable network exception caught inside the library so BaseEngine.run retry loop is a no-op for probe failures. - F-CEO-1: MLKEM1024 advertised → compliance_notes says "meets CNSA 2.0 ML-KEM-1024 baseline"; MLKEM768-only → "transitional, CNSA 2.0 requires ML-KEM-1024 by 2027-01-01". Honest mapping. - F-ENG-2: pqc.yaml's TLS step added in this milestone (was deferred from M1). Default port list mirrors ssl_expiring_certificate.yaml minus 1080 (SOCKS, not TLS). Tests: 91 unit tests in test_pqc.py + 7 integration tests = 98 total PQC tests, all passing under both serial and xdist-parallel. Full Nettacker baseline: 420 passed, 0 new regressions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ss fix Final wiring of the PQC compliance scanner. M3 closes the wedge. Verdict + compliance_notes finalized (F-CEO-1 honest mapping): - pqc_ready maps to "meets CNSA 2.0 ML-KEM-1024 baseline" ONLY when ML- KEM-1024 is advertised. ML-KEM-768-only / hybrid → "transitional — CNSA 2.0 requires ML-KEM-1024 by 2027-01-01". - classical_only → "fails OMB M-23-02 PQ posture baseline". - All four verdicts cite the relevant standard (FIPS 203, OpenSSH 10.1 WarnWeakCrypto, OMB M-23-02, CNSA 2.0). Opt-out wired (threat-model abuse-1 mitigation): - PqcEngine.run override checks options for pqc_no_active_probe truthy value; if set + method is tls_pqc_scan, short-circuits the active probe with a clear log line. SSH passive enumeration still runs. - Tripwire test asserts the library is NOT invoked under opt-out. - _is_truthy_extra_arg helper accepts true/1/yes/on (case-insensitive, whitespace-trimmed); rejects false/empty/None. Critical correctness fix discovered by M3 e2e: - M2 emitted TLS ClientHello with all-zero key_share buffer of IETF- pinned length. Real-world e2e against pq.cloudflareresearch.com returned decode_error — Cloudflare's PQ research server validates client key_share content, not just length. False-negative classical_only on a server that actually supports PQ. - Switched to empty KeyShareClientHello (zero entries) per RFC 8446 §4.2.8. Server replies with HelloRetryRequest specifying the group, same posture signal, no content validation. Verified live: scanner now correctly reports pqc_ready advertising X25519MLKEM768 for both pq.cloudflareresearch.com and cloudflare.com. End-to-end smoke (network-dependent, NETTACKER_NO_NETWORK_TESTS=1 to skip): - github.com:22 (or gitlab fallback) — SSH PQ kex enumeration. - pq.cloudflareresearch.com / cloudflare.com / openquantumsafe.org — TLS PQ named-group probe. - Loopback closed-port — non-network sanity that timeout path returns. User-facing docs: - docs/Modules.md gains the pqc_scan list entry plus a 140+ line PQC Compliance Scanner section: quick-start, verdict table, what we probe, safety operating model, compliance-mapping table, known v1 limitations, output JSON schema. - README.md gains a Key Features bullet linking to the Modules section. Tests: 31 new (4 unit/integration classes + 3 e2e). PQC suite total 121 tests, all passing serial + xdist-parallel. Full Nettacker baseline minus pre-existing-on-master Python-3.14 environmental flakes: 447 passed, 0 unexpected failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
M2's commit (07d9dfc) was supposed to add the tls_pqc_scan step to nettacker/modules/scan/pqc.yaml but the file edit was lost in the M2 batch — the M2 lessons / completion / runbook all describe the TLS step as wired, but the actual YAML file only contained the M1 SSH step. M2's unit and integration tests called PqcLibrary().tls_pqc_scan directly, completely bypassing the YAML→engine→library wire, so the regression sat invisible. User caught it by running `nettacker -m pqc_scan -i pq.cloudflareresearch.com -g 443` and getting an empty pqc_scan result. After the YAML fix, the same command correctly reports verdict=pqc_ready advertising X25519MLKEM768. Adds the tls_pqc_scan step to pqc.yaml with the standard TLS port set (443, 21, 25, 110, 143, 587, 990, 993, 995, 5061, 5222, 5269, 8443) and four regression tests in tests/core/test_module_pqc.py: - test_yaml_loads_via_templateloader_with_both_steps - test_expand_module_steps_yields_both_method_groups - test_yaml_tls_step_includes_443 - test_yaml_ssh_step_includes_22 These tests load the YAML through TemplateLoader the same way the runtime engine does, then run expand_module_steps to verify the manifest produces both ssh_pqc_scan and tls_pqc_scan sub-step groups. Asserts both methods are present, both default port lists include their canonical port. M1 lessons explicitly called out this gap as "missing tests that should exist now"; closing it now. Real-world verified: pq.cloudflareresearch.com:443 -> pqc_ready, advertises X25519MLKEM768, compliance_notes correctly says "transitional — CNSA 2.0 requires ML-KEM-1024 by 2027-01-01". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sage Adds a new "Post-Quantum Cryptography (PQC) Compliance Scanning" section to docs/Usage.md (placed before the API and WebUI section) covering: - Quick-start examples (single host, bulk, Docker) - How to read the verdict (jq + python one-liners) - CI gate snippet that exits non-zero on classical-only - pqc_no_active_probe operator opt-out for fragile environments - Verdict semantics table (pqc_ready / hybrid_only / classical_only / unknown) - Real-world examples (github SSH PQ-ready vs github HTTPS classical-only, google with MLKEM1024 = CNSA-2.0-baseline-met, etc.) README.md gets two new pqc_scan examples in the CLI Quick Setup block (SSH + TLS) plus a follow-up paragraph linking to Usage.md and Modules.md for full details, verdict semantics, opt-out, and compliance mapping. docs/Home.md (mkdocs site landing page) gets a 7th Key Features bullet mirroring the README, so the PQC scanner is discoverable through the hosted readthedocs site as well as the GitHub README. No code changes — pure documentation expansion. Builds on the existing `## PQC Compliance Scanner (pqc_scan)` section in docs/Modules.md which was added in M3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nner feat(pqc-scanner): add PQC compliance scanner module (TLS + SSH)
- Improved target expansion to extract ports and schemas from URLs. - Added baseline response comparison for HTTP requests to detect changes. - Introduced new CORS misconfiguration vulnerability detection module. - Updated various YAML configurations to support new response conditions. - Added unit tests for baseline response handling and target expansion logic.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (45)
Summary by CodeRabbitRelease Notes
WalkthroughThis PR introduces a complete post-quantum cryptography (PQC) compliance scanner module for OWASP Nettacker. It adds a new ChangesPQC Compliance Scanner Implementation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning |
|
PR validation failed: No linked issue and no valid closing issue reference in PR description |
Summary
This PR improves Nettacker's HTTP scanning accuracy for several cases that showed up during a lab assessment against modern Node/SPA applications (
bkimminich/juice-shopandnirocr/nodegoat). The main theme is reducing false positives caused by status-code-only matching while also fixing silent false negatives in header-based vulnerability modules.The changes are intentionally split between two layers:
content_length,content_sha1, missing-header matching, and catch-all baseline comparison)This should preserve the current YAML-driven module model while giving module authors safer matching tools for modern web applications.
Problems addressed
1. URL probe modules false-positive on SPA catch-all routes
dir_scan,admin_scan, andpma_scanpreviously treated200,401, or403as enough evidence that a path existed. That produces very noisy output against SPAs and catch-all routes that return the sameindex.htmlfor every unknown path.For example, Angular/React/Vue/Svelte applications commonly route unknown paths back to the frontend shell with
200 OK. In that situation, status-code-only matching cannot distinguish a real/adminpage from/nettacker-random-nonsense.This PR adds an opt-in
baseline_responsecondition for HTTP modules. When a module uses it, Nettacker performs a low-impact request to a random sibling path and only reports a probe hit if the probe response differs from the random baseline by status code, body length beyond tolerance, or body SHA-1.Applied to:
dir_scanadmin_scanpma_scan2. Header absence was not matchable by YAML header conditions
Several header vulnerability modules tried to detect unsafe header values, but a completely missing header could fail to match and therefore produce no finding. Missing security headers are often the common case, so this silently under-reports real issues.
The HTTP condition matcher now evaluates missing response headers as an empty string (
"") instead of a non-string falsey value. That lets existing regex-based YAML conditions explicitly match absence with^$.Updated modules:
clickjacking_vulncontent_type_options_vulnx_xss_protection_vulncontent_security_policy_vulnalready had an absence-compatible regex, but it benefits from the engine fix because missing headers can now match consistently.3. OPTIONS method detection missed CORS preflight method lists
http_options_enabled_vulnonly looked at the legacyAllowheader. Modern web frameworks frequently expose allowed methods throughAccess-Control-Allow-Methodson OPTIONS preflight responses instead.This PR makes either header sufficient evidence for the module:
AllowAccess-Control-Allow-Methods4. WAF detection produced false positives from status-code deltas
waf_scanhad a generic fallback heuristic: compare the baseline request status code to an XSS-payload request status code, and report "WAF detected" if they differ.That is too weak as a WAF signal. Normal application routing, caching, redirects, frontend fallbacks, and framework behavior can all produce status differences without a WAF or CDN in front of the app.
This PR removes the status-delta-only heuristic and leaves the existing positive-signature checks in place. WAF findings now require a vendor/header/body/status signature from the existing
iterative_response_matchdatabase rather than a generic status-code difference.It also removes the previous typo-bearing fallback log (
differenet).5. Explicit URL ports should not be blocked by default service discovery
When a user provides a URL with an explicit scheme and port, such as
http://jshop:3000, or provides-g 3000, Nettacker already has enough user intent to scan that port. The prior flow could run service discovery first, fail to classify the service, and stop with "no live service found" even though the requested HTTP service was reachable.This PR updates target expansion to preserve explicit URL scheme/port into the parsed runtime options and skip the service-discovery gate for explicit user-provided ports. This means modules such as
http_status_scancan run against common dev/test ports like3000,4000,8000, and8080without requiring-das a workaround.It also fixes the English message:
no any live service found to scan.no live service found to scan.6. Add a focused CORS misconfiguration module
This PR adds
cors_misconfiguration_vulnfor common unsafe CORS responses:Access-Control-Allow-Origincombined with credentialsPUT,PATCH, orDELETEThis complements the existing
http_cors_vulnmodule by adding checks for wildcard/reflected-origin and broad-method combinations observed in modern APIs.Implementation details
HTTP response fingerprints
nettacker/core/lib/http.pynow records two extra response fields for HTTP responses:content_lengthcontent_sha1These are exposed as normal YAML matchable response conditions. The YAML schema test was extended so module definitions can use those fields.
Missing header matching
Missing headers are now passed to regex matching as
"". This keeps the matching model simple and makes absence explicit in YAML:Baseline comparison condition
A module can now opt into catch-all filtering with:
For such modules, Nettacker requests a random sibling path and compares the probe to that baseline. The condition passes only when at least one of these differs:
max_content_length_deltaThis keeps the feature opt-in so it only affects modules that are vulnerable to catch-all false positives.
Explicit port handling
Nettacker.expand_targets()now usesurllib.parse.urlsplit()for URL targets. It extracts:arguments.portswhen-g/--portswas not separately suppliedarguments.schemawhen--schemawas not separately suppliedurl_base_pathIf
arguments.portsis present, the default service-discovery pre-pass is skipped because the user has already selected the port set to scan.Files changed
Core behavior:
nettacker/core/lib/http.pynettacker/core/app.pynettacker/locale/en.yamlModule definitions:
nettacker/modules/scan/dir.yamlnettacker/modules/scan/admin.yamlnettacker/modules/scan/pma.yamlnettacker/modules/scan/waf.yamlnettacker/modules/vuln/clickjacking.yamlnettacker/modules/vuln/content_type_options.yamlnettacker/modules/vuln/http_options_enabled.yamlnettacker/modules/vuln/x_xss_protection.yamlnettacker/modules/vuln/cors_misconfiguration.yamlTests:
tests/core/lib/test_http.pytests/core/test_app_targets.pytests/test_yaml_schema_and_regex.pyCompatibility notes
baseline_response.-g/--portsand explicit URL ports now bypass the service-discovery gate for downstream modules. Default scans without explicit ports still use the existing service-discovery pre-pass.Validation
I ran the focused regression and schema checks in the repository virtualenv:
.venv/bin/python -m pytest -o addopts='' \ tests/core/lib/test_http.py \ tests/core/test_app_targets.py \ tests/test_yaml_schema_and_regex.py -qResult:
I also ran Ruff on the changed Python files and new tests:
Result:
And checked whitespace:
Result: clean.
Reviewer notes
The most important design question is whether
baseline_responsebelongs in the generic HTTP matcher as implemented here, or whether maintainers would prefer a more explicit condition name or a module-level option. I kept it as an opt-in condition because it fits the existing YAML condition model and avoids changing unaffected modules.A second review point is CORS severity. The new module currently treats credentialed cross-origin access and broad unsafe methods as reportable. If the project prefers more granular severities for CORS combinations, this module can be split into multiple YAML steps or separate modules.
Finally, the WAF change intentionally favors false-positive reduction over broad heuristic detection. A status-code delta alone is not strong enough evidence for "WAF detected" on modern apps; vendor/header/body signatures remain the safer path.