Vandorisk API

Two endpoints, meant for one job: screen an SBOM from your build pipeline, write the result into the product’s technical file as dated evidence, and let the build fail when a high-severity finding has nobody’s name against it.

The API runs the samescreening pipeline as the upload button in the assessment wizard, writes the same evidence line to Annex I Part I(2), and stores the same scan record. A pipeline scan and a hand upload mean the same thing in the file — that is the point of having the API at all.

Base URL: https://www.vandorisk.com

Authentication

Create a key on the API keys page. It looks like vrk_live_… and is shown once, because only its SHA-256 hash is stored. Send it on every request:

Authorization: Bearer vrk_live_xxxxxxxxxxxxxxxxxxxxxxxx

A key belongs to one workspace. Every request is scoped to that workspace: an assessment id belonging to anyone else returns 404, which does not confirm that the id exists.

No cookies, and no Origin check.The browser routes are cookie-based and therefore refuse cross-origin mutations, because browsers attach cookies to cross-site requests automatically. A bearer token is never attached automatically by any browser — something has to hold the secret and put it on the request — so there is no confused deputy and no CSRF surface to close here. Enforcing an Origin rule would only break the legitimate caller: curl and CI runners send no Origin header at all.

Keep the key in your CI secret store, never in the repository. Revoking a key on the keys page stops it authenticating immediately.

Scopes

A key carries only what you tick. A scan key that cannot read assessments is a smaller blast radius if the runner leaks it.

ScopeGrants
sbom:scanPOST /api/v1/scan
assessments:readGET /api/v1/assessments/{id}

A request with a valid key but the wrong scope returns 403 and names the scope it needed.

Rate limits and body limits

POST /api/v1/scan

Scope sbom:scan. Body: { assessmentId, sbom } where sbomis a parsed CycloneDX 1.4–1.6 or SPDX 2.2/2.3 JSON document (the object itself, not a string).

Each call: screens the components, stores an SbomScan record, and appends one dated evidence line to requirement I.2 of the assessment.

curl -sS -X POST https://www.vandorisk.com/api/v1/scan \
  -H "Authorization: Bearer $VANDORISK_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"assessmentId\":\"$ASSESSMENT_ID\",\"sbom\":$(cat sbom.cdx.json)}"

Response

{
  "scan": {
    "id": "clx…",
    "createdAt": "2026-08-21T09:14:02.114Z",
    "assessmentId": "clx…",
    "format": "cyclonedx",
    "componentCount": 412,
    "screenedCount": 361,
    "notScreenableCount": 51,
    "unscannedCount": 0,
    "findings": [
      { "id": "CVE-2021-23337", "severity": "HIGH", "score": 7.2, "component": "lodash" }
    ],
    "findingsTruncated": false,
    "evidenceLine": "SBOM scanned 2026-08-21: 412 components, 361 screened, 1 with known vulnerabilities (OSV), highest: CVE-2021-23337 (HIGH 7.2, lodash), 51 not screenable by OSV (busybox, …)"
  },
  "gate": {
    "total": 1,
    "high": 1,
    "worst": { "id": "CVE-2021-23337", "severity": "HIGH", "score": 7.2, "component": "lodash" },
    "untriagedHigh": 1
  }
}

notScreenableCountis not a failure and not a pass: those components carry no purl of a type OSV indexes, so OSV was never asked about them. An empty answer for them would be a false “clean”, so they are counted separately and named in the evidence line. Check EUVD or NVD for them by hand.

The gate object

Everything a pipeline needs to decide an exit code.

{
  "total": 12,            // advisory rows across all components
  "high": 3,              // of those, HIGH or CRITICAL
  "worst": {              // the single worst finding, CVE id when one exists
    "id": "CVE-2021-23337",
    "severity": "HIGH",
    "score": 7.2,
    "component": "lodash"
  },
  "untriagedHigh": 1      // HIGH/CRITICAL findings with no not_affected/fixed disposition
}

Fail the build on untriagedHigh, not on high. Annex I Part I(2) is not “ship no CVEs” — it is a judgement per finding: this one cannot be reached in our product, that one is already fixed, this one is still open. In Vandorisk that judgement is a disposition recorded on the finding (VEX-style: not_affected, affected, fixed, under_investigation). not_affected will not save without a written justification, because that is the claim an assessor challenges first.

A finding dispositioned not_affected or fixed stops counting toward untriagedHigh. So the gate goes green when the team has actually answered the findings — not when the scanner happens to be quiet.

Dispositions are recorded in the assessment wizard’s SBOM panel. There is no API for writing them yet (see below).

GET /api/v1/assessments/{id}

Scope assessments:read. Returns the assessment summary and its readiness, scored by the same engine the wizard and the exported technical file use.

