Commit 25b2e441 authored by Guilhem Bonnefille's avatar Guilhem Bonnefille
Browse files

Merge branch 'initial' into 'main'

Template for Dependency Track (based on CLI tool)

See merge request to-be-continuous/dependency-track!4
parents ee30d0c5 ea9d205f
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -12,7 +12,7 @@ include:
    ref: '3.4.0'
    file: '/templates/gitlab-ci-bash.yml'
  - project: 'to-be-continuous/semantic-release'
    ref: '3.8.0'
    ref: '3.9.0'
    file: '/templates/gitlab-ci-semrel.yml'    

stages:
+121 −19
Original line number Diff line number Diff line
# GitLab CI template for Dependency Track

This project implements a GitLab CI/CD template to collect and send SBOM reports to a [Dependency Track](https://dependencytrack.org/) server.
This project implements a GitLab CI/CD template to collect and send your SBOM reports to
a [Dependency Track](https://dependencytrack.org/) server.

## Usage

This template can be used both as a [CI/CD component](https://docs.gitlab.com/ee/ci/components/#use-a-component-in-a-cicd-configuration)
This template can be used both as
a [CI/CD component](https://docs.gitlab.com/ee/ci/components/#use-a-component-in-a-cicd-configuration)
or using the legacy [`include:project`](https://docs.gitlab.com/ee/ci/yaml/index.html#includeproject) syntax.

### Use as a CI/CD component
@@ -18,7 +20,7 @@ include:
    # 2: set/override component inputs
    inputs:
      # ⚠ this is only an example
      build-args: "build --with-my-args"
      base-api-url: "https://dependency-track.my-company.org/api"
```

### Use as a CI/CD template (legacy)
@@ -35,35 +37,135 @@ include:
variables:
  # 2: set/override template variables
  # ⚠ this is only an example
  DEPTRACK_BUILD_ARGS: "build --with-my-args"
  DEPTRACK_BASE_API_URL: "https://dependency-track.my-company.org/api"
```

## Understanding the Dependency Track template

The template will be triggered at each pipeline execution on your production branch (`main` or `master` by default) and each release pipeline (upon semver tag creation).

It will scan for all SBOM file in your project structure and will upload them to the configured Dependency Track server.\
:information_source: SBOM files should have been generated in the upstream pipeline with appropriate tools, and propagated as build artifacts. Most to-be-continuous templates already support - whenever possible - a job to produce a SBOM report.

### API Key permissions

In order to operate, this template needs a Dependency Track API Key.
Here are some details about mandatory and optional permissions used by the template:

| Permission                                  | Required?     | Explaination                                                                                                                                                                                                                                             |
| ------------------------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `BOM_UPLOAD`                                | **mandatory** | Required to publish SBOM files to the Dependency Track server                                                                                                                                                                                            |
| `PROJECT_CREATION_UPLOAD`                   | **optional**  | This is required if you want to automatically create the project while uploading the SBOM files when the project does not exist (**but the parent project must exist**)                                                                                  |
| `VIEW_PORTFOLIO` and `PORTFOLIO_MANAGEMENT` | **optional**  | Required if you want to automatically create one or several project ancestors prior to uploading the SBOM files.<br/>Granting those permissions is not recommended in the general case as they virtually give administration rights to the API Key owner |

### Project Path

Whenever a SBOM file is found, the template uploads it to the Dependency Track server under a certain project.\
The target project is determined by evaluating the `project-path` input / `$DEPTRACK_PROJECT_PATH` variable (see [configuration chapter](#configuration)).

The project path is a sequence of elements separated by double slashes `//` (the separator is also configurable with the `path-separator` input / `$DEPTRACK_PATH_SEPARATOR` variable).\
Each element is expected to be one of the following:

1. `#11111111-1111-1111-1111-111111111111`: a project [Universally Unique Identifier (UUID)](https://en.wikipedia.org/wiki/Universally_unique_identifier) (starting with a hash `#`)
2. `project-name@version`: a **project name** and a **version** (separated with a `@`)
3. `project-name`: a **project name** only (empty version)

Lastly, the project path supports some **expressions**, that will be dynamically replaced when being evaluated:

| Expression       | Description                                                                                                                                                |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `{file_prefix}`  | SBOM filename prefix (before the first dot).<br/>Ex: when processing the file `reports/docker-sbom.cyclonedx.json`, `{file_prefix}` will be `docker-sbom`. |
| `{sbom_name}`    | `Metadata > Component > Name` info extracted from the SBOM file (json or xml)                                                                              |
| `{sbom_version}` | `Metadata > Component > Version` info extracted from the SBOM file (json or xml)                                                                           |
| `{sbom_type}`    | `Metadata > Component > Type` info extracted from the SBOM file (json or xml)                                                                              |

#### Default project path

By default, the Dependency Track project path is set to `$CI_PROJECT_NAMESPACE//$CI_PROJECT_PATH-{file_prefix}@$CI_COMMIT_REF_NAME` (with path separator `//`).

That means:

- The project structure in Dependency Track will always be two levels deep: 
    - a _root_ project bearing the name of the GitLab project namespace, 
    - and _leaf_ projects (hosting SBOM files) bearing the full path of the GitLab project as a name, suffixed with the 
      SBOM file prefix and with project version matching either the Git branch name or the Git tag name (depending on the 
      kind of pipeline that originated the SBOM file).
- If the API key has only the `BOM_UPLOAD` permission, then all the projects hierarchy must pre-exist down to the leaf 
  sub-projects.
- If the API key has the extra `PROJECT_CREATION_UPLOAD` permission, then only the root project must pre-exist, the template
  will automatically create the leaf projects if not found.
- If the API key has the extra `VIEW_PORTFOLIO` and `PORTFOLIO_MANAGEMENT` permissions (not recommanded), then the entire 
  project hierarchy will be automatically created by the template if it doesn't exist.

Example: Let's imagine a GitLab project located in `acme-program/acme-services/acme-user-api` with 2 SBOM files generated by the pipeline:

- `py-sbom.cyclonedx.json`: the SBOM of the Python application implementation, with version `1.1.0`
- `docker-sbom.cyclonedx.json`: the SBOM of the container image, with version `main`

Lastly, let's suppose the project production branch is `main`, and 2 software versions have been released so far: `1.0.0` and `1.1.0`.

Then the corresponding Dependency Track project structure will be:

```
| Project name                                              | Version |
| --------------------------------------------------------- | ------- |
| 📂 acme-program/acme-services                             |         |
| ├─📄 acme-program/acme-services/acme-user-api-py-sbom     | main    |
| ├─📄 acme-program/acme-services/acme-user-api-py-sbom     | 1.0.0   |
| ├─📄 acme-program/acme-services/acme-user-api-py-sbom     | 1.1.0   |
| ├─📄 acme-program/acme-services/acme-user-api-docker-sbom | main    |
| ├─📄 acme-program/acme-services/acme-user-api-docker-sbom | 1.0.0   |
| ├─📄 acme-program/acme-services/acme-user-api-docker-sbom | 1.1.0   |
| ...
```

> :bulb: this default configuration handles the Dependency Track constraint that each project name has to be globally 
> unique in the server.

#### Other use cases

The default template behavior can be changed by overridding the `project-path` input / `$DEPTRACK_PROJECT_PATH` variable.

Examples:

- `#550e8400-e29b-41d4-a716-446655440000`: every SBOM found will be published to the project with UUID `550e8400-e29b-41d4-a716-446655440000`<br/>
  :information_source: as Dependency Track is only able to store one SBOM per project, this configuration is suitable only if exactly one SBOM file is found (otherwise each one will overwrite the previous one)
- `my-project@v1.1.0`: every SBOM found will be published to the project with name `my-project` and version `v1.1.0`<br/>
  :information_source: depending on your API key permissions, `sbom-scanner` might try to automatically create the project if it doesn't exist<br/>
  :information_source: as in the previous example, this configuration is suitable only if exactly one SBOM file is found
- `#550e8400-e29b-41d4-a716-446655440000/my-project-{file_prefix}@{sbom_version}`: every SBOM found will be published to a project named `my-project-{file_prefix}` and version `{sbom_version}` (extracted from the SBOM file),
  direct child of project with UUID `550e8400-e29b-41d4-a716-446655440000`<br/>
  :information_source: depending on your API key permissions, `sbom-scanner` might try to automatically create the project if it doesn't exist
- `acme-program@v2/acme-services@v1.3/acme-user-api@v1.3/acme-user-api-{file_prefix}`: complete project path only defined by project names and versions<br/>
  :information_source: depending on your API key permissions, `sbom-scanner` might try to automatically create the project and its ancestors if they don't exist

## Configuration

The Dependency Track template uses the following configuration.

| Input / Variable                                     | Description                                                                                                              | Default value                                                       |
| ---------------------------------------------- | ------------------------------------------------------------------------------------ | --------------------------------------------------- |
| `base-image` / `DEPTRACK_BASE_IMAGE`           | The Docker image used to upload SBOM reports                                         | `registry.hub.docker.com/badouralix/curl-jq:latest` |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- |
| `sbom-scanner-image` / `DEPTRACK_SBOM_SCANNER_IMAGE` | The container image with [Dependency Track SBOM Scanner](https://gitlab.com/to-be-continuous/tools/dt-sbom-scanner) tool | `registry.gitlab.com/to-be-continuous/tools/dt-sbom-scanner:latest` |
| `base-api-url` / `DEPTRACK_BASE_API_URL`             | Dependency Track server base API url (includes `/api`)                                                                   | _none_ (required)                                                   |
| :lock: `DEPTRACK_API_KEY`                            | Dependency Track API key                                                                                                 | _none_ (required)                                                   |
| `project-uuid` / `DEPTRACK_PROJECT_UUID`       | Dependency Track project UUID                                                        | _none_                                              |
| `auto-create` / `DEPTRACK_AUTO_CREATE`         | Determines whether the project should be automatically created when it doesn't exist | `true`                                              |
| `project-name` / `DEPTRACK_PROJECT_NAME`       | Dependency Track project name                                                        | `$CI_PROJECT_NAME`                                  |
| `project-version` / `DEPTRACK_PROJECT_VERSION` | Dependency Track project version                                                     | _none_                                              |
| `parent-name` / `DEPTRACK_PARENT_NAME`         | Dependency Track parent name (optional)                                              | _none_                                              |
| `parent-version` / `DEPTRACK_PARENT_VERSION`   | Dependency Track parent version (optional)                                           | _none_                                              |
| `project-path` / `DEPTRACK_PROJECT_PATH`             | Dependency Track target project path to publish SBOM files to                                                            | `$CI_PROJECT_NAMESPACE//$CI_PROJECT_PATH-{file_prefix}@$CI_COMMIT_REF_NAME`    |
| `path-separator` / `DEPTRACK_PATH_SEPARATOR`         | Separator to use in project path                                                                                         | `//`                                                                 |
| `sbom-patterns` / `DEPTRACK_SBOM_PATTERNS`           | SBOM file patterns to publish (supports glob patterns)                                                                   | `**/*.cyclonedx.json **/*.cyclonedx.xml`                            |

### Secrets management

Here are some advices about your **secrets** (variables marked with a :lock:):

1. Manage them as [project or group CI/CD variables](https://docs.gitlab.com/ee/ci/variables/#create-a-custom-variable-in-the-ui):
   - [**masked**](https://docs.gitlab.com/ee/ci/variables/#mask-a-custom-variable) to prevent them from being inadvertently
1. Manage them
   as [project or group CI/CD variables](https://docs.gitlab.com/ee/ci/variables/#create-a-custom-variable-in-the-ui):
   - [**masked**](https://docs.gitlab.com/ee/ci/variables/#mask-a-custom-variable) to prevent them from being
     inadvertently
     displayed in your job logs,
   - [**protected**](https://docs.gitlab.com/ee/ci/variables/#protect-a-custom-variable) if you want to secure some secrets
   - [**protected**](https://docs.gitlab.com/ee/ci/variables/#protect-a-custom-variable) if you want to secure some
     secrets
     you don't want everyone in the project to have access to (for instance production secrets).
2. In case a secret contains [characters that prevent it from being masked](https://docs.gitlab.com/ee/ci/variables/#masked-variable-requirements),
2. In case a secret
   contains [characters that prevent it from being masked](https://docs.gitlab.com/ee/ci/variables/#masked-variable-requirements),
   simply define its value as the [Base64](https://en.wikipedia.org/wiki/Base64) encoded value prefixed with `@b64@`:
   it will then be possible to mask it and the template will automatically decode it prior to using it.
3. Don't forget to escape special characters (ex: `$` -> `$$`).
+14 −28
Original line number Diff line number Diff line

{
  "name": "Dependency Track",
  "description": "Identify and reduce risk in your software supply chain with [Dependency Track](https://dependencytrack.org/)",
@@ -9,50 +8,37 @@
  "is_component": true,
  "variables": [
    {
      "name": "DEPTRACK_BASE_IMAGE",
      "description": "The Docker image used to upload SBOM reports",
      "default": "registry.hub.docker.com/badouralix/curl-jq:latest",
      "name": "DEPTRACK_SBOM_SCANNER_IMAGE",
      "description": "The container image with [Dependency Track SBOM Scanner](https://gitlab.com/to-be-continuous/tools/dt-sbom-scanner) tool",
      "default": "registry.gitlab.com/to-be-continuous/tools/dt-sbom-scanner:latest",
      "advanced": true
    },
    {
      "name": "DEPTRACK_BASE_API_URL",
      "description": "Dependency Track server base API url (includes `/api`)",
      "mandatory": true,
      "type": "url"
    },
    {
      "name": "DEPTRACK_PROJECT_UUID",
      "description": "Dependency Track project UUID"
    },
    {
      "name": "DEPTRACK_API_KEY",
      "description": "Dependency Track API key",
      "mandatory": true,
      "secret": true
    },
    {
      "name": "DEPTRACK_AUTO_CREATE",
      "description": "Determines whether the project should be automatically created when it doesn't exist",
      "type": "boolean",
      "default": "true",
      "advanced": true
    },
    {
      "name": "DEPTRACK_PROJECT_NAME",
      "description": "Dependency Track project name",
      "default": "$CI_PROJECT_NAME"
      "name": "DEPTRACK_PROJECT_PATH",
      "description": "Dependency Track target project path to publish SBOM files to",
      "default": "$CI_PROJECT_NAMESPACE//$CI_PROJECT_PATH-{file_prefix}@$CI_COMMIT_REF_NAME"
    },
    {
      "name": "DEPTRACK_PROJECT_VERSION",
      "description": "Dependency Track project version"
    },
    {
      "name": "DEPTRACK_PARENT_NAME",
      "description": "Dependency Track parent name"
      "name": "DEPTRACK_PATH_SEPARATOR",
      "description": "Separator to use in project path",
      "default": "//",
      "advanced": true
    },
    {
      "name": "DEPTRACK_PARENT_VERSION",
      "description": "Dependency Track parent version"
      "name": "DEPTRACK_SBOM_PATTERNS",
      "description": "SBOM file patterns to publish (supports glob patterns)",
      "default": "**/*.cyclonedx.json **/*.cyclonedx.xml",
      "advanced": true
    }
  ]
}
+48 −81
Original line number Diff line number Diff line
@@ -15,31 +15,21 @@
# =========================================================================================
spec:
  inputs:
    base-image:
      description: The Docker image used to upload SBOM reports
      default: registry.hub.docker.com/badouralix/curl-jq:latest
    sbom-scanner-image:
      description: The container image with [Dependency Track SBOM Scanner](https://gitlab.com/to-be-continuous/tools/dt-sbom-scanner) tool
      default: registry.gitlab.com/to-be-continuous/tools/dt-sbom-scanner:latest
    base-api-url:
      description: Dependency Track server base API url (includes `/api`)
      default: ''
    project-uuid:
      description: Dependency Track project UUID
      default: ''
    auto-create:
      description: Determines whether the project should be automatically created when it doesn't exist
      type: boolean
      default: true
    project-name:
      description: Dependency Track project name
      default: $CI_PROJECT_NAME
    project-version:
      description: Dependency Track project version
      default: ''
    parent-name:
      description: Dependency Track parent name
      default: ''
    parent-version:
      description: Dependency Track parent version
      default: ''
    project-path:
      description: Dependency Track target project path to publish SBOM files to
      default: '$CI_PROJECT_NAMESPACE//$CI_PROJECT_PATH-{file_prefix}@$CI_COMMIT_REF_NAME'
    path-separator:
      description: Separator to use in project path
      default: '//'
    sbom-patterns:
      description: SBOM file patterns to publish (supports glob patterns)
      default: '**/*.cyclonedx.json **/*.cyclonedx.xml'
---
# default workflow rules: Merge Request pipelines
workflow:
@@ -70,19 +60,18 @@ variables:
  # variabilized tracking image
  TBC_TRACKING_IMAGE: registry.gitlab.com/to-be-continuous/tools/tracking:master

  DEPTRACK_BASE_IMAGE: $[[ inputs.base-image ]]
  DEPTRACK_SBOM_SCANNER_IMAGE: $[[ inputs.sbom-scanner-image ]]
  DEPTRACK_BASE_API_URL: $[[ inputs.base-api-url ]]
  DEPTRACK_AUTO_CREATE: $[[ inputs.auto-create ]]
  DEPTRACK_PROJECT_UUID: $[[ inputs.project-uuid ]]
  DEPTRACK_PROJECT_NAME: $[[ inputs.project-name ]]
  DEPTRACK_PROJECT_VERSION: $[[ inputs.project-version ]]
  DEPTRACK_PARENT_NAME: $[[ inputs.parent-name ]]
  DEPTRACK_PARENT_VERSION: $[[ inputs.parent-version ]]
  DEPTRACK_PROJECT_PATH: $[[ inputs.project-path ]]
  DEPTRACK_PATH_SEPARATOR: $[[ inputs.path-separator ]]
  DEPTRACK_SBOM_PATTERNS: $[[ inputs.sbom-patterns ]]

  # default production ref name (pattern)
  PROD_REF: '/^(master|main)$/'
  # default integration ref name (pattern)
  INTEG_REF: '/^develop$/'
  # default release tag name (pattern)
  RELEASE_REF: '/^v?[0-9]+\.[0-9]+\.[0-9]+$/'

stages:
  - build
@@ -98,6 +87,7 @@ stages:

.deptrack-scripts: &deptrack-scripts |
  # BEGSCRIPT

  set -e

  function log_info() {
@@ -132,13 +122,13 @@ stages:
      return
    fi

    # List of typical bundles
    if command -v apt-get > /dev/null; then
      apt-get update && apt-get install --no-install-recommends -y ca-certificates
    fi
    
    # List of supported bundles
    bundles="/etc/ssl/certs/ca-certificates.crt" # Debian/Ubuntu/Gentoo etc.
    bundles="${bundles} /etc/ssl/cert.pem"       # Alpine Linux
    bundles="${bundles} /etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"  # CentOS/RHEL 7
    bundles="${bundles} /etc/pki/tls/certs/ca-bundle.crt"                   # Fedora/RHEL 6
    bundles="${bundles} /etc/ssl/ca-bundle.pem"                             # OpenSUSE
    bundles="${bundles} /etc/pki/tls/cacert.pem"                            # OpenELEC

    # Try to find the right bundle to update it with custom CA certificates
    for bundle in ${bundles}
@@ -148,7 +138,8 @@ stages:
      then
        # Import certificates in bundle
        echo "${certs}" | tr -d '\r' >> "${bundle}"

        # also install for Python
        export REQUESTS_CA_BUNDLE=${bundle}
        log_info "Custom CA certificates imported in \\e[33;1m${bundle}\\e[0m"
        ca_imported=1
        break
@@ -314,33 +305,6 @@ stages:
    done
  }

  function exec_hook() {
    if [[ ! -x "$1" ]] && ! chmod +x "$1"
    then
      log_warn "... could not make \\e[33;1m${1}\\e[0m executable: please do it (chmod +x)"
      # fallback technique
      sh "$1"
    else
      "$1"
    fi
  }

  function collect_sboms() {
    # collect SBOM reports generated upstream
    # TODO
    # see: https://docs.dependencytrack.org/usage/cicd/
    curl -X "POST" "$DEPTRACK_BASE_API_URL/v1/bom" \
      -H 'Content-Type: multipart/form-data' \
      -H "X-Api-Key: $DEPTRACK_API_KEY" \
      ${DEPTRACK_PROJECT_UUID:+-F "project=$DEPTRACK_PROJECT_UUID"} \
      -F "autoCreate=$DEPTRACK_AUTO_CREATE" \
      -F "projectName=$DEPTRACK_PROJECT_NAME" \
      ${DEPTRACK_PROJECT_VERSION:+-F "projectVersion=$DEPTRACK_PROJECT_VERSION"} \
      ${DEPTRACK_PARENT_NAME:+-F "parentName=$DEPTRACK_PARENT_NAME"} \
      ${DEPTRACK_PARENT_VERSION:+-F "parentVersion=$DEPTRACK_PARENT_VERSION"} \
      -F "bom=@target/bom.xml"
  }

  unscope_variables
  eval_all_secrets

@@ -348,16 +312,19 @@ stages:

dependency-track:
  image:
    name: $DEPTRACK_BASE_IMAGE
    name: $DEPTRACK_SBOM_SCANNER_IMAGE
    entrypoint: [""]
  services:
    - name: "$TBC_TRACKING_IMAGE"
      command: ["--service", "dependency-track", "1.0.0"]
  stage: .post
  script:
  before_script:
    - !reference [.deptrack-scripts]
    - collect_sboms
    - install_ca_certs "${CUSTOM_CA_CERTS:-$DEFAULT_CA_CERTS}"
  script:
    - sbom-scanner
  rules:
    # on production branch
    # on production branch: auto
    - if: '$CI_COMMIT_REF_NAME =~ $PROD_REF'
      when: always
    # on tag with release pattern: auto
    - if: '$CI_COMMIT_TAG =~ $RELEASE_REF'