Abandonware: The Attack Surface Attackers Now Reach at Scale

We build attack-surface monitoring for a living, and we still had zombie subdomains sitting in Google's index. An internal infrastructure audit turned up our own 2024 seed-era staging site, five other stale published copies of our marketing site, dead CTAs, a personal scheduling link that bypassed the CRM, and missing SPF/DMARC records that were quietly landing our cold email in spam. Nobody put those there on purpose. Nobody remembered they existed. The assets that hurt you are almost never the ones you're looking at — they're the ones you forgot you owned, and they are now cheaper for an attacker to find than ever.

For most of the history of AppSec, the assets you forgot you owned were protected by a kind of accidental obscurity. Finding the archived-but-still-deployed service, the subdomain from a project that shipped and got abandoned, the repo with no commit in years still running in production — that took a patient adversary willing to spend real hours on reconnaissance. Obscurity wasn't a control, but it was friction, and friction bought time.

That floor just dropped. Cheap, capable open-weight models mean a single attacker can now direct automated reconnaissance at a scale that used to require a team — enumerating subdomains, fingerprinting services, and probing forgotten hosts without adding a single person. Attackers have decoupled their reconnaissance capability from headcount. Defenders have to answer in kind: the only durable response to automated recon is automated coverage that is continuous, controlled, and measurable — outcomes decoupled from the size of your security team. That is the thesis behind everything Nullify builds, and it is the lens for this post.

This is the part of an AppSec program that tooling consistently ignores. Scanners point at the repos and hosts you tell them about. But the attack surface that gets breached is disproportionately the surface nobody is pointing anything at. Nullify's answer is Bug Hunter — a continuous, autonomous external attack-surface scanner that runs every day against the public-facing assets in your inventory, watches for what changed, and only pulls a human in when something actually did. It is the discovery arm of what we call the Finding loop: the continuously-running half of the platform whose whole job is to surface real, previously-unknown exposure — so that the Fixing half never has to guess at what to work on. Bug Hunter exists to watch the surface nobody registered: continuously, cheaply, and with a hard rule that a human only hears about what actually changed.

The dead estate is bigger than the live one

We don't have to argue this in the abstract. In one enterprise engagement — a large financial-services organization that gave us temporary read access to scope a proof of value — the numbers told the story on their own. The estate was huge but mostly dead:

  • 2,419 repositories total, of which 1,409 (58%) were archived.
  • A "live" surface of 1,010 repos — but only 405 had seen activity in the last 90 days.
  • Of those live repos, only 385 had any scanning enabled at all.
  • 531 of the live repositories were complete blind spots — origination, identity, payments, and gateway code with zero coverage.

A majority of a real enterprise's estate was archived, and even inside the "live" slice, more than half of those 1,010 business-critical repos had no security coverage whatsoever. This is not an outlier. We've heard the same thing from prospects in almost thesis-ready language: they have thousands of repos and no confident answer to whether each one even runs in production. On a real customer's estate, we watched engineers page through repos marked "never scanned," many with no commit in years — and it was the customer's own security contact who pushed back on us, asking whether a specific repo was within coverage and whether its recent commit had actually been scanned. People are surprised by what isn't being watched, and often they're the ones who notice first.

You cannot fix what you can't see, and you can't see what you've forgotten. So the first job isn't detection. It's inventory that maintains itself — the same thesis behind the queryable asset graph we build for everything we already have credentials to see; Bug Hunter is what extends that discipline to the estate nobody remembered to register in the first place.

Deterministic first, LLM only where it earns its keep

The reflex in 2026 is to throw an agent at everything. We think that's backwards for attack-surface monitoring, and Bug Hunter is built to prove it. Its daily run leans on deterministic tooling and pure Go logic wherever the answer is a matter of fact, not judgment, and spends model tokens only on the compound-reasoning work that genuinely needs them.

