Pipeline and CI/CD Connectors
Connect your build pipeline to CVD Portal. The live connectors are SBOM push, SARIF ingest and FOSSA issues import.
SBOM push
Push a CycloneDX or SPDX JSON SBOM, or a dependency manifest such as package.json, from your pipeline.
| Attribute | Value |
|---|---|
| Method | POST |
| Path | /api/v1/sbom |
| Required scope | sbom:write, on a key made by an administrator |
| Plan | Enterprise |
| Body | { "filename": "sbom.cdx.json", "content": "<file contents>", "productId": "<optional>", "part": "<optional>", "releaseVersion": "<optional>", "releasedAt": "<optional>" } |
| Rate limit | 20 uploads per minute per IP |
The push replaces the stored SBOM for its scope. With productId, the scope is that product. Without it, the scope is the account-wide SBOM. The push then checks the components against OSV.dev and updates the vulnerability findings for that scope before it returns. VEX statements in the vulnerabilities array of a CycloneDX SBOM are on the findings when the response arrives. A VEX status that an administrator set by hand always wins over the SBOM. The daily supply-chain scan sends the alert emails for new findings.
With releaseVersion, the push also stores the SBOM in the archive of that release. releaseVersion needs a productId. releasedAt is optional and takes a date such as 2026-09-30 or an ISO 8601 timestamp with an offset. A date that does not exist, for example 2026-02-30, gets HTTP 400. The push replaces the stored SBOM of the product only when the release is the newest. A push for an older release goes to the archive only. The vulnerability check then does not run, and refresh is null. See SBOM Registry for how the newest release is found.
With part, the push replaces only that part of the product SBOM, for example firmware or device-api, and keeps the other parts. A part name has 1 to 40 lowercase letters, digits or hyphens. Without part, the push replaces the whole SBOM, with all its parts. part needs a productId, and a push with part and no productId gets HTTP 400. Each pipeline that builds one part can then push its own file. See Parts of one product SBOM.
jq -n --rawfile content sbom.cdx.json \
'{filename: "sbom.cdx.json", content: $content}' |
curl -fsS -X POST https://cvdportal.com/api/v1/sbom \
-H "Authorization: Bearer $CVD_API_KEY" -H "Content-Type: application/json" -d @-
A successful push returns HTTP 200.
{
"success": true,
"count": 3,
"source": "API",
"productId": null,
"productsCovered": 4,
"conformance": { "...": "BSI TR-03183-2 report" },
"minimumElements": { "...": "minimum elements report" },
"refresh": { "scanned": 3, "checked": 3, "unanswered": 0, "fresh": 1, "resolved": 1 },
"refreshError": null,
"release": null
}
refresh.scannedis the number of components in the SBOM.refresh.checkedis the number of components the scan checked against OSV.dev or NVD. A component is checked only when it has a version and a package URL (purl), or a CPE with a version. Whencheckedis 0, nothing was checked. The scan does not count as a check and closes no finding that the SBOM still lists.refresh.unansweredis the number of components that OSV.dev or NVD did not answer for. These components have the identifiers they need. Push again later, or wait for the next scan.refresh.freshis the number of findings that are open now and were not open before this push.refresh.resolvedis the number of findings that this push closed, because the SBOM no longer matches them.releaseisnullwhen the push has noreleaseVersion. Otherwise it holds the releaseid, theversion,createdandcurrent.createdistruewhen this push created the release.currentistruewhen this push also replaced the stored SBOM of the product.
When the vulnerability check fails, for example because OSV.dev does not answer, the SBOM is still stored and the response is still HTTP 200. refresh is then null and refreshError holds a message. The daily scan checks the SBOM again. Do not fail your build on refreshError.
SARIF static analysis ingest
Upload Static Analysis Results Format (SARIF) 2.1.0 reports from tools such as CodeQL, Semgrep, Trivy, and Grype.
| Attribute | Value |
|---|---|
| Method | POST |
| Path | /api/v1/scan/sarif |
| Required scope | sarif:write |
| Plan | Enterprise |
| Format | SARIF 2.1.0 JSON |
| Optional metadata | productId, source (github-actions, gitlab-ci, or manual) and repo, in the query string or the body |
| Rate limit | 20 uploads per minute per IP |
Each result becomes a pipeline finding for your company. The Pipeline Findings page lists them. The server removes duplicates by fingerprint for each product. A new upload of the same scan does not create duplicates, and it does not open a finding again that your team dismissed or fixed.
Send productId to link the findings to one product. The product must belong to your company, or the server returns HTTP 404. Without productId, the findings apply to the whole account.
The verification evidence of a product shows the number of imported and open findings as static analysis evidence. The count includes the findings of that product and the findings without a product. It never includes the findings of another product.
FOSSA issues import
Import the dependency vulnerabilities that FOSSA found. The body is the JSON response of the FOSSA issues API, GET https://app.fossa.com/api/v2/issues, or a bare array of its issues.
| Attribute | Value |
|---|---|
| Method | POST |
| Path | /api/v1/scan/fossa |
| Required scope | sarif:write |
| Plan | Enterprise |
| Format | FOSSA issues API v2 JSON ({ "issues": [...] }) |
| Optional metadata | repo and productId, in the query string or the body |
| Rate limit | 20 uploads per minute per IP |
Each issue of type vulnerability becomes a pipeline finding with the source fossa. An issue with a CVE, a FOSSA vulnerability id or a CVSS vector also counts as a vulnerability. Licensing and quality issues are not stored. The response counts them in warnings.
| Finding field | FOSSA field |
|---|---|
| Rule | cve, else vulnId |
| Severity | severity. When FOSSA says unknown, the cvss score decides. With no score, the severity is Medium. |
| Location | source.id, the package locator, for example npm+lodash$4.17.20 |
| Message | title, the package and version, details, and the fix version from remediation |
A finding is matched by its CVE (or FOSSA id) and its package locator within one product. The same CVE in the same package version is one finding, also when FOSSA lists it for more than one project. A re-upload refreshes the finding and never changes its status. When the export names one FOSSA project and you send no repo, the project locator is the repo.
FOSSA findings show on the Pipeline Findings page. They are dependency vulnerabilities from software composition analysis. They evidence Annex I Part II points (1) and (2). The static analysis evidence for clause 7.6 counts SARIF findings only.
FOSSA does not send outbound webhooks. Its webhook URL (/hooks/webhook/{project}) only starts a FOSSA analysis. Read the issues API in a CI step after fossa analyze and send the result to CVD Portal. CVD Portal never stores your FOSSA token.
fossa analyze
# fossa test exits 1 when FOSSA finds issues. Keep that status and export first.
test_status=0
fossa test || test_status=$?
curl -fsS -G https://app.fossa.com/api/v2/issues \
-H "Authorization: Bearer $FOSSA_API_KEY" \
--data-urlencode "category=vulnerability" \
--data-urlencode "scope[type]=project" \
--data-urlencode "scope[id]=$FOSSA_PROJECT_LOCATOR" \
-o fossa-issues.json
curl -fsS -X POST "https://cvdportal.com/api/v1/scan/fossa?productId=<product id>" \
-H "Authorization: Bearer $CVD_API_KEY" -H "Content-Type: application/json" \
--data @fossa-issues.json
exit "$test_status"
The FOSSA API returns issues in pages. Read each page with page and count, and send each page, or join the issues arrays first. One upload holds at most 5,000 findings.
A successful import returns HTTP 200.
{
"ingested": 12,
"created": 3,
"updated": 9,
"projects": ["custom+1234/acme-firmware"],
"warnings": ["Skipped 4 licensing or quality issue(s); only vulnerabilities are stored"]
}
Release gate
After the pushes, a pipeline asks for a verdict on the release. POST /api/v1/compliance/release-gate/runs runs six checks for one product, stores the verdict, and answers 422 with strict: true when the release must not ship.
| Attribute | Value |
|---|---|
| Method | POST |
| Path | /api/v1/compliance/release-gate/runs |
| Required scope | gate:write |
| Plan | Compliance or higher |
| Rate limit | 30 runs per minute per key |
See Stored gate runs for the body, the response and an example.
GitHub Action
The CRA release gate action does the SBOM push, the SARIF push and the verdict in one step. It prints the reason for each check in the job summary and fails the job on a block. The source is porta-regulus/cra-gate, under the MIT licence.
- uses: porta-regulus/cra-gate@v0
with:
api-key: ${{ secrets.CVD_API_KEY }}
product-id: ${{ vars.CVD_PRODUCT_ID }}
release: ${{ github.ref_name }}
sbom: sbom.cdx.json
The verdict needs the Compliance plan. The sbom and sarif inputs push files, and a push needs the Enterprise plan. On Compliance, leave them out and upload the SBOM in the dashboard. @v0 follows the newest 0.x release. For a release pipeline, pin the action by commit SHA. Other runners can run the same tool as one file with Node 20 or later. See the README of the repository.
SonarQube and VEX connectors
The Integrations settings page shows a SonarQube Quality Gate webhook (/api/webhooks/sonarqube) and a VEX ingest endpoint (/api/v1/vex) as "On request / Early access". These endpoints are not available.
To record VEX statements now, put them in the vulnerabilities array of a CycloneDX SBOM, or record them on the SBOM settings page. See SBOM Registry. Standalone OpenVEX and CSAF VEX documents are not read.
Optional query or body fields are source (github-actions, gitlab-ci or manual), repo, and productId. With productId, the findings are recorded for that product. Without it, they are recorded for the whole account. The product must belong to the company of the API key, otherwise the request returns 404 and records nothing. A finding is matched by its fingerprint within one product, so the same scan uploaded for two products keeps a separate finding for each. A re-upload refreshes the finding for the same product and never changes its status.
curl -X POST "https://cvdportal.com/api/v1/scan/sarif?source=github-actions&productId=<product id>" \
-H "Authorization: Bearer $CVD_API_KEY" -H "Content-Type: application/json" \
--data @results.sarif
Related guides
- Connecting GitHub covers automated nightly SBOM synchronization.
- CVD Portal app for Jira explains vulnerability synchronization to Jira projects.
- API Authentication documents API keys, OAuth scopes, and approval queues.