Commit 39b70204 authored by Dylan Griffith's avatar Dylan Griffith

Merge branch 'scanner-awareness' into 'master'

Create GraphQL query to extract analyzers information

See merge request gitlab-org/gitlab!36153
parents 01bb455f 5d3942d2
...@@ -9884,6 +9884,11 @@ type Project { ...@@ -9884,6 +9884,11 @@ type Project {
""" """
sastCiConfiguration: SastCiConfiguration sastCiConfiguration: SastCiConfiguration
"""
Information about security analyzers used in the project
"""
securityScanners: SecurityScanners
""" """
Detailed version of a Sentry error on the project Detailed version of a Sentry error on the project
""" """
...@@ -12053,6 +12058,37 @@ type SecurityReportSummarySection { ...@@ -12053,6 +12058,37 @@ type SecurityReportSummarySection {
vulnerabilitiesCount: Int vulnerabilitiesCount: Int
} }
"""
The type of the security scanner.
"""
enum SecurityScannerType {
CONTAINER_SCANNING
DAST
DEPENDENCY_SCANNING
SAST
SECRET_DETECTION
}
"""
Represents a list of security scanners
"""
type SecurityScanners {
"""
List of analyzers which are available for the project.
"""
available: [SecurityScannerType!]
"""
List of analyzers which are enabled for the project.
"""
enabled: [SecurityScannerType!]
"""
List of analyzers which ran successfully in the latest pipeline.
"""
pipelineRun: [SecurityScannerType!]
}
""" """
A Sentry error. A Sentry error.
""" """
......
...@@ -29080,6 +29080,20 @@ ...@@ -29080,6 +29080,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "securityScanners",
"description": "Information about security analyzers used in the project",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "SecurityScanners",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "sentryDetailedError", "name": "sentryDetailedError",
"description": "Detailed version of a Sentry error on the project", "description": "Detailed version of a Sentry error on the project",
...@@ -35291,6 +35305,126 @@ ...@@ -35291,6 +35305,126 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "ENUM",
"name": "SecurityScannerType",
"description": "The type of the security scanner.",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "SAST",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "DAST",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "DEPENDENCY_SCANNING",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "CONTAINER_SCANNING",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "SECRET_DETECTION",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SecurityScanners",
"description": "Represents a list of security scanners",
"fields": [
{
"name": "available",
"description": "List of analyzers which are available for the project.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "SecurityScannerType",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "enabled",
"description": "List of analyzers which are enabled for the project.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "SecurityScannerType",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pipelineRun",
"description": "List of analyzers which ran successfully in the latest pipeline.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "ENUM",
"name": "SecurityScannerType",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "SentryDetailedError", "name": "SentryDetailedError",
...@@ -1434,6 +1434,7 @@ Information about pagination in a connection. ...@@ -1434,6 +1434,7 @@ Information about pagination in a connection.
| `requirement` | Requirement | Find a single requirement. Available only when feature flag `requirements_management` is enabled. | | `requirement` | Requirement | Find a single requirement. Available only when feature flag `requirements_management` is enabled. |
| `requirementStatesCount` | RequirementStatesCount | Number of requirements for the project by their state | | `requirementStatesCount` | RequirementStatesCount | Number of requirements for the project by their state |
| `sastCiConfiguration` | SastCiConfiguration | SAST CI configuration for the project | | `sastCiConfiguration` | SastCiConfiguration | SAST CI configuration for the project |
| `securityScanners` | SecurityScanners | Information about security analyzers used in the project |
| `sentryDetailedError` | SentryDetailedError | Detailed version of a Sentry error on the project | | `sentryDetailedError` | SentryDetailedError | Detailed version of a Sentry error on the project |
| `sentryErrors` | SentryErrorCollection | Paginated collection of Sentry errors on the project | | `sentryErrors` | SentryErrorCollection | Paginated collection of Sentry errors on the project |
| `serviceDeskAddress` | String | E-mail address of the service desk. | | `serviceDeskAddress` | String | E-mail address of the service desk. |
...@@ -1746,6 +1747,16 @@ Represents a section of a summary of a security report ...@@ -1746,6 +1747,16 @@ Represents a section of a summary of a security report
| `scannedResourcesCsvPath` | String | Path to download all the scanned resources in CSV format | | `scannedResourcesCsvPath` | String | Path to download all the scanned resources in CSV format |
| `vulnerabilitiesCount` | Int | Total number of vulnerabilities | | `vulnerabilitiesCount` | Int | Total number of vulnerabilities |
## SecurityScanners
Represents a list of security scanners
| Name | Type | Description |
| --- | ---- | ---------- |
| `available` | SecurityScannerType! => Array | List of analyzers which are available for the project. |
| `enabled` | SecurityScannerType! => Array | List of analyzers which are enabled for the project. |
| `pipelineRun` | SecurityScannerType! => Array | List of analyzers which ran successfully in the latest pipeline. |
## SentryDetailedError ## SentryDetailedError
A Sentry error. A Sentry error.
......
...@@ -6,6 +6,12 @@ module EE ...@@ -6,6 +6,12 @@ module EE
extend ActiveSupport::Concern extend ActiveSupport::Concern
prepended do prepended do
field :securityScanners, ::Types::SecurityScanners, null: true,
description: 'Information about security analyzers used in the project',
resolve: -> (project, _args, ctx) do
project
end
field :vulnerabilities, field :vulnerabilities,
::Types::VulnerabilityType.connection_type, ::Types::VulnerabilityType.connection_type,
null: true, null: true,
......
# frozen_string_literal: true
module Types
class SecurityScannerTypeEnum < BaseEnum
graphql_name 'SecurityScannerType'
description 'The type of the security scanner.'
::Security::SecurityJobsFinder.allowed_job_types.each do |scanner|
value scanner.upcase.to_s
end
end
end
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class SecurityScanners < BaseObject
graphql_name 'SecurityScanners'
description 'Represents a list of security scanners'
field :enabled, [::Types::SecurityScannerTypeEnum], null: true,
description: 'List of analyzers which are enabled for the project.',
calls_gitaly: true,
resolve: -> (project, _args, ctx) do
project.enabled_scanners
end
field :available, [::Types::SecurityScannerTypeEnum], null: true,
description: 'List of analyzers which are available for the project.',
resolve: -> (project, _args, ctx) do
project.available_scanners
end
field :pipelineRun, [::Types::SecurityScannerTypeEnum], null: true,
description: 'List of analyzers which ran successfully in the latest pipeline.',
calls_gitaly: true,
resolve: -> (project, _args, ctx) do
project.scanners_run_in_last_pipeline
end
end
end
# frozen_string_literal: true
module EE
module ProjectSecurityScannersInformation
include ::Gitlab::Utils::StrongMemoize
include LatestPipelineInformation
def available_scanners
all_security_scanners.map { |scanner| scanner.upcase.to_s if feature_available?(scanner) }.compact
end
def enabled_scanners
all_security_scanners.map { |scanner| scanner.upcase.to_s if scanner_enabled?(scanner) }.compact
end
def scanners_run_in_last_pipeline
latest_builds_reports(only_successful_builds: true).map { |scanner| scanner.upcase.to_s }.compact
end
private
def all_security_scanners
::Security::SecurityJobsFinder.allowed_job_types
end
end
end
# frozen_string_literal: true
module LatestPipelineInformation
private
def scanner_enabled?(scan_type)
latest_builds_reports.include?(scan_type)
end
def latest_builds_reports(only_successful_builds: false)
strong_memoize("latest_builds_reports_#{only_successful_builds}" ) do
builds = latest_security_builds
builds = builds.select { |build| build.status == 'success' } if only_successful_builds
builds.map do |build|
if Feature.enabled?(:ci_build_metadata_config)
build.metadata.config_options[:artifacts][:reports].keys.map(&:to_sym)
else
build.options[:artifacts][:reports].keys
end
end.flatten
end
end
def latest_security_builds
return [] unless latest_default_branch_pipeline
::Security::SecurityJobsFinder.new(pipeline: latest_default_branch_pipeline).execute +
::Security::LicenseComplianceJobsFinder.new(pipeline: latest_default_branch_pipeline).execute
end
def latest_default_branch_pipeline
strong_memoize(:pipeline) { latest_pipeline_for_ref }
end
def auto_devops_source?
latest_default_branch_pipeline&.auto_devops_source?
end
end
...@@ -20,6 +20,7 @@ module EE ...@@ -20,6 +20,7 @@ module EE
include InsightsFeature include InsightsFeature
include DeprecatedApprovalsBeforeMerge include DeprecatedApprovalsBeforeMerge
include UsageStatistics include UsageStatistics
include ProjectSecurityScannersInformation
ignore_columns :mirror_last_update_at, :mirror_last_successful_update_at, remove_after: '2019-12-15', remove_with: '12.6' ignore_columns :mirror_last_update_at, :mirror_last_successful_update_at, remove_after: '2019-12-15', remove_with: '12.6'
......
...@@ -5,6 +5,7 @@ module Projects ...@@ -5,6 +5,7 @@ module Projects
class ConfigurationPresenter < Gitlab::View::Presenter::Delegated class ConfigurationPresenter < Gitlab::View::Presenter::Delegated
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include AutoDevopsHelper include AutoDevopsHelper
include LatestPipelineInformation
presents :project presents :project
...@@ -79,9 +80,7 @@ module Projects ...@@ -79,9 +80,7 @@ module Projects
def features def features
scans = scan_types.map do |scan_type| scans = scan_types.map do |scan_type|
if auto_devops_source? if scanner_enabled?(scan_type)
scan(scan_type, configured: true)
elsif latest_builds_reports.include?(scan_type)
scan(scan_type, configured: true) scan(scan_type, configured: true)
else else
scan(scan_type, configured: false) scan(scan_type, configured: false)
...@@ -92,29 +91,6 @@ module Projects ...@@ -92,29 +91,6 @@ module Projects
license_compliance_substitute(scans) license_compliance_substitute(scans)
end end
def latest_builds_reports
strong_memoize(:reports) do
latest_security_builds.map do |build|
if Feature.enabled?(:ci_build_metadata_config)
build.metadata.config_options[:artifacts][:reports].keys.map(&:to_sym)
else
build.options[:artifacts][:reports].keys
end
end.flatten
end
end
def latest_security_builds
return [] unless latest_default_branch_pipeline
::Security::SecurityJobsFinder.new(pipeline: latest_default_branch_pipeline).execute +
::Security::LicenseComplianceJobsFinder.new(pipeline: latest_default_branch_pipeline).execute
end
def latest_default_branch_pipeline
strong_memoize(:pipeline) { latest_pipeline_for_ref }
end
def latest_pipeline_path def latest_pipeline_path
return help_page_path('ci/pipelines') unless latest_default_branch_pipeline return help_page_path('ci/pipelines') unless latest_default_branch_pipeline
...@@ -150,10 +126,6 @@ module Projects ...@@ -150,10 +126,6 @@ module Projects
} }
end end
def auto_devops_source?
latest_default_branch_pipeline&.auto_devops_source?
end
def scan_types def scan_types
::Security::SecurityJobsFinder.allowed_job_types + ::Security::LicenseComplianceJobsFinder.allowed_job_types ::Security::SecurityJobsFinder.allowed_job_types + ::Security::LicenseComplianceJobsFinder.allowed_job_types
end end
......
---
title: Create GraphQL query to extract analyzers information
merge_request: 36153
author:
type: added
...@@ -22,6 +22,45 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -22,6 +22,45 @@ RSpec.describe GitlabSchema.types['Project'] do
expect(described_class).to include_graphql_fields(*expected_fields) expect(described_class).to include_graphql_fields(*expected_fields)
end end
describe 'security_scanners' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) }
let_it_be(:user) { create(:user) }
let_it_be(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
securityScanners {
enabled
available
pipelineRun
}
}
}
)
end
subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
before do
project.add_developer(user)
create(:ci_build, :sast, pipeline: pipeline, status: 'success')
create(:ci_build, :dast, pipeline: pipeline, status: 'success')
create(:ci_build, :secret_detection, pipeline: pipeline, status: 'pending')
end
it 'returns a list of analyzers enabled for the project' do
query_result = subject.dig('data', 'project', 'securityScanners', 'enabled')
expect(query_result).to match_array(%w(SAST DAST SECRET_DETECTION))
end
it 'returns a list of analyzers which were run in the last pipeline for the project' do
query_result = subject.dig('data', 'project', 'securityScanners', 'pipelineRun')
expect(query_result).to match_array(%w(DAST SAST))
end
end
describe 'vulnerabilities' do describe 'vulnerabilities' do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['SecurityScanners'] do
specify { expect(described_class.graphql_name).to eq('SecurityScanners') }
it 'has specific fields' do
expected_fields = %w[enabled available pipelineRun]
expect(described_class).to include_graphql_fields(*expected_fields)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ::EE::ProjectSecurityScannersInformation do
let(:project) { create(:project, :repository) }
let(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) }
before do
create(:ci_build, :sast, pipeline: pipeline, status: 'success')
create(:ci_build, :dast, pipeline: pipeline, status: 'success')
create(:ci_build, :secret_detection, pipeline: pipeline, status: 'pending')
end
describe '#available_scanners' do
before do
allow(project).to receive(:feature_available?) { false }
allow(project).to receive(:feature_available?).with(:sast) { true }
allow(project).to receive(:feature_available?).with(:dast) { true }
end
it 'returns a list of all scanners available for the project' do
expect(project.available_scanners).to match_array(%w(SAST DAST))
end
end
describe '#enabled_scanners' do
it 'returns a list of all scanners enabled for the project' do
expect(project.enabled_scanners).to match_array(%w(SAST DAST SECRET_DETECTION))
end
end
describe '#scanners_run_by_last_pipeline' do
it 'returns a list of all scanners which were run successfully in the latest pipeline' do
expect(project.scanners_run_in_last_pipeline).to match_array(%w(DAST SAST))
end
end
end
...@@ -39,13 +39,16 @@ RSpec.describe Projects::Security::ConfigurationPresenter do ...@@ -39,13 +39,16 @@ RSpec.describe Projects::Security::ConfigurationPresenter do
context "when the latest default branch pipeline's source is auto devops" do context "when the latest default branch pipeline's source is auto devops" do
before do before do
create( pipeline = create(
:ci_pipeline, :ci_pipeline,
:auto_devops_source, :auto_devops_source,
project: project, project: project,
ref: project.default_branch, ref: project.default_branch,
sha: project.commit.sha sha: project.commit.sha
) )
create(:ci_build, :sast, pipeline: pipeline, status: 'success')
create(:ci_build, :dast, pipeline: pipeline, status: 'success')
create(:ci_build, :secret_detection, pipeline: pipeline, status: 'pending')
end end
it 'reports that auto devops is enabled' do it 'reports that auto devops is enabled' do
...@@ -56,13 +59,13 @@ RSpec.describe Projects::Security::ConfigurationPresenter do ...@@ -56,13 +59,13 @@ RSpec.describe Projects::Security::ConfigurationPresenter do
expect(subject[:can_toggle_auto_fix_settings]).to be_truthy expect(subject[:can_toggle_auto_fix_settings]).to be_truthy
end end
it 'reports that all security jobs are configured' do it 'reports that all scanners are configured for which latest pipeline has builds' do
expect(Gitlab::Json.parse(subject[:features])).to contain_exactly( expect(Gitlab::Json.parse(subject[:features])).to contain_exactly(
security_scan(:dast, configured: true), security_scan(:dast, configured: true),
security_scan(:sast, configured: true), security_scan(:sast, configured: true),
security_scan(:container_scanning, configured: true), security_scan(:container_scanning, configured: false),
security_scan(:dependency_scanning, configured: true), security_scan(:dependency_scanning, configured: false),
security_scan(:license_scanning, configured: true), security_scan(:license_scanning, configured: false),
security_scan(:secret_detection, configured: true) security_scan(:secret_detection, configured: true)
) )
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