curl -sS https://www.vandorisk.com/api/v1/assessments/$ASSESSMENT_ID \
  -H "Authorization: Bearer $VANDORISK_API_KEY"
{
  "assessment": {
    "id": "clx…",
    "productName": "Sensor",
    "versionLabel": "1.0",
    "packVersion": "0.2.0",
    "currentPackVersion": "0.2.0",
    "category": "default",
    "route": "self-assessment"
  },
  "readiness": {
    "pct": 74,
    "evidencedPct": 61,
    "total": 31, "applicable": 29, "assessed": 24,
    "met": 19, "metEvidenced": 12, "metAsserted": 7,
    "partial": 3, "notMet": 2, "na": 2,
    "blockers": ["Risk assessment incomplete (3/5 fields) — mandatory under Art. 13(2)-(3)."],
    "gapCount": 7,
    "orderedGaps": [
      { "requirementId": "P.4", "title": "…", "legalRef": "Art. 14", "state": "not met" }
    ]
  },
  "sbom": {
    "lastScanAt": "2026-08-21T09:14:02.114Z",
    "componentCount": 412, "screenedCount": 361, "notScreenableCount": 51,
    "findingCount": 1, "highCount": 1,
    "untriagedHigh": 1, "untriagedTotal": 1
  }
}

pct counts a “met” answer in full. evidencedPct counts a “met” with no evidence text at half weight — it is the more honest number to put on a dashboard, because a claim with nothing behind it is not a demonstrated requirement. blockersare hard stops under Art. 13(2)–(4) that cannot be scored away; treat a non-empty array as a failure.

Machine-readable description

The same two endpoints as an OpenAPI 3.1 document, served without a key because it documents a public API surface and contains no secrets:

curl -sS https://www.vandorisk.com/api/v1/openapi

/api/v1/openapi— request and response schemas (including the gate object), the bearer security scheme, the scope each operation needs as x-required-scope, and the 401/403/404/429 responses. Enough to generate a client, and enough to import into Postman or Insomnia.

It is hand-written and kept honest by a test that fails the build if it ever describes a path with no route behind it. It is not a replacement for this page: the reasoning about untriagedHigh and notScreenableCount lives here.

The rule pack the assessment engine evaluates is public in the same spirit: /api/pack serves the versioned requirement pack and its changelog as JSON (human-readable, with the citation index, at /pack).

CI example: GitHub Actions

Generates an SBOM, screens it, prints the evidence line into the job log and fails the build when a HIGH or CRITICAL finding has not been triaged. Needs jq, which is preinstalled on GitHub-hosted runners. Store the key as the repository secret VANDORISK_API_KEY and the assessment id as the variable VANDORISK_ASSESSMENT_ID.

# .github/workflows/cra-sbom.yml
name: CRA SBOM screen

on: [push]

jobs:
  sbom-screen:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # Produce a CycloneDX SBOM however your stack does it. Examples:
      #   npm:    npx @cyclonedx/cyclonedx-npm --output-file sbom.cdx.json
      #   syft:   syft dir:. -o cyclonedx-json=sbom.cdx.json
      - name: Generate SBOM
        run: npx --yes @cyclonedx/cyclonedx-npm --output-file sbom.cdx.json

      - name: Screen it with Vandorisk
        env:
          VANDORISK_API_KEY: ${{ secrets.VANDORISK_API_KEY }}
          ASSESSMENT_ID: ${{ vars.VANDORISK_ASSESSMENT_ID }}
        run: |
          jq -n --arg id "$ASSESSMENT_ID" --slurpfile sbom sbom.cdx.json \
            '{assessmentId: $id, sbom: $sbom[0]}' > payload.json

          http_code=$(curl -sS -o response.json -w '%{http_code}' \
            -X POST https://www.vandorisk.com/api/v1/scan \
            -H "Authorization: Bearer $VANDORISK_API_KEY" \
            -H "Content-Type: application/json" \
            --data @payload.json)

          if [ "$http_code" != "200" ]; then
            echo "::error::Vandorisk scan failed (HTTP $http_code)"
            cat response.json
            exit 1
          fi

          jq -r '.scan.evidenceLine' response.json

          untriaged_high=$(jq -r '.gate.untriagedHigh' response.json)
          not_screenable=$(jq -r '.scan.notScreenableCount' response.json)

          if [ "$not_screenable" -gt 0 ]; then
            echo "::warning::$not_screenable components could not be screened by OSV — check EUVD/NVD manually"
          fi

          if [ "$untriaged_high" -gt 0 ]; then
            echo "::error::$untriaged_high untriaged HIGH/CRITICAL findings. Triage them in Vandorisk (not affected / fixed, with a reason) or fix the dependency."
            exit 1
          fi

The same shape works on any runner: POST, read .gate.untriagedHigh, exit non-zero when it is above zero.

Errors

Every error is JSON with an error string. Status codes:

What is not here yet

Rather than let you find these out in an outage:

Vandorisk is guided self-assessment software. Screening an SBOM is evidence toward Annex I Part I(2); it is not a conformity assessment, and the manufacturer remains responsible for the Declaration of Conformity.