Commit 55d66bba authored by Nikola Milojevic's avatar Nikola Milojevic

Merge branch '298760-security-report-findings' into 'master'

Extend GraphQL Ci::PipelineType to include Security Report Findings

See merge request gitlab-org/gitlab!54104
parents 9196d965 cac10f1d
......@@ -2965,6 +2965,7 @@ Information about pagination in a connection.
| `path` | String | Relative path to the pipeline's page. |
| `project` | Project | Project the pipeline belongs to. |
| `retryable` | Boolean! | Specifies if a pipeline can be retried. |
| `securityReportFindings` | PipelineSecurityReportFindingConnection | Vulnerability findings reported on the pipeline. |
| `securityReportSummary` | SecurityReportSummary | Vulnerability and scanned resource counts for each security scanner of the pipeline. |
| `sha` | String! | SHA of the pipeline's commit. |
| `sourceJob` | CiJob | Job where pipeline was triggered from. |
......@@ -3029,6 +3030,25 @@ Autogenerated return type of PipelineRetry.
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `pipeline` | Pipeline | The pipeline after mutation. |
### PipelineSecurityReportFinding
Represents vulnerability finding of a security report on the pipeline.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `confidence` | String | Type of the security report that found the vulnerability. |
| `description` | String | Description of the vulnerability finding. |
| `identifiers` | VulnerabilityIdentifier! => Array | Identifiers of the vulnerabilit finding. |
| `location` | VulnerabilityLocation | Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability. |
| `name` | String | Name of the vulnerability finding. |
| `project` | Project | The project on which the vulnerability finding was found. |
| `projectFingerprint` | String | Name of the vulnerability finding. |
| `reportType` | VulnerabilityReportType | Type of the security report that found the vulnerability finding. |
| `scanner` | VulnerabilityScanner | Scanner metadata for the vulnerability. |
| `severity` | VulnerabilitySeverity | Severity of the vulnerability finding. |
| `solution` | String | URL to the vulnerability's details page. |
| `uuid` | String | Name of the vulnerability finding. |
### Project
| Field | Type | Description |
......
......@@ -13,6 +13,12 @@ module EE
extras: [:lookahead],
description: 'Vulnerability and scanned resource counts for each security scanner of the pipeline.',
resolver: ::Resolvers::SecurityReportSummaryResolver
field :security_report_findings,
::Types::PipelineSecurityReportFindingType.connection_type,
null: true,
description: 'Vulnerability findings reported on the pipeline.',
resolver: ::Resolvers::PipelineSecurityReportFindingsResolver
end
end
end
......
# frozen_string_literal: true
module Resolvers
class PipelineSecurityReportFindingsResolver < BaseResolver
type ::Types::PipelineSecurityReportFindingType, null: true
alias_method :pipeline, :object
argument :report_type, [GraphQL::STRING_TYPE],
required: false,
description: 'Filter vulnerability findings by report type.'
argument :severity, [GraphQL::STRING_TYPE],
required: false,
description: 'Filter vulnerability findings by severity.'
argument :scanner, [GraphQL::STRING_TYPE],
required: false,
description: 'Filter vulnerability findings by Scanner.externalId.'
def resolve(**args)
Security::PipelineVulnerabilitiesFinder.new(pipeline: pipeline, params: args).execute.findings
end
end
end
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class PipelineSecurityReportFindingType < BaseObject
graphql_name 'PipelineSecurityReportFinding'
description 'Represents vulnerability finding of a security report on the pipeline.'
field :report_type, VulnerabilityReportTypeEnum, null: true,
description: 'Type of the security report that found the vulnerability finding.'
field :name, GraphQL::STRING_TYPE, null: true,
description: 'Name of the vulnerability finding.'
field :severity, VulnerabilitySeverityEnum, null: true,
description: 'Severity of the vulnerability finding.'
field :confidence, GraphQL::STRING_TYPE, null: true,
description: 'Type of the security report that found the vulnerability.'
field :scanner, VulnerabilityScannerType, null: true,
description: 'Scanner metadata for the vulnerability.'
field :identifiers, [VulnerabilityIdentifierType], null: false,
description: 'Identifiers of the vulnerabilit finding.'
field :project_fingerprint, GraphQL::STRING_TYPE, null: true,
description: 'Name of the vulnerability finding.'
field :uuid, GraphQL::STRING_TYPE, null: true,
description: 'Name of the vulnerability finding.'
field :project, ::Types::ProjectType, null: true,
description: 'The project on which the vulnerability finding was found.',
authorize: :read_project
field :description, GraphQL::STRING_TYPE, null: true,
description: 'Description of the vulnerability finding.'
field :location, VulnerabilityLocationType, null: true,
description: 'Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability.'
field :solution, GraphQL::STRING_TYPE, null: true,
description: "URL to the vulnerability's details page."
def location
object.location&.merge(report_type: object.report_type)
end
end
end
---
title: Extend GraphQL Ci::PipelineType to include Security Report Findings
merge_request: 54104
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::PipelineSecurityReportFindingsResolver do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline, reload: true) { create(:ci_pipeline, :success, project: project) }
describe '#resolve' do
subject { resolve(described_class, obj: pipeline, args: params) }
let_it_be(:low_vulnerability_finding) { build(:vulnerabilities_finding, severity: :low, report_type: :dast, project: project) }
let_it_be(:critical_vulnerability_finding) { build(:vulnerabilities_finding, severity: :critical, report_type: :sast, project: project) }
let_it_be(:high_vulnerability_finding) { build(:vulnerabilities_finding, severity: :high, report_type: :container_scanning, project: project) }
let(:params) { {} }
before do
allow_next_instance_of(Security::PipelineVulnerabilitiesFinder) do |instance|
allow(instance).to receive_message_chain(:execute, :findings).and_return(returned_findings)
end
end
context 'when given severities' do
let(:params) { { severity: ['low'] } }
let(:returned_findings) { [low_vulnerability_finding] }
it 'returns vulnerability findings of the given severities' do
is_expected.to contain_exactly(low_vulnerability_finding)
end
end
context 'when given scanner' do
let(:params) { { scanner: [high_vulnerability_finding.scanner.external_id] } }
let(:returned_findings) { [high_vulnerability_finding] }
it 'returns vulnerability findings of the given scanner' do
is_expected.to contain_exactly(high_vulnerability_finding)
end
end
context 'when given report types' do
let(:params) { { report_type: %i[dast sast] } }
let(:returned_findings) { [critical_vulnerability_finding, low_vulnerability_finding] }
it 'returns vulnerabilities of the given report types' do
is_expected.to contain_exactly(critical_vulnerability_finding, low_vulnerability_finding)
end
end
end
end
......@@ -8,6 +8,7 @@ RSpec.describe GitlabSchema.types['Pipeline'] do
it 'includes the ee specific fields' do
expected_fields = %w[
security_report_summary
security_report_findings
]
expect(described_class).to include_graphql_fields(*expected_fields)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['PipelineSecurityReportFinding'] do
let_it_be(:fields) do
%i[report_type
name
severity
confidence
scanner
identifiers
project_fingerprint
uuid
project
description
location
solution]
end
specify { expect(described_class.graphql_name).to eq('PipelineSecurityReportFinding') }
it { expect(described_class).to have_graphql_fields(fields) }
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Query.project(fullPath).pipeline(iid).securityReportFinding' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project) }
let_it_be(:user) { create(:user) }
before_all do
create(:ci_build, :success, name: 'dast_job', pipeline: pipeline, project: project) do |job|
create(:ee_ci_job_artifact, :dast_large_scanned_resources_field, job: job, project: project)
end
create(:ci_build, :success, name: 'sast_job', pipeline: pipeline, project: project) do |job|
create(:ee_ci_job_artifact, :sast, job: job, project: project)
end
end
let_it_be(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
pipeline(iid: "#{pipeline.iid}") {
securityReportFindings(reportType: ["sast", "dast"]) {
nodes {
confidence
severity
reportType
name
scanner {
name
}
projectFingerprint
identifiers {
name
}
uuid
solution
description
project {
fullPath
visibility
}
}
}
}
}
}
)
end
subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
let(:security_report_findings) { subject.dig('data', 'project', 'pipeline', 'securityReportFindings', 'nodes') }
context 'when `sast` and `dast` features are enabled' do
before do
stub_licensed_features(sast: true, dast: true)
end
context 'when user is memeber of the project' do
before do
project.add_developer(user)
end
it 'returns all the vulnerability findings' do
expect(security_report_findings.length).to eq(53)
end
it 'returns all the queried fields', :aggregate_failures do
security_report_finding = security_report_findings.first
expect(security_report_finding.dig('project', 'fullPath')).to eq(project.full_path)
expect(security_report_finding.dig('project', 'visibility')).to eq(project.visibility)
expect(security_report_finding['identifiers'].length).to eq(3)
expect(security_report_finding['confidence']).not_to be_nil
expect(security_report_finding['severity']).not_to be_nil
expect(security_report_finding['reportType']).not_to be_nil
expect(security_report_finding['name']).not_to be_nil
expect(security_report_finding['projectFingerprint']).not_to be_nil
expect(security_report_finding['uuid']).not_to be_nil
expect(security_report_finding['solution']).not_to be_nil
expect(security_report_finding['description']).not_to be_nil
end
end
context 'when user is not memeber of the project' do
it 'returns no vulnerability findings' do
expect(security_report_findings).to be_nil
end
end
end
context 'when `sast` and `dast` both features are disabled' do
before do
stub_licensed_features(sast: false, dast: false)
end
it 'returns no vulnerability findings' do
expect(security_report_findings).to be_nil
end
end
end
......@@ -16,7 +16,7 @@ RSpec.describe Types::Ci::PipelineType do
]
if Gitlab.ee?
expected_fields << 'security_report_summary'
expected_fields += %w[security_report_summary security_report_findings]
end
expect(described_class).to have_graphql_fields(*expected_fields)
......
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