Commit 0d2264a0 authored by Dylan Griffith's avatar Dylan Griffith

Merge branch '351524_implement_retentation_period_for_security_findings' into 'master'

Add security scan status to GraphQL API

See merge request gitlab-org/gitlab!81305
parents add17d54 c1ecdd58
...@@ -38,7 +38,7 @@ module Types ...@@ -38,7 +38,7 @@ module Types
description(enum_mod.description) if use_description description(enum_mod.description) if use_description
enum_mod.definition.each do |key, content| enum_mod.definition.each do |key, content|
value(key.to_s.upcase, **content) value(key.to_s.upcase, value: key.to_s, description: content[:description])
end end
end end
# rubocop: enable Graphql/Descriptions # rubocop: enable Graphql/Descriptions
......
...@@ -15271,6 +15271,7 @@ Represents the security scan information. ...@@ -15271,6 +15271,7 @@ Represents the security scan information.
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="scanerrors"></a>`errors` | [`[String!]!`](#string) | List of errors. | | <a id="scanerrors"></a>`errors` | [`[String!]!`](#string) | List of errors. |
| <a id="scanname"></a>`name` | [`String!`](#string) | Name of the scan. | | <a id="scanname"></a>`name` | [`String!`](#string) | Name of the scan. |
| <a id="scanstatus"></a>`status` | [`ScanStatus!`](#scanstatus) | Indicates the status of the scan. |
| <a id="scanwarnings"></a>`warnings` | [`[String!]!`](#string) | List of warnings. | | <a id="scanwarnings"></a>`warnings` | [`[String!]!`](#string) | List of warnings. |
### `ScanExecutionPolicy` ### `ScanExecutionPolicy`
...@@ -18142,6 +18143,20 @@ Size of UI component in SAST configuration page. ...@@ -18142,6 +18143,20 @@ Size of UI component in SAST configuration page.
| <a id="sastuicomponentsizemedium"></a>`MEDIUM` | Size of UI component in SAST configuration page is medium. | | <a id="sastuicomponentsizemedium"></a>`MEDIUM` | Size of UI component in SAST configuration page is medium. |
| <a id="sastuicomponentsizesmall"></a>`SMALL` | Size of UI component in SAST configuration page is small. | | <a id="sastuicomponentsizesmall"></a>`SMALL` | Size of UI component in SAST configuration page is small. |
### `ScanStatus`
The status of the security scan.
| Value | Description |
| ----- | ----------- |
| <a id="scanstatuscreated"></a>`CREATED` | The scan has been created. |
| <a id="scanstatusjob_failed"></a>`JOB_FAILED` | The related CI build failed. |
| <a id="scanstatuspreparation_failed"></a>`PREPARATION_FAILED` | Report couldn't be prepared. |
| <a id="scanstatuspreparing"></a>`PREPARING` | Preparing the report for the scan. |
| <a id="scanstatuspurged"></a>`PURGED` | Report for the scan has been removed from the database. |
| <a id="scanstatusreport_error"></a>`REPORT_ERROR` | The report artifact provided by the CI build couldn't be parsed. |
| <a id="scanstatussucceeded"></a>`SUCCEEDED` | The report has been successfully prepared. |
### `SecurityReportTypeEnum` ### `SecurityReportTypeEnum`
| Value | Description | | Value | Description |
# frozen_string_literal: true
module Security
module ScanStatusEnum
extend DeclarativeEnum
key :status
name 'ScanStatus'
description 'The status of the security scan'
define do
created value: 0, description: N_('The scan has been created.')
succeeded value: 1, description: N_('The report has been successfully prepared.')
job_failed value: 2, description: N_('The related CI build failed.')
report_error value: 3, description: N_('The report artifact provided by the CI build couldn\'t be parsed.')
preparing value: 4, description: N_('Preparing the report for the scan.')
preparation_failed value: 5, description: N_('Report couldn\'t be prepared.')
purged value: 6, description: N_('Report for the scan has been removed from the database.')
end
end
end
# frozen_string_literal: true
module Types
class ScanStatusEnum < BaseEnum
declarative_enum ::Security::ScanStatusEnum
end
end
...@@ -13,5 +13,6 @@ module Types ...@@ -13,5 +13,6 @@ module Types
field :errors, [GraphQL::Types::String], null: false, description: 'List of errors.' field :errors, [GraphQL::Types::String], null: false, description: 'List of errors.'
field :name, GraphQL::Types::String, null: false, description: 'Name of the scan.' field :name, GraphQL::Types::String, null: false, description: 'Name of the scan.'
field :warnings, [GraphQL::Types::String], null: false, description: 'List of warnings.' field :warnings, [GraphQL::Types::String], null: false, description: 'List of warnings.'
field :status, Types::ScanStatusEnum, null: false, description: 'Indicates the status of the scan.'
end end
end end
...@@ -27,7 +27,7 @@ module Security ...@@ -27,7 +27,7 @@ module Security
cluster_image_scanning: 8 cluster_image_scanning: 8
} }
enum status: { created: 0, succeeded: 1, failed: 2 } declarative_enum Security::ScanStatusEnum
scope :by_scan_types, -> (scan_types) { where(scan_type: sanitize_scan_types(scan_types)) } scope :by_scan_types, -> (scan_types) { where(scan_type: sanitize_scan_types(scan_types)) }
scope :distinct_scan_types, -> { select(:scan_type).distinct.pluck(:scan_type) } scope :distinct_scan_types, -> { select(:scan_type).distinct.pluck(:scan_type) }
......
...@@ -24,9 +24,11 @@ module Security ...@@ -24,9 +24,11 @@ module Security
override_finding_uuids! if override_uuids? override_finding_uuids! if override_uuids?
set_security_scan_non_latest! if job.retried? set_security_scan_non_latest! if job.retried?
return deduplicate if security_scan.has_errors? || !security_scan.latest? || !security_scan.succeeded? return deduplicate if !security_scan.latest? || security_scan.report_error? || security_scan.job_failed?
store_findings store_findings
deduplicate_findings?
end end
private private
...@@ -47,10 +49,16 @@ module Security ...@@ -47,10 +49,16 @@ module Security
@security_scan ||= Security::Scan.safe_find_or_create_by!(build: job, scan_type: artifact.file_type) do |scan| @security_scan ||= Security::Scan.safe_find_or_create_by!(build: job, scan_type: artifact.file_type) do |scan|
scan.processing_errors = security_report.errors.map(&:stringify_keys) if security_report.errored? scan.processing_errors = security_report.errors.map(&:stringify_keys) if security_report.errored?
scan.processing_warnings = security_report.warnings.map(&:stringify_keys) scan.processing_warnings = security_report.warnings.map(&:stringify_keys)
scan.status = job.success? ? :succeeded : :failed scan.status = initial_scan_status
end end
end end
def initial_scan_status
return :report_error if security_report.errored?
job.success? ? :preparing : :job_failed
end
def store_findings def store_findings
StoreFindingsMetadataService.execute(security_scan, security_report, register_finding_keys).then do |result| StoreFindingsMetadataService.execute(security_scan, security_report, register_finding_keys).then do |result|
# If `StoreFindingsMetadataService` returns error, it means the findings # If `StoreFindingsMetadataService` returns error, it means the findings
...@@ -58,7 +66,11 @@ module Security ...@@ -58,7 +66,11 @@ module Security
update_deduplicated_findings if result[:status] == :error && deduplicate_findings? update_deduplicated_findings if result[:status] == :error && deduplicate_findings?
end end
deduplicate_findings? security_scan.succeeded!
rescue StandardError => error
security_scan.preparation_failed!
Gitlab::ErrorTracking.track_exception(error)
end end
def set_security_scan_non_latest! def set_security_scan_non_latest!
......
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['Scan'] do RSpec.describe GitlabSchema.types['Scan'] do
include GraphqlHelpers include GraphqlHelpers
let(:fields) { %i(name errors warnings) } let(:fields) { %i(name errors warnings status) }
it { expect(described_class).to have_graphql_fields(fields) } it { expect(described_class).to have_graphql_fields(fields) }
it { expect(described_class).to require_graphql_authorizations(:read_scan) } it { expect(described_class).to require_graphql_authorizations(:read_scan) }
...@@ -48,5 +48,11 @@ RSpec.describe GitlabSchema.types['Scan'] do ...@@ -48,5 +48,11 @@ RSpec.describe GitlabSchema.types['Scan'] do
it { is_expected.to match_array(['[foo] bar']) } it { is_expected.to match_array(['[foo] bar']) }
end end
describe 'status' do
let(:field_name) { :status }
it { is_expected.to eq('created') }
end
end end
end end
...@@ -196,7 +196,7 @@ RSpec.describe Security::Scan do ...@@ -196,7 +196,7 @@ RSpec.describe Security::Scan do
describe '.latest_successful' do describe '.latest_successful' do
let!(:first_successful_scan) { create(:security_scan, latest: false, status: :succeeded) } let!(:first_successful_scan) { create(:security_scan, latest: false, status: :succeeded) }
let!(:second_successful_scan) { create(:security_scan, latest: true, status: :succeeded) } let!(:second_successful_scan) { create(:security_scan, latest: true, status: :succeeded) }
let!(:failed_scan) { create(:security_scan, latest: true, status: :failed) } let!(:failed_scan) { create(:security_scan, latest: true, status: :job_failed) }
subject { described_class.latest_successful } subject { described_class.latest_successful }
......
...@@ -102,6 +102,10 @@ RSpec.describe Security::StoreScanService do ...@@ -102,6 +102,10 @@ RSpec.describe Security::StoreScanService do
expect(store_scan).to be(false) expect(store_scan).to be(false)
expect(Security::StoreFindingsMetadataService).not_to have_received(:execute) expect(Security::StoreFindingsMetadataService).not_to have_received(:execute)
end end
it 'sets the status of the scan as `report_error`' do
expect { store_scan }.to change { Security::Scan.report_error.count }.by(1)
end
end end
context 'when the report is produced by a failed job' do context 'when the report is produced by a failed job' do
...@@ -109,13 +113,28 @@ RSpec.describe Security::StoreScanService do ...@@ -109,13 +113,28 @@ RSpec.describe Security::StoreScanService do
artifact.job.update!(status: :failed) artifact.job.update!(status: :failed)
end end
it 'does not call the `Security::StoreFindingsMetadataService` and sets the security scan as failed' do it 'does not call the `Security::StoreFindingsMetadataService` and sets the security scan as `job_failed`' do
expect { store_scan }.to change { Security::Scan.failed.count }.by(1) expect { store_scan }.to change { Security::Scan.job_failed.count }.by(1)
expect(Security::StoreFindingsMetadataService).not_to have_received(:execute) expect(Security::StoreFindingsMetadataService).not_to have_received(:execute)
end end
end end
context 'when storing the findings raises an error' do
let(:error) { RuntimeError.new }
before do
allow(Security::StoreFindingsMetadataService).to receive(:execute).and_raise(error)
allow(Gitlab::ErrorTracking).to receive(:track_exception)
end
it 'marks the security scan as `preparation_failed` and tracks the error' do
expect { store_scan }.to change { Security::Scan.preparation_failed.count }.by(1)
expect(Gitlab::ErrorTracking).to have_received(:track_exception).with(error)
end
end
context 'when the report is produced by a retried job' do context 'when the report is produced by a retried job' do
before do before do
artifact.job.update!(retried: true) artifact.job.update!(retried: true)
......
...@@ -27737,6 +27737,9 @@ msgstr "" ...@@ -27737,6 +27737,9 @@ msgstr ""
msgid "Preferences|When you type in a description or comment box, selected text is surrounded by the corresponding character after typing one of the following characters: %{supported_characters}." msgid "Preferences|When you type in a description or comment box, selected text is surrounded by the corresponding character after typing one of the following characters: %{supported_characters}."
msgstr "" msgstr ""
msgid "Preparing the report for the scan."
msgstr ""
msgid "Prev" msgid "Prev"
msgstr "" msgstr ""
...@@ -30792,6 +30795,12 @@ msgstr "" ...@@ -30792,6 +30795,12 @@ msgstr ""
msgid "Report abuse to admin" msgid "Report abuse to admin"
msgstr "" msgstr ""
msgid "Report couldn't be prepared."
msgstr ""
msgid "Report for the scan has been removed from the database."
msgstr ""
msgid "Reported %{timeAgo} by %{reportedBy}" msgid "Reported %{timeAgo} by %{reportedBy}"
msgstr "" msgstr ""
...@@ -36825,6 +36834,9 @@ msgstr "" ...@@ -36825,6 +36834,9 @@ msgstr ""
msgid "The regular expression used to find test coverage output in the job log. For example, use %{regex} for Simplecov (Ruby). Leave blank to disable." msgid "The regular expression used to find test coverage output in the job log. For example, use %{regex} for Simplecov (Ruby). Leave blank to disable."
msgstr "" msgstr ""
msgid "The related CI build failed."
msgstr ""
msgid "The remote mirror URL is invalid." msgid "The remote mirror URL is invalid."
msgstr "" msgstr ""
...@@ -36834,6 +36846,12 @@ msgstr "" ...@@ -36834,6 +36846,12 @@ msgstr ""
msgid "The remote repository is being updated..." msgid "The remote repository is being updated..."
msgstr "" msgstr ""
msgid "The report artifact provided by the CI build couldn't be parsed."
msgstr ""
msgid "The report has been successfully prepared."
msgstr ""
msgid "The repository can be committed to, and issues, comments and other entities can be created." msgid "The repository can be committed to, and issues, comments and other entities can be created."
msgstr "" msgstr ""
...@@ -36855,6 +36873,9 @@ msgstr "" ...@@ -36855,6 +36873,9 @@ msgstr ""
msgid "The same shared runner executes code from multiple projects, unless you configure autoscaling with %{link} set to 1 (which it is on GitLab.com)." msgid "The same shared runner executes code from multiple projects, unless you configure autoscaling with %{link} set to 1 (which it is on GitLab.com)."
msgstr "" msgstr ""
msgid "The scan has been created."
msgstr ""
msgid "The snippet can be accessed without any authentication." msgid "The snippet can be accessed without any authentication."
msgstr "" msgstr ""
......
...@@ -102,9 +102,9 @@ RSpec.describe Types::BaseEnum do ...@@ -102,9 +102,9 @@ RSpec.describe Types::BaseEnum do
it 'sets the values defined by the declarative enum' do it 'sets the values defined by the declarative enum' do
set_declarative_enum set_declarative_enum
expect(enum_type.values.keys).to eq(['FOO']) expect(enum_type.values.keys).to contain_exactly('FOO')
expect(enum_type.values.values.map(&:description)).to eq(['description of foo']) expect(enum_type.values.values.map(&:description)).to contain_exactly('description of foo')
expect(enum_type.values.values.map(&:value)).to eq([0]) expect(enum_type.values.values.map(&:value)).to contain_exactly('foo')
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment