CI/CD
Recipes for a pipeline that already sends a scan: pick the right token, choose a gate, check pull requests, record releases, scan a monorepo, and send the results to other tools. For the first job, see Get started. The examples use npm ci as the install step: use the install step of your project, before the scan (what the job needs).
Add the first job #
The steps for each CI system are on Get started: where to keep the token, and the job to add.
In your workspace, Repositories › Set up CI mints the token of one repository and shows its job for your CI system. Name the job securechain.
Pick a token for the pipeline #
Use the smallest token that does the job. If it leaks, less is exposed.
| Your pipeline | Who makes the token | What to select on Access tokens |
|---|---|---|
| The pipeline of one repository | An admin, or a member of that repository | The CI token (one repository) preset and the repository. The form starts with this preset. The token can send and read that repository only, so the CLI shows no link to the scan of a git dependency in another repository (see links to another repository). It expires in 90 days. A member must set an expiry of 90 days or less. The token stops when the person who made it is disabled or loses the member role on the repository, and Access tokens marks it; it works again if that person gets the role back. |
| A CI server or a shared CI template that builds many repositories of one team | An admin | Scope a department, the department, and the CI upload (all my repositories) rights. One token serves every repository of the department, so the team does not make and replace one token for each repository. A scan of a new repository puts the repository in that department. The token can read every repository of the department, so the CLI shows the link to the scan of a git dependency in another repository of the department (see links to another repository). The token cannot send scans to a repository of another department. This token does not depend on a person: it keeps working when the admin who made it leaves. |
| A pipeline that builds more than one repository, when you are not an admin | A member | The CI upload (all my repositories) rights. The token acts as you: it can read every repository that you can read, and the form warns about it. It expires in 90 days unless you clear the date. It stops when your user is disabled. A job that only sends scans (securechain sca, securechain check) can use Upload only: it reads only the findings of its own scans. |
- The Repository box of Set up CI and of Access tokens lists only the repositories that the workspace has, and for a member only the repositories that the member can see. Type a part of the name to filter the list, for example
web-shop
oracme
. The filter ignores upper case and lower case. Select the repository with the mouse, or with the arrow keys and Enter. A CI token needs a repository that the workspace has. For a new repository, select Register repository on Set up CI and paste its URL (an admin and a member can), or send a first scan from your computer. A member's repository goes where the member's first scan would put it: into each department in which the member is a member, or ungrouped when the member has no department. If the departments of the member do not allow a new repository, an admin registers it. - To replace a token that expires: make the new token, change the CI secret, run the pipeline, then revoke the old token.
- The Last CLI column of Access tokens shows the CLI version that each token used last. Use it to find a job that runs an old CLI.
Access tokens explains each right and scope.
Sign in with no stored token #
GitHub Actions and GitLab CI give each job a signed OIDC token. With a trust rule, the job trades that token for a portal token of one hour, and the pipeline stores no portal token. A leaked token stops by itself. The audit log names the repository, the ref, and the run of each sign-in.
- An admin opens Settings › CI sign-in and adds a rule: the CI system, the repository or project path, and the rights and the scope of the tokens. Set the ref too: a branch name, for example
main, or a full ref, for examplerefs/tags/v1.2.0. A branch name never takes a tag with the same name. With no ref, the rule takes each branch, tag, and pull request of the repository that can get an OIDC token. - Change the job as below.
PORTAL_URLis the address of your workspace, with no slash at the end, for examplehttps://acme.example.com. The audience of the OIDC token must be this address.
GitHub Actions:
permissions:
id-token: write
contents: read
steps:
- name: Sign in to the portal
env:
PORTAL_URL: https://acme.example.com
run: |
oidc=$(curl -sS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=$PORTAL_URL" | jq -r .value)
token=$(jq -n --arg t "$oidc" '{token: $t}' | curl -sS --fail-with-body -X POST \
--data-binary @- "$PORTAL_URL/securechain/v1/ci-token" | jq -r .access_token)
echo "::add-mask::$token"
echo "SECURECHAIN_PORTAL_TOKEN=$token" >> "$GITHUB_ENV"
GitLab CI:
scan:
variables:
PORTAL_URL: https://acme.example.com
id_tokens:
PORTAL_ID_TOKEN:
aud: https://acme.example.com
script:
- export SECURECHAIN_PORTAL_TOKEN=$(jq -n --arg t "$PORTAL_ID_TOKEN" '{token: $t}' |
curl -sS --fail-with-body -X POST --data-binary @- "$PORTAL_URL/securechain/v1/ci-token" |
jq -r .access_token)
- The portal answers 401 with one text for each cause: it does not tell a caller which rules exist. Check the audience, the repository, and the ref. An admin of a self-hosted install can read the cause in the log of the portal.
- An answer 503 means that the portal could not read the keys of the CI system. Try again after a minute.
- When you remove a rule, its live tokens stop at once. The tokens of a rule are not on Access tokens.
- The token of the sign-in lives one hour. A job that runs for longer signs in again.
Choose the gate #
| Command | The job fails when | Use it on |
|---|---|---|
securechain sca | Never. It only reports. | Any branch, to keep the portal current. |
securechain sca --fail-on high | An open finding is high or critical. The other levels are critical, medium, and low. | The default branch and the release branches. |
securechain sca --fail-on-exploited | An open finding is known exploited, at any severity. It combines with --fail-on: either gate fails the job. CLI 0.1.80 or later. | The default branch and the release branches. |
securechain compare --base main --fail-on-new | The branch has an open finding that the latest scan of main does not have. | Pull requests and merge requests. See Pull and merge requests. |
securechain check | A project that uses TuxCare builds is not in the state that harden made. | Projects that use TuxCare builds. See Fix vulnerabilities. |
- Skip what your team ignored.
--baseline-portalmakesscaskip the findings that your team ignored in the portal. To turn it on for every run, setbaseline.portal: truein.securechain.yaml. - The gate on known-exploited findings. A finding is known exploited when the portal has a claim for it from CISA KEV, CVE ADP, or EUVD. A finding that your team ignored in the portal under
--baseline-portal, a finding that a TuxCare build fixes, a finding with no fix planned or a live-patched finding (a CLI release after 0.1.84), and a dev finding under--ignore-devdo not fail this gate. When no finding that can fail the gate is known exploited and the run did not read a part of the project,sca --fail-on-exploitedalone fails the job, as--fail-ondoes (CLI 0.1.86 or later): exit code 5 with the fix of each part, or exit code 2 when the part is a gap of securechain (see A part of the project that the run did not read). CLI 0.1.82 to 0.1.85 printed a warning and exited with code 0.comparehas no--fail-on-exploited: the summary of the portal does not count the new known-exploited findings. Runsca --fail-on-exploitedbesidecompare --fail-on-new. To turn the gate on for every run, setfail_on_exploited: truein.securechain.yaml. - A part of the project that the run did not read. From CLI 0.1.86,
sca --fail-onfails the job when no finding reached the level and the run could not read a part of the project. The job exits with code 5 when you can fix the part: the message names each part and its fix, for example a lockfile that does not parse. It exits with code 2 when the part is a gap of securechain, for example a file that securechain does not support: report it to TuxCare. A finding at the level still gives exit code 1. To keep exit code 0 until 2027-12-01, give--accept-unread all, or setaccept_unread: allin.securechain.yaml. Each run that uses it prints a deprecation warning. Fix each part that the report names before that date. Plainscawith no gate does not change: it exits with code 0. - A portal that does not answer. The job then exits with code 4.
--on-portal-error warnmakes it go on with a warning. Use it when a build must never wait for the portal.
Pull and merge requests #
A pull request often inherits old findings from the base branch. compare --fail-on-new fails it only on the findings that it adds. The base branch needs a scan of its own, so scan the base branch on each push too.
A Gradle or Maven project runs its build. Each command that reads a Gradle or Maven project runs the build of the project: its build scripts and ./gradlew, or the files and extensions of .mvn/ of Maven. This holds with and without --run-project-hooks, and no option turns it off. So a scan of a merge request from a fork, or of other code that you do not trust yet, runs that code. Run such a scan in a sandbox job that holds no secret that the code must not read.
GitHub Actions:
name: securechain
on:
push:
branches: [main]
pull_request:
jobs:
securechain:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci # the install step of your project
- name: Scan the branch
if: github.event_name == 'push'
uses: TuxCare/securechain-action@v0
with:
command: sca
args: --fail-on high
portal-token: ${{ secrets.SECURECHAIN_PORTAL_TOKEN }}
registry-login: none
- name: Fail on new findings
if: github.event_name == 'pull_request'
uses: TuxCare/securechain-action@v0
with:
command: compare
args: --base ${{ github.base_ref }} --fail-on-new
portal-token: ${{ secrets.SECURECHAIN_PORTAL_TOKEN }}
registry-login: none
GitLab CI:
securechain:
image: node:22
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- npm ci # the install step of your project
- curl -fsSL https://securechain.tuxcare.com/get/securechain | sh
- securechain sca --fail-on high
securechain-new-findings:
image: node:22
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
script:
- npm ci
- curl -fsSL https://securechain.tuxcare.com/get/securechain | sh
- securechain compare --base "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" --fail-on-new
The merge request job runs on the source branch. If SECURECHAIN_PORTAL_TOKEN is Protected, GitLab gives it to protected branches only, and the job stops with exit code 3. Keep Protected on that variable when its token has wider rights than one repository, or when an admin saved a registry token in the workspace (see below): every branch could then read it. Choose one of these two ways.
- A second token for the merge request job (recommended when the workspace has no registry token). Mint the CI token (one repository) preset for this repository (pick a token). Save it in a variable
SECURECHAIN_MR_TOKEN: Masked, not Protected. Only the merge request job reads it. The protected token stays for the default branch. - Run the merge request job for protected source branches only. The protected token then works, but a merge request from an ordinary feature branch gets no check. Use this rule:
- if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_COMMIT_REF_PROTECTED == "true". In a merge request pipeline,CI_COMMIT_REF_PROTECTEDtests the source branch.
Here is the job for the first way:
securechain-new-findings:
image: node:22
variables:
SECURECHAIN_PORTAL_TOKEN: $SECURECHAIN_MR_TOKEN
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
script:
- npm ci
- curl -fsSL https://securechain.tuxcare.com/get/securechain | sh
- securechain compare --base "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" --fail-on-new
The risk: every branch of the project can read an unprotected variable. Anyone who can push a branch can print the token. The token can upload scans and read the findings of this one repository only. It cannot read other repositories, and it expires in 90 days at most. Mint a new one before it expires.
The token can also get the registry token of the workspace. Every token with the submit permission can get it, also a token of one repository. So anyone who can push a branch to the project can get the registry token from this variable. When an admin saved a registry token in the workspace, use the second way, or ask an admin before you use the first way. The Audit log records the reads of the registry token through the CLI: one row a day for each token and network.
Jenkins (multibranch pipeline): CHANGE_TARGET is set for a pull request build.
sh '''
npm ci # the install step of your project
curl -fsSL https://securechain.tuxcare.com/get/securechain | sh
export PATH="$HOME/.local/bin:$PATH"
if [ -n "$CHANGE_TARGET" ]; then
securechain compare --base "$CHANGE_TARGET" --fail-on-new
else
securechain sca --fail-on high
fi
'''
The portal files the scan of a pull request under the branch that it comes from. The portal sends alerts for the default branch and the supported releases only, so a feature branch sends none.
Python projects #
A pip job must install the requirements, or the CLI finds no packages. Each job ends with securechain sca --fail-on high. Use the install line of your project in place of the one shown.
uv (GitLab CI):
securechain:
image: ghcr.io/astral-sh/uv:python3.12-bookworm
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- uv sync --frozen
- curl -fsSL https://securechain.tuxcare.com/get/securechain | sh
- securechain sca --fail-on high
The line uv sync --frozen is optional. The CLI reads the packages from uv.lock, so the findings are the same without it. The line installs the packages into .venv, and the CLI reads from .venv the version ranges that each package sets for its dependencies. Without .venv, the CLI assumes a range that admits every version. Then, in the CLI report, the WHERE cell of a fix that rests on such a range ends in (range assumed)
, and the fix that it names can fail when the real range is narrower. On a large project, the install can download several hundred MB on each run. Remove the line to save the download, or keep it to get exact WHERE cells.
WHERE names the file that you change to move the package to the fixed version, not the file that holds the version. For a package that comes in through another package, this is often the override section of your package manager, for example pyproject.toml (tool.uv.override-dependencies)
, while the version is in uv.lock. add
, as in pyproject.toml (add tool.uv.override-dependencies)
, means that the section does not exist in the file yet: the fix adds it (from the CLI release after version 0.1.72).
Poetry (GitLab CI):
securechain:
image: python:3.12
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- pip install poetry
- poetry install --no-root
- curl -fsSL https://securechain.tuxcare.com/get/securechain | sh
- securechain sca --fail-on high
pip (GitLab CI):
securechain:
image: python:3.12
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- python -m venv .venv
- .venv/bin/pip install -r requirements.txt
- curl -fsSL https://securechain.tuxcare.com/get/securechain | sh
- securechain sca --fail-on high
- The pip job needs CLI 0.1.34 or later. That release reads the
.venvdirectory of the project whenVIRTUAL_ENVis not set. An earlier release finds no packages unless the job activates the environment. With an earlier release, add. .venv/bin/activatebeforesecurechain sca, or setVIRTUAL_ENV: "$CI_PROJECT_DIR/.venv"invariables. - CLI 0.1.34 or later also reads a requirements file as a lockfile when every requirement is pinned with
==and has a--hash. An--index-urlline does not change this. A fully hashed file other than the rootrequirements.txt, for exampledev-requirements.txt, is a lockfile in each run. An earlier release reads such a file only with--declared, as the requirements of the project and not as the installed versions. - When pip finds an environment, the CLI reads the installed packages from it, and not the root
requirements.txt. When pip finds no environment and the rootrequirements.txtis fully hashed, the CLI reads that file as a lockfile. With CLI 0.1.66 or later,securechain scathen exits 0 or 1 by its findings, so the job passes with the setting allow exit code 1 only. The CLI prints this note:No virtual environment: securechain read the hashed requirements.txt. Create the environment to scan what is installed.
An earlier release reads the file and prints the report, but exits with code 5, and the job fails. - Keep the virtual environment and the
pip installlines also for a fully hashed file: the CLI then reads the packages that are installed. The environment can hold packages that the file does not name, for example a package that a person installed by hand or that another requirements file installed. - When pip finds no environment and the root
requirements.txtis not fully hashed, or when there is no rootrequirements.txt,securechain scaexits with code 5, and the job fails. It also exits with code 5 when pip fails for another reason, for example when it finds an environment and cannot list its packages.
The installed environment comes before a lockfile #
From CLI 0.1.75, the installed Python environment is the source of the scan. The scan then shows what the image or the job holds, not what a lockfile recorded at an earlier time. An earlier release reads the lockfile first. Your results can change after the upgrade when the project has both an environment and a lockfile: the report shows the versions of the environment, and no package that only the lockfile names.
- The CLI finds an environment in
VIRTUAL_ENV, inCONDA_PREFIX, or in the.venvdirectory of the project. A.venvcounts only when the machine that runs the scan made it: itspythonlinks to a Python outside the project, the project is in a git work tree, and git tracks no file of the.venv. In other cases, for example in a copy of the project with no.git, setVIRTUAL_ENV. When the CLI finds an environment and the project has arequirements.txtor arequirements.in, the CLI reads the environment. It does not read theuv.lock,poetry.lock,pdm.lock, orPipfile.lockbeside it, and uv, Poetry, PDM, and Pipenv need not be installed. A warning names each lockfile that the CLI did not read. When thepythonof the environment does not start, the CLI reads the lockfile in its place. This is also true with--ecosystem pip: each package is counted once, and you do not need--exclude uv.lock. - When the CLI finds no environment, nothing changes: the tool of the lockfile reads it, as before.
- A project with only a
pyproject.tomland auv.lock,poetry.lock, orpdm.lockstays with the tool of the lockfile, also with an environment.hardenmust change the settings of the tool that installs the build. When the tool is uv and uv is not installed, the CLI reads the environment. The CLI also warns where the lockfile and the environment disagree: see A lockfile that disagrees with the installed environment. - An environment with no pip, for example one that
python -m venv --without-piporuv venvmakes, works: the CLI reads the metadata of the installed packages. An earlier release exits with code 5. - When the CLI read the environment, a
pyproject.toml,setup.py, or requirements file of a sub-project iscovered by the environment
when the environment holds the sub-project or each package that the file lists. The report does not show it under NOT ANALYSED. A file that names no package and no project is not covered. When the environment holds neither the sub-project nor each listed package, the file stays under NOT ANALYSED, and the report names the packages that the environment does not hold. - Pipenv: when the job runs
pipenv install --system, Pipenv makes no environment of its own, andpipenv graphfails. The CLI then reads the installed environment (VIRTUAL_ENV,CONDA_PREFIX, or the.venvdirectory of the project). With no environment, the CLI readsPipfile.lock, exits with code 5, and the report says that the scan is partial, fromPipfile.lock. The portal marks the scan partial. --excludechanges only the report of one run. The scan on the portal still holds the files that it leaves out. To leave a file out of the scan too, list it underignore:in.securechain.yaml(see the configuration file).
A lockfile that disagrees with the installed environment #
The project with only a pyproject.toml and a uv.lock, poetry.lock, or pdm.lock is the one exception to the rule above: it keeps the tool of the lockfile, also with an installed environment, because no requirements file says that pip installs the environment. The findings of uv and Poetry then come from the lockfile, and a stale lockfile reports versions that the environment does not hold. From CLI 0.1.75, sca, check, and harden compare the lockfile with the installed packages and warn where the two disagree.
- When a warning comes. Only when the CLI finds an installed environment:
VIRTUAL_ENV,CONDA_PREFIX, or the.venvof the project, as above. With no environment, nothing is compared and no warning comes. - A version that differs. One warning for each package that the two sides hold at different versions. It names the package, the version of the lockfile, the installed version, and the environment, and it gives the command that syncs them:
uv sync,poetry sync(poetry install --syncbefore Poetry 2.0), orpdm sync --clean. Two spellings of one version agree. Withpdm.lock, a hardened install is no disagreement: an installed1.4.5+tuxcare.1agrees with a locked1.4.5. - A package that only one side holds. One warning lists the packages that only the lockfile holds, and one lists the packages that only the environment holds, each with its version. pip, setuptools, and wheel make the environment and count only where the lockfile names them. A package that the lockfile marks for another platform, for example
colorama; sys_platform == 'win32'on Linux, is on no list. A package installed in editable mode or from a directory is the project's own code and is not compared. - When the environment has no
site-packagesdirectory, for example only Debian'sdist-packages, one warning says that the run did not compare. A warning also names eachsite-packagesdirectory and each package's metadata (.dist-infoor.egg-info) that the CLI cannot read.
Go projects #
The CLI matches the Go standard library against the Go version of the build. In CI, state the toolchain with --go-toolchain 1.24.13 or with the variable SECURECHAIN_GO_TOOLCHAIN=1.24.13 (CLI 0.1.34 or later). The commands securechain sca, check, harden, update, status, and compare all read the variable. Without it, the CLI uses go env GOVERSION, then the toolchain line of go.mod, then the go line. An earlier release has no flag and no variable: it uses go env GOVERSION, then the go line of go.mod. With an earlier release, run the job in an image that holds the Go toolchain of your build, for example golang:1.24.13.
securechain:
image: golang:1.24
variables:
SECURECHAIN_GO_TOOLCHAIN: "1.24.13"
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- curl -fsSL https://securechain.tuxcare.com/get/securechain | sh
- securechain sca --fail-on high
Record a release #
A release is the list of packages of a build that you ship. The portal keeps it, checks it against each new advisory, and can export it as a VEX file. Record it from the job that builds the release:
securechain sca --release "$CI_COMMIT_TAG" # GitLab
securechain sca --release "$GITHUB_REF_NAME" # GitHub, on a tag push
securechain sca --release "$TAG_NAME" # Jenkins, with tag discovery on
- Scan the same commit and the same lockfile that you ship.
- An empty name stops the job with exit code 2. So a tag variable that is not set cannot make a release with no name.
- A release does not change. The same name from another commit stops the job with exit code 2.
--force-releasereplaces the release, and the portal records the replacement. --product-release "Acme 3.2"also adds the release to a product release. The repository must be in exactly one product, else the job stops with exit code 2.
With the GitHub action, pass the name in args, for example args: --release ${{ github.ref_name }}. The action gives args to the CLI as a list of words, with no shell. Use the action of release 0.1.19 or later.
More than one project in a repository #
- Run the CLI in each project directory, or give the directory with
--dir path/to/project. The portal keeps the scans of each directory and each set of package managers apart. It calls each one a slot. - The CLI finds the package managers by itself. To choose them, use
--ecosystem npm(repeat it for more), or setecosystemsin.securechain.yaml. - To leave a path out of the scan, for example test fixtures, add it to
ignorein.securechain.yaml. - The repository page of the portal shows a Slot list when a branch has two or more slots. The repository list counts the findings of all the slots.
Send the results to other tools #
| Format | Command | For |
|---|---|---|
| SARIF | securechain sca --output sarif > securechain.sarif | GitHub code scanning and other code-scanning tools. |
| JSON | securechain sca --output json > findings.json | Your own scripts. The JSON format is stable and has a schema_version. |
| Markdown | securechain sca --output markdown > findings.md | A comment on a pull request, or a job summary. |
| JUnit | securechain check --output junit > report.xml | The test report of GitLab and Jenkins. |
To upload the SARIF file to GitHub code scanning, add this step after the scan. The job needs the permission security-events: write:
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: securechain.sarif
To read the findings from a dashboard, use the REST API.
Pin the CLI version #
The install script with no SECURECHAIN_VERSION installs the promoted release: a new release reaches it 48 hours after its tag (since release 0.1.86; see When a new release reaches you). To run a new release at once, or the same release in each job, pin it:
| Install method | How to pin |
|---|---|
| Install script | Set SECURECHAIN_VERSION=v0.1.22 before the script. |
| GitHub Action | Pin the action tag, or set the input version. |
| GitLab CI/CD component | Pin the component version, or set the input version. |
| Docker | Use a version tag, for example tuxcare/securechain:0.1.22, not latest. |
When the CLI is 75 or more releases behind the newest release, it prints a warning. The warning does not change the exit code. A CI job that updates the CLI each month sees no warning. When it is older than the portal accepts, it stops and tells you to upgrade. See Upgrade the CLI.
Exit codes in CI #
| Code | Meaning | What to do |
|---|---|---|
| 0 | The run finished. No finding failed the gate. | Nothing. |
| 1 | A finding failed the gate. | Read the findings in the job log or in the portal. |
| 2 | A wrong option, a configuration file that the CLI cannot read, no package manager that it knows, or an --ecosystem or ecosystems name that it does not know (from version 0.1.71; the message lists the names). | Read the message. |
| 3 | No token, or the portal refused it. | Check the secret variable. See Troubleshooting. |
| 4 | The portal did not answer in time, or its answer failed a check. | Run the job again, or use --on-portal-error warn. |
| 5 | A tool on your side failed: the install command of your package manager, a project environment that the CLI cannot find, or a file that the CLI cannot write. | Read the output of the tool in the job log. |
| 6 | harden, update, unharden, or fix changed the project, but the installed packages are not the planned ones. The CLI put your files back. | Read the message. Contact TuxCare if the error stays. |
| 7 | An error in the CLI. The CLI prints the path of a report file. | Send the report to TuxCare. |
The install script never exits 1 (from version 0.1.71). It exits 5 when you must fix something on your side: the operating system or the CPU type, a directory that it cannot write, no answer from the server, or a download that failed. It exits 7 when the TuxCare release is wrong: no version, no checksum line, or a checksum that does not match. So exit_codes: [1] below lets only findings through, and a failed install fails the job. An older install script exits 1 when it fails: pin version 0.1.71 or later, or end the install line with || exit 5.
To let a job report findings and not stop the pipeline, allow exit code 1 only. In GitLab, write allow_failure: exit_codes: [1]. Do not write allow_failure: true: it also hides the codes 2 to 7, where the CLI could not do its job, and the pipeline then shows green for a scan that did not run.
The install command of your project also exits 1 when it fails, for example npm ci or pip install. With exit_codes: [1], the job then shows amber for a scan that did not run. End each install line with || exit 5, as in the example below. The CI/CD component does this for its install input from the CLI release after version 0.1.72: it prints the code of the install and exits 5.
GitLab Runner 19.0 and earlier can report exit code 1 in place of the real code, so exit_codes: [1] also lets a code from 2 to 7 pass. Set the variable FF_USE_NEW_BASH_EVAL_STRATEGY: "true" in the job: the runner then reports the real code. Runner 19.1 and later always reports the real code and ignores the variable.
securechain:
image: node:22
variables:
FF_USE_NEW_BASH_EVAL_STRATEGY: "true" # GitLab Runner 19.0 and earlier
allow_failure:
exit_codes: [1]
script:
- npm ci || exit 5 # the install step of your project
- curl -fsSL https://securechain.tuxcare.com/get/securechain | sh
- securechain sca --fail-on high
Exit codes lists every code.
Next: Portal