Feat/blast radius views#67
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.