SCAP Security and Compliance Scanning of Docker Images in GitHub Actions and GitLab CI

SCAP scanning is a way to evaluate a system (hardware or software) for compliance with a standard. For example, SCAP can determine if a docker image complies with the PCI/DSS benchmark or, for those in the US federal space, many DoD (Department of Defense) projects require compliance against DISA STIGs (Security Technical Implementation Guides). Even in cases where such compliance isn’t strictly required, scanning against these security standards is beneficial as security hardening is always a good idea.

Performing SCAP scans has traditionally been a very manual process involving obscure tools, complex output requiring security specialists to interpret, and non-repeatable processes. Clearly, that approach is more waterfall than that rapid feedback, automate everything mindset of the modern Agile CI/CD-driven world. I set out to bring SCAP scanning to the fast feedback world by offering a way to run SCAP scans in GitHub Actions or GitLab CI with the output going directly into the familiar GitHub Advanced Security and GitLab Security Dashboard interfaces, respectively. Scan results can be sent to other tools, too, of course; for example, the SARIF generated by this approach can be used by Fortify Software Security Center using the Fortify SARIF Parser Plugin. I created these pipelines using well-supported, government-certified, free and open-source tooling. By lowering the barrier to entry, I’m hoping more projects can adopt SCAP scanning and improve the security baseline for everyone.

I already proved the value of this approach as, during the development and testing of these pipelines, I discovered some security issues, which I then reported and worked on fixing:

Skip to the bottom to see the GitHub Actions and GitLab CI pipeline implementations that you can add to your projects.

How It Works

First, what tool should do the actual SCAP scan? I set my requirements to run on Linux (so it can run in GitLab CI and GitHub Actions), free and open source, actively maintained, and scan docker images. OpenSCAP is the only tool that meets these requirements, and it also works great in my testing. OpenSCAP is NIST Certified, too.

Other tools I considered include SCC and Evaluate-STIG. SCC is unable to scan docker images and therefore isn’t usable for this effort. Evaluate-STIG is another option, but it’s not publicly available (and definitely is not free/open source), it’s written in PowerShell so it may be challenging to run on Linux, it requires .NET core to be installed on the image being scanned which may be very difficult or impossible, and it cannot scan docker images; therefore, this tool misses a number of key requirements for this effort.

Next, what benchmarks should the scanner evaluate the image against? There are basically two options: STIGS provided by DISA and Compliance as Code. The DISA benchmarks are not container-aware, so evaluating them against a docker image results in a lot of false positives for rules that do not apply to containers. For example, findings are reported for secure boot configuration and single-user mode. Compliance as Code benchmarks are container aware so such findings are (correctly) not reported. Compliance as Code benchmarks also detects prerequisite software for rules, further eliminating false positives (for example, the PAM configuration rules are only evaluated if PAM is installed). Another great benefit of Compliance as Code is that they are very responsive to issue reports and pull requests (I submitted 23 pull requests so far eliminating false positives), something that can’t be said of DISA. Another great feature of Compliance as Code is that it is a collaboration between United States Government agencies and commercial operating system vendors.

The next part is where a bit of complexity seeps in. OpenSCAP outputs XCCDF, but neither GitHub nor GitLab support the import of XCCDF. Therefore, OpenSCAP’s XCCDF output must be converted to an appropriate format: GitLab CodeQuality JSON or GitLab SAST JSON for GitLab, and SARIF for GitHub. However, there are no converters that go directly between XCCDF and any of those GitLab formats or SARIF. Therefore, multiple steps are required to achieve the conversion goal. The resulting pipeline with tools that perform the conversions is:

XCCDF —(Mitre’s SAF)—> Heimdall (HDF) —(Microsoft’s SARIF Multitool)—> SARIF —(Ignis SARIF Converter)—> GitLab JSON

Mitre’s SAF is used to consume XCCDF and output HDF (Heimdall Data Format). HDF seeks to be a common format for exchanging normalized security data between cybersecurity tools. OASIS is currently developing HDF as a standard. This tool is free and open-source software and is well supported by Mitre, an American not-for-profit organization.

Microsoft’s SARIF Multitool consumes the HDF and outputs SARIF. SARIF is an open standard that’s well supported by various tools. GitHub can directly consume SARIF, so this step concludes the GitHub Actions pipeline. This tool is free and open-source software and is well supported by Microsoft, a major vendor.

GitLab (at least currently) cannot consume SARIF so conversion is required. Ignis SARIF Converter converts SARIF to either GitLab CodeQuality JSON or GitLab SAST JSON. It’s maintained by a single individual, but the maintainer is responsive and dedicated. Hopefully, GitLab can support SARIF sooner rather than later, removing the need for this tool. I greatly appreciate this tool’s author doing the work to create, develop, and maintain this software, as otherwise full GitLab integration simply wouldn’t be possible.

The Journey of Getting It All to Work Together

Determining which tools to use and how to glue them all together was the easy part. Getting all the tools to work together, and fixing bugs in them, was the hard part. Along the way, I fixed many issues with most of the tools used. Below are the 14 pull requests and 6 issues I submitted broken down by the 5 projects:

All told this project took about 6 months from inception to completion. It was a really great journey, giving me exposure to a wide variety of tools, data formats, and concepts, but most importantly, introducing me to some great people too.

Implementations

By adding the following CI code to your project, you can “shift left” security scanning, enjoying all the benefits of early, easy-to-interpret results right from the comfort of the issue-tracking systems you already use.

These examples detect the base operating system by inspecting /etc/os-release in the image. If that file is not present, the image is likely a distroless image, so the scan fails because there is no platform available for distroless images. Based on the detected platform, an appropriate profile is selected. Then, the first available profile from a prioritized list of profiles (in this example, CIS Level 2 server, CIS, then standard) is selected. Those may not be the appropriate profiles for your use case – perhaps STIG, PCI, or something else is more appropriate for you. You may also not want to autodetect the platform or use a prioritized profile list; you may want to use a fixed baseline and a fixed profile. You’ll need to customize these implementations for your specific requirements.

The example scans only the operating system / platform; it does not scan any applications for compliance. For example, if you scan an Ubuntu 22.04 image that includes PostgreSQL, the scan will scan the Ubuntu 22.04 against the Ubuntu Linux 22.04 CIS Benchmark, but it won’t scan the PostgreSQL installation against the PostgreSQL CIS Benchmark.

The GitHub Actions Code

I developed, tested, and will continue to maintain this pipeline code in Jumpstart – so please see that project to see this code in action and see the latest version of it.

The result of this action is that issues will appear under “Security,” like this:

openscap is run from within an Alpine image. It would have been easier, with less code to develop and maintain, and potentially faster to run it from within the Ubuntu system that’s already setup. However, GitHub’s ubuntu-latest uses Ubuntu 22.04 which includes openscap version 1.2.16, which is too old and has significant problems that prevent its use for this purpose. openscap 1.38 (or later) is required for docker image scanning to work correctly. Therefore, Alpine Linux is used to run openscap, since it includes a later, usable version of openscap, and Alpine Linux is a small image that starts fast.

Here’s the GitHub Actions pipeline YAML to add to your project:

---
name: Build

permissions: {}

# Run this workflow every time a new commit pushed to your repository
on:  # yamllint disable-line rule:truthy
  - push

env:
  IMAGE_NAME: ghcr.io/${{ github.repository }}:${{ ( github.ref_name == github.event.repository.default_branch ) && 'latest' || format('ci-{0}', github.sha ) }}

