Webhook icon
Schedule icon
Download icon
Script icon
Process icon
Get icon
Set icon
Queries icon
Log icon
If icon
SlackIncomingWebhook icon
Fail icon

Gate a Lockfile on Licence Policy and Known Advisories with deps.dev

Check every package in an npm or Python lockfile against deps.dev licences and advisories, evaluate SPDX and CVSS policy in DuckDB, and gate CI.

Categories
Infrastructure

A pull request that bumps one dependency can pull in dozens of transitive packages, and nobody reads their licences. This flow takes the lockfile itself (npm package-lock.json v1, v2 or v3, pinned requirements.txt, poetry.lock, uv.lock or Pipfile.lock) from a raw URL, resolves every installed (system, name, version), and asks Google's deps.dev for the declared licences and the OSV advisories of each exact version. DuckDB evaluates your SPDX allow list (OR and AND expressions handled properly) and a CVSS severity ceiling, writes a per-package CSV report and a violations CSV, and the flow fails the CI call or ends in WARNING for the weekly sweep. Violations already reported for the same lockfile or pull request are not posted to Slack again.

How it works

  1. pull_request_gate (Webhook, wait and returnOutputs) is called by CI with {"lockfile_url": "...", "state_id": "owner/repo#123"} and sets fail_on_violation: true; weekly_main_branch_sweep (Schedule, Mondays 07:00) re-checks the main-branch lockfile in report mode, since new advisories land on versions you already ship.
  2. download_lockfile (HTTP Download) fetches the file; a github.com/.../blob/... link is rewritten to raw.githubusercontent.com, and an HTML page instead of a lockfile is rejected. A lockfile_file upload skips the download.
  3. parse_lockfile (Python, standard library only) detects the format from the file name and extracts pinned registry packages: npm packages entries with alias names, link workspaces and git or file sources skipped, dev, devOptional and optional scopes kept; pip == and === pins with extras, markers and --hash continuations, unpinned, URL and -r lines counted as skipped; PyPI names normalised per PEP 503.
  4. previous_violations and advisory_cache (KV Get) load the fingerprints reported last time for this lockfile (or state_id) and the advisory cache shared by every lockfile.
  5. query_deps_dev (Python) sends one POST /v3alpha/versionbatch per 1,000 package versions and follows every nextPageToken, then reads only the advisories not cached in the last 7 days from GET /v3/advisories/{id} (6 threads, paced, backoff on 429 and 5xx). SPDX expressions are turned into disjunctive normal form so SQL can evaluate them. save_advisory_cache writes the cache back.
  6. evaluate_policy (DuckDB) marks each licence ALLOWED, DISALLOWED, UNKNOWN (no licence or non-standard) or NOT_FOUND (deps.dev does not know the version), bands CVSS 3 scores from LOW to CRITICAL, keeps one row when a package lists the same vulnerability as PYSEC and GHSA, treats OSV MAL- malicious-package entries as always blocking, and fingerprints each violation to split new from already reported and count resolved ones. save_violations (KV Set) stores the fingerprints.
  7. gate (If) runs on any violation: notify_new (Slack) posts only the new ones, then block_merge (Fail) fails the execution when fail_on_violation is true, or warn_only (Fail with allowFailure) ends it in WARNING. Otherwise passed logs the clean result. The report, the violations CSV, the verdict and the counts are flow outputs.

What you get

  • A per-package CSV: scope, lockfile path, licence expression and status, deprecation flag, advisory count, worst severity, max CVSS, advisory ids and a deps.dev link.
  • A violations CSV with the rule (LICENSE_DISALLOWED, LICENSE_UNKNOWN, ADVISORY), the detail and whether it is new.
  • A CI verdict: the webhook answers with the outputs, HTTP 200 when the gate passes or only warns and HTTP 500 when it blocks.
  • Slack posts only for violations not seen before, so a weekly sweep or the fifth push to one pull request does not repeat itself.
  • Correct SPDX handling: MIT OR GPL-3.0-only passes with MIT allowed, MIT AND GPL-3.0-only does not.

Who it's for

  • Platform and DevSecOps teams who want a licence and vulnerability check on every pull request without installing a scanner in each repository.
  • Open-source program offices enforcing an approved-licence list across npm and Python projects.
  • Maintainers who want a weekly note when a new advisory hits a version already on the main branch.

Why orchestrate this with Kestra

  • A webhook with wait and returnOutputs turns the flow into a synchronous CI check that any CI system can call with curl.
  • KV state gives new-versus-known violation tracking and an advisory cache shared by every repository, with no database.
  • DuckDB keeps the policy in readable SQL next to the data; inputs change the policy per call without editing code.
  • If, runIf and allowFailure express block, warn and notify declaratively; every run keeps its report as an execution output for audit.

Prerequisites

  • Kestra 2.0 with the Python script, JDBC DuckDB and Slack plugins. The Python tasks use only the standard library (python:3.13-slim with the default Docker task runner).
  • Outbound HTTPS to api.deps.dev and to the host serving the lockfile. deps.dev needs no key; versionbatch is a v3alpha endpoint and may change.
  • A lockfile reachable over HTTPS without auth (public repositories), or upload it as lockfile_file. Packages from private registries show up as NOT_FOUND.
  • deps.dev does not give every advisory a CVSS 3 score; unscored ones count as MEDIUM. Licences come from registry metadata; non-standard means deps.dev could not map the declared text to SPDX, and uv.lock does not mark dev packages, so all its packages count as runtime.

Secrets

  • DEPS_DEV_GATE_WEBHOOK_KEY: key in the webhook URL that CI calls (required for the webhook trigger).
  • SLACK_WEBHOOK_URL: Slack incoming webhook for new violations (optional; without it the post fails with a warning and the verdict is unchanged).

Quick start

  1. Import the flow and click Execute with the defaults: OWASP NodeGoat's lockfile at a pinned commit, an intentionally outdated app. The first (BASELINE) run evaluates its 380 runtime and optional packages, finds 14 with no usable licence, around 50 HIGH, CRITICAL or malicious-package advisories, and ends in WARNING (numbers grow as advisories are published).
  2. Execute again: the same violations, 0 new, no Slack post.
  3. Run https://github.com/pallets/flask/blob/3.0.0/requirements/dev.txt with fail_on_violation: true to see a blocked Python file, then raise max_allowed_severity to CRITICAL and set allow_unknown_license to watch it pass with the earlier violations counted as resolved.
  4. Add the DEPS_DEV_GATE_WEBHOOK_KEY secret and call it from CI: curl -X POST -H 'Content-Type: application/json' -d '{"lockfile_url":"https://raw.githubusercontent.com/<owner>/<repo>/<sha>/package-lock.json","state_id":"<owner>/<repo>#<pr>"}' https://<kestra>/api/v1/main/executions/webhook/company.team/deps-dev-dependency-license-gate/<key> (curl --fail turns the HTTP 500 of a blocked gate into a failed step).
  5. Set lockfile_url to your main-branch lockfile and enable the schedule.

How to extend

  • Add MPL-2.0 or LGPL-2.1-only to allowed_licenses per project, or keep one copy of the flow per policy tier.
  • Post the violations CSV as a pull request comment with an HTTP Request to the GitHub API.
  • Combine max_cvss3 and the advisory ids with OpenSSF Scorecard results for a fuller supply-chain view.
  • Loop over the lockfiles of a monorepo with ForEach and a Subflow call to this flow.
  • Append each run's summary row to a DuckDB or warehouse table to chart licence and advisory debt over time.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.