Settings has six tabs: General, API keys, Proxies, Notifications, AI and
Users. Each tab is one card whose controls save on change; list tabs are
tables with a row menu and their Add action in the tab row. A non-admin
sees General and AI, read-only.
Out-of-band testing is the Callback server sheet on the Arsenal Nuclei
tab, with a Need a callback count that filters the library to the same
rows. Bounty Hub settings are a sheet on the Bounty Hub page. The old
settings routes redirect to both.
- API keys store the last test result on the row and clear it when the
key changes. Providers report whether a test exists. The usage count
is gone: only ViewDNS ever incremented it.
- Notification channels store the last delivery, written by the
notifier on its own connection. A channel offers the seven event
types the product emits, program changes on by default, and levels
named All events, Warning and critical, Critical only.
- A default proxy applies to a scan launched without a context and is
preselected for new contexts. A pool hands each scan one endpoint,
proxy URLs bracket IPv6 hosts, and deleting a proxy clears it from
its contexts.
- Users can be added with a role, disabled and promoted. An account
cannot change itself, and one that owns records answers 409.
- Retention windows are validated and include keep forever. The
instance name cannot be blank and names every page title.
- The AI list drops the two switches nothing read; each feature states
what it sends. The out-of-band Off help says the checks are skipped.
Connectors, Bounty Hub and Remote control lose their extra page
padding. Migration 5d2e9a8c1b47 adds the test and delivery columns and
drops proxies.mode.
Claude-Session: https://claude.ai/code/session_01RVAHQxZ7JAkoEiTCDiHYue
Every provider carries a group: subdomain sources, lookups, exploit
intelligence, bug bounty platforms, out-of-band testing and chat. The
labels are one map in shared/definitions/api_keys.py with a mirror in
config/api-keys.ts, and the provider list returns the group and its
label. The Telegram provider and the shared notification bot are
mirrored on the frontend, and a grammar test pins all three mirrors.
Claude-Session: https://claude.ai/code/session_01RVAHQxZ7JAkoEiTCDiHYue
An engine keeps its three intensity presets and now carries optional
per-tool rate and concurrency in transport_overrides. Any value that
differs from the preset shows as Custom; an empty field uses the preset.
A context rate caps the engine value rather than replacing it, and rate
is settable only for tools that have one. Edited through a Rates panel
on the engine builder, validated and clamped, carried through create,
update, duplicate, preview and launch.
Claude-Session: https://claude.ai/code/session_01Bb4u8p34euie1YFzb7qCPu
A Lookalike domains stage in the Analysis group generates permutations of the
target's registrable domain with dnstwist, resolves them with dnsx (a, aaaa, mx,
ns, one retry of silent names) and keeps the registered ones. A name whose only
answer is its TLD's wildcard address is dropped. Registered lookalikes are
fetched and compared with the target's page (ssdeep), and the highest ranked are
read over RDAP for registrar and creation date. Passive intensity skips the fetch.
Each lookalike carries one verdict: linked to target, similar page, MX
configured, parked, web content or registered. Linked is a redirect to the apex,
non-shared nameservers in common with it, or a project target. Placeholder MX
values and domain marketplace landers are not counted as mail or web content.
Rows live in lookalike_domains per scan; review state lives in lookalike_triage
per target and domain, so it carries across runs. The scan overview and the
target page show a lookalike line beside the associated domains, the target page
adds a cell, and a sheet lists the lookalikes with the changed characters marked,
per-verdict filters, records per row and review actions.
dnstwist and ppdeep join the tools package.
An injection-echo template (command injection, RCE, SSTI, shellshock) proves
itself with a token echoed into the response body. A page that reflects the
requested URL into a canonical or og:url tag satisfies that matcher without a
command running. The guard drops such a match for those template classes only,
after URL-decoding both sides, and never for XSS or out-of-band checks. Each
drop is counted on the coverage row and shown in the coverage strip.
A new AI Detection stage in the Analysis group runs julius v1.4.19 over
every web asset root and names model servers, AI gateways, AI
applications, cloud AI endpoints and MCP servers, with the models they
list. julius's MCP probe runs alone against /mcp and /api/mcp. Batches
of eight roots are paced to the scan's rate, AI ports are probed first,
and a minute budget and an asset cap are reported when they cut.
Results live on http_assets.ai_* and fold onto hosts and onto services
per ip:port, so an IP target with no host rows still carries them. Web
asset and service rows show the service, the host and service grammars
take ai: and ai.model:, the dashboard and target page count services per
product through /ports/ai, two built-in exposure rules flag AI services
and unauthenticated model lists, and compare watches ai_services.
The AI default ports join WEB_PORTS. julius ignores -f without a
positional target, so targets go on stdin.
- Store each run's row, first-seen and retired counts (scan_deltas, scan_retired), valid while
the revision stamp matches scan_revisions; statement triggers on the seven result tables bump
it. Finalize warms, a beat backfill fills the rest, a live run answers from its last count and
queues a refresh. /scans 1.5 s -> 43 ms, dashboard overview 5-10 s -> 0.9 s.
- lead_cache holds a live scope for 20 s with a stale copy, and every build is single-flight;
search pages opt out.
- Leads: lowercase subdomain names (CHECK) and LIKE for host:, a guard for empty JSON arrays,
branches as MATERIALIZED CTEs read to the cap, every count in one statement.
- Addresses and services: the derived CTE as one grouped pass, rows and count in one execution
(page_rows), GIN lookups for page hosts, facets through grouping sets.
- Correlation carriers counted by EXISTS; takeover latest-sighting in SQL, no silent cap.
- Screenshot backfill partial index, nine unused low-cardinality indexes dropped, hostname
helpers memoised, vhost names normalised.
Launches past the limit stay pending and start in launch order when a slot
frees, so each running scan gets full speed and memory stays bounded.
- run_scan admits a pending scan under an advisory lock: running plus waiting
scans ahead of it must be under the limit
- finalize, cancel, pause, delete and a limit change start the next scans;
the reaper re-sends only scans whose turn has come
- automatic limit: live worker-scans stage slots over the widest step,
so --scale raises it; Settings sets a fixed limit from 1 to 50
- watch probes skip the queue, resume is never held
- queued scans show how many are ahead
Every web asset carries the analytics, tag manager and ads accounts its
page loads (Universal Analytics folded to its account, GA4, GTM, Google
Ads, AdSense, Meta pixel, Hotjar, Clarity, Yandex, HubSpot), read at
write and by the stored-response backfill. tracking is a correlation
kind, a host grammar field and the second cross link after the
certificate, judged by the page rule so a parked or default page never
vouches for its provider's account.
generic_page kept only ASCII, so a Devanagari or Chinese title read as
empty and every hub of a non-Latin estate was the server's. It keeps
Unicode word characters now.
After the match, every inferred CVE row gets fixed_in: the nearest newer
release of the same product this scan saw that does not carry the CVE,
and how many assets run it. Each release's CVE set is read from the NVD
corpus, uncapped, so a scan at the match cap is still judged whole. A
version whose leading number sits far above the product's median is a
misread and never the fix. is:fixable filters on it, and the row and the
sheet name the release.
http_probe becomes always-on support: saved engines, YAML and run overrides
drop its switch, passive still turns it off. Its every-port setting moves to
port_scan.http_on_every_port, the Passive Recon preset carries passive
intensity, and the launch solver includes the probe without letting it pull
in active support stages.
The summary bar draws one lane per event type over the last 30 days,
14 on a phone: a dot per day sized by its count, findings coloured by
the worst severity. Hover shows the day, a click or a drag picks the
days, the current period is tinted and Caught up is a dashed line.
Every event starts collapsed and expands in place. A rescan lists its
new critical and high findings, each opening its sheet. A bounty event
lists its rows with their actions. Hover is a background, never an
underline.
The period now picks whole runs, so a run's count is always what its
link opens. Days follow the viewer's time zone across the day filter,
the lanes and the headings. The badge counts events, a triaged finding
also hides its cluster replays, and a scan still running is dated by
its newest finding.
Claude-Session: https://claude.ai/code/session_015EWyHNJRpxkvhtapY3wNGx
What's new lists only what changed: rescans that found new critical or
high findings, new programs, assets added to or leaving scope, bounty
table and rule changes, certificate hosts from watches, and targets a
scope or watch added. Events group by day on a rail, with a divider at
the Caught up mark. Each rescan names its worst new checks, and every
count opens the scan filtered to exactly those findings.
Program syncs now record payout, safe harbor and 2FA changes, and per
asset tier, bounty eligibility, severity cap and instruction changes.
The public feed sync diffs known programs for these too.
The search cache no longer stores a page that carries an error, so a
timed-out search no longer answers 0 rows for ten minutes.
Only a capability stage's success covers a dimension; a support stage
covers through the rows it wrote. A cancelled run that finished takeover
and zone_transfer hid the previous run's findings on every page.
The targets list reads findings from each target's vulnerability
covering scan, and both strips gain a medium chip.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- sync the account's reports, bounties and standing from the HackerOne
hacker API into bounty_reports, bounty_awards and bounty_accounts;
money comes from /payments/earnings, totals stay per currency
- /bounty-hub/<platform> as a Bounty Hub child in the sidebar, in the
/scans idiom: stat strip, severity toggles, monthly chart with drag
range, stage tabs with filtered counts, URL filters, expandable rows,
keyboard navigation and a by-program view
- program names open the Bounty Hub program sheet, which links back to
the reports on that program and fetches unfetched scope on open
- hourly report sync gated on bug bounty mode and the sync interval
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The findings table follows the scan history idiom: a summary strip that
collapses to one line, review-state tabs, filter pills, and rows that
expand into a brief with the web asset, the other findings on it, the
same check elsewhere, the evidence and the intel.
Each finding now carries the open findings on its host by severity, and
its asset carries open ports, names on the address, endpoints and
inferred CVEs, one grouped query per page. Columns hide by the table's
container width, so the table fits at any size without scrolling
sideways.
The exploitation ring in the row becomes a rank number, the left row
bands are gone, and the strip drops the Info chip.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AQDJwqjTD69CUKa95DjM8H
Each row carries open critical, high and medium counts, computed with the
Vulnerabilities page's suppression rules, and whether the vulnerability
stage ran. Medium is behind a toggle in the run brief; low and info are
not shown.
- Row brief with Findings (inline triage), Changes, Coverage and Engine tabs
- Latest per target view with earlier runs nested beneath
- Query grammar: target, engine, status, severity, is:partial, is:added
- 30-day runs chart with a date brush, strip counts equal to the rows opened
- Findings trend per target, inline compare sheet, keyboard navigation
- API: findings and target_runs on ScanRead, severity/short/added/date/latest
list filters, GET /scans/trends and /scans/daily
- Removed the unused /scans/targets grouping and the old list components
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuoZt3GW4VjRSF7FJeFtuV
Out-of-band testing is an instance setting — `off`, `self_hosted` or
`public` — with the server, the token (the INTERACTSH API key row) and the
wait resolved once in `shared/services/oast.py` and read on the stage
thread. A mode that cannot be used is off and says why: self-hosted with no
server, or public without the disclosure acknowledged. A fresh instance is
off. `POST /oast/test` asks a public address for `/poll` over HTTPS and
reports that the server answered, never that OAST works.
The engine keeps one switch. `FieldNeeds` puts the resource key on the
field and `GET /engines/catalog` fills `unavailable_reason`, so the builder
renders the instance reason beside a switch that stays settable.
Measured on nuclei v3.11.1: with `-no-interactsh` a check that needs a
callback still loads and the request is refused — 0 requests, exit 0. So
the batch reported COMPLETED and every origin in it read `scanned` for
checks that never touched the target. The selection now drops them when
nothing is listening, one coverage row reports the count and the reason,
and `Tier.OAST` is left out of the planned tiers.
Membership is what a check references, not what it is tagged: 615 of the
12,586 official rows reference an interactsh placeholder and 233 of those
carry no `oast` tag. `vuln_templates.needs_oast` is parsed at index time; a
library resync fills the column.
The wait is paid on out-of-band batches alone, which sweep for 30 minutes
so it is paid fewer times, and the tier names the server on its own rows.
A finding's callback is rendered on the sheet from one label map; the list
path is bared to the metadata.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A target can follow the check library: once a day, when the library gains
checks, one focused run tests every web asset the target's completed scans
found, with those checks only. Per-target watermark, one sweep lock, a
`daily.run` chain that registers future once-a-day jobs, two notifications,
a What's new group, a "New" badge on the library, a monitored-target signal
and a "New checks runs" scan filter.
The pipeline review behind it, in three passes:
Planner and scanner. The deep sweep is its own tier, outside ROOT_TIERS and
after replay, so a cut costs the tail and cluster members are still
confirmed; the one-request tier runs first, sized on its clustered cost;
blind core ranks CISA then VulnCheck then severity then cost; the planned
tiers come from the selection; skipped hosts are marked alone; library gaps
are one row per run; the sweep cap is 8 requests.
Output. Payload-free fingerprints for fuzzing, out-of-band and dalfox
findings; suppression follows the replay edge; the digest baseline is every
earlier scan; host and port read through split_host_port; interaction obeys
store_evidence and redaction; the rollup excludes replays.
Tools. nuclei's exit code, abort and budget are read; a tool arg cannot
restate a stage flag under its short spelling; one refused row costs one
row; a headless block decides the protocol.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B58eEM68a5qDFTQXPE5aoW
Three corpora, one miner: web asset roots, the responses endpoint_probe
read, and a scanner's proof. `endpoint_responses` is its own table so the
nine select(Endpoint) sites do not carry every body; retention deletes
those rows with the web asset bodies, and protects each target's newest
census run.
A passive leaf past the last awaited level joins the deferred tail, so
the finding corpus lands before finalize re-mines it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B58eEM68a5qDFTQXPE5aoW
GET /whats-new/visual pairs every host two consecutive settled census
runs both captured and keeps the ones whose screenshot dHash moved past
the cluster distance, so the pair is no longer the same page. Each pair
carries the hamming distance and the compare fields that moved; a pair
where none moved is a silent redeploy, its own badge and filter. The tab
shares the page's period, target and text filters, shows before and
after with hover previews, and opens the web asset sheet, Compare runs
or a scan from the card. The feed carries the pair count for the tab.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018XqZAsFckWFdr7EFiyZH9V
GET /whats-new reports what appeared in a project since the operator
last caught up, grouped by the run or program it came from. Ten kinds,
each noise-gated: web assets, services, findings and secrets from repeat
scans (a first scan sets a baseline and reports nothing, certificate
names stay with the watch), in-scope assets, programs, certificate hosts
and targets from Bounty Hub, and a Gone pair of out-of-scope assets and
retired web assets. A run group's "All N" opens that scan's tab with
is:new, so the count equals the rows. POST /whats-new/seen writes the
mark from the Caught up button alone; GET /whats-new/unseen feeds the
sidebar badge. Scope events count for programs the project watches or
whose scope covers a target; program events count library-wide, with a
ring switch for the whole library. The 13-week grid is per kind and
doubles as the period filter, by day or range.
The page carries kind tiles with facts and sparklines, a source switch
(All, My targets, Bounty Hub) that collapses in corporate mode, target
and program pickers, a text filter, rows that open the results tables'
detail sheets in place, inline Add target, Watch, Scan, Mute and Remove
target, card collapse with card-level Add N targets, shift-click range
selection, keyboard navigation, copy on hover, a sticky filter bar and
a reload when a run finishes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018XqZAsFckWFdr7EFiyZH9V
The feed reports new surface only: web assets a later scan was first to
find, scope a program opened, programs added or made public, hosts a
certificate revealed under a watch, and targets Bounty Hub brought in by
watch or scope. A first scan yields nothing, hand-added targets are the
activity log's business, and scope removals stay in Bounty Hub. Program
events count for programs the project watches or whose scope covers one
of its targets. The response carries per-day buckets per kind, drawn as
a week-column grid above the list; a lit day scrolls the list to its
header. Days are keyed in UTC on both sides. Windows are 7, 30 and 90
days, default 30.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfLmNNiLk5VdePfq9CpugW
GET /changes reports what arrived in a project after a point in time: web
assets a scan was first to report (baseline-gated, certificate hosts left
to the watch ledger), in-scope hosts from program watches, scope and
program events for watched programs, and new targets with how they were
added. POST /changes/seen writes a per-user watermark keyed by project;
the page marks on open and keeps the returned cutoff for the visit, with
a 7-day window standing in on a first visit. Timeline mode reads a
24h to 90d window filtered by target or program. Linked from the
sidebar, the palette, the Bounty Hub header and the target page.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfLmNNiLk5VdePfq9CpugW
The attack surface funnel is gone. The target page draws Reachability
instead: one composition bar over the four states a name can be in —
unresolved, resolved without an HTTP answer, answering, answering with
a finding — and the dashboard's Inventory row carries the count that
answered.
That count is is:web, not is:live. is:live compiles to 2xx and 3xx, so
a 401, 403 or 5xx responder answered and was still reported as having
dropped out; the funnel's "Answer HTTP" step had been reading the
wrong one, and the Inventory sub-line inherited it. Measured on this
instance: 3,418 answer where 2,814 are live, so 604 web assets — the
denied and error surface — were counted as not answering.
DashboardOverview.answering_hosts carries it and the predicate has one
definition, predicates.answered(), which the is:web flag compiles to.
subdomains insights re-derived live, web and resolved inline beside
the predicates module it already imports. It reads them from there.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DafWxfoWZXVDgr9KroJBDZ
A third transport over the MCP tool layer, beside HTTP and stdio. The
`channels` compose service long-polls the Bot API, so an instance needs
no public URL, certificate or open port; the listener runs on the api
image because MCP tools lazy-import app.services.
Commands are curated at the tool: a tool joins chat by declaring one
short `command` (/scan, /add, /vulns, /progress); no command means
MCP-only. Twenty-three commands in five groups, pinned by a test, with
replies rendered through ToolResult.blocks instead of the data dump.
An unknown chat gets a device code approved under Toolkit, Remote
control; anything above read is confirmed with the account's own
authenticator and capped at read when none is enrolled. The bot token is
the Telegram API key, shared with notifications. Replies are text plus
entities with UTF-16 offsets, never parse_mode, so target-written text
is literal by construction.
Also fixes Dimension.search passing scan_id= to a service that takes
scope, and gives query_assets an optional target for project-wide
search.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VckaDx6mBB4TsG76u61gvv
Per target, live web assets against open findings stacked by severity,
with actionable, act-now and KEV counts. The cell shows the six targets
with the most findings; a modal shows every target with organization and
tag filters, severity toggles and four sort orders.
GET /dashboard/surface-risk reads the same covering scans and suppression
as the overview, so its totals equal the board and the inventory.
The hero headline lost the space before "on N targets".
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011h3F8L6bnjycZcXtWk4u6p
Each asset's change list is folded into one count per field and
direction, so the row draws the dimension icon with an up or down mark
instead of stating that something changed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ADin2DiZS8SXh8oGzLBbC
A focused run took its baseline from the target's whole history, so a
four-asset recheck rendered as a collapse against the last census. The
baselines are census-only now and a rescan row carries its own tally,
assets rechecked and how many moved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ADin2DiZS8SXh8oGzLBbC
The parent row shows a rescan count and a running or failed chip, and
reveals the runs on click, ten at a time. A running rescan now counts as
live, so it reaches the sidebar and the activity panel. The Focused
rescans checkbox is gone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dashboard's Scan activity cell plots runs per day over the same three
windows and links here. ScanStats loses daily, so /scans/stats no longer runs
the status-by-day and new-hosts-per-day passes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The eight-tile strip restated counts the Inventory cell already carries, and
its xl grid was seven columns from when there were seven tiles, so Secrets
left one tile alone on a second row. The page opens on the estate band.
DashboardTargetRow was built for the strip's per-target sheet. Only id, value
and monitored are read now, so the row carries those three: the seven
DashboardTargetSurface objects per target, each with its covering scan,
previous count and delta, are gone, and _risk_by_target with them, a grouped
aggregate over vulnerabilities nothing reads.
GET /dashboard/overview on a 76-target project: 205.2 KB to 105.1 KB.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
POST /scans/cancel-all runs each open scan through the existing cancel
path, optionally scoped to one target. The toolbar control is always
rendered and carries its reason when there is nothing to stop.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
http_assets.software was never declared on the model, so the components the
probe parsed were dropped at insert and the five-minute backfill became the
only writer. Its pending test was an empty list beside a tech, and a
versionless technology yields an empty list, so those rows were queued for
ever. Each tick rebuilt the scan with a delete and a reinsert, and a tick
outran its own interval, so eight ran at once, deadlocked on the same scan
and held every worker slot.
The column is three-state: NULL is not evaluated, [] is evaluated with
nothing versioned. The probe writes it at insert. The backfill reads NULL
alone under an instance lock and matches only when it wrote a component.
match_scan builds the scan's rows in a temp table, applies confidence and
rank there, and merges them by fingerprint under a per-scan advisory lock, so
a row whose columns did not move is not written and finalize, the nightly
rematch and the backfill queue rather than deadlock. Backfill beat entries
expire after one interval. software_cves carries the result tables'
autovacuum thresholds. The dashboard store loads its cells beside the
overview rather than after it.
A full rematch of 441,044 rows now writes none of them. The dashboard
overview went from 13.7 s to 0.8 s and its software count from 13.6 s to
22 ms with no heap fetches.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CjhsosXbGgSkBU5z5yPYiz
The import dialog carries the same organization and tag pickers the Add
target dialog does, applied to every target in the batch, so a paste of 200
names lands tagged instead of needing a hand-written JSON body.
TargetImportRequest takes organization_names and tag_names, the CSV route
takes them as form fields, and a row's own tags and organizations are added
on top, deduped by id.
The bulk import activity line no longer says "from CSV" for a JSON import.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014W1hHRMVyDRdfax3qEViz7
The scan overview reads GET /targets/{id}/estate?scan_id= and renders the
same estate-tray the targets page does, so a scan, its target and the
project state one number for one target: 282 untracked on go.id where the
old tiles capped at 25.
Deletes the second cert-SAN walk end to end: GET /subdomains/related-domains,
related_domains.py, shared/models/related.py, RelatedReason.CERT_SAN, the
frontend types and client, and the Related domain tiles in Needs attention.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- a mail host row skips the zone checks (DNSSEC, CAA) its parent answers
for, and the panel shows no zone facts on it
- inheritance reads only v=DMARC1 records at _dmarc; a stray TXT no longer
blocks it, and a parent that never answered leaves DMARC unjudged
- the MX sweep asks mail-named and short names first, capped at
MX_SWEEP_CAP, retries names that gave no answer once, and reports names
not asked or still silent as partial
- zones and mail hosts are counted apart in the cell, the panel, the report
and the MCP summary
- the posture summary loads on the table's first activation and on a scan
switch; a pending-only summary still shows the control
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KAP7PmMKSfNAFse3s2y2Xj
- a name that publishes exchanges different from its zone's gets its own
posture row under parent; with no _dmarc of its own it carries the parent's
record marked inherited and is judged on sp
- the fold gives each host its longest matching zone (zone_of), so a mail
host's row wins for itself and its children
- posture rail on the Web Assets tab beside the hygiene rail, reading the
host-grain /subdomains/posture summary; the cell meters read the same
summary so every number equals the rows its filter opens
- MCP tool domain_posture over a target's newest settled run
- review fixes: exact fold without LIKE, MX preference stripped, policy fetch
outcome separated from policy content, redirect= qualifier resolved,
dual-CIDR lookups counted, SPF includes resolved one batch per depth,
DNSSEC and MTA-STS probes on a thread pool, a capped lookup count no longer
a pass, GIN index on subdomains.posture_issues, spoofable read over every
row, census runs only, excluded names never derive a zone, rows committed
before the fold and finalize re-folds
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KAP7PmMKSfNAFse3s2y2Xj
SPF, DMARC, DKIM, MTA-STS, TLS-RPT, DNSSEC and CAA checked per registrable
domain by a new dns_posture stage, stored in domain_posture and folded onto
every subdomains row under the zone, so posture:<check> is a host filter whose
count equals the rows it opens.
- shared/definitions/domain_posture.py mirrored by config/domain-posture.ts
- dnsx for TXT/MX/CAA; a stdlib DO-bit query for DNSSEC (dnsx has none);
the MTA-STS policy fetched over HTTPS, so the stage is touches_target with
passive_capable
- DMARC inheritance through sp, SPF lookups counted through includes in one
batch per depth, null MX as a positive fact, DKIM stated as probed
- an unanswered zone has no verdict and marks the run partial; a capped
lookup count is not a pass
- grammar field, facet, group dimension, overview panel, target and dashboard
cell, web asset sheet section, report section, compare signal
posture_weakened, digest line, dashboard spoofable signal over the rows,
MCP tool domain_posture
- finalize re-folds so rows written after the stage carry the zone's checks
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KAP7PmMKSfNAFse3s2y2Xj
Every result and management table gains multi-row selection and the floating
action bar targets and scans already had: copy, export selection, delete, and
the dimension's own actions. Rescan and triage move off the inline strip, which
now only offers the rescan that acts on the whole query.
Delete is dimension-local. A row takes only what cannot stand without it: a web
asset its stored responses, a secret its sightings. Endpoints, services and
findings on a deleted host are kept, so every count stays equal to its table.
POST /surface/delete is the one route and the scope is a WHERE, so an id from
another scan deletes nothing. Vulnerabilities are addressable by template_id so
the Issues view deletes a whole check. Exposures dismiss rather than delete: an
exposure row is a web asset.
Selection export runs through the export pipeline. Every surface filter gains
ids, capped by MAX_SELECTED_ROWS.
Secrets and software become exportable, which fixes two breaks: a bundle export
raised KeyError on the secrets dimension, and mcp read a SURFACE_COLUMNS entry
for secrets that did not exist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XyDpdVXL7YVsr5D7BVj2zt
Seventh surface dimension. Passive response mining for credentials,
tokens, keys and contacts already stored in http_assets bodies and
headers — no request is ever sent, no key is tested for validity.
- shared/definitions/secrets.py: 44 detectors across cloud, code,
payments, messaging, AI, auth, data, platform and contact groups
- shared/services/secret_mining/: detector sweep, the only writer of
secrets/secret_sightings/secret_coverage, fingerprint + context
- stages/secret_mining/: one stage, one knob, on by default, off
under passive intensity via touches_target=False
- shared/services/asset_query/secret_compiler.py + the SECRET_QUERY
grammar: is:exposed / is:new / group: / secret: / state: search
- api/app/services/secret.py + api/app/api/v1/secrets.py: search,
facets, coverage, groups, detail
- worker/app/tasks/secrets.py: backfill beat for scans mined before
the stage existed
- Frontend: secrets table and detail sheet built to match the scans
table (plain values, soft state badges, no explanatory copy),
own query bar, coverage strip, quick filters
- Wired into every SURFACE_ORDER fan-out (surface scope, dashboard,
reports, rescan, scan compare, notes, MCP)
- Wired into scan compare (SPECS/COMPARE_KEYS/WATCHED_FIELDS) and
notes (ASSET_IDENTITY)
Values are stored and shown plain, no encryption, no masking, no
reveal endpoint, no audit trail — a stored value is already exposed
the moment the response was read.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpxhGvFNFCcEEf4gZykvka
The target overview now uses the same hairline cells as the dashboard: a
counts strip with per-run sparklines, the attack surface funnel, geography,
findings, posture, and the composition cells (hosting, services, technology,
hygiene, software CVEs, certificates, exposures) scoped to the covering scan
through ROUTES.results. Identity, monitoring and seeds are cells; the run
timeline is a cell with a surface-by-run chart once two runs completed.
components/cell.svelte is the shared primitive; the dashboard cell wraps it.
A target's estate is one engine (api/app/services/target_estate.py):
redirects and CNAMEs off the apex, certificates a host serves for another
organisation, names on the target's own certificates, cross-target relations
and shared addresses. A certificate naming more than five registrable domains
with a subject off the apex is a platform's and its names are neighbours, not
relatives. Names under the target's own zone are its own, evidence seen only
on an IP host is shared, and CNAME, NS, MX or SPF targets in PROVIDER_SUFFIXES
are providers. The target page, the targets page and the dashboard's untracked
domains read the same engine through GET /targets/{id}/estate and
GET /targets/estate, shown as a one-line strip with Review and Add.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0181ibJxKGBFiTbbHoXuhosP
Four bands in reading order: counts, the attack surface funnel beside
geography and the added/retired changes chart beside inventory, an
Act/Attend/Track findings board with severity per day, exploitation and
an evidence matrix, a mode band (platform events, watched programs and
browsing in bug bounty mode; certificates, hygiene and ownership in
corporate mode), scan activity and software CVEs, composition cells, and
an activity rail. The 7d/14d/30d window redraws every time chart; every
cell can be hidden and restored per mode from a catalog.
The overview gains the funnel, retired rows per day, findings per day by
severity, run outcomes per day, the severity by evidence matrix, queue
tiers and certificate expiry buckets. GET /dashboard/activity unions runs,
feeds, exploitation changes, connector syncs and, in bug bounty mode,
watch and platform events. GET /dashboard/programs carries the bug bounty
band.
Copy states a measure or nothing. Counts tiles open a per-target sheet
with a scan action, funnel steps hint and dim on hover, board cards open
a hover card.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjHnxAPLMDSfQgVQDfDYBj
URL discovery and subdomain discovery send traffic through some of
their providers and were marked as sending none, so the passive rule,
the Passive preset, the launch picker and the footprint all read them
wrong. touches_target is true and passive_capable keeps them in a
passive run, where their own gating already skipped the active parts.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq4xYJbgW5euUa9sCWBKFw
Every stage config field is a decision; rate, concurrency, timeout and
retries come from a per-tool intensity profile the stage reads as its
transport. 173 fields become 83: 41 transport values, 40 constants and
the dead ones leave the models, and the builder groups by capability
with support stages as checkboxes and advanced fields behind one
disclosure. Aggressive now changes what a tool is handed.
Every project carries a built-in Default engine, seeded on project
creation and on first list, undeletable: stage defaults, subfinder as
the only passive source, vulnerability scan on with nuclei.
From the tracing review: tool args can no longer restate a flag the
stage set, the session check obeys the probe scheme, excluded scope
reaches every stage that reads addresses, hosts or paths, the sitemap
and source-map providers throttle at the transport rate, the blind tier
obeys its switch, the fuzzing lanes split the budget, and the removed
private-address and evidence switches are back as advanced fields.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq4xYJbgW5euUa9sCWBKFw
The vulnerability stage handed every http_assets row to nuclei, ordered by URL,
with one 90-minute clock cutting wherever it landed. Template-spray then gave
every host the first slice of the corpus in load order. On the two largest local
scans that was 23.4M and 10.7M planned requests, of which 3.9% and 9% were sent,
and no host recorded what it got. One GeoServer CVE was found 11 times on 11
hostnames of one server.
Introduce one planner, shared/services/scan_surface/, that decides what every
vulnerability scanner receives. It writes scan_surface_items per scan and is a
pure function of the tables the scan already produced plus the context; nothing
in it names a nuclei flag. Vocabulary lives in shared/definitions/scan_surface.py,
mirrored by config/scan-surface.ts.
- Inputs are typed by protocol, because nuclei rewrites input per protocol: roots
for HTTP templates (any status), bare names for DNS, host:port for TLS and
network, parameterised requests for fuzzing. Path and query are stripped from
non-DAST input; values are normalised to one spelling.
- Web assets that are one backend fold into one origin on exact agreement of
address, certificate, body hash, status, title, canary and favicon — never a
similarity score. Measured: 1,812 web assets to 1,607 origins on gov.np.
- The corpus is spent in tiers: one-request checks on every asset, the universal
set on each origin, product-matched checks through a curated tech map, the
blind sweep last. Each tier is a ranked host-spray batch with its own coverage
row; the budget cut lands between batches and every origin is scanned, covered
or not scanned.
- A finding on a representative is replayed onto each cluster member with one
request, writing a real observed row or splitting the member out. Never an
inferred row.
- dast_scan is a new stage (off by default, after endpoint_probe): nuclei -dast
over request items and exposure checks under directories, then dalfox behind a
reflection probe. dalfox v3.2.2 is pinned in the worker image.
Verified live on testasp.vulnweb.com: the tiers ran in order, nuclei -dast found
two LFIs, dalfox executed under the reflection gate. All suites green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R7sBu1jBa4e2CXLS1KEsNr
The three-column sankey's first column was always a single node, so two thirds
of it was a stacked bar drawn as ribbons. On badssl.com — 15,477 web assets, one
ASN — it degenerated to a 250px slab of one colour carrying one sentence, and
486 lines of layout, a linearGradient per link, an IntersectionObserver draw-in
and a recursive hover walk paid for three numbers.
GET /subdomains/hosting returns a HostingComposition: the fronting split, then
the top MAX_NETWORKS AS orgs plus "Other networks" as ranked rows, each carrying
its own fronting mix. hosting-body.svelte draws a stacked hairline with an
inline legend and a ranked list whose meter is segmented by that mix, so "19
hosts in Salesforce AS14340, all direct to origin" is one glance. One body, used
by hosting-panel.svelte on the scan Overview and hosting-section.svelte on the
target Overview; the host passes its own padding. Rows reuse ranked-list.svelte,
which gains segments, hint and a rendered meta, and the strip reuses
composition-bar.svelte, which gains inline.
Two count-is-a-promise breaks went with it. The service grouped on
coalesce(Subdomain.asn, HttpAsset.asn) where the grammar's asn reads
Subdomain.asn alone, and it read cloud off a DISTINCT ON (host) latest row where
is:cloud is an EXISTS over every asset of the host, now bool_or. All 23 numbers
on affirm.com equal their search/counts, checked across five scans.
Unattributed hosts are a row, not a column: they appear as "Network not
attributed" when some networks resolve, and when none do the list is dropped and
the panel head says it instead of the network count.
The dashboard widget showed two segments both reading "10,000 100%" — COUNT_CAP,
with the denominator capped too. It now reads 10,000+ and drops the percentages
when any count is capped. Its fronting colours come from config/hosting.ts,
which the panel shares.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6DocJiHaZQdMhgQcWddsb
A full static review of the 3.0 tree, run as two fan-out passes with every
finding verified against the running stack rather than taken on inspection.
Correctness and security
- instance settings encrypted the mask over the stored AI key: `!= MASK` never
matched `••••••••abcd`, so saving settings destroyed the key and `test_ai`
posted the mask as a bearer token
- the MFA token was replayable for its full five-minute life; the gate now
checks revocation and consumes the token after a successful exchange
- logout revoked nothing for a bearer client while answering "Logged out"
- `POST /api-keys/{id}/test` returned the plaintext key inside httpx's error
string, which embeds the whole URL
- an interest rule could be edited or deleted through another project's id
- redis ran the celery broker under `allkeys-lru`, free to evict a queued task
or a chord counter; now `noeviction`
- nuclei's stored command carried the OAST token and every auth header
- a finalize step could poison the session and leave a scan RUNNING for ever
- `is:issue` raised TypeError on every search
- offset paging repeated and skipped rows in four dimensions
- a cancelled run kept a stale `scans.*_found` rollup
- every PDF report raised `KeyError: 'software'`
- `version_key` truncated a wide numeric run and inverted release ordering
- psycopg raises a NUL as a bare ValueError, above StatementError, so every
savepoint guard was blind to the case it was written for
Performance
- endpoints.host had no trigram index: 35.9ms seq scan -> 0.389ms
- free text compiled to one OR, not the prescribed UNION ALL: 132ms -> 41ms
- `/http-assets/summary` and `/ips/summary` materialised whole tables
- the cross-link `ip` kind ran an estate query per page and drew nothing
Frontend
- `$effect` loaders that read the state they write, an unbounded request loop
on a failing endpoint
- error states that rendered as empty states
- 2FA backup codes were unusable while the copy said they worked
- a "Scan history" dialog that always reported none
- ~400 duplicated lines across five filter bars, now one component
Build and tooling
- every scanner installed at @latest; all 13 pinned to the measured versions
- CI built `web/`, gone since 2.x, with tags that cannot be valid
- the prettier pre-commit hook discarded its filenames and always passed
- the test suite destroyed itself when run twice
Also: subdomains from public code, via a pinned github-subdomains source and a
GitHub API key that subfinder receives too; sublist3r and oneforall removed
after measuring one unique host in 89 across four targets.
Verified: 853 tests, ruff clean over eleven packages, svelte-check 0 errors,
the count-is-a-promise gate at 131 checks and 0 mismatches, and 143 GET routes
with no 5xx. Four migrations, each with an exercised downgrade.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVkdx5ynZ6aof5kLFczmhX