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.
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
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
The export engine, with no surface on it yet. Given a dimension, a scope
and the filter body a result table POSTs to search, it writes the same
rows to csv, json or plain text.
It selects only the columns the file will carry, so a vulnerability
export does not drag a stored request and response through memory to
throw them away, and it streams with yield_per, so a 50,000 row export
holds one chunk at a time. Statement timeout is the export's own: a
background job does not read under the api's twenty second ceiling.
Two columns a table shows are joined per chunk rather than per row —
the ports behind a host's addresses, and the hosts behind an address —
because #523 and #703 ask for those by name. The four rollup counts the
api computes per page are left out rather than written blank.
A cell opening with = + - @ tab or return is a formula to Excel and
Sheets, and page titles and banners are written by the target, so every
cell is prefixed and stripped of control characters. CSV carries a BOM
or Excel reads utf-8 as latin-1.
Proven against a live scan and in tests: for every dimension, filtered
or not, the export writes exactly as many rows as the table reports.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYZfaNjq3Ym8GPvWYuHisS
The five search services build their statement with pure, session-free
helpers — _context, _apply_filter, _order, _scoped — and then enrich the
page asynchronously. Only the enrichment needs the api. Moving the
builders to shared/services/surface_query lets the worker compile the
same filtered set the table shows, which an export has to do and could
not reach from worker/app.
Web assets gains the _scoped it never had: its base statement was
inlined in search, leads, seeds and renders, so a predicate could drift
between the rows a query returns and the rows its leads count. The four
now read one definition.
No behaviour change. Every route over the moved builders — search,
leads, groups, renders and facets across all five dimensions — was
exercised against a live scan.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LYZfaNjq3Ym8GPvWYuHisS