diff --git a/app/models/concerns/enums/sbom.rb b/app/models/concerns/enums/sbom.rb index 9c3bbc92e86bbf9a98b6fe526dba43f042464433..4e54e48e6677b9cf76d05c8f00ae79b5da444bb3 100644 --- a/app/models/concerns/enums/sbom.rb +++ b/app/models/concerns/enums/sbom.rb @@ -22,10 +22,37 @@ class Sbom wolfi: 13 }.with_indifferent_access.freeze + DEPENDENCY_SCANNING_PURL_TYPES = %w[ + composer + conan + gem + golang + maven + npm + nuget + pypi + ].freeze + + CONTAINER_SCANNING_PURL_TYPES = %w[ + apk + rpm + deb + cbl-mariner + wolfi + ].freeze + def self.component_types COMPONENT_TYPES end + def self.dependency_scanning_purl_type?(purl_type) + DEPENDENCY_SCANNING_PURL_TYPES.include?(purl_type) + end + + def self.container_scanning_purl_type?(purl_type) + CONTAINER_SCANNING_PURL_TYPES.include?(purl_type) + end + def self.purl_types # return 0 by default if the purl_type is not found, to prevent # consumers from producing invalid SQL caused by null entries diff --git a/ee/app/models/sbom/source.rb b/ee/app/models/sbom/source.rb index a38caaadda0a512d25757336b230da5836b90d83..f16a7df5cfa07c3a77c60a8560a79f73bf96aa87 100644 --- a/ee/app/models/sbom/source.rb +++ b/ee/app/models/sbom/source.rb @@ -2,6 +2,8 @@ module Sbom class Source < ApplicationRecord + include Gitlab::Ci::Reports::Sbom::SourceHelper + enum source_type: { dependency_scanning: 0, container_scanning: 1 @@ -10,16 +12,6 @@ class Source < ApplicationRecord validates :source_type, presence: true validates :source, presence: true, json_schema: { filename: 'sbom_source' } - def packager - source.dig('package_manager', 'name') - end - - def input_file_path - source.dig('input_file', 'path') - end - - def source_file_path - source.dig('source_file', 'path') - end + alias_attribute :data, :source end end diff --git a/ee/lib/gitlab/vulnerability_scanning/container_scanning/finding_builder.rb b/ee/lib/gitlab/vulnerability_scanning/container_scanning/finding_builder.rb new file mode 100644 index 0000000000000000000000000000000000000000..a37e917bf9e9bf0b8a30acc785e9bc814b6c9896 --- /dev/null +++ b/ee/lib/gitlab/vulnerability_scanning/container_scanning/finding_builder.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module Gitlab + module VulnerabilityScanning + module ContainerScanning + class FindingBuilder < VulnerabilityScanning::FindingBuilder + include Gitlab::Utils::StrongMemoize + + MissingPropertiesError = Class.new(StandardError) + + private + + def validate! + return unless sbom_source.image_name.nil? || sbom_source.image_tag.nil? + + raise MissingPropertiesError, + 'Missing required gitlab:container_scanning CycloneDX properties' + end + + def report_type + "container_scanning" + end + + def title + advisory.title + end + strong_memoize_attr :title + + def details + { + vulnerable_package: { + name: "Vulnerable Package", + type: "text", + value: "#{affected_component.name}:#{affected_component.version}" + } + }.with_indifferent_access.freeze + end + + def image_and_tag + "#{sbom_source.image_name}:#{sbom_source.image_tag}" + end + strong_memoize_attr :image_and_tag + + def operating_system_name_and_version + "#{sbom_source.operating_system_name} #{sbom_source.operating_system_version}" + end + strong_memoize_attr :image_and_tag + + def location + ::Gitlab::Ci::Reports::Security::Locations::ContainerScanning.new( + image: image_and_tag, + operating_system: operating_system_name_and_version, + package_name: affected_component.name, + package_version: affected_component.version + ) + end + strong_memoize_attr :location + + def original_data + { + message: title, + description: advisory.description, + solution: advisory.solution, + location: { + image: image_and_tag, + operating_system: operating_system_name_and_version, + dependency: { + package: { name: affected_component.name }, + version: affected_component.version + } + } + }.with_indifferent_access.freeze + end + end + end + end +end diff --git a/ee/lib/gitlab/vulnerability_scanning/finding_builder.rb b/ee/lib/gitlab/vulnerability_scanning/finding_builder.rb index ccd7d8fbf18ac99f1165a301f9d4002bdab94e9b..27942395c342beecb0ee64da632ad65ff7c76a7e 100644 --- a/ee/lib/gitlab/vulnerability_scanning/finding_builder.rb +++ b/ee/lib/gitlab/vulnerability_scanning/finding_builder.rb @@ -6,16 +6,21 @@ class FindingBuilder include Gitlab::Utils::StrongMemoize def self.for_report_type(report_type) - return unless report_type == "dependency_scanning" - - ::Gitlab::VulnerabilityScanning::DependencyScanning::FindingBuilder + case report_type + when "dependency_scanning" + ::Gitlab::VulnerabilityScanning::DependencyScanning::FindingBuilder + when "container_scanning" + ::Gitlab::VulnerabilityScanning::ContainerScanning::FindingBuilder + end end # .for_purl_type will return a builder for the given purl type if it exists. def self.for_purl_type(purl_type) - return unless DEPENDENCY_SCANNING_PURL_TYPES.include?(purl_type) - - ::Gitlab::VulnerabilityScanning::DependencyScanning::FindingBuilder + if Enums::Sbom.dependency_scanning_purl_type?(purl_type) + ::Gitlab::VulnerabilityScanning::DependencyScanning::FindingBuilder + elsif Enums::Sbom.container_scanning_purl_type?(purl_type) + ::Gitlab::VulnerabilityScanning::ContainerScanning::FindingBuilder + end end # .for_purl_type! is exactly like .for_purl_type but will raise an error @@ -69,17 +74,6 @@ def finding private - DEPENDENCY_SCANNING_PURL_TYPES = %w[ - composer - conan - gem - golang - maven - npm - nuget - pypi - ].freeze - attr_reader :project, :pipeline, :sbom_source, :scanner, :advisory, :affected_component def validate! diff --git a/ee/lib/gitlab/vulnerability_scanning/security_report_builder.rb b/ee/lib/gitlab/vulnerability_scanning/security_report_builder.rb index 15dc3cb53bd3de763f8a5d968347100a2c732e93..26c1811bdcdace8f0fdc5a76d7337be7d8aa650a 100644 --- a/ee/lib/gitlab/vulnerability_scanning/security_report_builder.rb +++ b/ee/lib/gitlab/vulnerability_scanning/security_report_builder.rb @@ -50,7 +50,8 @@ def add_affection(advisory, affected_component) finding = builder.finding report.add_finding(finding) finding.identifiers.each { |ident| report.add_identifier(ident) } - rescue DependencyScanning::FindingBuilder::MissingPropertiesError => error + rescue DependencyScanning::FindingBuilder::MissingPropertiesError, + ContainerScanning::FindingBuilder::MissingPropertiesError => error report.add_error('MissingPropertiesError', error.message) end end diff --git a/ee/spec/factories/vulnerability_scanning/possibly_affected_components.rb b/ee/spec/factories/vulnerability_scanning/possibly_affected_components.rb index a0601a93c47779f544f98717f3c9e1b51c7c9d46..dab60df9cb160a7a88f5497be76443b154cfed20 100644 --- a/ee/spec/factories/vulnerability_scanning/possibly_affected_components.rb +++ b/ee/spec/factories/vulnerability_scanning/possibly_affected_components.rb @@ -10,6 +10,10 @@ project { pipeline.project } source { association :sbom_source } + trait :container_scanning do + source { association :sbom_source, :container_scanning } + end + skip_create initialize_with do diff --git a/ee/spec/fixtures/security_reports/simple/gl-container-scanning-report.json b/ee/spec/fixtures/security_reports/simple/gl-container-scanning-report.json new file mode 100644 index 0000000000000000000000000000000000000000..7284b845090a691d8fdc4122a562d8f24089f439 --- /dev/null +++ b/ee/spec/fixtures/security_reports/simple/gl-container-scanning-report.json @@ -0,0 +1,67 @@ +{ + "version": "15.0.6", + "vulnerabilities": [ + { + "id": "df6969bdb23ce636df334f8f6d5fe631e58f75c4dc33ec0a4466d4af8e58c9d6", + "description": "An SSE2-optimized memmove implementation for i386 in sysdeps/i386/i686/multiarch/memcpy-sse2-unaligned.S in the GNU C Library (aka glibc or libc6) 2.21 through 2.27 does not correctly perform the overlapping memory check if the source memory range spans the middle of the address space, resulting in corrupt data being produced by the copy operation. This may disclose information to context-dependent attackers, or result in a denial of service, or, possibly, code execution.", + "severity": "High", + "solution": "Upgrade glibc from 2.24-11+deb9u3 to 2.24-11+deb9u4", + "location": { + "dependency": { + "package": { + "name": "glibc" + }, + "version": "2.24-11+deb9u3" + }, + "operating_system": "debian:9", + "image": "registry.gitlab.com/gitlab-org/security-products/dast/webgoat-8.0@sha256:bc09fe2e0721dfaeee79364115aeedf2174cce0947b9ae5fe7c33312ee019a4e" + }, + "identifiers": [ + { + "type": "cve", + "name": "CVE-2017-18269", + "value": "CVE-2017-18269", + "url": "https://security-tracker.debian.org/tracker/CVE-2017-18269" + } + ], + "links": [ + { + "url": "https://security-tracker.debian.org/tracker/CVE-2017-18269" + } + ], + "details": { + "vulnerable_package": { + "name": "Vulnerable Package", + "type": "text", + "value": "glibc:2.24-11+deb9u3" + } + } + } + ], + "remediations": [ + + ], + "scan": { + "scanner": { + "id": "trivy", + "name": "Trivy", + "url": "https://github.com/aquasecurity/trivy", + "vendor": { + "name": "GitLab" + }, + "version": "2.1.4" + }, + "analyzer": { + "id": "gcs", + "name": "GitLab Container Scanning", + "vendor": { + "name": "GitLab" + }, + "version": "5.2.8" + }, + "type": "container_scanning", + "status": "success", + "start_time": "2022-08-10T22:37:00", + "end_time": "2022-08-10T22:37:00" + } +} diff --git a/ee/spec/lib/gitlab/vulnerability_scanning/container_scanning/finding_builder_spec.rb b/ee/spec/lib/gitlab/vulnerability_scanning/container_scanning/finding_builder_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..c31a01b4e1fd326b7f44cdc814d115d8262786b0 --- /dev/null +++ b/ee/spec/lib/gitlab/vulnerability_scanning/container_scanning/finding_builder_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe Gitlab::VulnerabilityScanning::ContainerScanning::FindingBuilder, feature_category: :software_composition_analysis do + let(:now) { Time.zone.now } + let(:ci_build) { build(:ci_build) } + let(:sbom_source) { build(:ci_reports_sbom_source) } + let(:security_scanner) { Gitlab::VulnerabilityScanning::SecurityScanner.fabricate } + + let(:affected_component) do + build(:vs_possibly_affected_component, name: 'Bind 9', + version: '9.0.1') + end + + let(:advisory) do + build(:vs_advisory, + title: "bind: assertion failure in buffer.c while building responses to a specifically constructed request", + description: "buffer.c in named in ISC BIND 9 before 9.9.9-P3, 9.10.x before 9.10.4-P3, and 9.11.x", + cvss_v2: "AV:N/AC:L/Au:N/C:N/I:N/A:C", + cvss_v3: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + urls: %w[http://rhn.redhat.com/errata/RHSA-2016-1944.html http://rhn.redhat.com/errata/RHSA-2016-1945.html http://rhn.redhat.com/errata/RHSA-2016-2099.html], + identifiers: [ + build(:pm_identifier, type: "cve", name: "CVE-2018-1000538", + url: "https://nvd.nist.gov/vuln/detail/CVE-2018-1000538", value: "CVE-2018-1000538") + ], + solution: 'Update Bind Daemon' + ) + end + + subject(:builder) do + described_class.new(project: ci_build.project, pipeline: ci_build.pipeline, sbom_source: sbom_source, + scanner: security_scanner, advisory: advisory, affected_component: affected_component) + end + + describe "#finding" do + let(:finding) { builder.finding } + + context "when cyclonedx sbom contains required gitlab:container_scanning properties" do + let(:sbom_source) do + build(:ci_reports_sbom_source, :container_scanning, image_name: 'rhel', image_tag: '7.1', + operating_system_name: 'Red Hat Enterprise Linux', operating_system_version: '7') + end + + it "does not add any errors to the report" do + expect { builder.finding }.not_to raise_error + end + + it "creates the links" do + expect(finding.links).to match_array([ + have_attributes(url: "http://rhn.redhat.com/errata/RHSA-2016-1944.html"), + have_attributes(url: "http://rhn.redhat.com/errata/RHSA-2016-1945.html"), + have_attributes(url: "http://rhn.redhat.com/errata/RHSA-2016-2099.html") + ]) + end + + it "creates the name" do + expect(finding.name).to eq(advisory.title) + end + + it "creates the description" do + expect(finding.description).to eq(advisory.description) + end + + it "creates the solution" do + expect(finding.solution).to eq("Update Bind Daemon") + end + + it "creates a valid location" do + expect(finding.location).to have_attributes( + image: 'rhel:7.1', + operating_system: 'Red Hat Enterprise Linux 7', + package_name: 'Bind 9', + package_version: '9.0.1') + end + + it "creates the severity" do + expect(finding.severity).to eq("high") + end + + it "creates the confidence" do + expect(finding.confidence).to eq("unknown") + end + + it "creates the metadata version" do + expect(finding.metadata_version).to eq("0.0.0") + end + end + + context "when cyclonedx does not contain required gitlab:container_scanning properties" do + using RSpec::Parameterized::TableSyntax + + where(:image_name, :image_tag) do + 'rhel' | nil + nil | '7.1' + nil | nil + end + + with_them do + let(:sbom_source) do + build(:ci_reports_sbom_source, :container_scanning, image_name: image_name, image_tag: image_tag) + end + + it "adds an error to the generated report" do + expect do + builder.finding + end.to raise_error(described_class::MissingPropertiesError, + "Missing required gitlab:container_scanning CycloneDX properties") + end + end + end + end +end diff --git a/ee/spec/lib/gitlab/vulnerability_scanning/finding_builder_spec.rb b/ee/spec/lib/gitlab/vulnerability_scanning/finding_builder_spec.rb index a1e4699c85410829f835f5b91afd31a2883d8e5f..85704e3b85069faf2ffba0e0069bd1e10ddf793b 100644 --- a/ee/spec/lib/gitlab/vulnerability_scanning/finding_builder_spec.rb +++ b/ee/spec/lib/gitlab/vulnerability_scanning/finding_builder_spec.rb @@ -139,4 +139,79 @@ end end end + + describe '.for_report_type' do + subject(:builder) { described_class.for_report_type(report_type) } + + context 'when given dependency_scanning' do + let(:report_type) { 'dependency_scanning' } + + it { is_expected.to be(::Gitlab::VulnerabilityScanning::DependencyScanning::FindingBuilder) } + end + + context 'when given container_scanning' do + let(:report_type) { 'container_scanning' } + + it { is_expected.to be(::Gitlab::VulnerabilityScanning::ContainerScanning::FindingBuilder) } + end + + context 'when anything else' do + let(:report_type) { 'sast' } + + it { is_expected.to be(nil) } + end + end + + describe '.for_purl_type' do + shared_examples_for 'it returns the correct builder' do + specify do + purl_types.each do |purl_type| + builder = described_class.for_purl_type(purl_type) + expect(builder).to be(expected_builder) + end + end + end + + context 'when given a dependency scanning purl_type' do + let(:purl_types) do + %w[ + composer + conan + gem + golang + maven + npm + nuget + pypi + ] + end + + let(:expected_builder) { ::Gitlab::VulnerabilityScanning::DependencyScanning::FindingBuilder } + + it_behaves_like 'it returns the correct builder' + end + + context 'when given a container scanning purl_type' do + let(:purl_types) do + %w[ + apk + rpm + deb + cbl-mariner + wolfi + ] + end + + let(:expected_builder) { ::Gitlab::VulnerabilityScanning::ContainerScanning::FindingBuilder } + + it_behaves_like 'it returns the correct builder' + end + + context 'when given anything else' do + let(:purl_types) { ['foo', 'bar', nil] } + let(:expected_builder) { nil } + + it_behaves_like 'it returns the correct builder' + end + end end diff --git a/ee/spec/lib/gitlab/vulnerability_scanning/security_report_builder_spec.rb b/ee/spec/lib/gitlab/vulnerability_scanning/security_report_builder_spec.rb index 5eb2a249795d151dc341244bf4c58568aa27757b..1f4f70169e11937dbaee03efdf368085d2ce1a5c 100644 --- a/ee/spec/lib/gitlab/vulnerability_scanning/security_report_builder_spec.rb +++ b/ee/spec/lib/gitlab/vulnerability_scanning/security_report_builder_spec.rb @@ -3,135 +3,220 @@ require 'spec_helper' RSpec.describe Gitlab::VulnerabilityScanning::SecurityReportBuilder, feature_category: :software_composition_analysis do - let(:report_type) { "dependency_scanning" } - let(:sbom_source) { build(:ci_reports_sbom_source, input_file_path: "go.mod", source_file_path: "go.mod") } let(:sbom) { build(:ci_reports_sbom_report, source: sbom_source) } let_it_be(:ci_build) { build(:ci_build) } - let_it_be(:minio_component) do - build(:vs_possibly_affected_component, name: "github.com/minio/minio", - version: "v0.0.0-20180419184637-5a16671f721f", purl_type: "golang") - end - - let_it_be(:cve_2018_1000538) do - build(:vs_advisory, - xid: "051e7fdd-4e0a-4dfd-ba52-083ee235a580", - title: "Allocation of File Descriptors or Handles Without Limits or Throttling", - description: "Minio a Allocation of Memory Without Limits or Throttling vulnerability in write-to-RAM.", - cvss_v2: "AV:N/AC:L/Au:N/C:N/I:N/A:P", - cvss_v3: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", - identifiers: [ - build(:pm_identifier, type: "cve", name: "CVE-2018-1000538", - url: "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1000538", value: "CVE-2018-1000538") - ], - urls: ["https://github.com/minio/minio/pull/5957", "https://nvd.nist.gov/vuln/detail/CVE-2018-1000538"], - solution: "Unfortunately, there is no solution available yet." - ) - end - - let_it_be(:cve_2020_11012) do - build(:vs_advisory, - xid: "216192fe-2efa-4c52-addd-4bf3522c2b69", - title: "Improper Authentication", - description: "MinIO versions before has an authentication bypass issue in the MinIO admin API. " \ - "Given an admin access key, it is possible to perform admin API operations, i.e., " \ - "creating new service accounts for existing access keys without knowing the admin secret key.", - cvss_v2: "AV:N/AC:L/Au:N/C:N/I:P/A:N", - cvss_v3: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N", - identifiers: [ - build(:pm_identifier, type: "cve", name: "CVE-2020-11012", - url: "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11012", value: "CVE-2020-11012") - ], - urls: ["https://nvd.nist.gov/vuln/detail/CVE-2020-11012"], - solution: "Upgrade to version RELEASE.2020-04-23T00-58-49Z or above." - ) - end - - let(:dependency_scanning_json_report) do - File.read( - Rails.root.join('ee/spec/fixtures/security_reports/simple/gl-dependency-scanning-report.json') - ) - end - let(:expected) { Gitlab::Ci::Reports::Security::Report.new(report_type, ci_build.pipeline, created_at) } let(:created_at) { Time.zone.now } + let(:json_report) { File.read(Rails.root.join(report_path)) } + let(:attributes) do + %i[ + report_type + project_fingerprint + compare_key + uuid + name + description + solution + identifiers + flags + links + location + evidence + severity + confidence + details + signatures + ] + end subject(:builder) do described_class.new(report_type: report_type, project: ci_build.project, pipeline: ci_build.pipeline, sbom: sbom) end before do - Gitlab::Ci::Parsers::Security::DependencyScanning.parse!(dependency_scanning_json_report, expected, validate: true) - builder.add_affections([[cve_2020_11012, minio_component], [cve_2018_1000538, minio_component]]) + parser_class.parse!(json_report, expected, validate: true) + builder.add_affections(cves.map { |cve| [cve, affected_component] }) end describe "#report" do - context "when components are vulnerable" do - it "builds a valid report" do - expect(builder.report.errored?).to eq(false) - expect(builder.report.warnings?).to eq(false) + shared_examples_for 'it handles unsupported report types' do + context "when report type is not supported" do + let(:report_type) { "sast" } + + it "does not add any findings" do + expect(builder.report.findings).to be_empty + end end + end - it "adds correct findings" do - attributes = %i[ - report_type - project_fingerprint - compare_key - uuid - name - description - solution - identifiers - flags - links - location - evidence - severity - confidence - details - signatures - ].freeze - - convert_to_hash = ->(finding) { finding.to_hash.slice(*attributes) } - findings = builder.report.findings.map(&convert_to_hash) - expected_findings = expected.findings.map(&convert_to_hash) - - expect(findings).to match_array(expected_findings) + context "for dependency scanning" do + let(:report_type) { "dependency_scanning" } + let(:parser_class) { Gitlab::Ci::Parsers::Security::DependencyScanning } + let(:sbom_source) { build(:ci_reports_sbom_source, input_file_path: "go.mod", source_file_path: "go.mod") } + let(:report_path) { 'ee/spec/fixtures/security_reports/simple/gl-dependency-scanning-report.json' } + let_it_be(:affected_component) do + build(:vs_possibly_affected_component, name: "github.com/minio/minio", + version: "v0.0.0-20180419184637-5a16671f721f", purl_type: "golang") end - it "adds correct identifiers" do - expect(builder.report.identifiers).to match_array(expected.identifiers) + let_it_be(:cves) do + [ + build(:vs_advisory, + xid: "051e7fdd-4e0a-4dfd-ba52-083ee235a580", + title: "Allocation of File Descriptors or Handles Without Limits or Throttling", + description: "Minio a Allocation of Memory Without Limits or Throttling vulnerability in write-to-RAM.", + cvss_v2: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + cvss_v3: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + identifiers: [ + build(:pm_identifier, type: "cve", name: "CVE-2018-1000538", + url: "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1000538", value: "CVE-2018-1000538") + ], + urls: ["https://github.com/minio/minio/pull/5957", "https://nvd.nist.gov/vuln/detail/CVE-2018-1000538"], + solution: "Unfortunately, there is no solution available yet." + ), + build(:vs_advisory, + xid: "216192fe-2efa-4c52-addd-4bf3522c2b69", + title: "Improper Authentication", + description: "MinIO versions before has an authentication bypass issue in the MinIO admin API. " \ + "Given an admin access key, it is possible to perform admin API operations, i.e., " \ + "creating new service accounts for existing access keys without knowing the admin secret key.", + cvss_v2: "AV:N/AC:L/Au:N/C:N/I:P/A:N", + cvss_v3: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N", + identifiers: [ + build(:pm_identifier, type: "cve", name: "CVE-2020-11012", + url: "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-11012", value: "CVE-2020-11012") + ], + urls: ["https://nvd.nist.gov/vuln/detail/CVE-2020-11012"], + solution: "Upgrade to version RELEASE.2020-04-23T00-58-49Z or above." + ) + ] end - it "does not produce or remove findings when compared against analyzer report" do - comparer = Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer.new(ci_build.project, expected, - builder.report) - expect(comparer.added).to be_empty - expect(comparer.fixed).to be_empty + context "when components are vulnerable" do + it "builds a valid report" do + expect(builder.report.errored?).to eq(false) + expect(builder.report.warnings?).to eq(false) + end + + it "adds correct findings" do + convert_to_hash = ->(finding) { finding.to_hash.slice(*attributes) } + findings = builder.report.findings.map(&convert_to_hash) + expected_findings = expected.findings.map(&convert_to_hash) + + expect(findings).to match_array(expected_findings) + end + + it "adds correct identifiers" do + expect(builder.report.identifiers).to match_array(expected.identifiers) + end + + it "does not produce or remove findings when compared against analyzer report" do + comparer = Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer.new(ci_build.project, expected, + builder.report) + expect(comparer.added).to be_empty + expect(comparer.fixed).to be_empty + end end + + context "when supplied cylonedx is incompatible" do + let(:sbom_source) { build(:ci_reports_sbom_source, data: {}) } + + it "adds an error to the report" do + expect(builder.report.errored?).to eq(true) + expect(builder.report.errors).to match_array([ + { type: "MissingPropertiesError", + message: "Missing required gitlab:dependency_scanning CycloneDX properties" }, + { type: "MissingPropertiesError", + message: "Missing required gitlab:dependency_scanning CycloneDX properties" } + ]) + end + end + + it_behaves_like 'it handles unsupported report types' end - context "when report type is container_scanning" do + context "for container scanning" do + let_it_be(:cves) do + [ + build(:vs_advisory, + xid: "df6969bdb23ce636df334f8f6d5fe631e58f75c4dc33ec0a4466d4af8e58c9d6", + title: "CVE-2017-18269 in registry.gitlab.com/gitlab-org/security-products/dast/webgoat-8.0@sha256:glibc", + description: "An SSE2-optimized memmove implementation for i386 in " \ + "sysdeps/i386/i686/multiarch/memcpy-sse2-unaligned.S in the GNU C Library (aka glibc or " \ + "libc6) 2.21 through 2.27 does not correctly perform the overlapping memory check if the " \ + "source memory range spans the middle of the address space, resulting in corrupt data " \ + "being produced by the copy operation. This may disclose information to context-dependent " \ + "attackers, or result in a denial of service, or, possibly, code execution.", + cvss_v2: "AV:N/AC:L/Au:N/C:N/I:N/A:P", + cvss_v3: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + identifiers: [ + build(:pm_identifier, type: "cve", name: "CVE-2017-18269", + url: "https://security-tracker.debian.org/tracker/CVE-2017-18269", value: "CVE-2017-18269") + ], + urls: ["https://security-tracker.debian.org/tracker/CVE-2017-18269"], + solution: "Upgrade glibc from 2.24-11+deb9u3 to 2.24-11+deb9u4" + ) + ] + end + let(:report_type) { "container_scanning" } + let(:sbom_source) do + build(:ci_reports_sbom_source, :container_scanning, + image_name: "registry.gitlab.com/gitlab-org/security-products/dast/webgoat-8.0@sha256", + image_tag: "bc09fe2e0721dfaeee79364115aeedf2174cce0947b9ae5fe7c33312ee019a4e", + operating_system_name: "debian", + operating_system_version: "9") + end + + let(:parser_class) { Gitlab::Ci::Parsers::Security::ContainerScanning } + let(:report_path) { 'ee/spec/fixtures/security_reports/simple/gl-container-scanning-report.json' } - it "does not add any findings" do - expect(builder.report.findings).to be_empty + let_it_be(:affected_component) do + build(:vs_possibly_affected_component, :container_scanning, name: "glibc", + version: "2.24-11+deb9u3", purl_type: "deb") end - end - context "when supplied cylonedx is incompatible" do - let(:sbom_source) { build(:ci_reports_sbom_source, data: {}) } - - it "adds an error to the report" do - expect(builder.report.errored?).to eq(true) - expect(builder.report.errors).to match_array([ - { type: "MissingPropertiesError", - message: "Missing required gitlab:dependency_scanning CycloneDX properties" }, - { type: "MissingPropertiesError", - message: "Missing required gitlab:dependency_scanning CycloneDX properties" } - ]) + context "when components are vulnerable" do + it "builds a valid report" do + expect(builder.report.errored?).to eq(false) + expect(builder.report.warnings?).to eq(false) + end + + it "adds correct findings" do + convert_to_hash = ->(finding) { finding.to_hash.slice(*attributes) } + findings = builder.report.findings.map(&convert_to_hash) + expected_findings = expected.findings.map(&convert_to_hash) + + expect(findings).to match_array(expected_findings) + end + + it "adds correct identifiers" do + expect(builder.report.identifiers).to match_array(expected.identifiers) + end + + it "does not produce or remove findings when compared against analyzer report" do + comparer = Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer.new(ci_build.project, expected, + builder.report) + expect(comparer.added).to be_empty + expect(comparer.fixed).to be_empty + end end + + context "when supplied cylonedx is incompatible" do + let(:sbom_source) { build(:ci_reports_sbom_source, data: {}) } + + it "adds an error to the report" do + expect(builder.report.errored?).to eq(true) + expect(builder.report.errors).to match_array([ + { type: "MissingPropertiesError", + message: "Missing required gitlab:container_scanning CycloneDX properties" } + ]) + end + end + + it_behaves_like 'it handles unsupported report types' end end end diff --git a/ee/spec/models/sbom/source_spec.rb b/ee/spec/models/sbom/source_spec.rb index 2f747060237c99e6dd2eff516e380ecdc833609f..120b02098351b00ea64b1fecffbb968a2eb67fc5 100644 --- a/ee/spec/models/sbom/source_spec.rb +++ b/ee/spec/models/sbom/source_spec.rb @@ -119,34 +119,69 @@ end describe 'readers' do - let(:source_attributes) do - { - 'category' => 'development', - 'input_file' => { 'path' => 'package-lock.json' }, - 'source_file' => { 'path' => 'package.json' }, - 'package_manager' => { 'name' => 'npm' }, - 'language' => { 'name' => 'JavaScript' } - } - end - let(:source) { build(:sbom_source, source: source_attributes) } - describe '#packager' do - subject { source.packager } + context 'for dependency scanning' do + let(:source_attributes) do + { + 'category' => 'development', + 'input_file' => { 'path' => 'package-lock.json' }, + 'source_file' => { 'path' => 'package.json' }, + 'package_manager' => { 'name' => 'npm' }, + 'language' => { 'name' => 'JavaScript' } + } + end + + describe '#packager' do + subject { source.packager } - it { is_expected.to eq('npm') } - end + it { is_expected.to eq('npm') } + end - describe '#input_file_path' do - subject { source.input_file_path } + describe '#input_file_path' do + subject { source.input_file_path } - it { is_expected.to eq('package-lock.json') } + it { is_expected.to eq('package-lock.json') } + end + + describe '#source_file_path' do + subject { source.source_file_path } + + it { is_expected.to eq('package.json') } + end end - describe '#source_file_path' do - subject { source.source_file_path } + context "for container scanning" do + let(:source_attributes) do + { + "image" => { "name" => "rhel", "tag" => "7.1" }, + "operating_system" => { "name" => "Red Hat Enterprise Linux", "version" => "7" } + } + end + + describe "#image_name" do + subject { source.image_name } + + it { is_expected.to eq("rhel") } + end + + describe "#image_tag" do + subject { source.image_tag } + + it { is_expected.to eq("7.1") } + end - it { is_expected.to eq('package.json') } + describe "#operating_system_name" do + subject { source.operating_system_name } + + it { is_expected.to eq("Red Hat Enterprise Linux") } + end + + describe "#operating_system_version" do + subject { source.operating_system_version } + + it { is_expected.to eq("7") } + end end end end diff --git a/ee/spec/services/security/vulnerability_scanning/create_vulnerability_service_spec.rb b/ee/spec/services/security/vulnerability_scanning/create_vulnerability_service_spec.rb index c1f79f38ab4f45613478feb9b32a25547c495b55..9a58d25ae6b5c2bc7985bbea889f1b4fcdcbeed1 100644 --- a/ee/spec/services/security/vulnerability_scanning/create_vulnerability_service_spec.rb +++ b/ee/spec/services/security/vulnerability_scanning/create_vulnerability_service_spec.rb @@ -52,7 +52,7 @@ context 'when the component type is not supported' do let(:affected_components) do [ - build(:vs_possibly_affected_component, purl_type: 'apk', pipeline: pipeline, project: pipeline.project) + build(:vs_possibly_affected_component, purl_type: 'foo', pipeline: pipeline, project: pipeline.project) ] end diff --git a/lib/gitlab/ci/reports/sbom/source.rb b/lib/gitlab/ci/reports/sbom/source.rb index b7af6ea17c3d1555ddf7b227946dc0f89bd4e3a5..7d284b5babfee2a4304c1b38d1cd627fc392d304 100644 --- a/lib/gitlab/ci/reports/sbom/source.rb +++ b/lib/gitlab/ci/reports/sbom/source.rb @@ -5,28 +5,14 @@ module Ci module Reports module Sbom class Source + include SourceHelper + attr_reader :source_type, :data def initialize(type:, data:) @source_type = type @data = data end - - def source_file_path - data.dig('source_file', 'path') - end - - def input_file_path - data.dig('input_file', 'path') - end - - def packager - data.dig('package_manager', 'name') - end - - def language - data.dig('language', 'name') - end end end end diff --git a/lib/gitlab/ci/reports/sbom/source_helper.rb b/lib/gitlab/ci/reports/sbom/source_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..49b606f658b02de8ea6b3375c27adc242d8a54aa --- /dev/null +++ b/lib/gitlab/ci/reports/sbom/source_helper.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Reports + module Sbom + module SourceHelper + def source_file_path + data.dig('source_file', 'path') + end + + def input_file_path + data.dig('input_file', 'path') + end + + def packager + data.dig('package_manager', 'name') + end + + def language + data.dig('language', 'name') + end + + def image_name + data.dig('image', 'name') + end + + def image_tag + data.dig('image', 'tag') + end + + def operating_system_name + data.dig('operating_system', 'name') + end + + def operating_system_version + data.dig('operating_system', 'version') + end + end + end + end + end +end diff --git a/spec/factories/ci/reports/sbom/sources.rb b/spec/factories/ci/reports/sbom/sources.rb index 688c0250b5f94b622b3b86c1c1a38c26014954f2..a82dac1d7e21e883909e5c309ad98d082a892aaa 100644 --- a/spec/factories/ci/reports/sbom/sources.rb +++ b/spec/factories/ci/reports/sbom/sources.rb @@ -2,21 +2,50 @@ FactoryBot.define do factory :ci_reports_sbom_source, class: '::Gitlab::Ci::Reports::Sbom::Source' do - type { :dependency_scanning } + dependency_scanning - transient do - sequence(:input_file_path) { |n| "subproject-#{n}/package-lock.json" } - sequence(:source_file_path) { |n| "subproject-#{n}/package.json" } + trait :dependency_scanning do + type { :dependency_scanning } + + transient do + sequence(:input_file_path) { |n| "subproject-#{n}/package-lock.json" } + sequence(:source_file_path) { |n| "subproject-#{n}/package.json" } + end + + data do + { + 'category' => 'development', + 'input_file' => { 'path' => input_file_path }, + 'source_file' => { 'path' => source_file_path }, + 'package_manager' => { 'name' => 'npm' }, + 'language' => { 'name' => 'JavaScript' } + } + end end - data do - { - 'category' => 'development', - 'input_file' => { 'path' => input_file_path }, - 'source_file' => { 'path' => source_file_path }, - 'package_manager' => { 'name' => 'npm' }, - 'language' => { 'name' => 'JavaScript' } - } + trait :container_scanning do + type { :container_scanning } + + transient do + image_name { 'photon' } + sequence(:image_tag) { |n| "5.#{n}-12345678" } + operating_system_name { 'Photon OS' } + sequence(:operating_system_version) { |n| "5.#{n}" } + end + + data do + { + 'category' => 'development', + 'image' => { + 'name' => image_name, + 'tag' => image_tag + }, + 'operating_system' => { + 'name' => operating_system_name, + 'version' => operating_system_version + } + } + end end skip_create diff --git a/spec/lib/gitlab/ci/reports/sbom/source_spec.rb b/spec/lib/gitlab/ci/reports/sbom/source_spec.rb index c1eaea511b73c9e4ae55d5689a00639d8432b37b..09a601833ad9163c4c3f298b608c249677750c50 100644 --- a/spec/lib/gitlab/ci/reports/sbom/source_spec.rb +++ b/spec/lib/gitlab/ci/reports/sbom/source_spec.rb @@ -5,47 +5,93 @@ RSpec.describe Gitlab::Ci::Reports::Sbom::Source, feature_category: :dependency_management do let(:attributes) do { - type: :dependency_scanning, - data: { - 'category' => 'development', - 'input_file' => { 'path' => 'package-lock.json' }, - 'source_file' => { 'path' => 'package.json' }, - 'package_manager' => { 'name' => 'npm' }, - 'language' => { 'name' => 'JavaScript' } - } + type: type, + data: { 'category' => 'development', + 'package_manager' => { 'name' => 'npm' }, + 'language' => { 'name' => 'JavaScript' } }.merge(extra_attributes) } end - subject { described_class.new(**attributes) } + subject(:source) { described_class.new(**attributes) } - it 'has correct attributes' do - expect(subject).to have_attributes( - source_type: attributes[:type], - data: attributes[:data] - ) - end + shared_examples_for 'it has correct common attributes' do + it 'has correct type and data' do + expect(subject).to have_attributes( + source_type: type, + data: attributes[:data] + ) + end - describe '#source_file_path' do - it 'returns the correct source_file_path' do - expect(subject.source_file_path).to eq('package.json') + describe '#packager' do + it 'returns the correct package manager name' do + expect(subject.packager).to eq("npm") + end end - end - describe '#input_file_path' do - it 'returns the correct input_file_path' do - expect(subject.input_file_path).to eq("package-lock.json") + describe '#language' do + it 'returns the correct language' do + expect(subject.language).to eq("JavaScript") + end end end - describe '#packager' do - it 'returns the correct package manager name' do - expect(subject.packager).to eq("npm") + context 'when dependency scanning' do + let(:type) { :dependency_scanning } + let(:extra_attributes) do + { + 'input_file' => { 'path' => 'package-lock.json' }, + 'source_file' => { 'path' => 'package.json' } + } + end + + it_behaves_like 'it has correct common attributes' + + describe '#source_file_path' do + it 'returns the correct source_file_path' do + expect(subject.source_file_path).to eq('package.json') + end + end + + describe '#input_file_path' do + it 'returns the correct input_file_path' do + expect(subject.input_file_path).to eq("package-lock.json") + end end end - describe '#language' do - it 'returns the correct langauge' do - expect(subject.language).to eq("JavaScript") + context 'when container scanning' do + let(:type) { :container_scanning } + let(:extra_attributes) do + { + "image" => { "name" => "rhel", "tag" => "7.1" }, + "operating_system" => { "name" => "Red Hat Enterprise Linux", "version" => "7" } + } + end + + it_behaves_like 'it has correct common attributes' + + describe "#image_name" do + subject { source.image_name } + + it { is_expected.to eq("rhel") } + end + + describe "#image_tag" do + subject { source.image_tag } + + it { is_expected.to eq("7.1") } + end + + describe "#operating_system_name" do + subject { source.operating_system_name } + + it { is_expected.to eq("Red Hat Enterprise Linux") } + end + + describe "#operating_system_version" do + subject { source.operating_system_version } + + it { is_expected.to eq("7") } end end end diff --git a/spec/models/concerns/enums/sbom_spec.rb b/spec/models/concerns/enums/sbom_spec.rb index e2f56cc637d9c2f2085eec2555ef943b374c7f00..3bbdf619a8c5c47460f20e249f7d4846ed72d57d 100644 --- a/spec/models/concerns/enums/sbom_spec.rb +++ b/spec/models/concerns/enums/sbom_spec.rb @@ -3,9 +3,9 @@ require "spec_helper" RSpec.describe Enums::Sbom, feature_category: :dependency_management do - describe '.purl_types' do - using RSpec::Parameterized::TableSyntax + using RSpec::Parameterized::TableSyntax + describe '.purl_types' do subject(:actual_purl_type) { described_class.purl_types[package_manager] } where(:given_package_manager, :expected_purl_type) do @@ -35,5 +35,63 @@ expect(actual_purl_type).to eql(expected_purl_type) end end + + it 'contains all of the dependency scanning and container scanning purl types' do + expect(described_class::DEPENDENCY_SCANNING_PURL_TYPES + described_class::CONTAINER_SCANNING_PURL_TYPES) + .to eql(described_class::PURL_TYPES.keys) + end + end + + describe '.dependency_scanning_purl_type?' do + where(:purl_type, :expected) do + :composer | false + 'composer' | true + 'conan' | true + 'gem' | true + 'golang' | true + 'maven' | true + 'npm' | true + 'nuget' | true + 'pypi' | true + 'unknown' | false + 'apk' | false + 'rpm' | false + 'deb' | false + 'wolfi' | false + end + + with_them do + it 'returns true if the purl_type is for dependency_scanning' do + actual = described_class.dependency_scanning_purl_type?(purl_type) + expect(actual).to eql(expected) + end + end + end + + describe '.container_scanning_purl_type?' do + where(:purl_type, :expected) do + 'composer' | false + 'conan' | false + 'gem' | false + 'golang' | false + 'maven' | false + 'npm' | false + 'nuget' | false + 'pypi' | false + 'unknown' | false + :apk | false + 'apk' | true + 'rpm' | true + 'deb' | true + 'cbl-mariner' | true + 'wolfi' | true + end + + with_them do + it 'returns true if the purl_type is for container_scanning' do + actual = described_class.container_scanning_purl_type?(purl_type) + expect(actual).to eql(expected) + end + end end end