Skip to content

Security findings ​

One list of what the scans found, across all repositories and all four finding types (vulnerabilities, licenses, secrets, AI analysis), plus a security health summary for administrators and a counts-only summary per repository or package. They read the stored scan results: nothing here starts a scan.

RouteWhoReturns
GET /api/v1/security/findingsglobal admins, project maintainers and ownersthe findings list
GET /api/v1/security/findings/summarythe samecounts per type for the same scope
GET /api/v1/security/findings/exportthe sameCSV, JSON or VEX
GET /api/v1/security/healthglobal adminsscanner and integration health
GET /api/v1/repositories/{name}/security/summaryanyone who can read the repositorycounts only

Who sees what ​

  • Global admins see the findings of every repository.
  • Project maintainers and owners see the findings of the repositories of the projects where they hold one of those roles, nothing else. The role is the effective one: recorded ownership, membership and group grants.
  • Developers, viewers, users without a project role, anonymous callers, scoped tokens, deploy tokens and project-token service accounts get 403 forbidden on the findings routes. They can read the counts of a repository they may read through the repository summary.
  • /security/health needs the global manage permission; a maintainer gets 403.

The scope is applied inside the query. The repo and project filters narrow it and never widen it: asking for a repository outside the scope returns an empty list, not an error.

What a finding is ​

A finding is one row per stored scan result, that is per package version and type, not per CVE. Open the row's scan (GET …/vuln/scans/{id} and the equivalents) for the individual vulnerabilities. A result that reports nothing is not a finding and is not listed or counted.

TypeIt is a finding whenCounts
vulnthe scan found at least one vulnerabilitythe stored critical, high, medium and low counts. Only the newest scan of an artifact counts: an artifact scanned by several scanner sets is one finding
licensea blocking license policy quarantined it, an admin approved it after such a quarantine, or its licenses are not SPDX expressionsa blocking quarantine is one high; each unresolved license is one low
secretthe scan found at least one secret, or it is quarantinedone count per secret at its severity; an unknown severity counts as low
aithe analysis recommended quarantine, or requires a license reviewa quarantine recommendation is one high, a license review one medium

The row's severity is its worst non-zero count (critical, high, medium, low) or none when every count is zero. counts carries all four numbers.

status is one of:

  • quarantined: the result's quarantine flag is set. AI findings are never quarantined: an AI recommendation does not quarantine anything.
  • suppressed: a vulnerability result whose every vulnerability has a VEX decision not_affected or fixed, or a license result an admin approved.
  • open: everything else.

vex_status appears on vulnerability findings that have VEX decisions. It is the least resolved one: affected, under_investigation, fixed or not_affected.

The findings list ​

GET /api/v1/security/findings returns {findings, has_more, next_cursor}. A finding has id, type, repository, format, package_name, package_version, artifact_id, severity, counts (critical, high, medium, low), title, scanner, status, quarantined, vex_status (when set) and scanned_at. package_name and package_version are the scanned package's name and version; a scan started from a package page, which records the package row id as artifact_id, resolves to the package's name and version, and a package that no longer exists leaves both empty (an id is never returned as a name). The title names what was found: the id of the worst vulnerability of a vulnerability finding, the unresolved licenses (else the licenses) of a license finding, the description of the worst secret of a secret finding and the first line of the notes (at most 160 characters) of an AI finding.

QueryMeaning
typecomma-separated list of vuln, license, secret, ai; default all four
repoonly this repository (by name)
projectonly the repositories of this project (by name)
severitycritical, high, medium or low: findings whose worst severity is exactly that, so the numbers match the summary
statusopen, quarantined or suppressed
qtext matched against name@version and the repository name (at most 255 characters)
limitpage size, default 50, at most 200
cursorthe next_cursor of the previous page; send the same filters with it

The order is fixed: worst severity first, then newest scan, then id. Paging is keyset: no row is counted and a deep page costs what the first costs. An unknown type, severity or status, an over-long value or a malformed cursor is 400 validation_failed.

The severity filter is an exact match, unlike the severity filter of the vulnerability overview, which means "at least".

The summary ​

GET /api/v1/security/findings/summary (optional repo and project) returns:

