Commit 98e2875f authored by Saikat Sarkar's avatar Saikat Sarkar

Show configuration status of stand-alone secrets analyzer

parent fd05e5ef
......@@ -27,6 +27,7 @@ module Ci
accessibility: 'gl-accessibility.json',
codequality: 'gl-code-quality-report.json',
sast: 'gl-sast-report.json',
secret_detection: 'gl-secret-detection-report.json',
dependency_scanning: 'gl-dependency-scanning-report.json',
container_scanning: 'gl-container-scanning-report.json',
dast: 'gl-dast-report.json',
......@@ -63,6 +64,7 @@ module Ci
accessibility: :raw,
codequality: :raw,
sast: :raw,
secret_detection: :raw,
dependency_scanning: :raw,
container_scanning: :raw,
dast: :raw,
......@@ -177,7 +179,8 @@ module Ci
cobertura: 17,
terraform: 18, # Transformed json
accessibility: 19,
cluster_applications: 20
cluster_applications: 20,
secret_detection: 21 ## EE-specific
}
enum file_format: {
......
......@@ -11989,7 +11989,8 @@ type Vulnerability {
project: Project
"""
Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST)
Type of the security report that found the vulnerability (SAST,
DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION)
"""
reportType: VulnerabilityReportType
......@@ -12202,6 +12203,7 @@ enum VulnerabilityReportType {
DAST
DEPENDENCY_SCANNING
SAST
SECRET_DETECTION
}
"""
......
......@@ -35483,7 +35483,7 @@
},
{
"name": "reportType",
"description": "Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST)",
"description": "Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION)",
"args": [
],
......@@ -36156,6 +36156,12 @@
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "SECRET_DETECTION",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
......@@ -1812,7 +1812,7 @@ Represents a vulnerability.
| `id` | ID! | GraphQL ID of the vulnerability |
| `location` | VulnerabilityLocation | Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability |
| `project` | Project | The project on which the vulnerability was found |
| `reportType` | VulnerabilityReportType | Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST) |
| `reportType` | VulnerabilityReportType | Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION) |
| `severity` | VulnerabilitySeverity | Severity of the vulnerability (INFO, UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL) |
| `state` | VulnerabilityState | State of the vulnerability (DETECTED, DISMISSED, RESOLVED, CONFIRMED) |
| `title` | String | Title of the vulnerability |
......
......@@ -17,7 +17,7 @@ module EE
before_action :whitelist_query_limiting_ee_merge, only: [:merge]
before_action :whitelist_query_limiting_ee_show, only: [:show]
before_action :authorize_read_pipeline!, only: [:container_scanning_reports, :dependency_scanning_reports,
:sast_reports, :dast_reports, :metrics_reports]
:sast_reports, :secret_detection_reports, :dast_reports, :metrics_reports]
end
def approve
......@@ -62,6 +62,10 @@ module EE
reports_response(merge_request.compare_sast_reports(current_user))
end
def secret_detection_reports
reports_response(merge_request.compare_secret_detection_reports(current_user))
end
def dast_reports
reports_response(merge_request.compare_dast_reports(current_user))
end
......
......@@ -13,7 +13,7 @@
module Security
class SecurityJobsFinder < JobsFinder
def self.allowed_job_types
[:sast, :dast, :dependency_scanning, :container_scanning]
[:sast, :dast, :dependency_scanning, :container_scanning, :secret_detection]
end
end
end
......@@ -12,6 +12,7 @@ module EE
LICENSED_PARSER_FEATURES = {
sast: :sast,
secret_detection: :secret_detection,
dependency_scanning: :dependency_scanning,
container_scanning: :container_scanning,
dast: :dast
......
......@@ -11,12 +11,13 @@ module EE
prepended do
after_destroy :log_geo_deleted_event
SECURITY_REPORT_FILE_TYPES = %w[sast dependency_scanning container_scanning dast].freeze
SECURITY_REPORT_FILE_TYPES = %w[sast secret_detection dependency_scanning container_scanning dast].freeze
LICENSE_SCANNING_REPORT_FILE_TYPES = %w[license_management license_scanning].freeze
DEPENDENCY_LIST_REPORT_FILE_TYPES = %w[dependency_scanning].freeze
METRICS_REPORT_FILE_TYPES = %w[metrics].freeze
CONTAINER_SCANNING_REPORT_TYPES = %w[container_scanning].freeze
SAST_REPORT_TYPES = %w[sast].freeze
SECRET_DETECTION_REPORT_TYPES = %w[secret_detection].freeze
DAST_REPORT_TYPES = %w[dast].freeze
scope :not_expired, -> { where('expire_at IS NULL OR expire_at > ?', Time.current) }
......@@ -43,6 +44,10 @@ module EE
with_file_types(SAST_REPORT_TYPES)
end
scope :secret_detection_reports, -> do
with_file_types(SECRET_DETECTION_REPORT_TYPES)
end
scope :dast_reports, -> do
with_file_types(DAST_REPORT_TYPES)
end
......
......@@ -29,7 +29,7 @@ module EE
# Legacy way to fetch security reports based on job name. This has been replaced by the reports feature.
scope :with_legacy_security_reports, -> do
joins(:artifacts).where(ci_builds: { name: %w[sast dependency_scanning sast:container container_scanning dast] })
joins(:artifacts).where(ci_builds: { name: %w[sast secret_detection dependency_scanning sast:container container_scanning dast] })
end
scope :with_vulnerabilities, -> do
......@@ -41,6 +41,7 @@ module EE
REPORT_LICENSED_FEATURES = {
codequality: nil,
sast: %i[sast],
secret_detection: %i[secret_detection],
dependency_scanning: %i[dependency_scanning],
container_scanning: %i[container_scanning],
dast: %i[dast],
......
......@@ -182,12 +182,22 @@ module EE
!!(actual_head_pipeline&.has_reports?(::Ci::JobArtifact.sast_reports))
end
def has_secret_detection_reports?
!!(actual_head_pipeline&.has_reports?(::Ci::JobArtifact.secret_detection_reports))
end
def compare_sast_reports(current_user)
return missing_report_error("SAST") unless has_sast_reports?
compare_reports(::Ci::CompareSastReportsService, current_user)
end
def compare_secret_detection_reports(current_user)
return missing_report_error("secret detection") unless has_secret_detection_reports?
compare_reports(::Ci::CompareSecretDetectionReportsService, current_user)
end
def has_dast_reports?
!!(actual_head_pipeline&.has_reports?(::Ci::JobArtifact.dast_reports))
end
......
......@@ -294,6 +294,7 @@ module EE
def store_security_reports_available?
feature_available?(:sast) ||
feature_available?(:secret_detection) ||
feature_available?(:dependency_scanning) ||
feature_available?(:container_scanning) ||
feature_available?(:dast)
......
......@@ -127,6 +127,7 @@ class License < ApplicationRecord
report_approver_rules
requirements
sast
secret_detection
security_dashboard
status_page
subepics
......
......@@ -14,7 +14,8 @@ module Security
sast: 1,
dependency_scanning: 2,
container_scanning: 3,
dast: 4
dast: 4,
secret_detection: 5
}
end
end
......@@ -15,7 +15,7 @@ module Vulnerabilities
attr_accessor :vulnerability_data
enum feedback_type: { dismissal: 0, issue: 1, merge_request: 2 }, _prefix: :for
enum category: { sast: 0, dependency_scanning: 1, container_scanning: 2, dast: 3 }
enum category: { sast: 0, dependency_scanning: 1, container_scanning: 2, dast: 3, secret_detection: 4 }
validates :project, presence: true
validates :author, presence: true
......
......@@ -53,7 +53,8 @@ module Vulnerabilities
sast: 0,
dependency_scanning: 1,
container_scanning: 2,
dast: 3
dast: 3,
secret_detection: 4
}.with_indifferent_access.freeze
enum confidence: CONFIDENCE_LEVELS, _prefix: :confidence
......
......@@ -22,6 +22,7 @@ module EE
return false unless can?(current_user, :read_vulnerability, pipeline.project)
batch_lookup_report_artifact_for_file_type(:sast) ||
batch_lookup_report_artifact_for_file_type(:secret_detection) ||
batch_lookup_report_artifact_for_file_type(:dependency_scanning) ||
batch_lookup_report_artifact_for_file_type(:dast) ||
batch_lookup_report_artifact_for_file_type(:container_scanning)
......
......@@ -13,7 +13,8 @@ module Projects
dependency_scanning: 'user/application_security/dependency_scanning/index',
license_management: 'user/compliance/license_compliance/index',
license_scanning: 'user/compliance/license_compliance/index',
sast: 'user/application_security/sast/index'
sast: 'user/application_security/sast/index',
secret_detection: 'user/application_security/secret_detection/index'
}.freeze
def self.localized_scan_descriptions
......@@ -23,7 +24,8 @@ module Projects
dependency_scanning: _('Analyze your dependencies for known vulnerabilities.'),
license_management: _('Search your project dependencies for their licenses and apply policies.'),
license_scanning: _('Search your project dependencies for their licenses and apply policies.'),
sast: _('Analyze your source code for known vulnerabilities.')
sast: _('Analyze your source code for known vulnerabilities.'),
secret_detection: _('Analyze your source code and git history for secrets')
}.freeze
end
......@@ -34,7 +36,8 @@ module Projects
dependency_scanning: _('Dependency Scanning'),
license_management: 'License Management',
license_scanning: _('License Compliance'),
sast: _('Static Application Security Testing (SAST)')
sast: _('Static Application Security Testing (SAST)'),
secret_detection: _('Secret Detection')
}.freeze
end
......
# frozen_string_literal: true
module Ci
class CompareSecretDetectionReportsService < ::Ci::CompareReportsBaseService
def comparer_class
Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer
end
def serializer_class
Vulnerabilities::FindingDiffSerializer
end
def get_report(pipeline)
Security::PipelineVulnerabilitiesFinder.new(pipeline: pipeline, params: { report_type: %w[secret_detection], scope: 'all' }).execute
end
def build_comparer(base_pipeline, head_pipeline)
comparer_class.new(get_report(base_pipeline), get_report(head_pipeline), head_security_scans: head_pipeline.security_scans)
end
end
end
......@@ -10,6 +10,7 @@
window.gl.mrWidgetData.is_geo_secondary_node = '#{Gitlab::Geo.secondary?}' === 'true';
window.gl.mrWidgetData.geo_secondary_help_path = '#{help_page_path("administration/geo/replication/configuration.md")}';
window.gl.mrWidgetData.sast_help_path = '#{help_page_path("user/application_security/sast/index")}';
window.gl.mrWidgetData.secret_detection_help_path = '#{help_page_path("user/application_security/secret_detection/index")}';
window.gl.mrWidgetData.container_scanning_help_path = '#{help_page_path("user/application_security/container_scanning/index")}';
window.gl.mrWidgetData.dast_help_path = '#{help_page_path("user/application_security/dast/index")}';
window.gl.mrWidgetData.dependency_scanning_help_path = '#{help_page_path("user/application_security/dependency_scanning/index")}';
......
---
title: Show the status of stand-alone secrets analyzer on the configuration page
merge_request: 31167
author:
type: added
......@@ -9,6 +9,7 @@ resources :merge_requests, only: [], constraints: { id: /\d+/ } do
get :container_scanning_reports
get :dependency_scanning_reports
get :sast_reports
get :secret_detection_reports
get :dast_reports
get :approvals
......
......@@ -15,6 +15,7 @@ module EE
container_scanning: ::Gitlab::Ci::Parsers::Security::ContainerScanning,
dast: ::Gitlab::Ci::Parsers::Security::Dast,
sast: ::Gitlab::Ci::Parsers::Security::Sast,
secret_detection: ::Gitlab::Ci::Parsers::Security::SecretDetection,
metrics: ::Gitlab::Ci::Parsers::Metrics::Generic
})
end
......
# frozen_string_literal: true
module Gitlab
module Ci
module Parsers
module Security
class SecretDetection < Common
include Security::Concerns::DeprecatedSyntax
DEPRECATED_REPORT_VERSION = "1.2".freeze
private
def create_location(location_data)
::Gitlab::Ci::Reports::Security::Locations::SecretDetection.new(
file_path: location_data['file'],
start_line: location_data['start_line'],
end_line: location_data['end_line'],
class_name: location_data['class'],
method_name: location_data['method']
)
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
module Security
module Locations
class SecretDetection < Base
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
private
def fingerprint_data
"#{file_path}:#{start_line}:#{end_line}"
end
end
end
end
end
end
end
......@@ -628,6 +628,77 @@ describe Projects::MergeRequestsController do
it_behaves_like 'authorize read pipeline'
end
describe 'GET #secret_detection_reports' do
let(:merge_request) { create(:ee_merge_request, :with_secret_detection_reports, source_project: project, author: create(:user)) }
let(:params) do
{
namespace_id: project.namespace.to_param,
project_id: project,
id: merge_request.iid
}
end
subject { get :secret_detection_reports, params: params, format: :json }
before do
allow_any_instance_of(::MergeRequest).to receive(:compare_reports)
.with(::Ci::CompareSecretDetectionReportsService, project.users.first).and_return(comparison_status)
end
context 'when comparison is being processed' do
let(:comparison_status) { { status: :parsing } }
it 'sends polling interval' do
expect(::Gitlab::PollingInterval).to receive(:set_header)
subject
end
it 'returns 204 HTTP status' do
subject
expect(response).to have_gitlab_http_status(:no_content)
end
end
context 'when comparison is done' do
let(:comparison_status) { { status: :parsed, data: { added: [], fixed: [], existing: [] } } }
it 'does not send polling interval' do
expect(::Gitlab::PollingInterval).not_to receive(:set_header)
subject
end
it 'returns 200 HTTP status' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq({ "added" => [], "fixed" => [], "existing" => [] })
end
end
context 'when user created corrupted vulnerability reports' do
let(:comparison_status) { { status: :error, status_reason: 'Failed to parse secret detection reports' } }
it 'does not send polling interval' do
expect(::Gitlab::PollingInterval).not_to receive(:set_header)
subject
end
it 'returns 400 HTTP status' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq({ 'status_reason' => 'Failed to parse secret detection reports' })
end
end
it_behaves_like 'authorize read pipeline'
end
describe 'GET #dast_reports' do
let(:merge_request) { create(:ee_merge_request, :with_dast_reports, source_project: project) }
let(:params) do
......
......@@ -6,7 +6,7 @@ FactoryBot.define do
failure_reason { Ci::Build.failure_reasons[:protected_environment_failure] }
end
%i[codequality container_scanning dast dependency_scanning license_management license_scanning performance sast].each do |report_type|
%i[codequality container_scanning dast dependency_scanning license_management license_scanning performance sast secret_detection].each do |report_type|
trait "legacy_#{report_type}".to_sym do
success
artifacts
......@@ -54,6 +54,12 @@ FactoryBot.define do
end
end
trait :secret_detection_feature_branch do
after(:build) do |build|
build.job_artifacts << create(:ee_ci_job_artifact, :secret_detection_feature_branch, job: build)
end
end
trait :dast_feature_branch do
after(:build) do |build|
build.job_artifacts << create(:ee_ci_job_artifact, :dast_feature_branch, job: build)
......
......@@ -12,6 +12,16 @@ FactoryBot.define do
end
end
trait :secret_detection do
file_type { :secret_detection }
file_format { :raw }
after(:build) do |artifact, _|
artifact.file = fixture_file_upload(
Rails.root.join('ee/spec/fixtures/security_reports/master/gl-secret-detection-report.json'), 'application/json')
end
end
trait :dast do
file_format { :raw }
file_type { :dast }
......@@ -119,6 +129,16 @@ FactoryBot.define do
end
end
trait :secret_detection_feature_branch do
file_format { :raw }
file_type { :secret_detection }
after(:build) do |artifact, _|
artifact.file = fixture_file_upload(
Rails.root.join('ee/spec/fixtures/security_reports/feature-branch/gl-secret-detection-report.json'), 'application/json')
end
end
trait :sast_deprecated do
file_type { :sast }
file_format { :raw }
......
......@@ -2,7 +2,7 @@
FactoryBot.define do
factory :ee_ci_pipeline, class: 'Ci::Pipeline', parent: :ci_pipeline do
%i[container_scanning dast dependency_list dependency_scanning license_management license_scanning sast].each do |report_type|
%i[container_scanning dast dependency_list dependency_scanning license_management license_scanning sast secret_detection].each do |report_type|
trait "with_#{report_type}_report".to_sym do
status { :success }
......@@ -52,6 +52,14 @@ FactoryBot.define do
end
end
trait :with_secret_detection_feature_branch do
status { :success }
after(:build) do |pipeline, evaluator|
pipeline.builds << build(:ee_ci_build, :secret_detection_feature_branch, pipeline: pipeline, project: pipeline.project)
end
end
trait :with_dast_feature_branch do
status { :success }
......
......@@ -133,6 +133,18 @@ FactoryBot.define do
end
end
trait :with_secret_detection_reports do
after(:build) do |merge_request|
merge_request.head_pipeline = build(
:ee_ci_pipeline,
:success,
:with_secret_detection_report,
project: merge_request.source_project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha)
end
end
trait :with_dast_reports do
after(:build) do |merge_request|
merge_request.head_pipeline = build(
......
......@@ -15,12 +15,14 @@ describe Security::SecurityJobsFinder do
let!(:sast_build) { create(:ci_build, :sast, pipeline: pipeline) }
let!(:container_scanning_build) { create(:ci_build, :container_scanning, pipeline: pipeline) }
let!(:dast_build) { create(:ci_build, :dast, pipeline: pipeline) }
let!(:secret_detection_build) { create(:ci_build, :secret_detection, pipeline: pipeline) }
let(:finder) { described_class.new(pipeline: pipeline, job_types: [:sast, :container_scanning]) }
let(:finder) { described_class.new(pipeline: pipeline, job_types: [:sast, :container_scanning, :secret_detection]) }
it 'returns only those requested' do
is_expected.to include(sast_build)
is_expected.to include(container_scanning_build)
is_expected.to include(secret_detection_build)
is_expected.not_to include(dast_build)
end
......@@ -30,13 +32,14 @@ describe Security::SecurityJobsFinder do
let!(:sast_build) { create(:ci_build, :sast, pipeline: pipeline) }
let!(:container_scanning_build) { create(:ci_build, :container_scanning, pipeline: pipeline) }
let!(:dast_build) { create(:ci_build, :dast, pipeline: pipeline) }
let!(:secret_detection_build) { create(:ci_build, :secret_detection, pipeline: pipeline) }
let!(:license_management_build) { create(:ci_build, :license_management, pipeline: pipeline) }
it 'returns only the security jobs' do
is_expected.to include(sast_build)
is_expected.to include(container_scanning_build)
is_expected.to include(dast_build)
is_expected.to include(secret_detection_build)
is_expected.not_to include(license_management_build)
end
end
......
{
"version": "3.0",
"vulnerabilities": [],
"remediations": []
}
{
"version": "3.0",
"vulnerabilities": [
{
"id": "27d2322d519c94f803ffed1cf6d14e455df97e5a0668e229eb853fdb0d277d2c",
"category": "secret_detection",
"name": "AWS API key",
"message": "AWS API key",
"description": "Historic AWS secret has been found in commit 0830d9e4c0b43c0533cde798841b499e9df0653a.",
"cve": "aws-key.py:e275768c071cf6a6ea70a70b40f27c98debfe26bfe623c1539ec21c4478c6fca:AWS",
"severity": "Critical",
"confidence": "Unknown",
"scanner": {
"id": "gitleaks",
"name": "Gitleaks"
},
"location": {
"file": "aws-key.py",
"dependency": {
"package": {}
}
},
"identifiers": [
{
"type": "gitleaks_rule_id",
"name": "Gitleaks rule ID AWS",
"value": "AWS"
}
]
}
],
"remediations": []
}
......@@ -4,6 +4,6 @@ require 'spec_helper'
describe GitlabSchema.types['VulnerabilityReportType'] do
it 'exposes all vulnerability report types' do
expect(described_class.values.keys).to contain_exactly(*%w[SAST DAST CONTAINER_SCANNING DEPENDENCY_SCANNING])
expect(described_class.values.keys).to contain_exactly(*%w[SAST SECRET_DETECTION DAST CONTAINER_SCANNING DEPENDENCY_SCANNING])
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Parsers::Security::SecretDetection do
describe '#parse!' do
subject(:parser) { described_class.new }
let(:commit_sha) { "d8978e74745e18ce44d88814004d4255ac6a65bb" }
let(:created_at) { 2.weeks.ago }
context "when parsing valid reports" do
where(report_format: %i(secret_detection))
with_them do
let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, commit_sha, created_at) }
let(:artifact) { create(:ee_ci_job_artifact, report_format) }
before do
artifact.each_blob do |blob|
parser.parse!(blob, report)
end
end
it "parses all identifiers and occurrences" do
expect(report.occurrences.length).to eq(1)
expect(report.identifiers.length).to eq(1)
expect(report.scanners.length).to eq(1)
end
it 'generates expected location' do
location = report.occurrences.first.location
expect(location).to be_a(::Gitlab::Ci::Reports::Security::Locations::SecretDetection)
expect(location).to have_attributes(
file_path: 'aws-key.py',
start_line: nil,
end_line: nil,
class_name: nil,
method_name: nil
)
end
it "generates expected metadata_version" do
expect(report.occurrences.first.metadata_version).to eq('3.0')
end
end
end
context "when parsing an empty report" do
let(:report) { Gitlab::Ci::Reports::Security::Report.new('secret_detection', commit_sha, created_at) }
let(:blob) { Gitlab::Json.generate({}) }
it { expect(parser.parse!(blob, report)).to be_empty }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Reports::Security::Locations::SecretDetection do
let(:params) do
{
file_path: 'src/main/App.java',
start_line: 29,
end_line: 31,
class_name: 'com.gitlab.security_products.tests.App',
method_name: 'insecureCypher'
}
end
let(:mandatory_params) { %i[file_path start_line] }
let(:expected_fingerprint) { Digest::SHA1.hexdigest('src/main/App.java:29:31') }
it_behaves_like 'vulnerability location'
end
......@@ -56,9 +56,10 @@ describe EE::Ci::JobArtifact do
subject { Ci::JobArtifact.security_reports }
context 'when there is a security report' do
let!(:artifact) { create(:ee_ci_job_artifact, :sast) }
let!(:sast_artifact) { create(:ee_ci_job_artifact, :sast) }
let!(:secret_detection_artifact) { create(:ee_ci_job_artifact, :secret_detection) }
it { is_expected.to eq([artifact]) }
it { is_expected.to eq([sast_artifact, secret_detection_artifact]) }
end
context 'when there are no security reports' do
......
......@@ -1284,7 +1284,7 @@ describe Namespace do
subject { namespace.store_security_reports_available? }
context 'when at least one security report feature is enabled' do
where(report_type: [:sast, :dast, :dependency_scanning, :container_scanning])
where(report_type: [:sast, :secret_detection, :dast, :dependency_scanning, :container_scanning])
with_them do
before do
......
......@@ -207,6 +207,28 @@ describe MergeRequest do
end
end
describe '#has_secret_detection_reports?' do
subject { merge_request.has_secret_detection_reports? }
let(:project) { create(:project, :repository) }
before do
stub_licensed_features(secret_detection: true)
end
context 'when head pipeline has secret detection reports' do
let(:merge_request) { create(:ee_merge_request, :with_secret_detection_reports, source_project: project) }
it { is_expected.to be_truthy }
end
context 'when head pipeline does not have secrets detection reports' do
let(:merge_request) { create(:ee_merge_request, source_project: project) }
it { is_expected.to be_falsey }
end
end
describe '#has_dast_reports?' do
subject { merge_request.has_dast_reports? }
......@@ -336,6 +358,66 @@ describe MergeRequest do
end
end
describe '#compare_secret_detection_reports' do
subject { merge_request.compare_secret_detection_reports(current_user) }
let(:project) { create(:project, :repository) }
let(:current_user) { project.users.first }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:base_pipeline) do
create(:ee_ci_pipeline,
:with_secret_detection_report,
project: project,
ref: merge_request.target_branch,
sha: merge_request.diff_base_sha)
end
before do
merge_request.update!(head_pipeline_id: head_pipeline.id)
end
context 'when head pipeline has secret detection reports' do
let!(:head_pipeline) do
create(:ee_ci_pipeline,
:with_secret_detection_report,
project: project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha)
end
context 'when reactive cache worker is parsing asynchronously' do
it 'returns status' do
expect(subject[:status]).to eq(:parsing)
end
end
context 'when reactive cache worker is inline' do
before do
synchronous_reactive_cache(merge_request)
end
it 'returns status and data' do
expect_any_instance_of(Ci::CompareSecretDetectionReportsService)
.to receive(:execute).with(base_pipeline, head_pipeline).and_call_original
subject
end
context 'when cached results is not latest' do
before do
allow_any_instance_of(Ci::CompareSecretDetectionReportsService)
.to receive(:latest?).and_return(false)
end
it 'raises and InvalidateReactiveCache error' do
expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
end
end
end
end
end
describe '#compare_sast_reports' do
subject { merge_request.compare_sast_reports(current_user) }
......
......@@ -132,6 +132,9 @@ describe Vulnerabilities::Occurrence do
create_list(:vulnerabilities_occurrence, 1,
pipelines: [pipeline], project: project, report_type: :dast, severity: :low)
create_list(:vulnerabilities_occurrence, 2,
pipelines: [pipeline], project: project, report_type: :secret_detection, severity: :critical)
end
end
......@@ -145,7 +148,7 @@ describe Vulnerabilities::Occurrence do
let(:range) { 3.days }
it 'returns expected counts for occurrences' do
first, second = subject
first, second, third = subject
expect(first.day).to eq(date_2)
expect(first.severity).to eq('low')
......@@ -153,6 +156,9 @@ describe Vulnerabilities::Occurrence do
expect(second.day).to eq(date_2)
expect(second.severity).to eq('medium')
expect(second.count).to eq(1)
expect(third.day).to eq(date_2)
expect(third.severity).to eq('critical')
expect(third.count).to eq(2)
end
end
......@@ -160,7 +166,7 @@ describe Vulnerabilities::Occurrence do
let(:range) { 4.days }
it 'returns expected counts for occurrences' do
first, second, third = subject
first, second, third, forth = subject
expect(first.day).to eq(date_1)
expect(first.severity).to eq('high')
......@@ -171,12 +177,16 @@ describe Vulnerabilities::Occurrence do
expect(third.day).to eq(date_2)
expect(third.severity).to eq('medium')
expect(third.count).to eq(1)
expect(forth.day).to eq(date_2)
expect(forth.severity).to eq('critical')
expect(forth.count).to eq(2)
end
end
end
describe '.by_report_types' do
let!(:vulnerability_sast) { create(:vulnerabilities_occurrence, report_type: :sast) }
let!(:vulnerability_secret_detection) { create(:vulnerabilities_occurrence, report_type: :secret_detection) }
let!(:vulnerability_dast) { create(:vulnerabilities_occurrence, report_type: :dast) }
let!(:vulnerability_depscan) { create(:vulnerabilities_occurrence, report_type: :dependency_scanning) }
......@@ -191,10 +201,10 @@ describe Vulnerabilities::Occurrence do
end
context 'with array of params' do
let(:param) { [1, 3] }
let(:param) { [1, 3, 4] }
it 'returns found records' do
is_expected.to contain_exactly(vulnerability_dast, vulnerability_depscan)
is_expected.to contain_exactly(vulnerability_dast, vulnerability_depscan, vulnerability_secret_detection)
end
end
......
......@@ -13,7 +13,8 @@ describe Vulnerability do
{ sast: 0,
dependency_scanning: 1,
container_scanning: 2,
dast: 3 }
dast: 3,
secret_detection: 4 }
end
it { is_expected.to define_enum_for(:state).with_values(state_values) }
......
......@@ -42,7 +42,8 @@ describe Projects::Security::ConfigurationPresenter do
security_scan(:sast, configured: true),
security_scan(:container_scanning, configured: true),
security_scan(:dependency_scanning, configured: true),
security_scan(:license_scanning, configured: true)
security_scan(:license_scanning, configured: true),
security_scan(:secret_detection, configured: true)
)
end
end
......@@ -62,7 +63,8 @@ describe Projects::Security::ConfigurationPresenter do
security_scan(:sast, configured: false),
security_scan(:container_scanning, configured: false),
security_scan(:dependency_scanning, configured: false),
security_scan(:license_scanning, configured: false)
security_scan(:license_scanning, configured: false),
security_scan(:secret_detection, configured: false)
)
end
end
......@@ -80,6 +82,7 @@ describe Projects::Security::ConfigurationPresenter do
before do
create(:ci_build, :sast, pipeline: pipeline)
create(:ci_build, :dast, pipeline: pipeline)
create(:ci_build, :secret_detection, pipeline: pipeline)
end
it 'uses the latest default branch pipeline to determine whether a security job is configured' do
......@@ -88,7 +91,8 @@ describe Projects::Security::ConfigurationPresenter do
security_scan(:sast, configured: true),
security_scan(:container_scanning, configured: false),
security_scan(:dependency_scanning, configured: false),
security_scan(:license_scanning, configured: false)
security_scan(:license_scanning, configured: false),
security_scan(:secret_detection, configured: true)
)
end
......@@ -102,7 +106,8 @@ describe Projects::Security::ConfigurationPresenter do
security_scan(:sast, configured: true),
security_scan(:container_scanning, configured: false),
security_scan(:dependency_scanning, configured: false),
security_scan(:license_scanning, configured: false)
security_scan(:license_scanning, configured: false),
security_scan(:secret_detection, configured: false)
)
end
......@@ -122,7 +127,8 @@ describe Projects::Security::ConfigurationPresenter do
security_scan(:sast, configured: true),
security_scan(:container_scanning, configured: false),
security_scan(:dependency_scanning, configured: false),
security_scan(:license_scanning, configured: false)
security_scan(:license_scanning, configured: false),
security_scan(:secret_detection, configured: false)
)
end
......@@ -134,7 +140,8 @@ describe Projects::Security::ConfigurationPresenter do
security_scan(:sast, configured: true),
security_scan(:container_scanning, configured: false),
security_scan(:dependency_scanning, configured: false),
security_scan(:license_scanning, configured: true)
security_scan(:license_scanning, configured: true),
security_scan(:secret_detection, configured: true)
)
end
......
# frozen_string_literal: true
require 'spec_helper'
describe Ci::CompareSecretDetectionReportsService do
let(:current_user) { build(:user, :admin) }
let(:service) { described_class.new(project, current_user) }
let(:project) { build(:project, :repository) }
before do
stub_licensed_features(container_scanning: true)
stub_licensed_features(secret_detection: true)
end
describe '#execute' do
subject { service.execute(base_pipeline, head_pipeline) }
context 'when head pipeline has secret_detection reports' do
let!(:base_pipeline) { create(:ee_ci_pipeline) }
let!(:head_pipeline) { create(:ee_ci_pipeline, :with_secret_detection_report, project: project) }
it 'reports new vulnerabilities' do
expect(subject[:status]).to eq(:parsed)
expect(subject[:data]['added'].count).to eq(1)
expect(subject[:data]['existing'].count).to eq(0)
expect(subject[:data]['fixed'].count).to eq(0)
end
end
context 'when base and head pipelines have secret_detection reports' do
let!(:base_pipeline) { create(:ee_ci_pipeline, :with_secret_detection_report, project: project) }
let!(:head_pipeline) { create(:ee_ci_pipeline, :with_secret_detection_feature_branch, project: project) }
it 'reports status as parsed' do
expect(subject[:status]).to eq(:parsed)
end
it 'populates fields based on current_user' do
payload = subject[:data]['existing'].first
expect(payload).to be_nil
expect(service.current_user).to eq(current_user)
end
it 'reports new vulnerability' do
expect(subject[:data]['added'].count).to eq(0)
end
it 'reports existing secret_detection vulnerabilities' do
expect(subject[:data]['existing'].count).to eq(0)
end
it 'reports fixed secret_detection vulnerabilities' do
expect(subject[:data]['fixed'].count).to eq(1)
compare_keys = subject[:data]['fixed'].map { |t| t['identifiers'].first['external_id'] }
expected_keys = %w(AWS)
expect(compare_keys - expected_keys).to eq([])
end
end
end
end
......@@ -2413,6 +2413,9 @@ msgstr ""
msgid "Analyze your dependencies for known vulnerabilities."
msgstr ""
msgid "Analyze your source code and git history for secrets"
msgstr ""
msgid "Analyze your source code for known vulnerabilities."
msgstr ""
......@@ -19071,6 +19074,9 @@ msgstr ""
msgid "Secret"
msgstr ""
msgid "Secret Detection"
msgstr ""
msgid "Security"
msgstr ""
......
......@@ -400,6 +400,14 @@ FactoryBot.define do
end
end
trait :secret_detection do
options do
{
artifacts: { reports: { secret_detection: 'gl-secret-detection-report.json' } }
}
end
end
trait :dependency_scanning do
options do
{
......
......@@ -30,7 +30,7 @@ describe Ci::RetryBuildService do
created_at updated_at started_at finished_at queued_at erased_by
erased_at auto_canceled_by job_artifacts job_artifacts_archive
job_artifacts_metadata job_artifacts_trace job_artifacts_junit
job_artifacts_sast job_artifacts_dependency_scanning
job_artifacts_sast job_artifacts_secret_detection job_artifacts_dependency_scanning
job_artifacts_container_scanning job_artifacts_dast
job_artifacts_license_management job_artifacts_license_scanning
job_artifacts_performance job_artifacts_lsif
......
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