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
Second review round. The fingerprinted matched tier now runs before the
known-exploited sweep, so a budget cut lands on the broad sweep and the
lower-yield tiers, never on the high-yield product checks. The blind
budget is sized from both lane rates and floored at zero rather than a
per-check cap.
The "not reached within the time budget" sentence is one shared constant;
the DAST stage now reports a budget cut as partial instead of success.
Rate and concurrency overrides are accepted only for tools that have a
rate, matching the builder. The rates panel no longer marks the engine
unsaved when a value returns to the preset. Narrating comments removed.
Claude-Session: https://claude.ai/code/session_01Bb4u8p34euie1YFzb7qCPu
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.
- no bot connected: one panel, BotFather link and a token field; Connect
checks the token with Telegram, stores the API key and starts the listener
- Settings sheet holds the bot, Change bot, Disconnect, the ceiling and the
rate limit; Disconnect removes the paired chats, pending requests and key
- a token for a different bot drops every pairing; deleting or replacing the
Telegram key under API keys follows the same rules
- page: bot header, pairing requests with the code and an up-to capability
picker, then Chats, Activity and Commands tabs
- activity records the command as typed, control characters stripped
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
/mcp is now /agents: a table of agents with access, scope, last call and
expiry, and a row menu for History, Edit access and Cut access. History,
the tool catalog, server settings and the connect flow open in sheets.
Connecting names the client and hands over its own setup snippet with the
key filled in (mcp/clients.py), then waits for the key's first call.
PATCH /mcp/tokens/{id} edits a key's project and access, and a key
reports the projects and targets it reaches.
Every tool call is recorded, refusals and invalid arguments included,
with the key id, a readable line of arguments, the result's summary and
its pivot, so History opens the rows the agent was given.
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.
A passive stage after origin_probe judges every owned name against the
redirect, the certificate, page links and the bare-address response.
rengine-foreign-site (high) when the name serves another organisation's
page, rengine-unhosted-name (medium) when the server answers with its
default and someone else's certificate. The estate drops every domain a
claim names, so a tenant no longer reads as a related domain.
provider_of moves to shared/definitions/estate.py for the worker; sign-in
and code hosts join VENDOR_DOMAINS.
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
Stage tasks hold a slot for their whole tool run, so step hand-overs and
finalize queued behind them and background jobs took scan slots.
- worker-scans consumes scans (run_scan_stage only), scalable with --scale
- worker-control consumes scan_control: launch, hand-over, resume, finalize
- worker-default consumes default and critical
- reaper reads both scan queues; readiness requires a consumer on every queue
- connection budget counts every worker; max_connections 250
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.
Templates tab in the /scans idiom: All/Default/Custom count tabs, search,
audience and scope filters, rows with a cover thumbnail, section badges with
a +N hover list, format badges and a Generate action that opens the dialog
on that template. Default templates offer View instead of Edit. "Shipped" is
"Default" across report, wordlist, rule and typeface copy.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
/targets follows the scan history idiom: a strip of counts that filter
(targets, not scanned, stale, critical and high findings, enrichment
signals), type tabs and tools in one card, and rows that carry the latest
run's status, findings chips with previews, assets, change and a
findings trend, expanding into the run brief. Columns hide by the
table's container width and the list has the /scans keyboard.
GET /scans/latest returns the newest census run per target with the
history fields. Targets gain unscanned, stale, critical and high
signals whose counts equal the rows they filter; critical and high read
the latest run and skip suppressed findings.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AQDJwqjTD69CUKa95DjM8H
The sheet opens on the decision: severity, KEV and the facts line in the
header, review state and rescan in a fixed action bar, then evidence,
exploitation rank, EPSS and reach as four cells that each filter the
table. Content moves into tabs: Overview (description, remediation and
the web asset with its neighbour counts and other findings), Evidence,
Same check, Intel and Notes. The single-finding endpoint now returns the
same asset correlation as the search.
Signal and intel copy states facts only, and the overdue signal names
the deadline date instead of a day count that went stale.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AQDJwqjTD69CUKa95DjM8H
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
A scope bar under the header narrows every cell to chosen targets, an
organization or a tag, kept in the URL. The dimension endpoints, the
dashboard reads, threat intel, interest and domain posture accept
target_id, organization_id and tag_id through one resolver. A target row
in Surface against risk scopes the board on click, cell links carry
target=[...] so each count opens the same rows, and cells that cannot
narrow are marked All targets.
Dashboard copy reworded to state facts: no explanations, reassurance or
colourful labels, and failures read "<Noun> not loaded."
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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
A nested chord canvas put the whole remaining pipeline into every
stage message, duplicated per sibling per level: 33 MB per message,
39 of them filled a 1 GB Redis, and the worker was OOM-killed
deserializing them. Every later launch then failed with "command not
allowed when used memory > 'maxmemory'".
shared/services/orchestrator/canvas.py now builds one step at a time;
a step's chord body is app.tasks.scan.run_scan_step(steps, index+1),
which dispatches the next step once the current one settles. Messages
are ~1 KB now; tests/pipeline/test_canvas_shape.py caps them.
Also, from an end-to-end review of the pipeline:
- The reaper re-queues a PENDING scan with no launch anywhere in the
queue, so a lost or failed send_task no longer strands it forever.
api create() now dispatches off the event loop and records a failed
dispatch the way create_batch already did.
- Cancel no longer revokes queued stage tasks. A cancelled run used to
walk every remaining step recording one aborted row per stage, and
whether finalize ran depended on a race with the revoke broadcast.
The step body now finalizes a cancelled run directly, and the api
also dispatches finalize so a run with no live canvas still settles.
- The reaper only skips scans that actually have a message queued,
read off the scans queue, instead of skipping every scan whenever
the queue holds anything.
- Every beat tick carries expires, so a worker that comes back after
being down does not replay hours of stacked ticks.
- worker-default and worker-beat restart on their own.
- _run_process kills the tool in a finally; a soft time limit raised
inside proc.wait used to leave the tool running past the stage.
- on_worker_ready crashed on a missing Consumer attribute, so the
worker's IP-range and threat-feed warm-up never ran.
- Worker logging now goes through the root logger; it was configured
on a "rengine.worker" logger nothing writes to, so INFO logs (task
starts, step hand-overs) never appeared in the container output.
- _normalize_url no longer raises on an unparseable value, which had
been killing the whole bounty feed sync on one bad scope entry.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0181JfVNatLVWnDbtGWRWPov
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
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
The list header takes the viewport's scroll width under a ResizeObserver,
so its columns lay out against the same width as the rows and mirror
their scroll exactly. The Changes page keeps its visit baseline in
session storage for twelve hours, resets it on a project switch, and
sends a window rather than a cutoff when no earlier visit exists.
Program and scope rows map the event tone onto the feed's tones, the
event query filters by kind in SQL, the act-now link compiles the
dashboard's own predicate, the x shortcut tolerates a stale cursor, and
the triage keys ignore a focused select or menu.
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
Scan page follows the URL on every navigation, carries the target in its
breadcrumb and back link, and keeps stage progress, Re-scan and Compare in
the condensed strip. A New toggle in the tab bar and an IPs chip apply
is:new. Results tables keep a sticky header outside the horizontal
ScrollArea. Web asset status and ASN pivot, CVE badges open the CVE page.
Findings deep-link with vuln=<id>, triage from the keyboard, show their
proof first with the response open, and tint critical and high rows. The
expanded issue reloads its instances on a query change and the
project-wide vulnerabilities menu navigates. Dashboard headline, surface
risk stats and failed runs link to the searches that return their counts;
the scans list reads status and range and gains a 14-day range.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfLmNNiLk5VdePfq9CpugW
record_finding masks credential headers in the request and the response
it stores, and wrote the tester's notes through untouched. A repro
pasted into notes — the usual place a request goes — stored a live
Cookie or Authorization in the database and carried it into every
report and export. The notes take the same redaction, which is anchored
to a header line, so prose about a missing Authorization header is left
alone.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DafWxfoWZXVDgr9KroJBDZ
ensure_builtin exists twice — sync for the worker, async for the api —
and each spelled the same thirteen-field InterestRule from a preset.
A field added to Preset had to be added to both, and a miss would seed
different rules depending on which side ran first. presets.row_values
holds the mapping, beside PRESETS and drift, which the two already
share.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DafWxfoWZXVDgr9KroJBDZ
_fold_layouts folds siblings that share a structure into one row, and
_emit_group wrote its dir token from the first 40 members while
subtree_count and the folder count covered them all. Past forty, the
row claimed more than the link returned — the promise every other
count in this codebase keeps.
The group now stops at the cap, ranked, and the folders past it stay
their own rows. Measured on the gov.cy tree: seven layout groups, each
naming exactly what it counts.
endpoint_tree.py had no test. Its pure parts have one now: the fold,
the cap, the two refusals to group, the anomaly sentence and the
archive-only test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DafWxfoWZXVDgr9KroJBDZ
build.yml pushes yogeshojha/rengine-api and rengine-worker from the
development target on every master push and every release. That target
deliberately does not carry stages/, reports/, interest/, mcp/,
channels/, connectors/ or toolbox/ — compose mounts them from the host
— so the published api image holds app/ nothing and `import app.main`
answers ModuleNotFoundError. Both Dockerfiles said a prod stage must
copy them; neither had one.
Both now do, and the two build workflows target it. Verified by
building each and importing inside: the api loads app.main with
production settings, the worker loads its celery app and all 27 stages.
The ruff package list in the test workflow is written once.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DafWxfoWZXVDgr9KroJBDZ
The reloader watched eight of the ten packages compose mounts into the
api, so a save in interest/ or connectors/ served stale code until the
container was restarted. The entrypoint builds --reload-dir from one
list, and a touch of interest/providers/rules/provider.py now reloads.
worker-default, worker-beat and flower repeated the same fourteen
mounts; the list is declared once and the three share it, so beat and
flower cannot drift from the worker. The rendered config is unchanged
apart from the anchor itself.
celery_beat_data was declared and mounted nowhere — beat keeps its
schedule in /tmp deliberately, after a stale shelve bit once.
.github is 33 MB of README screenshots and reached every build
context; the api context is 45 MB down to 11 MB.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DafWxfoWZXVDgr9KroJBDZ
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
The access gate, the refresh gate and the two-factor gate each decoded
`sub`, parsed the UUID, loaded the user and raised the same three 401s.
They had already started to drift: two checked the row exists, the
third folded is_active into the same answer. deps.user_for_payload()
holds the step and each gate keeps the rule that is its own — refresh
answers 403 for an inactive account, two-factor answers 401.
Responses are unchanged. Exercised: login, me, refresh, me again, and
a malformed bearer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DafWxfoWZXVDgr9KroJBDZ
Eleven call sites across six services each spelled the same
QueryError(message=..., hint=..., start=..., end=...) so the UI could
underline the query in place. errors.py already owned the other half
of that vocabulary, query_error_for(DBAPIError); syntax_error() is its
counterpart and the services call it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DafWxfoWZXVDgr9KroJBDZ
link() sent every "Open in reNgine" URL as a text_link entity
unconditionally; Telegram rejects the whole message when one entity
names a host it will not resolve for a preview (localhost, a bare IP,
a no-dot name) — exactly what ui_base_url is in dev, so no reply with
a pivot would have rendered against the real Bot API. linkable() now
gates it and an unlinkable URL is sent as copyable code text instead.
TelegramListener._locks was a dict keyed by chat id that only grew,
never dropped, for the life of the process. channels/base.py:ChatLocks
holds a lock only while something is waiting on it and forgets the
chat once nothing is.
Also: line-length accounting used len() where message limits are
UTF-16 code units, so a message near the cap could still overflow
Telegram's limit and a wide character could be cut mid-surrogate-pair;
_split_at now walks by code unit. channels/service.py generalizes
Telegram-specific plumbing (bot_token, TelegramError) so a second
channel is a provider entry and a DriverError catch, not a rewrite.
telemetry.recent/calls_today/last_call_at take client/without filters
so a channel's own call trail is filtered before the row cut, not
after — a chat's calls no longer hide behind newer traffic from other
clients once the trail exceeds the requested limit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VckaDx6mBB4TsG76u61gvv
urllib.parse.SplitResult.port raises ValueError when the authority
carries a non-numeric port; nine call sites read it unguarded, so one
malformed URL (a stray quote, a client typo) turned into an unhandled
exception in the middle of a scan, a report, an interest rule or a
proxy dial. shared/utils/net.py:url_port makes the read itself the
failure boundary; unreadable_port names the bogus text so the naabu
and banner proxy paths can report what they refused rather than
retrying against a host string that dropped its port silently.
shared/definitions/ports.py:SCHEME_PORTS replaces four private copies
of the same http/https default-port map (endpoints.py, endpoint_judge,
scan_surface/normalize, nuclei's parser, dalfox's parser).
Also renames live_hosts to answered_hosts in the report source and the
interest context, and lifts the asset query's inline is:web predicate
into predicates.answered() — the existing distinction between is:live
(2xx) and is:web (answered at all) was already the rule; live_hosts
was the one name still claiming the stronger one.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VckaDx6mBB4TsG76u61gvv
mcp/, toolbox/ and the api's rate limiter each held their own lazy
Redis client, three copies of the same lru-singleton pattern reaching
into api.core.ratelimit from worker-side code that has no FastAPI app.
shared/redis.py is the one place that builds the async and sync
clients; ratelimit, throttle, mcp/limits and toolbox/store now import
from it. pyproject.toml's isort first-party list gains the packages
that have shipped since it was last written (channels, connectors,
interest, mcp, reports, stages, toolbox).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VckaDx6mBB4TsG76u61gvv
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