From 58fac333cd5cf5f840c77b4aa0ea74d77878dcdc Mon Sep 17 00:00:00 2001 From: Imam Hossain Date: Wed, 3 Dec 2025 14:25:20 +0600 Subject: [PATCH 1/7] Feat: Add EPSS and KEV filtering to scan result policies Introduces a new cache for security finding enrichments, storing EPSS scores and CISA KEV status. This enables filtering vulnerabilities in scan result policies based on exploit prediction and known exploitation. --- ...esult_policy_vulnerability_attributes.json | 28 +++++++++ .../security_finding_enrichment_caches.yml | 11 ++++ ...eate_security_finding_enrichment_caches.rb | 33 +++++++++++ db/schema_migrations/20251202105409 | 1 + db/structure.sql | 36 ++++++++++++ .../scan_result_policies/findings_finder.rb | 11 ++++ ee/app/models/approval_project_rule.rb | 8 +++ ee/app/models/ee/vulnerability.rb | 4 ++ ee/app/models/security/finding.rb | 16 +++++ .../security/finding_enrichment_cache.rb | 19 ++++++ ...eexisting_states_approval_rules_service.rb | 4 +- .../update_approvals_service.rb | 2 + .../security/store_findings_service.rb | 58 +++++++++++++++++++ .../approval_policy_rule_content.json | 28 +++++++++ .../security_orchestration_policy.json | 28 +++++++++ .../security/finding_enrichment_caches.rb | 13 +++++ 16 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 db/docs/security_finding_enrichment_caches.yml create mode 100644 db/migrate/20251202105409_create_security_finding_enrichment_caches.rb create mode 100644 db/schema_migrations/20251202105409 create mode 100644 ee/app/models/security/finding_enrichment_cache.rb create mode 100644 ee/spec/factories/security/finding_enrichment_caches.rb diff --git a/app/validators/json_schemas/scan_result_policy_vulnerability_attributes.json b/app/validators/json_schemas/scan_result_policy_vulnerability_attributes.json index e0051179a1dd93..da5ebf29b56c43 100644 --- a/app/validators/json_schemas/scan_result_policy_vulnerability_attributes.json +++ b/app/validators/json_schemas/scan_result_policy_vulnerability_attributes.json @@ -8,6 +8,34 @@ }, "fix_available": { "type": "boolean" + }, + "known_exploited": { + "type": "boolean", + "description": "Filter vulnerabilities based on CISA Known Exploited Vulnerabilities (KEV) catalog. When true, only includes vulnerabilities actively exploited in the wild." + }, + "epss_score": { + "type": "object", + "description": "Filter vulnerabilities based on Exploit Prediction Scoring System (EPSS) score. EPSS estimates the probability (0-1) that a vulnerability will be exploited.", + "properties": { + "operator": { + "type": "string", + "enum": [ + "greater_than_or_equal_to" + ], + "description": "Comparison operator for EPSS score threshold." + }, + "value": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0, + "description": "EPSS score threshold value (0.0 to 1.0). For example, 0.8 represents 80% exploitation probability." + } + }, + "required": [ + "operator", + "value" + ], + "additionalProperties": false } }, "additionalProperties": false diff --git a/db/docs/security_finding_enrichment_caches.yml b/db/docs/security_finding_enrichment_caches.yml new file mode 100644 index 00000000000000..95371fb82d2c74 --- /dev/null +++ b/db/docs/security_finding_enrichment_caches.yml @@ -0,0 +1,11 @@ +--- +table_name: security_finding_enrichment_caches +classes: +- Security::FindingEnrichmentCache +feature_categories: +- security_policy_management +description: Represents a security finding enrichment cache entry to store enrichment data for security findings. +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26619 +milestone: '18.7' +gitlab_schema: gitlab_pm # See discussion on keys for pm_ tables https://gitlab.com/gitlab-org/gitlab/-/issues/434988#note_1827421068 +table_size: small diff --git a/db/migrate/20251202105409_create_security_finding_enrichment_caches.rb b/db/migrate/20251202105409_create_security_finding_enrichment_caches.rb new file mode 100644 index 00000000000000..96c0978ea4d569 --- /dev/null +++ b/db/migrate/20251202105409_create_security_finding_enrichment_caches.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class CreateSecurityFindingEnrichmentCaches < Gitlab::Database::Migration[2.3] + milestone '18.7' + + def up + create_table :security_finding_enrichment_caches, if_not_exists: true do |t| + t.uuid :finding_uuid, null: false + t.text :cve, null: false, limit: 255 + t.float :epss_score, null: false + t.boolean :is_known_exploit, default: false, null: false + + t.datetime_with_timezone :enriched_at, null: false + t.timestamps_with_timezone null: false + + t.index :finding_uuid, name: 'index_sec_finding_enrichment_caches_on_finding_uuid' + t.index :epss_score, name: 'index_sec_finding_enrichment_caches_on_epss_score' + t.index :is_known_exploit, name: 'index_sec_finding_enrichment_caches_on_is_known_exploit' + + t.index [:is_known_exploit, :epss_score], + where: "is_known_exploit = TRUE", + name: "idx_sec_finding_enrichment_caches_on_is_known_exploit_and_epss" + + t.index [:finding_uuid, :cve], + unique: true, + name: 'index_sec_finding_enrichment_caches_on_uuid_and_cve' + end + end + + def down + drop_table :security_finding_enrichment_caches + end +end diff --git a/db/schema_migrations/20251202105409 b/db/schema_migrations/20251202105409 new file mode 100644 index 00000000000000..9244e2c42c5987 --- /dev/null +++ b/db/schema_migrations/20251202105409 @@ -0,0 +1 @@ +88fbf4ad36e4b7773d1ca08f5bf7b2366148ffc5485e2c60ade31560d07205ca \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index d941aa0cce6e56..3a28012a93a787 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -26892,6 +26892,27 @@ CREATE SEQUENCE security_categories_id_seq ALTER SEQUENCE security_categories_id_seq OWNED BY security_categories.id; +CREATE TABLE security_finding_enrichment_caches ( + id bigint NOT NULL, + finding_uuid uuid NOT NULL, + cve text NOT NULL, + epss_score double precision NOT NULL, + is_known_exploit boolean DEFAULT false NOT NULL, + enriched_at timestamp with time zone NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + CONSTRAINT check_d26cd994d5 CHECK ((char_length(cve) <= 255)) +); + +CREATE SEQUENCE security_finding_enrichment_caches_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE security_finding_enrichment_caches_id_seq OWNED BY security_finding_enrichment_caches.id; + CREATE TABLE security_finding_token_statuses ( security_finding_id bigint NOT NULL, project_id bigint NOT NULL, @@ -33164,6 +33185,8 @@ ALTER TABLE ONLY security_attributes ALTER COLUMN id SET DEFAULT nextval('securi ALTER TABLE ONLY security_categories ALTER COLUMN id SET DEFAULT nextval('security_categories_id_seq'::regclass); +ALTER TABLE ONLY security_finding_enrichment_caches ALTER COLUMN id SET DEFAULT nextval('security_finding_enrichment_caches_id_seq'::regclass); + ALTER TABLE ONLY security_findings ALTER COLUMN id SET DEFAULT nextval('security_findings_id_seq'::regclass); ALTER TABLE ONLY security_inventory_filters ALTER COLUMN id SET DEFAULT nextval('security_inventory_filters_id_seq'::regclass); @@ -36961,6 +36984,9 @@ ALTER TABLE ONLY security_attributes ALTER TABLE ONLY security_categories ADD CONSTRAINT security_categories_pkey PRIMARY KEY (id); +ALTER TABLE ONLY security_finding_enrichment_caches + ADD CONSTRAINT security_finding_enrichment_caches_pkey PRIMARY KEY (id); + ALTER TABLE ONLY security_finding_token_statuses ADD CONSTRAINT security_finding_token_statuses_pkey PRIMARY KEY (security_finding_id); @@ -40206,6 +40232,8 @@ CREATE INDEX idx_scan_result_policies_on_configuration_id_id_updated_at ON scan_ CREATE INDEX idx_scan_result_policy_violations_on_policy_id_and_id ON scan_result_policy_violations USING btree (scan_result_policy_id, id); +CREATE INDEX idx_sec_finding_enrichment_caches_on_is_known_exploit_and_epss ON security_finding_enrichment_caches USING btree (is_known_exploit, epss_score) WHERE (is_known_exploit = true); + CREATE INDEX idx_sec_inv_filters_traversals_unarchived_proj_severities_sort ON security_inventory_filters USING btree (traversal_ids, project_id, id DESC) WHERE ((NOT archived) AND ((critical > 0) OR (high > 0))); CREATE INDEX idx_secret_detect_token_on_project_id ON secret_detection_token_statuses USING btree (project_id); @@ -44322,6 +44350,14 @@ CREATE UNIQUE INDEX index_scim_oauth_access_tokens_on_group_id_and_token_encrypt CREATE INDEX index_scim_oauth_access_tokens_on_organization_id ON scim_oauth_access_tokens USING btree (organization_id); +CREATE INDEX index_sec_finding_enrichment_caches_on_epss_score ON security_finding_enrichment_caches USING btree (epss_score); + +CREATE INDEX index_sec_finding_enrichment_caches_on_finding_uuid ON security_finding_enrichment_caches USING btree (finding_uuid); + +CREATE INDEX index_sec_finding_enrichment_caches_on_is_known_exploit ON security_finding_enrichment_caches USING btree (is_known_exploit); + +CREATE UNIQUE INDEX index_sec_finding_enrichment_caches_on_uuid_and_cve ON security_finding_enrichment_caches USING btree (finding_uuid, cve); + CREATE INDEX index_secret_rotation_infos_on_next_reminder_at ON secret_rotation_infos USING btree (next_reminder_at); CREATE INDEX index_security_attributes_on_namespace_id ON security_attributes USING btree (namespace_id); diff --git a/ee/app/finders/security/scan_result_policies/findings_finder.rb b/ee/app/finders/security/scan_result_policies/findings_finder.rb index b8047c2a1697a9..1b3b3ba77421d1 100644 --- a/ee/app/finders/security/scan_result_policies/findings_finder.rb +++ b/ee/app/finders/security/scan_result_policies/findings_finder.rb @@ -39,6 +39,8 @@ def execute findings = findings.fix_available if params[:fix_available] == true findings = findings.no_fix_available if params[:fix_available] == false findings = findings.with_scan_partition_number.by_uuid(params[:uuids]) if params[:uuids].present? + findings = findings.known_exploited if params[:known_exploited].present? + findings = filter_by_epss_score(findings) if params[:epss_score].present? findings end @@ -72,6 +74,15 @@ def only_new_undismissed_findings? def undismissed_security_findings(findings) findings.undismissed_by_vulnerability end + + def filter_by_epss_score(findings) + case params[:epss_score][:operator] + when 'greater_than_or_equal_to' + findings.epss_score_greater_than_or_equal_to(params[:epss_score][:value]) + else + findings + end + end end end end diff --git a/ee/app/models/approval_project_rule.rb b/ee/app/models/approval_project_rule.rb index c09ef176054774..a9e952f93059e5 100644 --- a/ee/app/models/approval_project_rule.rb +++ b/ee/app/models/approval_project_rule.rb @@ -85,6 +85,14 @@ def vulnerability_attribute_fix_available vulnerability_attributes&.dig('fix_available') end + def vulnerability_attribute_known_exploited + vulnerability_attributes&.dig('known_exploited') + end + + def vulnerability_attribute_epss_score + vulnerability_attributes&.dig('epss_score') + end + def applies_to_branch?(branch) return false if ignore_policy_rules_for_unprotected_branches?(branch) return !applies_to_all_protected_branches? if protected_branches.empty? diff --git a/ee/app/models/ee/vulnerability.rb b/ee/app/models/ee/vulnerability.rb index 74bd76c791a534..a55d4f26a2c660 100644 --- a/ee/app/models/ee/vulnerability.rb +++ b/ee/app/models/ee/vulnerability.rb @@ -165,6 +165,10 @@ module Vulnerability joins(:findings).merge(Vulnerabilities::Finding.with_fix_available(fix_available)) end + scope :with_epss_score_greater_than_or_equal_to, ->(score) do + joins(:findings).merge(Vulnerabilities::Finding.epss_score_greater_than_or_equal_to(score)) + end + scope :for_default_branch, ->(present_on_default_branch = true) { where(present_on_default_branch: present_on_default_branch) } scope :present_on_default_branch, -> { where('present_on_default_branch IS TRUE') } diff --git a/ee/app/models/security/finding.rb b/ee/app/models/security/finding.rb index e939f0cdd5249b..4f84793aefd8f2 100644 --- a/ee/app/models/security/finding.rb +++ b/ee/app/models/security/finding.rb @@ -58,6 +58,12 @@ class Finding < ::SecApplicationRecord primary_key: 'uuid', foreign_key: 'finding_uuid' + has_many :enrichment_caches, + class_name: 'Security::FindingEnrichmentCache', + inverse_of: :security_finding, + primary_key: 'uuid', + foreign_key: 'finding_uuid' + enum :severity, ::Enums::Vulnerability.severity_levels, prefix: :severity validates :uuid, presence: true @@ -165,6 +171,16 @@ class Finding < ::SecApplicationRecord ) end + scope :known_exploited, -> do + joins(:enrichment_caches).where(security_finding_enrichment_cache: { is_known_exploit: true }).distinct + end + + scope :epss_score_greater_than_or_equal_to, ->(epss_score) do + joins(:enrichment_caches).where( + Security::FindingEnrichmentCache.arel_table[:epss_score].gteq(epss_score) + ).distinct + end + scope :except_scanners, ->(scanners) do where.not(scanner: scanners) end diff --git a/ee/app/models/security/finding_enrichment_cache.rb b/ee/app/models/security/finding_enrichment_cache.rb new file mode 100644 index 00000000000000..1f5f1ebfeebd6e --- /dev/null +++ b/ee/app/models/security/finding_enrichment_cache.rb @@ -0,0 +1,19 @@ +# ee/app/models/security/finding_enrichment_cache.rb +# frozen_string_literal: true + +module Security + class FindingEnrichmentCache < ApplicationRecord + self.table_name = 'security_finding_enrichment_caches' + + belongs_to :security_finding, + primary_key: :uuid, + foreign_key: :finding_uuid, + class_name: 'Security::Finding', + inverse_of: :enrichment_caches + + validates :finding_uuid, presence: true, uniqueness: { scope: :cve } + validates :cve, presence: true, format: { with: PackageMetadata::CveEnrichment::CVE_REGEX } + validates :epss_score, presence: true + validates :is_known_exploit, inclusion: { in: [true, false] } + end +end diff --git a/ee/app/services/security/scan_result_policies/sync_preexisting_states_approval_rules_service.rb b/ee/app/services/security/scan_result_policies/sync_preexisting_states_approval_rules_service.rb index b455ae4121ebfe..7a81010156722f 100644 --- a/ee/app/services/security/scan_result_policies/sync_preexisting_states_approval_rules_service.rb +++ b/ee/app/services/security/scan_result_policies/sync_preexisting_states_approval_rules_service.rb @@ -61,7 +61,9 @@ def vulnerabilities(approval_rule) report_type: approval_rule.scanners, fix_available: approval_rule.vulnerability_attribute_fix_available, false_positive: approval_rule.vulnerability_attribute_false_positive, - vulnerability_age: approval_rule.scan_result_policy_read&.vulnerability_age + vulnerability_age: approval_rule.scan_result_policy_read&.vulnerability_age, + known_exploited: approval_rule.vulnerability_attribute_known_exploited, + epss_score: approval_rule.vulnerability_attribute_epss_score } ::Security::ScanResultPolicies::VulnerabilitiesFinder.new(project, finder_params).execute end diff --git a/ee/app/services/security/scan_result_policies/update_approvals_service.rb b/ee/app/services/security/scan_result_policies/update_approvals_service.rb index ab1795f91a474c..70105d79576604 100644 --- a/ee/app/services/security/scan_result_policies/update_approvals_service.rb +++ b/ee/app/services/security/scan_result_policies/update_approvals_service.rb @@ -290,6 +290,8 @@ def findings_uuids(pipeline, approval_rule, pipeline_ids, check_dismissed = fals scanners: approval_rule.scanners, fix_available: approval_rule.vulnerability_attribute_fix_available, false_positive: approval_rule.vulnerability_attribute_false_positive, + known_exploited: approval_rule.vulnerability_attribute_known_exploited, + epss_score: approval_rule.vulnerability_attribute_epss_score, check_dismissed: check_dismissed } diff --git a/ee/app/services/security/store_findings_service.rb b/ee/app/services/security/store_findings_service.rb index 703c295e6482d3..9ef98bf1cf145c 100644 --- a/ee/app/services/security/store_findings_service.rb +++ b/ee/app/services/security/store_findings_service.rb @@ -46,6 +46,8 @@ def report_findings def store_finding_batch(batch) batch.map { finding_data(_1) } .then { import_batch(_1) } + + populate_finding_enrichment_cache(batch) end def import_batch(report_finding_data) @@ -92,5 +94,61 @@ def finding_data_for(report_finding) raw_source_code_extract: report_finding.raw_source_code_extract } end + + def populate_finding_enrichment_cache(report_findings) + cve_names = extract_unique_cve_names(report_findings) + return if cve_names.empty? + + enrichments_by_cve = load_enrichments_by_cve(cve_names) + return if enrichments_by_cve.empty? + + cache_records = build_cache_records(report_findings, enrichments_by_cve) + return if cache_records.empty? + + Security::FindingEnrichmentCache.upsert_all( + cache_records, + unique_by: %i[finding_uuid cve] + ) + rescue StandardError => exception + Gitlab::ErrorTracking.track_exception( + exception, + security_scan_id: security_scan.id, + project_id: project.id, + class: self.class.name + ) + end + + def extract_unique_cve_names(report_findings) + report_findings + .flat_map(&:identifiers) + .select(&:cve?) + .filter_map(&:name) + .uniq + end + + def load_enrichments_by_cve(cve_names) + PackageMetadata::CveEnrichment + .by_cves(cve_names) + .index_by(&:cve) + end + + def build_cache_records(report_findings, enrichments_by_cve) + report_findings.flat_map do |finding| + cve_identifiers = finding.identifiers.select(&:cve?) + next [] if cve_identifiers.empty? + + cve_identifiers.filter_map do |identifier| + enrichment = enrichments_by_cve[identifier.name] + next unless enrichment + + { + finding_uuid: finding.uuid, + cve: enrichment.cve, + epss_score: enrichment.epss_score, + is_known_exploit: enrichment.is_known_exploit + } + end + end + end end end diff --git a/ee/app/validators/json_schemas/approval_policy_rule_content.json b/ee/app/validators/json_schemas/approval_policy_rule_content.json index dab31171f35b91..1f6d0c88f78032 100644 --- a/ee/app/validators/json_schemas/approval_policy_rule_content.json +++ b/ee/app/validators/json_schemas/approval_policy_rule_content.json @@ -114,6 +114,34 @@ }, "fix_available": { "type": "boolean" + }, + "known_exploited": { + "type": "boolean", + "description": "Filter vulnerabilities based on CISA Known Exploited Vulnerabilities (KEV) catalog. When true, only includes vulnerabilities actively exploited in the wild." + }, + "epss_score": { + "type": "object", + "description": "Filter vulnerabilities based on Exploit Prediction Scoring System (EPSS) score. EPSS estimates the probability (0-1) that a vulnerability will be exploited.", + "properties": { + "operator": { + "type": "string", + "enum": [ + "greater_than_or_equal_to" + ], + "description": "Comparison operator for EPSS score threshold." + }, + "value": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0, + "description": "EPSS score threshold value (0.0 to 1.0). For example, 0.8 represents 80% exploitation probability." + } + }, + "required": [ + "operator", + "value" + ], + "additionalProperties": false } }, "additionalProperties": false diff --git a/ee/app/validators/json_schemas/security_orchestration_policy.json b/ee/app/validators/json_schemas/security_orchestration_policy.json index 785013f42d4405..c2ab586d249497 100644 --- a/ee/app/validators/json_schemas/security_orchestration_policy.json +++ b/ee/app/validators/json_schemas/security_orchestration_policy.json @@ -1279,6 +1279,34 @@ }, "fix_available": { "type": "boolean" + }, + "known_exploited": { + "type": "boolean", + "description": "Filter vulnerabilities based on CISA Known Exploited Vulnerabilities (KEV) catalog. When true, only includes vulnerabilities actively exploited in the wild." + }, + "epss_score": { + "type": "object", + "description": "Filter vulnerabilities based on Exploit Prediction Scoring System (EPSS) score. EPSS estimates the probability (0-1) that a vulnerability will be exploited.", + "properties": { + "operator": { + "type": "string", + "enum": [ + "greater_than_or_equal_to" + ], + "description": "Comparison operator for EPSS score threshold." + }, + "value": { + "type": "number", + "minimum": 0.0, + "maximum": 1.0, + "description": "EPSS score threshold value (0.0 to 1.0). For example, 0.8 represents 80% exploitation probability." + } + }, + "required": [ + "operator", + "value" + ], + "additionalProperties": false } }, "additionalProperties": false diff --git a/ee/spec/factories/security/finding_enrichment_caches.rb b/ee/spec/factories/security/finding_enrichment_caches.rb new file mode 100644 index 00000000000000..4acb7828c6b322 --- /dev/null +++ b/ee/spec/factories/security/finding_enrichment_caches.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :security_finding_enrichment_cache, class: 'Security::FindingEnrichmentCache' do + cve { "CVE-2024-27971" } + epss_score { rand(0.0..1.0).round(2) } + is_known_exploit { false } + + trait :known_exploit do + is_known_exploit { true } + end + end +end -- GitLab From a516dc488979165db17bd453328457d32912555d Mon Sep 17 00:00:00 2001 From: Imam Hossain Date: Wed, 3 Dec 2025 21:08:48 +0600 Subject: [PATCH 2/7] Filter vulnerabilities by kev and epss --- .../scan_result_policies/vulnerabilities_finder.rb | 13 ++++++++++++- ee/app/models/approval_project_rule.rb | 2 ++ ee/app/models/concerns/approval_rule_like.rb | 8 ++++++++ ee/app/models/ee/vulnerability.rb | 4 ++++ ee/app/models/security/finding.rb | 2 +- ee/app/models/vulnerabilities/finding.rb | 12 +++++++++++- 6 files changed, 38 insertions(+), 3 deletions(-) diff --git a/ee/app/finders/security/scan_result_policies/vulnerabilities_finder.rb b/ee/app/finders/security/scan_result_policies/vulnerabilities_finder.rb index 926ee0299002da..64f3224c4cbf08 100644 --- a/ee/app/finders/security/scan_result_policies/vulnerabilities_finder.rb +++ b/ee/app/finders/security/scan_result_policies/vulnerabilities_finder.rb @@ -19,7 +19,7 @@ def initialize(project, params = {}) attr_reader :project, :params - def execute + def execute # rubocop:disable Metrics/AbcSize -- Needs refactoring vulnerabilities = project.vulnerabilities.for_default_branch vulnerabilities = vulnerabilities.with_limit(params[:limit]) if params[:limit].present? @@ -29,6 +29,8 @@ def execute vulnerabilities = vulnerabilities.by_age(vulnerability_age[:operator], age_in_days) if vulnerability_age_valid? vulnerabilities = vulnerabilities.with_fix_available(params[:fix_available]) unless params[:fix_available].nil? vulnerabilities = vulnerabilities.with_findings_by_uuid(params[:uuids]) if params[:uuids].present? + vulnerabilities = vulnerabilities.known_exploited if params[:known_exploited].present? + vulnerabilities = filter_by_epss_score(vulnerabilities) if params[:epss_score].present? unless params[:false_positive].nil? vulnerabilities = vulnerabilities.with_false_positive(params[:false_positive]) @@ -53,6 +55,15 @@ def vulnerability_age_valid? def age_in_days vulnerability_age[:value] * INTERVAL_IN_DAYS[vulnerability_age[:interval]] end + + def filter_by_epss_score(vulnerabilities) + case params[:epss_score][:operator] + when 'greater_than_or_equal_to' + vulnerabilities.with_epss_score_greater_than_or_equal_to(params[:epss_score][:value]) + else + vulnerabilities + end + end end end end diff --git a/ee/app/models/approval_project_rule.rb b/ee/app/models/approval_project_rule.rb index a9e952f93059e5..7cb39398043b6b 100644 --- a/ee/app/models/approval_project_rule.rb +++ b/ee/app/models/approval_project_rule.rb @@ -85,10 +85,12 @@ def vulnerability_attribute_fix_available vulnerability_attributes&.dig('fix_available') end + override :vulnerability_attribute_known_exploited def vulnerability_attribute_known_exploited vulnerability_attributes&.dig('known_exploited') end + override :vulnerability_attribute_epss_score def vulnerability_attribute_epss_score vulnerability_attributes&.dig('epss_score') end diff --git a/ee/app/models/concerns/approval_rule_like.rb b/ee/app/models/concerns/approval_rule_like.rb index 292e7d7f933165..9a961543b5bdeb 100644 --- a/ee/app/models/concerns/approval_rule_like.rb +++ b/ee/app/models/concerns/approval_rule_like.rb @@ -97,6 +97,14 @@ def vulnerability_attribute_fix_available nil end + def vulnerability_attribute_known_exploited + nil + end + + def vulnerability_attribute_epss_score + nil + end + def audit_add(_model) raise NotImplementedError end diff --git a/ee/app/models/ee/vulnerability.rb b/ee/app/models/ee/vulnerability.rb index a55d4f26a2c660..9d898294af613c 100644 --- a/ee/app/models/ee/vulnerability.rb +++ b/ee/app/models/ee/vulnerability.rb @@ -165,6 +165,10 @@ module Vulnerability joins(:findings).merge(Vulnerabilities::Finding.with_fix_available(fix_available)) end + scope :known_exploited, -> do + joins(:findings).merge(Vulnerabilities::Finding.known_exploited) + end + scope :with_epss_score_greater_than_or_equal_to, ->(score) do joins(:findings).merge(Vulnerabilities::Finding.epss_score_greater_than_or_equal_to(score)) end diff --git a/ee/app/models/security/finding.rb b/ee/app/models/security/finding.rb index 4f84793aefd8f2..52bca077dfcac6 100644 --- a/ee/app/models/security/finding.rb +++ b/ee/app/models/security/finding.rb @@ -172,7 +172,7 @@ class Finding < ::SecApplicationRecord end scope :known_exploited, -> do - joins(:enrichment_caches).where(security_finding_enrichment_cache: { is_known_exploit: true }).distinct + joins(:enrichment_caches).where(Security::FindingEnrichmentCache.arel_table[:is_known_exploit].eq(true)).distinct end scope :epss_score_greater_than_or_equal_to, ->(epss_score) do diff --git a/ee/app/models/vulnerabilities/finding.rb b/ee/app/models/vulnerabilities/finding.rb index 20408cb6d7ad79..a66087d77471cf 100644 --- a/ee/app/models/vulnerabilities/finding.rb +++ b/ee/app/models/vulnerabilities/finding.rb @@ -90,7 +90,7 @@ class Finding < ::SecApplicationRecord has_many :finding_identifiers, class_name: 'Vulnerabilities::FindingIdentifier', inverse_of: :finding, foreign_key: 'occurrence_id' has_many :identifiers, through: :finding_identifiers, class_name: 'Vulnerabilities::Identifier' - has_many :cve_identifiers, -> { where('LOWER(external_type) = ?', 'cve') }, through: :finding_identifiers, source: :identifier, class_name: 'Vulnerabilities::Identifier' + has_many :cve_identifiers, -> { where('LOWER(vulnerability_identifiers.external_type) = ?', 'cve') }, through: :finding_identifiers, source: :identifier, class_name: 'Vulnerabilities::Identifier' has_many :cve_enrichments, through: :cve_identifiers, disable_joins: true has_one :finding_token_status, class_name: 'Vulnerabilities::FindingTokenStatus', foreign_key: 'vulnerability_occurrence_id', inverse_of: :finding @@ -218,6 +218,16 @@ class Finding < ::SecApplicationRecord fix_available ? solution_query.or(exist_query) : solution_query.and(exist_query) end + scope :known_exploited, -> do + joins(:cve_enrichments).where(pm_cve_enrichment: { is_known_exploit: true }).distinct + end + + scope :epss_score_greater_than_or_equal_to, ->(epss_score) do + joins(:cve_enrichments).where( + PackageMetadata::CveEnrichment.arel_table[:epss_score].gteq(epss_score) + ).distinct + end + scope :scoped_project, -> { where('vulnerability_occurrences.project_id = projects.id') } scope :eager_load_vulnerability_flags, -> { includes(:vulnerability_flags) } scope :by_location_image, ->(images) do -- GitLab From 5c776a5d62d9b66800746281a8a97ac0b5a849bc Mon Sep 17 00:00:00 2001 From: Imam Hossain Date: Thu, 4 Dec 2025 15:36:00 +0600 Subject: [PATCH 3/7] Feat: Enhance EPSS score filtering with more operators This change extends EPSS score filtering to include 'greater_than', 'less_than_or_equal_to', and 'less_than' operators. It refactors CVE enrichment filtering into a unified scope, allowing more granular and flexible policy definitions based on EPSS scores. --- ...esult_policy_vulnerability_attributes.json | 5 +++- .../scan_result_policies/findings_finder.rb | 22 +++++++++------- .../vulnerabilities_finder.rb | 22 +++++++++------- .../concerns/security/scan_result_policy.rb | 1 + ee/app/models/ee/vulnerability.rb | 14 ++++++----- ee/app/models/security/finding.rb | 25 +++++++++++++------ ee/app/models/vulnerabilities/finding.rb | 25 +++++++++++++------ .../approval_policy_rule_content.json | 5 +++- .../security_orchestration_policy.json | 5 +++- 9 files changed, 83 insertions(+), 41 deletions(-) diff --git a/app/validators/json_schemas/scan_result_policy_vulnerability_attributes.json b/app/validators/json_schemas/scan_result_policy_vulnerability_attributes.json index da5ebf29b56c43..a21108e297293f 100644 --- a/app/validators/json_schemas/scan_result_policy_vulnerability_attributes.json +++ b/app/validators/json_schemas/scan_result_policy_vulnerability_attributes.json @@ -20,7 +20,10 @@ "operator": { "type": "string", "enum": [ - "greater_than_or_equal_to" + "greater_than_or_equal_to", + "greater_than", + "less_than_or_equal_to", + "less_than" ], "description": "Comparison operator for EPSS score threshold." }, diff --git a/ee/app/finders/security/scan_result_policies/findings_finder.rb b/ee/app/finders/security/scan_result_policies/findings_finder.rb index 1b3b3ba77421d1..b91c75a03e29ea 100644 --- a/ee/app/finders/security/scan_result_policies/findings_finder.rb +++ b/ee/app/finders/security/scan_result_policies/findings_finder.rb @@ -39,8 +39,7 @@ def execute findings = findings.fix_available if params[:fix_available] == true findings = findings.no_fix_available if params[:fix_available] == false findings = findings.with_scan_partition_number.by_uuid(params[:uuids]) if params[:uuids].present? - findings = findings.known_exploited if params[:known_exploited].present? - findings = filter_by_epss_score(findings) if params[:epss_score].present? + findings = apply_cve_enrichment_filters(findings) if params[:known_exploited].present? || epss_score_valid? findings end @@ -75,13 +74,18 @@ def undismissed_security_findings(findings) findings.undismissed_by_vulnerability end - def filter_by_epss_score(findings) - case params[:epss_score][:operator] - when 'greater_than_or_equal_to' - findings.epss_score_greater_than_or_equal_to(params[:epss_score][:value]) - else - findings - end + def epss_score_valid? + params[:epss_score].present? && + params[:epss_score][:operator].in?(::Security::ScanResultPolicy::VALID_EPSS_SCORE_OPERATORS) && + params[:epss_score][:value].is_a?(Numeric) + end + + def apply_cve_enrichment_filters(findings) + findings.with_cve_enrichment_filters( + known_exploited: params[:known_exploited], + epss_operator: params.dig(:epss_score, :operator), + epss_value: params.dig(:epss_score, :value) + ) end end end diff --git a/ee/app/finders/security/scan_result_policies/vulnerabilities_finder.rb b/ee/app/finders/security/scan_result_policies/vulnerabilities_finder.rb index 64f3224c4cbf08..262fe059dff923 100644 --- a/ee/app/finders/security/scan_result_policies/vulnerabilities_finder.rb +++ b/ee/app/finders/security/scan_result_policies/vulnerabilities_finder.rb @@ -29,8 +29,7 @@ def execute # rubocop:disable Metrics/AbcSize -- Needs refactoring vulnerabilities = vulnerabilities.by_age(vulnerability_age[:operator], age_in_days) if vulnerability_age_valid? vulnerabilities = vulnerabilities.with_fix_available(params[:fix_available]) unless params[:fix_available].nil? vulnerabilities = vulnerabilities.with_findings_by_uuid(params[:uuids]) if params[:uuids].present? - vulnerabilities = vulnerabilities.known_exploited if params[:known_exploited].present? - vulnerabilities = filter_by_epss_score(vulnerabilities) if params[:epss_score].present? + vulnerabilities = apply_cve_enrichment_filters(vulnerabilities) if params[:known_exploited] || epss_score_valid? unless params[:false_positive].nil? vulnerabilities = vulnerabilities.with_false_positive(params[:false_positive]) @@ -56,13 +55,18 @@ def age_in_days vulnerability_age[:value] * INTERVAL_IN_DAYS[vulnerability_age[:interval]] end - def filter_by_epss_score(vulnerabilities) - case params[:epss_score][:operator] - when 'greater_than_or_equal_to' - vulnerabilities.with_epss_score_greater_than_or_equal_to(params[:epss_score][:value]) - else - vulnerabilities - end + def epss_score_valid? + params[:epss_score].present? && + params[:epss_score][:operator].in?(::Security::ScanResultPolicy::VALID_EPSS_SCORE_OPERATORS) && + params[:epss_score][:value].is_a?(Numeric) + end + + def apply_cve_enrichment_filters(vulnerabilities) + vulnerabilities.with_cve_enrichment_filters( + known_exploited: params[:known_exploited], + epss_operator: params.dig(:epss_score, :operator), + epss_value: params.dig(:epss_score, :value) + ) end end end diff --git a/ee/app/models/concerns/security/scan_result_policy.rb b/ee/app/models/concerns/security/scan_result_policy.rb index ac7962f810b740..7feae573b35baa 100644 --- a/ee/app/models/concerns/security/scan_result_policy.rb +++ b/ee/app/models/concerns/security/scan_result_policy.rb @@ -21,6 +21,7 @@ module ScanResultPolicy SEND_BOT_MESSAGE = 'send_bot_message' ALLOWED_ROLES = %w[developer maintainer owner].freeze + VALID_EPSS_SCORE_OPERATORS = %i[greater_than_or_equal_to greater_than less_than_or_equal_to less_than].freeze included do has_many :scan_result_policy_reads, diff --git a/ee/app/models/ee/vulnerability.rb b/ee/app/models/ee/vulnerability.rb index 9d898294af613c..9903cb1d61a849 100644 --- a/ee/app/models/ee/vulnerability.rb +++ b/ee/app/models/ee/vulnerability.rb @@ -165,12 +165,14 @@ module Vulnerability joins(:findings).merge(Vulnerabilities::Finding.with_fix_available(fix_available)) end - scope :known_exploited, -> do - joins(:findings).merge(Vulnerabilities::Finding.known_exploited) - end - - scope :with_epss_score_greater_than_or_equal_to, ->(score) do - joins(:findings).merge(Vulnerabilities::Finding.epss_score_greater_than_or_equal_to(score)) + scope :with_cve_enrichment_filters, ->(known_exploited: nil, epss_operator: nil, epss_value: nil) do + joins(:findings).merge( + Vulnerabilities::Finding.with_cve_enrichment_filters( + known_exploited: known_exploited, + epss_operator: epss_operator, + epss_value: epss_value + ) + ) end scope :for_default_branch, ->(present_on_default_branch = true) { where(present_on_default_branch: present_on_default_branch) } diff --git a/ee/app/models/security/finding.rb b/ee/app/models/security/finding.rb index 52bca077dfcac6..7ec28b856912f1 100644 --- a/ee/app/models/security/finding.rb +++ b/ee/app/models/security/finding.rb @@ -171,14 +171,25 @@ class Finding < ::SecApplicationRecord ) end - scope :known_exploited, -> do - joins(:enrichment_caches).where(Security::FindingEnrichmentCache.arel_table[:is_known_exploit].eq(true)).distinct - end + scope :with_cve_enrichment_filters, ->(known_exploited: nil, epss_operator: nil, epss_value: nil) do + break none unless known_exploited || (epss_operator && epss_value) + + enrichments = Security::FindingEnrichmentCache.arel_table + query = joins(:enrichment_caches) + query = query.where(enrichments[:is_known_exploit].eq(true)) if known_exploited + + if epss_operator && epss_value + epss_score = enrichments[:epss_score] + condition = case epss_operator.to_sym + when :greater_than_or_equal_to then epss_score.gteq(epss_value) + when :greater_than then epss_score.gt(epss_value) + when :less_than_or_equal_to then epss_score.lteq(epss_value) + when :less_than then epss_score.lt(epss_value) + end + query = query.where(condition) if condition + end - scope :epss_score_greater_than_or_equal_to, ->(epss_score) do - joins(:enrichment_caches).where( - Security::FindingEnrichmentCache.arel_table[:epss_score].gteq(epss_score) - ).distinct + query.distinct end scope :except_scanners, ->(scanners) do diff --git a/ee/app/models/vulnerabilities/finding.rb b/ee/app/models/vulnerabilities/finding.rb index a66087d77471cf..3d5b86bc29185e 100644 --- a/ee/app/models/vulnerabilities/finding.rb +++ b/ee/app/models/vulnerabilities/finding.rb @@ -218,14 +218,25 @@ class Finding < ::SecApplicationRecord fix_available ? solution_query.or(exist_query) : solution_query.and(exist_query) end - scope :known_exploited, -> do - joins(:cve_enrichments).where(pm_cve_enrichment: { is_known_exploit: true }).distinct - end + scope :with_cve_enrichment_filters, ->(known_exploited: nil, epss_operator: nil, epss_value: nil) do + break none unless known_exploited || (epss_operator && epss_value) + + enrichments = PackageMetadata::CveEnrichment.arel_table + query = joins(:cve_enrichments) + query = query.where(enrichments[:is_known_exploit].eq(true)) if known_exploited + + if epss_operator && epss_value + epss_score = enrichments[:epss_score] + condition = case epss_operator.to_sym + when :greater_than_or_equal_to then epss_score.gteq(epss_value) + when :greater_than then epss_score.gt(epss_value) + when :less_than_or_equal_to then epss_score.lteq(epss_value) + when :less_than then epss_score.lt(epss_value) + end + query = query.where(condition) if condition + end - scope :epss_score_greater_than_or_equal_to, ->(epss_score) do - joins(:cve_enrichments).where( - PackageMetadata::CveEnrichment.arel_table[:epss_score].gteq(epss_score) - ).distinct + query.distinct end scope :scoped_project, -> { where('vulnerability_occurrences.project_id = projects.id') } diff --git a/ee/app/validators/json_schemas/approval_policy_rule_content.json b/ee/app/validators/json_schemas/approval_policy_rule_content.json index 1f6d0c88f78032..a84e5e51143116 100644 --- a/ee/app/validators/json_schemas/approval_policy_rule_content.json +++ b/ee/app/validators/json_schemas/approval_policy_rule_content.json @@ -126,7 +126,10 @@ "operator": { "type": "string", "enum": [ - "greater_than_or_equal_to" + "greater_than_or_equal_to", + "greater_than", + "less_than_or_equal_to", + "less_than" ], "description": "Comparison operator for EPSS score threshold." }, diff --git a/ee/app/validators/json_schemas/security_orchestration_policy.json b/ee/app/validators/json_schemas/security_orchestration_policy.json index c2ab586d249497..8558dd45fd303e 100644 --- a/ee/app/validators/json_schemas/security_orchestration_policy.json +++ b/ee/app/validators/json_schemas/security_orchestration_policy.json @@ -1291,7 +1291,10 @@ "operator": { "type": "string", "enum": [ - "greater_than_or_equal_to" + "greater_than_or_equal_to", + "greater_than", + "less_than_or_equal_to", + "less_than" ], "description": "Comparison operator for EPSS score threshold." }, -- GitLab From e4b72a54791bbb343cb567b5bc3a7e9a202de747 Mon Sep 17 00:00:00 2001 From: Imam Hossain Date: Thu, 4 Dec 2025 18:26:03 +0600 Subject: [PATCH 4/7] Fix score extraction --- ...5409_create_security_finding_enrichment_caches.rb | 1 - db/structure.sql | 1 - ee/app/models/approval_project_rule.rb | 12 ------------ ee/app/models/concerns/approval_rule_like.rb | 6 ++++-- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/db/migrate/20251202105409_create_security_finding_enrichment_caches.rb b/db/migrate/20251202105409_create_security_finding_enrichment_caches.rb index 96c0978ea4d569..d874fd0711bf87 100644 --- a/db/migrate/20251202105409_create_security_finding_enrichment_caches.rb +++ b/db/migrate/20251202105409_create_security_finding_enrichment_caches.rb @@ -10,7 +10,6 @@ def up t.float :epss_score, null: false t.boolean :is_known_exploit, default: false, null: false - t.datetime_with_timezone :enriched_at, null: false t.timestamps_with_timezone null: false t.index :finding_uuid, name: 'index_sec_finding_enrichment_caches_on_finding_uuid' diff --git a/db/structure.sql b/db/structure.sql index 3a28012a93a787..eb2dfdc149a7e9 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -26898,7 +26898,6 @@ CREATE TABLE security_finding_enrichment_caches ( cve text NOT NULL, epss_score double precision NOT NULL, is_known_exploit boolean DEFAULT false NOT NULL, - enriched_at timestamp with time zone NOT NULL, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, CONSTRAINT check_d26cd994d5 CHECK ((char_length(cve) <= 255)) diff --git a/ee/app/models/approval_project_rule.rb b/ee/app/models/approval_project_rule.rb index 7cb39398043b6b..3841a90455636e 100644 --- a/ee/app/models/approval_project_rule.rb +++ b/ee/app/models/approval_project_rule.rb @@ -73,8 +73,6 @@ class ApprovalProjectRule < ApplicationRecord validates :severity_levels, inclusion: { in: ::Enums::Vulnerability.severity_levels.keys } validates :vulnerability_states, inclusion: { in: APPROVAL_VULNERABILITY_STATES.keys } - delegate :vulnerability_attributes, to: :scan_result_policy_read, allow_nil: true - override :vulnerability_attribute_false_positive def vulnerability_attribute_false_positive vulnerability_attributes&.dig('false_positive') @@ -85,16 +83,6 @@ def vulnerability_attribute_fix_available vulnerability_attributes&.dig('fix_available') end - override :vulnerability_attribute_known_exploited - def vulnerability_attribute_known_exploited - vulnerability_attributes&.dig('known_exploited') - end - - override :vulnerability_attribute_epss_score - def vulnerability_attribute_epss_score - vulnerability_attributes&.dig('epss_score') - end - def applies_to_branch?(branch) return false if ignore_policy_rules_for_unprotected_branches?(branch) return !applies_to_all_protected_branches? if protected_branches.empty? diff --git a/ee/app/models/concerns/approval_rule_like.rb b/ee/app/models/concerns/approval_rule_like.rb index 9a961543b5bdeb..6f990cb4cd2907 100644 --- a/ee/app/models/concerns/approval_rule_like.rb +++ b/ee/app/models/concerns/approval_rule_like.rb @@ -83,6 +83,8 @@ module ApprovalRuleLike scope :by_report_types, ->(report_types) { where(report_type: report_types) } end + delegate :vulnerability_attributes, to: :scan_result_policy_read, allow_nil: true + def security_report_time_window return unless approval_policy_rule @@ -98,11 +100,11 @@ def vulnerability_attribute_fix_available end def vulnerability_attribute_known_exploited - nil + vulnerability_attributes&.dig('known_exploited') end def vulnerability_attribute_epss_score - nil + vulnerability_attributes&.dig('epss_score') end def audit_add(_model) -- GitLab From f383011e8472ba2887b28266f41974cd106a5bc3 Mon Sep 17 00:00:00 2001 From: Imam Hossain Date: Fri, 5 Dec 2025 02:30:50 +0600 Subject: [PATCH 5/7] Display CVE enrichment in policy violation comments This change enhances security policy violation comments by including CVE enrichment data. It displays CVE ID, EPSS score, and known exploit status for vulnerabilities, providing critical context directly in MRs. This helps developers and security teams better assess and prioritize findings, improving the efficiency of vulnerability remediation. --- ee/app/models/ee/vulnerability.rb | 1 + ee/app/models/security/finding.rb | 4 ++++ .../policy_violation_comment.rb | 15 +++++++++++++++ .../policy_violation_details.rb | 16 ++++++++++------ 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/ee/app/models/ee/vulnerability.rb b/ee/app/models/ee/vulnerability.rb index 9903cb1d61a849..1b02e1606701fa 100644 --- a/ee/app/models/ee/vulnerability.rb +++ b/ee/app/models/ee/vulnerability.rb @@ -96,6 +96,7 @@ module Vulnerability scope :with_author, -> { preload(:author) } scope :with_author_and_project, -> { includes(:author, :project) } scope :with_findings, -> { includes(:findings) } + scope :with_findings_cve_enrichments, -> { includes(findings: :cve_enrichments) } scope :with_triaging_users, -> { preload(:resolved_by, :dismissed_by, :confirmed_by) } scope :with_state_transitions, -> { includes(:state_transitions) } scope :with_findings_by_uuid, ->(uuid) { with_findings.where(findings: { uuid: uuid }) } diff --git a/ee/app/models/security/finding.rb b/ee/app/models/security/finding.rb index 7ec28b856912f1..94772f5515eb03 100644 --- a/ee/app/models/security/finding.rb +++ b/ee/app/models/security/finding.rb @@ -200,6 +200,10 @@ class Finding < ::SecApplicationRecord preload(scan: { project: { namespace: :namespace_settings_with_ancestors_inherited_settings } }) end + scope :with_enrichment_caches, -> do + includes(:enrichment_caches) + end + delegate :scan_type, :project, :pipeline, :remediations_proxy, to: :scan, allow_nil: true delegate :sha, to: :pipeline diff --git a/ee/lib/security/scan_result_policies/policy_violation_comment.rb b/ee/lib/security/scan_result_policies/policy_violation_comment.rb index 13a094b1c95080..53720dcfd912d2 100644 --- a/ee/lib/security/scan_result_policies/policy_violation_comment.rb +++ b/ee/lib/security/scan_result_policies/policy_violation_comment.rb @@ -390,9 +390,24 @@ def build_scan_finding_violation_line(violation) end line += " (#{violation.report_type.humanize})" if violation.report_type + + # Add CVE enrichment data inline + enrichment_info = build_enrichment_info(violation) + line += "\n #{enrichment_info}" if enrichment_info.present? + line end + def build_enrichment_info(violation) + enrichments = violation.cve_enrichments + return if enrichments.empty? + + enrichments.map do |enrichment| + " - `#{enrichment.cve}` **·** EPSS: #{enrichment.epss_score} **·** " \ + "Known Exploit: #{enrichment.is_known_exploit ? 'Yes :rotating_light:' : 'No'}" + end.join("\n") + end + def any_merge_request_overview(violations, bypassable: false) list = violations.sort_by(&:name).map do |violation| description = case violation.commits diff --git a/ee/lib/security/scan_result_policies/policy_violation_details.rb b/ee/lib/security/scan_result_policies/policy_violation_details.rb index 18d1e6d89f88a6..2ec1bef6b7c835 100644 --- a/ee/lib/security/scan_result_policies/policy_violation_details.rb +++ b/ee/lib/security/scan_result_policies/policy_violation_details.rb @@ -8,7 +8,8 @@ class PolicyViolationDetails Violation = Struct.new(:report_type, :name, :scan_result_policy_id, :data, :warning, :status, :warn_mode, :security_policy, :security_policy_id, :enforcement_type, :dismissed, keyword_init: true) ViolationError = Struct.new(:report_type, :error, :data, :message, :warning, keyword_init: true) - ScanFindingViolation = Struct.new(:name, :report_type, :severity, :location, :path, keyword_init: true) + ScanFindingViolation = Struct.new(:name, :report_type, :severity, :location, :path, :cve_enrichments, + keyword_init: true) AnyMergeRequestViolation = Struct.new(:name, :commits, :warn_mode, keyword_init: true) LicenseScanningViolation = Struct.new(:license, :dependencies, :url, keyword_init: true) ComparisonPipelines = Struct.new(:report_type, :source, :target, keyword_init: true) @@ -60,7 +61,7 @@ def initialize(merge_request) end def violations - merge_request.scan_result_policy_violations.filter_map do |violation| + merge_request.scan_result_policy_violations.including_security_policies.filter_map do |violation| rule = scan_result_policy_rules[violation.scan_result_policy_id] next if rule.blank? # there may be a race condition situation where rule is missing @@ -282,14 +283,16 @@ def previously_existing_violations(uuids) return [] if uuids.blank? Security::ScanResultPolicies::VulnerabilitiesFinder.new(project, - { limit: uuids_limit, uuids: uuids.first(uuids_limit) }).execute.map do |vulnerability| + { limit: uuids_limit, uuids: uuids.first(uuids_limit) }).execute + .with_findings_cve_enrichments.map do |vulnerability| finding = vulnerability.finding ScanFindingViolation.new( report_type: finding.report_type, severity: finding.severity, path: vulnerability.present.location_link, location: finding.location.with_indifferent_access, - name: finding.name + name: finding.name, + cve_enrichments: finding.cve_enrichments ) end end @@ -299,13 +302,14 @@ def newly_detected_violations(uuids, related_pipeline_ids) Security::ScanResultPolicies::FindingsFinder.new(project, pipeline, { related_pipeline_ids: related_pipeline_ids, uuids: uuids.first(uuids_limit) }).execute - .uniq(&:uuid).map do |finding| + .with_enrichment_caches.distinct(&:uuid).map do |finding| # rubocop:disable CodeReuse/ActiveRecord -- logic specific to the service ScanFindingViolation.new( report_type: finding.report_type, severity: finding.severity, path: finding.finding_data.present? ? finding.present.blob_url : nil, location: finding.finding_data.present? ? finding.location : nil, - name: finding.finding_data.present? ? finding.name : nil + name: finding.finding_data.present? ? finding.name : nil, + cve_enrichments: finding.enrichment_caches ) end end -- GitLab From 2cd3bfcb864d5a5eb2a358fba271d28b7986b10a Mon Sep 17 00:00:00 2001 From: Imam Hossain Date: Wed, 10 Dec 2025 19:42:15 +0600 Subject: [PATCH 6/7] Symbolize keys in epss score --- ...s.yml => security_finding_enrichments.yml} | 8 ++--- ...eate_security_finding_enrichment_caches.rb | 32 ----------------- ...409_create_security_finding_enrichments.rb | 22 ++++++++++++ db/structure.sql | 34 ++++++++----------- .../scan_result_policies/findings_finder.rb | 2 +- .../vulnerabilities_finder.rb | 2 +- ee/app/models/concerns/approval_rule_like.rb | 2 +- .../models/package_metadata/cve_enrichment.rb | 9 +++++ ee/app/models/security/finding.rb | 17 ++++++---- ee/app/models/security/finding_enrichment.rb | 19 +++++++++++ .../security/finding_enrichment_cache.rb | 19 ----------- .../security/store_findings_service.rb | 16 ++++----- .../policy_violation_details.rb | 4 +-- .../security/finding_enrichment_caches.rb | 13 ------- .../factories/security/finding_enrichments.rb | 8 +++++ 15 files changed, 99 insertions(+), 108 deletions(-) rename db/docs/{security_finding_enrichment_caches.yml => security_finding_enrichments.yml} (57%) delete mode 100644 db/migrate/20251202105409_create_security_finding_enrichment_caches.rb create mode 100644 db/migrate/20251202105409_create_security_finding_enrichments.rb create mode 100644 ee/app/models/security/finding_enrichment.rb delete mode 100644 ee/app/models/security/finding_enrichment_cache.rb delete mode 100644 ee/spec/factories/security/finding_enrichment_caches.rb create mode 100644 ee/spec/factories/security/finding_enrichments.rb diff --git a/db/docs/security_finding_enrichment_caches.yml b/db/docs/security_finding_enrichments.yml similarity index 57% rename from db/docs/security_finding_enrichment_caches.yml rename to db/docs/security_finding_enrichments.yml index 95371fb82d2c74..43ff80bcd537fa 100644 --- a/db/docs/security_finding_enrichment_caches.yml +++ b/db/docs/security_finding_enrichments.yml @@ -1,11 +1,11 @@ --- -table_name: security_finding_enrichment_caches +table_name: security_finding_enrichments classes: -- Security::FindingEnrichmentCache +- Security::FindingEnrichment feature_categories: - security_policy_management -description: Represents a security finding enrichment cache entry to store enrichment data for security findings. +description: Links security findings to CVE enrichment data through a foreign key relationship. introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26619 -milestone: '18.7' +milestone: '18.8' gitlab_schema: gitlab_pm # See discussion on keys for pm_ tables https://gitlab.com/gitlab-org/gitlab/-/issues/434988#note_1827421068 table_size: small diff --git a/db/migrate/20251202105409_create_security_finding_enrichment_caches.rb b/db/migrate/20251202105409_create_security_finding_enrichment_caches.rb deleted file mode 100644 index d874fd0711bf87..00000000000000 --- a/db/migrate/20251202105409_create_security_finding_enrichment_caches.rb +++ /dev/null @@ -1,32 +0,0 @@ -# frozen_string_literal: true - -class CreateSecurityFindingEnrichmentCaches < Gitlab::Database::Migration[2.3] - milestone '18.7' - - def up - create_table :security_finding_enrichment_caches, if_not_exists: true do |t| - t.uuid :finding_uuid, null: false - t.text :cve, null: false, limit: 255 - t.float :epss_score, null: false - t.boolean :is_known_exploit, default: false, null: false - - t.timestamps_with_timezone null: false - - t.index :finding_uuid, name: 'index_sec_finding_enrichment_caches_on_finding_uuid' - t.index :epss_score, name: 'index_sec_finding_enrichment_caches_on_epss_score' - t.index :is_known_exploit, name: 'index_sec_finding_enrichment_caches_on_is_known_exploit' - - t.index [:is_known_exploit, :epss_score], - where: "is_known_exploit = TRUE", - name: "idx_sec_finding_enrichment_caches_on_is_known_exploit_and_epss" - - t.index [:finding_uuid, :cve], - unique: true, - name: 'index_sec_finding_enrichment_caches_on_uuid_and_cve' - end - end - - def down - drop_table :security_finding_enrichment_caches - end -end diff --git a/db/migrate/20251202105409_create_security_finding_enrichments.rb b/db/migrate/20251202105409_create_security_finding_enrichments.rb new file mode 100644 index 00000000000000..a93be9b74233e6 --- /dev/null +++ b/db/migrate/20251202105409_create_security_finding_enrichments.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class CreateSecurityFindingEnrichments < Gitlab::Database::Migration[2.3] + milestone '18.8' + + def up + create_table :security_finding_enrichments, if_not_exists: true do |t| + t.uuid :finding_uuid, null: false + t.references :cve_enrichment, null: false, foreign_key: { to_table: :pm_cve_enrichment, on_delete: :cascade } + + t.index [:finding_uuid, :cve_enrichment_id], + unique: true, + name: 'index_sec_finding_enrichments_on_finding_and_enrichment' + + t.timestamps_with_timezone null: false + end + end + + def down + drop_table :security_finding_enrichments + end +end diff --git a/db/structure.sql b/db/structure.sql index eb2dfdc149a7e9..90cdaa8109c1aa 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -26892,25 +26892,22 @@ CREATE SEQUENCE security_categories_id_seq ALTER SEQUENCE security_categories_id_seq OWNED BY security_categories.id; -CREATE TABLE security_finding_enrichment_caches ( +CREATE TABLE security_finding_enrichments ( id bigint NOT NULL, finding_uuid uuid NOT NULL, - cve text NOT NULL, - epss_score double precision NOT NULL, - is_known_exploit boolean DEFAULT false NOT NULL, + cve_enrichment_id bigint NOT NULL, created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL, - CONSTRAINT check_d26cd994d5 CHECK ((char_length(cve) <= 255)) + updated_at timestamp with time zone NOT NULL ); -CREATE SEQUENCE security_finding_enrichment_caches_id_seq +CREATE SEQUENCE security_finding_enrichments_id_seq START WITH 1 INCREMENT BY 1 NO MINVALUE NO MAXVALUE CACHE 1; -ALTER SEQUENCE security_finding_enrichment_caches_id_seq OWNED BY security_finding_enrichment_caches.id; +ALTER SEQUENCE security_finding_enrichments_id_seq OWNED BY security_finding_enrichments.id; CREATE TABLE security_finding_token_statuses ( security_finding_id bigint NOT NULL, @@ -33184,7 +33181,7 @@ ALTER TABLE ONLY security_attributes ALTER COLUMN id SET DEFAULT nextval('securi ALTER TABLE ONLY security_categories ALTER COLUMN id SET DEFAULT nextval('security_categories_id_seq'::regclass); -ALTER TABLE ONLY security_finding_enrichment_caches ALTER COLUMN id SET DEFAULT nextval('security_finding_enrichment_caches_id_seq'::regclass); +ALTER TABLE ONLY security_finding_enrichments ALTER COLUMN id SET DEFAULT nextval('security_finding_enrichments_id_seq'::regclass); ALTER TABLE ONLY security_findings ALTER COLUMN id SET DEFAULT nextval('security_findings_id_seq'::regclass); @@ -36983,8 +36980,8 @@ ALTER TABLE ONLY security_attributes ALTER TABLE ONLY security_categories ADD CONSTRAINT security_categories_pkey PRIMARY KEY (id); -ALTER TABLE ONLY security_finding_enrichment_caches - ADD CONSTRAINT security_finding_enrichment_caches_pkey PRIMARY KEY (id); +ALTER TABLE ONLY security_finding_enrichments + ADD CONSTRAINT security_finding_enrichments_pkey PRIMARY KEY (id); ALTER TABLE ONLY security_finding_token_statuses ADD CONSTRAINT security_finding_token_statuses_pkey PRIMARY KEY (security_finding_id); @@ -40231,8 +40228,6 @@ CREATE INDEX idx_scan_result_policies_on_configuration_id_id_updated_at ON scan_ CREATE INDEX idx_scan_result_policy_violations_on_policy_id_and_id ON scan_result_policy_violations USING btree (scan_result_policy_id, id); -CREATE INDEX idx_sec_finding_enrichment_caches_on_is_known_exploit_and_epss ON security_finding_enrichment_caches USING btree (is_known_exploit, epss_score) WHERE (is_known_exploit = true); - CREATE INDEX idx_sec_inv_filters_traversals_unarchived_proj_severities_sort ON security_inventory_filters USING btree (traversal_ids, project_id, id DESC) WHERE ((NOT archived) AND ((critical > 0) OR (high > 0))); CREATE INDEX idx_secret_detect_token_on_project_id ON secret_detection_token_statuses USING btree (project_id); @@ -44349,13 +44344,7 @@ CREATE UNIQUE INDEX index_scim_oauth_access_tokens_on_group_id_and_token_encrypt CREATE INDEX index_scim_oauth_access_tokens_on_organization_id ON scim_oauth_access_tokens USING btree (organization_id); -CREATE INDEX index_sec_finding_enrichment_caches_on_epss_score ON security_finding_enrichment_caches USING btree (epss_score); - -CREATE INDEX index_sec_finding_enrichment_caches_on_finding_uuid ON security_finding_enrichment_caches USING btree (finding_uuid); - -CREATE INDEX index_sec_finding_enrichment_caches_on_is_known_exploit ON security_finding_enrichment_caches USING btree (is_known_exploit); - -CREATE UNIQUE INDEX index_sec_finding_enrichment_caches_on_uuid_and_cve ON security_finding_enrichment_caches USING btree (finding_uuid, cve); +CREATE UNIQUE INDEX index_sec_finding_enrichments_on_finding_and_enrichment ON security_finding_enrichments USING btree (finding_uuid, cve_enrichment_id); CREATE INDEX index_secret_rotation_infos_on_next_reminder_at ON secret_rotation_infos USING btree (next_reminder_at); @@ -44369,6 +44358,8 @@ CREATE UNIQUE INDEX index_security_categories_namespace_name ON security_categor CREATE INDEX index_security_categories_on_namespace_id_where_not_deleted ON security_categories USING btree (namespace_id) WHERE (deleted_at IS NULL); +CREATE INDEX index_security_finding_enrichments_on_cve_enrichment_id ON security_finding_enrichments USING btree (cve_enrichment_id); + CREATE UNIQUE INDEX index_security_inventory_filters_on_project_id ON security_inventory_filters USING btree (project_id); CREATE INDEX index_security_inventory_filters_on_traversal_ids ON security_inventory_filters USING btree (traversal_ids); @@ -53438,6 +53429,9 @@ ALTER TABLE ONLY group_deploy_tokens ALTER TABLE ONLY reviews ADD CONSTRAINT fk_rails_64798be025 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; +ALTER TABLE ONLY security_finding_enrichments + ADD CONSTRAINT fk_rails_648935d719 FOREIGN KEY (cve_enrichment_id) REFERENCES pm_cve_enrichment(id) ON DELETE CASCADE; + ALTER TABLE ONLY operations_feature_flags ADD CONSTRAINT fk_rails_648e241be7 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; diff --git a/ee/app/finders/security/scan_result_policies/findings_finder.rb b/ee/app/finders/security/scan_result_policies/findings_finder.rb index b91c75a03e29ea..e8d81843dd1220 100644 --- a/ee/app/finders/security/scan_result_policies/findings_finder.rb +++ b/ee/app/finders/security/scan_result_policies/findings_finder.rb @@ -76,7 +76,7 @@ def undismissed_security_findings(findings) def epss_score_valid? params[:epss_score].present? && - params[:epss_score][:operator].in?(::Security::ScanResultPolicy::VALID_EPSS_SCORE_OPERATORS) && + params[:epss_score][:operator]&.to_sym&.in?(::Security::ScanResultPolicy::VALID_EPSS_SCORE_OPERATORS) && params[:epss_score][:value].is_a?(Numeric) end diff --git a/ee/app/finders/security/scan_result_policies/vulnerabilities_finder.rb b/ee/app/finders/security/scan_result_policies/vulnerabilities_finder.rb index 262fe059dff923..96d6c24d05a293 100644 --- a/ee/app/finders/security/scan_result_policies/vulnerabilities_finder.rb +++ b/ee/app/finders/security/scan_result_policies/vulnerabilities_finder.rb @@ -57,7 +57,7 @@ def age_in_days def epss_score_valid? params[:epss_score].present? && - params[:epss_score][:operator].in?(::Security::ScanResultPolicy::VALID_EPSS_SCORE_OPERATORS) && + params[:epss_score][:operator]&.to_sym&.in?(::Security::ScanResultPolicy::VALID_EPSS_SCORE_OPERATORS) && params[:epss_score][:value].is_a?(Numeric) end diff --git a/ee/app/models/concerns/approval_rule_like.rb b/ee/app/models/concerns/approval_rule_like.rb index 6f990cb4cd2907..2d87183a83929e 100644 --- a/ee/app/models/concerns/approval_rule_like.rb +++ b/ee/app/models/concerns/approval_rule_like.rb @@ -104,7 +104,7 @@ def vulnerability_attribute_known_exploited end def vulnerability_attribute_epss_score - vulnerability_attributes&.dig('epss_score') + vulnerability_attributes&.dig('epss_score')&.symbolize_keys end def audit_add(_model) diff --git a/ee/app/models/package_metadata/cve_enrichment.rb b/ee/app/models/package_metadata/cve_enrichment.rb index 0389e0154976f4..c71cf047dc1da2 100644 --- a/ee/app/models/package_metadata/cve_enrichment.rb +++ b/ee/app/models/package_metadata/cve_enrichment.rb @@ -15,6 +15,15 @@ class CveEnrichment < ApplicationRecord foreign_key: :cve, inverse_of: :cve_enrichment + has_many :finding_enrichments, + class_name: 'Security::FindingEnrichment', + inverse_of: :cve_enrichment + + has_many :security_findings, + through: :finding_enrichments, + source: :security_finding, + class_name: 'Security::Finding' + validates :cve, presence: true, format: { with: CVE_REGEX } validates :epss_score, presence: true validates :is_known_exploit, inclusion: { in: [true, false] } diff --git a/ee/app/models/security/finding.rb b/ee/app/models/security/finding.rb index 94772f5515eb03..48112cb0256bc5 100644 --- a/ee/app/models/security/finding.rb +++ b/ee/app/models/security/finding.rb @@ -58,12 +58,17 @@ class Finding < ::SecApplicationRecord primary_key: 'uuid', foreign_key: 'finding_uuid' - has_many :enrichment_caches, - class_name: 'Security::FindingEnrichmentCache', + has_many :finding_enrichments, + class_name: 'Security::FindingEnrichment', inverse_of: :security_finding, primary_key: 'uuid', foreign_key: 'finding_uuid' + has_many :cve_enrichments, + through: :finding_enrichments, + source: :cve_enrichment, + class_name: 'PackageMetadata::CveEnrichment' + enum :severity, ::Enums::Vulnerability.severity_levels, prefix: :severity validates :uuid, presence: true @@ -174,8 +179,8 @@ class Finding < ::SecApplicationRecord scope :with_cve_enrichment_filters, ->(known_exploited: nil, epss_operator: nil, epss_value: nil) do break none unless known_exploited || (epss_operator && epss_value) - enrichments = Security::FindingEnrichmentCache.arel_table - query = joins(:enrichment_caches) + enrichments = PackageMetadata::CveEnrichment.arel_table + query = joins(:cve_enrichments) query = query.where(enrichments[:is_known_exploit].eq(true)) if known_exploited if epss_operator && epss_value @@ -200,8 +205,8 @@ class Finding < ::SecApplicationRecord preload(scan: { project: { namespace: :namespace_settings_with_ancestors_inherited_settings } }) end - scope :with_enrichment_caches, -> do - includes(:enrichment_caches) + scope :with_cve_enrichments, -> do + includes(:cve_enrichments) end delegate :scan_type, :project, :pipeline, :remediations_proxy, to: :scan, allow_nil: true diff --git a/ee/app/models/security/finding_enrichment.rb b/ee/app/models/security/finding_enrichment.rb new file mode 100644 index 00000000000000..b56d548c7ae294 --- /dev/null +++ b/ee/app/models/security/finding_enrichment.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Security + class FindingEnrichment < ApplicationRecord + self.table_name = 'security_finding_enrichments' + + belongs_to :security_finding, + class_name: 'Security::Finding', + inverse_of: :finding_enrichments, + primary_key: 'uuid', + foreign_key: 'finding_uuid' + + belongs_to :cve_enrichment, + class_name: 'PackageMetadata::CveEnrichment', + inverse_of: :finding_enrichments + + validates :finding_uuid, presence: true, uniqueness: { scope: :cve_enrichment_id } + end +end diff --git a/ee/app/models/security/finding_enrichment_cache.rb b/ee/app/models/security/finding_enrichment_cache.rb deleted file mode 100644 index 1f5f1ebfeebd6e..00000000000000 --- a/ee/app/models/security/finding_enrichment_cache.rb +++ /dev/null @@ -1,19 +0,0 @@ -# ee/app/models/security/finding_enrichment_cache.rb -# frozen_string_literal: true - -module Security - class FindingEnrichmentCache < ApplicationRecord - self.table_name = 'security_finding_enrichment_caches' - - belongs_to :security_finding, - primary_key: :uuid, - foreign_key: :finding_uuid, - class_name: 'Security::Finding', - inverse_of: :enrichment_caches - - validates :finding_uuid, presence: true, uniqueness: { scope: :cve } - validates :cve, presence: true, format: { with: PackageMetadata::CveEnrichment::CVE_REGEX } - validates :epss_score, presence: true - validates :is_known_exploit, inclusion: { in: [true, false] } - end -end diff --git a/ee/app/services/security/store_findings_service.rb b/ee/app/services/security/store_findings_service.rb index 9ef98bf1cf145c..02b9efc8592f7e 100644 --- a/ee/app/services/security/store_findings_service.rb +++ b/ee/app/services/security/store_findings_service.rb @@ -102,12 +102,12 @@ def populate_finding_enrichment_cache(report_findings) enrichments_by_cve = load_enrichments_by_cve(cve_names) return if enrichments_by_cve.empty? - cache_records = build_cache_records(report_findings, enrichments_by_cve) - return if cache_records.empty? + finding_enrichments = build_finding_enrichments(report_findings, enrichments_by_cve) + return if finding_enrichments.empty? - Security::FindingEnrichmentCache.upsert_all( - cache_records, - unique_by: %i[finding_uuid cve] + Security::FindingEnrichment.upsert_all( + finding_enrichments, + unique_by: %i[finding_uuid cve_enrichment_id] ) rescue StandardError => exception Gitlab::ErrorTracking.track_exception( @@ -132,7 +132,7 @@ def load_enrichments_by_cve(cve_names) .index_by(&:cve) end - def build_cache_records(report_findings, enrichments_by_cve) + def build_finding_enrichments(report_findings, enrichments_by_cve) report_findings.flat_map do |finding| cve_identifiers = finding.identifiers.select(&:cve?) next [] if cve_identifiers.empty? @@ -143,9 +143,7 @@ def build_cache_records(report_findings, enrichments_by_cve) { finding_uuid: finding.uuid, - cve: enrichment.cve, - epss_score: enrichment.epss_score, - is_known_exploit: enrichment.is_known_exploit + cve_enrichment_id: enrichment.id } end end diff --git a/ee/lib/security/scan_result_policies/policy_violation_details.rb b/ee/lib/security/scan_result_policies/policy_violation_details.rb index 2ec1bef6b7c835..805e94cb4e73f8 100644 --- a/ee/lib/security/scan_result_policies/policy_violation_details.rb +++ b/ee/lib/security/scan_result_policies/policy_violation_details.rb @@ -302,14 +302,14 @@ def newly_detected_violations(uuids, related_pipeline_ids) Security::ScanResultPolicies::FindingsFinder.new(project, pipeline, { related_pipeline_ids: related_pipeline_ids, uuids: uuids.first(uuids_limit) }).execute - .with_enrichment_caches.distinct(&:uuid).map do |finding| # rubocop:disable CodeReuse/ActiveRecord -- logic specific to the service + .with_cve_enrichments.distinct(&:uuid).map do |finding| # rubocop:disable CodeReuse/ActiveRecord -- logic specific to the service ScanFindingViolation.new( report_type: finding.report_type, severity: finding.severity, path: finding.finding_data.present? ? finding.present.blob_url : nil, location: finding.finding_data.present? ? finding.location : nil, name: finding.finding_data.present? ? finding.name : nil, - cve_enrichments: finding.enrichment_caches + cve_enrichments: finding.cve_enrichments ) end end diff --git a/ee/spec/factories/security/finding_enrichment_caches.rb b/ee/spec/factories/security/finding_enrichment_caches.rb deleted file mode 100644 index 4acb7828c6b322..00000000000000 --- a/ee/spec/factories/security/finding_enrichment_caches.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -FactoryBot.define do - factory :security_finding_enrichment_cache, class: 'Security::FindingEnrichmentCache' do - cve { "CVE-2024-27971" } - epss_score { rand(0.0..1.0).round(2) } - is_known_exploit { false } - - trait :known_exploit do - is_known_exploit { true } - end - end -end diff --git a/ee/spec/factories/security/finding_enrichments.rb b/ee/spec/factories/security/finding_enrichments.rb new file mode 100644 index 00000000000000..f103f3061bbe94 --- /dev/null +++ b/ee/spec/factories/security/finding_enrichments.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :security_finding_enrichment, class: 'Security::FindingEnrichment' do + finding_uuid { Gitlab::UUID.v5(SecureRandom.hex) } + association :cve_enrichment, factory: :pm_cve_enrichment + end +end -- GitLab From d7c081ea3bb9a8321958ffd25cd0cb179e98914b Mon Sep 17 00:00:00 2001 From: Imam Hossain Date: Thu, 18 Dec 2025 12:58:53 +0600 Subject: [PATCH 7/7] Move validator to scan result policy --- .../security/scan_result_policies/findings_finder.rb | 4 +--- .../security/scan_result_policies/vulnerabilities_finder.rb | 4 +--- ee/app/models/concerns/security/scan_result_policy.rb | 6 ++++++ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/ee/app/finders/security/scan_result_policies/findings_finder.rb b/ee/app/finders/security/scan_result_policies/findings_finder.rb index e8d81843dd1220..c40fe9fab6f7cb 100644 --- a/ee/app/finders/security/scan_result_policies/findings_finder.rb +++ b/ee/app/finders/security/scan_result_policies/findings_finder.rb @@ -75,9 +75,7 @@ def undismissed_security_findings(findings) end def epss_score_valid? - params[:epss_score].present? && - params[:epss_score][:operator]&.to_sym&.in?(::Security::ScanResultPolicy::VALID_EPSS_SCORE_OPERATORS) && - params[:epss_score][:value].is_a?(Numeric) + ::Security::ScanResultPolicy.epss_score_valid?(params[:epss_score]) end def apply_cve_enrichment_filters(findings) diff --git a/ee/app/finders/security/scan_result_policies/vulnerabilities_finder.rb b/ee/app/finders/security/scan_result_policies/vulnerabilities_finder.rb index 96d6c24d05a293..ff8c6402f8499b 100644 --- a/ee/app/finders/security/scan_result_policies/vulnerabilities_finder.rb +++ b/ee/app/finders/security/scan_result_policies/vulnerabilities_finder.rb @@ -56,9 +56,7 @@ def age_in_days end def epss_score_valid? - params[:epss_score].present? && - params[:epss_score][:operator]&.to_sym&.in?(::Security::ScanResultPolicy::VALID_EPSS_SCORE_OPERATORS) && - params[:epss_score][:value].is_a?(Numeric) + ::Security::ScanResultPolicy.epss_score_valid?(params[:epss_score]) end def apply_cve_enrichment_filters(vulnerabilities) diff --git a/ee/app/models/concerns/security/scan_result_policy.rb b/ee/app/models/concerns/security/scan_result_policy.rb index 7feae573b35baa..328f8879d38d12 100644 --- a/ee/app/models/concerns/security/scan_result_policy.rb +++ b/ee/app/models/concerns/security/scan_result_policy.rb @@ -23,6 +23,12 @@ module ScanResultPolicy ALLOWED_ROLES = %w[developer maintainer owner].freeze VALID_EPSS_SCORE_OPERATORS = %i[greater_than_or_equal_to greater_than less_than_or_equal_to less_than].freeze + def self.epss_score_valid?(epss_score) + return false unless epss_score.present? + + epss_score[:operator]&.to_sym&.in?(VALID_EPSS_SCORE_OPERATORS) && epss_score[:value].is_a?(Numeric) + end + included do has_many :scan_result_policy_reads, class_name: 'Security::ScanResultPolicyRead', -- GitLab