jobs:
  build:
    permissions:
      packages: write
    runs-on: ubuntu-latest
    steps:
     # build and push a docker image to $IMAGE_NAME
  scap:
    needs: build
    permissions:
      security-events: write # for github/codeql-action/upload-sarif to upload SARIF results
      actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status
    runs-on: ubuntu-latest
    container:
      image: alpine:3.18.3@sha256:7144f7bab3d4c2648d7e59409f15ec52a18006a128c733fcff20d3a4a54ba44a
      env:
        SCAP_SECURITY_GUIDE_VERSION: "0.1.69"
        MICROSOFT_SARIF_MULTITOOL_VERSION: "4.3.5"
        MITRE_SAF_VERSION: "1.2.33"
        SSG_DIR: "ssg"
    steps:
      - name: Install prerequisites
        run: |
          # shellcheck shell=sh
          set -eu
          apk add curl docker openscap-docker npm gcompat unzip
          npm install -g "@microsoft/sarif-multitool@${MICROSOFT_SARIF_MULTITOOL_VERSION}"
          npm install -g "@mitre/saf@${MITRE_SAF_VERSION}"
          mkdir -p "${SSG_DIR}"
          curl "https://github.com/ComplianceAsCode/content/releases/download/v${SCAP_SECURITY_GUIDE_VERSION}/scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}.zip" -Lso "${SSG_DIR}/ssg.zip"
          unzip "${SSG_DIR}/ssg.zip" -d "${SSG_DIR}"
      - name: Login to GitHub Container Registry
        uses: docker/login-action@a9794064588be971151ec5e7144cb535bcb56e36 # v2
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Pull the docker image to scan
        run: |
          # shellcheck shell=sh
          set -eu
          # oscap-docker requires the image to have been pulled
          docker pull "${IMAGE_NAME}"
      - name: Run openscap
        run: |
          # shellcheck shell=sh
          set -eu
          # extract /etc/os-release
          container_id=$(docker create "${IMAGE_NAME}")
          if ! docker export "${container_id}" | tar -tvf - | grep -E '\setc/os-release( ->.*)?$' > /dev/null 2>&1 ; then
            >&2 echo "The operating system used by ${IMAGE_NAME} could not be detected."
            >&2 echo "Images that are not based on an operating system (such as distroless images) cannot be scanned by SCAP."
            exit 1
          fi
          docker cp -L "$container_id:/etc/os-release" .
          docker rm "$container_id"
          unset container_id
          # determine which ssg to use based on /etc/os-release
          # see https://www.freedesktop.org/software/systemd/man/os-release.html
          version_id=$(awk -F= '$1=="VERSION_ID" { print $2 ;}' os-release | sed 's/"//g')
          id=$(awk -F= '$1=="ID" { print $2 ;}' os-release | sed 's/"//g')
          if [ "${id}" = "ubuntu" ] && echo "${version_id}" | grep -qE '^18\.04(\..*)?$' ; then
            ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-ubuntu1804-ds.xml"
          elif [ "${id}" = "ubuntu" ] && echo "${version_id}" | grep -qE '^20\.04(\..*)?$' ; then
            ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-ubuntu2004-ds.xml"
          elif [ "${id}" = "ubuntu" ] && echo "${version_id}" | grep -qE '^22\.04(\..*)?$' ; then\
            ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-ubuntu2204-ds.xml"
          elif [ "${id}" = "centos" ] && echo "${version_id}" | grep -qE '^7(\..*)?$' ; then
            ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-centos7-ds.xml"
          elif [ "${id}" = "centos" ] && echo "${version_id}" | grep -qE '^8(\..*)?$' ; then
            ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-centos8-ds.xml"
          elif [ "${id}" = "ol" ] && echo "${version_id}" | grep -qE '^7(\..*)?$' ; then
            ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-ol7-ds.xml"
          elif [ "${id}" = "ol" ] && echo "${version_id}" | grep -qE '^8(\..*)?$' ; then
            ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-ol8-ds.xml"
          elif [ "${id}" = "ol" ] && echo "${version_id}" | grep -qE '^9(\..*)?$' ; then
            ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-ol9-ds.xml"
          elif [ "${id}" = "rhel" ] && echo "${version_id}" | grep -qE '^7(\..*)?$' ; then
            ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-rhel7-ds.xml"
          elif [ "${id}" = "rhel" ] && echo "${version_id}" | grep -qE '^8(\..*)?$' ; then
            ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-rhel8-ds.xml"
          elif [ "${id}" = "rhel" ] && echo "${version_id}" | grep -qE '^9(\..*)?$' ; then
            ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-rhel9-ds.xml"
          elif [ "${id}" = "sles" ] && echo "${version_id}" | grep -qE '^12(\..*)?$' ; then
            ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-sle12-ds.xml"
          elif [ "${id}" = "sles" ] && echo "${version_id}" | grep -qE '^15(\..*)?$' ; then
            ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-sle15-ds.xml"
          else
            >&2 echo "There is no configuration available for ${id} ${version_id}"
            exit 1
          fi
          # Select the profile to use. The first profile that exists in the ssg is used.
          for profile in xccdf_org.ssgproject.content_profile_cis_level2_server xccdf_org.ssgproject.content_profile_cis xccdf_org.ssgproject.content_profile_standard; do
            if oscap info --profiles "${SSG_DIR}/${ssg}" | grep -qF "${profile}:"; then
              echo "Selected profile: ${profile}"
              break;
            fi
          done

          set +e
          oscap-docker image "${IMAGE_NAME}" xccdf eval --verbose ERROR --fetch-remote-resources --profile "${profile}" --results "openscap-report.xml" --report "openscap-report.html" "${SSG_DIR}/${ssg}"
          OSCAP_EXIT_CODE=$?
          set -e

          case "${OSCAP_EXIT_CODE}" in
            0)
              echo "All rules passed"
            ;;
            1)
              >&2 echo "An error occurred during evaluation"
              exit 2
            ;;
            2)
              echo "There is at least one rule with either fail or unknown result"
            ;;
            *)
              >&2 echo "openscap returned an unexpected exit status of $OSCAP_EXIT_CODE"
              exit "$OSCAP_EXIT_CODE"
            ;;
          esac
      - name: Convert xml to hdf
        run: |
          # shellcheck shell=sh
          set -eu
          saf convert xccdf_results2hdf -i "openscap-report.xml" -o openscap-report.hdf
      - name: Convert hdf to sarif
        run: |
          # shellcheck shell=sh
          set -eu
          DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 sarif-multitool convert -t Hdf -o openscap-report.sarif openscap-report.hdf.json
      - name: filter results that shouldn't be GitHub security alerts
        # Hopefully GitHub adds support for SARIF's "kind" eliminating the need for this step: https://github.com/orgs/community/discussions/65477
        run: |
          # shellcheck shell=sh
          set -eu
          jq 'del(.runs[].results[] | select(.kind == "notApplicable" or .kind == "pass" or .kind == "informational" ))' openscap-report.sarif > filtered.sarif
          mv filtered.sarif openscap-report.sarif
      - name: Upload reports
        if: success() || failure() # always run even if the previous step fails
        uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3
        with:
          name: openscap-reports
          path: |
            openscap-report.html
            openscap-report.xml
            openscap-report.hdf.json
            openscap-report.sarif
      - name: Upload SARIF file
        uses: github/codeql-action/upload-sarif@46ed16ded91731b2df79a2893d3aea8e9f03b5c4 # v2
        # Results are generated only on a success or failure
        # this is required since GitHub by default won't run the next step
        # when the previous one has failed. Security checks that do not pass will 'fail'.
        # An alternative is to add `continue-on-error: true` to the previous step
        if: success() || failure()
        with:
          sarif_file: openscap-report.sarif

