Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gitlab/ci/reports')
-rw-r--r--lib/gitlab/ci/reports/security/aggregated_report.rb24
-rw-r--r--lib/gitlab/ci/reports/security/finding.rb150
-rw-r--r--lib/gitlab/ci/reports/security/finding_key.rb36
-rw-r--r--lib/gitlab/ci/reports/security/finding_signature.rb46
-rw-r--r--lib/gitlab/ci/reports/security/locations/base.rb41
-rw-r--r--lib/gitlab/ci/reports/security/locations/sast.rb33
-rw-r--r--lib/gitlab/ci/reports/security/locations/secret_detection.rb33
-rw-r--r--lib/gitlab/ci/reports/security/report.rb76
-rw-r--r--lib/gitlab/ci/reports/security/reports.rb42
-rw-r--r--lib/gitlab/ci/reports/security/vulnerability_reports_comparer.rb163
10 files changed, 644 insertions, 0 deletions
diff --git a/lib/gitlab/ci/reports/security/aggregated_report.rb b/lib/gitlab/ci/reports/security/aggregated_report.rb
new file mode 100644
index 00000000000..a8bb2196043
--- /dev/null
+++ b/lib/gitlab/ci/reports/security/aggregated_report.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+# Used to represent combined Security Reports. This is typically done for vulnerability deduplication purposes.
+
+module Gitlab
+ module Ci
+ module Reports
+ module Security
+ class AggregatedReport
+ attr_reader :findings
+
+ def initialize(reports, findings)
+ @reports = reports
+ @findings = findings
+ end
+
+ def created_at
+ @reports.map(&:created_at).compact.min
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/reports/security/finding.rb b/lib/gitlab/ci/reports/security/finding.rb
new file mode 100644
index 00000000000..dc1c51b3ed0
--- /dev/null
+++ b/lib/gitlab/ci/reports/security/finding.rb
@@ -0,0 +1,150 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Reports
+ module Security
+ class Finding
+ include ::VulnerabilityFindingHelpers
+
+ attr_reader :compare_key
+ attr_reader :confidence
+ attr_reader :identifiers
+ attr_reader :links
+ attr_reader :location
+ attr_reader :metadata_version
+ attr_reader :name
+ attr_reader :old_location
+ attr_reader :project_fingerprint
+ attr_reader :raw_metadata
+ attr_reader :report_type
+ attr_reader :scanner
+ attr_reader :scan
+ attr_reader :severity
+ attr_accessor :uuid
+ attr_accessor :overridden_uuid
+ attr_reader :remediations
+ attr_reader :details
+ attr_reader :signatures
+ attr_reader :project_id
+
+ delegate :file_path, :start_line, :end_line, to: :location
+
+ def initialize(compare_key:, identifiers:, links: [], remediations: [], location:, metadata_version:, name:, raw_metadata:, report_type:, scanner:, scan:, uuid:, confidence: nil, severity: nil, details: {}, signatures: [], project_id: nil, vulnerability_finding_signatures_enabled: false) # rubocop:disable Metrics/ParameterLists
+ @compare_key = compare_key
+ @confidence = confidence
+ @identifiers = identifiers
+ @links = links
+ @location = location
+ @metadata_version = metadata_version
+ @name = name
+ @raw_metadata = raw_metadata
+ @report_type = report_type
+ @scanner = scanner
+ @scan = scan
+ @severity = severity
+ @uuid = uuid
+ @remediations = remediations
+ @details = details
+ @signatures = signatures
+ @project_id = project_id
+ @vulnerability_finding_signatures_enabled = vulnerability_finding_signatures_enabled
+
+ @project_fingerprint = generate_project_fingerprint
+ end
+
+ def to_hash
+ %i[
+ compare_key
+ confidence
+ identifiers
+ links
+ location
+ metadata_version
+ name
+ project_fingerprint
+ raw_metadata
+ report_type
+ scanner
+ scan
+ severity
+ uuid
+ details
+ signatures
+ ].each_with_object({}) do |key, hash|
+ hash[key] = public_send(key) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
+ def primary_identifier
+ identifiers.first
+ end
+
+ def update_location(new_location)
+ @old_location = location
+ @location = new_location
+ end
+
+ def unsafe?(severity_levels)
+ severity.in?(severity_levels)
+ end
+
+ def eql?(other)
+ return false unless report_type == other.report_type && primary_identifier_fingerprint == other.primary_identifier_fingerprint
+
+ if @vulnerability_finding_signatures_enabled
+ matches_signatures(other.signatures, other.uuid)
+ else
+ location.fingerprint == other.location.fingerprint
+ end
+ end
+
+ def hash
+ if @vulnerability_finding_signatures_enabled && !signatures.empty?
+ highest_signature = signatures.max_by(&:priority)
+ report_type.hash ^ highest_signature.signature_hex.hash ^ primary_identifier_fingerprint.hash
+ else
+ report_type.hash ^ location.fingerprint.hash ^ primary_identifier_fingerprint.hash
+ end
+ end
+
+ def valid?
+ scanner.present? && primary_identifier.present? && location.present? && uuid.present?
+ end
+
+ def keys
+ @keys ||= identifiers.reject(&:type_identifier?).map do |identifier|
+ FindingKey.new(location_fingerprint: location&.fingerprint, identifier_fingerprint: identifier.fingerprint)
+ end
+ end
+
+ def primary_identifier_fingerprint
+ primary_identifier&.fingerprint
+ end
+
+ def <=>(other)
+ if severity == other.severity
+ compare_key <=> other.compare_key
+ else
+ ::Enums::Vulnerability.severity_levels[other.severity] <=>
+ ::Enums::Vulnerability.severity_levels[severity]
+ end
+ end
+
+ def scanner_order_to(other)
+ return 1 unless scanner
+ return -1 unless other&.scanner
+
+ scanner <=> other.scanner
+ end
+
+ private
+
+ def generate_project_fingerprint
+ Digest::SHA1.hexdigest(compare_key)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/reports/security/finding_key.rb b/lib/gitlab/ci/reports/security/finding_key.rb
new file mode 100644
index 00000000000..0acd923a60f
--- /dev/null
+++ b/lib/gitlab/ci/reports/security/finding_key.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Reports
+ module Security
+ class FindingKey
+ def initialize(location_fingerprint:, identifier_fingerprint:)
+ @location_fingerprint = location_fingerprint
+ @identifier_fingerprint = identifier_fingerprint
+ end
+
+ def ==(other)
+ has_fingerprints? && other.has_fingerprints? &&
+ location_fingerprint == other.location_fingerprint &&
+ identifier_fingerprint == other.identifier_fingerprint
+ end
+
+ def hash
+ location_fingerprint.hash ^ identifier_fingerprint.hash
+ end
+
+ alias_method :eql?, :==
+
+ protected
+
+ attr_reader :location_fingerprint, :identifier_fingerprint
+
+ def has_fingerprints?
+ location_fingerprint.present? && identifier_fingerprint.present?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/reports/security/finding_signature.rb b/lib/gitlab/ci/reports/security/finding_signature.rb
new file mode 100644
index 00000000000..d1d7ef5c377
--- /dev/null
+++ b/lib/gitlab/ci/reports/security/finding_signature.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Reports
+ module Security
+ class FindingSignature
+ include VulnerabilityFindingSignatureHelpers
+
+ attr_accessor :algorithm_type, :signature_value
+
+ def initialize(params = {})
+ @algorithm_type = params.dig(:algorithm_type)
+ @signature_value = params.dig(:signature_value)
+ end
+
+ def signature_sha
+ Digest::SHA1.digest(signature_value)
+ end
+
+ def signature_hex
+ signature_sha.unpack1("H*")
+ end
+
+ def to_hash
+ {
+ algorithm_type: algorithm_type,
+ signature_sha: signature_sha
+ }
+ end
+
+ def valid?
+ algorithm_types.key?(algorithm_type)
+ end
+
+ def eql?(other)
+ other.algorithm_type == algorithm_type &&
+ other.signature_sha == signature_sha
+ end
+
+ alias_method :==, :eql?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/reports/security/locations/base.rb b/lib/gitlab/ci/reports/security/locations/base.rb
new file mode 100644
index 00000000000..9ad1d81287f
--- /dev/null
+++ b/lib/gitlab/ci/reports/security/locations/base.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Reports
+ module Security
+ module Locations
+ class Base
+ include ::Gitlab::Utils::StrongMemoize
+
+ def ==(other)
+ other.fingerprint == fingerprint
+ end
+
+ def fingerprint
+ strong_memoize(:fingerprint) do
+ Digest::SHA1.hexdigest(fingerprint_data)
+ end
+ end
+
+ def as_json(options = nil)
+ fingerprint # side-effect call to initialize the ivar for serialization
+
+ super
+ end
+
+ def fingerprint_path
+ fingerprint_data
+ end
+
+ private
+
+ def fingerprint_data
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/reports/security/locations/sast.rb b/lib/gitlab/ci/reports/security/locations/sast.rb
new file mode 100644
index 00000000000..23ffa91e720
--- /dev/null
+++ b/lib/gitlab/ci/reports/security/locations/sast.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Reports
+ module Security
+ module Locations
+ class Sast < Base
+ include Security::Concerns::FingerprintPathFromFile
+
+ attr_reader :class_name
+ attr_reader :end_line
+ attr_reader :file_path
+ attr_reader :method_name
+ attr_reader :start_line
+
+ def initialize(file_path:, start_line:, end_line: nil, class_name: nil, method_name: nil)
+ @class_name = class_name
+ @end_line = end_line
+ @file_path = file_path
+ @method_name = method_name
+ @start_line = start_line
+ end
+
+ def fingerprint_data
+ "#{file_path}:#{start_line}:#{end_line}"
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/reports/security/locations/secret_detection.rb b/lib/gitlab/ci/reports/security/locations/secret_detection.rb
new file mode 100644
index 00000000000..0fd5cc5af11
--- /dev/null
+++ b/lib/gitlab/ci/reports/security/locations/secret_detection.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Reports
+ module Security
+ module Locations
+ class SecretDetection < Base
+ include Security::Concerns::FingerprintPathFromFile
+
+ attr_reader :class_name
+ attr_reader :end_line
+ attr_reader :file_path
+ attr_reader :method_name
+ attr_reader :start_line
+
+ def initialize(file_path:, start_line:, end_line: nil, class_name: nil, method_name: nil)
+ @class_name = class_name
+ @end_line = end_line
+ @file_path = file_path
+ @method_name = method_name
+ @start_line = start_line
+ end
+
+ def fingerprint_data
+ "#{file_path}:#{start_line}:#{end_line}"
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/reports/security/report.rb b/lib/gitlab/ci/reports/security/report.rb
new file mode 100644
index 00000000000..1ba2d909d99
--- /dev/null
+++ b/lib/gitlab/ci/reports/security/report.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Reports
+ module Security
+ class Report
+ attr_reader :created_at, :type, :pipeline, :findings, :scanners, :identifiers
+ attr_accessor :scan, :scanned_resources, :errors, :analyzer, :version
+
+ delegate :project_id, to: :pipeline
+
+ def initialize(type, pipeline, created_at)
+ @type = type
+ @pipeline = pipeline
+ @created_at = created_at
+ @findings = []
+ @scanners = {}
+ @identifiers = {}
+ @scanned_resources = []
+ @errors = []
+ end
+
+ def commit_sha
+ pipeline.sha
+ end
+
+ def add_error(type, message = 'An unexpected error happened!')
+ errors << { type: type, message: message }
+ end
+
+ def errored?
+ errors.present?
+ end
+
+ def add_scanner(scanner)
+ scanners[scanner.key] ||= scanner
+ end
+
+ def add_identifier(identifier)
+ identifiers[identifier.key] ||= identifier
+ end
+
+ def add_finding(finding)
+ findings << finding
+ end
+
+ def clone_as_blank
+ Report.new(type, pipeline, created_at)
+ end
+
+ def replace_with!(other)
+ instance_variables.each do |ivar|
+ instance_variable_set(ivar, other.public_send(ivar.to_s[1..-1])) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+
+ def merge!(other)
+ replace_with!(::Security::MergeReportsService.new(self, other).execute)
+ end
+
+ def primary_scanner
+ scanners.first&.second
+ end
+
+ def primary_scanner_order_to(other)
+ return 1 unless primary_scanner
+ return -1 unless other.primary_scanner
+
+ primary_scanner <=> other.primary_scanner
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/reports/security/reports.rb b/lib/gitlab/ci/reports/security/reports.rb
new file mode 100644
index 00000000000..b7a5e36b108
--- /dev/null
+++ b/lib/gitlab/ci/reports/security/reports.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Reports
+ module Security
+ class Reports
+ attr_reader :reports, :pipeline
+
+ delegate :each, :empty?, to: :reports
+
+ def initialize(pipeline)
+ @reports = {}
+ @pipeline = pipeline
+ end
+
+ def get_report(report_type, report_artifact)
+ reports[report_type] ||= Report.new(report_type, pipeline, report_artifact.created_at)
+ end
+
+ def findings
+ reports.values.flat_map(&:findings)
+ end
+
+ def violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels)
+ unsafe_findings_count(target_reports, severity_levels) > vulnerabilities_allowed
+ end
+
+ private
+
+ def findings_diff(target_reports)
+ findings - target_reports&.findings.to_a
+ end
+
+ def unsafe_findings_count(target_reports, severity_levels)
+ findings_diff(target_reports).count {|finding| finding.unsafe?(severity_levels)}
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/reports/security/vulnerability_reports_comparer.rb b/lib/gitlab/ci/reports/security/vulnerability_reports_comparer.rb
new file mode 100644
index 00000000000..6cb2e0ddb33
--- /dev/null
+++ b/lib/gitlab/ci/reports/security/vulnerability_reports_comparer.rb
@@ -0,0 +1,163 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Reports
+ module Security
+ class VulnerabilityReportsComparer
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :base_report, :head_report
+
+ ACCEPTABLE_REPORT_AGE = 1.week
+
+ def initialize(project, base_report, head_report)
+ @base_report = base_report
+ @head_report = head_report
+
+ @signatures_enabled = project.licensed_feature_available?(:vulnerability_finding_signatures)
+
+ if @signatures_enabled
+ @added_findings = []
+ @fixed_findings = []
+ calculate_changes
+ end
+ end
+
+ def base_report_created_at
+ @base_report.created_at
+ end
+
+ def head_report_created_at
+ @head_report.created_at
+ end
+
+ def base_report_out_of_date
+ return false unless @base_report.created_at
+
+ ACCEPTABLE_REPORT_AGE.ago > @base_report.created_at
+ end
+
+ def added
+ strong_memoize(:added) do
+ if @signatures_enabled
+ @added_findings
+ else
+ head_report.findings - base_report.findings
+ end
+ end
+ end
+
+ def fixed
+ strong_memoize(:fixed) do
+ if @signatures_enabled
+ @fixed_findings
+ else
+ base_report.findings - head_report.findings
+ end
+ end
+ end
+
+ private
+
+ def calculate_changes
+ # This is a deconstructed version of the eql? method on
+ # Ci::Reports::Security::Finding. It:
+ #
+ # * precomputes for the head_findings (using FindingMatcher):
+ # * sets of signature shas grouped by priority
+ # * mappings of signature shas to the head finding object
+ #
+ # These are then used when iterating the base findings to perform
+ # fast(er) prioritized, signature-based comparisons between each base finding
+ # and the head findings.
+ #
+ # Both the head_findings and base_findings arrays are iterated once
+
+ base_findings = base_report.findings
+ head_findings = head_report.findings
+
+ matcher = FindingMatcher.new(head_findings)
+
+ base_findings.each do |base_finding|
+ matched_head_finding = matcher.find_and_remove_match!(base_finding)
+
+ @fixed_findings << base_finding if matched_head_finding.nil?
+ end
+
+ @added_findings = matcher.unmatched_head_findings.values
+ end
+ end
+
+ class FindingMatcher
+ attr_reader :unmatched_head_findings, :head_findings
+
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(head_findings)
+ @head_findings = head_findings
+ @unmatched_head_findings = @head_findings.index_by(&:object_id)
+ end
+
+ def find_and_remove_match!(base_finding)
+ matched_head_finding = find_matched_head_finding_for(base_finding)
+
+ # no signatures matched, so check the normal uuids of the base and head findings
+ # for a match
+ matched_head_finding = head_signatures_shas[base_finding.uuid] if matched_head_finding.nil?
+
+ @unmatched_head_findings.delete(matched_head_finding.object_id) unless matched_head_finding.nil?
+
+ matched_head_finding
+ end
+
+ private
+
+ def find_matched_head_finding_for(base_finding)
+ base_signature = sorted_signatures_for(base_finding).find do |signature|
+ # at this point a head_finding exists that has a signature with a
+ # matching priority, and a matching sha --> lookup the actual finding
+ # object from head_signatures_shas
+ head_signatures_shas[signature.signature_sha].eql?(base_finding)
+ end
+
+ base_signature.present? ? head_signatures_shas[base_signature.signature_sha] : nil
+ end
+
+ def sorted_signatures_for(base_finding)
+ base_finding.signatures.select { |signature| head_finding_signature?(signature) }
+ .sort_by { |sig| -sig.priority }
+ end
+
+ def head_finding_signature?(signature)
+ head_signatures_priorities[signature.priority].include?(signature.signature_sha)
+ end
+
+ def head_signatures_priorities
+ strong_memoize(:head_signatures_priorities) do
+ signatures_priorities = Hash.new { |hash, key| hash[key] = Set.new }
+
+ head_findings.each_with_object(signatures_priorities) do |head_finding, memo|
+ head_finding.signatures.each do |signature|
+ memo[signature.priority].add(signature.signature_sha)
+ end
+ end
+ end
+ end
+
+ def head_signatures_shas
+ strong_memoize(:head_signatures_shas) do
+ head_findings.each_with_object({}) do |head_finding, memo|
+ head_finding.signatures.each do |signature|
+ memo[signature.signature_sha] = head_finding
+ end
+ # for the final uuid check when no signatures have matched
+ memo[head_finding.uuid] = head_finding
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end