MR.
CASE STUDY · 02 / 04
CLI TOOL · OPEN SOURCE

Scanner.

Non-intrusive attack surface and transport security scanning for SaaS.
ROLE
Sole author
PERIOD
2026
STATUS
Open source · MIT
READ
≈ 7 min
CHAPTER 01
THE BRIEF

What is publicly exposed — and how risky is it?

Most real-world SaaS incidents do not start with a clever exploit. They start with a forgotten subdomain, an expired certificate, a missing security header, or a CDN frontend nobody documented. The attack surface drifts and nobody notices.

Capital One's 2019 incident routed through an exposed metadata service nobody on the asset list. The MOVEit exploits in 2023 spread across hundreds of forgotten ingestion subdomains. Half the post-incident reviews in the past five years contain a sentence that begins with "we didn't know we still had...". The attack surface of a modern SaaS doesn't fail because someone wrote bad code; it fails because the perimeter the security team thought they were defending hasn't matched the perimeter that was actually serving traffic for months.

The scanner answers one question, in one command: what is publicly visible about this domain right now, and where is the security posture weakest. Asset discovery, transport security, HTTP headers — combined into a single deterministic risk view that a security engineer can hand to an ops team on a Monday. The output isn't a verdict; it is a triage queue.

The scope is intentional. This is not a penetration test runner. There is no exploitation, no authentication, no port brute-forcing, no traffic that an enterprise NDR team would have to defend. Everything is passive enumeration plus standard HTTPS — the same kind of traffic any browser already produces. That constraint is the product, not a limitation. A scanner safe enough to wire into CI, to point at a partner's domain during procurement review, or to run against a production environment without writing an internal memo first, is more useful than a scanner with broader capabilities and tighter guardrails.

Audience is the security engineer who needs a concrete reading of an external posture in under a minute. Adjacent uses: drift detection between a baseline and current state, third-party risk during vendor onboarding, due-diligence in M&A, blue-team exercises that need a reproducible "what does the attacker see first" view.

CHAPTER 02
THE APPROACH

Passive intelligence, deterministic scoring, structured output.

Discovery starts with Certificate Transparency logs — a public, append-only record of every certificate ever issued for a domain. CT-based enumeration is the closest thing to ground truth for "what subdomains exist", without ever sending a packet to the target. The browsers that enforce CT have already done the hard work; the scanner just reads the logs. Brute-force wordlists are explicitly avoided; they are noisy, never complete, produce false positives that erode trust in the report, and generate the kind of traffic that gets the scanner blocked at the WAF before the second run.

Discovered assets are resolved (A / AAAA), probed over HTTP and HTTPS with redirect awareness, and inspected for transport security and HTTP header configuration. Each finding ships with a severity, a one-paragraph technical explanation, and a concrete remediation hint. Risk is scored deterministically — explainable rules, no ML, no black box. The same domain produces the same risk view on every run, which is the precondition for using the output as a baseline against drift.

Redirect-awareness sounds boring; it is the difference between a scanner that finds the real production endpoint and one that reports on the bare-domain marketing page. A request for example.com often lands on www.example.com, then on a CDN frontend, then on the actual app — three different TLS configurations, three different header sets, three different risks. The scanner walks the chain with a bounded redirect budget and reports each hop as its own asset, which is what an attacker would in fact see.

Output is two-track on purpose. A timestamped JSON artifact for machine consumers — diff against a previous baseline, feed into a SIEM, persist for audit. A rich CLI summary for fast triage — risk overview at the top, top contributing reasons named, environment warnings called out. The same scan produces both; neither is downstream of the other.

FIG. 01 · SCAN PIPELINE
DOMAIN EXAMPLE.COM DISCOVER CT LOGS DNS A / AAAA HTTP/HTTPS PROBE REDIRECT-AWARE CHECK TLS VERSION CERT EXPIRY HSTS · CSP · XFO XCTO · REFERRER SCORE DETERMINISTIC OUTPUT: scan_<timestamp>.json + RICH CLI SUMMARY
"The goal is visibility and prioritisation, not exploitation."
CHAPTER 03
WHAT IT CHECKS

Five signal classes, every finding explained.

