Skip to content

Shell script dependency pinning

Supply-chain controls for packages and downloads inside GitLab CI job scripts. Packaged as shell-pinning.feature with IDs GLCI-SHELL-PIN-001GLCI-SHELL-PIN-010.

Run the full pack:

gitlab-compliance shell-check -p .gitlab-ci.yml

Or with check:

gitlab-compliance check -p .gitlab-ci.yml --with-shell-check

Package managers

Each manager uses the shared step Then package installs of type "{manager}" must use pinned versions. The Gherkin below matches the bundled policies. Bad/good snippets show what pass and fail.

apk (GLCI-SHELL-PIN-004)

# METADATA
# title: apk packages must be version-pinned
# custom:
#   id: GLCI-SHELL-PIN-004
#   severity: MEDIUM
Scenario: apk add must pin package versions
  Given I have any job with effective script defined
  Then package installs of type "apk" must use pinned versions

Bad:

demo:
  script:
    - apk add curl
    - apk add --no-cache curl=8.5.0-r0 wget

Good:

demo:
  script:
    - apk add --no-cache curl=8.5.0-r0
    - apk add --virtual .build-deps gcc=13.2.1-r0 musl-dev=1.2.4-r0

apt / apt-get (GLCI-SHELL-PIN-005)

# METADATA
# title: apt packages must be version-pinned
# custom:
#   id: GLCI-SHELL-PIN-005
#   severity: MEDIUM
Scenario: apt-get install must pin package versions
  Given I have any job with effective script defined
  Then package installs of type "apt" must use pinned versions

Bad:

demo:
  script:
    - apt-get install curl
    - apt install curl=7.88.1-10 wget

Good:

demo:
  script:
    - apt-get install -y curl=7.88.1-10
    - apt install --no-install-recommends wget=1.21.3-1

yum / dnf / microdnf (GLCI-SHELL-PIN-010)

# METADATA
# title: yum/dnf packages must be version-pinned
# description: Applies to yum, dnf, and microdnf install commands in job scripts.
# custom:
#   id: GLCI-SHELL-PIN-010
#   severity: MEDIUM
Scenario: yum and dnf install must pin package versions
  Given I have any job with effective script defined
  Then package installs of type "yum" must use pinned versions

Bad:

demo:
  script:
    - yum install curl
    - dnf install python3-requests
    - yum install curl-7.76.1-23.el9 wget

Good:

demo:
  script:
    - yum install curl-7.76.1-23.el9
    - dnf install -y curl-7.76.1
    - microdnf install curl-7.76.1-23.el9.x86_64
    - dnf install --enablerepo=epel pkg-1.0.0

pip / pip3 (GLCI-SHELL-PIN-003)

# METADATA
# title: pip packages must be version-pinned
# description: Applies to both pip and pip3 install commands in job scripts.
# custom:
#   id: GLCI-SHELL-PIN-003
#   severity: MEDIUM
Scenario: pip install must pin package versions
  Given I have any job with effective script defined
  Then package installs of type "pip" must use pinned versions

Bad:

demo:
  script:
    - pip install requests
    - pip3 install pkg@latest
    - pip install requests==2.32.0 flask

Good:

demo:
  script:
    - pip3 install "requests==2.32.0"
    - pip install -r requirements.txt
    - pip install --index-url https://pypi.org/simple "gitlab-compliance==2.1.1"

npm / yarn (GLCI-SHELL-PIN-006)

# METADATA
# title: npm global packages must be version-pinned
# custom:
#   id: GLCI-SHELL-PIN-006
#   severity: MEDIUM
Scenario: npm global installs must pin package versions
  Given I have any job with effective script defined
  Then package installs of type "npm" must use pinned versions

Bad:

demo:
  script:
    - npm install -g cowsay
    - npm install -g @scope/pkg
    - yarn global add lodash

Good:

demo:
  script:
    - npm install -g cowsay@1.5.0
    - npm install -g @scope/pkg@1.0.0
    - yarn global add lodash@4.17.21

go (GLCI-SHELL-PIN-007)

# METADATA
# title: go install must pin versions
# custom:
#   id: GLCI-SHELL-PIN-007
#   severity: MEDIUM
Scenario: go install must pin module versions
  Given I have any job with effective script defined
  Then package installs of type "go" must use pinned versions

Bad:

demo:
  script:
    - go install example.com/cmd
    - go install example.com/cmd@latest

Good:

demo:
  script:
    - go install example.com/cmd@v1.2.3

These are part of the same pack but are not OS/language package managers:

Downloads and remote pipes

Scenario: Script downloads must verify checksums
  Given I have any job with effective script defined
  Then script downloads must verify checksums

Scenario: Scripts must not pipe remote downloads to a shell
  Given I have any job with effective script defined
  Then untrusted remote scripts must not be executed

git clone (GLCI-SHELL-PIN-008)

Scenario: git clone must verify commit or tag
  Given I have any job with effective script defined
  Then git clone must verify commit or tag

docker run / pull / create (GLCI-SHELL-PIN-009)

# METADATA
# title: docker run and pull must pin images
# custom:
#   id: GLCI-SHELL-PIN-009
#   severity: MEDIUM
Scenario: docker run and pull must pin container images
  Given I have any job with effective script defined
  Then docker commands must pin container images to a tag or sha256 digest

Bad: docker pull nginx, docker run --rm nginx

Good: docker pull python:3.12.0, docker run --rm python@sha256:…

Custom policies

Reuse the same steps in your own features:

# METADATA
# title: Org apt packages must be pinned
# custom:
#   id: GLCI-ORG-APT-001
#   severity: HIGH
Feature: Org apt pinning

  Scenario: apt installs must pin versions
    Given I have any job with effective script defined
    Then package installs of type "apt" must use pinned versions
gitlab-compliance check -f policies/ -p .gitlab-ci.yml

See also Shell check and shell-check reference.

Back to Examples.