Gitlab security jobs in Merge Request pipelines

2 minute read

Gitlab provides an extensive set of built-in security templates that we can add to our CI/CD pipelines in order to perform defensive checks on our code. As one of the pipelines I use started taking too long to complete (due to the number of jobs defined in it), I started refactoring it so that all security jobs would run in a separate Merge Request pipeline, while my “regular” jobs would keep running in the usual branch pipeline.

While doing so, I encountered a problem: security jobs simply wouldn’t run in the merge_request pipeline (the pipeline wouldn’t show any job and fail). Digging into the templates' code, I figured out that this is because of their rules definition and that there is a not-yet-implemented improvement in Gitlab’s issue tracker to address this limitation.

Knowing that, it’s easy to change the jobs' behaviour to fit our needs by overriding their rules definition, as in the following YAML snippet. (note: I’m only including a couple of security templates, for demonstration purposes).

# .security.gitlab-ci.yaml

include:
  - template: Security/SAST.gitlab-ci.yml
  - template: Security/License-Scanning.gitlab-ci.yml
  - template: Security/Secret-Detection.gitlab-ci.yml

stages:
  - test

gosec-sast:
  artifacts:
    reports:
      sast: gl-sast-report.json
    paths: [gl-sast-report.json]
  rules:
    - if: $CI_MERGE_REQUEST_IID
      when: always

license_scanning:
  rules:
    - if: $CI_MERGE_REQUEST_IID
      when: always

secret_detection:
  rules:
    - if: $CI_MERGE_REQUEST_IID
      when: always

The $CI_MERGE_REQUEST_IID is a built-in variable that is only available in merge requests, so the rules read as “only run this job when we’re in a merge request”; we unfortunately have to redefine every single job that we want to use; we save this smaller pipeline to a file named .security.gitlab-ci.yaml (the actual name is of course up to you).

As a side note, when overriding job elements, Gitlab behaves in different ways according to the element type:

  • when we override an array (such as rules), the whole array will be replaced, thus removing any previously-defined elements in the final job definition;
  • when we override a map (such as variables), its elements will be merged with any existing ones.

Now we only need to instruct the branch pipeline to trigger the security pipeline when we’re in a merge request (and whenever we’re in the main branch, to be thorough in our checks), which we can do as follows:

# .gitlab-ci.yaml

stages:
  - security
  # - other stages as needed by your pipeline ...

variables:
  SECURE_LOG_LEVEL: "info"

security:
  stage: security
  trigger:
    include: .security.gitlab-ci.yaml
    strategy: depend
  rules:
    - if: $CI_PIPELINE_SOURCE == 'merge_request_event'
      when: always
    - if: $CI_COMMIT_REF_NAME == 'main'
      when: always

# other jobs ...

From now on, whenever we commit to a branch that belongs to a merge request or to the main branch, we’ll see two pipelines being triggered at the same time.

Important note: in order to ensure that code can only be merged to main when all pipelines (including the security pipeline) succeed, we have to remember to enable the Pipelines must succeed option in the repository settings (General -> Merge requests -> Merge checks, at the time of writing).