Skip to content

Feat/blast radius views#67

Merged
0xmanhnv merged 5 commits into
developfrom
feat/blast-radius-views
May 11, 2026
Merged

Feat/blast radius views#67
0xmanhnv merged 5 commits into
developfrom
feat/blast-radius-views

Conversation

@0xmanhnv
Copy link
Copy Markdown
Collaborator

No description provided.

Nguyen Manh added 5 commits May 11, 2026 10:24
Adds two partial composite indexes that power the new blast-radius
reverse-lookup endpoints:

  - asset_components(tenant_id, component_id) WHERE component_id IS NOT NULL
    Powers GET /components/{id}/assets — "which assets in tenant X use
    component Y?". Existing idx_asset_components_tenant only covered
    tenant_id alone; PG would still scan all of the tenant's components
    after that. Bad on tenants with >100k SBOM rows.

  - findings(tenant_id, vulnerability_id) WHERE vulnerability_id IS NOT NULL
    Powers GET /vulnerabilities/{id}/affected-assets — "which assets in
    tenant X are affected by CVE Y?". Existing idx_findings_vulnerability_id
    is single-column and would force PG to filter by tenant_id after a
    vuln-wide scan — bad when a popular CVE (Log4Shell, etc.) appears
    across many tenants.

The component-CVE direction is already covered by migration 000166.

Cannot use CREATE INDEX CONCURRENTLY here — golang-migrate wraps each
file in a transaction (see 000165 history for prior incident).
…tive CVEs view

Adds five new tenant-scoped read endpoints that close the CTEM blast-radius
loop. They aggregate the existing findings × assets × components data into
the questions a SOC analyst actually asks ("what is affected, by what,
where") rather than forcing them to navigate findings one-by-one.

Endpoints (all GET, paginated, tenant-scoped via JWT):

  /components/{id}/assets                  — which tenant assets use this
                                             component? Optional at_risk_only=
                                             true filter (assets with at least
                                             one open finding for this comp).

  /components/{id}/vulnerabilities         — which CVEs affect this component
                                             (within tenant)? One row per CVE,
                                             affected_assets_count rolled up.

  /vulnerabilities/active                  — distinct CVEs currently impacting
                                             tenant assets (forward catalog
                                             view); filters by severity, KEV,
                                             min CVSS/EPSS, exploit_available.

  /vulnerabilities/active/stats            — tenant-wide aggregate counts for
                                             the Active CVEs page header (8
                                             counts in 1 query via FILTER).

  /vulnerabilities/{id}/affected-assets    — which tenant assets are affected
                                             by this CVE? Aggregates findings
  /vulnerabilities/cve/{cveId}/affected-assets    GROUP BY asset_id; default
                                             include_resolved=false.

Domain DTOs (pkg/domain/component, pkg/domain/vulnerability):
  ComponentAssetUsage, ComponentVulnerability,
  VulnerabilityAffectedAsset, ActiveCVE, ActiveCVEStats, ActiveCVEFilter

Repository methods are added to the existing Repository interfaces and
implemented in postgres. Each list query uses CTE + aggregate FILTER so
GROUP BY happens in one round-trip; no N+1.

Routing notes:
  - Vulnerability blast-radius routes are registered under the existing
    /api/v1/vulnerabilities Group (chi forbids two Group blocks on the
    same mount path) and apply tenantOverlayMiddlewares() per-route to
    upgrade them from the global-catalog group's auth-only chain to
    full tenant-scoped middlewares (RequireTenant + activeMembership +
    CSRF + readRateLimit). Helper added to routes/routes.go.
  - /components/{id}/{assets,vulnerabilities} sit under the existing
    components Group which is already tenant-scoped; literal paths are
    registered before /{id} so they win path matching.

Mock updates (10 test files): the new methods on FindingRepository and
component.Repository interfaces propagate to all implementing mocks.
…sted proxies

Batch fix for the security tasks raised by /ultrareview:

S-1 — AssetGroup tenant scoping (IDOR)
  Repository.Update() and Delete() now require tenantID and emit
  WHERE tenant_id = ? in SQL. Previously a member of tenant A could
  mutate or delete tenant B's group by guessing the UUID. Service +
  handler + bulk-delete callers updated; mocks rewired.

S-2 — Branch handler tenant validation (IDOR)
  Branch endpoints accept a {repository_id} URL segment but never
  verified the repo belongs to the caller's tenant. New helper
  ensureRepoOwnedByTenant() runs at the top of every branch handler
  (List/Get/Create/Update/Delete/SetDefault/GetDefault/Compare). It
  delegates to AssetService.GetAsset which is already tenant-scoped.
  On miss: 404 (never 403, to avoid leaking existence). Wired via
  SetAssetService at server boot.

S-3-rotate — ExchangeToken refresh rotation
  /auth/exchange now rotates the refresh token (MarkUsed + new token
  in same family) matching the pattern used by Refresh and
  CreateFirstTeam. Without rotation a stolen refresh token stayed
  valid for the full window; with rotation, theft is detected on the
  next legitimate use (token-already-used). Handler persists the new
  token via httpOnly cookie; body intentionally omits it (S-3).

S-4 — Trusted-proxy guard in getClientIP
  Centralizes IP attribution behind httpsec.ClientIP() with a
  TrustedProxySet (CIDR allowlist) wired from config.Server.TrustedProxies.
  When the list is empty: only r.RemoteAddr is honored (correct for
  direct-Internet deployments). With CIDRs (K8s pod range, LB subnet):
  X-Forwarded-For and X-Real-IP are honored only from peers in the
  range. Closes the spoof-via-XFF abuse on rate limiting and audit
  logs that was possible with the previous trust-everything path.
…components / ~80 findings)

