Commit a30e2b49 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch 'coverage_fuzzing_report' into 'master'

Add support for coverage_fuzzing report in .gitlab-ci.yml

See merge request gitlab-org/gitlab!34648
parents f8d862a7 588937ac
...@@ -40,7 +40,8 @@ module Ci ...@@ -40,7 +40,8 @@ module Ci
cobertura: 'cobertura-coverage.xml', cobertura: 'cobertura-coverage.xml',
terraform: 'tfplan.json', terraform: 'tfplan.json',
cluster_applications: 'gl-cluster-applications.json', cluster_applications: 'gl-cluster-applications.json',
requirements: 'requirements.json' requirements: 'requirements.json',
coverage_fuzzing: 'gl-coverage-fuzzing.json'
}.freeze }.freeze
INTERNAL_TYPES = { INTERNAL_TYPES = {
...@@ -73,7 +74,8 @@ module Ci ...@@ -73,7 +74,8 @@ module Ci
license_scanning: :raw, license_scanning: :raw,
performance: :raw, performance: :raw,
terraform: :raw, terraform: :raw,
requirements: :raw requirements: :raw,
coverage_fuzzing: :raw
}.freeze }.freeze
DOWNLOADABLE_TYPES = %w[ DOWNLOADABLE_TYPES = %w[
...@@ -187,7 +189,8 @@ module Ci ...@@ -187,7 +189,8 @@ module Ci
accessibility: 19, accessibility: 19,
cluster_applications: 20, cluster_applications: 20,
secret_detection: 21, ## EE-specific secret_detection: 21, ## EE-specific
requirements: 22 ## EE-specific requirements: 22, ## EE-specific
coverage_fuzzing: 23 ## EE-specific
} }
enum file_format: { enum file_format: {
......
...@@ -11314,6 +11314,11 @@ type SecurityReportSummary { ...@@ -11314,6 +11314,11 @@ type SecurityReportSummary {
""" """
containerScanning: SecurityReportSummarySection containerScanning: SecurityReportSummarySection
"""
Aggregated counts for the coverage_fuzzing scan
"""
coverageFuzzing: SecurityReportSummarySection
""" """
Aggregated counts for the dast scan Aggregated counts for the dast scan
""" """
...@@ -13999,7 +14004,8 @@ type Vulnerability { ...@@ -13999,7 +14004,8 @@ type Vulnerability {
""" """
Type of the security report that found the vulnerability (SAST, Type of the security report that found the vulnerability (SAST,
DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION) DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION,
COVERAGE_FUZZING)
""" """
reportType: VulnerabilityReportType reportType: VulnerabilityReportType
...@@ -14337,6 +14343,7 @@ The type of the security scan that found the vulnerability. ...@@ -14337,6 +14343,7 @@ The type of the security scan that found the vulnerability.
""" """
enum VulnerabilityReportType { enum VulnerabilityReportType {
CONTAINER_SCANNING CONTAINER_SCANNING
COVERAGE_FUZZING
DAST DAST
DEPENDENCY_SCANNING DEPENDENCY_SCANNING
SAST SAST
......
...@@ -33218,6 +33218,20 @@ ...@@ -33218,6 +33218,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "coverageFuzzing",
"description": "Aggregated counts for the coverage_fuzzing scan",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "SecurityReportSummarySection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "dast", "name": "dast",
"description": "Aggregated counts for the dast scan", "description": "Aggregated counts for the dast scan",
...@@ -41241,7 +41255,7 @@ ...@@ -41241,7 +41255,7 @@
}, },
{ {
"name": "reportType", "name": "reportType",
"description": "Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION)", "description": "Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION, COVERAGE_FUZZING)",
"args": [ "args": [
], ],
...@@ -42311,6 +42325,12 @@ ...@@ -42311,6 +42325,12 @@
"description": null, "description": null,
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
},
{
"name": "COVERAGE_FUZZING",
"description": null,
"isDeprecated": false,
"deprecationReason": null
} }
], ],
"possibleTypes": null "possibleTypes": null
...@@ -1643,6 +1643,7 @@ Represents summary of a security report ...@@ -1643,6 +1643,7 @@ Represents summary of a security report
| Name | Type | Description | | Name | Type | Description |
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `containerScanning` | SecurityReportSummarySection | Aggregated counts for the container_scanning scan | | `containerScanning` | SecurityReportSummarySection | Aggregated counts for the container_scanning scan |
| `coverageFuzzing` | SecurityReportSummarySection | Aggregated counts for the coverage_fuzzing scan |
| `dast` | SecurityReportSummarySection | Aggregated counts for the dast scan | | `dast` | SecurityReportSummarySection | Aggregated counts for the dast scan |
| `dependencyScanning` | SecurityReportSummarySection | Aggregated counts for the dependency_scanning scan | | `dependencyScanning` | SecurityReportSummarySection | Aggregated counts for the dependency_scanning scan |
| `sast` | SecurityReportSummarySection | Aggregated counts for the sast scan | | `sast` | SecurityReportSummarySection | Aggregated counts for the sast scan |
...@@ -2101,7 +2102,7 @@ Represents a vulnerability. ...@@ -2101,7 +2102,7 @@ Represents a vulnerability.
| `location` | VulnerabilityLocation | Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability | | `location` | VulnerabilityLocation | Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability |
| `primaryIdentifier` | VulnerabilityIdentifier | Primary identifier of the vulnerability. | | `primaryIdentifier` | VulnerabilityIdentifier | Primary identifier of the vulnerability. |
| `project` | Project | The project on which the vulnerability was found | | `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, SECRET_DETECTION) | | `reportType` | VulnerabilityReportType | Type of the security report that found the vulnerability (SAST, DEPENDENCY_SCANNING, CONTAINER_SCANNING, DAST, SECRET_DETECTION, COVERAGE_FUZZING) |
| `scanner` | VulnerabilityScanner | Scanner metadata for the vulnerability. | | `scanner` | VulnerabilityScanner | Scanner metadata for the vulnerability. |
| `severity` | VulnerabilitySeverity | Severity of the vulnerability (INFO, UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL) | | `severity` | VulnerabilitySeverity | Severity of the vulnerability (INFO, UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL) |
| `state` | VulnerabilityState | State of the vulnerability (DETECTED, DISMISSED, RESOLVED, CONFIRMED) | | `state` | VulnerabilityState | State of the vulnerability (DETECTED, DISMISSED, RESOLVED, CONFIRMED) |
......
...@@ -15,6 +15,7 @@ export const REPORT_TYPES = { ...@@ -15,6 +15,7 @@ export const REPORT_TYPES = {
dependency_scanning: s__('ciReport|Dependency Scanning'), dependency_scanning: s__('ciReport|Dependency Scanning'),
sast: s__('ciReport|SAST'), sast: s__('ciReport|SAST'),
secret_detection: s__('ciReport|Secret Detection'), secret_detection: s__('ciReport|Secret Detection'),
coverage_fuzzing: s__('ciReport|Coverage Fuzzing'),
}; };
export const DASHBOARD_TYPES = { export const DASHBOARD_TYPES = {
......
...@@ -52,6 +52,12 @@ export default { ...@@ -52,6 +52,12 @@ export default {
identifiers() { identifiers() {
return this.asNonEmptyListOrNull(this.vulnerability.identifiers); return this.asNonEmptyListOrNull(this.vulnerability.identifiers);
}, },
crashAddress() {
return this.vulnerability.location?.crash_address;
},
crashState() {
return this.vulnerability.location?.crash_state;
},
className() { className() {
return this.vulnerability.location?.class; return this.vulnerability.location?.class;
}, },
...@@ -101,6 +107,9 @@ export default { ...@@ -101,6 +107,9 @@ export default {
scannerVersion: this.scannerVersion, scannerVersion: this.scannerVersion,
}); });
}, },
stacktraceSnippet() {
return this.vulnerability.stacktrace_snippet;
},
}, },
methods: { methods: {
hasMoreValues(index, values) { hasMoreValues(index, values) {
...@@ -169,6 +178,18 @@ export default { ...@@ -169,6 +178,18 @@ export default {
<gl-friendly-wrap :text="vulnerability.evidence" /> <gl-friendly-wrap :text="vulnerability.evidence" />
</vulnerability-detail> </vulnerability-detail>
<vulnerability-detail v-if="crashAddress" :label="s__('Vulnerability|Crash Address')">
<gl-friendly-wrap ref="crashAddress" :text="crashAddress" />
</vulnerability-detail>
<vulnerability-detail v-if="crashState" :label="s__('Crash State')">
<code-block ref="crashState" :code="crashState" max-height="225px" />
</vulnerability-detail>
<vulnerability-detail v-if="stacktraceSnippet" :label="s__('Stacktrace snippet')">
<code-block ref="stacktraceSnippet" :code="stacktraceSnippet" max-height="225px" />
</vulnerability-detail>
<vulnerability-detail v-if="identifiers" :label="s__('Vulnerability|Identifiers')"> <vulnerability-detail v-if="identifiers" :label="s__('Vulnerability|Identifiers')">
<span v-for="(identifier, i) in identifiers" :key="i"> <span v-for="(identifier, i) in identifiers" :key="i">
<gl-link <gl-link
......
...@@ -15,7 +15,8 @@ module EE ...@@ -15,7 +15,8 @@ module EE
secret_detection: :secret_detection, secret_detection: :secret_detection,
dependency_scanning: :dependency_scanning, dependency_scanning: :dependency_scanning,
container_scanning: :container_scanning, container_scanning: :container_scanning,
dast: :dast dast: :dast,
coverage_fuzzing: :coverage_fuzzing
}.with_indifferent_access.freeze }.with_indifferent_access.freeze
prepended do prepended do
......
...@@ -11,7 +11,7 @@ module EE ...@@ -11,7 +11,7 @@ module EE
prepended do prepended do
after_destroy :log_geo_deleted_event after_destroy :log_geo_deleted_event
SECURITY_REPORT_FILE_TYPES = %w[sast secret_detection dependency_scanning container_scanning dast].freeze SECURITY_REPORT_FILE_TYPES = %w[sast secret_detection dependency_scanning container_scanning dast coverage_fuzzing].freeze
LICENSE_SCANNING_REPORT_FILE_TYPES = %w[license_management license_scanning].freeze LICENSE_SCANNING_REPORT_FILE_TYPES = %w[license_management license_scanning].freeze
DEPENDENCY_LIST_REPORT_FILE_TYPES = %w[dependency_scanning].freeze DEPENDENCY_LIST_REPORT_FILE_TYPES = %w[dependency_scanning].freeze
METRICS_REPORT_FILE_TYPES = %w[metrics].freeze METRICS_REPORT_FILE_TYPES = %w[metrics].freeze
...@@ -20,6 +20,7 @@ module EE ...@@ -20,6 +20,7 @@ module EE
SECRET_DETECTION_REPORT_TYPES = %w[secret_detection].freeze SECRET_DETECTION_REPORT_TYPES = %w[secret_detection].freeze
DAST_REPORT_TYPES = %w[dast].freeze DAST_REPORT_TYPES = %w[dast].freeze
REQUIREMENTS_REPORT_FILE_TYPES = %w[requirements].freeze REQUIREMENTS_REPORT_FILE_TYPES = %w[requirements].freeze
COVERAGE_FUZZING_REPORT_TYPES = %w[coverage_fuzzing].freeze
scope :project_id_in, ->(ids) { where(project_id: ids) } scope :project_id_in, ->(ids) { where(project_id: ids) }
scope :with_files_stored_remotely, -> { where(file_store: ::JobArtifactUploader::Store::REMOTE) } scope :with_files_stored_remotely, -> { where(file_store: ::JobArtifactUploader::Store::REMOTE) }
...@@ -56,6 +57,10 @@ module EE ...@@ -56,6 +57,10 @@ module EE
with_file_types(METRICS_REPORT_FILE_TYPES) with_file_types(METRICS_REPORT_FILE_TYPES)
end end
scope :coverage_fuzzing, -> do
with_file_types(COVERAGE_FUZZING_REPORT_TYPES)
end
def self.associated_file_types_for(file_type) def self.associated_file_types_for(file_type)
return unless file_types.include?(file_type) return unless file_types.include?(file_type)
return LICENSE_SCANNING_REPORT_FILE_TYPES if LICENSE_SCANNING_REPORT_FILE_TYPES.include?(file_type) return LICENSE_SCANNING_REPORT_FILE_TYPES if LICENSE_SCANNING_REPORT_FILE_TYPES.include?(file_type)
......
...@@ -48,7 +48,8 @@ module EE ...@@ -48,7 +48,8 @@ module EE
license_management: %i[license_scanning], license_management: %i[license_scanning],
license_scanning: %i[license_scanning], license_scanning: %i[license_scanning],
metrics: %i[metrics_reports], metrics: %i[metrics_reports],
requirements: %i[requirements] requirements: %i[requirements],
coverage_fuzzing: %i[coverage_fuzzing]
}.freeze }.freeze
state_machine :status do state_machine :status do
......
...@@ -307,7 +307,8 @@ module EE ...@@ -307,7 +307,8 @@ module EE
feature_available?(:secret_detection) || feature_available?(:secret_detection) ||
feature_available?(:dependency_scanning) || feature_available?(:dependency_scanning) ||
feature_available?(:container_scanning) || feature_available?(:container_scanning) ||
feature_available?(:dast) feature_available?(:dast) ||
feature_available?(:coverage_fuzzing)
end end
def free_plan? def free_plan?
......
...@@ -112,6 +112,7 @@ class License < ApplicationRecord ...@@ -112,6 +112,7 @@ class License < ApplicationRecord
EEU_FEATURES = EEP_FEATURES + %i[ EEU_FEATURES = EEP_FEATURES + %i[
compliance_framework compliance_framework
container_scanning container_scanning
coverage_fuzzing
credentials_inventory credentials_inventory
dast dast
dependency_scanning dependency_scanning
......
...@@ -15,7 +15,8 @@ module Security ...@@ -15,7 +15,8 @@ module Security
dependency_scanning: 2, dependency_scanning: 2,
container_scanning: 3, container_scanning: 3,
dast: 4, dast: 4,
secret_detection: 5 secret_detection: 5,
coverage_fuzzing: 6
} }
end end
end end
...@@ -15,7 +15,7 @@ module Vulnerabilities ...@@ -15,7 +15,7 @@ module Vulnerabilities
attr_accessor :vulnerability_data attr_accessor :vulnerability_data
enum feedback_type: { dismissal: 0, issue: 1, merge_request: 2 }, _prefix: :for enum feedback_type: { dismissal: 0, issue: 1, merge_request: 2 }, _prefix: :for
enum category: { sast: 0, dependency_scanning: 1, container_scanning: 2, dast: 3, secret_detection: 4 } enum category: { sast: 0, dependency_scanning: 1, container_scanning: 2, dast: 3, secret_detection: 4, coverage_fuzzing: 5 }
validates :project, presence: true validates :project, presence: true
validates :author, presence: true validates :author, presence: true
......
...@@ -54,7 +54,8 @@ module Vulnerabilities ...@@ -54,7 +54,8 @@ module Vulnerabilities
dependency_scanning: 1, dependency_scanning: 1,
container_scanning: 2, container_scanning: 2,
dast: 3, dast: 3,
secret_detection: 4 secret_detection: 4,
coverage_fuzzing: 5
}.with_indifferent_access.freeze }.with_indifferent_access.freeze
enum confidence: CONFIDENCE_LEVELS, _prefix: :confidence enum confidence: CONFIDENCE_LEVELS, _prefix: :confidence
......
...@@ -14,7 +14,8 @@ module Projects ...@@ -14,7 +14,8 @@ module Projects
license_management: 'user/compliance/license_compliance/index', license_management: 'user/compliance/license_compliance/index',
license_scanning: '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' secret_detection: 'user/application_security/secret_detection/index',
coverage_fuzzing: 'user/application_security/coverage_fuzzing/index'
}.freeze }.freeze
def self.localized_scan_descriptions def self.localized_scan_descriptions
...@@ -25,7 +26,8 @@ module Projects ...@@ -25,7 +26,8 @@ module Projects
license_management: _('Search your project dependencies for their licenses and apply policies.'), license_management: _('Search your project dependencies for their licenses and apply policies.'),
license_scanning: _('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.') secret_detection: _('Analyze your source code and git history for secrets.'),
coverage_fuzzing: _('Find bugs in your code with coverage-guided fuzzing')
}.freeze }.freeze
end end
...@@ -37,7 +39,8 @@ module Projects ...@@ -37,7 +39,8 @@ module Projects
license_management: 'License Management', license_management: 'License Management',
license_scanning: _('License Compliance'), license_scanning: _('License Compliance'),
sast: _('Static Application Security Testing (SAST)'), sast: _('Static Application Security Testing (SAST)'),
secret_detection: _('Secret Detection') secret_detection: _('Secret Detection'),
coverage_fuzzing: _('Coverage Fuzzing')
}.freeze }.freeze
end end
......
---
title: Add support for gl-coverage-fuzzing-report.json and security dashboard parsing
merge_request: 34648
author:
type: added
...@@ -15,6 +15,7 @@ module EE ...@@ -15,6 +15,7 @@ module EE
container_scanning: ::Gitlab::Ci::Parsers::Security::ContainerScanning, container_scanning: ::Gitlab::Ci::Parsers::Security::ContainerScanning,
dast: ::Gitlab::Ci::Parsers::Security::Dast, dast: ::Gitlab::Ci::Parsers::Security::Dast,
sast: ::Gitlab::Ci::Parsers::Security::Sast, sast: ::Gitlab::Ci::Parsers::Security::Sast,
coverage_fuzzing: ::Gitlab::Ci::Parsers::Security::CoverageFuzzing,
secret_detection: ::Gitlab::Ci::Parsers::Security::SecretDetection, secret_detection: ::Gitlab::Ci::Parsers::Security::SecretDetection,
metrics: ::Gitlab::Ci::Parsers::Metrics::Generic, metrics: ::Gitlab::Ci::Parsers::Metrics::Generic,
requirements: ::Gitlab::Ci::Parsers::RequirementsManagement::Requirement requirements: ::Gitlab::Ci::Parsers::RequirementsManagement::Requirement
......
# frozen_string_literal: true
module Gitlab
module Ci
module Parsers
module Security
class CoverageFuzzing < Common
private
def create_location(location_data)
::Gitlab::Ci::Reports::Security::Locations::CoverageFuzzing.new(
crash_address: location_data['crash_address'],
crash_state: location_data['crash_state'],
crash_type: location_data['crash_type']
)
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
module CoverageFuzzing
class Crash
attr_accessor :crash_address
attr_accessor :crash_type
attr_accessor :crash_state
attr_reader :stacktrace_snippet
def initialize(params = {})
@crash_address = params.fetch(:crash_address)
@crash_type = params.fetch(:crash_type)
@crash_state = params.fetch(:crash_state)
@stacktrace_snippet = params.fetch(:stacktrace_snippet)
end
def hash
crash_state.hash
end
def eql?(other)
other.state == state && other.crash_type == crash_type
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
module CoverageFuzzing
class Report
attr_reader :regression
attr_reader :exit_code
attr_reader :crashes
def initialize
@crashes = []
end
def add_crash(crash)
@crashes << Crash.new(crash)
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
module Security
module Locations
class CoverageFuzzing < Base
attr_reader :crash_address
attr_reader :crash_type
attr_reader :crash_state
def initialize(crash_address:, crash_type:, crash_state:)
@crash_address = crash_address
@crash_type = crash_type
@crash_state = crash_state
end
private
def fingerprint_data
"#{crash_type}:#{crash_state}"
end
end
end
end
end
end
end
...@@ -127,5 +127,11 @@ FactoryBot.define do ...@@ -127,5 +127,11 @@ FactoryBot.define do
build.job_artifacts << create(:ee_ci_job_artifact, :all_passing_requirements, job: build) build.job_artifacts << create(:ee_ci_job_artifact, :all_passing_requirements, job: build)
end end
end end
trait :coverage_fuzzing_report do
after(:build) do |build|
build.job_artifacts << create(:ee_ci_job_artifact, :coverage_fuzzing, job: build)
end
end
end end
end end
...@@ -352,5 +352,16 @@ FactoryBot.define do ...@@ -352,5 +352,16 @@ FactoryBot.define do
Rails.root.join('ee/spec/fixtures/requirements_management/report_by_requirement.json'), 'application/json') Rails.root.join('ee/spec/fixtures/requirements_management/report_by_requirement.json'), 'application/json')
end end
end end
trait :coverage_fuzzing do
file_format { :raw }
file_type { :coverage_fuzzing }
after(:build) do |artifact, _|
artifact.file = fixture_file_upload(
Rails.root.join('ee/spec/fixtures/security_reports/master/gl-coverage-fuzzing-report.json'),
'application/json')
end
end
end end
end end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
FactoryBot.define do FactoryBot.define do
factory :ee_ci_pipeline, class: 'Ci::Pipeline', parent: :ci_pipeline 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 secret_detection].each do |report_type| %i[container_scanning dast dependency_list dependency_scanning license_management license_scanning sast secret_detection coverage_fuzzing].each do |report_type|
trait "with_#{report_type}_report".to_sym do trait "with_#{report_type}_report".to_sym do
status { :success } status { :success }
......
{
"version": "v1.0.8",
"regression": false,
"exit_code": -1,
"vulnerabilities": [
{
"category": "coverage_fuzzing",
"message": "Heap-buffer-overflow\nREAD 1",
"description": "Heap-buffer-overflow\nREAD 1",
"severity": "Critical",
"stacktrace_snippet": "INFO: Seed: 3415817494\nINFO: Loaded 1 modules (7 inline 8-bit counters): 7 [0x10eee2470, 0x10eee2477), \nINFO: Loaded 1 PC tables (7 PCs): 7 [0x10eee2478,0x10eee24e8), \nINFO: 5 files found in corpus\nINFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes\nINFO: seed corpus: files: 5 min: 1b max: 4b total: 14b rss: 26Mb\n#6\tINITED cov: 7 ft: 7 corp: 5/14b exec/s: 0 rss: 26Mb\n=================================================================\n==43405==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000001573 at pc 0x00010eea205a bp 0x7ffee0d5e090 sp 0x7ffee0d5e088\nREAD of size 1 at 0x602000001573 thread T0\n #0 0x10eea2059 in FuzzMe(unsigned char const*, unsigned long) fuzz_me.cc:9\n #1 0x10eea20ba in LLVMFuzzerTestOneInput fuzz_me.cc:13\n #2 0x10eebe020 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) FuzzerLoop.cpp:556\n #3 0x10eebd765 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool*) FuzzerLoop.cpp:470\n #4 0x10eebf966 in fuzzer::Fuzzer::MutateAndTestOne() FuzzerLoop.cpp:698\n #5 0x10eec0665 in fuzzer::Fuzzer::Loop(std::__1::vector\u003cfuzzer::SizedFile, fuzzer::fuzzer_allocator\u003cfuzzer::SizedFile\u003e \u003e\u0026) FuzzerLoop.cpp:830\n #6 0x10eead0cd in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) FuzzerDriver.cpp:829\n #7 0x10eedaf82 in main FuzzerMain.cpp:19\n #8 0x7fff684fecc8 in start+0x0 (libdyld.dylib:x86_64+0x1acc8)\n\n0x602000001573 is located 0 bytes to the right of 3-byte region [0x602000001570,0x602000001573)\nallocated by thread T0 here:\n #0 0x10ef92cfd in wrap__Znam+0x7d (libclang_rt.asan_osx_dynamic.dylib:x86_64+0x50cfd)\n #1 0x10eebdf31 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) FuzzerLoop.cpp:541\n #2 0x10eebd765 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool*) FuzzerLoop.cpp:470\n #3 0x10eebf966 in fuzzer::Fuzzer::MutateAndTestOne() FuzzerLoop.cpp:698\n #4 0x10eec0665 in fuzzer::Fuzzer::Loop(std::__1::vector\u003cfuzzer::SizedFile, fuzzer::fuzzer_allocator\u003cfuzzer::SizedFile\u003e \u003e\u0026) FuzzerLoop.cpp:830\n #5 0x10eead0cd in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) FuzzerDriver.cpp:829\n #6 0x10eedaf82 in main FuzzerMain.cpp:19\n #7 0x7fff684fecc8 in start+0x0 (libdyld.dylib:x86_64+0x1acc8)\n\nSUMMARY: AddressSanitizer: heap-buffer-overflow fuzz_me.cc:9 in FuzzMe(unsigned char const*, unsigned long)\nShadow bytes around the buggy address:\n 0x1c0400000250: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa\n 0x1c0400000260: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa\n 0x1c0400000270: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa\n 0x1c0400000280: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa\n 0x1c0400000290: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa\n=\u003e0x1c04000002a0: fa fa fd fa fa fa fd fa fa fa fd fa fa fa[03]fa\n 0x1c04000002b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa\n 0x1c04000002c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa\n 0x1c04000002d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa\n 0x1c04000002e0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa\n 0x1c04000002f0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa\nShadow byte legend (one shadow byte represents 8 application bytes):\n Addressable: 00\n Partially addressable: 01 02 03 04 05 06 07 \n Heap left redzone: fa\n Freed heap region: fd\n Stack left redzone: f1\n Stack mid redzone: f2\n Stack right redzone: f3\n Stack after return: f5\n Stack use after scope: f8\n Global redzone: f9\n Global init order: f6\n Poisoned by user: f7\n Container overflow: fc\n Array cookie: ac\n Intra object redzone: bb\n ASan internal: fe\n Left alloca redzone: ca\n Right alloca redzone: cb\n Shadow gap: cc\n==43405==ABORTING\nMS: 1 EraseBytes-; base unit: de3a753d4f1def197604865d76dba888d6aefc71\n0x46,0x55,0x5a,\nFUZ\nartifact_prefix='./crashes/'; Test unit written to ./crashes/crash-0eb8e4ed029b774d80f2b66408203801cb982a60\nBase64: RlVa\nstat::number_of_executed_units: 122\nstat::average_exec_per_sec: 0\nstat::new_units_added: 0\nstat::slowest_unit_time_sec: 0\nstat::peak_rss_mb: 28",
"scanner": {
"id": "libFuzzer",
"name": "libFuzzer"
},
"location": {
"crash_address": "0x602000001573",
"crash_state": "FuzzMe\nstart\nstart+0x0\n\n",
"crash_type": "Heap-buffer-overflow\nREAD 1"
},
"tool": "libFuzzer"
}
]
}
INFO: Seed: 599314159
INFO: Loaded 1 modules (7 inline 8-bit counters): 7 [0x102dbc470, 0x102dbc477),
INFO: Loaded 1 PC tables (7 PCs): 7 [0x102dbc478,0x102dbc4e8),
INFO: -max_len is not provided; libFuzzer will not generate inputs larger than 4096 bytes
INFO: A corpus is not provided, starting from an empty corpus
#2 INITED cov: 3 ft: 3 corp: 1/1b exec/s: 0 rss: 26Mb
#8 NEW cov: 4 ft: 4 corp: 2/5b lim: 4 exec/s: 0 rss: 26Mb L: 4/4 MS: 1 CrossOver-
#10 REDUCE cov: 4 ft: 4 corp: 2/4b lim: 4 exec/s: 0 rss: 26Mb L: 3/3 MS: 2 CopyPart-EraseBytes-
#270 REDUCE cov: 5 ft: 5 corp: 3/7b lim: 6 exec/s: 0 rss: 26Mb L: 3/3 MS: 5 ShuffleBytes-EraseBytes-ChangeByte-InsertByte-ChangeBit-
#12287 NEW cov: 6 ft: 6 corp: 4/52b lim: 122 exec/s: 0 rss: 27Mb L: 45/45 MS: 2 CopyPart-InsertRepeatedBytes-
#12323 REDUCE cov: 6 ft: 6 corp: 4/48b lim: 122 exec/s: 0 rss: 27Mb L: 41/41 MS: 1 EraseBytes-
#12324 REDUCE cov: 6 ft: 6 corp: 4/28b lim: 122 exec/s: 0 rss: 27Mb L: 21/21 MS: 1 EraseBytes-
#12334 REDUCE cov: 6 ft: 6 corp: 4/23b lim: 122 exec/s: 0 rss: 27Mb L: 16/16 MS: 5 ShuffleBytes-InsertRepeatedBytes-ChangeByte-ChangeBit-EraseBytes-
#12335 REDUCE cov: 6 ft: 6 corp: 4/21b lim: 122 exec/s: 0 rss: 27Mb L: 14/14 MS: 1 EraseBytes-
#12582 REDUCE cov: 6 ft: 6 corp: 4/20b lim: 122 exec/s: 0 rss: 27Mb L: 13/13 MS: 2 ShuffleBytes-EraseBytes-
#12606 REDUCE cov: 6 ft: 6 corp: 4/15b lim: 122 exec/s: 0 rss: 27Mb L: 8/8 MS: 4 ChangeByte-ChangeByte-ChangeBit-EraseBytes-
#12629 REDUCE cov: 6 ft: 6 corp: 4/13b lim: 122 exec/s: 0 rss: 27Mb L: 6/6 MS: 3 InsertByte-EraseBytes-EraseBytes-
#12651 REDUCE cov: 6 ft: 6 corp: 4/11b lim: 122 exec/s: 0 rss: 27Mb L: 4/4 MS: 2 ChangeBit-EraseBytes-
#12811 REDUCE cov: 6 ft: 6 corp: 4/10b lim: 122 exec/s: 0 rss: 27Mb L: 3/3 MS: 5 InsertByte-InsertByte-ChangeByte-ChangeBit-EraseBytes-
#32469 REDUCE cov: 7 ft: 7 corp: 5/16b lim: 317 exec/s: 0 rss: 30Mb L: 6/6 MS: 3 ChangeBinInt-EraseBytes-CMP- DE: "Z\x00\x00\x00"-
#32660 REDUCE cov: 7 ft: 7 corp: 5/14b lim: 317 exec/s: 0 rss: 30Mb L: 4/4 MS: 1 EraseBytes-
=================================================================
==56857==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000099533 at pc 0x000102d7c05a bp 0x7ffeece84080 sp 0x7ffeece84078
READ of size 1 at 0x602000099533 thread T0
#0 0x102d7c059 in FuzzMe(unsigned char const*, unsigned long) fuzz_me.cc:9
#1 0x102d7c0ba in LLVMFuzzerTestOneInput fuzz_me.cc:13
#2 0x102d98020 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) FuzzerLoop.cpp:556
#3 0x102d97765 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool*) FuzzerLoop.cpp:470
#4 0x102d99966 in fuzzer::Fuzzer::MutateAndTestOne() FuzzerLoop.cpp:698
#5 0x102d9a665 in fuzzer::Fuzzer::Loop(std::__1::vector<fuzzer::SizedFile, fuzzer::fuzzer_allocator<fuzzer::SizedFile> >&) FuzzerLoop.cpp:830
#6 0x102d870cd in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) FuzzerDriver.cpp:829
#7 0x102db4f82 in main FuzzerMain.cpp:19
#8 0x7fff68ff2cc8 in start+0x0 (libdyld.dylib:x86_64+0x1acc8)
0x602000099533 is located 0 bytes to the right of 3-byte region [0x602000099530,0x602000099533)
allocated by thread T0 here:
#0 0x102e68cfd in wrap__Znam+0x7d (libclang_rt.asan_osx_dynamic.dylib:x86_64+0x50cfd)
#1 0x102d97f31 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) FuzzerLoop.cpp:541
#2 0x102d97765 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool*) FuzzerLoop.cpp:470
#3 0x102d99966 in fuzzer::Fuzzer::MutateAndTestOne() FuzzerLoop.cpp:698
#4 0x102d9a665 in fuzzer::Fuzzer::Loop(std::__1::vector<fuzzer::SizedFile, fuzzer::fuzzer_allocator<fuzzer::SizedFile> >&) FuzzerLoop.cpp:830
#5 0x102d870cd in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) FuzzerDriver.cpp:829
#6 0x102db4f82 in main FuzzerMain.cpp:19
#7 0x7fff68ff2cc8 in start+0x0 (libdyld.dylib:x86_64+0x1acc8)
SUMMARY: AddressSanitizer: heap-buffer-overflow fuzz_me.cc:9 in FuzzMe(unsigned char const*, unsigned long)
Shadow bytes around the buggy address:
0x1c0400013250: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
0x1c0400013260: fa fa fd fa fa fa fd fa fa fa fd fa fa fa fd fa
0x1c0400013270: fa fa fd fa fa fa fd fd fa fa fd fd fa fa fd fa
0x1c0400013280: fa fa fd fd fa fa fd fd fa fa fd fa fa fa fd fd
0x1c0400013290: fa fa fd fd fa fa fd fa fa fa fd fa fa fa fd fa
=>0x1c04000132a0: fa fa fd fa fa fa[03]fa fa fa fa fa fa fa fa fa
0x1c04000132b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x1c04000132c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x1c04000132d0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x1c04000132e0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x1c04000132f0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==56857==ABORTING
MS: 1 EraseBytes-; base unit: de3a753d4f1def197604865d76dba888d6aefc71
0x46,0x55,0x5a,
FUZ
artifact_prefix='./'; Test unit written to ./crash-0eb8e4ed029b774d80f2b66408203801cb982a60
Base64: RlVa
zsh: abort ./cmd/testdata/libfuzzer-darwin
...@@ -95,6 +95,12 @@ key2: value2" ...@@ -95,6 +95,12 @@ key2: value2"
/> />
</vulnerability-detail-stub> </vulnerability-detail-stub>
<!---->
<!---->
<!---->
<vulnerability-detail-stub <vulnerability-detail-stub
label="Identifiers" label="Identifiers"
> >
......
...@@ -33,6 +33,9 @@ describe('VulnerabilityDetails component', () => { ...@@ -33,6 +33,9 @@ describe('VulnerabilityDetails component', () => {
const findRequestHeaders = () => wrapper.find({ ref: 'requestHeaders' }); const findRequestHeaders = () => wrapper.find({ ref: 'requestHeaders' });
const findResponseHeaders = () => wrapper.find({ ref: 'responseHeaders' }); const findResponseHeaders = () => wrapper.find({ ref: 'responseHeaders' });
const findResponseStatusCode = () => wrapper.find({ ref: 'responseStatusCode' }); const findResponseStatusCode = () => wrapper.find({ ref: 'responseStatusCode' });
const findCrashState = () => wrapper.find({ ref: 'crashState' });
const findCrashAddress = () => wrapper.find({ ref: 'crashAddress' });
const findStacktraceSnippet = () => wrapper.find({ ref: 'stacktraceSnippet' });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
...@@ -155,6 +158,33 @@ describe('VulnerabilityDetails component', () => { ...@@ -155,6 +158,33 @@ describe('VulnerabilityDetails component', () => {
}); });
}); });
describe('with coverage fuzzing information', () => {
beforeEach(() => {
const vulnerability = makeVulnerability({
stacktrace_snippet: 'test snippet',
location: {
crash_address: '0x602000001573',
crash_state: 'FuzzMe\nstart\nstart+0x0\n\n',
crash_type: 'Heap-buffer-overflow\nREAD 1',
},
});
componentFactory(vulnerability);
});
it('renders a code-block containing the crash_state', () => {
expect(findCrashState().is(CodeBlock)).toBe(true);
expect(findCrashState().text()).toBe('FuzzMe\nstart\nstart+0x0');
});
it('renders crash_address', () => {
expect(findCrashAddress().exists()).toBe(true);
});
it('renders stacktrace_snippet', () => {
expect(findStacktraceSnippet().exists()).toBe(true);
});
});
describe('with request information', () => { describe('with request information', () => {
beforeEach(() => { beforeEach(() => {
const vulnerability = makeVulnerability({ const vulnerability = makeVulnerability({
......
...@@ -4,6 +4,6 @@ require 'spec_helper' ...@@ -4,6 +4,6 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['VulnerabilityReportType'] do RSpec.describe GitlabSchema.types['VulnerabilityReportType'] do
it 'exposes all vulnerability report types' do it 'exposes all vulnerability report types' do
expect(described_class.values.keys).to contain_exactly(*%w[SAST SECRET_DETECTION DAST CONTAINER_SCANNING DEPENDENCY_SCANNING]) expect(described_class.values.keys).to contain_exactly(*%w[SAST SECRET_DETECTION DAST CONTAINER_SCANNING DEPENDENCY_SCANNING COVERAGE_FUZZING])
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Parsers::Security::CoverageFuzzing do
let(:project) { artifact.project }
let(:pipeline) { artifact.job.pipeline }
let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline.sha, 2.weeks.ago) }
let(:parser) { described_class.new }
let(:artifact) { create(:ee_ci_job_artifact, :coverage_fuzzing) }
describe '#parse!' do
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.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::CoverageFuzzing)
expect(location).to have_attributes(
"crash_address": "0x602000001573",
"crash_state": "FuzzMe\nstart\nstart+0x0\n\n",
"crash_type": "Heap-buffer-overflow\nREAD 1")
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Ci::Reports::CoverageFuzzing::Report do
let(:report) { described_class.new }
describe '#add_crash' do
let(:crash) do
{
crash_address: "0x602000001573",
crash_type: "Heap-buffer-overflow\nREAD 1",
crash_state: "FuzzMe\nstart\nstart+0x0\n\n",
stacktrace_snippet: fixture_file('stacktrace_snippet.txt', dir: 'ee')
}
end
subject { report.add_crash(crash) }
it 'stores given crash params in the map' do
subject
expect(report.crashes.length).to eq(1)
expect(report.crashes[0].crash_address).to eq("0x602000001573")
expect(report.crashes[0].crash_type).to eq("Heap-buffer-overflow\nREAD 1")
expect(report.crashes[0].crash_state).to eq("FuzzMe\nstart\nstart+0x0\n\n")
end
end
end
...@@ -69,6 +69,22 @@ RSpec.describe EE::Ci::JobArtifact do ...@@ -69,6 +69,22 @@ RSpec.describe EE::Ci::JobArtifact do
end end
end end
describe '.coverage_fuzzing_reports' do
subject { Ci::JobArtifact.coverage_fuzzing }
context 'when there is a metrics report' do
let!(:artifact) { create(:ee_ci_job_artifact, :coverage_fuzzing) }
it { is_expected.to eq([artifact]) }
end
context 'when there is no coverage fuzzing reports' do
let!(:artifact) { create(:ee_ci_job_artifact, :trace) }
it { is_expected.to be_empty }
end
end
describe '.associated_file_types_for' do describe '.associated_file_types_for' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
......
...@@ -14,7 +14,8 @@ RSpec.describe Vulnerability do ...@@ -14,7 +14,8 @@ RSpec.describe Vulnerability do
dependency_scanning: 1, dependency_scanning: 1,
container_scanning: 2, container_scanning: 2,
dast: 3, dast: 3,
secret_detection: 4 } secret_detection: 4,
coverage_fuzzing: 5 }
end end
it { is_expected.to define_enum_for(:state).with_values(state_values) } it { is_expected.to define_enum_for(:state).with_values(state_values) }
......
...@@ -15,7 +15,7 @@ module Gitlab ...@@ -15,7 +15,7 @@ module Gitlab
%i[junit codequality sast secret_detection dependency_scanning container_scanning %i[junit codequality sast secret_detection dependency_scanning container_scanning
dast performance license_management license_scanning metrics lsif dast performance license_management license_scanning metrics lsif
dotenv cobertura terraform accessibility cluster_applications dotenv cobertura terraform accessibility cluster_applications
requirements].freeze requirements coverage_fuzzing].freeze
attributes ALLOWED_KEYS attributes ALLOWED_KEYS
...@@ -25,7 +25,8 @@ module Gitlab ...@@ -25,7 +25,8 @@ module Gitlab
with_options allow_nil: true do with_options allow_nil: true do
validates :junit, array_of_strings_or_string: true validates :junit, array_of_strings_or_string: true
validates :codequality, array_of_strings_or_string: true validates :coverage_fuzzing, array_of_strings_or_string: true
validates :sast, array_of_strings_or_string: true
validates :sast, array_of_strings_or_string: true validates :sast, array_of_strings_or_string: true
validates :secret_detection, array_of_strings_or_string: true validates :secret_detection, array_of_strings_or_string: true
validates :dependency_scanning, array_of_strings_or_string: true validates :dependency_scanning, array_of_strings_or_string: true
......
...@@ -6552,6 +6552,12 @@ msgstr "" ...@@ -6552,6 +6552,12 @@ msgstr ""
msgid "Coverage" msgid "Coverage"
msgstr "" msgstr ""
msgid "Coverage Fuzzing"
msgstr ""
msgid "Crash State"
msgstr ""
msgid "Create" msgid "Create"
msgstr "" msgstr ""
...@@ -10028,6 +10034,9 @@ msgstr "" ...@@ -10028,6 +10034,9 @@ msgstr ""
msgid "Find File" msgid "Find File"
msgstr "" msgstr ""
msgid "Find bugs in your code with coverage-guided fuzzing"
msgstr ""
msgid "Find by path" msgid "Find by path"
msgstr "" msgstr ""
...@@ -21553,6 +21562,9 @@ msgstr "" ...@@ -21553,6 +21562,9 @@ msgstr ""
msgid "Stack trace" msgid "Stack trace"
msgstr "" msgstr ""
msgid "Stacktrace snippet"
msgstr ""
msgid "Stage" msgid "Stage"
msgstr "" msgstr ""
...@@ -25597,6 +25609,9 @@ msgstr "" ...@@ -25597,6 +25609,9 @@ msgstr ""
msgid "Vulnerability|Class" msgid "Vulnerability|Class"
msgstr "" msgstr ""
msgid "Vulnerability|Crash Address"
msgstr ""
msgid "Vulnerability|Description" msgid "Vulnerability|Description"
msgstr "" msgstr ""
...@@ -26888,6 +26903,9 @@ msgstr "" ...@@ -26888,6 +26903,9 @@ msgstr ""
msgid "ciReport|Container scanning detects known vulnerabilities in your docker images." msgid "ciReport|Container scanning detects known vulnerabilities in your docker images."
msgstr "" msgstr ""
msgid "ciReport|Coverage Fuzzing"
msgstr ""
msgid "ciReport|Create a merge request to implement this solution, or download and apply the patch manually." msgid "ciReport|Create a merge request to implement this solution, or download and apply the patch manually."
msgstr "" msgstr ""
......
...@@ -39,7 +39,7 @@ RSpec.describe Ci::RetryBuildService do ...@@ -39,7 +39,7 @@ RSpec.describe Ci::RetryBuildService do
job_variables waiting_for_resource_at job_artifacts_metrics_referee job_variables waiting_for_resource_at job_artifacts_metrics_referee
job_artifacts_network_referee job_artifacts_dotenv job_artifacts_network_referee job_artifacts_dotenv
job_artifacts_cobertura needs job_artifacts_accessibility job_artifacts_cobertura needs job_artifacts_accessibility
job_artifacts_requirements].freeze job_artifacts_requirements job_artifacts_coverage_fuzzing].freeze
ignore_accessors = ignore_accessors =
%i[type lock_version target_url base_tags trace_sections %i[type lock_version target_url base_tags trace_sections
......
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