diff --git a/ee/app/models/sbom/occurrence.rb b/ee/app/models/sbom/occurrence.rb index 7166f3753de75e720efe01da631e95d59d311241..65fa565ab3296021fd2eb093974f0f6fdd316d45 100644 --- a/ee/app/models/sbom/occurrence.rb +++ b/ee/app/models/sbom/occurrence.rb @@ -12,6 +12,7 @@ class Occurrence < ApplicationRecord belongs_to :project, optional: false belongs_to :pipeline, class_name: 'Ci::Pipeline' belongs_to :source + belongs_to :source_package, optional: true has_many :occurrences_vulnerabilities, class_name: 'Sbom::OccurrencesVulnerability', diff --git a/ee/app/services/sbom/ingestion/ingest_report_slice_service.rb b/ee/app/services/sbom/ingestion/ingest_report_slice_service.rb index c2f79cd17c6a4193a24bdc4a29b39f30e66dca63..1b3168bd4b83ed77b04b0cd666399d2060841f60 100644 --- a/ee/app/services/sbom/ingestion/ingest_report_slice_service.rb +++ b/ee/app/services/sbom/ingestion/ingest_report_slice_service.rb @@ -7,6 +7,7 @@ class IngestReportSliceService ::Sbom::Ingestion::Tasks::IngestComponents, ::Sbom::Ingestion::Tasks::IngestComponentVersions, ::Sbom::Ingestion::Tasks::IngestSources, + ::Sbom::Ingestion::Tasks::IngestSourcePackages, ::Sbom::Ingestion::Tasks::IngestOccurrences ].freeze diff --git a/ee/app/services/sbom/ingestion/occurrence_map.rb b/ee/app/services/sbom/ingestion/occurrence_map.rb index 697921cfa33679c3fe898f8cc96443515ada8a3a..ed22d847821e27715fef6e9d1e1e623fb577d316 100644 --- a/ee/app/services/sbom/ingestion/occurrence_map.rb +++ b/ee/app/services/sbom/ingestion/occurrence_map.rb @@ -6,7 +6,7 @@ class OccurrenceMap include Gitlab::Utils::StrongMemoize attr_reader :report_component, :report_source, :vulnerabilities - attr_accessor :component_id, :component_version_id, :source_id, :occurrence_id + attr_accessor :component_id, :component_version_id, :source_id, :occurrence_id, :source_package_id def initialize(report_component, report_source, vulnerabilities) @report_component = report_component @@ -24,6 +24,8 @@ def to_h source_id: source_id, source_type: report_source&.source_type, source: report_source&.data, + source_package_id: source_package_id, + source_package_name: report_component.source_package_name, version: version } end @@ -45,15 +47,15 @@ def vulnerability_ids end strong_memoize_attr :vulnerability_ids - delegate :packager, :input_file_path, to: :report_source, allow_nil: true - delegate :name, :version, to: :report_component - - private - def purl_type report_component.purl&.type end + delegate :packager, :input_file_path, to: :report_source, allow_nil: true + delegate :name, :version, :source_package_name, to: :report_component + + private + def vulnerabilities_info @vulnerabilities.fetch(name, version, input_file_path) end diff --git a/ee/app/services/sbom/ingestion/tasks/ingest_occurrences.rb b/ee/app/services/sbom/ingestion/tasks/ingest_occurrences.rb index 0bab96515ffecc96a9902a8e5609542f4e180ff6..64a35e6e5f6ffc238f3aa21ebdb3e549cd2aedaf 100644 --- a/ee/app/services/sbom/ingestion/tasks/ingest_occurrences.rb +++ b/ee/app/services/sbom/ingestion/tasks/ingest_occurrences.rb @@ -28,6 +28,7 @@ def attributes component_id: occurrence_map.component_id, component_version_id: occurrence_map.component_version_id, source_id: occurrence_map.source_id, + source_package_id: occurrence_map.source_package_id, commit_sha: pipeline.sha, uuid: uuid(occurrence_map), package_manager: occurrence_map.packager, diff --git a/ee/app/services/sbom/ingestion/tasks/ingest_source_packages.rb b/ee/app/services/sbom/ingestion/tasks/ingest_source_packages.rb new file mode 100644 index 0000000000000000000000000000000000000000..c2cd764e5e46c5e87aaa7607439d9859fd28b980 --- /dev/null +++ b/ee/app/services/sbom/ingestion/tasks/ingest_source_packages.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module Sbom + module Ingestion + module Tasks + class IngestSourcePackages < Base + include Gitlab::Ingestion::BulkInsertableTask + + SOURCE_PACKAGE_ATTRIBUTES = %i[name purl_type].freeze + + self.model = Sbom::SourcePackage + self.unique_by = SOURCE_PACKAGE_ATTRIBUTES + self.uses = ([:id] + SOURCE_PACKAGE_ATTRIBUTES).freeze + + private + + def after_ingest + return_data.each do |source_package_id, source_package_name, purl_type| + maps_with(source_package_name, purl_type)&.each do |occurrence_map| + occurrence_map.source_package_id = source_package_id + end + end + end + + def attributes + valid_occurrence_maps.map do |occurrence_map| + { + name: occurrence_map.source_package_name, + purl_type: occurrence_map.purl_type + } + end + end + + def valid_occurrence_maps + @valid_occurrence_maps ||= occurrence_maps.filter(&:source_package_name) + end + + def maps_with(source_package_name, purl_type) + grouped_maps[[source_package_name, purl_type]] + end + + def grouped_maps + @grouped_maps ||= valid_occurrence_maps.group_by do |occurrence_map| + report_component = occurrence_map.report_component + + [report_component.source_package_name, report_component.purl_type] + end + end + end + end + end +end diff --git a/ee/spec/factories/sbom/ingestion/occurrence_maps.rb b/ee/spec/factories/sbom/ingestion/occurrence_maps.rb index 217d961b67f3266dcce90d376f1ecab58bf7b6fe..7d3b610924e7e4e4425be0554de0fcf9ab6349ee 100644 --- a/ee/spec/factories/sbom/ingestion/occurrence_maps.rb +++ b/ee/spec/factories/sbom/ingestion/occurrence_maps.rb @@ -22,10 +22,16 @@ occurrence factory: :sbom_occurrence end + trait :with_source_package do + report_component factory: [:ci_reports_sbom_component, :with_source_package_name] + source_package factory: :sbom_source_package + end + trait :for_occurrence_ingestion do with_component with_component_version with_source + with_source_package end skip_create @@ -38,6 +44,7 @@ object.component_version_id = attributes[:component_version]&.id object.source_id = attributes[:source]&.id object.occurrence_id = attributes[:occurrence]&.id + object.source_package_id = attributes[:source_package]&.id end end end diff --git a/ee/spec/factories/sbom/source_packages.rb b/ee/spec/factories/sbom/source_packages.rb index 5c678f20fca201d53dc08f526d9b31a79f6e6d06..2dba6cee5bc761ec422dfa237bbff41fb14fd998 100644 --- a/ee/spec/factories/sbom/source_packages.rb +++ b/ee/spec/factories/sbom/source_packages.rb @@ -3,6 +3,6 @@ FactoryBot.define do factory :sbom_source_package, class: 'Sbom::SourcePackage' do purl_type { 'deb' } - name { 'perl' } + sequence(:name) { |n| "component-#{n}" } end end diff --git a/ee/spec/models/sbom/occurrence_spec.rb b/ee/spec/models/sbom/occurrence_spec.rb index b6b713652e65656eda7be568d6e64670abef4b92..7228b1882ed3a543ee6751481ae84cfcbbca7341 100644 --- a/ee/spec/models/sbom/occurrence_spec.rb +++ b/ee/spec/models/sbom/occurrence_spec.rb @@ -11,6 +11,7 @@ it { is_expected.to belong_to(:project).required } it { is_expected.to belong_to(:pipeline) } it { is_expected.to belong_to(:source) } + it { is_expected.to belong_to(:source_package) } it { is_expected.to have_many(:occurrences_vulnerabilities) } it { is_expected.to have_many(:vulnerabilities) } end diff --git a/ee/spec/services/sbom/ingestion/occurrence_map_spec.rb b/ee/spec/services/sbom/ingestion/occurrence_map_spec.rb index 29ceaec9356abf43bf480d80a72b5b923d4e7b4b..c6b66dd0890eeb3ed321a14afbda316c7f3a62d5 100644 --- a/ee/spec/services/sbom/ingestion/occurrence_map_spec.rb +++ b/ee/spec/services/sbom/ingestion/occurrence_map_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Sbom::Ingestion::OccurrenceMap, feature_category: :dependency_management do - let_it_be(:report_component) { build_stubbed(:ci_reports_sbom_component) } + let_it_be(:report_component) { build_stubbed(:ci_reports_sbom_component, source_package_name: 'source-package-name') } let_it_be(:report_source) { build_stubbed(:ci_reports_sbom_source) } let(:vulnerability_info) { create(:sbom_vulnerabilities) } @@ -17,6 +17,8 @@ source: report_source.data, source_id: nil, source_type: report_source.source_type, + source_package_id: nil, + source_package_name: report_component.source_package_name, version: report_component.version } end @@ -33,7 +35,8 @@ { component_id: 1, component_version_id: 2, - source_id: 3 + source_id: 3, + source_package_id: 4 } end @@ -41,6 +44,7 @@ occurrence_map.component_id = ids[:component_id] occurrence_map.component_version_id = ids[:component_version_id] occurrence_map.source_id = ids[:source_id] + occurrence_map.source_package_id = ids[:source_package_id] end it 'returns a hash with ids and base data' do @@ -62,6 +66,8 @@ source: nil, source_id: nil, source_type: nil, + source_package_id: nil, + source_package_name: report_component.source_package_name, version: report_component.version } ) @@ -82,6 +88,8 @@ source: report_source.data, source_id: nil, source_type: report_source.source_type, + source_package_id: nil, + source_package_name: report_component.source_package_name, version: report_component.version } ) @@ -105,6 +113,8 @@ source: report_source.data, source_id: nil, source_type: report_source.source_type, + source_package_id: nil, + source_package_name: report_component.source_package_name, version: report_component.version } ) @@ -135,6 +145,7 @@ it { is_expected.to delegate_method(:input_file_path).to(:report_source).allow_nil } it { is_expected.to delegate_method(:name).to(:report_component) } it { is_expected.to delegate_method(:version).to(:report_component) } + it { is_expected.to delegate_method(:source_package_name).to(:report_component) } end context 'without vulnerability data' do diff --git a/ee/spec/services/sbom/ingestion/tasks/ingest_occurrences_spec.rb b/ee/spec/services/sbom/ingestion/tasks/ingest_occurrences_spec.rb index b19026114b6da11885c05a4b66e9a35731341af4..d0900b7da1e500ae9d282e0e1bcb35999bac7178 100644 --- a/ee/spec/services/sbom/ingestion/tasks/ingest_occurrences_spec.rb +++ b/ee/spec/services/sbom/ingestion/tasks/ingest_occurrences_spec.rb @@ -49,6 +49,7 @@ 'commit_sha' => pipeline.sha, 'package_manager' => occurrence_map.packager, 'input_file_path' => occurrence_map.input_file_path, + 'source_package_id' => occurrence_map.source_package_id, 'licenses' => [ { 'spdx_identifier' => 'Apache-2.0', @@ -113,6 +114,15 @@ end end + context 'when there is no source package' do + let(:occurrence_maps) { create_list(:sbom_occurrence_map, 4, :for_occurrence_ingestion, source_package: nil) } + + it 'inserts records without the source package' do + expect { ingest_occurrences }.to change(Sbom::Occurrence, :count).by(4) + expect(occurrence_maps).to all(have_attributes(occurrence_id: Integer)) + end + end + context 'when there is no purl' do let(:component) { create(:ci_reports_sbom_component, purl: nil) } let(:occurrence_map) { create(:sbom_occurrence_map, :for_occurrence_ingestion, report_component: component) } diff --git a/ee/spec/services/sbom/ingestion/tasks/ingest_source_packages_spec.rb b/ee/spec/services/sbom/ingestion/tasks/ingest_source_packages_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..86ff83448a06e73cce15ebf7af350db5874d73f4 --- /dev/null +++ b/ee/spec/services/sbom/ingestion/tasks/ingest_source_packages_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sbom::Ingestion::Tasks::IngestSourcePackages, feature_category: :dependency_management do + describe '#execute' do + let_it_be(:pipeline) { build_stubbed(:ci_pipeline) } + let_it_be(:occurrence_maps) { build_list(:sbom_occurrence_map, 2, :with_source_package) } + + subject(:ingest_source_package) { described_class.new(pipeline, occurrence_maps) } + + it_behaves_like 'bulk insertable task' + + it 'creates source packages' do + expect { ingest_source_package.execute }.to change { Sbom::SourcePackage.count }.by(2) + expect(occurrence_maps).to all(have_attributes(source_package_id: Integer)) + end + + context 'when there is existing source package' do + before do + map = occurrence_maps.first.to_h + create(:sbom_source_package, name: map[:source_package_name], purl_type: map[:purl_type]) + end + + it 'does not create a new record for the existing source package' do + expect { ingest_source_package.execute }.to change { Sbom::SourcePackage.count }.by(1) + expect(occurrence_maps).to all(have_attributes(source_package_id: Integer)) + end + end + + context 'with same source package for multiple occurrences' do + let_it_be(:source_package) { build(:sbom_source_package) } + + let_it_be(:sbom_components) do + [ + build(:ci_reports_sbom_component, purl_type: source_package.purl_type, + source_package_name: source_package.name), + build(:ci_reports_sbom_component, purl_type: source_package.purl_type, + source_package_name: source_package.name), + build(:ci_reports_sbom_component, purl_type: source_package.purl_type, + source_package_name: "other_source_package"), + build(:ci_reports_sbom_component, purl_type: 'wolfi', source_package_name: source_package.name) + ] + end + + let_it_be(:occurrence_maps) do + sbom_components.map do |component| + build(:sbom_occurrence_map, report_component: component) + end + end + + it 'maps source package id with correct occurrence_maps' do + expect { ingest_source_package.execute }.to change { Sbom::SourcePackage.count }.by(3) + expect(occurrence_maps).to all(have_attributes(source_package_id: Integer)) + end + end + end +end diff --git a/spec/factories/ci/reports/sbom/components.rb b/spec/factories/ci/reports/sbom/components.rb index 231fefff99c75ce3a3c81eee6740a51a1115cb60..091bd92ab185ba163073aa085307ab6fd83ee201 100644 --- a/spec/factories/ci/reports/sbom/components.rb +++ b/spec/factories/ci/reports/sbom/components.rb @@ -22,6 +22,10 @@ ) end + trait :with_source_package_name do + sequence(:source_package_name) { |n| "source-package-name-#{n}" } + end + skip_create initialize_with do