To keep the tools up to date using Renovate (it is very important to keep dependencies up to date):

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:base"
  ],
  "regexManagers": [
    {
      "fileMatch": ["\\.yml$"],
      "matchStrings": ["MICROSOFT_SARIF_MULTITOOL_VERSION *: *\"(?<currentValue>.+?)\""],
      "depNameTemplate": "@microsoft/sarif-multitool",
      "datasourceTemplate": "npm"
    },
    {
      "fileMatch": ["\\.yml$"],
      "matchStrings": ["MITRE_SAF_VERSION *: *\"(?<currentValue>.+?)\""],
      "depNameTemplate": "@mitre/saf",
      "datasourceTemplate": "npm"
    }
  ]
}

The GitLab CI Code

I developed, tested, and will continue to maintain this pipeline code in Jumpstart – so please see that project to see this code in action and see the latest version of it.

The result of this pipeline is that issues will appear under “Security,” like this:

Here’s the GitLab CI pipeline YAML to add to your project:

---
# An earlier job/stage must have build and pushed a docker image to $IMAGE_NAME that the scap job will scan
scap:
  stage: test
  image:
    name: alpine:3.18.3@sha256:7144f7bab3d4c2648d7e59409f15ec52a18006a128c733fcff20d3a4a54ba44a  # alpine official image
    entrypoint: [""]
  services:
    - name: docker:24.0.4-dind@sha256:594b3a4dc012e5160fed2a5c47a36ac5af6fa25f0c155bd9cff23d4f8c6fec16
      alias: docker
      # explicitly disable tls to avoid docker startup interruption as of docker 20.10.9
      # there is so security concern because docker is only accessed from within the same system;
      # there is no external network communication to worry about.
      # See https://github.com/testcontainers/testcontainers-java/pull/4573
      command: ["--tls=false"]
  variables:
    # Instruct Testcontainers to use the daemon of DinD.
    DOCKER_HOST: "tcp://docker:2375"
    DOCKER_TLS_CERTDIR: ""
    SCAP_SECURITY_GUIDE_VERSION: "0.1.69"
    MICROSOFT_SARIF_MULTITOOL_VERSION: "4.3.5"
    MITRE_SAF_VERSION: "1.2.33"
    SARIF_CONVERTER_VERSION: "0.7.1"
  script:
    - |
      # shellcheck shell=sh
      set -eu
      printf "\e[0Ksection_start:%s:prerequisites[collapsed=true]\r\e[0KInstalling prerequisites...\n" "$(date +%s)"
      apk add curl docker openscap-docker npm gcompat unzip
      npm install -g "@microsoft/sarif-multitool@${MICROSOFT_SARIF_MULTITOOL_VERSION}"
      npm install -g "@mitre/saf@${MITRE_SAF_VERSION}"
      mkdir ssg
      ssgdir="ssg"
      curl "https://github.com/ComplianceAsCode/content/releases/download/v${SCAP_SECURITY_GUIDE_VERSION}/scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}.zip" -Lso "${ssgdir}/ssg.zip"
      unzip "${ssgdir}/ssg.zip" -d "${ssgdir}"
      curl "https://gitlab.com/ignis-build/sarif-converter/-/releases/v${SARIF_CONVERTER_VERSION}/downloads/bin/sarif-converter-linux" -Lso sarif-converter
      chmod +x sarif-converter
      printf "\e[0Ksection_end:%s:prerequisites\r\e[0K\n" "$(date +%s)"

      printf "\e[0Ksection_start:%s:docker[collapsed=true]\r\e[0KPulling the docker image to scan...\n" "$(date +%s)"
      # oscap-docker requires the image to have been pulled
      docker login -u "${CI_REGISTRY_USER}" -p "${CI_REGISTRY_PASSWORD}" "${CI_REGISTRY}"
      docker pull "${IMAGE_NAME}"
      printf "\e[0Ksection_end:%s:docker\r\e[0K\n" "$(date +%s)"

      printf "\e[0Ksection_start:%s:scan\r\e[0KRunning openscap...\n" "$(date +%s)"

      # extract /etc/os-release
      container_id=$(docker create "${IMAGE_NAME}")
      if ! docker export "${container_id}" | tar -tvf - | grep -E '\setc/os-release( ->.*)$' > /dev/null 2>&1 ; then
        >&2 echo "The operating system used by ${IMAGE_NAME} could not be detected."
        >&2 echo "Images that are not based on an operating system (such as distroless images) cannot be scanned by SCAP."
        exit 1
      fi
      docker cp -L "$container_id:/etc/os-release" .
      docker rm "$container_id"
      unset container_id
      # determine which ssg to use based on /etc/os-release
      # see https://www.freedesktop.org/software/systemd/man/os-release.html
      version_id=$(awk -F= '$1=="VERSION_ID" { print $2 ;}' os-release | sed 's/"//g')
      id=$(awk -F= '$1=="ID" { print $2 ;}' os-release | sed 's/"//g')
      if [ "${id}" = "ubuntu" ] && echo "${version_id}" | grep -qE '^18\.04(\..*)?$' ; then
        ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-ubuntu1804-ds.xml"
      elif [ "${id}" = "ubuntu" ] && echo "${version_id}" | grep -qE '^20\.04(\..*)?$' ; then
        ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-ubuntu2004-ds.xml"
      elif [ "${id}" = "ubuntu" ] && echo "${version_id}" | grep -qE '^22\.04(\..*)?$' ; then\
        ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-ubuntu2204-ds.xml"
      elif [ "${id}" = "centos" ] && echo "${version_id}" | grep -qE '^7(\..*)?$' ; then
        ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-centos7-ds.xml"
      elif [ "${id}" = "centos" ] && echo "${version_id}" | grep -qE '^8(\..*)?$' ; then
        ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-centos8-ds.xml"
      elif [ "${id}" = "ol" ] && echo "${version_id}" | grep -qE '^7(\..*)?$' ; then
        ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-ol7-ds.xml"
      elif [ "${id}" = "ol" ] && echo "${version_id}" | grep -qE '^8(\..*)?$' ; then
        ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-ol8-ds.xml"
      elif [ "${id}" = "ol" ] && echo "${version_id}" | grep -qE '^9(\..*)?$' ; then
        ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-ol9-ds.xml"
      elif [ "${id}" = "rhel" ] && echo "${version_id}" | grep -qE '^7(\..*)?$' ; then
        ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-rhel7-ds.xml"
      elif [ "${id}" = "rhel" ] && echo "${version_id}" | grep -qE '^8(\..*)?$' ; then
        ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-rhel8-ds.xml"
      elif [ "${id}" = "rhel" ] && echo "${version_id}" | grep -qE '^9(\..*)?$' ; then
        ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-rhel9-ds.xml"
      elif [ "${id}" = "sles" ] && echo "${version_id}" | grep -qE '^12(\..*)?$' ; then
        ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-sle12-ds.xml"
      elif [ "${id}" = "sles" ] && echo "${version_id}" | grep -qE '^15(\..*)?$' ; then
        ssg="scap-security-guide-${SCAP_SECURITY_GUIDE_VERSION}/ssg-sle15-ds.xml"
      else
        >&2 echo "There is no configuration available for ${id} ${version_id}"
        exit 1
      fi
      # Select the profile to use. The first profile that exists in the ssg is used.
      for profile in xccdf_org.ssgproject.content_profile_cis_level2_server xccdf_org.ssgproject.content_profile_cis xccdf_org.ssgproject.content_profile_standard; do
        if oscap info --profiles "${ssgdir}/${ssg}" | grep -qF "${profile}:"; then
          echo "Selected profile: ${profile}"
          break;
        fi
      done

      set +e
      oscap-docker image "${IMAGE_NAME}" xccdf eval --verbose ERROR --fetch-remote-resources --profile "${profile}" --results "openscap-report.xml" --report "openscap-report.html" "${ssgdir}/${ssg}"
      OSCAP_EXIT_CODE=$?
      set -e

      echo "To view the openscap report: ${CI_JOB_URL}/artifacts/external_file/openscap-report.html"

      case "${OSCAP_EXIT_CODE}" in
        0)
          echo "All rules passed"
        ;;
        1)
          >&2 echo "An error occurred during evaluation"
          exit 2
        ;;
        2)
          echo "There is at least one rule with either fail or unknown result"
        ;;
        *)
          >&2 echo "openscap returned an unexpected exit status of $OSCAP_EXIT_CODE"
          exit "$OSCAP_EXIT_CODE"
        ;;
      esac
      printf "\e[0Ksection_end:%s:scan\r\e[0K\n" "$(date +%s)"

      printf "\e[0Ksection_start:%s:xml_to_hdf\r\e[0KConverting xml to hdf...\n" "$(date +%s)"
      saf convert xccdf_results2hdf -i "openscap-report.xml" -o openscap-report.hdf
      printf "\e[0Ksection_end:%s:xml_to_hdf\r\e[0K\n" "$(date +%s)"

      printf "\e[0Ksection_start:%s:hdf_to_sarif\r\e[0KConverting hdf to sarif...\n" "$(date +%s)"
      DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 sarif-multitool convert -t Hdf -o openscap-report.sarif openscap-report.hdf.json
      printf "\e[0Ksection_end:%s:hdf_to_sarif\r\e[0K\n" "$(date +%s)"

      printf "\e[0Ksection_start:%s:sarif_to_gitlab\r\e[0KConverting sarif to GitLab SAST JSON...\n" "$(date +%s)"
      ./sarif-converter --type sast openscap-report.sarif gl-sast-report.json
      printf "\e[0Ksection_end:%s:sarif_to_gitlab\r\e[0K\n" "$(date +%s)"
  artifacts:
    when: always
    paths:
      - "openscap-report.xml"
      - "openscap-report.html"
      - "openscap-report.sarif"
      - "openscap-report.hdf.json"
    reports:
      sast:
        - "gl-sast-report.json"

To keep the tools up to date using Renovate (it is very important to keep dependencies up to date):

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:base"
  ],
  "regexManagers": [
    {
      "fileMatch": ["\\.yml$"],
      "matchStrings": ["SARIF_CONVERTER_VERSION *: *\"(?<currentValue>.+?)\""],
      "depNameTemplate": "ignis-build/sarif-converter",
      "datasourceTemplate": "gitlab-releases"
    },
    {
      "fileMatch": ["\\.yml$"],
      "matchStrings": ["MICROSOFT_SARIF_MULTITOOL_VERSION *: *\"(?<currentValue>.+?)\""],
      "depNameTemplate": "@microsoft/sarif-multitool",
      "datasourceTemplate": "npm"
    },
    {
      "fileMatch": ["\\.yml$"],
      "matchStrings": ["MITRE_SAF_VERSION *: *\"(?<currentValue>.+?)\""],
      "depNameTemplate": "@mitre/saf",
      "datasourceTemplate": "npm"
    }
  ]
}

CC BY-SA 4.0 SCAP Security and Compliance Scanning of Docker Images in GitHub Actions and GitLab CI by Craig Andrews is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.

One thought on “SCAP Security and Compliance Scanning of Docker Images in GitHub Actions and GitLab CI

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.