Concretely, of Bug Hunter's coverage categories:

  • Subdomain takeover detection runs on a signature tool — always on, real findings, no LLM.
  • Open ports and service/version identification come from masscan plus nmap -sV — again, no model in the loop; the finding carries only what the scanner actually observed.
  • TLS issues, missing security headers, sensitive-file exposure, and GraphQL introspection are four pure-Go finders that reuse data other tools already collected. The TLS finder walks certificates tlsx already captured — no new network I/O, no LLM cost — and emits distinct typed findings for expired certs, legacy protocols, weak ciphers, self-signed certs, and hostname mismatches. The header finder replaced an earlier Python agent that "had the same data shape in mind but never produced findings." Deterministic code shipped where the agent stalled.
  • Only the higher-intensity, compound-analysis suites — browser-driven recon of admin portals and debug consoles, and per-port protocol probing — spend real model tokens, and only when the customer's intensity setting allows it.

That deterministic layer is essentially free: it's reuse of data the scan already paid to collect. This is the efficiency argument made structural rather than aspirational — real, typed findings for the least compute, because we don't send a language model to do arithmetic a string comparison can do. Internally, our own engineers describe Bug Hunter, without ceremony, as "a glorified ASM." We mean that as a compliment. Attack-surface management done continuously and cheaply is exactly the boring, reliable thing most programs are missing.

Autonomy with a spend ceiling: budget-driven cadence

"Controlled and measurable" can't just be a slogan when the system runs itself every day across thousands of tenants. The place that gets tested hardest is cost: an autonomous scanner that quietly runs up an unbounded model bill is not autonomy, it's a liability. Bug Hunter's answer is a budget-driven cadence with hysteresis — one of the mechanisms we're proudest of precisely because it makes self-governance concrete.

Each tenant has a yearly bug-hunt budget. Every morning, before dispatching a tenant's scan, the orchestrator reads how much of that yearly headroom has been consumed and decides the effective cadence:

  • Below 60% consumed, the scan runs at its configured cadence — full speed, daily by default.
  • At or above 80% consumed, the cadence downshifts one step — daily becomes weekly, weekly becomes fortnightly, fortnightly becomes monthly — so a tenant that's burning through its budget automatically slows down instead of blowing past the ceiling.
  • Between 60% and 80%, the system holds the previous decision rather than recomputing from scratch.

That last rule is the hysteresis, and it's the detail that makes the whole thing usable. Without it, a tenant whose spend hovers right around a single threshold would flap — daily one morning, weekly the next, daily again — as the number drifts a percent up and down. The asymmetric band (downshift only at the high mark, upshift only below the low mark) means the resolved cadence is stable: it changes when spend genuinely crosses a boundary, not when it jitters. It's the same principle a thermostat uses to avoid short-cycling.

Two properties matter for trust. First, this cadence decision is advisory scheduling only — the hard spend ceiling is a separate atomic admission check at the moment a run starts, so the budget can never be exceeded even if the scheduling hint were wrong. The cadence logic decides how often to bother trying; admission decides whether this specific run is allowed to spend. Second, every decision is logged with the inputs that produced it — configured cadence, previous cadence, headroom consumed, resolved cadence — so you can see exactly why your scans ran when they ran. Autonomy you can audit is the only kind worth having.

The mechanism that turns a firehose into a diff

Continuous scanning is worthless if it re-alerts you every day about the same 900 things. The point of monitoring isn't the scan — it's the delta. Bug Hunter's answer is a stable fingerprint plus a four-bucket diff, and it's worth being precise about how deterministic this is.

Every finding gets a fingerprint built from a fixed, escaped format:

`` v2 | host | port | proto | service | type | http_method | http_path ``

Host is lowercased and de-dotted, method is uppercased, path is canonicalized (query and fragment stripped, slashes normalized), and every segment is escaped so a literal pipe or backslash in one field can never collide with another. This is string math, not a model's opinion — the same fingerprint on the same asset produces the same key every run, for free. That single primitive also drives the internal probe ledger, so a probe outcome and the finding it produced reconcile one-to-one.

With stable identity, the daily diff sorts every finding into exactly one of four buckets: New, Reopened (a previously-fixed finding that came back), Unchanged, and Resolved (gone from the latest scan). A human sees New and Reopened. That's it. Unchanged is silence by design.

We've watched this pay off across the customer base. When a supply-chain worm scare hit, an automated sweep ran across every tenant's entire repository estate — from dozens to several thousand repos depending on the customer — and produced exactly one human-facing message per tenant: a clean yes/no that no compromised package was present. Not a queue to triage — an answer. Monitoring that hands you a conclusion instead of a workload is the entire point.

