Commit f4745c28 authored by Vitali Tatarintev's avatar Vitali Tatarintev

Merge branch '321927_expose_security_scan_information_on_graphql_api' into 'master'

Add `scans` field to securityReportSummary on GraphQL

See merge request gitlab-org/gitlab!56547
parents 395734ea 813cded5
......@@ -5425,6 +5425,34 @@ An edge in a connection.
| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
| `node` | [`SastCiConfigurationOptionsEntity`](#sastciconfigurationoptionsentity) | The item at the end of the edge. |
### `Scan`
Represents the security scan information.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `errors` | [`[String!]!`](#string) | List of errors. |
| `name` | [`String!`](#string) | Name of the scan. |
### `ScanConnection`
The connection type for Scan.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `edges` | [`[ScanEdge]`](#scanedge) | A list of edges. |
| `nodes` | [`[Scan]`](#scan) | A list of nodes. |
| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
### `ScanEdge`
An edge in a connection.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
| `node` | [`Scan`](#scan) | The item at the end of the edge. |
### `ScannedResource`
Represents a resource scanned by a security scan.
......@@ -5476,6 +5504,7 @@ Represents a section of a summary of a security report.
| `scannedResources` | [`ScannedResourceConnection`](#scannedresourceconnection) | A list of the first 20 scanned resources. |
| `scannedResourcesCount` | [`Int`](#int) | Total number of scanned resources. |
| `scannedResourcesCsvPath` | [`String`](#string) | Path to download all the scanned resources in CSV format. |
| `scans` | [`ScanConnection!`](#scanconnection) | List of security scans ran for the type. |
| `vulnerabilitiesCount` | [`Int`](#int) | Total number of vulnerabilities. |
### `SecurityScanners`
......
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class ScanType < BaseObject
graphql_name 'Scan'
description 'Represents the security scan information'
authorize :read_scan
field :name, GraphQL::STRING_TYPE, null: false, description: 'Name of the scan.'
field :errors, [GraphQL::STRING_TYPE], null: false, description: 'List of errors.'
def errors
object.info['errors'].to_a
end
end
end
......@@ -10,5 +10,6 @@ module Types
field :scanned_resources_count, GraphQL::INT_TYPE, null: true, description: 'Total number of scanned resources.'
field :scanned_resources, ::Types::ScannedResourceType.connection_type, null: true, description: 'A list of the first 20 scanned resources.'
field :scanned_resources_csv_path, GraphQL::STRING_TYPE, null: true, description: 'Path to download all the scanned resources in CSV format.'
field :scans, ::Types::ScanType.connection_type, null: false, description: 'List of security scans ran for the type.'
end
end
......@@ -39,6 +39,6 @@ module Security
scope :latest_successful_by_build, -> { joins(:build).where(ci_builds: { status: 'success', retried: [nil, false] }) }
delegate :project, to: :build
delegate :project, :name, to: :build
end
end
# frozen_string_literal: true
module Security
class ScanPolicy < BasePolicy
delegate { @subject.project }
rule { can?(:read_vulnerability) }.policy do
enable :read_scan
end
end
end
......@@ -4,6 +4,8 @@ module Security
class ReportSummaryService
include Gitlab::Utils::StrongMemoize
SCANNED_RESOURCES_LIMIT = 20
# @param [Ci::Pipeline] pipeline
# @param [Hash[Symbol, Array[Symbol]] selection_information keys must be in the set of Enums::Vulnerability.report_types for example: {dast: [:scanned_resources_count, :vulnerabilities_count], container_scanning:[:vulnerabilities_count]}
def initialize(pipeline, selection_information)
......@@ -32,6 +34,8 @@ module Security
response[:scanned_resources] = scanned_resources[report_type.to_s]
when :scanned_resources_csv_path
response[:scanned_resources_csv_path] = csv_path
when :scans
response[:scans] = grouped_scans[report_type.to_s]
end
end
end
......@@ -50,8 +54,7 @@ module Security
def scanned_resources
strong_memoize(:scanned_resources) do
scanned_resources_limit = 20
::Security::ScannedResourcesService.new(@pipeline, requested_report_types(:scanned_resources), scanned_resources_limit).execute
::Security::ScannedResourcesService.new(@pipeline, requested_report_types(:scanned_resources), SCANNED_RESOURCES_LIMIT).execute
end
end
......@@ -67,6 +70,10 @@ module Security
end
end
def grouped_scans
@grouped_scans ||= @pipeline.security_scans.by_scan_types(@selection_information.keys).group_by(&:scan_type)
end
def report_exists?(report_type)
@pipeline&.security_reports&.reports&.key?(report_type.to_s)
end
......
---
title: Add `scan` field into `SecurityReportSummarySectionType` GraphQL type
merge_request: 56547
author:
type: added
......@@ -15,7 +15,7 @@ RSpec.describe Resolvers::SecurityReportSummaryResolver do
let(:expected_selection_info) do
{
dast: [:scanned_resources_count, :vulnerabilities_count],
dast: [:scanned_resources_count, :vulnerabilities_count, :scans],
sast: [:scanned_resources_count, :vulnerabilities_count],
container_scanning: [:scanned_resources_count, :vulnerabilities_count],
dependency_scanning: [:scanned_resources_count, :vulnerabilities_count],
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['Scan'] do
let(:fields) { %i(name errors) }
it { expect(described_class).to have_graphql_fields(fields) }
it { expect(described_class).to require_graphql_authorizations(:read_scan) }
end
......@@ -6,7 +6,7 @@ RSpec.describe GitlabSchema.types['SecurityReportSummarySection'] do
specify { expect(described_class.graphql_name).to eq('SecurityReportSummarySection') }
it 'has specific fields' do
expected_fields = %w[vulnerabilities_count scanned_resources_count scanned_resources]
expected_fields = %w[vulnerabilities_count scanned_resources_count scanned_resources scans]
expect(described_class).to include_graphql_fields(*expected_fields)
end
......
......@@ -40,6 +40,10 @@ RSpec.describe Security::Scan do
it { is_expected.to delegate_method(:project).to(:build) }
end
describe '#name' do
it { is_expected.to delegate_method(:name).to(:build) }
end
describe '.by_scan_types' do
let!(:sast_scan) { create(:security_scan, scan_type: :sast) }
let!(:dast_scan) { create(:security_scan, scan_type: :dast) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Security::ScanPolicy do
describe 'read_scan' do
let_it_be(:user) { create(:user) }
let_it_be(:scan) { create(:security_scan) }
let_it_be(:project) { scan.project }
subject { described_class.new(user, scan) }
context 'when the security_dashboard feature is enabled' do
before do
stub_licensed_features(security_dashboard: true)
end
context "when the current user has developer access to the scan's project" do
before do
project.add_developer(user)
end
it { is_expected.to be_allowed(:read_scan) }
end
context "when the current user does not have developer access to the scan's project" do
it { is_expected.to be_disallowed(:read_scan) }
end
end
context 'when the security_dashboard feature is disabled' do
before do
stub_licensed_features(security_dashboard: false)
project.add_developer(user)
end
it { is_expected.to be_disallowed(:read_scan) }
end
end
end
......@@ -8,18 +8,22 @@ RSpec.describe Security::ReportSummaryService, '#execute' do
let_it_be(:build_ds) { create(:ci_build, :success, name: 'dependency_scanning', pipeline: pipeline) }
let_it_be(:artifact_ds) { create(:ee_ci_job_artifact, :dependency_scanning, job: build_ds) }
let_it_be(:report_ds) { create(:ci_reports_security_report, type: :dependency_scanning) }
let_it_be(:scan_ds) { create(:security_scan, scan_type: :dependency_scanning, build: build_ds) }
let_it_be(:build_sast) { create(:ci_build, :success, name: 'sast', pipeline: pipeline) }
let_it_be(:artifact_sast) { create(:ee_ci_job_artifact, :sast, job: build_sast) }
let_it_be(:report_sast) { create(:ci_reports_security_report, type: :sast) }
let_it_be(:scan_sast) { create(:security_scan, scan_type: :sast, build: build_sast) }
let_it_be(:build_dast) { create(:ci_build, :success, name: 'dast', pipeline: pipeline) }
let_it_be(:artifact_dast) { create(:ee_ci_job_artifact, :dast_large_scanned_resources_field, job: build_dast) }
let_it_be(:report_dast) { create(:ci_reports_security_report, type: :dast) }
let_it_be(:scan_dast) { create(:security_scan, scan_type: :dast, build: build_dast) }
let_it_be(:build_cs) { create(:ci_build, :success, name: 'container_scanning', pipeline: pipeline) }
let_it_be(:artifact_cs) { create(:ee_ci_job_artifact, :container_scanning, job: build_cs) }
let_it_be(:report_cs) { create(:ci_reports_security_report, type: :container_scanning) }
let_it_be(:scan_cs) { create(:security_scan, scan_type: :container_scanning, build: build_cs) }
before(:all) do
ds_content = File.read(artifact_ds.file.path)
......@@ -39,16 +43,13 @@ RSpec.describe Security::ReportSummaryService, '#execute' do
report_cs.merge!(report_cs)
{ artifact_cs => report_cs, artifact_dast => report_dast, artifact_ds => report_ds, artifact_sast => report_sast }.each do |artifact, report|
scan = create(:security_scan, scan_type: artifact.job.name, build: artifact.job)
report.findings.each_with_index do |finding, index|
report.findings.each do |finding|
create(:security_finding,
severity: finding.severity,
confidence: finding.confidence,
project_fingerprint: finding.project_fingerprint,
deduplicated: true,
position: index,
scan: scan)
scan: artifact.job.security_scans.first)
end
end
end
......@@ -98,6 +99,14 @@ RSpec.describe Security::ReportSummaryService, '#execute' do
end
end
context 'when the scans is requested' do
let(:selection_information) { { dast: [:scans] } }
it 'responds with the scan information' do
expect(result).to include(dast: { scans: [scan_dast] })
end
end
context 'All fields are requested' do
let(:selection_information) do
{
......
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