json
{
  "by_type": {
    "vuln":    {"total": 9, "critical": 1, "high": 2, "medium": 3, "low": 3, "quarantined": 4},
    "license": {"total": 0, "critical": 0, "high": 0, "medium": 0, "low": 0, "quarantined": 0},
    "secret":  {"total": 0, "critical": 0, "high": 0, "medium": 0, "low": 0, "quarantined": 0},
    "ai":      {"total": 0, "critical": 0, "high": 0, "medium": 0, "low": 0, "quarantined": 0}
  },
  "scanned_repositories": 5,
  "total_repositories": 7,
  "last_scan_at": "2026-05-06T07:08:09Z"
}

Per type, critical, high, medium and low count findings by their worst severity, so they add up to total; quarantined counts the findings with the quarantine flag. Every type is present, also with zeros.

total_repositories counts the repositories in the scope that can hold artifacts, which is every repository but the group ones. A repository is scanned when at least one vulnerability, license, secret or AI scan of one of its artifacts completed (also partially); the others are unscanned. last_scan_at is when the last such scan finished, or null.

Exporting ​

GET /api/v1/security/findings/export?format=csv|json|vex takes the list's filters (type, repo, project, severity, status, q; limit and cursor are ignored) and the same scope. Responses are attachments. The default format is json; any other value is 400 validation_failed.

  • csv has one row per finding with the columns type, repository, format, package_name, package_version, artifact_id, severity, critical, high, medium, low, title, scanner, status, quarantined, vex_status, scanned_at, id. A cell that starts with =, +, -, @, a tab or a carriage return is prefixed with ', so a spreadsheet does not evaluate it.
  • json is {"findings": [...], "truncated": false} with the list's finding objects.
  • vex is a CycloneDX VEX document of the vulnerability findings: the vulnerabilities of each selected artifact with their VEX decisions. The type filter is forced to vuln. The filters pick the artifacts, and each artifact's document holds all of its vulnerabilities. A package that appears in several repositories (the same package URL) is listed once.

At most 10000 findings are read for csv and json and at most 500 artifacts for vex. A larger selection is cut, X-Export-Truncated: true is set and, for json, truncated is true. Narrow the filters to export the rest.

Health ​

GET /api/v1/security/health (global admins) returns:

json
{
  "scanners": [
    {"name": "osv", "ready": true},
    {"name": "trivy", "ready": false, "message": "what to do to make it ready", "docs_url": "https://…"}
  ],
  "last_scan_at": "2026-05-06T07:08:09Z",
  "scans_pending": 3,
  "scans_failed_24h": 2,
  "dependency_track": {"enabled": true, "last_push_ok": false, "last_push_at": "2026-05-06T07:08:09Z"},
  "ai": {"enabled": true},
  "unscanned_repositories": 4
}
  • scanners has one entry per vulnerability scanner (osv, trivy, grype; see checking scanner readiness); message and docs_url are present when it is not ready.
  • scans_pending counts vulnerability, license, secret and AI scans that are pending or running; scans_failed_24h those that failed in the last 24 hours.
  • last_scan_at is when the last scan finished (null before the first), unscanned_repositories how many repositories have no completed scan, as defined above.
  • dependency_track.enabled is the global Dependency-Track switch. last_push_ok and last_push_at describe the latest push that finished (done or failed) and are absent until there is one.
  • ai.enabled is whether AI analysis is on, stored setting over environment.

The repository summary ​

GET /api/v1/repositories/{name}/security/summary needs read permission on the repository, which includes every project role. Optionally package_name narrows it to one package and version (only valid with package_name) to one version. Both are at most 255 characters. An unknown repository is 404.

json
{
  "scanned_at": "2026-05-06T07:08:09Z",
  "vuln": {"critical": 1, "high": 2, "medium": 3, "low": 4},
  "license": {"violations": 5},
  "secret": {"count": 6},
  "ai": {"quarantine_recommended": 7, "review_required": 8},
  "quarantined": true
}

It carries numbers, a time and one flag only: no package names, no vulnerability ids, no titles, so a viewer can see how a repository stands without the findings. vuln sums the counts of the repository's results; license.violations counts license findings, secret.count secret findings, ai the two AI outcomes. quarantined is true when at least one result in the selection is quarantined. scanned_at is the latest scan that finished in the selection, null when nothing was scanned.

Anonymous callers can read it for a public repository, the same rule as for the repository itself.

Free software under the AGPL-3.0-or-later.