Every check produces the same shape of result — a severity, a short technical reason, and a concrete remediation. The risk score is the rolled-up view; the per-finding output is what an engineer actually fixes from.

C.01 · ASSET DISCOVERY

Subdomains via Certificate Transparency.

Passive enumeration over public CT logs. Deterministic — no wordlists, no brute force. Degrades gracefully when corporate proxies block the lookup, with a structured warning rather than a silent miss. The output names every subdomain seen on a public certificate, with the issuing CA and earliest seen date — enough context to triage which assets are stale.

C.02 · TRANSPORT SECURITY

TLS protocol versions and certificate expiry.

Detects deprecated TLS 1.0 / 1.1 endpoints and certificates approaching expiry. Both are silent failures until the day they aren't — TLS 1.0 endpoints get blocked by clients without warning, certs expire on a Sunday and the on-call gets paged at 03:00. The scanner names the asset, the version, and the days-to-expiry, so the fix can be queued before the failure.

C.03 · HTTP HEADERS

HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy.

The header set the OWASP Secure Headers Project treats as table stakes. Each missing or weak value produces an explicit finding with a fix — not "set CSP" but the specific directive missing for this endpoint. HSTS without preload is flagged differently from no HSTS at all. A weak Referrer-Policy is flagged differently from a missing one.

C.04 · RISK SCORING

Asset-level risk, scan-level summary, top contributors.

Rules-based, explainable, reproducible. Each finding contributes a fixed weight to the asset score. Asset scores roll up to a scan score. The summary names the top reasons the score is what it is — no opaque number to argue with, no "the model says so." A reviewer can disagree with a weight; that disagreement is itself a config file change, versioned, reviewable.

C.05 · ENTERPRISE FALLBACK

Proxy-aware degradation.

In restricted networks where CT lookups or HTTPS probes fail, the scanner emits structured warnings instead of crashing. The report says exactly what was skipped and why. A scan run from a locked-down corporate environment looks different from a scan run from a clean network — but both are honest about their gaps. Silent degradation is the worst possible failure mode for a security tool; this avoids it by design.

C.06 · DRIFT DETECTION

Compare today's scan to yesterday's baseline.

The JSON artifact is dated and stable enough to diff against a previous run. New subdomains, expired certs, regressed headers — all surface as deltas. Wired into CI on a daily cron, the scanner becomes a drift sensor for any production domain: an asset that stopped serving HSTS yesterday is a finding today, before the next pen-test cycle catches it three months out.

CHAPTER 04
OUT OF SCOPE

What the tool intentionally does not do.

No exploitation. No authenticated crawling. No port scanning beyond the standard HTTP/HTTPS probe. No vulnerability scanning. No brute forcing. No intrusive traffic generation. No credentials, secrets, or sensitive data are collected. Each exclusion is principled, not capability-limited.

No exploitation because the moment a tool tries to verify a finding by exploiting it, it becomes unsafe to run in shared environments. The scanner reports a posture; verifying that posture is the job of the human reviewer with the right authorisation.

No authentication because credentials in a scanner widen the blast radius. A scanner with credentials can be coerced into doing anything those credentials authorise. Pure-public scanning is bounded; authenticated scanning is unbounded by definition.

No port scanning beyond the standard probes because broad port scans look identical to reconnaissance from an attacker, get the source IP blocked at the edge, and produce data that is rarely actionable from outside the asset's own VPC. The scanner sticks to the ports a browser already touches.

No vulnerability scanning because that's a different category of tool with different constraints (CVE feed currency, signature management, false-positive triage). Mixing it in would compromise the "safe to run in production" property that makes this scanner useful in the first place.

The boundary is the entire point. A scanner that stays inside this boundary is safe to run against production-like environments, safe to wire into CI for drift detection, and safe to point at a partner's domain during a procurement review without writing a memo first. Tools that try to do everything end up trusted nowhere; tools with a tight scope earn the right to be wired into automated pipelines.

scanner · usage · simplified
# single command, single domain
$ python -m ass.cli example.com

# produces:
results/scan_<timestamp>.json   # structured artifact
                                # + rich CLI summary:
                                #   risk overview
                                #   top risky assets
                                #   finding counts
                                #   environment warnings
CHAPTER 05
ENGINEERING DECISIONS

Boring on purpose. Boring is the feature.

