Refactor AffectedVersionRangeMatcher class to work with Container Scanning advisories

Proposal

Container Scanning advisories contain extra data compared to Dependency Scanning advisories, such as distro. Because of this, the current Gitlab::VulnerabilityScanning::AffectedVersionRangeMatcher#affected? is not sufficient to handle Container Scanning advisories.

The purpose of this issue is to refactor the Gitlab::VulnerabilityScanning::AffectedVersionRangeMatcher class so it works with both Dependency and Container Scanning advisories.

Implementation Plan

  1. Create a factory class Gitlab::VulnerabilityScanning::AffectedVersionRangeMatcherFactory

    module Gitlab
      module VulnerabilityScanning
        class AffectedVersionRangeMatcherFactory
          def self.create(purl_type:, range:, version:, distro: nil, source: nil)
            if distro
              return Gitlab::VulnerabilityScanning::ContainerScanning::AffectedVersionRangeMatcher.new(
                purl_type: purl_type, range: range, version: version, distro: distro, source: source)
            end
    
            Gitlab::VulnerabilityScanning::DependencyScanning::AffectedVersionRangeMatcher.new(
              purl_type: purl_type, range: range, version: version)
          end
    
          attr_reader :purl_type, :range, :version, :distro, :source
    
          def initialize(purl_type:, range:, version:, distro: nil, source: nil)
            @purl_type = purl_type
            @range = range
            @version = version
            @distro = distro
            @source = source
          end
    
          def affected?
            raise NotImplementedError
          end
        end
      end
    end
  2. Move Gitlab::VulnerabilityScanning::AffectedVersionRangeMatcher to Gitlab::VulnerabilityScanning::DependencyScanning::AffectedVersionRangeMatcher and update it to work with the new factory class added in step 1. above.

  3. Create a new class Gitlab::VulnerabilityScanning::ContainerScanning::AffectedVersionRangeMatcher and implement the affected? method:

    module Gitlab
      module VulnerabilityScanning
        module ContainerScanning
          class AffectedVersionRangeMatcher < Gitlab::VulnerabilityScanning::AffectedVersionRangeMatcherFactory
            def affected?  
              # could also move the following logic to source.matches_distro?(distro)
              distro_name, distro_version = distro.split(" ")
              return false unless distro_name == source.operating_system_name &&
                distro_version == sanitize_operating_system_version(source.operating_system_name, source.operating_system_version)
    
              return true if range == '*'
    
              # the following requires code implemented by https://gitlab.com/gitlab-org/gitlab/-/issues/427961
              SemverDialects::VersionChecker.version_sat?(purl_type, version, range)
            end
    
            private
    
            def sanitize_operating_system_version(operating_system_name, operating_system_version)
              # add code to convert the operating system version to a trivy bucket version
            end
          end
        end
      end
    end

    The behaviour of the sanitize_operating_system_version method differs depending upon the operating_system_name. Please see Operating System Version Sanitizing Logic for details.

  4. Add unit tests for the above logic

  5. Perform benchmarking to ensure the new code for Container Scanning advisories is as efficient as the existing code for Dependency Scanning advisories.

    Operating System Version Sanitizing Logic

    Click for sanitize_operating_system_version implementation details

    trivy uses the following logic to convert operating system version values to trivy-db compatible version values:

    • alpine

      Parses the version from the file /etc/apk/repositories on the Docker container. For example, if this file contains:

      http://dl-cdn.alpinelinux.org/alpine/v3.12/main
      http://dl-cdn.alpinelinux.org/alpine/v3.12/community

      Then the returned version will be 3.12

      If the version cannot be parsed from the /etc/apk/repositories file, then trivy will use the minor version.

      In order to simplify this process, we'll always use the minor version, for example, 3.7.3 will become 3.7.

      Example:

      image name trivy OS name OS name from SBOM OS version from SBOM parsed version
      alpine:3.7 alpine alpine 3.7.3 3.7
      alpine:20230901 alpine alpine 3.19_alpha20230901 edge
    • debian

      extracts the major version:

      Example:

      image name trivy OS name OS name from SBOM OS version from SBOM parsed version
      debian:testing-20230522-slim debian debian 12.0 12
    • rocky

      extracts the major version

      Example:

      image name trivy OS name OS name from SBOM OS version from SBOM parsed version
      rockylinux:9 rocky rocky 9.2 9
    • openSUSE Leap

      Uses the OS version as given

      Example:

      image name trivy OS name OS name from SBOM OS version from SBOM parsed version
      opensuse/leap:15.5 opensuse.leap openSUSE Leap 15.5 15.5
      opensuse/leap:15.2.1 opensuse.leap openSUSE Leap 15.2.1 15.2.1
    • SUSE Linux Enterprise

      Uses the OS version as given

      Example:

      image name trivy OS name OS name from SBOM OS version from SBOM parsed version
      registry.suse.com/suse/sles12sp5:5.2.257 suse linux enterprise server SUSE Linux Enterprise 12.5 12.5
    • alma

      Uses the major version

      Example:

      image name trivy OS name OS name from SBOM OS version from SBOM parsed version
      almalinux:9.3-minimal-20231124 alma alma 9.3 9
    • wolfi (chainguard)

      Version is ignored (notice that no osVer parameter is passed in the Detect function)

      Example:

      image name trivy OS name OS name from SBOM OS version from SBOM parsed version
      cgr.dev/chainguard/go:latest wolfi wolfi 20230201
    • amazon linux

      Uses the following logic to extract the version value.

      Example:

      image name trivy OS name OS name from SBOM OS version from SBOM parsed version
      amazonlinux:2023 amazon amazon linux 2023 (Amazon Linux) 2023
      amazonlinux:2.0.20201218.1 amazon amazon linux 2 (Karoo) 2
      amazonlinux:2016.09.0.20161028 amazon amazon linux AMI release 2016.09 1
      amazonlinux:1 amazon amazon linux AMI release 2018.03 1
    • Photon OS

      Uses the OS version as given

      Example:

      image name trivy OS name OS name from SBOM OS version from SBOM parsed version
      photon:1.0-20210409 photon Photon OS 1.0 1.0
    • Oracle Linux

      Uses the major version.

      Example:

      image name trivy OS name OS name from SBOM OS version from SBOM parsed version
      oraclelinux:8.2 oracle Oracle Linux 8.2 8
    • CBL-Mariner

      Uses the minor version, for example: 2.0

      Example:

      image name trivy OS name OS name from SBOM OS version from SBOM parsed version
      mcr.microsoft.com/cbl-mariner/base/core:2.0 cbl-mariner CBL-Mariner 2.0.20230303 2.0
    • ubuntu

      Version is used as-is, however, trivy has additional logic for versions that end in -ESM. If the version ends in -ESM and it does not appear in the list of eolDates, then trivy removes the -ESM suffix and attempts to use the non-ESM version from the eolDates. If this version still does not appear in the list of eolDates, then the version is passed back as-is

      Example:

      image name trivy OS name OS name from SBOM OS version from SBOM parsed version
      ubuntu:bionic-20210222 ubuntu ubuntu 18.04 18.04
      ? (couldn't find an image to demonstrate this) ubuntu ubuntu 18.02-ESM 18.02-ESM
      ? (couldn't find an image to demonstrate this) ubuntu ubuntu 16.04-ESM 16.04-ESM
      ? (couldn't find an image to demonstrate this) ubuntu ubuntu 19.04-ESM 19.04

Auto-Summary 🤖

Discoto Usage

Points

Discussion points are declared by headings, list items, and single lines that start with the text (case-insensitive) point:. For example, the following are all valid points:

  • #### POINT: This is a point
  • * point: This is a point
  • + Point: This is a point
  • - pOINT: This is a point
  • point: This is a **point**

Note that any markdown used in the point text will also be propagated into the topic summaries.

Topics

Topics can be stand-alone and contained within an issuable (epic, issue, MR), or can be inline.

Inline topics are defined by creating a new thread (discussion) where the first line of the first comment is a heading that starts with (case-insensitive) topic:. For example, the following are all valid topics:

  • # Topic: Inline discussion topic 1
  • ## TOPIC: **{+A Green, bolded topic+}**
  • ### tOpIc: Another topic

Quick Actions

Action Description
/discuss sub-topic TITLE Create an issue for a sub-topic. Does not work in epics
/discuss link ISSUABLE-LINK Link an issuable as a child of this discussion

Last updated by this job

Discoto Settings
---
summary:
  max_items: -1
  sort_by: created
  sort_direction: ascending

See the settings schema for details.

Edited by Adam Cohen