> ## 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.

# Azure Pipelines

> Generate a CycloneDX SBOM with Syft in Azure DevOps and send it to Pharus with curl, with the token kept as a secret variable.

<Info>
  **Plans:** Every plan. **Who:** an owner or admin creates the API token; anyone who can edit the pipeline adds the steps.
</Info>

These two steps install Syft, generate a CycloneDX JSON SBOM of the repository and send it to Pharus. They are the same steps as the **Azure DevOps** tab of **Connect a pipeline** on the **SBOMs** page.

## 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 secret variable">
    In Azure DevOps, edit the pipeline, select **Variables**, and add a variable named `PHARUS_TOKEN` with the token as its value. Tick **Keep this value secret**. A variable group works too.
  </Step>
</Steps>

<Note>
  Azure Pipelines doesn't pass secret variables to scripts by itself. The `env:` block on the second step maps `PHARUS_TOKEN` into the script; keep it.
</Note>

## The pipeline

A complete `azure-pipelines.yml` that runs on every push to `main`. The `name:` line sets the build number, which becomes the product version in Pharus.

```yaml theme={null}
# azure-pipelines.yml
# Add PHARUS_TOKEN as a secret pipeline variable. Set name: to your product version (e.g. 4.2.$(Rev:r)).
name: 4.2.$(Rev:r)

trigger:
  branches:
    include:
      - main

pool:
  vmImage: ubuntu-latest

steps:
  - script: |
      curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b "$(Agent.TempDirectory)"
      "$(Agent.TempDirectory)/syft" dir:. -o cyclonedx-json=sbom.cdx.json
    displayName: Generate SBOM

  - script: |
      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-Source: azure_devops" \
        -H "X-Pharus-Version: $(Build.BuildNumber)" \
        -H "X-Pharus-Commit: $(Build.SourceVersion)" \
        -H "X-Pharus-Branch: $(Build.SourceBranch)" \
        -H "X-Pharus-Repo: $(Build.Repository.Name)" \
        -H "X-Pharus-Run-Url: $(System.CollectionUri)$(System.TeamProject)/_build/results?buildId=$(Build.BuildId)" \
        --data-binary @-
    displayName: Send SBOM to Pharus
    env:
      PHARUS_TOKEN: $(PHARUS_TOKEN)
```

To add Pharus to an existing pipeline, copy only the two steps, and set `name:` to your product version.

What it does:

* **Generate SBOM** installs Syft into the agent's temporary directory and writes `sbom.cdx.json` from the checked-out repository.
* **Send SBOM to Pharus** gzips the file and posts it to [`POST /api/v1/sboms`](/api-reference/sboms). Azure DevOps fills in the `$(…)` values before the script runs. Pharus removes `refs/heads/` from the branch.
* `--fail-with-body` fails the step on any error and prints Pharus' message in the log.

## Variations

<AccordionGroup>
  <Accordion title="Send SPDX instead of CycloneDX">
    Use `-o spdx-json=sbom.spdx.json` in the first step and gzip `sbom.spdx.json` in the second. Pharus reads SPDX JSON 2.2 and 2.3.
  </Accordion>

  <Accordion title="Scan a container image">
    Replace `dir:.` with the image reference, for example `myregistry.azurecr.io/gateway-g3:4.2`. The agent must be able to pull the image.
  </Accordion>

  <Accordion title="Send the release version instead of the build number">
    Each build number, such as `4.2.17`, is its own product version in Pharus, so every build adds an active SBOM to the family. If you build often, put your release version in `X-Pharus-Version` instead, for example from a variable such as `$(productVersion)`. A new SBOM for a version replaces the previous SBOM of that version. You can also retire old SBOMs on their page in Pharus.
  </Accordion>
</AccordionGroup>

## Check that it worked

The **Send SBOM to Pharus** 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** with **Azure DevOps** as its **Source**, and the token's **Last used** time changes under **Settings → API tokens**.

| Status | Fix |
| - | - |
| 401 | `PHARUS_TOKEN` is missing, wrong, revoked or expired, or the `env:` mapping is gone |
| 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="GitLab CI" icon="git-merge" href="/api-reference/ci/gitlab-ci" />
</CardGroup>


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