Commit 75b0feb5 authored by Sashi Kumar Kumaresan's avatar Sashi Kumar Kumaresan Committed by Mayra Cabrera

Add image filtering for vulnerabilities graphql query

parent 4090646c
---
name: vulnerability_location_image_filter
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69867
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340915
milestone: '14.4'
type: development
group: group::container security
default_enabled: false
...@@ -469,6 +469,7 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -469,6 +469,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="queryvulnerabilitieshasissues"></a>`hasIssues` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have linked issues. | | <a id="queryvulnerabilitieshasissues"></a>`hasIssues` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have linked issues. |
| <a id="queryvulnerabilitieshasresolution"></a>`hasResolution` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have been resolved on default branch. | | <a id="queryvulnerabilitieshasresolution"></a>`hasResolution` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have been resolved on default branch. |
| <a id="queryvulnerabilitiesimage"></a>`image` | [`[String!]`](#string) | Filter vulnerabilities by location image. When this filter is present, the response only matches entries for a `reportType` that includes `container_scanning`, `cluster_image_scanning`. |
| <a id="queryvulnerabilitiesprojectid"></a>`projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. | | <a id="queryvulnerabilitiesprojectid"></a>`projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. |
| <a id="queryvulnerabilitiesreporttype"></a>`reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. | | <a id="queryvulnerabilitiesreporttype"></a>`reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. |
| <a id="queryvulnerabilitiesscanner"></a>`scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. | | <a id="queryvulnerabilitiesscanner"></a>`scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. |
...@@ -10527,6 +10528,7 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -10527,6 +10528,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="groupvulnerabilitieshasissues"></a>`hasIssues` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have linked issues. | | <a id="groupvulnerabilitieshasissues"></a>`hasIssues` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have linked issues. |
| <a id="groupvulnerabilitieshasresolution"></a>`hasResolution` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have been resolved on default branch. | | <a id="groupvulnerabilitieshasresolution"></a>`hasResolution` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have been resolved on default branch. |
| <a id="groupvulnerabilitiesimage"></a>`image` | [`[String!]`](#string) | Filter vulnerabilities by location image. When this filter is present, the response only matches entries for a `reportType` that includes `container_scanning`, `cluster_image_scanning`. |
| <a id="groupvulnerabilitiesprojectid"></a>`projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. | | <a id="groupvulnerabilitiesprojectid"></a>`projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. |
| <a id="groupvulnerabilitiesreporttype"></a>`reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. | | <a id="groupvulnerabilitiesreporttype"></a>`reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. |
| <a id="groupvulnerabilitiesscanner"></a>`scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. | | <a id="groupvulnerabilitiesscanner"></a>`scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. |
...@@ -13203,6 +13205,7 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -13203,6 +13205,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="projectvulnerabilitieshasissues"></a>`hasIssues` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have linked issues. | | <a id="projectvulnerabilitieshasissues"></a>`hasIssues` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have linked issues. |
| <a id="projectvulnerabilitieshasresolution"></a>`hasResolution` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have been resolved on default branch. | | <a id="projectvulnerabilitieshasresolution"></a>`hasResolution` | [`Boolean`](#boolean) | Returns only the vulnerabilities which have been resolved on default branch. |
| <a id="projectvulnerabilitiesimage"></a>`image` | [`[String!]`](#string) | Filter vulnerabilities by location image. When this filter is present, the response only matches entries for a `reportType` that includes `container_scanning`, `cluster_image_scanning`. |
| <a id="projectvulnerabilitiesprojectid"></a>`projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. | | <a id="projectvulnerabilitiesprojectid"></a>`projectId` | [`[ID!]`](#id) | Filter vulnerabilities by project. |
| <a id="projectvulnerabilitiesreporttype"></a>`reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. | | <a id="projectvulnerabilitiesreporttype"></a>`reportType` | [`[VulnerabilityReportType!]`](#vulnerabilityreporttype) | Filter vulnerabilities by report type. |
| <a id="projectvulnerabilitiesscanner"></a>`scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. | | <a id="projectvulnerabilitiesscanner"></a>`scanner` | [`[String!]`](#string) | Filter vulnerabilities by VulnerabilityScanner.externalId. |
......
...@@ -9,10 +9,11 @@ ...@@ -9,10 +9,11 @@
# params: optional! a hash with one or more of the following: # params: optional! a hash with one or more of the following:
# project_ids: if `vulnerable` includes multiple projects (like a Group), this filter will restrict # project_ids: if `vulnerable` includes multiple projects (like a Group), this filter will restrict
# the vulnerabilities returned to those in the group's projects that also match these IDs # the vulnerabilities returned to those in the group's projects that also match these IDs
# image: only return vulnerabilities with these location images
# report_types: only return vulnerabilities from these report types # report_types: only return vulnerabilities from these report types
# severities: only return vulnerabilities with these severities # severities: only return vulnerabilities with these severities
# states: only return vulnerabilities in these states # states: only return vulnerabilities in these states
# has_resolution: only return vulnerabilities thah have resolution # has_resolution: only return vulnerabilities that have resolution
# has_issues: only return vulnerabilities that have issues linked # has_issues: only return vulnerabilities that have issues linked
# sort: return vulnerabilities ordered by severity_asc or severity_desc # sort: return vulnerabilities ordered by severity_asc or severity_desc
...@@ -22,11 +23,13 @@ module Security ...@@ -22,11 +23,13 @@ module Security
def initialize(vulnerable, params = {}) def initialize(vulnerable, params = {})
@params = params @params = params
@vulnerable = vulnerable
@vulnerabilities = vulnerable.vulnerabilities @vulnerabilities = vulnerable.vulnerabilities
end end
def execute def execute
filter_by_projects filter_by_projects
filter_by_image
filter_by_report_types filter_by_report_types
filter_by_severities filter_by_severities
filter_by_states filter_by_states
...@@ -40,7 +43,7 @@ module Security ...@@ -40,7 +43,7 @@ module Security
private private
attr_reader :params, :vulnerabilities attr_reader :params, :vulnerable, :vulnerabilities
def filter_by_projects def filter_by_projects
if params[:project_id].present? if params[:project_id].present?
...@@ -90,6 +93,15 @@ module Security ...@@ -90,6 +93,15 @@ module Security
end end
end end
def filter_by_image
# This filter will not work for InstanceSecurityDashboard, because InstanceSecurityDashboard could have multiple projects.
return if vulnerable.is_a?(InstanceSecurityDashboard)
if params[:image].present? && Feature.enabled?(:vulnerability_location_image_filter, vulnerable, default_enabled: :yaml)
@vulnerabilities = vulnerabilities.with_container_image(params[:image])
end
end
def sort(items) def sort(items)
items.order_by(params[:sort]) items.order_by(params[:sort])
end end
......
...@@ -43,6 +43,12 @@ module Resolvers ...@@ -43,6 +43,12 @@ module Resolvers
required: false, required: false,
description: 'Returns only the vulnerabilities which have linked issues.' description: 'Returns only the vulnerabilities which have linked issues.'
argument :image, [GraphQL::Types::String],
required: false,
description: "Filter vulnerabilities by location image. When this filter is present, "\
"the response only matches entries for a `reportType` "\
"that includes #{::Vulnerabilities::Finding::REPORT_TYPES_WITH_LOCATION_IMAGE.map { |type| "`#{type}`" }.join(', ')}."
def resolve(**args) def resolve(**args)
return Vulnerability.none unless vulnerable return Vulnerability.none unless vulnerable
......
...@@ -119,6 +119,9 @@ module EE ...@@ -119,6 +119,9 @@ module EE
scope :order_id_desc, -> { reorder(id: :desc) } scope :order_id_desc, -> { reorder(id: :desc) }
scope :with_limit, -> (maximum) { limit(maximum) } scope :with_limit, -> (maximum) { limit(maximum) }
scope :with_container_image, -> (images) do
joins(:findings).merge(Vulnerabilities::Finding.by_location_image(images))
end
delegate :scanner_name, :scanner_external_id, :scanner_id, :metadata, :message, :description, :details, delegate :scanner_name, :scanner_external_id, :scanner_id, :metadata, :message, :description, :details,
to: :finding, prefix: true, allow_nil: true to: :finding, prefix: true, allow_nil: true
......
...@@ -13,6 +13,7 @@ module Vulnerabilities ...@@ -13,6 +13,7 @@ module Vulnerabilities
FINDINGS_PER_PAGE = 20 FINDINGS_PER_PAGE = 20
MAX_NUMBER_OF_IDENTIFIERS = 20 MAX_NUMBER_OF_IDENTIFIERS = 20
REPORT_TYPES_WITH_LOCATION_IMAGE = %w[container_scanning cluster_image_scanning].freeze
paginates_per FINDINGS_PER_PAGE paginates_per FINDINGS_PER_PAGE
...@@ -95,6 +96,10 @@ module Vulnerabilities ...@@ -95,6 +96,10 @@ module Vulnerabilities
scope :scoped_project, -> { where('vulnerability_occurrences.project_id = projects.id') } scope :scoped_project, -> { where('vulnerability_occurrences.project_id = projects.id') }
scope :eager_load_vulnerability_flags, -> { includes(:vulnerability_flags) } scope :eager_load_vulnerability_flags, -> { includes(:vulnerability_flags) }
scope :by_location_image, -> (images) do
where(report_type: REPORT_TYPES_WITH_LOCATION_IMAGE)
.where("vulnerability_occurrences.location -> 'image' ?| array[:images]", images: images)
end
def self.for_pipelines(pipelines) def self.for_pipelines(pipelines)
joins(:finding_pipelines) joins(:finding_pipelines)
......
...@@ -520,6 +520,55 @@ FactoryBot.define do ...@@ -520,6 +520,55 @@ FactoryBot.define do
end end
end end
trait :with_cluster_image_scanning_scanning_metadata do
after(:build) do |finding, _|
finding.report_type = "cluster_image_scanning"
finding.name = "CVE-2017-16997 in libc"
finding.metadata_version = "2.3"
finding.location = {
"dependency": {
"package": {
"name": "glibc"
},
"version": "2.24-11+deb9u3"
},
"operating_system": "alpine 3.7",
"image": "alpine:3.7"
}
finding.raw_metadata = {
"category": "cluster_image_scanning",
"name": "CVE-2017-16997 in libc",
"severity": "high",
"solution": "Upgrade glibc from 2.24-11+deb9u3 to 2.24-11+deb9u4",
"scanner": {
"id": "starboard",
"name": "Starboard"
},
"location": {
"dependency": {
"package": {
"name": "glibc"
},
"version": "2.24-11+deb9u3"
},
"operating_system": "alpine 3.7",
"image": "alpine:3.7"
},
"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"
}
]
}.to_json
end
end
trait :identifier do trait :identifier do
after(:build) do |finding| after(:build) do |finding|
identifier = build( identifier = build(
......
...@@ -157,4 +157,46 @@ RSpec.describe Security::VulnerabilitiesFinder do ...@@ -157,4 +157,46 @@ RSpec.describe Security::VulnerabilitiesFinder do
is_expected.to contain_exactly(vulnerability4) is_expected.to contain_exactly(vulnerability4)
end end
end end
context 'when filtered by image' do
let_it_be(:cluster_vulnerability) { create(:vulnerability, :cluster_image_scanning, project: project) }
let_it_be(:finding) { create(:vulnerabilities_finding, :with_cluster_image_scanning_scanning_metadata, vulnerability: cluster_vulnerability) }
let(:filters) { { image: [finding.location['image']] } }
let(:feature_enabled) { true }
before do
stub_feature_flags(vulnerability_location_image_filter: feature_enabled)
end
context 'when vulnerability_location_image_filter is disabled' do
let(:feature_enabled) { false }
it 'does not include cluster vulnerability' do
is_expected.not_to contain_exactly(cluster_vulnerability)
end
end
context 'when vulnerability_location_image_filter is enabled' do
it 'only returns vulnerabilities matching the given image' do
is_expected.to contain_exactly(cluster_vulnerability)
end
context 'when different report_type is passed' do
let(:filters) { { report_type: %w[dast], image: [finding.location['image']] }}
it 'returns empty list' do
is_expected.to be_empty
end
end
end
context 'when vulnerable is InstanceSecurityDashboard' do
let(:vulnerable) { InstanceSecurityDashboard.new(project.users.first) }
it 'does not include cluster vulnerability' do
is_expected.not_to contain_exactly(cluster_vulnerability)
end
end
end
end end
...@@ -191,5 +191,24 @@ RSpec.describe Resolvers::VulnerabilitiesResolver do ...@@ -191,5 +191,24 @@ RSpec.describe Resolvers::VulnerabilitiesResolver do
end end
end end
end end
context 'when image is given' do
let_it_be(:cluster_vulnerability) { create(:vulnerability, :cluster_image_scanning, project: project) }
let_it_be(:cluster_finding) { create(:vulnerabilities_finding, :with_cluster_image_scanning_scanning_metadata, vulnerability: cluster_vulnerability) }
let(:params) { { image: [cluster_finding.location['image']] } }
it 'only returns vulnerabilities with given image' do
is_expected.to contain_exactly(cluster_vulnerability)
end
context 'when different report_type is given along with image' do
let(:params) { { report_type: %w[sast], image: [cluster_finding.location['image']] } }
it 'returns empty list' do
is_expected.to be_empty
end
end
end
end end
end end
...@@ -579,6 +579,28 @@ RSpec.describe Vulnerability do ...@@ -579,6 +579,28 @@ RSpec.describe Vulnerability do
it { is_expected.not_to match("gitlab-org/gitlab-foss/milestones/123") } it { is_expected.not_to match("gitlab-org/gitlab-foss/milestones/123") }
end end
describe '.with_container_image' do
let_it_be(:vulnerability) { create(:vulnerability, project: project, report_type: 'cluster_image_scanning') }
let_it_be(:finding) { create(:vulnerabilities_finding, :with_cluster_image_scanning_scanning_metadata, vulnerability: vulnerability) }
let_it_be(:image) { finding.location['image'] }
before do
finding_with_different_image = create(
:vulnerabilities_finding,
:with_cluster_image_scanning_scanning_metadata,
vulnerability: create(:vulnerability, report_type: 'cluster_image_scanning')
)
finding_with_different_image.location['image'] = 'alpine:latest'
finding_with_different_image.save!
end
subject(:cluster_vulnerabilities) { described_class.with_container_image(image) }
it 'returns vulnerabilities with given image' do
expect(cluster_vulnerabilities).to contain_exactly(vulnerability)
end
end
describe 'created_in_time_range' do describe 'created_in_time_range' do
it 'returns vulnerabilities created in given time range', :aggregate_failures do it 'returns vulnerabilities created in given time range', :aggregate_failures do
record1 = create(:vulnerability, created_at: 1.day.ago) record1 = create(:vulnerability, created_at: 1.day.ago)
......
...@@ -340,6 +340,30 @@ RSpec.describe Vulnerabilities::Finding do ...@@ -340,6 +340,30 @@ RSpec.describe Vulnerabilities::Finding do
end end
end end
describe '.by_location_image' do
let_it_be(:vulnerability) { create(:vulnerability, report_type: 'cluster_image_scanning') }
let_it_be(:finding) { create(:vulnerabilities_finding, :with_cluster_image_scanning_scanning_metadata, vulnerability: vulnerability) }
let_it_be(:image) { finding.location['image'] }
before do
finding_with_different_image = create(
:vulnerabilities_finding,
:with_cluster_image_scanning_scanning_metadata,
vulnerability: create(:vulnerability, report_type: 'cluster_image_scanning')
)
finding_with_different_image.location['image'] = 'alpine:latest'
finding_with_different_image.save!
create(:vulnerabilities_finding, report_type: :dast)
end
subject(:cluster_findings) { described_class.by_location_image(image) }
it 'returns findings with given image' do
expect(cluster_findings).to contain_exactly(finding)
end
end
describe '#false_positive?' do describe '#false_positive?' do
let_it_be(:finding) { create(:vulnerabilities_finding) } let_it_be(:finding) { create(:vulnerabilities_finding) }
let_it_be(:finding_with_fp) { create(:vulnerabilities_finding, vulnerability_flags: [create(:vulnerabilities_flag)]) } let_it_be(:finding_with_fp) { create(:vulnerabilities_finding, vulnerability_flags: [create(:vulnerabilities_flag)]) }
......
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