> ## Documentation Index
> Fetch the complete documentation index at: https://docs.scanoss.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Scan every pull request in CI

> Scan the files each pull request adds or changes with scanoss-cli, and fail the pull request when it brings in open source you have not approved.

## Goal

Run a SCANOSS scan on every pull request. Fail the check when the change contains open source
code that your project's `scanoss.json` does not approve yet, and keep the full result as a build
artefact.

## When to use it

* You want to catch copied or vendored open source code before anyone merges it.
* You already have a CI system and want SCANOSS as one more check in it.
* You approve components through code review of `scanoss.json` (see
  [Approve components with scanoss.json](/en/latest/developer-tools/recipes/approve-components-bom-rules)).

To also fail on licences or vulnerabilities, add the
[licence and vulnerability gate](/en/latest/developer-tools/recipes/licence-and-vulnerability-gate)
to the same job.

## Prerequisites

* A Linux x86-64 CI runner with `bash`, `git`, `curl`, `jq`, and `sha256sum`
* `SCANOSS_API_KEY` available to the job as a secret environment variable
* The pull request's target branch available in the checkout, with enough history to find the
  merge base (a full clone is simplest)
* `scanoss/unapproved.jq` committed to the repository (see [Approve components](/en/latest/developer-tools/recipes/approve-components-bom-rules#how-it-fails-the-pipeline))

## The script

Save this as `ci/scanoss-pr.sh` in your repository and make it executable
(`chmod +x ci/scanoss-pr.sh`).

```bash theme={null}
#!/usr/bin/env bash
# Scan a pull request with scanoss-cli and fail on unapproved open source.
#
# Environment:
#   SCANOSS_API_KEY      required, from your CI secret store
#   BASE_REF             the branch the pull request targets, e.g. origin/main
#   SCAN_MODE            "changed" (default): scan only added/modified files
#                        "full": scan the whole repository
#   SCANOSS_CLI_VERSION  scanoss-cli release to install (default v0.9.0)
set -euo pipefail

BASE_REF="${BASE_REF:-origin/main}"
SCAN_MODE="${SCAN_MODE:-changed}"
SCANOSS_CLI_VERSION="${SCANOSS_CLI_VERSION:-v0.9.0}"
RESULTS="${RESULTS:-scanoss-results.json}"

# 1. Install scanoss-cli outside the repository, so it is not scanned itself.
tools_dir="$(mktemp -d)"
archive="scanoss-cli-linux-amd64.tar.gz"
release_url="https://github.com/scanoss/scanoss.go/releases/download/${SCANOSS_CLI_VERSION}"
(
  cd "$tools_dir"
  curl -sSLO "${release_url}/${archive}"
  curl -sSLO "${release_url}/checksums.txt"
  sha256sum -c --ignore-missing checksums.txt
  tar xzf "$archive" scanoss-cli
)
export PATH="${tools_dir}:${PATH}"
scanoss-cli --version

# 2. Choose what to scan.
target="."
if [ "$SCAN_MODE" = "changed" ]; then
  # Copy the added, copied, modified, and renamed files into a temporary
  # directory, keeping their paths.
  target="$(mktemp -d)"
  git diff --name-only -z --diff-filter=ACMR "${BASE_REF}...HEAD" \
    | git checkout-index --prefix="${target}/" -z --stdin
  if [ -z "$(ls -A "$target")" ]; then
    echo "No added or modified files to scan."
    exit 0
  fi
fi

# 3. Use the repository's scanoss.json, if there is one.
settings_flag=""
if [ -f scanoss.json ]; then
  settings_flag="--settings=scanoss.json"
fi

# 4. Scan. Exits 1 if the scan itself fails.
scanoss-cli scan "$target" \
  --include licenses,vulns \
  ${settings_flag:+"$settings_flag"} \
  --output "$RESULTS"

# 5. Gate: every detected component must be approved in scanoss.json.
#    scanoss/unapproved.jq is the filter from the "Approve components" recipe.
rules=scanoss.json; [ -f "$rules" ] || rules=/dev/null
unapproved="$(jq -c --slurpfile rules "$rules" -f scanoss/unapproved.jq "$RESULTS" \
  | jq -r '"\(.purl)@\(.version // "unknown")  in: \([.evidence[]?.path] | join(", "))"')"

if [ -n "$unapproved" ]; then
  echo "Open source components that are not approved in scanoss.json:"
  echo "$unapproved"
  exit 2
fi
echo "No unapproved open source components."
```

## What each step does

1. **Install.** Downloads a pinned scanoss-cli release, checks it against the published
   `checksums.txt`, and puts it on the `PATH`. It goes into a temporary directory so a full scan
   does not fingerprint the binary.
2. **Choose the target.** In `changed` mode, `git diff` lists the files the pull request adds or
   modifies compared to the merge base with `BASE_REF`. `git checkout-index` copies exactly those
   files into a temporary directory with the same relative paths. The script leaves out deleted
   files, because they cannot bring in new code. In `full` mode the script scans the whole
   repository, which suits small repositories or a nightly job.
3. **Settings.** When the script scans the changed-files directory, the CLI cannot find the
   repository's `scanoss.json` by itself, so the script passes it with `--settings`. Its BOM rules
   (approved components) and skip rules then apply. Because the copied files keep their relative
   paths, rules scoped to a `path` still match.
4. **Scan.** The CLI fingerprints the files locally, uploads only the fingerprints, waits for the
   result, and writes the raw inventory to `scanoss-results.json`. `--include licenses,vulns`
   adds licence and vulnerability data, so the same file can feed the
   [licence and vulnerability gate](/en/latest/developer-tools/recipes/licence-and-vulnerability-gate).
5. **Gate.** The [`unapproved.jq` filter](/en/latest/developer-tools/recipes/approve-components-bom-rules#how-it-fails-the-pipeline)
   lists every component detected in the code that `scanoss.json` doesn't approve. When a rule
   pins a version, it approves only that version. Any line in the list fails the check.

## What the output means

A failing run prints one line per unapproved component:

```
Open source components that are not approved in scanoss.json:
pkg:github/madler/zlib@1.2.11  in: third_party/zlib/inflate.c, third_party/zlib/inflate.h
```

For each line, the reviewer decides:

* **The component is acceptable.** Add it to `bom.identify` in `scanoss.json` in the same pull
  request, so reviewers see the change to `scanoss.json` in code review.
* **The match is not real**, for example your own code that resembles a public project. Add a
  `bom.ignore` rule for that component, scoped to the path if you can.
* **The code should not be there.** Remove it from the pull request.

See [Approve components with scanoss.json](/en/latest/developer-tools/recipes/approve-components-bom-rules)
for each rule.

## How it fails the pipeline

| Exit code | Meaning |
| - | - |
| `0` | Scan completed, and every detected component is approved, or nothing changed |
| `1` | The scan or the install failed. Check the API key, network access, and the log. |
| `2` | The scan completed and found unapproved components |

scanoss-cli itself always exits `0` after a successful scan, whatever it finds. The `jq` gate in
step 5 turns findings into a failure.

## Run it in your CI system

All the logic is in the script. Your CI configuration only needs to check out the code with history,
expose the secret, set `BASE_REF`, call the script, and keep `scanoss-results.json`. These two
examples show the pattern. The same steps work in any CI system.

<Tabs>
  <Tab title="GitHub Actions">
    ```yaml theme={null}
    # .github/workflows/scanoss.yml
    name: SCANOSS
    on: pull_request
    jobs:
      scan:
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v4
            with:
              fetch-depth: 0
          - name: Scan changed files
            env:
              SCANOSS_API_KEY: ${{ secrets.SCANOSS_API_KEY }}
              BASE_REF: origin/${{ github.base_ref }}
            run: ./ci/scanoss-pr.sh
          - uses: actions/upload-artifact@v4
            if: always()
            with:
              name: scanoss-results
              path: scanoss-results.json
              if-no-files-found: ignore
    ```
  </Tab>

  <Tab title="GitLab CI">
    ```yaml theme={null}
    # .gitlab-ci.yml
    scanoss:
      image: debian:stable-slim
      rules:
        - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      variables:
        GIT_DEPTH: "0"
        BASE_REF: origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME
      before_script:
        - apt-get update && apt-get install -y --no-install-recommends bash ca-certificates curl git jq
        - git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
      script:
        - ./ci/scanoss-pr.sh
      artifacts:
        when: always
        paths:
          - scanoss-results.json
    ```

    Define `SCANOSS_API_KEY` as a masked CI/CD variable.
  </Tab>
</Tabs>

## Variations

* **Scan the whole repository on a schedule.** Run the same script with `SCAN_MODE=full` from a
  nightly job. A full scan also re-checks files that did not change, for example after you
  remove an approval from `scanoss.json`.
* **Use the container image instead of downloading the binary.** Replace step 1 with
  `docker run --rm -v "$PWD:/src" -e SCANOSS_API_KEY ghcr.io/scanoss/scanoss:<version> scan /src ...`.
  See [Using Docker](/en/latest/cli/scanoss-go/using-docker) for the output-permission caveat.
* **Speed up large scans.** Raise `--threads`, the number of fingerprint workers (default `10`).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.