Commit 55924293 authored by Adam Cohen's avatar Adam Cohen Committed by Imre Farkas

Handle new Container Scanning report format

Deprecate old `clair-scanner` container scanning report format and
handle the new report format based on the Security Products Common
Format

Add conditional to switch container scanning formatting behaviour
based on report type

Update container scanning fixtures to include deprecated `clair-scanner`
report and Security Products Common Format
parent 348d09b1
---
title: Handle new Container Scanning report format
merge_request: 19123
author:
type: changed
......@@ -9,32 +9,35 @@ module Gitlab
DEPRECATED_REPORT_VERSION = "1.3".freeze
def parse!(json_data, report)
vulnerabilities = format_report(JSON.parse!(json_data))
vulnerabilities.each do |vulnerability|
create_vulnerability(report, vulnerability, DEPRECATED_REPORT_VERSION)
end
rescue JSON::ParserError
raise SecurityReportParserError, 'JSON parsing failed'
rescue
raise SecurityReportParserError, "#{report.type} security report parsing failed"
def parse_report(json_data)
report = super
return format_deprecated_report(report) if deprecated?(report)
report
end
private
# Transforms the Clair JSON report into the expected format
def format_report(data)
vulnerabilities = data['vulnerabilities']
# Transforms the clair-scanner JSON report into the expected format
# TODO: remove the following block when we no longer need to support legacy
# clair-scanner data. See https://gitlab.com/gitlab-org/gitlab/issues/35442
def format_deprecated_report(data)
unapproved = data['unapproved']
formatter = Formatters::ContainerScanning.new(data['image'])
formatter = Formatters::DeprecatedContainerScanning.new(data['image'])
vulnerabilities.map do |vulnerability|
vulnerabilities = data['vulnerabilities'].map do |vulnerability|
# We only report unapproved vulnerabilities
next unless unapproved.include?(vulnerability['vulnerability'])
formatter.format(vulnerability)
end.compact
{ "vulnerabilities" => vulnerabilities, "version" => DEPRECATED_REPORT_VERSION }
end
def deprecated?(data)
data['image']
end
def create_location(location_data)
......
# frozen_string_literal: true
# TODO: remove this class when we no longer need to support legacy
# clair-scanner data. See https://gitlab.com/gitlab-org/gitlab/issues/35442
module Gitlab
module Ci
module Parsers
module Security
module Formatters
class ContainerScanning
class DeprecatedContainerScanning
def initialize(image)
@image = image
end
def format(vulnerability)
formatted_vulnerability = FormattedContainerScanningVulnerability.new(vulnerability)
formatted_vulnerability = DeprecatedFormattedContainerScanningVulnerability.new(vulnerability)
{
'category' => 'container_scanning',
......
# frozen_string_literal: true
# TODO: remove this class when we no longer need to support legacy
# clair-scanner data. See https://gitlab.com/gitlab-org/gitlab/issues/35442
module Gitlab
module Ci
module Parsers
module Security
module Formatters
class FormattedContainerScanningVulnerability
class DeprecatedFormattedContainerScanningVulnerability
def initialize(vulnerability)
@vulnerability = vulnerability
end
......
......@@ -72,6 +72,12 @@ FactoryBot.define do
end
end
trait :deprecated_container_scanning_report do
after(:build) do |build|
build.job_artifacts << create(:ee_ci_job_artifact, :deprecated_container_scanning_report, job: build)
end
end
trait :dependency_scanning_feature_branch do
after(:build) do |build|
build.job_artifacts << create(:ee_ci_job_artifact, :dependency_scanning_feature_branch, job: build)
......
......@@ -232,6 +232,16 @@ FactoryBot.define do
end
end
trait :deprecated_container_scanning_report do
file_format { :raw }
file_type { :container_scanning }
after(:build) do |artifact, _|
artifact.file = fixture_file_upload(
Rails.root.join('ee/spec/fixtures/security_reports/deprecated/gl-container-scanning-report.json'), 'text/plain')
end
end
trait :metrics do
file_format { :gzip }
file_type { :metrics }
......
......@@ -19,292 +19,309 @@ describe Security::PipelineVulnerabilitiesFinder do
end
end
describe '#execute' do
set(:project) { create(:project, :repository) }
set(:pipeline) { create(:ci_pipeline, :success, project: project) }
let(:params) { {} }
set(:build_cs) { create(:ci_build, :success, name: 'cs_job', pipeline: pipeline, project: project) }
set(:build_dast) { create(:ci_build, :success, name: 'dast_job', pipeline: pipeline, project: project) }
set(:build_ds) { create(:ci_build, :success, name: 'ds_job', pipeline: pipeline, project: project) }
set(:build_sast) { create(:ci_build, :success, name: 'sast_job', pipeline: pipeline, project: project) }
set(:artifact_cs) { create(:ee_ci_job_artifact, :container_scanning, job: build_cs, project: project) }
set(:artifact_dast) { create(:ee_ci_job_artifact, :dast, job: build_dast, project: project) }
set(:artifact_ds) { create(:ee_ci_job_artifact, :dependency_scanning, job: build_ds, project: project) }
set(:artifact_sast) { create(:ee_ci_job_artifact, :sast, job: build_sast, project: project) }
let(:cs_count) { read_fixture(artifact_cs)['unapproved'].count }
let(:ds_count) { read_fixture(artifact_ds)['vulnerabilities'].count }
let(:sast_count) { read_fixture(artifact_sast)['vulnerabilities'].count }
let(:dast_count) do
read_fixture(artifact_dast)['site'].sum do |site|
site['alerts'].sum do |alert|
alert['instances'].size
let_it_be(:project) { create(:project, :repository) }
let_it_be(:pipeline) { create(:ci_pipeline, :success, project: project) }
let_it_be(:build_cs) { create(:ci_build, :success, name: 'cs_job', pipeline: pipeline, project: project) }
shared_examples_for 'a pipeline vulnerabilities finder' do
describe '#execute' do
let(:params) { {} }
let_it_be(:build_dast) { create(:ci_build, :success, name: 'dast_job', pipeline: pipeline, project: project) }
let_it_be(:build_ds) { create(:ci_build, :success, name: 'ds_job', pipeline: pipeline, project: project) }
let_it_be(:build_sast) { create(:ci_build, :success, name: 'sast_job', pipeline: pipeline, project: project) }
let_it_be(:artifact_dast) { create(:ee_ci_job_artifact, :dast, job: build_dast, project: project) }
let_it_be(:artifact_ds) { create(:ee_ci_job_artifact, :dependency_scanning, job: build_ds, project: project) }
let_it_be(:artifact_sast) { create(:ee_ci_job_artifact, :sast, job: build_sast, project: project) }
let(:ds_count) { read_fixture(artifact_ds)['vulnerabilities'].count }
let(:sast_count) { read_fixture(artifact_sast)['vulnerabilities'].count }
let(:dast_count) do
read_fixture(artifact_dast)['site'].sum do |site|
site['alerts'].sum do |alert|
alert['instances'].size
end
end
end
end
before do
stub_licensed_features(sast: true, dependency_scanning: true, container_scanning: true, dast: true)
# Stub out deduplication, if not done the expectations will vary based on the fixtures (which may/may not have duplicates)
disable_deduplication
end
subject { described_class.new(pipeline: pipeline, params: params).execute }
before do
stub_licensed_features(sast: true, dependency_scanning: true, container_scanning: true, dast: true)
# Stub out deduplication, if not done the expectations will vary based on the fixtures (which may/may not have duplicates)
disable_deduplication
end
it 'assigns commit sha to findings' do
expect(subject.map(&:sha).uniq).to eq [pipeline.sha]
end
subject { described_class.new(pipeline: pipeline, params: params).execute }
context 'by order' do
let(:params) { { report_type: %w[sast] } }
let!(:high_high) { build(:vulnerabilities_occurrence, confidence: :high, severity: :high) }
let!(:critical_medium) { build(:vulnerabilities_occurrence, confidence: :medium, severity: :critical) }
let!(:critical_high) { build(:vulnerabilities_occurrence, confidence: :high, severity: :critical) }
let!(:unknown_high) { build(:vulnerabilities_occurrence, confidence: :high, severity: :unknown) }
let!(:unknown_medium) { build(:vulnerabilities_occurrence, confidence: :medium, severity: :unknown) }
let!(:unknown_low) { build(:vulnerabilities_occurrence, confidence: :low, severity: :unknown) }
it 'orders by severity and confidence' do
allow_any_instance_of(described_class).to receive(:filter).and_return([
unknown_low,
unknown_medium,
critical_high,
unknown_high,
critical_medium,
high_high
])
expect(subject).to eq([critical_high, critical_medium, high_high, unknown_high, unknown_medium, unknown_low])
it 'assigns commit sha to findings' do
expect(subject.map(&:sha).uniq).to eq [pipeline.sha]
end
end
context 'by report type' do
context 'when sast' do
context 'by order' do
let(:params) { { report_type: %w[sast] } }
let(:sast_report_fingerprints) {pipeline.security_reports.reports['sast'].occurrences.map(&:location).map(&:fingerprint) }
it 'includes only sast' do
expect(subject.map(&:location_fingerprint)).to match_array(sast_report_fingerprints)
expect(subject.count).to eq sast_count
let!(:high_high) { build(:vulnerabilities_occurrence, confidence: :high, severity: :high) }
let!(:critical_medium) { build(:vulnerabilities_occurrence, confidence: :medium, severity: :critical) }
let!(:critical_high) { build(:vulnerabilities_occurrence, confidence: :high, severity: :critical) }
let!(:unknown_high) { build(:vulnerabilities_occurrence, confidence: :high, severity: :unknown) }
let!(:unknown_medium) { build(:vulnerabilities_occurrence, confidence: :medium, severity: :unknown) }
let!(:unknown_low) { build(:vulnerabilities_occurrence, confidence: :low, severity: :unknown) }
it 'orders by severity and confidence' do
allow_next_instance_of(described_class) do |pipeline_vulnerabilities_finder|
allow(pipeline_vulnerabilities_finder).to receive(:filter).and_return([
unknown_low,
unknown_medium,
critical_high,
unknown_high,
critical_medium,
high_high
])
expect(subject).to eq([critical_high, critical_medium, high_high, unknown_high, unknown_medium, unknown_low])
end
end
end
context 'when dependency_scanning' do
let(:params) { { report_type: %w[dependency_scanning] } }
let(:ds_report_fingerprints) {pipeline.security_reports.reports['dependency_scanning'].occurrences.map(&:location).map(&:fingerprint) }
context 'by report type' do
context 'when sast' do
let(:params) { { report_type: %w[sast] } }
let(:sast_report_fingerprints) {pipeline.security_reports.reports['sast'].occurrences.map(&:location).map(&:fingerprint) }
it 'includes only dependency_scanning' do
expect(subject.map(&:location_fingerprint)).to match_array(ds_report_fingerprints)
expect(subject.count).to eq ds_count
it 'includes only sast' do
expect(subject.map(&:location_fingerprint)).to match_array(sast_report_fingerprints)
expect(subject.count).to eq sast_count
end
end
end
context 'when dast' do
let(:params) { { report_type: %w[dast] } }
let(:dast_report_fingerprints) {pipeline.security_reports.reports['dast'].occurrences.map(&:location).map(&:fingerprint) }
context 'when dependency_scanning' do
let(:params) { { report_type: %w[dependency_scanning] } }
let(:ds_report_fingerprints) {pipeline.security_reports.reports['dependency_scanning'].occurrences.map(&:location).map(&:fingerprint) }
it 'includes only dast' do
expect(subject.map(&:location_fingerprint)).to match_array(dast_report_fingerprints)
expect(subject.count).to eq dast_count
it 'includes only dependency_scanning' do
expect(subject.map(&:location_fingerprint)).to match_array(ds_report_fingerprints)
expect(subject.count).to eq ds_count
end
end
end
context 'when container_scanning' do
let(:params) { { report_type: %w[container_scanning] } }
context 'when dast' do
let(:params) { { report_type: %w[dast] } }
let(:dast_report_fingerprints) {pipeline.security_reports.reports['dast'].occurrences.map(&:location).map(&:fingerprint) }
it 'includes only container_scanning' do
fingerprints = pipeline.security_reports.reports['container_scanning'].occurrences.map(&:location).map(&:fingerprint)
expect(subject.map(&:location_fingerprint)).to match_array(fingerprints)
expect(subject.count).to eq cs_count
it 'includes only dast' do
expect(subject.map(&:location_fingerprint)).to match_array(dast_report_fingerprints)
expect(subject.count).to eq dast_count
end
end
end
end
context 'by scope' do
let(:ds_occurrence) { pipeline.security_reports.reports["dependency_scanning"].occurrences.first }
let(:sast_occurrence) { pipeline.security_reports.reports["sast"].occurrences.first }
let!(:feedback) do
[
create(
:vulnerability_feedback,
:dismissal,
:dependency_scanning,
project: project,
pipeline: pipeline,
project_fingerprint: ds_occurrence.project_fingerprint,
vulnerability_data: ds_occurrence.raw_metadata
),
create(
:vulnerability_feedback,
:dismissal,
:sast,
project: project,
pipeline: pipeline,
project_fingerprint: sast_occurrence.project_fingerprint,
vulnerability_data: sast_occurrence.raw_metadata
)
]
context 'when container_scanning' do
let(:params) { { report_type: %w[container_scanning] } }
it 'includes only container_scanning' do
fingerprints = pipeline.security_reports.reports['container_scanning'].occurrences.map(&:location).map(&:fingerprint)
expect(subject.map(&:location_fingerprint)).to match_array(fingerprints)
expect(subject.count).to eq cs_count
end
end
end
context 'when unscoped' do
subject { described_class.new(pipeline: pipeline).execute }
context 'by scope' do
let(:ds_occurrence) { pipeline.security_reports.reports["dependency_scanning"].occurrences.first }
let(:sast_occurrence) { pipeline.security_reports.reports["sast"].occurrences.first }
let!(:feedback) do
[
create(
:vulnerability_feedback,
:dismissal,
:dependency_scanning,
project: project,
pipeline: pipeline,
project_fingerprint: ds_occurrence.project_fingerprint,
vulnerability_data: ds_occurrence.raw_metadata
),
create(
:vulnerability_feedback,
:dismissal,
:sast,
project: project,
pipeline: pipeline,
project_fingerprint: sast_occurrence.project_fingerprint,
vulnerability_data: sast_occurrence.raw_metadata
)
]
end
context 'when unscoped' do
subject { described_class.new(pipeline: pipeline).execute }
it 'returns non-dismissed vulnerabilities' do
expect(subject.count).to eq cs_count + dast_count + ds_count + sast_count - feedback.count
expect(subject.map(&:project_fingerprint)).not_to include(*feedback.map(&:project_fingerprint))
it 'returns non-dismissed vulnerabilities' do
expect(subject.count).to eq cs_count + dast_count + ds_count + sast_count - feedback.count
expect(subject.map(&:project_fingerprint)).not_to include(*feedback.map(&:project_fingerprint))
end
end
end
context 'when `dismissed`' do
subject { described_class.new(pipeline: pipeline, params: { report_type: %w[dependency_scanning], scope: 'dismissed' } ).execute }
context 'when `dismissed`' do
subject { described_class.new(pipeline: pipeline, params: { report_type: %w[dependency_scanning], scope: 'dismissed' } ).execute }
it 'returns non-dismissed vulnerabilities' do
expect(subject.count).to eq(ds_count - 1)
expect(subject.map(&:project_fingerprint)).not_to include(ds_occurrence.project_fingerprint)
it 'returns non-dismissed vulnerabilities' do
expect(subject.count).to eq(ds_count - 1)
expect(subject.map(&:project_fingerprint)).not_to include(ds_occurrence.project_fingerprint)
end
end
end
context 'when `all`' do
let(:params) { { report_type: %w[sast dast container_scanning dependency_scanning], scope: 'all' } }
context 'when `all`' do
let(:params) { { report_type: %w[sast dast container_scanning dependency_scanning], scope: 'all' } }
it 'returns all vulnerabilities' do
expect(subject.count).to eq cs_count + dast_count + ds_count + sast_count
it 'returns all vulnerabilities' do
expect(subject.count).to eq cs_count + dast_count + ds_count + sast_count
end
end
end
end
context 'by severity' do
context 'when unscoped' do
subject { described_class.new(pipeline: pipeline).execute }
context 'by severity' do
context 'when unscoped' do
subject { described_class.new(pipeline: pipeline).execute }
it 'returns all vulnerability severity levels' do
expect(subject.map(&:severity).uniq).to match_array %w[undefined unknown low medium high critical info]
it 'returns all vulnerability severity levels' do
expect(subject.map(&:severity).uniq).to match_array %w[undefined unknown low medium high critical info]
end
end
end
context 'when `low`' do
subject { described_class.new(pipeline: pipeline, params: { severity: 'low' } ).execute }
context 'when `low`' do
subject { described_class.new(pipeline: pipeline, params: { severity: 'low' } ).execute }
it 'returns only low-severity vulnerabilities' do
expect(subject.map(&:severity).uniq).to match_array %w[low]
it 'returns only low-severity vulnerabilities' do
expect(subject.map(&:severity).uniq).to match_array %w[low]
end
end
end
end
context 'by confidence' do
context 'when unscoped' do
subject { described_class.new(pipeline: pipeline).execute }
context 'by confidence' do
context 'when unscoped' do
subject { described_class.new(pipeline: pipeline).execute }
it 'returns all vulnerability confidence levels' do
expect(subject.map(&:confidence).uniq).to match_array %w[undefined unknown low medium high]
it 'returns all vulnerability confidence levels' do
expect(subject.map(&:confidence).uniq).to match_array %w[undefined unknown low medium high]
end
end
end
context 'when `medium`' do
subject { described_class.new(pipeline: pipeline, params: { confidence: 'medium' } ).execute }
context 'when `medium`' do
subject { described_class.new(pipeline: pipeline, params: { confidence: 'medium' } ).execute }
it 'returns only medium-confidence vulnerabilities' do
expect(subject.map(&:confidence).uniq).to match_array %w[medium]
it 'returns only medium-confidence vulnerabilities' do
expect(subject.map(&:confidence).uniq).to match_array %w[medium]
end
end
end
end
context 'by all filters' do
context 'with found entity' do
let(:params) { { report_type: %w[sast dast container_scanning dependency_scanning], scope: 'all' } }
context 'by all filters' do
context 'with found entity' do
let(:params) { { report_type: %w[sast dast container_scanning dependency_scanning], scope: 'all' } }
it 'filters by all params' do
expect(subject.count).to eq cs_count + dast_count + ds_count + sast_count
expect(subject.map(&:confidence).uniq).to match_array %w[undefined unknown low medium high]
expect(subject.map(&:severity).uniq).to match_array %w[undefined unknown low medium high critical info]
it 'filters by all params' do
expect(subject.count).to eq cs_count + dast_count + ds_count + sast_count
expect(subject.map(&:confidence).uniq).to match_array %w[undefined unknown low medium high]
expect(subject.map(&:severity).uniq).to match_array %w[undefined unknown low medium high critical info]
end
end
end
context 'without found entity' do
let(:params) { { report_type: %w[code_quality] } }
context 'without found entity' do
let(:params) { { report_type: %w[code_quality] } }
it 'did not find anything' do
is_expected.to be_empty
it 'did not find anything' do
is_expected.to be_empty
end
end
end
end
context 'without params' do
subject { described_class.new(pipeline: pipeline).execute }
context 'without params' do
subject { described_class.new(pipeline: pipeline).execute }
it 'returns all report_types' do
expect(subject.count).to eq cs_count + dast_count + ds_count + sast_count
it 'returns all report_types' do
expect(subject.count).to eq cs_count + dast_count + ds_count + sast_count
end
end
end
context 'when matching vulnerability records exist' do
before do
create(:vulnerabilities_finding,
:confirmed,
project: project,
report_type: 'sast',
project_fingerprint: confirmed_fingerprint)
create(:vulnerabilities_finding,
:resolved,
project: project,
report_type: 'sast',
project_fingerprint: resolved_fingerprint)
create(:vulnerabilities_finding,
:dismissed,
project: project,
report_type: 'sast',
project_fingerprint: dismissed_fingerprint)
end
context 'when matching vulnerability records exist' do
before do
create(:vulnerabilities_finding,
:confirmed,
project: project,
report_type: 'sast',
project_fingerprint: confirmed_fingerprint)
create(:vulnerabilities_finding,
:resolved,
project: project,
report_type: 'sast',
project_fingerprint: resolved_fingerprint)
create(:vulnerabilities_finding,
:dismissed,
project: project,
report_type: 'sast',
project_fingerprint: dismissed_fingerprint)
end
let(:confirmed_fingerprint) do
Digest::SHA1.hexdigest(
'python/hardcoded/hardcoded-tmp.py:52865813c884a507be1f152d654245af34aba8a391626d01f1ab6d3f52ec8779:B108')
end
let(:confirmed_fingerprint) do
Digest::SHA1.hexdigest(
'python/hardcoded/hardcoded-tmp.py:52865813c884a507be1f152d654245af34aba8a391626d01f1ab6d3f52ec8779:B108')
end
let(:resolved_fingerprint) do
Digest::SHA1.hexdigest(
'groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:47:PREDICTABLE_RANDOM')
end
let(:resolved_fingerprint) do
Digest::SHA1.hexdigest(
'groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:47:PREDICTABLE_RANDOM')
end
let(:dismissed_fingerprint) do
Digest::SHA1.hexdigest(
'groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:41:PREDICTABLE_RANDOM')
end
let(:dismissed_fingerprint) do
Digest::SHA1.hexdigest(
'groovy/src/main/java/com/gitlab/security_products/tests/App.groovy:41:PREDICTABLE_RANDOM')
end
subject { described_class.new(pipeline: pipeline, params: { report_type: %w[sast], scope: 'all' }).execute }
subject { described_class.new(pipeline: pipeline, params: { report_type: %w[sast], scope: 'all' }).execute }
it 'assigns vulnerability records to findings providing them with computed state' do
confirmed = subject.find { |f| f.project_fingerprint == confirmed_fingerprint }
resolved = subject.find { |f| f.project_fingerprint == resolved_fingerprint }
dismissed = subject.find { |f| f.project_fingerprint == dismissed_fingerprint }
it 'assigns vulnerability records to findings providing them with computed state' do
confirmed = subject.find { |f| f.project_fingerprint == confirmed_fingerprint }
resolved = subject.find { |f| f.project_fingerprint == resolved_fingerprint }
dismissed = subject.find { |f| f.project_fingerprint == dismissed_fingerprint }
expect(confirmed.state).to eq 'confirmed'
expect(resolved.state).to eq 'resolved'
expect(dismissed.state).to eq 'dismissed'
expect(subject - [confirmed, resolved, dismissed]).to all have_attributes(state: 'new')
expect(confirmed.state).to eq 'confirmed'
expect(resolved.state).to eq 'resolved'
expect(dismissed.state).to eq 'dismissed'
expect(subject - [confirmed, resolved, dismissed]).to all have_attributes(state: 'new')
end
end
end
context 'when being tested for sort stability' do
let(:params) { { report_type: %w[sast] } }
context 'when being tested for sort stability' do
let(:params) { { report_type: %w[sast] } }
it 'maintains the order of the occurrences having the same severity and confidence' do
select_proc = proc { |o| o.severity == 'medium' && o.confidence == 'high' }
report_occurrences = pipeline.security_reports.reports['sast'].occurrences.select(&select_proc)
it 'maintains the order of the occurrences having the same severity and confidence' do
select_proc = proc { |o| o.severity == 'medium' && o.confidence == 'high' }
report_occurrences = pipeline.security_reports.reports['sast'].occurrences.select(&select_proc)
found_occurrences = subject.select(&select_proc)
found_occurrences = subject.select(&select_proc)
found_occurrences.each_with_index do |found, i|
expect(found.metadata['cve']).to eq(report_occurrences[i].compare_key)
found_occurrences.each_with_index do |found, i|
expect(found.metadata['cve']).to eq(report_occurrences[i].compare_key)
end
end
end
end
def read_fixture(fixture)
JSON.parse(File.read(fixture.file.path))
def read_fixture(fixture)
JSON.parse(File.read(fixture.file.path))
end
end
end
context 'container_scanning' do
let_it_be(:artifact_cs) { create(:ee_ci_job_artifact, :container_scanning, job: build_cs, project: project) }
let(:cs_count) { read_fixture(artifact_cs)['vulnerabilities'].count }
it_behaves_like 'a pipeline vulnerabilities finder'
end
context 'deprecated container_scanning' do
let_it_be(:artifact_cs) { create(:ee_ci_job_artifact, :deprecated_container_scanning_report, job: build_cs, project: project) }
let(:cs_count) { read_fixture(artifact_cs)['unapproved'].count }
it_behaves_like 'a pipeline vulnerabilities finder'
end
end
{
"image": "registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff",
"unapproved": [
"CVE-2017-18269",
"CVE-2017-16997",
"CVE-2018-1000001",
"CVE-2016-10228",
"CVE-2018-18520",
"CVE-2010-4052",
"CVE-2018-16869",
"CVE-2018-18311"
],
"vulnerabilities": [
{
"featurename": "glibc",
"featureversion": "2.24-11+deb9u3",
"vulnerability": "CVE-2017-18269",
"namespace": "debian:9",
"description": "SSE2-optimized memmove implementation problem.",
"link": "https://security-tracker.debian.org/tracker/CVE-2017-18269",
"severity": "Defcon1",
"fixedby": "2.24-11+deb9u4"
},
{
"featurename": "glibc",
"featureversion": "2.24-11+deb9u3",
"vulnerability": "CVE-2017-16997",
"namespace": "debian:9",
"description": "elf/dl-load.c in the GNU C Library (aka glibc or libc6) 2.19 through 2.26 mishandles RPATH and RUNPATH containing $ORIGIN for a privileged (setuid or AT_SECURE) program, which allows local users to gain privileges via a Trojan horse library in the current working directory, related to the fillin_rpath and decompose_rpath functions. This is associated with misinterpretion of an empty RPATH/RUNPATH token as the \"./\" directory. NOTE: this configuration of RPATH/RUNPATH for a privileged program is apparently very uncommon; most likely, no such program is shipped with any common Linux distribution.",
"link": "https://security-tracker.debian.org/tracker/CVE-2017-16997",
"severity": "Critical",
"fixedby": ""
},
{
"featurename": "glibc",
"featureversion": "2.24-11+deb9u3",
"vulnerability": "CVE-2018-1000001",
"namespace": "debian:9",
"description": "In glibc 2.26 and earlier there is confusion in the usage of getcwd() by realpath() which can be used to write before the destination buffer leading to a buffer underflow and potential code execution.",
"link": "https://security-tracker.debian.org/tracker/CVE-2018-1000001",
"severity": "High",
"fixedby": ""
},
{
"featurename": "glibc",
"featureversion": "2.24-11+deb9u3",
"vulnerability": "CVE-2016-10228",
"namespace": "debian:9",
"description": "The iconv program in the GNU C Library (aka glibc or libc6) 2.25 and earlier, when invoked with the -c option, enters an infinite loop when processing invalid multi-byte input sequences, leading to a denial of service.",
"link": "https://security-tracker.debian.org/tracker/CVE-2016-10228",
"severity": "Medium",
"fixedby": ""
},
{
"featurename": "elfutils",
"featureversion": "0.168-1",
"vulnerability": "CVE-2018-18520",
"namespace": "debian:9",
"description": "An Invalid Memory Address Dereference exists in the function elf_end in libelf in elfutils through v0.174. Although eu-size is intended to support ar files inside ar files, handle_ar in size.c closes the outer ar file before handling all inner entries. The vulnerability allows attackers to cause a denial of service (application crash) with a crafted ELF file.",
"link": "https://security-tracker.debian.org/tracker/CVE-2018-18520",
"severity": "Low",
"fixedby": ""
},
{
"featurename": "glibc",
"featureversion": "2.24-11+deb9u3",
"vulnerability": "CVE-2010-4052",
"namespace": "debian:9",
"description": "Stack consumption vulnerability in the regcomp implementation in the GNU C Library (aka glibc or libc6) through 2.11.3, and 2.12.x through 2.12.2, allows context-dependent attackers to cause a denial of service (resource exhaustion) via a regular expression containing adjacent repetition operators, as demonstrated by a {10,}{10,}{10,}{10,} sequence in the proftpd.gnu.c exploit for ProFTPD.",
"link": "https://security-tracker.debian.org/tracker/CVE-2010-4052",
"severity": "Negligible",
"fixedby": ""
},
{
"featurename": "nettle",
"featureversion": "3.3-1",
"vulnerability": "CVE-2018-16869",
"namespace": "debian:9",
"description": "A Bleichenbacher type side-channel based padding oracle attack was found in the way nettle handles endian conversion of RSA decrypted PKCS#1 v1.5 data. An attacker who is able to run a process on the same physical core as the victim process, could use this flaw extract plaintext or in some cases downgrade any TLS connections to a vulnerable server.",
"link": "https://security-tracker.debian.org/tracker/CVE-2018-16869",
"severity": "Unknown",
"fixedby": ""
},
{
"featurename": "perl",
"featureversion": "5.24.1-3+deb9u4",
"vulnerability": "CVE-2018-18311",
"namespace": "debian:9",
"description": "Perl before 5.26.3 and 5.28.x before 5.28.1 has a buffer overflow via a crafted regular expression that triggers invalid write operations.",
"link": "https://security-tracker.debian.org/tracker/CVE-2018-18311",
"severity": "Unknown",
"fixedby": "5.24.1-3+deb9u5"
},
{
"featurename": "foo",
"featureversion": "1.3",
"vulnerability": "CVE-2018-666",
"namespace": "debian:9",
"description": "Foo has a vulnerability nobody cares about and whitelist.",
"link": "https://security-tracker.debian.org/tracker/CVE-2018-666",
"severity": "Unknown",
"fixedby": "1.4"
}
]
}
{
"image": "registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff",
"unapproved": [
"CVE-2017-18269",
"CVE-2017-16997",
"CVE-2018-1000001",
"CVE-2016-10228",
"CVE-2018-18520",
"CVE-2010-4052",
"CVE-2018-16869",
"CVE-2018-18311"
],
"version": "2.3",
"vulnerabilities": [
{
"featurename": "glibc",
"featureversion": "2.24-11+deb9u3",
"vulnerability": "CVE-2017-18269",
"namespace": "debian:9",
"description": "SSE2-optimized memmove implementation problem.",
"link": "https://security-tracker.debian.org/tracker/CVE-2017-18269",
"severity": "Defcon1",
"fixedby": "2.24-11+deb9u4"
"category": "container_scanning",
"message": "CVE-2017-18269 in glibc",
"description": "An SSE2-optimized memmove implementation for i386 in sysdeps/i386/i686/multiarch/memcpy-sse2-unaligned.S in the GNU C Library (aka glibc or libc6) 2.21 through 2.27 does not correctly perform the overlapping memory check if the source memory range spans the middle of the address space, resulting in corrupt data being produced by the copy operation. This may disclose information to context-dependent attackers, or result in a denial of service, or, possibly, code execution.",
"cve": "debian:9:glibc:CVE-2017-18269",
"severity": "Critical",
"confidence": "Unknown",
"solution": "Upgrade glibc from 2.24-11+deb9u3 to 2.24-11+deb9u4",
"scanner": {
"id": "klar",
"name": "klar"
},
"location": {
"dependency": {
"package": {
"name": "glibc"
},
"version": "2.24-11+deb9u3"
},
"operating_system": "debian:9",
"image": "registry.gitlab.com/gitlab-org/security-products/dast/webgoat-8.0@sha256:bc09fe2e0721dfaeee79364115aeedf2174cce0947b9ae5fe7c33312ee019a4e"
},
"identifiers": [
{
"type": "cve",
"name": "CVE-2017-18269",
"value": "CVE-2017-18269",
"url": "https://security-tracker.debian.org/tracker/CVE-2017-18269"
}
],
"links": [
{
"url": "https://security-tracker.debian.org/tracker/CVE-2017-18269"
}
]
},
{
"featurename": "glibc",
"featureversion": "2.24-11+deb9u3",
"vulnerability": "CVE-2017-16997",
"namespace": "debian:9",
"category": "container_scanning",
"message": "CVE-2017-16997 in glibc",
"description": "elf/dl-load.c in the GNU C Library (aka glibc or libc6) 2.19 through 2.26 mishandles RPATH and RUNPATH containing $ORIGIN for a privileged (setuid or AT_SECURE) program, which allows local users to gain privileges via a Trojan horse library in the current working directory, related to the fillin_rpath and decompose_rpath functions. This is associated with misinterpretion of an empty RPATH/RUNPATH token as the \"./\" directory. NOTE: this configuration of RPATH/RUNPATH for a privileged program is apparently very uncommon; most likely, no such program is shipped with any common Linux distribution.",
"link": "https://security-tracker.debian.org/tracker/CVE-2017-16997",
"cve": "debian:9:glibc:CVE-2017-16997",
"severity": "Critical",
"fixedby": ""
"confidence": "Unknown",
"solution": "Upgrade glibc from 2.24-11+deb9u3 to 2.24-11+deb9u4",
"scanner": {
"id": "klar",
"name": "klar"
},
"location": {
"dependency": {
"package": {
"name": "glibc"
},
"version": "2.24-11+deb9u3"
},
"operating_system": "debian:9",
"image": "registry.gitlab.com/gitlab-org/security-products/dast/webgoat-8.0@sha256:bc09fe2e0721dfaeee79364115aeedf2174cce0947b9ae5fe7c33312ee019a4e"
},
"identifiers": [
{
"type": "cve",
"name": "CVE-2017-16997",
"value": "CVE-2017-16997",
"url": "https://security-tracker.debian.org/tracker/CVE-2017-16997"
}
],
"links": [
{
"url": "https://security-tracker.debian.org/tracker/CVE-2017-16997"
}
]
},
{
"featurename": "glibc",
"featureversion": "2.24-11+deb9u3",
"vulnerability": "CVE-2018-1000001",
"namespace": "debian:9",
"category": "container_scanning",
"message": "CVE-2018-1000001 in glibc",
"description": "In glibc 2.26 and earlier there is confusion in the usage of getcwd() by realpath() which can be used to write before the destination buffer leading to a buffer underflow and potential code execution.",
"link": "https://security-tracker.debian.org/tracker/CVE-2018-1000001",
"cve": "debian:9:glibc:CVE-2018-1000001",
"severity": "High",
"fixedby": ""
"confidence": "Unknown",
"scanner": {
"id": "klar",
"name": "klar"
},
"location": {
"dependency": {
"package": {
"name": "glibc"
},
"version": "2.24-11+deb9u3"
},
"operating_system": "debian:9",
"image": "registry.gitlab.com/gitlab-org/security-products/dast/webgoat-8.0@sha256:bc09fe2e0721dfaeee79364115aeedf2174cce0947b9ae5fe7c33312ee019a4e"
},
"identifiers": [
{
"type": "cve",
"name": "CVE-2018-1000001",
"value": "CVE-2018-1000001",
"url": "https://security-tracker.debian.org/tracker/CVE-2018-1000001"
}
],
"links": [
{
"url": "https://security-tracker.debian.org/tracker/CVE-2018-1000001"
}
]
},
{
"featurename": "glibc",
"featureversion": "2.24-11+deb9u3",
"vulnerability": "CVE-2016-10228",
"namespace": "debian:9",
"category": "container_scanning",
"message": "CVE-2016-10228 in glibc",
"description": "The iconv program in the GNU C Library (aka glibc or libc6) 2.25 and earlier, when invoked with the -c option, enters an infinite loop when processing invalid multi-byte input sequences, leading to a denial of service.",
"link": "https://security-tracker.debian.org/tracker/CVE-2016-10228",
"cve": "debian:9:glibc:CVE-2016-10228",
"severity": "Medium",
"fixedby": ""
"confidence": "Unknown",
"scanner": {
"id": "klar",
"name": "klar"
},
"location": {
"dependency": {
"package": {
"name": "glibc"
},
"version": "2.24-11+deb9u3"
},
"operating_system": "debian:9",
"image": "registry.gitlab.com/gitlab-org/security-products/dast/webgoat-8.0@sha256:bc09fe2e0721dfaeee79364115aeedf2174cce0947b9ae5fe7c33312ee019a4e"
},
"identifiers": [
{
"type": "cve",
"name": "CVE-2016-10228",
"value": "CVE-2016-10228",
"url": "https://security-tracker.debian.org/tracker/CVE-2016-10228"
}
],
"links": [
{
"url": "https://security-tracker.debian.org/tracker/CVE-2016-10228"
}
]
},
{
"featurename": "elfutils",
"featureversion": "0.168-1",
"vulnerability": "CVE-2018-18520",
"namespace": "debian:9",
"category": "container_scanning",
"message": "CVE-2018-18520 in elfutils",
"description": "An Invalid Memory Address Dereference exists in the function elf_end in libelf in elfutils through v0.174. Although eu-size is intended to support ar files inside ar files, handle_ar in size.c closes the outer ar file before handling all inner entries. The vulnerability allows attackers to cause a denial of service (application crash) with a crafted ELF file.",
"link": "https://security-tracker.debian.org/tracker/CVE-2018-18520",
"cve": "debian:9:elfutils:CVE-2018-18520",
"severity": "Low",
"fixedby": ""
"confidence": "Unknown",
"scanner": {
"id": "klar",
"name": "klar"
},
"location": {
"dependency": {
"package": {
"name": "elfutils"
},
"version": "0.168-1"
},
"operating_system": "debian:9",
"image": "registry.gitlab.com/gitlab-org/security-products/dast/webgoat-8.0@sha256:bc09fe2e0721dfaeee79364115aeedf2174cce0947b9ae5fe7c33312ee019a4e"
},
"identifiers": [
{
"type": "cve",
"name": "CVE-2018-18520",
"value": "CVE-2018-18520",
"url": "https://security-tracker.debian.org/tracker/CVE-2018-18520"
}
],
"links": [
{
"url": "https://security-tracker.debian.org/tracker/CVE-2018-18520"
}
]
},
{
"featurename": "glibc",
"featureversion": "2.24-11+deb9u3",
"vulnerability": "CVE-2010-4052",
"namespace": "debian:9",
"category": "container_scanning",
"message": "CVE-2010-4052 in glibc",
"description": "Stack consumption vulnerability in the regcomp implementation in the GNU C Library (aka glibc or libc6) through 2.11.3, and 2.12.x through 2.12.2, allows context-dependent attackers to cause a denial of service (resource exhaustion) via a regular expression containing adjacent repetition operators, as demonstrated by a {10,}{10,}{10,}{10,} sequence in the proftpd.gnu.c exploit for ProFTPD.",
"link": "https://security-tracker.debian.org/tracker/CVE-2010-4052",
"severity": "Negligible",
"fixedby": ""
"cve": "debian:9:glibc:CVE-2010-4052",
"severity": "Low",
"confidence": "Unknown",
"scanner": {
"id": "klar",
"name": "klar"
},
"location": {
"dependency": {
"package": {
"name": "glibc"
},
"version": "2.24-11+deb9u3"
},
"operating_system": "debian:9",
"image": "registry.gitlab.com/gitlab-org/security-products/dast/webgoat-8.0@sha256:bc09fe2e0721dfaeee79364115aeedf2174cce0947b9ae5fe7c33312ee019a4e"
},
"identifiers": [
{
"type": "cve",
"name": "CVE-2010-4052",
"value": "CVE-2010-4052",
"url": "https://security-tracker.debian.org/tracker/CVE-2010-4052"
}
],
"links": [
{
"url": "https://security-tracker.debian.org/tracker/CVE-2010-4052"
}
]
},
{
"featurename": "nettle",
"featureversion": "3.3-1",
"vulnerability": "CVE-2018-16869",
"namespace": "debian:9",
"category": "container_scanning",
"message": "CVE-2018-16869 in nettle",
"description": "A Bleichenbacher type side-channel based padding oracle attack was found in the way nettle handles endian conversion of RSA decrypted PKCS#1 v1.5 data. An attacker who is able to run a process on the same physical core as the victim process, could use this flaw extract plaintext or in some cases downgrade any TLS connections to a vulnerable server.",
"link": "https://security-tracker.debian.org/tracker/CVE-2018-16869",
"cve": "debian:9:nettle:CVE-2018-16869",
"severity": "Unknown",
"fixedby": ""
"confidence": "Unknown",
"scanner": {
"id": "klar",
"name": "klar"
},
"location": {
"dependency": {
"package": {
"name": "nettle"
},
"version": "3.3-1"
},
"operating_system": "debian:9",
"image": "registry.gitlab.com/gitlab-org/security-products/dast/webgoat-8.0@sha256:bc09fe2e0721dfaeee79364115aeedf2174cce0947b9ae5fe7c33312ee019a4e"
},
"identifiers": [
{
"type": "cve",
"name": "CVE-2018-16869",
"value": "CVE-2018-16869",
"url": "https://security-tracker.debian.org/tracker/CVE-2018-16869"
}
],
"links": [
{
"url": "https://security-tracker.debian.org/tracker/CVE-2018-16869"
}
]
},
{
"featurename": "perl",
"featureversion": "5.24.1-3+deb9u4",
"vulnerability": "CVE-2018-18311",
"namespace": "debian:9",
"category": "container_scanning",
"message": "CVE-2018-18311 in perl",
"description": "Perl before 5.26.3 and 5.28.x before 5.28.1 has a buffer overflow via a crafted regular expression that triggers invalid write operations.",
"link": "https://security-tracker.debian.org/tracker/CVE-2018-18311",
"cve": "debian:9:perl:CVE-2018-18311",
"severity": "Unknown",
"fixedby": "5.24.1-3+deb9u5"
},
{
"featurename": "foo",
"featureversion": "1.3",
"vulnerability": "CVE-2018-666",
"namespace": "debian:9",
"description": "Foo has a vulnerability nobody cares about and whitelist.",
"link": "https://security-tracker.debian.org/tracker/CVE-2018-666",
"severity": "Unknown",
"fixedby": "1.4"
"confidence": "Unknown",
"solution": "Upgrade perl from 5.24.1-3+deb9u3 to 5.24.1-3+deb9u5",
"scanner": {
"id": "klar",
"name": "klar"
},
"location": {
"dependency": {
"package": {
"name": "perl"
},
"version": "5.24.1-3+deb9u3"
},
"operating_system": "debian:9",
"image": "registry.gitlab.com/gitlab-org/security-products/dast/webgoat-8.0@sha256:bc09fe2e0721dfaeee79364115aeedf2174cce0947b9ae5fe7c33312ee019a4e"
},
"identifiers": [
{
"type": "cve",
"name": "CVE-2018-18311",
"value": "CVE-2018-18311",
"url": "https://security-tracker.debian.org/tracker/CVE-2018-18311"
}
],
"links": [
{
"url": "https://security-tracker.debian.org/tracker/CVE-2018-18311"
}
]
}
]
],
"remediations": []
}
......@@ -4,52 +4,52 @@ require 'spec_helper'
describe Gitlab::Ci::Parsers::Security::ContainerScanning do
let(:parser) { described_class.new }
let(:project) { artifact.project }
let(:pipeline) { artifact.job.pipeline }
let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline.sha) }
let(:clair_vulnerabilities) do
JSON.parse!(
File.read(
Rails.root.join('ee/spec/fixtures/security_reports/master/gl-container-scanning-report.json')
)
)['vulnerabilities']
before do
artifact.each_blob do |blob|
parser.parse!(blob, report)
end
end
describe '#parse!' do
let(:project) { artifact.project }
let(:pipeline) { artifact.job.pipeline }
let(:artifact) { create(:ee_ci_job_artifact, :container_scanning) }
let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline.sha) }
before do
artifact.each_blob do |blob|
parser.parse!(blob, report)
end
end
using RSpec::Parameterized::TableSyntax
it "parses all identifiers and occurrences for unapproved vulnerabilities" do
expect(report.occurrences.length).to eq(8)
expect(report.identifiers.length).to eq(8)
expect(report.scanners.length).to eq(1)
where(:report_type, :image, :version) do
:deprecated_container_scanning_report | 'registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff' | '1.3'
:container_scanning | 'registry.gitlab.com/gitlab-org/security-products/dast/webgoat-8.0@sha256:bc09fe2e0721dfaeee79364115aeedf2174cce0947b9ae5fe7c33312ee019a4e' | '2.3'
end
it 'generates expected location' do
location = report.occurrences.first.location
with_them do
let(:artifact) { create(:ee_ci_job_artifact, report_type) }
expect(location).to be_a(::Gitlab::Ci::Reports::Security::Locations::ContainerScanning)
expect(location).to have_attributes(
image: 'registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff',
operating_system: 'debian:9',
package_name: 'glibc',
package_version: '2.24-11+deb9u3'
)
end
it "parses all identifiers and occurrences for unapproved vulnerabilities" do
expect(report.occurrences.length).to eq(8)
expect(report.identifiers.length).to eq(8)
expect(report.scanners.length).to eq(1)
end
it "generates expected metadata_version" do
expect(report.occurrences.first.metadata_version).to eq('1.3')
end
it 'generates expected location' do
location = report.occurrences.first.location
it "adds report image's name to raw_metadata" do
expect(JSON.parse(report.occurrences.first.raw_metadata).dig('location', 'image'))
.to eq('registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff')
expect(location).to be_a(::Gitlab::Ci::Reports::Security::Locations::ContainerScanning)
expect(location).to have_attributes(
image: image,
operating_system: 'debian:9',
package_name: 'glibc',
package_version: '2.24-11+deb9u3'
)
end
it "generates expected metadata_version" do
expect(report.occurrences.first.metadata_version).to eq(version)
end
it "adds report image's name to raw_metadata" do
expect(JSON.parse(report.occurrences.first.raw_metadata).dig('location', 'image')).to eq(image)
end
end
end
end
......@@ -2,19 +2,19 @@
require 'spec_helper'
describe Gitlab::Ci::Parsers::Security::Formatters::ContainerScanning do
let(:raw_report) do
JSON.parse!(
File.read(
Rails.root.join('ee/spec/fixtures/security_reports/master/gl-container-scanning-report.json')
)
)
end
describe Gitlab::Ci::Parsers::Security::Formatters::DeprecatedContainerScanning do
let(:vulnerability) { raw_report['vulnerabilities'].first }
describe '#format' do
it 'format ZAP vulnerability into the 1.3 format' do
let(:raw_report) do
JSON.parse!(
File.read(
Rails.root.join('ee/spec/fixtures/security_reports/deprecated/gl-container-scanning-report.json')
)
)
end
it 'formats the vulnerability into the 1.3 format' do
formatter = described_class.new('image_name')
expect(formatter.format(vulnerability)).to eq( {
......
......@@ -2,11 +2,11 @@
require 'spec_helper'
describe Gitlab::Ci::Parsers::Security::Formatters::FormattedContainerScanningVulnerability do
describe Gitlab::Ci::Parsers::Security::Formatters::DeprecatedFormattedContainerScanningVulnerability do
let(:raw_report) do
JSON.parse!(
File.read(
Rails.root.join('ee/spec/fixtures/security_reports/master/gl-container-scanning-report.json')
Rails.root.join('ee/spec/fixtures/security_reports/deprecated/gl-container-scanning-report.json')
)
)
end
......
......@@ -18,7 +18,7 @@ describe Ci::CompareContainerScanningReportsService do
let!(:base_pipeline) { create(:ee_ci_pipeline) }
let!(:head_pipeline) { create(:ee_ci_pipeline, :with_container_scanning_report, project: project) }
it 'reports new licenses' do
it 'reports new, existing and fixed vulnerabilities' do
expect(subject[:status]).to eq(:parsed)
expect(subject[:data]['added'].count).to eq(8)
expect(subject[:data]['existing'].count).to eq(0)
......
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