Python src/-layout package. The boring layout. No clever single-file CLI, no monorepo experiments. pyproject.toml, importable cleanly, installable in editable mode for development. It is the layout that survives a maintainer change because the next maintainer doesn't have to learn a project-specific convention before they can ship.

Pydantic models for typed data flow. Every value that crosses a module boundary is a typed model — scan request, asset, finding, severity, scan result. The validation runs at the edges; the inside of the program speaks in objects, not dicts. When the JSON output schema needs a new field, it's added in one place and the type-checker finds every consumer that needs to handle it.

Deterministic scoring. Rule-based, weights in config, scoring logic unit-tested. Two reviewers can argue about whether a missing X-Content-Type-Options should weigh the same as a missing CSP — and that argument turns into a config-change PR with a diff. No ML, no fitted thresholds, no "the model says so." The point of a security tool is to be argued with; the scoring layer is built to support that.

Python 3.10–3.12 matrix in CI. Three versions, every push, every PR. New Python releases break things in subtle ways (importlib metadata behaviour, exception group handling, type-evaluation timing). Catching it in CI before a production user does is the cheap part of the bargain.

Ruff for linting and static analysis. Fast, opinionated, zero-config-shipped. Fewer lint debates, more shipped code. The configuration that ships in the repo is the configuration that runs in CI is the configuration that runs locally — no drift, no per-developer overrides.

JSON artifact format pinned and versioned. Downstream consumers can depend on the schema. Breaking changes go through a major version bump. This is the contract that lets the scanner be wired into CI pipelines, SIEM ingestion, or daily-cron drift detection without rewriting the consumer every time the tool ships a feature.

CHAPTER 06
ENGINEERING HYGIENE

Trust the output, trust the apparatus.

A security tool's output is trusted exactly as much as the tool itself. That makes engineering hygiene a security property, not a nice-to-have. The repository is built to make that trust auditable from outside.

Unit tests for every scoring rule. Each finding-to-weight mapping is a test. Each severity tier transition is a test. The scoring is small enough that 100% branch coverage is achievable and the cost of changing a weight stays bounded.

Integration tests against fixture domains. Synthetic assets with known TLS / header configurations exercise the full pipeline end-to-end. The fixtures double as documentation: a reader can see what a "well-configured asset" looks like next to what a "stale, weakly-protected asset" looks like, and what the scanner says about each.

GitHub Actions CI on every push. Test matrix, lint, type-check, build. Nothing merges that doesn't pass. The CI badge is a contract: green means the version of the code on main is the version that produced the test results.

No external state. The scanner does not maintain a database, a state file, or a cache between runs. The only persistent artifact is the JSON output — and the user owns that file. Tools that maintain hidden state are tools whose behaviour is hard to reason about; making the state explicit makes the apparatus explainable.

MIT license, no telemetry. The tool does not phone home. It does not collect usage data. It does not require an account. The bar for running a security tool against your own infrastructure is low by design.

CHAPTER 07
ROADMAP & LIMITATIONS

Honest about what's next, and what won't be.

The roadmap is incremental on purpose. Big additions risk the safe-to-run property; small additions compound.

Baseline comparison and drift detection. First-class diff between two scan artifacts: new assets, regressed headers, certs that moved closer to expiry. Wired into a daily cron, this is the difference between knowing your posture today and knowing your posture is degrading week-over-week.

Extended TLS analysis. Cipher suites, named curves, OCSP stapling status, signature algorithms. Today the scanner reports the negotiated version and the cert expiry. The cipher posture is the next layer of detail that ops teams ask for first.

JSON schema versioning. Pin the output schema explicitly, ship migration tooling for breaking changes. Downstream consumers should be able to depend on the format without holding the tool back from improvements.

Containerised read-only execution. Distroless image, no writable layers, suitable for cloud-runner execution from a serverless platform. The scanner's "no external state" property maps cleanly to a container with no persistent volume.

Limitations stay limitations. The scanner relies on passive public data sources and standard HTTPS — both have known gaps. CT-only enumeration misses internal subdomains that never receive a public cert. HTTP-only header inspection misses anything that depends on a TLS handshake quirk. The tool says so explicitly in its output rather than pretending those gaps don't exist.

— END OF REPORT —