Commit 0b5598be authored by Clement Ho's avatar Clement Ho

Merge branch '208847-location-type' into 'master'

Create VulnerabilityLocationType and supporting types

See merge request gitlab-org/gitlab!30129
parents f55f20a8 d2e5ad7b
......@@ -10669,10 +10669,9 @@ type Vulnerability {
id: ID!
"""
The JSON location metadata for the vulnerability. Its format depends on the
type of the security scan that found the vulnerability
Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability
"""
location: JSON
location: VulnerabilityLocation
"""
The project on which the vulnerability was found
......@@ -10745,6 +10744,101 @@ type VulnerabilityEdge {
node: Vulnerability
}
"""
Represents a vulnerability location. The fields with data will depend on the vulnerability report type
"""
union VulnerabilityLocation = VulnerabilityLocationContainerScanning | VulnerabilityLocationDast | VulnerabilityLocationDependencyScanning | VulnerabilityLocationSast
"""
Represents the location of a vulnerability found by a container security scan
"""
type VulnerabilityLocationContainerScanning {
"""
Dependency containing the vulnerability
"""
dependency: VulnerableDependency
"""
Name of the vulnerable container image
"""
image: String
"""
Operating system that runs on the vulnerable container image
"""
operatingSystem: String
}
"""
Represents the location of a vulnerability found by a DAST scan
"""
type VulnerabilityLocationDast {
"""
Domain name of the vulnerable request
"""
hostname: String
"""
Query parameter for the URL on which the vulnerability occurred
"""
param: String
"""
URL path and query string of the vulnerable request
"""
path: String
"""
HTTP method of the vulnerable request
"""
requestMethod: String
}
"""
Represents the location of a vulnerability found by a dependency security scan
"""
type VulnerabilityLocationDependencyScanning {
"""
Dependency containing the vulnerability
"""
dependency: VulnerableDependency
"""
Path to the vulnerable file
"""
file: String
}
"""
Represents the location of a vulnerability found by a SAST scan
"""
type VulnerabilityLocationSast {
"""
Number of the last relevant line in the vulnerable file
"""
endLine: String
"""
Path to the vulnerable file
"""
file: String
"""
Number of the first relevant line in the vulnerable file
"""
startLine: String
"""
Class containing the vulnerability
"""
vulnerableClass: String
"""
Method containing the vulnerability
"""
vulnerableMethod: String
}
"""
Check permissions for the current user on a vulnerability
"""
......@@ -10856,3 +10950,28 @@ enum VulnerabilityState {
DISMISSED
RESOLVED
}
"""
Represents a vulnerable dependency. Used in vulnerability location data
"""
type VulnerableDependency {
"""
The package associated with the vulnerable dependency
"""
package: VulnerablePackage
"""
The version of the vulnerable dependency
"""
version: String
}
"""
Represents a vulnerable package. Used in vulnerability dependency data
"""
type VulnerablePackage {
"""
The name of the vulnerable package
"""
name: String
}
\ No newline at end of file
......@@ -1632,7 +1632,7 @@ Represents a vulnerability.
| --- | ---- | ---------- |
| `description` | String | Description of the vulnerability |
| `id` | ID! | GraphQL ID of the vulnerability |
| `location` | JSON | The JSON location metadata for the vulnerability. Its format depends on the type of the 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 |
| `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) |
| `severity` | VulnerabilitySeverity | Severity of the vulnerability (INFO, UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL) |
......@@ -1641,6 +1641,48 @@ Represents a vulnerability.
| `userPermissions` | VulnerabilityPermissions! | Permissions for the current user on the resource |
| `vulnerabilityPath` | String | URL to the vulnerability's details page |
## VulnerabilityLocationContainerScanning
Represents the location of a vulnerability found by a container security scan
| Name | Type | Description |
| --- | ---- | ---------- |
| `dependency` | VulnerableDependency | Dependency containing the vulnerability |
| `image` | String | Name of the vulnerable container image |
| `operatingSystem` | String | Operating system that runs on the vulnerable container image |
## VulnerabilityLocationDast
Represents the location of a vulnerability found by a DAST scan
| Name | Type | Description |
| --- | ---- | ---------- |
| `hostname` | String | Domain name of the vulnerable request |
| `param` | String | Query parameter for the URL on which the vulnerability occurred |
| `path` | String | URL path and query string of the vulnerable request |
| `requestMethod` | String | HTTP method of the vulnerable request |
## VulnerabilityLocationDependencyScanning
Represents the location of a vulnerability found by a dependency security scan
| Name | Type | Description |
| --- | ---- | ---------- |
| `dependency` | VulnerableDependency | Dependency containing the vulnerability |
| `file` | String | Path to the vulnerable file |
## VulnerabilityLocationSast
Represents the location of a vulnerability found by a SAST scan
| Name | Type | Description |
| --- | ---- | ---------- |
| `endLine` | String | Number of the last relevant line in the vulnerable file |
| `file` | String | Path to the vulnerable file |
| `startLine` | String | Number of the first relevant line in the vulnerable file |
| `vulnerableClass` | String | Class containing the vulnerability |
| `vulnerableMethod` | String | Method containing the vulnerability |
## VulnerabilityPermissions
Check permissions for the current user on a vulnerability
......@@ -1668,3 +1710,20 @@ Represents vulnerability counts by severity
| `low` | Int | Number of vulnerabilities of LOW severity of the project |
| `medium` | Int | Number of vulnerabilities of MEDIUM severity of the project |
| `unknown` | Int | Number of vulnerabilities of UNKNOWN severity of the project |
## VulnerableDependency
Represents a vulnerable dependency. Used in vulnerability location data
| Name | Type | Description |
| --- | ---- | ---------- |
| `package` | VulnerablePackage | The package associated with the vulnerable dependency |
| `version` | String | The version of the vulnerable dependency |
## VulnerablePackage
Represents a vulnerable package. Used in vulnerability dependency data
| Name | Type | Description |
| --- | ---- | ---------- |
| `name` | String | The name of the vulnerable package |
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
import FirstClassProjectSecurityDashboard from './components/first_class_project_security_dashboard.vue';
import FirstClassGroupSecurityDashboard from './components/first_class_group_security_dashboard.vue';
......@@ -11,6 +9,7 @@ import createRouter from './store/router';
import projectsPlugin from './store/plugins/projects';
import projectSelector from './store/plugins/project_selector';
import syncWithRouter from './store/plugins/sync_with_router';
import apolloProvider from './graphql/provider';
const isRequired = message => {
throw new Error(message);
......@@ -36,12 +35,6 @@ export default (
});
}
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
const props = {
emptyStateSvgPath: el.dataset.emptyStateSvgPath,
dashboardDocumentation: el.dataset.dashboardDocumentation,
......
{"__schema":{"types":[{"kind":"UNION","name":"VulnerabilityLocation","possibleTypes":[{"name":"VulnerabilityLocationContainerScanning"},{"name":"VulnerabilityLocationDast"},{"name":"VulnerabilityLocationDependencyScanning"},{"name":"VulnerabilityLocationSast"}]}]}}
\ No newline at end of file
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import createDefaultClient from '~/lib/graphql';
import introspectionQueryResultData from './fragmentTypes.json';
Vue.use(VueApollo);
// We create a fragment matcher so that we can create a fragment from an interface
// Without this, Apollo throws a heuristic fragment matcher warning
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData,
});
const defaultClient = createDefaultClient(
{},
{
cacheConfig: {
fragmentMatcher,
},
},
);
export default new VueApollo({
defaultClient,
});
......@@ -111,6 +111,9 @@ export default {
return acc;
}, {});
},
shouldShowVulnerabilityPath(item) {
return Boolean(item.location.image || item.location.file);
},
toggleAllVulnerabilities() {
if (this.hasSelectedAllVulnerabilities) {
this.deselectAllVulnerabilities();
......@@ -127,14 +130,6 @@ export default {
this.$set(this.selectedVulnerabilities, `${vulnerability.id}`, vulnerability);
}
},
parseLocation(item) {
try {
const parsed = JSON.parse(item.location);
return `${parsed.image || parsed.file}`;
} catch (e) {
return '';
}
},
},
VULNERABILITIES_PER_PAGE,
};
......@@ -187,8 +182,8 @@ export default {
<div v-if="shouldShowProjectNamespace">
{{ item.project.nameWithNamespace }}
</div>
<div class="monospace">
{{ parseLocation(item) }}
<div v-if="shouldShowVulnerabilityPath(item)" class="monospace">
{{ item.location.image || item.location.file }}
</div>
</div>
<remediated-badge v-if="item.resolved_on_default_branch" class="ml-2" />
......
......@@ -4,7 +4,17 @@ fragment Vulnerability on Vulnerability {
state
severity
vulnerabilityPath
location
location {
... on VulnerabilityLocationContainerScanning {
image
}
... on VulnerabilityLocationDependencyScanning {
file
}
... on VulnerabilityLocationSast {
file
}
}
project {
nameWithNamespace
}
......
# frozen_string_literal: true
module Types
module VulnerabilityLocation
# rubocop: disable Graphql/AuthorizeTypes
class ContainerScanningType < BaseObject
graphql_name 'VulnerabilityLocationContainerScanning'
description 'Represents the location of a vulnerability found by a container security scan'
field :dependency, ::Types::VulnerableDependencyType, null: true,
description: 'Dependency containing the vulnerability'
field :image, GraphQL::STRING_TYPE, null: true,
description: 'Name of the vulnerable container image'
field :operating_system, GraphQL::STRING_TYPE, null: true,
description: 'Operating system that runs on the vulnerable container image'
end
end
end
# frozen_string_literal: true
module Types
module VulnerabilityLocation
# rubocop: disable Graphql/AuthorizeTypes
class DastType < BaseObject
graphql_name 'VulnerabilityLocationDast'
description 'Represents the location of a vulnerability found by a DAST scan'
field :hostname, GraphQL::STRING_TYPE, null: true,
description: 'Domain name of the vulnerable request'
field :param, GraphQL::STRING_TYPE, null: true,
description: 'Query parameter for the URL on which the vulnerability occurred'
field :path, GraphQL::STRING_TYPE, null: true,
description: 'URL path and query string of the vulnerable request'
field :request_method, GraphQL::STRING_TYPE, null: true,
description: 'HTTP method of the vulnerable request'
end
end
end
# frozen_string_literal: true
module Types
module VulnerabilityLocation
# rubocop: disable Graphql/AuthorizeTypes
class DependencyScanningType < BaseObject
graphql_name 'VulnerabilityLocationDependencyScanning'
description 'Represents the location of a vulnerability found by a dependency security scan'
field :dependency, ::Types::VulnerableDependencyType, null: true,
description: 'Dependency containing the vulnerability'
field :file, GraphQL::STRING_TYPE, null: true,
description: 'Path to the vulnerable file'
end
end
end
# frozen_string_literal: true
module Types
module VulnerabilityLocation
# rubocop: disable Graphql/AuthorizeTypes
class SastType < BaseObject
graphql_name 'VulnerabilityLocationSast'
description 'Represents the location of a vulnerability found by a SAST scan'
field :vulnerable_class, GraphQL::STRING_TYPE, null: true,
description: 'Class containing the vulnerability',
hash_key: :class
field :end_line, GraphQL::STRING_TYPE, null: true,
description: 'Number of the last relevant line in the vulnerable file'
field :file, GraphQL::STRING_TYPE, null: true,
description: 'Path to the vulnerable file'
field :vulnerable_method, GraphQL::STRING_TYPE, null: true,
description: 'Method containing the vulnerability',
hash_key: :method
field :start_line, GraphQL::STRING_TYPE, null: true,
description: 'Number of the first relevant line in the vulnerable file'
end
end
end
# frozen_string_literal: true
module Types
class VulnerabilityLocationType < BaseUnion
UnexpectedReportType = Class.new(StandardError)
graphql_name 'VulnerabilityLocation'
description 'Represents a vulnerability location. The fields with data will depend on the vulnerability report type'
possible_types VulnerabilityLocation::ContainerScanningType,
VulnerabilityLocation::DependencyScanningType,
VulnerabilityLocation::DastType,
VulnerabilityLocation::SastType
def self.resolve_type(object, context)
case object[:report_type]
when 'container_scanning'
VulnerabilityLocation::ContainerScanningType
when 'dependency_scanning'
VulnerabilityLocation::DependencyScanningType
when 'dast'
VulnerabilityLocation::DastType
when 'sast'
VulnerabilityLocation::SastType
else
raise UnexpectedReportType, "Report type must be one of #{::Vulnerabilities::Occurrence::REPORT_TYPES.keys}"
end
end
end
end
......@@ -31,9 +31,9 @@ module Types
description: "URL to the vulnerability's details page",
resolve: -> (obj, _args, _ctx) { ::Gitlab::Routing.url_helpers.project_security_vulnerability_path(obj.project, obj) }
field :location, GraphQL::Types::JSON, null: true,
description: 'The JSON location metadata for the vulnerability. Its format depends on the type of the security scan that found the vulnerability',
resolve: -> (obj, _args, _ctx) { obj.finding&.location.to_json }
field :location, VulnerabilityLocationType, null: true,
description: 'Location metadata for the vulnerability. Its fields depend on the type of security scan that found the vulnerability',
resolve: -> (obj, _args, _ctx) { obj.finding&.location&.merge(report_type: obj.report_type) }
field :project, ::Types::ProjectType, null: true,
description: 'The project on which the vulnerability was found',
......
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class VulnerableDependencyType < BaseObject
graphql_name 'VulnerableDependency'
description 'Represents a vulnerable dependency. Used in vulnerability location data'
field :package, ::Types::VulnerablePackageType, null: true,
description: 'The package associated with the vulnerable dependency'
field :version, GraphQL::STRING_TYPE, null: true,
description: 'The version of the vulnerable dependency'
end
end
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class VulnerablePackageType < BaseObject
graphql_name 'VulnerablePackage'
description 'Represents a vulnerable package. Used in vulnerability dependency data'
field :name, GraphQL::STRING_TYPE, null: true,
description: 'The name of the vulnerable package'
end
end
......@@ -4,10 +4,10 @@ export const generateVulnerabilities = () => [
title: 'Vulnerability 1',
severity: 'critical',
state: 'dismissed',
location: JSON.stringify({
location: {
image:
'registry.gitlab.com/groulot/container-scanning-test/master:5f21de6956aee99ddb68ae49498662d9872f50ff',
}),
},
project: {
nameWithNamespace: 'Administrator / Security reports',
},
......@@ -17,9 +17,9 @@ export const generateVulnerabilities = () => [
title: 'Vulnerability 2',
severity: 'high',
state: 'opened',
location: JSON.stringify({
location: {
file: 'src/main/java/com/gitlab/security_products/tests/App.java',
}),
},
project: {
nameWithNamespace: 'Administrator / Vulnerability reports',
},
......
......@@ -99,15 +99,11 @@ describe('Vulnerability list component', () => {
});
describe('when displayed on instance or group level dashboard', () => {
let newVulnerabilities;
beforeEach(() => {
newVulnerabilities = generateVulnerabilities();
it('should display the vulnerability locations', () => {
const newVulnerabilities = generateVulnerabilities();
wrapper = createWrapper({
props: { vulnerabilities: newVulnerabilities, shouldShowProjectNamespace: true },
});
});
it('should display the vulnerability locations', () => {
expect(findLocation(newVulnerabilities[0].id).text()).toContain(
'Administrator / Security reports',
);
......@@ -121,6 +117,28 @@ describe('Vulnerability list component', () => {
'src/main/java/com/gitlab/security_products/tests/App.java',
);
});
it('should not display the vulnerability locations', () => {
const vulnerabilityWithoutLocation = [
{
id: 'id_0',
title: 'Vulnerability 1',
severity: 'critical',
state: 'dismissed',
location: {},
project: {
nameWithNamespace: 'Administrator / Security reports',
},
},
];
wrapper = createWrapper({
props: { vulnerabilities: vulnerabilityWithoutLocation, shouldShowProjectNamespace: true },
});
expect(findLocation(vulnerabilityWithoutLocation[0].id).text()).toContain(
'Administrator / Security reports',
);
expect(findLocation(vulnerabilityWithoutLocation[0].id).findAll('div').length).toBe(2);
});
});
describe('when displayed on a project level dashboard', () => {
......
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['VulnerabilityLocationContainerScanning'] do
it { expect(described_class).to have_graphql_fields(:dependency, :image, :operating_system) }
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['VulnerabilityLocationDast'] do
it { expect(described_class).to have_graphql_fields(:hostname, :param, :path, :request_method) }
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['VulnerabilityLocationDependencyScanning'] do
it { expect(described_class).to have_graphql_fields(:dependency, :file) }
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['VulnerabilityLocationSast'] do
it do
expect(described_class).to have_graphql_fields(
:end_line,
:file,
:start_line,
:vulnerable_class,
:vulnerable_method
)
end
end
......@@ -46,45 +46,4 @@ describe GitlabSchema.types['Vulnerability'] do
)
end
end
describe 'location' do
let_it_be(:location) do
{
'end_line' => 666,
'file' => 'vulnerable-file.js',
'start_line' => 420
}
end
let(:query) do
%(
query {
project(fullPath:"#{project.full_path}") {
name
vulnerabilities {
nodes {
location
}
}
}
}
)
end
before do
create(
:vulnerabilities_occurrence,
vulnerability: vulnerability,
raw_metadata: {
location: location
}.to_json
)
end
it "is the JSON metadata for the vulnerability's location" do
vulnerabilities = subject.dig('data', 'project', 'vulnerabilities', 'nodes')
expect(Gitlab::Json.parse(vulnerabilities.first['location'])).to eq(location)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['VulnerableDependency'] do
it { expect(described_class).to have_graphql_fields(:package, :version) }
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['VulnerablePackage'] do
it { expect(described_class).to have_graphql_fields(:name) }
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Query.vulnerabilities.location' do
include GraphqlHelpers
subject { graphql_data.dig('vulnerabilities', 'nodes') }
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user, security_dashboard_projects: [project]) }
let_it_be(:fields) do
<<~QUERY
location {
__typename
... on VulnerabilityLocationContainerScanning {
image
operatingSystem
dependency {
version
package {
name
}
}
}
... on VulnerabilityLocationDependencyScanning {
file
dependency {
version
package {
name
}
}
}
... on VulnerabilityLocationDast {
hostname
param
path
requestMethod
}
... on VulnerabilityLocationSast {
endLine
file
startLine
vulnerableClass
vulnerableMethod
}
}
QUERY
end
let_it_be(:query) do
graphql_query_for('vulnerabilities', {}, query_graphql_field('nodes', {}, fields))
end
before do
project.add_developer(user)
stub_licensed_features(security_dashboard: true)
post_graphql(query, current_user: user)
end
context 'when the vulnerability was found by a container scan' do
let_it_be(:vulnerability) do
create(:vulnerability, project: project, report_type: :container_scanning)
end
let_it_be(:metadata) do
{
location: {
image: 'vulnerable_image',
operating_system: 'vulnerable_os',
dependency: {
version: '6.6.6',
package: {
name: 'vulnerable_container'
}
}
}
}
end
let_it_be(:finding) do
create(
:vulnerabilities_occurrence,
vulnerability: vulnerability,
raw_metadata: metadata.to_json
)
end
it 'returns a container location' do
location = subject.first['location']
expect(location['__typename']).to eq('VulnerabilityLocationContainerScanning')
expect(location['image']).to eq('vulnerable_image')
expect(location['operatingSystem']).to eq('vulnerable_os')
expect(location['dependency']['version']).to eq('6.6.6')
expect(location['dependency']['package']['name']).to eq('vulnerable_container')
end
end
context 'when the vulnerability was found by a dependency scan' do
let_it_be(:vulnerability) do
create(:vulnerability, project: project, report_type: :dependency_scanning)
end
let_it_be(:metadata) do
{
location: {
file: 'vulnerable_file',
dependency: {
version: '6.6.6',
package: {
name: 'vulnerable_package'
}
}
}
}
end
let_it_be(:finding) do
create(
:vulnerabilities_occurrence,
vulnerability: vulnerability,
raw_metadata: metadata.to_json
)
end
it 'returns a location in a dependency' do
location = subject.first['location']
expect(location['__typename']).to eq('VulnerabilityLocationDependencyScanning')
expect(location['file']).to eq('vulnerable_file')
expect(location['dependency']['version']).to eq('6.6.6')
expect(location['dependency']['package']['name']).to eq('vulnerable_package')
end
end
context 'when the vulnerability was found by a SAST scan' do
let_it_be(:vulnerability) do
create(:vulnerability, project: project, report_type: :sast)
end
let_it_be(:metadata) do
{
location: {
class: 'VulnerableClass',
method: 'vulnerable_method',
file: 'vulnerable_file',
start_line: '420',
end_line: '666'
}
}
end
let_it_be(:finding) do
create(
:vulnerabilities_occurrence,
vulnerability: vulnerability,
raw_metadata: metadata.to_json
)
end
it 'returns the file and line numbers where the vulnerability is located' do
location = subject.first['location']
expect(location['__typename']).to eq('VulnerabilityLocationSast')
expect(location['file']).to eq('vulnerable_file')
expect(location['startLine']).to eq('420')
expect(location['endLine']).to eq('666')
expect(location['vulnerableClass']).to eq('VulnerableClass')
expect(location['vulnerableMethod']).to eq('vulnerable_method')
end
end
context 'when the vulnerability was found by a DAST scan' do
let_it_be(:vulnerability) do
create(:vulnerability, project: project, report_type: :dast)
end
let_it_be(:metadata) do
{
location: {
hostname: 'https://crimethinc.com',
param: 'ARTICLE=may-day-2020',
path: 'https://crimethinc.com/2020/04/22',
request_method: 'GET'
}
}
end
let_it_be(:finding) do
create(
:vulnerabilities_occurrence,
vulnerability: vulnerability,
raw_metadata: metadata.to_json
)
end
it 'returns the URL where the vulnerability was found' do
location = subject.first['location']
expect(location['__typename']).to eq('VulnerabilityLocationDast')
expect(location['hostname']).to eq('https://crimethinc.com')
expect(location['param']).to eq('ARTICLE=may-day-2020')
expect(location['path']).to eq('https://crimethinc.com/2020/04/22')
expect(location['requestMethod']).to eq('GET')
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment