Commit Graph
4 Commits
Author SHA1 Message Date
Yogesh OjhaandClaude Opus 5 3644dffe63 feat(tables): select rows and act on them from one action bar
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
2026-09-15 07:38:48 +05:30
Yogesh OjhaandClaude Opus 5 e3b310b5b1 feat(cve): CVEs become a surface of their own
One evidence axis runs across both findings dimensions
(shared/definitions/evidence.py): inferred < observed < corroborated <
proven. Software rows store inferred or corroborated, set in match_scan from
a host and CVE join to non-info, non-suppressed findings; vulnerabilities
store observed or proven, proven being an out-of-band interaction recorded at
write. Both grammars gain an evidence: field and the old is:proven becomes
is:recorded. GET /cves and GET /cves/{cve} answer "am I affected by X" across
both dimensions, with an MCP tool and a toolbox lookup beside them. Ladder
counts are count(*) FILTER over the same predicates the searches compile, so
a rung opens exactly the rows it counted; tests/grammar/test_evidence.py
asserts it. match_scan carries discovered_at forward through a
software_previous temp table and returns the newly exposed delta for the
nightly notification; nvd_corpus.load skips the download when the mirror
stamp is unchanged.

The Attack surface group had grown to eight items with three of them sharing
a word. Vulnerabilities is now one entry parenting three sibling routes
joined by a tab rail: Findings, Software and CVEs. The routes are unchanged,
so vuln_q and sw_q state, deep links and per-route caching survive, and
NavItem.match keeps the parent lit on the siblings. The Software dimension
label drops to "Software" while its rows keep the noun "software CVE".

The CVE index gets the layout its siblings have: coverage strip, a search bar
attached to the card, a severity rail, a filter row with sort and refresh,
ListHeader rows and pagination. components/search-bar.svelte is the
plain-text twin of QueryBar for surfaces with no grammar. GET /cves gained
page, size, sort, dir, severity, kev, ransomware and evidence and returns
severity_counts; severity and the top rung are resolved in SQL from
SEVERITY_RANK and EVIDENCE_RANK rather than in Python.

Counts verified against the rows they open: overview.cves 1,165 equals the
index total, the Software tab 91,039 equals coverage.findings, and the
severity rail 157/373/583/52 sums to 1,165. _INDEX_TOTAL now excludes the
triage-suppressed findings the listing already excluded, and the software
dimension is counted exactly rather than capped at COUNT_CAP, so the tab
matches the page. The distinct-CVE count is 10s with JIT on and 91ms under
SET LOCAL jit = off, leaving the overview at 137ms cold and 17ms warm.

Also fixes software-table, which passed a 1-based page to ResultsPagination
where every other table passes a 0-based one, so page one read
"Showing 51-100".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MirAGiQ2fSJJtikWi6YRNj
2026-09-12 18:28:41 +05:30
Yogesh OjhaandClaude Opus 5 8f341131fc refactor(ui): enterprise naming for the navigation, and Interest becomes Exposures
The menu read as hacker cosplay rather than security software: "Worth a look"
as a product feature, swords for the tool library, a sparkle for a risk
surface, a crosshair for the asset list.

"Worth a look" is now Exposures. Studying what the layer does settles the name:
it scores an asset for what it IS — admin interface, developer tooling, remote
access, non-production, legacy, no authentication, exposed content, takeover
risk — and for how far it stands out from the estate. That is an exposure, the
Gartner CTEM term, and it pairs with Vulnerabilities, which is what a scanner
found. Bands are Critical/High/Notable, not "Look first"/"Worth a look".

Only the user-visible layer is renamed. The interest/ package, the
interest_signals table and InterestKind keep their names because renaming them
is a migration with no user benefit. The host grammar's canonical tokens become
is:exposed, exposure:<kind> and exposure_band:<band>, with the old spellings
kept as aliases so no saved rule breaks — and the endpoint grammar's separate
interest: (open_redirect, traversal, admin) keeps its name, so the rename
removes a collision rather than creating one.

Exposures also stops being a settings page. The nav item opened Rules and
Dismissed while its name promised findings; it now opens the project-wide
ranked list over the same QueryScope as the five dimensions, with Rules and
Dismissed as tabs. Judging and rule suggestions are per-run and stay hidden
there, and a project view reports stale=false so it never queues a re-label.

Naming and icons elsewhere:
- IPs -> IP addresses; the other four dimensions keep their names
- Reconnaissance -> Discovery
- the Services "Exposure" panel -> "Open services", freeing the word
- swords -> library, sparkle -> scan-eye, crosshair -> target, plug-zap ->
  cable, network -> bot for MCP, layers -> sliders for scan contexts, and no
  two items in a group share a glyph

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EQgatRaA9fNsv2jXh4rZ47
2026-09-09 06:06:30 +05:30
Yogesh OjhaandClaude Opus 5 9be7c1d7cc feat(surface): the five result dimensions become project-wide, not scan-scoped
A pentester asked how to see vulnerabilities and the answer was "open a scan".
Every result surface was scoped to one run, which is a scanner's model rather
than an ASM platform's.

The keystone is QueryScope: the set of scans a query reads — one on a scan
page, the latest covering scan of every target on a project view (a producing
stage succeeded, or rows exist; the rule target_summary already used, now
shared as covering_stages()). scope.match() emits `== id` for a single scan so
no scan-page plan changed, and the predicates accept a UUID, a QueryScope or a
sequence so single-scan callers were untouched.

Because the scope generalises rather than duplicates, the same table components
render both views: the grammar, facets, leads, group-by, evidence and detail
sheets all follow. No results table exists twice.

Scan-level facts had to become per-row: _per_scan() ORs and_(col == sid,
build(sid)) so *_has_baseline — and therefore is:new — stays exact across many
scans, and baselined_targets() makes the row-level is_new flag ask per target,
or a first-scanned target badges everything New. Addresses are the exception:
the IPs view folds one row per address across every target that serves it, so
newness is judged against target_ids with the row's own first_seen as cutoff.

- target: is a field in all five grammars and a group dimension in all five
- /surface/[dimension] renders from SURFACE_ORDER; routeLabels take their five
  labels from it so nav, breadcrumb and heading cannot drift
- a scope strip states how many targets the reading covers, how old the oldest
  is, and which targets were never scanned for that dimension, with a launch
  action — absent is not empty
- anything needing a parent run (rescan, recheck, the endpoints Gone lens) is
  off project-wide, and the coverage strips drop their "on this scan" copy
- the scan page's tab strip links to the same dimension across all targets,
  carrying the typed query

Verified: every group token and every counted lead in all five dimensions
returns exactly its own count in both scopes, and project-scoped is:new equals
the union of the per-scan answers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EQgatRaA9fNsv2jXh4rZ47
2026-09-09 05:34:37 +05:30