The human lever stays in your hand

Autonomous-by-default does not mean the security engineer is locked out — it means they're not conscripted into busywork they didn't ask for. The controls that matter are the ones that persist.

When a human triages a Bug Hunter finding — allowlists that admin panel they already know about, archives it, overrides its severity, marks it a false positive — that decision is copied forward on every subsequent daily scan. The workflow explicitly carries over allowlist state, archive state, severity overrides, priority, and notes, while assigning lifecycle fields itself. Allowlisted and archived findings are pulled out of both sides of the diff before comparison, so they can never be misclassified as "New" if rediscovered and never re-trigger a notification. As the code comment puts it, that's "the only way to preserve customer-classified findings across daily scans." Say "I know about that, ignore it forever" once, and it stays ignored — without going dark on genuinely new exposure.

Power users get more levers, all rate-limited so nobody can accidentally hammer their own infrastructure:

  • Per-suite enable/disable, where a disabled suite is skipped before admission so it never bills against quota.
  • Scan intensity (low/medium/high) — the primary control over token spend, since lower intensity skips the heavy LLM suites entirely.
  • Scope include/exclude selectors, with excludes enforced at the scope layer before any tool runs — masscan, nmap, nuclei, and the browser recon never even see an excluded host.
  • Per-vuln-class severity floors that gate event emission, so customers aren't notified about findings below the bar they set.
  • On-demand scans — a full re-scan or a single-suite re-run — for when you don't want to wait for the next daily window, which fires at 9 AM in your schedule's local timezone.

Each suite maps to its own billing agent type, so cost is tracked independently per suite. You can see, per suite, what you're spending.

Watching the internet responsibly — and showing our work

A tool that actively probes internet-facing assets has to be trustworthy about two things: what it's allowed to touch, and what it claims to have found.

On authorization, the rules of engagement are fail-closed. A self-serve customer can't just type in a domain and have us start probing it — ownership has to be proven first (DNS TXT record, a hosted HTTP file, a meta tag, or a platform vouch on behalf of a bug-bounty program), and active testing only proceeds when the config carries an explicit acknowledgement. Scope validation rejects the dangerous defaults outright: loopback, RFC1918 ranges, the AWS metadata IP, bare wildcards, 0.0.0.0/0. We know these rails matter because we've caught our own regressions in them — an earlier build had a scope check that had degraded into an unconditional return true, enforcing nothing. We found it and fixed it, and we'd rather tell you that than pretend our history is spotless.

On evidence, the rule is that a finding carries only what the scanner actually observed. An earlier version of the port scanner attached synthetic GET / requests as fake evidence; that bug is fixed, and the finding now shows only real observations. We swapped nuclei's opaque -automatic-scan flag — "a black box per tenant" — for an explicit, debuggable tag list. And the diff endpoint reads from the authoritative findings repository, not from a raw S3 dump, so your triage and allowlist state are visible in the diff itself. Every bucket, every carried-over decision, every suite's cost is inspectable. An attack-surface monitor you can't audit is just a different kind of blind spot — so we built one you can.

The surface you forgot is still yours

The uncomfortable truth is that the assets most likely to get you breached are the ones with the least attention on them — archived, undocumented, running on infrastructure whose owner left two reorgs ago. A majority of a real enterprise estate can be dead weight, and the live remainder can still be more than half uncovered. Nobody is going to remember all of it — and now nobody has to, because attackers using cheap automated recon certainly won't wait for you to. That's precisely why the job belongs to a system that runs every day, governs its own spend against a budget, spends model tokens only where judgment is actually required, and hands a human a short list of what changed instead of a standing backlog of what exists.

We got caught by our own forgotten subdomains once. It's a good reminder that the surface you stopped watching didn't stop being yours.

If you can't give a confident answer to which of your assets still run in production, that's exactly the gap this closes. Start with the queryable asset graph for everything you already have credentials to see, then let Bug Hunter extend the same discipline to the estate nobody remembered to register. Reach out to the team at Nullify to point it at your external attack surface.

Book a demo