Skip to content
Snippets Groups Projects
Verified Commit 3db232ca authored by Dominic Bauer's avatar Dominic Bauer :palm_tree: Committed by GitLab
Browse files

Read security report schemas from RubyGem

Changelog: changed
parent 8448408d
No related branches found
No related tags found
2 merge requests!164749Enable parallel in test-on-omnibus,!158598Read security report schemas from RubyGem
......@@ -427,6 +427,9 @@ gem 'prometheus-client-mmap', '~> 1.1', '>= 1.1.1', require: 'prometheus/client'
# Required manually in config/initializers/require_async_gem
gem 'async', '~> 2.12.1', require: false # rubocop:disable Gemfile/MissingFeatureCategory -- This is general utility gem
# Security report schemas used to validate CI job artifacts of security jobs
gem 'gitlab-security_report_schemas', '0.1.2.min15.0.0.max15.2.0', feature_category: :vulnerability_management
# OpenTelemetry
group :opentelemetry do
# Core OpenTelemetry gems
......
......@@ -224,6 +224,7 @@
{"name":"gitlab-markup","version":"1.9.0","platform":"ruby","checksum":"7eda045a08ec2d110084252fa13a8c9eac8bdac0e302035ca7db4b82bcbd7ed4"},
{"name":"gitlab-net-dns","version":"0.9.2","platform":"ruby","checksum":"f726d978479d43810819f12a45c0906d775a07e34df111bbe693fffbbef3059d"},
{"name":"gitlab-sdk","version":"0.3.1","platform":"ruby","checksum":"48ba49084f4ab92df7c7ef9f347020d9dfdf6ed9c1e782b67264e98ffe6ea710"},
{"name":"gitlab-security_report_schemas","version":"0.1.2.min15.0.0.max15.2.0","platform":"ruby","checksum":"c40afe378b52539f610f67394dafe47376bd2f0588d217ff6c25d12b52d2a663"},
{"name":"gitlab-styles","version":"12.0.1","platform":"ruby","checksum":"d8a302b0ab0e1f18e2d11501760f1b85c5e70b5e5ca628828a0786c7984ed133"},
{"name":"gitlab_chronic_duration","version":"0.12.0","platform":"ruby","checksum":"0d766944d415b5c831f176871ee8625783fc0c5bfbef2d79a3a616f207ffc16d"},
{"name":"gitlab_omniauth-ldap","version":"2.2.0","platform":"ruby","checksum":"bb4d20acb3b123ed654a8f6a47d3fac673ece7ed0b6992edb92dca14bad2838c"},
......
......@@ -747,6 +747,9 @@ GEM
activesupport (>= 5.2.0)
rake (~> 13.0)
snowplow-tracker (~> 0.8.0)
gitlab-security_report_schemas (0.1.2.min15.0.0.max15.2.0)
activesupport (>= 6, < 8)
json_schemer (~> 2.3.0)
gitlab-styles (12.0.1)
rubocop (~> 1.62.1)
rubocop-factory_bot (~> 2.25.1)
......@@ -2068,6 +2071,7 @@ DEPENDENCIES
gitlab-schema-validation!
gitlab-sdk (~> 0.3.0)
gitlab-secret_detection!
gitlab-security_report_schemas (= 0.1.2.min15.0.0.max15.2.0)
gitlab-sidekiq-fetcher!
gitlab-styles (~> 12.0.1)
gitlab-topology-service-client (~> 0.1)!
......
---
name: security_report_schemas_rubygem
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/383516
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/158598
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/470692
milestone: '17.3'
group: group::threat insights
type: gitlab_com_derisk
default_enabled: false
......@@ -5,254 +5,274 @@
RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, feature_category: :vulnerability_management do
let_it_be(:project) { create(:project) }
context 'with stubbed supported versions' do
let(:supported_schema_versions) { %w[15.0.0] }
let(:validator) { described_class.new(report_type, report_data, report_data['version'], project: project) }
let(:supported_hash) do
{
cluster_image_scanning: supported_schema_versions,
container_scanning: supported_schema_versions,
coverage_fuzzing: supported_schema_versions,
dast: supported_schema_versions,
dependency_scanning: supported_schema_versions,
api_fuzzing: supported_schema_versions
}
end
[true, false].each do |read_schemas_from_gem|
context format("when reading schemas from %s", read_schemas_from_gem ? "gem" : "tree") do
before do
stub_feature_flags(security_report_schemas_rubygem: read_schemas_from_gem)
end
let(:deprecated_schema_versions) { %w[14.1.0] }
let(:deprecations_hash) do
{
cluster_image_scanning: deprecated_schema_versions,
container_scanning: deprecated_schema_versions,
coverage_fuzzing: deprecated_schema_versions,
dast: deprecated_schema_versions,
dependency_scanning: deprecated_schema_versions,
api_fuzzing: deprecated_schema_versions
}
end
context 'with stubbed supported versions' do
let(:supported_schema_versions) { %w[15.0.0] }
let(:validator) { described_class.new(report_type, report_data, report_data['version'], project: project) }
let(:supported_hash) do
{
cluster_image_scanning: supported_schema_versions,
container_scanning: supported_schema_versions,
coverage_fuzzing: supported_schema_versions,
dast: supported_schema_versions,
dependency_scanning: supported_schema_versions,
api_fuzzing: supported_schema_versions
}
end
let(:report_type) { :dast }
let(:valid_data) do
{
'version' => '15.0.0',
'vulnerabilities' => [],
'scan' => {
'scanned_resources' => [],
'type' => report_type.to_s,
'start_time' => '2012-02-10T05:16:59',
'end_time' => '2012-02-10T05:26:02',
'status' => 'success',
'analyzer' => {
'id' => 'some-gitlab-analyzer-id',
'name' => 'Some Analyzer',
'version' => '0.2.0',
'vendor' => {
'name' => 'Some Analyzer Vendor'
}
},
'scanner' => {
'id' => 'some-gitlab-scanner-id',
'name' => 'Some Scanner',
'version' => '0.1.0',
'vendor' => {
'name' => 'Some Scanner Vendor'
let(:deprecated_schema_versions) { %w[14.1.0] }
let(:deprecations_hash) do
{
cluster_image_scanning: deprecated_schema_versions,
container_scanning: deprecated_schema_versions,
coverage_fuzzing: deprecated_schema_versions,
dast: deprecated_schema_versions,
dependency_scanning: deprecated_schema_versions,
api_fuzzing: deprecated_schema_versions
}
end
let(:report_type) { :dast }
let(:valid_data) do
{
'version' => '15.0.0',
'vulnerabilities' => [],
'scan' => {
'scanned_resources' => [],
'type' => report_type.to_s,
'start_time' => '2012-02-10T05:16:59',
'end_time' => '2012-02-10T05:26:02',
'status' => 'success',
'analyzer' => {
'id' => 'some-gitlab-analyzer-id',
'name' => 'Some Analyzer',
'version' => '0.2.0',
'vendor' => {
'name' => 'Some Analyzer Vendor'
}
},
'scanner' => {
'id' => 'some-gitlab-scanner-id',
'name' => 'Some Scanner',
'version' => '0.1.0',
'vendor' => {
'name' => 'Some Scanner Vendor'
}
}
}
}
}
}
end
end
let(:valid_data_for_dependency_scanning) do
valid_data['dependency_files'] = []
valid_data
end
let(:valid_data_for_dependency_scanning) do
valid_data['dependency_files'] = []
valid_data
end
let(:expected_missing_key_message) do
'root is missing required keys: vulnerabilities'
end
let(:expected_missing_key_message) do
'root is missing required keys: vulnerabilities'
end
let(:expected_unsupported_message) do
"Version #{report_data['version']} for report type #{report_type} is unsupported, supported versions for "\
"this report type are: #{supported_versions}. GitLab will attempt to validate this report against the earliest "\
"supported versions of this report type, to show all the errors but will not ingest the report"
end
let(:expected_unsupported_message) do
"Version #{report_data['version']} for report type #{report_type} is unsupported, supported versions for " \
"this report type are: #{supported_versions}. GitLab will attempt to validate this report against the earliest " \
"supported versions of this report type, to show all the errors but will not ingest the report"
end
let(:expected_error_messages) do
[expected_missing_key_message, expected_unsupported_message]
end
let(:expected_error_messages) do
[expected_missing_key_message, expected_unsupported_message]
end
let(:expected_missing_key_message_for_dependency_scanning) do
'root is missing required keys: dependency_files, vulnerabilities'
end
let(:expected_missing_key_message_for_dependency_scanning) do
'root is missing required keys: dependency_files, vulnerabilities'
end
let(:expected_error_messages_for_dependency_scanning) do
[expected_missing_key_message_for_dependency_scanning, expected_unsupported_message]
end
let(:expected_error_messages_for_dependency_scanning) do
[expected_missing_key_message_for_dependency_scanning, expected_unsupported_message]
end
before do
stub_const("#{described_class}::SUPPORTED_VERSIONS", supported_hash)
stub_const("#{described_class}::DEPRECATED_VERSIONS", deprecations_hash)
end
before do
if read_schemas_from_gem
schema_versions = ->(hash) do
hash.values.flatten.map { |ver| Gitlab::SecurityReportSchemas::SchemaVer.new(ver) }
end
supported_versions = schema_versions.call(supported_hash)
deprecated_versions = schema_versions.call(deprecations_hash)
allow(Gitlab::SecurityReportSchemas).to receive(:supported_versions).and_return(supported_versions)
allow(Gitlab::SecurityReportSchemas).to receive(:deprecated_versions).and_return(deprecated_versions)
else
stub_const("#{described_class}::SUPPORTED_VERSIONS", supported_hash)
stub_const("#{described_class}::DEPRECATED_VERSIONS", deprecations_hash)
end
end
using RSpec::Parameterized::TableSyntax
using RSpec::Parameterized::TableSyntax
where(:report_type, :expected_errors, :report_data) do
:cluster_image_scanning | ref(:expected_error_messages) | ref(:valid_data)
:container_scanning | ref(:expected_error_messages) | ref(:valid_data)
:coverage_fuzzing | ref(:expected_error_messages) | ref(:valid_data)
:dast | ref(:expected_error_messages) | ref(:valid_data)
:dependency_scanning | ref(:expected_error_messages_for_dependency_scanning) | ref(:valid_data_for_dependency_scanning)
:api_fuzzing | ref(:expected_error_messages) | ref(:valid_data)
end
where(:report_type, :expected_errors, :report_data) do
:cluster_image_scanning | ref(:expected_error_messages) | ref(:valid_data)
:container_scanning | ref(:expected_error_messages) | ref(:valid_data)
:coverage_fuzzing | ref(:expected_error_messages) | ref(:valid_data)
:dast | ref(:expected_error_messages) | ref(:valid_data)
:dependency_scanning | ref(:expected_error_messages_for_dependency_scanning) | ref(:valid_data_for_dependency_scanning)
:api_fuzzing | ref(:expected_error_messages) | ref(:valid_data)
end
with_them do
describe "#valid?" do
subject { validator.valid? }
with_them do
describe "#valid?" do
subject { validator.valid? }
context 'when given data is invalid according to the schema' do
let(:report_data) { {} }
context 'when given data is invalid according to the schema' do
let(:report_data) { {} }
it { is_expected.to be_falsey }
end
it { is_expected.to be_falsey }
end
context 'when given data is valid according to the schema' do
it { is_expected.to be_truthy }
end
end
context 'when given data is valid according to the schema' do
it { is_expected.to be_truthy }
end
end
describe '#deprecation_warnings' do
subject { validator.deprecation_warnings }
describe '#deprecation_warnings' do
subject { validator.deprecation_warnings }
let(:current_versions) { described_class::CURRENT_VERSIONS[report_type].join(", ") }
let(:current_versions) { described_class.current_versions(report_type, project).join(", ") }
context 'when report uses a deprecated version' do
let(:deprecated_schema_version) { deprecated_schema_versions.first }
let(:report_data) do
valid_data['version'] = deprecated_schema_version
valid_data
end
context 'when report uses a deprecated version' do
let(:deprecated_schema_version) { deprecated_schema_versions.first }
let(:report_data) do
valid_data['version'] = deprecated_schema_version
valid_data
end
let(:expected_deprecation_message) do
"version #{deprecated_schema_version} for report type #{report_type} is deprecated. "\
"However, GitLab will still attempt to parse and ingest this report. "\
"Upgrade the security report to one of the following versions: #{current_versions}."
end
let(:expected_deprecation_message) do
"version #{deprecated_schema_version} for report type #{report_type} is deprecated. " \
"However, GitLab will still attempt to parse and ingest this report. " \
"Upgrade the security report to one of the following versions: #{current_versions}."
end
let(:expected_deprecation_warnings) do
[
expected_deprecation_message
]
end
let(:expected_deprecation_warnings) do
[
expected_deprecation_message
]
end
it { is_expected.to eq(expected_deprecation_warnings) }
end
it { is_expected.to eq(expected_deprecation_warnings) }
end
context 'when report uses a supported version' do
let(:supported_version) { described_class::SUPPORTED_VERSIONS[report_type].first }
let(:report_data) { valid_data }
context 'when report uses a supported version' do
let(:supported_version) { described_class.supported_version(report_type, project).first }
let(:report_data) { valid_data }
it { is_expected.to eq([]) }
end
end
it { is_expected.to eq([]) }
end
end
describe '#warnings' do
subject { validator.warnings }
describe '#warnings' do
subject { validator.warnings }
context 'when given data is valid according to the schema' do
let(:supported_version) { described_class::SUPPORTED_VERSIONS[report_type].join(", ") }
let(:expected_warnings) { [] }
context 'when given data is valid according to the schema' do
let(:supported_version) { described_class.supported_versions(report_type, project).join(", ") }
let(:expected_warnings) { [] }
it { is_expected.to eq(expected_warnings) }
end
it { is_expected.to eq(expected_warnings) }
end
context 'when given data is invalid according to the schema' do
let(:report_data) { {} }
context 'when given data is invalid according to the schema' do
let(:report_data) { {} }
it { is_expected.to be_empty }
end
end
it { is_expected.to be_empty }
end
end
describe '#errors' do
subject { validator.errors }
describe '#errors' do
subject { validator.errors }
let(:report_data) do
valid_data['version'] = "2.1.3"
valid_data.delete('vulnerabilities')
valid_data
end
let(:report_data) do
valid_data['version'] = "2.1.3"
valid_data.delete('vulnerabilities')
valid_data
end
let(:supported_versions) { described_class::SUPPORTED_VERSIONS[report_type].join(", ") }
let(:supported_versions) { described_class.supported_versions(report_type, project).join(", ") }
it { is_expected.to match_array(expected_errors) }
it { is_expected.to match_array(expected_errors) }
end
end
end
end
end
# These tests validate that the security report fixtures are valid
# against our schema.
#
# - All .json reports in fixture_dir are checked except for those containing
# 'license-scanning' in the file name.
# - If a report does not contain a 'version' attribute, the latest schema
# for the report type is used.
# - Some report fixtures are intentionally invalid. In those cases we check
# that only the expected validation failures are found.
#
describe 'validate fixture reports' do
fixture_dir = 'ee/spec/fixtures/security_reports'
all_reports = Dir.glob("#{fixture_dir}/**/*.json")
reports_to_test = all_reports.reject { |report| report.include?('license-scanning') }
reports_expected_to_be_invalid = {
"#{fixture_dir}/master/gl-dast-report-missing-version.json" => [
"root is missing required keys: version"
],
"#{fixture_dir}/master/gl-sast-report-without-any-identifiers.json" => [
"property '/vulnerabilities/0/identifiers' is invalid: error_type=minItems"
],
"#{fixture_dir}/master/gl-dast-report-missing-scan.json" => [
"root is missing required keys: scan"
]
}
reports_expected_to_be_valid = reports_to_test - reports_expected_to_be_invalid.keys
def latest_schema_version_for_report_type(report_type)
described_class::SUPPORTED_VERSIONS.fetch(report_type).last
end
# These tests validate that the security report fixtures are valid
# against our schema.
#
# - All .json reports in fixture_dir are checked except for those containing
# 'license-scanning' in the file name.
# - If a report does not contain a 'version' attribute, the latest schema
# for the report type is used.
# - Some report fixtures are intentionally invalid. In those cases we check
# that only the expected validation failures are found.
#
describe 'validate fixture reports' do
fixture_dir = 'ee/spec/fixtures/security_reports'
all_reports = Dir.glob("#{fixture_dir}/**/*.json")
reports_to_test = all_reports.reject { |report| report.include?('license-scanning') }
reports_expected_to_be_invalid = {
"#{fixture_dir}/master/gl-dast-report-missing-version.json" => [
"root is missing required keys: version"
],
"#{fixture_dir}/master/gl-sast-report-without-any-identifiers.json" => [
"property '/vulnerabilities/0/identifiers' is invalid: error_type=minItems"
],
"#{fixture_dir}/master/gl-dast-report-missing-scan.json" => [
"root is missing required keys: scan"
]
}
def get_report_type(path)
filename = File.basename(path)
reports_expected_to_be_valid = reports_to_test - reports_expected_to_be_invalid.keys
matches = /gl-(\S+)-report(\S+)?.json/.match(filename)
def latest_schema_version_for_report_type(report_type)
# TODO: Amend for RubyGem
described_class::SUPPORTED_VERSIONS.fetch(report_type).last
end
return :sast unless matches
def get_report_type(path)
filename = File.basename(path)
matches[1].tr('-', '_').to_sym
end
matches = /gl-(\S+)-report(\S+)?.json/.match(filename)
subject(:validator) { described_class.new(report_type, report_data, report_version, project: project) }
return :sast unless matches
let(:report_type) { get_report_type(report) }
let(:report_data) { Gitlab::Json.parse(File.read(report)) }
let(:report_version) { report_data.fetch('version', latest_schema_version_for_report_type(report_type)) }
matches[1].tr('-', '_').to_sym
end
subject(:validator) { described_class.new(report_type, report_data, report_version, project: project) }
reports_expected_to_be_valid.sort.each do |report|
describe report do
let(:report) { report }
let(:report_type) { get_report_type(report) }
let(:report_data) { Gitlab::Json.parse(File.read(report)) }
let(:report_version) { report_data.fetch('version', latest_schema_version_for_report_type(report_type)) }
it 'is expected to be valid' do
expect(subject.errors).to be_empty
reports_expected_to_be_valid.sort.each do |report|
describe report do
let(:report) { report }
it 'is expected to be valid' do
expect(subject.errors).to be_empty
end
end
end
end
end
reports_expected_to_be_invalid.sort.each do |report, expected_errors|
describe report do
let(:report) { report }
reports_expected_to_be_invalid.sort.each do |report, expected_errors|
describe report do
let(:report) { report }
it 'is expected to be invalid' do
expect(subject.errors).to eq expected_errors
it 'is expected to be invalid' do
expect(subject.errors).to eq expected_errors
end
end
end
end
end
......
......@@ -36,22 +36,48 @@ class SchemaValidator
# https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/e3d280d7f0862ca66a1555ea8b24016a004bb914/src/security-report-format.json#L151
SCHEMA_VERSION_REGEX = /^[0-9]+\.[0-9]+\.[0-9]+$/
class Schema
def root_path
File.join(__dir__, 'schemas')
end
def self.source_schemas_from_gem?(project)
Feature.enabled?(:security_report_schemas_rubygem, project)
end
def self.supported_versions(report_type, project)
return SUPPORTED_VERSIONS[report_type] unless source_schemas_from_gem?(project)
Gitlab::SecurityReportSchemas.supported_versions.map(&:to_s)
end
def self.current_versions(report_type, project)
return CURRENT_VERSIONS[report_type] unless source_schemas_from_gem?(project)
supported_versions(report_type, project).map(&:to_s)
end
def initialize(report_type, report_version)
def self.deprecated_versions(report_type, project)
return DEPRECATED_VERSIONS[report_type] unless source_schemas_from_gem?(project)
Gitlab::SecurityReportSchemas.deprecated_versions.map(&:to_s)
end
class Schema
def initialize(report_type, report_version, project)
@report_type = report_type.to_sym
@report_version = report_version.to_s
@supported_versions = SUPPORTED_VERSIONS[@report_type]
@project = project
end
def root_path
return File.join(__dir__, 'schemas') unless source_schemas_from_gem?(project)
Gitlab::SecurityReportSchemas.schemas_path
end
delegate :validate, to: :schemer
private
attr_reader :report_type, :report_version, :supported_versions
attr_reader :report_type, :report_version, :project
delegate :source_schemas_from_gem?, :supported_versions, to: SchemaValidator
def schemer
JSONSchemer.schema(pathname)
......@@ -72,14 +98,14 @@ def schema_path
return latest_vendored_patch_version_file if File.file?(latest_vendored_patch_version_file)
end
earliest_supported_version = SUPPORTED_VERSIONS[report_type].min
earliest_supported_version = supported_versions(report_type, project).min
File.join(root_path, earliest_supported_version, file_name)
end
def latest_vendored_patch_version
::Security::ReportSchemaVersionMatcher.new(
report_declared_version: report_version,
supported_versions: supported_versions
supported_versions: supported_versions(report_type, project)
).call
rescue ArgumentError
nil
......@@ -135,11 +161,11 @@ def add_schema_version_error?
end
def report_uses_deprecated_schema_version?
DEPRECATED_VERSIONS[report_type].include?(report_version)
deprecated_versions(report_type, project).include?(report_version)
end
def report_uses_supported_schema_version?
SUPPORTED_VERSIONS[report_type].include?(report_version)
supported_versions(report_type, project).include?(report_version)
end
def report_uses_supported_major_and_minor_schema_version?
......@@ -158,7 +184,7 @@ def report_version_matches_schema?
def find_latest_patch_version
::Security::ReportSchemaVersionMatcher.new(
report_declared_version: report_version,
supported_versions: SUPPORTED_VERSIONS[report_type]
supported_versions: supported_versions(report_type, project)
).call
rescue ArgumentError
nil
......@@ -222,11 +248,11 @@ def log_warnings(problem_type:)
end
def current_schema_versions
CURRENT_VERSIONS[report_type].join(", ")
current_versions(report_type, project).join(", ")
end
def supported_schema_versions
SUPPORTED_VERSIONS[report_type].join(", ")
supported_versions(report_type, project).join(", ")
end
def add_message_as(level:, message:)
......@@ -244,10 +270,12 @@ def add_message_as(level:, message:)
private
attr_reader :report_type, :report_data, :report_version
attr_reader :report_type, :report_data, :report_version, :project
delegate :source_schemas_from_gem?, :supported_versions, :current_versions, :deprecated_versions, to: "self.class"
def schema
Schema.new(report_type, report_version)
Schema.new(report_type, report_version, project)
end
end
end
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment