Commit 16b6fc8f authored by Alan (Maciej) Paruszewski's avatar Alan (Maciej) Paruszewski Committed by Tiger Watson

Add filtering for cluster agent in GraphQL Vulnerabilities API

parent 5c65060d
...@@ -511,6 +511,7 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -511,6 +511,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="queryvulnerabilitiesclusteragentid"></a>`clusterAgentId` | [`[ClustersAgentID!]`](#clustersagentid) | Filter vulnerabilities by `cluster_agent_id`. Vulnerabilities with a `reportType` of `cluster_image_scanning` are only included with this filter. |
| <a id="queryvulnerabilitiesclusterid"></a>`clusterId` | [`[ClustersClusterID!]`](#clustersclusterid) | Filter vulnerabilities by `cluster_id`. Vulnerabilities with a `reportType` of `cluster_image_scanning` are only included with this filter. | | <a id="queryvulnerabilitiesclusterid"></a>`clusterId` | [`[ClustersClusterID!]`](#clustersclusterid) | Filter vulnerabilities by `cluster_id`. Vulnerabilities with a `reportType` of `cluster_image_scanning` are only included with this filter. |
| <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. |
...@@ -10995,6 +10996,7 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -10995,6 +10996,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="groupvulnerabilitiesclusteragentid"></a>`clusterAgentId` | [`[ClustersAgentID!]`](#clustersagentid) | Filter vulnerabilities by `cluster_agent_id`. Vulnerabilities with a `reportType` of `cluster_image_scanning` are only included with this filter. |
| <a id="groupvulnerabilitiesclusterid"></a>`clusterId` | [`[ClustersClusterID!]`](#clustersclusterid) | Filter vulnerabilities by `cluster_id`. Vulnerabilities with a `reportType` of `cluster_image_scanning` are only included with this filter. | | <a id="groupvulnerabilitiesclusterid"></a>`clusterId` | [`[ClustersClusterID!]`](#clustersclusterid) | Filter vulnerabilities by `cluster_id`. Vulnerabilities with a `reportType` of `cluster_image_scanning` are only included with this filter. |
| <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. |
...@@ -13808,6 +13810,7 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -13808,6 +13810,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="projectvulnerabilitiesclusteragentid"></a>`clusterAgentId` | [`[ClustersAgentID!]`](#clustersagentid) | Filter vulnerabilities by `cluster_agent_id`. Vulnerabilities with a `reportType` of `cluster_image_scanning` are only included with this filter. |
| <a id="projectvulnerabilitiesclusterid"></a>`clusterId` | [`[ClustersClusterID!]`](#clustersclusterid) | Filter vulnerabilities by `cluster_id`. Vulnerabilities with a `reportType` of `cluster_image_scanning` are only included with this filter. | | <a id="projectvulnerabilitiesclusterid"></a>`clusterId` | [`[ClustersClusterID!]`](#clustersclusterid) | Filter vulnerabilities by `cluster_id`. Vulnerabilities with a `reportType` of `cluster_image_scanning` are only included with this filter. |
| <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. |
...@@ -177,6 +177,7 @@ You can [configure](#customize-the-cluster-image-scanning-settings) analyzers by ...@@ -177,6 +177,7 @@ You can [configure](#customize-the-cluster-image-scanning-settings) analyzers by
| `CIS_RESOURCE_NAMESPACE` | `""` | Namespace of the Kubernetes resource you want to filter vulnerabilities for. For example, `production`. | | `CIS_RESOURCE_NAMESPACE` | `""` | Namespace of the Kubernetes resource you want to filter vulnerabilities for. For example, `production`. |
| `CIS_RESOURCE_KIND` | `""` | Kind of the Kubernetes resource you want to filter vulnerabilities for. For example, `deployment`. | | `CIS_RESOURCE_KIND` | `""` | Kind of the Kubernetes resource you want to filter vulnerabilities for. For example, `deployment`. |
| `CIS_CLUSTER_IDENTIFIER` | `""` | ID of the Kubernetes cluster integrated with GitLab. This is used to map vulnerabilities to the cluster so they can be filtered in the Vulnerability Report page. | | `CIS_CLUSTER_IDENTIFIER` | `""` | ID of the Kubernetes cluster integrated with GitLab. This is used to map vulnerabilities to the cluster so they can be filtered in the Vulnerability Report page. |
| `CIS_CLUSTER_AGENT_IDENTIFIER` | `""` | ID of the Kubernetes cluster agent integrated with GitLab. This maps vulnerabilities to the agent so they can be filtered in the Vulnerability Report page. |
#### Override the cluster image scanning template #### Override the cluster image scanning template
......
...@@ -38,6 +38,7 @@ module Security ...@@ -38,6 +38,7 @@ module Security
filter_by_resolution filter_by_resolution
filter_by_issues filter_by_issues
filter_by_cluster_id filter_by_cluster_id
filter_by_cluster_agent_id
sort(vulnerabilities) sort(vulnerabilities)
end end
...@@ -109,6 +110,12 @@ module Security ...@@ -109,6 +110,12 @@ module Security
end end
end end
def filter_by_cluster_agent_id
if params[:cluster_agent_id].present?
@vulnerabilities = vulnerabilities.with_cluster_agent_ids(params[:cluster_agent_id])
end
end
def sort(items) def sort(items)
items.order_by(params[:sort]) items.order_by(params[:sort])
end end
......
...@@ -56,6 +56,12 @@ module Resolvers ...@@ -56,6 +56,12 @@ module Resolvers
description: "Filter vulnerabilities by `cluster_id`. Vulnerabilities with a `reportType` "\ description: "Filter vulnerabilities by `cluster_id`. Vulnerabilities with a `reportType` "\
"of `cluster_image_scanning` are only included with this filter." "of `cluster_image_scanning` are only included with this filter."
argument :cluster_agent_id, [::Types::GlobalIDType[::Clusters::Agent]],
prepare: ->(ids, _) { ids.map(&:model_id) },
required: false,
description: "Filter vulnerabilities by `cluster_agent_id`. Vulnerabilities with a `reportType` "\
"of `cluster_image_scanning` are only included with this filter."
def resolve_with_lookahead(**args) def resolve_with_lookahead(**args)
return Vulnerability.none unless vulnerable return Vulnerability.none unless vulnerable
......
...@@ -123,6 +123,9 @@ module EE ...@@ -123,6 +123,9 @@ module EE
scope :with_cluster_ids, -> (cluster_ids) do scope :with_cluster_ids, -> (cluster_ids) do
joins(:findings).merge(Vulnerabilities::Finding.by_location_cluster(cluster_ids)) joins(:findings).merge(Vulnerabilities::Finding.by_location_cluster(cluster_ids))
end end
scope :with_cluster_agent_ids, -> (agent_ids) do
joins(:findings).merge(Vulnerabilities::Finding.by_location_cluster_agent(agent_ids))
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
......
...@@ -105,6 +105,10 @@ module Vulnerabilities ...@@ -105,6 +105,10 @@ module Vulnerabilities
where(report_type: 'cluster_image_scanning') where(report_type: 'cluster_image_scanning')
.where("vulnerability_occurrences.location -> 'cluster_id' ?| array[:cluster_ids]", cluster_ids: cluster_ids) .where("vulnerability_occurrences.location -> 'cluster_id' ?| array[:cluster_ids]", cluster_ids: cluster_ids)
end end
scope :by_location_cluster_agent, -> (agent_ids) do
where(report_type: 'cluster_image_scanning')
.where("vulnerability_occurrences.location -> 'agent_id' ?| array[:agent_ids]", agent_ids: agent_ids)
end
def self.counted_by_severity def self.counted_by_severity
group(:severity).count.transform_keys do |severity| group(:severity).count.transform_keys do |severity|
......
...@@ -11,6 +11,7 @@ module Gitlab ...@@ -11,6 +11,7 @@ module Gitlab
::Gitlab::Ci::Reports::Security::Locations::ClusterImageScanning.new( ::Gitlab::Ci::Reports::Security::Locations::ClusterImageScanning.new(
image: location_data['image'], image: location_data['image'],
cluster_id: location_data.dig('kubernetes_resource', 'cluster_id'), cluster_id: location_data.dig('kubernetes_resource', 'cluster_id'),
agent_id: location_data.dig('kubernetes_resource', 'agent_id'),
operating_system: location_data['operating_system'], operating_system: location_data['operating_system'],
package_name: location_data.dig('dependency', 'package', 'name'), package_name: location_data.dig('dependency', 'package', 'name'),
package_version: location_data.dig('dependency', 'version')) package_version: location_data.dig('dependency', 'version'))
......
...@@ -6,9 +6,9 @@ module Gitlab ...@@ -6,9 +6,9 @@ module Gitlab
module Security module Security
module Locations module Locations
class ClusterImageScanning < ContainerScanning class ClusterImageScanning < ContainerScanning
attr_reader :cluster_id attr_reader :cluster_id, :agent_id
def initialize(image:, operating_system:, package_name: nil, package_version: nil, cluster_id: nil) def initialize(image:, operating_system:, package_name: nil, package_version: nil, cluster_id: nil, agent_id: nil)
super( super(
image: image, image: image,
operating_system: operating_system, operating_system: operating_system,
...@@ -16,6 +16,13 @@ module Gitlab ...@@ -16,6 +16,13 @@ module Gitlab
package_version: package_version package_version: package_version
) )
@cluster_id = cluster_id @cluster_id = cluster_id
@agent_id = agent_id
end
def fingerprint_data
return super if agent_id.blank?
"AGENT_#{agent_id}:#{super}"
end end
end end
end end
......
...@@ -533,7 +533,8 @@ FactoryBot.define do ...@@ -533,7 +533,8 @@ FactoryBot.define do
}, },
"operating_system": "alpine 3.7", "operating_system": "alpine 3.7",
"image": "alpine:3.7", "image": "alpine:3.7",
"cluster_id": "1" "cluster_id": "1",
"agent_id": "46357"
} }
finding.raw_metadata = { finding.raw_metadata = {
"category": "cluster_image_scanning", "category": "cluster_image_scanning",
...@@ -553,7 +554,8 @@ FactoryBot.define do ...@@ -553,7 +554,8 @@ FactoryBot.define do
}, },
"operating_system": "alpine 3.7", "operating_system": "alpine 3.7",
"image": "alpine:3.7", "image": "alpine:3.7",
"cluster_id": "1" "cluster_id": "1",
"agent_id": "46357"
}, },
"identifiers": [{ "identifiers": [{
"type": "cve", "type": "cve",
......
...@@ -218,4 +218,23 @@ RSpec.describe Security::VulnerabilitiesFinder do ...@@ -218,4 +218,23 @@ RSpec.describe Security::VulnerabilitiesFinder do
end end
end end
end end
context 'when filtered by cluster_agent_id' 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) { { cluster_agent_id: [finding.location['agent_id']] } }
it 'only returns vulnerabilities matching the given agent_id' do
is_expected.to contain_exactly(cluster_vulnerability)
end
context 'when different report_type is passed' do
let(:filters) { { report_type: %w[dast], cluster_agent_id: [finding.location['agent_id']] }}
it 'returns empty list' do
is_expected.to be_empty
end
end
end
end end
...@@ -27,7 +27,8 @@ ...@@ -27,7 +27,8 @@
"name":"sample-app", "name":"sample-app",
"kind":"ReplicaSet", "kind":"ReplicaSet",
"container_name":"webgoat", "container_name":"webgoat",
"cluster_id":"1" "cluster_id":"1",
"agent_id":"46357"
} }
}, },
"identifiers": [ "identifiers": [
...@@ -70,7 +71,8 @@ ...@@ -70,7 +71,8 @@
"name":"sample-app", "name":"sample-app",
"kind":"ReplicaSet", "kind":"ReplicaSet",
"container_name":"webgoat", "container_name":"webgoat",
"cluster_id":"1" "cluster_id":"1",
"agent_id":"46357"
} }
}, },
"identifiers": [ "identifiers": [
......
...@@ -230,5 +230,25 @@ RSpec.describe Resolvers::VulnerabilitiesResolver do ...@@ -230,5 +230,25 @@ RSpec.describe Resolvers::VulnerabilitiesResolver do
end end
end end
end end
context 'when cluster_agent_id 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_it_be(:cluster_gid) { ::Gitlab::GlobalId.as_global_id(cluster_finding.location['agent_id'].to_i, model_name: 'Clusters::Cluster') }
let(:params) { { cluster_agent_id: [cluster_gid] } }
it 'only returns vulnerabilities with given cluster' do
is_expected.to contain_exactly(cluster_vulnerability)
end
context 'when different report_type is given along with cluster' do
let(:params) { { report_type: %w[sast], cluster_agent_id: [cluster_gid] } }
it 'returns empty list' do
is_expected.to be_empty
end
end
end
end end
end end
...@@ -29,6 +29,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::ClusterImageScanning do ...@@ -29,6 +29,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::ClusterImageScanning do
expect(location).to have_attributes( expect(location).to have_attributes(
image: image, image: image,
cluster_id: '1', cluster_id: '1',
agent_id: '46357',
operating_system: 'debian:9', operating_system: 'debian:9',
package_name: 'glibc', package_name: 'glibc',
package_version: '2.24-11+deb9u3' package_version: '2.24-11+deb9u3'
......
...@@ -3,19 +3,39 @@ ...@@ -3,19 +3,39 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Ci::Reports::Security::Locations::ClusterImageScanning do RSpec.describe Gitlab::Ci::Reports::Security::Locations::ClusterImageScanning do
let(:params) do context 'when cluster_id is provided in finding' do
{ let(:params) do
image: 'registry.gitlab.com/my/project:latest', {
cluster_id: '1', image: 'registry.gitlab.com/my/project:latest',
operating_system: 'debian:9', cluster_id: '1',
package_name: 'glibc', operating_system: 'debian:9',
package_version: '1.2.3' package_name: 'glibc',
} package_version: '1.2.3'
}
end
let(:mandatory_params) { %i[image operating_system] }
let(:expected_fingerprint) { Digest::SHA1.hexdigest('registry.gitlab.com/my/project:glibc') }
let(:expected_fingerprint_path) { 'registry.gitlab.com/my/project:glibc' }
it_behaves_like 'vulnerability location'
end end
let(:mandatory_params) { %i[image operating_system] } context 'when agent id is provided in finding' do
let(:expected_fingerprint) { Digest::SHA1.hexdigest('registry.gitlab.com/my/project:glibc') } let(:params) do
let(:expected_fingerprint_path) { 'registry.gitlab.com/my/project:glibc' } {
image: 'registry.gitlab.com/my/project:latest',
agent_id: '46357',
operating_system: 'debian:9',
package_name: 'glibc',
package_version: '1.2.3'
}
end
it_behaves_like 'vulnerability location' let(:mandatory_params) { %i[image operating_system] }
let(:expected_fingerprint) { Digest::SHA1.hexdigest('AGENT_46357:registry.gitlab.com/my/project:glibc') }
let(:expected_fingerprint_path) { 'AGENT_46357:registry.gitlab.com/my/project:glibc' }
it_behaves_like 'vulnerability location'
end
end end
...@@ -631,6 +631,36 @@ RSpec.describe Vulnerability do ...@@ -631,6 +631,36 @@ RSpec.describe Vulnerability do
end end
end end
describe '.with_cluster_agent_ids' 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(:cluster_agent_ids) { [finding.location['agent_id']] }
before do
finding_with_different_agent_id = create(
:vulnerabilities_finding,
:with_cluster_image_scanning_scanning_metadata,
vulnerability: create(:vulnerability, report_type: 'cluster_image_scanning')
)
finding_with_different_agent_id.location['agent_id'] = '2'
finding_with_different_agent_id.save!
finding_without_agent_id = create(
:vulnerabilities_finding,
:with_cluster_image_scanning_scanning_metadata,
vulnerability: create(:vulnerability, report_type: 'cluster_image_scanning')
)
finding_without_agent_id.location['agent_id'] = nil
finding_without_agent_id.save!
end
subject(:cluster_agent_vulnerabilities) { described_class.with_cluster_agent_ids(cluster_agent_ids) }
it 'returns vulnerabilities with given agent_id' do
expect(cluster_agent_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)
......
...@@ -387,6 +387,30 @@ RSpec.describe Vulnerabilities::Finding do ...@@ -387,6 +387,30 @@ RSpec.describe Vulnerabilities::Finding do
end end
end end
describe '.by_location_cluster_agent' 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(:agent_ids) { [finding.location['agent_id']] }
before do
finding_with_different_agent_id = create(
:vulnerabilities_finding,
:with_cluster_image_scanning_scanning_metadata,
vulnerability: create(:vulnerability, report_type: 'cluster_image_scanning')
)
finding_with_different_agent_id.location['agent_id'] = '2'
finding_with_different_agent_id.save!
create(:vulnerabilities_finding, report_type: :dast)
end
subject(:cluster_findings) { described_class.by_location_cluster_agent(agent_ids) }
it 'returns findings with given agent_id' 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