Webhook icon
Schedule icon
Switch icon
Sequential icon
Download icon
Script icon
Docker icon
Delete icon
Set icon
Fail icon
Return icon
Loop icon
Subflow icon
GetKeys icon
Concat icon
Queries icon
Log icon
If icon
SlackIncomingWebhook icon
Pause icon
Eval icon

Org-Wide Dependency Vulnerability Triage with Approval and Expiring Waivers

Triage npm and pip lockfiles across repos with OSV.dev, gate HIGH and CRITICAL findings behind human approval, and expire waivers with a Kestra KV TTL.

Categories
CoreInfrastructure

Security and platform teams need one answer for many repositories: which known vulnerabilities ship, which are accepted on purpose, and until when. This flow scans every lockfile in a list against OSV.dev (keyless), merges the same advisory across alias ids and repositories, blocks on HIGH and CRITICAL findings, and asks a human to waive or reject them. A waiver is a KV entry with a TTL, so an exception lasts as long as the approver said and then blocks again. A lockfile that cannot be fetched or parsed goes to a dead-letter list instead of failing the batch, and mode=replay retries it. The default repos are five public repositories pinned at fixed commits (three are intentionally vulnerable training apps), so results are real and reproducible. Nothing is sample data.

How it works

  1. A Webhook (key from a secret) receives a list of {repo, lockfile_url} from CI or an inventory job. A weekly Schedule re-triages the defaults, because OSV.dev changes even when lockfiles do not.
  2. route_by_mode (Switch) picks one of three roles of the same file. In orchestrate, validate_input fails fast on a bad list, then fan_out (Loop, 3 at a time) calls this flow again in scan mode, once per lockfile.
  3. In scan, download_lockfile fetches the file. scan_osv (Python, standard library) parses package-lock.json v1 to v3 or pinned requirements (PEP 503 names, markers, hashes, # via comments, git URLs skipped). It sends one POST /v1/querybatch per 1,000 packages, follows next_page_token, and fetches each distinct advisory once (GET /v1/vulns/{id}) for severity, aliases and fixed versions. Withdrawn advisories are dropped. The JSONL file returns to the parent as a flow output.
  4. triage (DuckDB) picks one id per alias group (lowest CVE, else GHSA, else OSV id), merges repos and versions, applies block_at, joins live waivers from live_waivers, marks findings NEW or KNOWN against the last run, counts resolved ones and writes triage.csv.
  5. When blocking findings have no waiver, approval_gate posts to Slack and wait_for_decision (Pause) asks for a decision, an optional advisory list, the waiver TTL, the approver and a justification. No answer within approval_timeout counts as REJECT.
  6. apply_decision (GraalVM JS) builds waiver records, write_waivers stores one KV key per advisory and package with its TTL, and enforce_policy fails the run if anything is still blocking, so CI sees a red gate.

What this blueprint teaches

Self-calling Subflow

fan_out.scan_repo calls {{ flow.id }} with mode: scan. Each lockfile gets its own execution with its own retries, logs, restart and SLA, and transmitFailed: false keeps one bad lockfile from failing the parent. replay_orchestration reuses the trick: replay is orchestrate over the dead-letter list. Flow concurrency (limit 6) counts these children too. A limit of 1 would deadlock, because the parent waits for children that queue behind it.

Flow-level checks

checks block a bad API or UI execution before it exists (HTTP 422). On Kestra 2.0.5, Webhook executions skip checks, so validate_input repeats the rule, kept once in vars.repos_problem.

Flow-level sla, finally and afterExecution

triage_deadline fails any run after 8 hours and adds the label sla: breached. finally (run_summary) runs on success, gate failure and errors, but not after an SLA kill. afterExecution (notify_outcome) runs once the final state is known, so it is the only hook that reports an SLA breach. Keep approval_timeout well below the SLA.

KV TTL as an expiring exception

store_waiver uses ttl. Expired keys disappear from live_waivers (GetKeys), so there is no cleanup job and no date logic. A test with waiver_ttl: PT2M showed 18 waived findings blocking again after two minutes.

Dead-letter list and replay

The errors branch of scan_lockfile writes vulntriage_dlq_<repo> with the reason (UNREACHABLE, PARSE_ERROR, OSV_ERROR), the failing task, the message and an attempt counter. clear_dead_letter deletes it after the next successful scan.

What you get

  • triage.csv: one row per advisory and package with repos, versions, all alias ids, fixed versions, blocking, waived and NEW or KNOWN.
  • Waivers that record approver, justification and TTL, and expire on their own.
  • A gate that fails with the exact advisories still blocking, and an alert with the failed task and execution link for real failures (not for a gate rejection).
  • Idempotent re-runs: waivers, dead letters and the snapshot are overwritten per key.

Who it's for

  • Platform and AppSec teams running one vulnerability policy across many repositories.
  • Engineering managers who approve time-boxed risk acceptance.
  • Kestra users learning subflows, checks, SLA, finally, afterExecution and KV TTL.

Why orchestrate this with Kestra

  • One file covers fan-out, per-repo isolation, approval and audit trail. A cron script would need a queue, a database and a web form.
  • Pause with typed inputs gives a review step through the UI or API.
  • Retries, timeouts, SLA and concurrency are declared, not coded.

Prerequisites

  • Kestra 2.0 with the Python scripts (Docker runner), DuckDB, GraalVM and Slack plugins.
  • Outbound HTTPS to api.osv.dev and to wherever your lockfiles live. No API key is needed.

Secrets

  • VULN_TRIAGE_WEBHOOK_KEY: a long random string that guards the webhook URL (required for the webhook).
  • SLACK_WEBHOOK_URL: Slack incoming webhook for approval requests, failures and outcomes (optional; Slack tasks use allowFailure).

Quick start

  1. Execute with defaults. The first run is the baseline: every finding is NEW. Expect 474 findings merged into 276 advisories, 121 blocking at HIGH.
  2. Approve in the UI, or POST the form fields decision, waiver_ttl, approver, justification and optional waive_only to /api/v1/main/executions/{id}/actions/resume.
  3. Re-run: covered advisories no longer ask for approval until their TTL ends.
  4. Add the secrets, point CI at the webhook and enable weekly_org_sweep.

How to extend

  • Scope waivers per repo by adding the repo to waiver_key.
  • Add more OSV ecosystems (Go, Maven, crates.io) in scan_osv.
  • Park dead letters after N attempts, or open a ticket per new CRITICAL.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.