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.
The documentation
detailing the implementation indicates that Dependency Scanning should be
enabled, then add the appropriate license approval policies, and the policies
are enforced. Unfortunately, the Dependency Scanning job rules
are 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 #
So, another tool must be used to generate the SBOM. GitLab simply expects a CycloneDX formatted SBOM, a well established standard. The chosen solution here is the Syft application from Anchore. Syft is designed to create SBOMs from a host of different ecosystems, scanning either container images or file systems. It includes support for C/C++, Go, Java, JavaScript, Python, Ruby, Rust, Swift, and others. See the documentation on supported ecosystems for complete details. There are many other tools available, but Syft seemed to provide the most comprehensive single solution, and in tests consistently produced a valid CycloneDX SBOM with or without dependencies.
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 results in a log like the following because
GitLab requires a shell to execute the script
(see
supported shells):
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, 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 previously, 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
Dependency Scanning is be allowed to do its 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, 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 triggers the job (this is important later), then a standard set of rules used in GitLab security scans:
- An option for a variable
DEPENDENCY_SCANNING_DISABLED
to disable the Dependency Scanning job - An option to disable Gemnasium via the
DS_EXCLUDED_ANALYZERS
variable - Otherwise, if
dependency_scanning
is an available feature (theGITLAB_FEATURES
variable 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 previously, with overridden rules
to 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 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).