Commit 7546d591 authored by Thong Kuah's avatar Thong Kuah

Merge branch 'extract-vulnerability-formatters' into 'master'

Extract formatter for container scanning report vulnerabilities

See merge request gitlab-org/gitlab-ee!14879
parents 39f71ce5 ee036ce0
......@@ -27,108 +27,14 @@ module Gitlab
def format_report(data)
vulnerabilities = data['vulnerabilities']
unapproved = data['unapproved']
results = []
formatter = Formatters::ContainerScanning.new(data['image'])
vulnerabilities.each do |vulnerability|
vulnerabilities.map do |vulnerability|
# We only report unapproved vulnerabilities
next unless unapproved.include?(vulnerability['vulnerability'])
results.append(format_vulnerability(vulnerability, data['image']))
end
results
end
def format_vulnerability(vulnerability, image)
{
'category' => 'container_scanning',
'message' => message(vulnerability),
'description' => description(vulnerability),
'cve' => vulnerability['vulnerability'],
'severity' => translate_severity(vulnerability['severity']),
'solution' => solution(vulnerability),
'confidence' => 'Medium',
'location' => {
'image' => image,
'operating_system' => vulnerability["namespace"],
'dependency' => {
'package' => {
'name' => vulnerability["featurename"]
},
'version' => vulnerability["featureversion"]
}
},
'scanner' => { 'id' => 'clair', 'name' => 'Clair' },
'identifiers' => [
{
'type' => 'cve',
'name' => vulnerability['vulnerability'],
'value' => vulnerability['vulnerability'],
'url' => vulnerability['link']
}
],
'links' => [{ 'url' => vulnerability['link'] }]
}
end
def translate_severity(severity)
case severity
when 'Negligible'
'low'
when 'Unknown', 'Low', 'Medium', 'High', 'Critical'
severity.downcase
when 'Defcon1'
'critical'
else
safe_severity = ERB::Util.html_escape(severity)
raise SecurityReportParserError, "Unknown severity in container scanning report: #{safe_severity}"
end
end
def message(vulnerability)
format(
vulnerability,
%w[vulnerability featurename] =>
'%{vulnerability} in %{featurename}',
'vulnerability' =>
'%{vulnerability}'
)
end
def description(vulnerability)
format(
vulnerability,
'description' =>
'%{description}',
%w[featurename featureversion] =>
'%{featurename}:%{featureversion} is affected by %{vulnerability}',
'featurename' =>
'%{featurename} is affected by %{vulnerability}',
'namespace' =>
'%{namespace} is affected by %{vulnerability}'
)
end
def solution(vulnerability)
format(
vulnerability,
%w[fixedby featurename featureversion] =>
'Upgrade %{featurename} from %{featureversion} to %{fixedby}',
%w[fixedby featurename] =>
'Upgrade %{featurename} to %{fixedby}',
'fixedby' =>
'Upgrade to %{fixedby}'
)
end
def format(vulnerability, definitions)
definitions.each do |keys, value|
if vulnerability.values_at(*Array(keys)).all?(&:present?)
return value % vulnerability.symbolize_keys
end
end
nil
formatter.format(vulnerability)
end.compact
end
def create_location(location_data)
......
# frozen_string_literal: true
module Gitlab
module Ci
module Parsers
module Security
module Formatters
class ContainerScanning
def initialize(image)
@image = image
end
def format(vulnerability)
formatted_vulnerability = FormattedContainerScanningVulnerability.new(vulnerability)
{
'category' => 'container_scanning',
'message' => formatted_vulnerability.message,
'description' => formatted_vulnerability.description,
'cve' => formatted_vulnerability.cve,
'severity' => formatted_vulnerability.severity,
'solution' => formatted_vulnerability.solution,
'confidence' => 'Medium',
'location' => {
'image' => image,
'operating_system' => formatted_vulnerability.operating_system,
'dependency' => {
'package' => {
'name' => formatted_vulnerability.package_name
},
'version' => formatted_vulnerability.version
}
},
'scanner' => { 'id' => 'clair', 'name' => 'Clair' },
'identifiers' => [
{
'type' => 'cve',
'name' => formatted_vulnerability.cve,
'value' => formatted_vulnerability.cve,
'url' => formatted_vulnerability.url
}
],
'links' => [{ 'url' => formatted_vulnerability.url }]
}
end
private
attr_reader :image
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Parsers
module Security
module Formatters
class FormattedContainerScanningVulnerability
def initialize(vulnerability)
@vulnerability = vulnerability
end
def message
@message ||= format_definitions(
%w[vulnerability featurename] => '%{vulnerability} in %{featurename}',
'vulnerability' => '%{vulnerability}'
)
end
def description
@description ||= format_definitions(
'description' => '%{description}',
%w[featurename featureversion] => '%{featurename}:%{featureversion} is affected by %{vulnerability}',
'featurename' => '%{featurename} is affected by %{vulnerability}',
'namespace' => '%{namespace} is affected by %{vulnerability}'
)
end
def severity
raw_severity = vulnerability['severity']
@severity ||= case raw_severity
when 'Negligible'
'low'
when 'Unknown', 'Low', 'Medium', 'High', 'Critical'
raw_severity.downcase
when 'Defcon1'
'critical'
else
safe_severity = ERB::Util.html_escape(raw_severity)
raise(
::Gitlab::Ci::Parsers::Security::Common::SecurityReportParserError,
"Unknown severity in container scanning report: #{safe_severity}"
)
end
end
def solution
@solution ||= format_definitions(
%w[fixedby featurename featureversion] => 'Upgrade %{featurename} from %{featureversion} to %{fixedby}',
%w[fixedby featurename] => 'Upgrade %{featurename} to %{fixedby}',
'fixedby' => 'Upgrade to %{fixedby}'
)
end
def cve
@cve ||= vulnerability['vulnerability']
end
def operating_system
@operating_system ||= vulnerability['namespace']
end
def package_name
@package_name ||= vulnerability['featurename']
end
def version
@version ||= vulnerability['featureversion']
end
def url
@url ||= vulnerability['link']
end
private
attr_reader :vulnerability
def format_definitions(definitions)
find_definitions(definitions).then do |_, value|
if value.present?
value % vulnerability.symbolize_keys
end
end
end
def find_definitions(definitions)
definitions.find do |keys, value|
vulnerability.values_at(*keys).all?(&:present?)
end
end
end
end
end
end
end
end
......@@ -52,212 +52,4 @@ describe Gitlab::Ci::Parsers::Security::ContainerScanning do
.to eq('registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff')
end
end
describe '#format_vulnerability' do
it 'format ZAP vulnerability into the 1.3 format' do
expect(parser.send(:format_vulnerability, clair_vulnerabilities[0], 'image_name')).to eq( {
'category' => 'container_scanning',
'message' => 'CVE-2017-18269 in glibc',
'confidence' => 'Medium',
'cve' => 'CVE-2017-18269',
'identifiers' => [
{
'type' => 'cve',
'name' => 'CVE-2017-18269',
'value' => 'CVE-2017-18269',
'url' => 'https://security-tracker.debian.org/tracker/CVE-2017-18269'
}
],
'location' => {
'image' => 'image_name',
'operating_system' => 'debian:9',
'dependency' => {
'package' => {
'name' => 'glibc'
},
'version' => '2.24-11+deb9u3'
}
},
'links' => [{ 'url' => 'https://security-tracker.debian.org/tracker/CVE-2017-18269' }],
'description' => 'SSE2-optimized memmove implementation problem.',
'scanner' => { 'id' => 'clair', 'name' => 'Clair' },
'severity' => 'critical',
'solution' => 'Upgrade glibc from 2.24-11+deb9u3 to 2.24-11+deb9u4'
} )
end
end
describe '#translate_severity' do
context 'with recognised values' do
using RSpec::Parameterized::TableSyntax
where(:severity, :expected) do
'Unknown' | 'unknown'
'Negligible' | 'low'
'Low' | 'low'
'Medium' | 'medium'
'High' | 'high'
'Critical' | 'critical'
'Defcon1' | 'critical'
end
with_them do
it "translate severity from Clair" do
expect(parser.send(:translate_severity, severity)).to eq(expected)
end
end
end
context 'with a wrong value' do
it 'throws an exception' do
expect { parser.send(:translate_severity, 'abcd<efg>') }.to raise_error(
::Gitlab::Ci::Parsers::Security::Common::SecurityReportParserError,
'Unknown severity in container scanning report: abcd&lt;efg&gt;'
)
end
end
end
describe '#message' do
let(:input) do
{
'featurename' => 'foo',
'featureversion' => '',
'vulnerability' => 'CVE-2018-777',
'namespace' => 'debian:9',
'description' => 'CVE-2018-777 is affecting your system',
'link' => 'https://security-tracker.debian.org/tracker/CVE-2018-777',
'severity' => 'Unknown',
'fixedby' => '1.4'
}
end
subject { parser.send(:message, input)}
context 'when there is a featurename' do
it 'formats message using the featurename' do
is_expected.to eq('CVE-2018-777 in foo')
end
end
context 'when there is no featurename' do
before do
input['featurename'] = ''
end
it 'formats message using the vulnerability only' do
is_expected.to eq('CVE-2018-777')
end
end
end
describe '#description' do
let(:input) do
{
'featurename' => 'foo',
'featureversion' => '1.2.3',
'vulnerability' => 'CVE-2018-777',
'namespace' => 'debian:9',
'description' => 'SSE2-optimized memmove implementation problem.',
'link' => 'https://security-tracker.debian.org/tracker/CVE-2018-777',
'severity' => 'Unknown',
'fixedby' => '1.4'
}
end
subject { parser.send(:description, input) }
context 'when there is a description' do
it 'returns the provided description' do
is_expected.to eq('SSE2-optimized memmove implementation problem.')
end
end
context 'when there is no description' do
before do
input['description'] = ''
end
context 'when there is no featurename' do
before do
input['featurename'] = ''
end
it 'formats description using the namespace' do
is_expected.to eq('debian:9 is affected by CVE-2018-777')
end
end
context 'when there is no featureversion' do
before do
input['featureversion'] = ''
end
it 'formats description using the featurename only' do
is_expected.to eq('foo is affected by CVE-2018-777')
end
end
context 'when featurename and featureversion are present' do
it 'formats description using featurename and featureversion' do
is_expected.to eq('foo:1.2.3 is affected by CVE-2018-777')
end
end
end
end
describe '#solution' do
let(:input) do
{
'featurename' => 'foo',
'featureversion' => '1.2.3',
'vulnerability' => 'CVE-2018-777',
'namespace' => 'debian:9',
'description' => 'SSE2-optimized memmove implementation problem.',
'link' => 'https://security-tracker.debian.org/tracker/CVE-2018-777',
'severity' => 'Unknown',
'fixedby' => '1.4'
}
end
subject { parser.send(:solution, input) }
context 'when there is no fixedby value' do
before do
input['fixedby'] = ''
end
it 'returns nil' do
is_expected.to be_nil
end
end
context 'when there is a fixedby' do
context 'when there is no featurename' do
before do
input['featurename'] = ''
end
it 'formats solution using the fixedby only' do
is_expected.to eq('Upgrade to 1.4')
end
end
context 'when there is no featureversion' do
before do
input['featureversion'] = ''
end
it 'formats solution using the featurename only' do
is_expected.to eq('Upgrade foo to 1.4')
end
end
context 'when featurename and featureversion are present' do
it 'formats solution using featurename and featureversion' do
is_expected.to eq('Upgrade foo from 1.2.3 to 1.4')
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Parsers::Security::Formatters::ContainerScanning do
let(:raw_report) do
JSON.parse!(
File.read(
Rails.root.join('spec/fixtures/security-reports/master/gl-container-scanning-report.json')
)
)
end
let(:vulnerability) { raw_report['vulnerabilities'].first }
describe '#format' do
it 'format ZAP vulnerability into the 1.3 format' do
formatter = described_class.new('image_name')
expect(formatter.format(vulnerability)).to eq( {
'category' => 'container_scanning',
'message' => 'CVE-2017-18269 in glibc',
'confidence' => 'Medium',
'cve' => 'CVE-2017-18269',
'identifiers' => [
{
'type' => 'cve',
'name' => 'CVE-2017-18269',
'value' => 'CVE-2017-18269',
'url' => 'https://security-tracker.debian.org/tracker/CVE-2017-18269'
}
],
'location' => {
'image' => 'image_name',
'operating_system' => 'debian:9',
'dependency' => {
'package' => {
'name' => 'glibc'
},
'version' => '2.24-11+deb9u3'
}
},
'links' => [{ 'url' => 'https://security-tracker.debian.org/tracker/CVE-2017-18269' }],
'description' => 'SSE2-optimized memmove implementation problem.',
'scanner' => { 'id' => 'clair', 'name' => 'Clair' },
'severity' => 'critical',
'solution' => 'Upgrade glibc from 2.24-11+deb9u3 to 2.24-11+deb9u4'
} )
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Parsers::Security::Formatters::FormattedContainerScanningVulnerability do
let(:raw_report) do
JSON.parse!(
File.read(
Rails.root.join('spec/fixtures/security-reports/master/gl-container-scanning-report.json')
)
)
end
let(:vulnerability) { raw_report['vulnerabilities'].first }
let(:data_with_all_keys) do
raw_report['vulnerabilities'].first.merge(
'description' => 'Better hurry and fix that.',
'featurename' => 'hexes',
'featureversion' => '6.6.6',
'fixedby' => '6.6.7',
'link' => 'https://theintercept.com',
'namespace' => 'malevolences',
'vulnerability' => 'Level 9000 Soul Breach'
)
end
subject { described_class.new(data_with_all_keys) }
describe '#message' do
it 'creates a message from the vulnerability and featurename' do
expect(subject.message).to eq('Level 9000 Soul Breach in hexes')
end
context 'when there is no featurename' do
it 'uses vulnerability for the message' do
data_without_featurename = data_with_all_keys.deep_dup.merge('featurename' => '')
formatted_vulnerability = described_class.new(data_without_featurename)
expect(formatted_vulnerability.message).to eq('Level 9000 Soul Breach')
end
end
end
describe '#description' do
it 'uses the given description' do
expect(subject.description).to eq('Better hurry and fix that.')
end
context 'when there is no description' do
let(:data_without_description) { data_with_all_keys.deep_dup.merge('description' => '') }
it 'creates a description from the featurename and featureversion' do
formatted_vulnerability = described_class.new(data_without_description)
expect(formatted_vulnerability.description).to eq('hexes:6.6.6 is affected by Level 9000 Soul Breach')
end
context 'when there is no featureversion' do
it 'creates a description from the featurename' do
data_without_featureversion = data_without_description.deep_dup.merge('featureversion' => '')
formatted_vulnerability = described_class.new(data_without_featureversion)
expect(formatted_vulnerability.description).to eq('hexes is affected by Level 9000 Soul Breach')
end
end
context 'when there is no featurename and no featureversion' do
it 'creates a description from the namespace' do
data_only_namespace = data_without_description.deep_dup.merge(
'featurename' => '',
'featureversion' => ''
)
formatted_vulnerability = described_class.new(data_only_namespace)
expect(formatted_vulnerability.description).to eq('malevolences is affected by Level 9000 Soul Breach')
end
end
end
end
describe '#severity' do
using RSpec::Parameterized::TableSyntax
where(:report_severity_category, :gitlab_severity_category) do
'Unknown' | 'unknown'
'Negligible' | 'low'
'Low' | 'low'
'Medium' | 'medium'
'High' | 'high'
'Critical' | 'critical'
'Defcon1' | 'critical'
end
with_them do
it 'translates the severity into our categorization' do
data_with_severity = data_with_all_keys.deep_dup.merge('severity' => report_severity_category)
formatted_vulnerability = described_class.new(data_with_severity)
expect(formatted_vulnerability.severity).to eq(gitlab_severity_category)
end
end
context 'when the given severity is not valid' do
it 'throws a parser error' do
data_with_invalid_severity = vulnerability.deep_dup.merge(
'severity' => 'cats, curses, and <coffee>'
)
formatted_vulnerability = described_class.new(data_with_invalid_severity)
expect { formatted_vulnerability.severity }.to raise_error(
::Gitlab::Ci::Parsers::Security::Common::SecurityReportParserError,
'Unknown severity in container scanning report: cats, curses, and &lt;coffee&gt;'
)
end
end
end
describe '#solution' do
it 'creates a solution from the featurename, featureversion, and fixedby' do
expect(subject.solution).to eq('Upgrade hexes from 6.6.6 to 6.6.7')
end
context 'when there is no featurename' do
it 'formats the solution using fixedby' do
data_without_featurename = data_with_all_keys.deep_dup.merge('featurename' => '')
formatted_vulnerability = described_class.new(data_without_featurename)
expect(formatted_vulnerability.solution).to eq('Upgrade to 6.6.7')
end
end
context 'when there is no featureversion' do
it 'formats a solution using featurename' do
data_without_featureversion = data_with_all_keys.deep_dup.merge('featureversion' => '')
formatted_vulnerability = described_class.new(data_without_featureversion)
expect(formatted_vulnerability.solution).to eq('Upgrade hexes to 6.6.7')
end
end
context 'when there is no fixedby' do
it 'does not include a solution' do
data_without_fixedby = vulnerability.deep_dup.merge('fixedby' => '')
formatted_vulnerability = described_class.new(data_without_fixedby)
expect(formatted_vulnerability.solution).to be_nil
end
end
end
describe '#cve' do
it 'reads the CVE from the vulnerability' do
expect(subject.cve).to eq('Level 9000 Soul Breach')
end
end
describe '#operating_system' do
it 'reads the operating system from the namespace' do
expect(subject.operating_system).to eq('malevolences')
end
end
describe '#package_name' do
it 'reads the package name from the featurename' do
expect(subject.package_name).to eq('hexes')
end
end
describe '#version' do
it 'reads the version from featureversion' do
expect(subject.version).to eq('6.6.6')
end
end
describe '#url' do
it 'reads the url from the link in the report' do
expect(subject.url).to eq('https://theintercept.com')
end
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