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

# GitLab CI

> Generate a CycloneDX SBOM with Syft for every Git tag in GitLab CI/CD and send it to Pharus with curl.

<Info>
  **Plans:** Every plan. **Who:** an owner or admin creates the API token; a project maintainer adds the CI/CD variable and the job.
</Info>

This job runs when you push a Git tag. It installs Syft, generates a CycloneDX JSON SBOM of the repository and sends it to Pharus, with the tag as the product version.

## Before you start

<Steps>
  <Step title="Create a token">
    In Pharus, go to **Settings → API tokens** and create a token for the product family. See [create a token](/api-reference/introduction#create-a-token).
  </Step>

  <Step title="Store it as a CI/CD variable">
    In GitLab, open the project's **Settings → CI/CD → Variables** and add a variable with the key `PHARUS_TOKEN` and the token as its value. Mask it, so it doesn't show in job logs.
  </Step>
</Steps>

<Note>
  If you also mark the variable as protected, GitLab only passes it to pipelines on protected branches and tags. Protect the tags the job runs on, or leave the variable unprotected.
</Note>

## The job

Add this job to `.gitlab-ci.yml`:

```yaml theme={null}
# .gitlab-ci.yml
pharus-sbom:
  stage: deploy
  image: alpine:3.22
  rules:
    - if: $CI_COMMIT_TAG
  script:
    - apk add --no-cache curl
    - curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
    - syft dir:. -o cyclonedx-json=sbom.cdx.json
    - |
      gzip -c sbom.cdx.json | curl --fail-with-body -sS -X POST "https://app.pharussecurity.com/api/v1/sboms" \
        -H "Authorization: Bearer $PHARUS_TOKEN" \
        -H "Content-Type: application/json" \
        -H "Content-Encoding: gzip" \
        -H "X-Pharus-Version: $CI_COMMIT_TAG" \
        -H "X-Pharus-Commit: $CI_COMMIT_SHA" \
        -H "X-Pharus-Branch: $CI_COMMIT_REF_NAME" \
        -H "X-Pharus-Repo: $CI_PROJECT_PATH" \
        -H "X-Pharus-Run-Url: $CI_JOB_URL" \
        --data-binary @-
```

What it does:

* **Trigger**: `rules: - if: $CI_COMMIT_TAG` runs the job only in pipelines for a Git tag.
* **Generate**: the job installs curl and Syft in a small Alpine image, and Syft writes `sbom.cdx.json` from the repository.
* **Version**: the tag, such as `v4.2`, becomes the product version in Pharus.
* **Send**: the last command gzips the file and posts it to [`POST /api/v1/sboms`](/api-reference/sboms). The `X-Pharus-*` headers fill in the **Pipeline** card on the SBOM's page, with a link to the job.
* **Fail loudly**: `--fail-with-body` fails the job on any error and prints Pharus' message in the log.

The job uses the `deploy` stage, one of GitLab's default stages. If your `.gitlab-ci.yml` lists its own `stages`, use one of those.

<Note>
  Pharus has no source label for GitLab, so these SBOMs show **API** as their **Source**. The **Pipeline** card still links to the GitLab job.
</Note>

## Variations

<AccordionGroup>
  <Accordion title="Send SPDX instead of CycloneDX">
    Use `syft dir:. -o spdx-json=sbom.spdx.json` and gzip `sbom.spdx.json` in the last command. Pharus reads SPDX JSON 2.2 and 2.3.
  </Accordion>

  <Accordion title="Scan a container image">
    Replace `dir:.` with the image reference, for example `registry.gitlab.com/acme/gateway-g3:4.2`. For a private registry, give Syft credentials as Syft's documentation describes.
  </Accordion>

  <Accordion title="Run on the default branch instead of tags">
    Use `rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH`, and send your release version in `X-Pharus-Version` in place of `$CI_COMMIT_TAG`. Without a version, Pharus uses the version inside the SBOM, if it has one.
  </Accordion>
</AccordionGroup>

## Check that it worked

The job log shows Pharus' answer, with `"status":"parsed"` for a new SBOM and `"duplicate":true` when Pharus already had the file. In Pharus, the SBOM appears under **SBOMs**, and the token's **Last used** time changes under **Settings → API tokens**.

| Status | Fix |
| - | - |
| 401 | `PHARUS_TOKEN` is missing or wrong (check protected variables), or the token was revoked or expired |
| 402 | The workspace is read-only; an owner subscribes under **Settings → Billing** |
| 413 | The SBOM is too large, even gzipped |
| 415 | The file isn't CycloneDX JSON 1.4–1.7 or SPDX JSON 2.2–2.3 |

All codes are listed under [errors](/api-reference/sboms#errors).

## Related

<CardGroup cols={2}>
  <Card title="Upload an SBOM" icon="upload" href="/api-reference/sboms" />

  <Card title="API introduction" icon="key-round" href="/api-reference/introduction" />

  <Card title="GitHub Actions" icon="git-branch" href="/api-reference/ci/github-actions" />

  <Card title="Azure Pipelines" icon="cloud" href="/api-reference/ci/azure-pipelines" />
</CardGroup>


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