Skip to content

feat: discover hostnames from TLS certificate SNI/CN/SAN data - #51

Open
bandrel wants to merge 10 commits into
nightlyfrom
feat/sni-hostname-discovery
Open

feat: discover hostnames from TLS certificate SNI/CN/SAN data#51
bandrel wants to merge 10 commits into
nightlyfrom
feat/sni-hostname-discovery

Conversation

@bandrel

@bandrel bandrel commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Why

TLS services frequently reveal hostnames the operator never typed into the target file — via the certificate's commonName and subjectAltName (SAN). SpooNMAP already runs the ssl-cert NSE script on common TLS ports for External scans (used today only to flag expired certs); this adds no new scanning, just parses output already collected.

What changed

  • _extract_ssl_cert_hostnames() parses ssl-cert output for the certificate's commonName (anchored to the Subject: line, never Issuer:) and each Subject Alternative Name: DNS: entry — deduped, order-preserved, wildcards included.
  • _merge_ssl_cert_hostnames() walks nse_results/*.xml after the NSE script pass and fills gaps in the operator hostname map with the first non-wildcard name per host — never overwriting an operator-supplied hostname from the target file — then rewrites discovery/ip_hostname_map.json. Runs before both _aggregate_result_dir() and generate_findings() so spoonmap_output.* and the findings report reflect it for the current run.
  • New LOW-severity finding, TLS Certificate Hostname(s) Identified, listing every name found (wildcards included, since they're informative even though unused for targeting).
  • ip_to_hostname map is not persisted forward across --resume by design: preprocess_targets() rewrites ip_hostname_map.json from the target file alone on every run. A resume re-derives the same cert hostnames from cached nse_results/*.xml instead. This is deliberate — a cert-derived hostname reaching create_hostname_target_file() could send nmap after a name-resolved address different from the one actually in scope (e.g. a shared/CDN certificate), so nothing carries the merged map into the next run's targeting.

Verification

1208 passed, 5 skipped (skips are the documented environmental/root-gated ones) · 100% coverage on spoonmap.py (2542 stmts, 0 miss; floor is 95%) · ruff check spoonmap.py tests/ clean.

Built via subagent-driven development: 4 tasks (extraction helper → merge/wiring → finding → docs), each with an independent spec-compliance + code-quality review; one fix round in Task 2 (a bare .attrib['id'] subscript that violated the file's defensive-parsing convention). A final whole-branch review then caught and a single fix wave closed:

  • two stray SDD planning docs that had crept back into docs/ despite an explicit prior decision (Fix broken wheel/sdist packaging, and harden CI (timeouts, Dependabot, root-only NSE job, workflow lint) #42-era) to keep them out of this public repo
  • a CLAUDE.md claim overstating what survives --resume (see the design note above)
  • the module's only two uncovered statements, one of which (a find('address') fallback in the new map-building code) was removed rather than tested, since a fallback there could let a dual-stacked host's IPv6/MAC string become a hostname-map key — this repo's IPv4-only doctrine sanctions that fallback only for display paths, not map-building ones.

Docs

CLAUDE.md gains a "TLS certificate hostname discovery" bullet under Key Implementation Details, matching the file's existing dense, rationale-first documentation style.

🤖 Generated with Claude Code

bandrel and others added 10 commits August 28, 2026 10:36
docs/ is not tracked on main, and these two files carry absolute local
paths from the machine they were written on, same as 9ea2bb3. Add a
docs/ entry to .gitignore this time so the pattern can't recur.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The TLS certificate hostname discovery bullet claimed the merged
ip_hostname_map.json benefits "a future --resume". It doesn't:
main() calls preprocess_targets() unconditionally, including on
resume, and that rewrites ip_hostname_map.json from the target file
alone with no merge of the existing file's contents, wiping out the
cert-derived entries. A resume run's behavior is still correct
because it re-derives the same cert hostnames from the still-cached
nse_results/*.xml — re-derivation, not persistence — and that's
deliberate: a persisted cert-derived hostname reaching
create_hostname_target_file() could send nmap after a name-resolved
address different from the one actually in scope (a commonName off a
shared/CDN cert can resolve elsewhere entirely). Also documents that
the new finding was added to _PER_HOST_DETAIL_TITLES, without which
findings.txt would collapse the group with no per-host detail.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_merge_ssl_cert_hostnames() builds the persisted ip_to_hostname map
that later feeds create_hostname_target_file(), unlike the display-only
paths (generate_findings(), _scan_extra_sql_ports()) that CLAUDE.md's
IPv4-only doctrine allows a find('address') fallback for. Remove that
fallback here so a dual-stacked host's IPv6 or MAC string can never
become a key in the map, closing the last uncovered branch in the
function and aligning it with the existing doctrine in one move.

Also add a test covering the `if not fname.endswith('.xml'): continue`
guard, which is load-bearing in production (nse_results/ legitimately
holds .coverage sidecars and .failed quarantine files) but was
previously untested. And update a stale comment in
_write_findings_txt to mention the new TLS certificate hostname list
case in _PER_HOST_DETAIL_TITLES.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@bandrel
bandrel requested a review from Spoonman1091 August 28, 2026 18:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant