Files
rengine/shared/services/surface_query/vulnerabilities.py
T
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

126 lines
4.2 KiB
Python

"""Vulnerability statement building, shared by the api's search and the worker's export."""
from __future__ import annotations
from typing import TYPE_CHECKING
from sqlalchemy import case, cast, func, not_, or_, select
from sqlalchemy.dialects.postgresql import JSONB
from shared.definitions.asset_query import VULN_QUERY
from shared.definitions.vulnerabilities import ACTIONABLE_SEVERITIES, SEVERITY_ORDER
from shared.models.vulnerability import Vulnerability
from shared.services.asset_query import (
QueryScope,
VulnQueryContext,
compile_vuln_query,
parse_query,
vuln_corroborated,
vuln_corroborated_ids,
vuln_is_new,
vuln_state,
vuln_suppressed,
)
if TYPE_CHECKING:
from datetime import datetime
from shared.models.vulnerability import VulnerabilityFilter
def severity_rank():
return case(
{name: index for index, name in enumerate(SEVERITY_ORDER)},
value=Vulnerability.severity,
else_=len(SEVERITY_ORDER),
)
def context(scope: QueryScope, now: datetime) -> VulnQueryContext:
return VulnQueryContext(scope=scope, now=now)
def apply_filter(query, f: VulnerabilityFilter, scope: QueryScope):
if f.ids:
query = query.where(Vulnerability.id.in_(f.ids))
if f.severities:
query = query.where(Vulnerability.severity.in_(f.severities))
elif not f.include_info:
query = query.where(Vulnerability.severity.in_(ACTIONABLE_SEVERITIES))
if f.protocols:
query = query.where(Vulnerability.protocol.in_(f.protocols))
if f.templates:
query = query.where(Vulnerability.template_id.in_(f.templates))
if f.hosts:
query = query.where(Vulnerability.host.in_(f.hosts))
if f.scanners:
query = query.where(Vulnerability.scanner.in_(f.scanners))
if f.tags:
query = query.where(
or_(
*[
func.jsonb_exists(cast(Vulnerability.tags, JSONB), tag)
for tag in f.tags
]
)
)
if f.kev:
query = query.where(Vulnerability.is_kev.is_(True))
if f.cve:
query = query.where(
func.jsonb_array_length(cast(Vulnerability.cve_ids, JSONB)) > 0
)
if f.new:
query = query.where(vuln_is_new(scope))
if f.corroborated:
query = query.where(vuln_corroborated(scope))
if f.states:
query = query.where(vuln_state(scope).in_(f.states))
elif not f.include_suppressed:
query = query.where(not_(vuln_suppressed(scope)))
return query
def order(query, f: VulnerabilityFilter, scope: QueryScope):
if f.sort == "risk":
confirmed = vuln_corroborated_ids(scope).subquery()
return query.outerjoin(confirmed, confirmed.c.id == Vulnerability.id).order_by(
severity_rank().asc(),
Vulnerability.is_kev.desc(),
Vulnerability.exploit_score.desc(),
confirmed.c.id.isnot(None).desc(),
Vulnerability.epss_score.desc().nulls_last(),
Vulnerability.cvss_score.desc().nulls_last(),
Vulnerability.template_id.asc(),
Vulnerability.matched_at.asc(),
Vulnerability.id.asc(),
)
column = {
"severity": severity_rank(),
"name": Vulnerability.template_name,
"template": Vulnerability.template_id,
"host": Vulnerability.host,
"seen": Vulnerability.discovered_at,
"cvss": Vulnerability.cvss_score,
"epss": Vulnerability.epss_score,
"exploit": Vulnerability.exploit_score,
"type": Vulnerability.protocol,
}.get(f.sort, severity_rank())
primary = column.desc() if f.order == "desc" else column.asc()
return query.order_by(
primary.nulls_last(),
Vulnerability.template_id.asc(),
Vulnerability.matched_at.asc(),
Vulnerability.id.asc(),
)
def scoped(scope: QueryScope, f: VulnerabilityFilter, columns=None):
base = select(Vulnerability) if columns is None else select(*columns)
base = base.where(scope.match(Vulnerability.scan_id))
return apply_filter(base, f, scope)
def compiled(scope: QueryScope, f: VulnerabilityFilter, now: datetime):
return compile_vuln_query(parse_query(f.q, VULN_QUERY), context(scope, now))