Jenkins
The portal does not ship a Jenkins plugin. Instead, a small declarative-pipeline snippet calls the portal's REST API directly. This keeps the integration auditable and avoids tying you to a specific Jenkins version.
Engineers maintaining a Jenkins controller / agent. Familiarity with declarative pipelines and the Credentials plugin.
Quick start
// Jenkinsfile
pipeline {
agent any
environment {
TRUSTEDOSS_API_URL = 'https://trustedoss.example.com'
TRUSTEDOSS_PROJECT_ID = '01H7XYZ…'
}
stages {
stage('TRUSCA SCA') {
steps {
withCredentials([string(credentialsId: 'trustedoss-api-key',
variable: 'TRUSTEDOSS_API_KEY')]) {
sh '''
set -eu
curl --version >/dev/null
jq --version >/dev/null
# scan-retention: forward the branch as metadata.ref so the portal
# groups this branch's scans under one retention key (the latest
# supersedes the prior). BRANCH_NAME is set by Multibranch / PR jobs;
# GIT_BRANCH (often "origin/<branch>") is the fallback — adjust to
# your job's variables. Empty ref is omitted.
SCAN_REF="${BRANCH_NAME:-${GIT_BRANCH:-}}"
BODY=$(jq -nc --arg ref "${SCAN_REF}" \
'{kind: "source", metadata: ({source: "jenkins"} + (if $ref == "" then {} else {ref: $ref} end))}')
SCAN_ID=$(curl -fsS -X POST \
-H "Authorization: Bearer ${TRUSTEDOSS_API_KEY}" \
-H "Content-Type: application/json" \
-d "${BODY}" \
"${TRUSTEDOSS_API_URL}/v1/projects/${TRUSTEDOSS_PROJECT_ID}/scans" \
| jq -r .id)
echo "scan_id=${SCAN_ID}"
# Poll until terminal (timeout 30 min, every 30 s). A scan that
# fails or never finishes must fail the build here: the gate reads
# the last SUCCEEDED scan, so falling through would grade this
# build against an older one and report a pass nobody earned.
FINISHED=""
for _ in $(seq 1 60); do
STATUS=$(curl -fsS -H "Authorization: Bearer ${TRUSTEDOSS_API_KEY}" \
"${TRUSTEDOSS_API_URL}/v1/scans/${SCAN_ID}" | jq -r .status)
echo "status=${STATUS}"
case "${STATUS}" in
succeeded) FINISHED=yes; break ;;
failed|cancelled)
echo "scan ended ${STATUS}; refusing to grade a stale scan" >&2
exit 1 ;;
esac
sleep 30
done
if [ -z "${FINISHED}" ]; then
echo "scan did not finish within 30 min (it keeps running in the portal)" >&2
exit 1
fi
# Evaluate the gate for the ref we just scanned. Without ref= the
# portal answers with its main line's newest succeeded scan.
set -- -fsS -H "Authorization: Bearer ${TRUSTEDOSS_API_KEY}" \
--get "${TRUSTEDOSS_API_URL}/v1/projects/${TRUSTEDOSS_PROJECT_ID}/gate-result"
if [ -n "${SCAN_REF}" ]; then
set -- "$@" --data-urlencode "ref=${SCAN_REF}"
fi
GATE=$(curl "$@" | jq -r .gate)
echo "gate=${GATE}"
test "${GATE}" = "pass"
'''
}
}
}
}
}
Save as Jenkinsfile at the repo root. Make sure the agent has bash, curl, and jq installed.
Setup
1. Generate an API key
In the portal: /integrations → API keys → Create API key. Pick scope project and bind it to the project this job scans (or team to cover every project a team owns). Set "What this key may do" to Read and write; this job triggers a scan, which a read-only key (the default) is refused. API keys otherwise inherit the issuing user's role. See API keys.
2. Add the key as a Jenkins credential
- Jenkins → Manage Jenkins → Credentials.
- Choose a domain (typically Global) → Add Credentials.
- Kind: Secret text.
- Secret: the API key.
- ID:
trustedoss-api-key(matches thewithCredentialsblock).
The credential's value is masked in console output.
3. Create the pipeline job
- New item → Pipeline (or Multibranch Pipeline for repos with feature branches).
- Pipeline definition: Pipeline script from SCM.
- SCM: Git → your repo URL → branches to build.
The pipeline runs the SCA stage on every build.
Recipes
Use a shared library
If you maintain a Jenkins shared library, wrap the SCA call into a step:
// vars/trustedossSCA.groovy in your shared library
def call(Map config = [:]) {
withCredentials([string(credentialsId: config.credentialsId ?: 'trustedoss-api-key',
variable: 'TRUSTEDOSS_API_KEY')]) {
sh """
set -eu
# …same body as the quick-start…
"""
}
}
Then in Jenkinsfile:
@Library('shared') _
pipeline {
agent any
stages {
stage('SCA') { steps { trustedossSCA() } }
}
}
PR (multibranch) decoration
For Multibranch Pipelines, the change request status (CHANGE_ID, CHANGE_BRANCH) lets you skip non-PR builds:
when {
anyOf {
branch 'main'
expression { env.CHANGE_ID != null }
}
}
Advisory mode (don't fail the build)
Replace the final test "${GATE}" = "pass" line with:
echo "::warning::TRUSCA gate=${GATE}"
The build stays green; the gate verdict is recorded in the console log only.
Post the SBOM as a build artifact
The SBOM export endpoint does not accept API keys in this release — it
requires a user access token (require_role("developer")), so an API key gets
401. A CI job can only archive the SBOM if you give it a JWT, which means
handling a user credential in the pipeline. Weigh that before adopting this
recipe; downloading the SBOM from the portal UI avoids the problem entirely.
If you do have a token available as TRUSTEDOSS_JWT:
sh '''
curl -fsS -L -OJ \
-H "Authorization: Bearer ${TRUSTEDOSS_JWT}" \
"${TRUSTEDOSS_API_URL}/v1/projects/${TRUSTEDOSS_PROJECT_ID}/sbom?format=cyclonedx-json"
'''
archiveArtifacts artifacts: 'sbom-*.cdx.json', fingerprint: true
-OJ takes the filename from the response, which the portal sends as
sbom-<project-slug>.cdx.json — match that pattern when archiving, not
*.cyclonedx.json.
When the scan found no components
The gate result carries component_outcome, and the Evaluate gate stage prints it on the verdict line as components=<value> and keeps it in env.TRUSTEDOSS_COMPONENT_OUTCOME. Two values mean the scan produced no components, so every count is 0 for that reason and a pass means nothing was judged:
empty_with_manifests: the source declares dependencies (dependency manifests are present), but the scan produced no components. Something failed during the scan, for example dependency resolution. Check the scan log.empty_no_manifests: the source has no dependency manifest TRUSCA reads. Check that TRUSCA supports the build system.
For either value the stage prints a WARNING: line and leaves the build result alone, so the build still passes. To fail the build on empty_with_manifests instead, an administrator sets GATE_INCOMPLETE_SCAN_ENABLED=true on the portal (see the optional gate axes). The gate then returns fail with incomplete_scan_basis set to empty_with_manifests, and Apply gate verdict fails the build as it does for any failed gate. See When a scan finds nothing for what each case means.
Branch protection (without GitHub / GitLab)
Native Jenkins does not enforce check status on a Git host's PR / MR — that is the host's job. If you use:
- GitHub PRs with Jenkins via the Multibranch plugin: status is reported via the GitHub Checks API. Branch-protect on GitHub by requiring the Jenkins check.
- GitLab MRs with Jenkins: install the GitLab plugin to publish the build status. Branch-protect on GitLab by requiring the pipeline to pass.
- Bitbucket / Gitea: install the equivalent status-publisher plugin.
The TRUSCA gate does not change the wiring — it only changes the build's exit status.
Idempotency
Re-running a Jenkins build re-issues a new scan, and the portal keeps both in the project history. The gate reads only the newest succeeded scan for the ref the build sent, so older scans stay visible without moving the verdict.
The ref matters for a second reason. The portal allows one active scan per (project, ref), so a build whose previous scan is still queued or running gets a 409 rather than a second scan on the same target. The bundled Jenkinsfile.example derives the ref from CHANGE_ID (multibranch PR builds), TAG_NAME, BRANCH_NAME, or GIT_BRANCH in that order. A job that sends no ref puts every build in one ad-hoc cohort, where two branches building concurrently collide — disableConcurrentBuilds() does not help, because it is scoped per branch job.
Troubleshooting
curl: command not found on the agent
The agent image is too minimal. Either add curl and jq to the image, or use a docker agent:
agent {
docker { image 'alpine:3.20'; args '-u root' }
}
options { skipDefaultCheckout(false) }
Pipeline silently passes despite a fail gate
The set -eu at the top of the shell block is essential — without it, a non-zero test does not propagate to Jenkins. Confirm the shebang and set -eu are present.
Credential is leaked in the log
Confirm the credential is wrapped in withCredentials and that the shell expands ${TRUSTEDOSS_API_KEY} only inside the wrapped block. Jenkins masks the value in stdout/stderr only when sourced from withCredentials.
Network timeouts on long scans
Real ORT scans can take 30–60 minutes. Bump the polling loop bound:
for _ in $(seq 1 120); do … sleep 30; done # 60 minutes
The Jenkins build timeout itself is a separate setting — set the job's "Abort the build if it's stuck" to a value larger than your worst-case scan.