Managing GitLab License Policies With No Dependencies
GitLab's license approval policies provide a powerful and flexible means of managing dependency license approvals. Using these policies requires performing GitLab's Dependency Scanning to generate and report a Software Bill of Materials (SBOM). For the cases where there are no dependencies, Dependency Scanning is not run, no SBOM is created, and the license policies are considered failed and require approval. For projects without dependencies this is an added hassle, and this post proposes a solution.
The Solution for No Dependencies #
In GitLab 16.4, license approval policies were added as an enhancement to the
previous License Scanning template.
detailing the implementation indicates that Dependency Scanning should be
enabled, then add the appropriate license approval policies, and the policies
will be enforced. Unfortunately, the Dependency Scanning job
optimized to only run when there are dependency lockfiles present, so when they
are not it does not run, no SBOM is created, and the license approval policies
fail and require approval even though there are no dependencies. Not the end of
the world, but a hassle.
The solution for these cases is simple, and is actually captured in the GitLab
documentation - ensure that a CycloneDX SBOM is created in some job and provided
to GitLab as
reports:cyclonedx. This then triggers license scanning using the
contents of the SBOM.
This post covers non container-based projects, otherwise Container Scanning should be run to provide an SBOM.
Always Run Dependency Scanning? #
One option investigated is simply overriding the rules for the Dependency Scanning template to run on all pipelines. Theoretically this should produce an SBOM with no dependencies, which is valid. Unfortunately, attempts with multiple projects showed inconsistent results. In some cases the SBOM was created with no dependencies, as expected, but in others no SBOM was created and no errors were identified by Gemnasium (GitLab's Dependency Scanning tool). There was no obvious commonality for these two cases.
Syft to the Rescue #
There is a
Syft container images on Docker Hub,
but this image is built
from scratch and has no shell available (see the
Dockerfile). Using an
image with no shell in GitLab CI will result in a log like the following because
GitLab requires a shell to execute the
Executing "step_script" stage of the job script Using docker image sha256:c9b97dbf39ec0138c627c8bd3b56e5d879bc0b8304f05db9dde6db9daff6d129 for anchore/syft:v0.97.1 with digest anchore/syft@sha256:abc8d4310c54b56dd1e789d5f60b8ebc43f472652b34971d4b0d0dbed7f4ebda Cleaning up project directory and file based variables ERROR: Job failed (system failure): Error response from daemon: OCI runtime create failed: container_linux.go:380: starting container process caused: exec: "sh": executable file not found in $PATH: unknown (exec.go:78:0s)
So, a new Syft image was created that includes Syft in an Alpine Linux image so a shell is available. The details of this image are not addressed here, but feel free to post questions in the comments (or the project issues).
The GitLab CI Job #
First, we'll setup a job to simply run Syft and provide an SBOM:
syft_sbom: image: name: registry.gitlab.com/gitlab-ci-utils/container-images/syft:latest entrypoint: [''] script: - /syft/syft $CI_PROJECT_DIR -o cyclonedx-json=syft.cdx.json artifacts: paths: - syft.cdx.json reports: cyclonedx: - syft.cdx.json
This uses the new Syft image noted above, with the
entrypoint overridden to
nothing. Syft is executed in the
script with options to scan the current
directory and produce a CycloneDX formatted file. This is saved as an artifact
and uploaded as
reports:cyclonedx to satisfy the license approval policy
needs. This job could be used to always generate an SBOM, but in this example
we'll let Dependency Scanning do it's job where it can, and just use Syft to
fill in the cases with no dependencies.
To finalize the
rules for when Syft should be run, let's first examine the
Dependency Scanning template rules.
A subset is included below for the primary Gemnasium job.
.gemnasium-shared-rule: exists: - '**/Gemfile.lock' - '**/composer.lock' - '**/gems.locked' - '**/go.sum' - '**/npm-shrinkwrap.json' - '**/package-lock.json' - '**/yarn.lock' - '**/pnpm-lock.yaml' - '**/packages.lock.json' - '**/conan.lock' gemnasium-dependency_scanning: rules: - if: $DEPENDENCY_SCANNING_DISABLED == 'true' || $DEPENDENCY_SCANNING_DISABLED == '1' when: never - if: $DS_EXCLUDED_ANALYZERS =~ /gemnasium([^-]|$)/ when: never - if: $CI_COMMIT_BRANCH && $GITLAB_FEATURES =~ /\bdependency_scanning\b/ && $CI_GITLAB_FIPS_MODE == 'true' exists: !reference [.gemnasium-shared-rule, exists] variables: DS_IMAGE_SUFFIX: '-fips' DS_REMEDIATE: 'false' - if: $CI_COMMIT_BRANCH && $GITLAB_FEATURES =~ /\bdependency_scanning\b/ exists: !reference [.gemnasium-shared-rule, exists]
There's a rule template identifying the dependency lockfiles that will trigger the job (this will be important later), then a standard set of rules used in GitLab security scans:
- An option for a variable
DEPENDENCY_SCANNING_DISABLEDto disable the Dependency Scanning job
- An option to disable Gemnasium via the
- Otherwise, if
dependency_scanningis an available feature (the
GITLAB_FEATURESvariable is set by GitLab based on the license), execute the job if one of the available lockfiles exists (with both FIPS and non-FIPS variants)
There are similar jobs for the Maven and Python Gemnasium scanners that follow the same pattern.
Pulling this all together is a CI file that includes both the Dependency
Scanning template and the Syft job defined above, with overridden
only run in the appropriate cases.
include: - template: Dependency-Scanning.gitlab-ci.yml - local: /jobs/Syft.gitlab-ci.yml # Included from Syft.gitlab-ci.yml - the job defined above syft_sbom: rules: # If any of of these files exist, Gemnasium will be run and create an SBOM - exists: !reference [.gemnasium-shared-rule, exists] when: never # If any of of these files exist, Gemnasium-maven will be run and create an SBOM - exists: !reference [.gemnasium-maven-shared-rule, exists] when: never # If any of of these files exist, Gemnasium-python will be run and create an SBOM - exists: !reference [.gemnasium-python-shared-rule, exists] when: never # Otherwise, run Syft to create an SBOM, even if empty - if: $CI_COMMIT_BRANCH
In this case we'll use the
.gemnasium-shared-rule* templates defined in the
Dependency Scanning template and setup
rules to never run the Syft job if any
of them are present, then finally a rule to run on any other branch pipelines
(to match the GitLab template, although this could include tags, MRs, etc).