SQL seed file that populates a single tenant (matched by name/slug LIKE
'%org%') with realistic data so the new blast-radius views have something
to render in dev and demos.

Layout:
  - 50 vulnerabilities (real CVEs from 2021-2024 mix: KEV, Spring4Shell,
    Log4Shell, XZ backdoor, regreSSHion, plus per-ecosystem npm/pypi/
    maven/go/nuget findings)
  - 6 assets covering the common asset types (web app, api, service,
    mobile, repository, kubernetes_cluster)
  - ~85 global components in components(purl, name, version, ecosystem)
  - License junction rows so /components/licenses page shows distribution
  - ~80 asset_components rows (duplicated reasonably across assets so
    blast-radius reverse lookups have something to aggregate)
  - ~38 findings linking asset × component × CVE
  - Final UPDATE sweeps asset_components.{vulnerability_count,
    has_known_vulnerabilities, highest_severity} from the new findings
    and refreshes components.vulnerability_count globally

Idempotent: every INSERT uses ON CONFLICT DO NOTHING; safe to re-run.
Run: psql ... -f migrations/seed/seed_components_demo.sql
… subject

Two staticcheck failures in middleware/ that broke CI on the
feat/blast-radius-views branch:

unified_auth.go (U1000)
  extractTokenWithQueryParam was added by S-5 as the SSE-only escape
  hatch when removing query-param fallback from extractToken. The
  codebase has since migrated all streaming endpoints to WebSocket
  (which forwards cookies during the upgrade handshake) so the
  helper has zero callers and stays unused. Removed it; updated the
  surrounding comments so the rationale for not reintroducing
  query-param auth is preserved without referencing a function that
  no longer exists. Also corrected UnifiedAuth's doc-comment which
  still listed query-param as extraction step #2.

bodylimit.go (ST1020)
  Comment block on HandleBodyLimitError started with "BodyLimitHandler"
  (the type that was renamed to a function). Brought the godoc
  subject in line with the actual symbol name.

No behavior change. Build + staticcheck clean.
@0xmanhnv 0xmanhnv merged commit d9975f8 into develop May 11, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant