Inspiration

Security scanners hand you a flat list — severity, a file, a line, and nothing about consequence. A "High" in dead code looks identical to a "High" in an auth helper imported by forty services, so the genuinely dangerous finding gets the same one-line comment as the noise. CVSS isn't risk. We wanted to answer the question every triage misses: if this is exploited, what else breaks — and who owns it? GitLab Orbit's code knowledge graph makes that answer computable for the first time.

What it does

When a security scan finds a vulnerability, Vulnerability Blast Radius:

  1. Discovers it via the GitLab GraphQL API (with EPSS, KEV, and reachability enrichment).
  2. Asks Orbit which code actually calls and imports the vulnerable file — the real blast radius.
  3. Scores a deterministic 0–100 contextual risk (severity + EPSS + KEV + reachability + blast size).
  4. Resolves the owner from CODEOWNERS so it lands with the right person.
  5. Posts an idempotent note back into GitLab — and proposes (never silently applies) a severity change.

Live example: a vulnerability in a Ruby model returns 11 cross-file dependents → 96/100 (Critical), routed to @security-team. The traversal is language-agnostic and verified live on Rust, Go, and Ruby — the languages Orbit fully indexes today (TypeScript/Python support is rolling out in Orbit).

How we built it

Python (uv-managed), 94 tests at 99% coverage, MIT-licensed. The risk score is pure and deterministic — no LLM in the number — so it's reproducible and auditable. The core: an orbit_client that runs a verified two-pass query_graph traversal (reverse CALLS + an ImportedSymbol-routed IMPORTS), a GraphQL discovery layer, a CODEOWNERS resolver, and a deterministic risk scorer. We shipped it as a custom Duo agent published to the AI Catalog plus a Duo Chat skill. Every Orbit assumption was verified against the live API (glab orbit remote dsl/schema/query), not the docs.

Challenges we ran into

  • The intended Vulnerability → File graph path doesn't exist in Orbit — we verified this live and bridged in via the file-path property, then traverse the code graph.
  • IMPORTS has no Definition→Definition variant — import-based dependents must route through ImportedSymbol (found by reading live DSL validation errors).
  • Orbit node IDs are strings (the docs example is misleading); project.vulnerabilities is Ultimate + default-branch; enabling an agent is project-scoped, not via the global catalog.
  • Orbit is Beta and point-in-time, so we built drift-tolerant parsing and graceful degradation.

Accomplishments that we're proud of

  • A deterministic, testable risk score — not an LLM guess — that gives the same number every run.
  • The agent passed 11/11 manual scenarios including adversarial guardrail tests: it refuses to auto-change severity, fabricate a score, post duplicates, or act out of scope.
  • Every Orbit traversal verified against the live graph — Rust, Go, and Ruby (the languages Orbit fully indexes today), with real cross-file dependent counts.
  • Dogfooding proof: run live, the agent flagged a real CWE-377 insecure-temp-file in our own demo script — triage that works on actual findings, not just curated ones.

What we learned

  • Orbit's real value for security is the code graph, not its (currently empty) security nodes — blast radius is a code-reachability question.
  • "Verify against the live API, don't trust the example" — the string-ID gotcha would have silently broken parsing in production.

What's next for Vulnerability Blast Radius

  • Tune blast-radius depth and add pipeline/job exposure to the score.
  • Enrichment fallbacks for findings without a CVE; a weekly digest of the highest-blast-radius vulnerabilities per service; one-click enable for any team on Orbit + Duo.

Built With

  • ai-catalog
  • gitlab-duo-agent-platform
  • gitlab-knowledge-graph
  • gitlab-orbit
  • graphql
  • pydantic
  • pytest
  • python
  • uv
Share this project:

Updates