Commit 7ff151eb authored by Avielle Wolfe's avatar Avielle Wolfe Committed by Heinrich Lee Yu

Add vulnerability history resolver and field

* Add VulnerabilitiesHistoryResolver
* Add vulnerabilitySeveritiesCountByDay field to ProjectType
* Add day field to VulnerabilitySeveritiesCountType

TODO:

* Use `days` arg to determine number of days to fetch
* Iterate over groups of 10 days so we don't break the DB
* Testing!
parent d33ad2c0
...@@ -4302,6 +4302,41 @@ type Group { ...@@ -4302,6 +4302,41 @@ type Group {
state: [VulnerabilityState!] state: [VulnerabilityState!]
): VulnerabilityConnection ): VulnerabilityConnection
"""
Number of vulnerabilities per severity level, per day, for the projects in the group and its subgroups
"""
vulnerabilitiesCountByDayAndSeverity(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Last day for which to fetch vulnerability history
"""
endDate: ISO8601Date!
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
First day for which to fetch vulnerability history
"""
startDate: ISO8601Date!
): VulnerabilitiesCountByDayAndSeverityConnection
""" """
Web URL of the group Web URL of the group
""" """
...@@ -4324,6 +4359,11 @@ enum HealthStatus { ...@@ -4324,6 +4359,11 @@ enum HealthStatus {
onTrack onTrack
} }
"""
An ISO 8601-encoded date
"""
scalar ISO8601Date
type InstanceSecurityDashboard { type InstanceSecurityDashboard {
""" """
Projects selected in Instance Security Dashboard Projects selected in Instance Security Dashboard
...@@ -8223,6 +8263,41 @@ type Query { ...@@ -8223,6 +8263,41 @@ type Query {
""" """
state: [VulnerabilityState!] state: [VulnerabilityState!]
): VulnerabilityConnection ): VulnerabilityConnection
"""
Number of vulnerabilities per severity level, per day, for the projects on the current user's instance security dashboard
"""
vulnerabilitiesCountByDayAndSeverity(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Last day for which to fetch vulnerability history
"""
endDate: ISO8601Date!
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
First day for which to fetch vulnerability history
"""
startDate: ISO8601Date!
): VulnerabilitiesCountByDayAndSeverityConnection
} }
""" """
...@@ -10720,6 +10795,61 @@ enum VisibilityScopesEnum { ...@@ -10720,6 +10795,61 @@ enum VisibilityScopesEnum {
public public
} }
"""
Represents the number of vulnerabilities for a particular severity on a particular day
"""
type VulnerabilitiesCountByDayAndSeverity {
"""
Number of vulnerabilities
"""
count: Int
"""
Date for the count
"""
day: ISO8601Date
"""
Severity of the counted vulnerabilities
"""
severity: VulnerabilitySeverity
}
"""
The connection type for VulnerabilitiesCountByDayAndSeverity.
"""
type VulnerabilitiesCountByDayAndSeverityConnection {
"""
A list of edges.
"""
edges: [VulnerabilitiesCountByDayAndSeverityEdge]
"""
A list of nodes.
"""
nodes: [VulnerabilitiesCountByDayAndSeverity]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type VulnerabilitiesCountByDayAndSeverityEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: VulnerabilitiesCountByDayAndSeverity
}
""" """
Represents a vulnerability. Represents a vulnerability.
""" """
......
...@@ -11939,6 +11939,87 @@ ...@@ -11939,6 +11939,87 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "vulnerabilitiesCountByDayAndSeverity",
"description": "Number of vulnerabilities per severity level, per day, for the projects in the group and its subgroups",
"args": [
{
"name": "startDate",
"description": "First day for which to fetch vulnerability history",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ISO8601Date",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "endDate",
"description": "Last day for which to fetch vulnerability history",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ISO8601Date",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "VulnerabilitiesCountByDayAndSeverityConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "webUrl", "name": "webUrl",
"description": "Web URL of the group", "description": "Web URL of the group",
...@@ -12035,6 +12116,16 @@ ...@@ -12035,6 +12116,16 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "SCALAR",
"name": "ISO8601Date",
"description": "An ISO 8601-encoded date",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "InstanceSecurityDashboard", "name": "InstanceSecurityDashboard",
...@@ -24232,6 +24323,87 @@ ...@@ -24232,6 +24323,87 @@
}, },
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
},
{
"name": "vulnerabilitiesCountByDayAndSeverity",
"description": "Number of vulnerabilities per severity level, per day, for the projects on the current user's instance security dashboard",
"args": [
{
"name": "startDate",
"description": "First day for which to fetch vulnerability history",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ISO8601Date",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "endDate",
"description": "Last day for which to fetch vulnerability history",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ISO8601Date",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "VulnerabilitiesCountByDayAndSeverityConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
} }
], ],
"inputFields": null, "inputFields": null,
...@@ -31893,6 +32065,173 @@ ...@@ -31893,6 +32065,173 @@
], ],
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "VulnerabilitiesCountByDayAndSeverity",
"description": "Represents the number of vulnerabilities for a particular severity on a particular day",
"fields": [
{
"name": "count",
"description": "Number of vulnerabilities",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "day",
"description": "Date for the count",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "ISO8601Date",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "severity",
"description": "Severity of the counted vulnerabilities",
"args": [
],
"type": {
"kind": "ENUM",
"name": "VulnerabilitySeverity",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "VulnerabilitiesCountByDayAndSeverityConnection",
"description": "The connection type for VulnerabilitiesCountByDayAndSeverity.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "VulnerabilitiesCountByDayAndSeverityEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "VulnerabilitiesCountByDayAndSeverity",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "VulnerabilitiesCountByDayAndSeverityEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "VulnerabilitiesCountByDayAndSeverity",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Vulnerability", "name": "Vulnerability",
......
...@@ -1633,6 +1633,16 @@ Autogenerated return type of UpdateSnippet ...@@ -1633,6 +1633,16 @@ Autogenerated return type of UpdateSnippet
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `createSnippet` | Boolean! | Indicates the user can perform `create_snippet` on this resource | | `createSnippet` | Boolean! | Indicates the user can perform `create_snippet` on this resource |
## VulnerabilitiesCountByDayAndSeverity
Represents the number of vulnerabilities for a particular severity on a particular day
| Name | Type | Description |
| --- | ---- | ---------- |
| `count` | Int | Number of vulnerabilities |
| `day` | ISO8601Date | Date for the count |
| `severity` | VulnerabilitySeverity | Severity of the counted vulnerabilities |
## Vulnerability ## Vulnerability
Represents a vulnerability. Represents a vulnerability.
......
...@@ -31,6 +31,12 @@ module EE ...@@ -31,6 +31,12 @@ module EE
null: true, null: true,
description: 'Vulnerabilities reported on the projects in the group and its subgroups', description: 'Vulnerabilities reported on the projects in the group and its subgroups',
resolver: ::Resolvers::VulnerabilitiesResolver resolver: ::Resolvers::VulnerabilitiesResolver
field :vulnerabilities_count_by_day_and_severity,
::Types::VulnerabilitiesCountByDayAndSeverityType.connection_type,
null: true,
description: 'Number of vulnerabilities per severity level, per day, for the projects in the group and its subgroups',
resolver: ::Resolvers::VulnerabilitiesHistoryResolver
end end
end end
end end
......
...@@ -15,6 +15,12 @@ module EE ...@@ -15,6 +15,12 @@ module EE
description: "Vulnerabilities reported on projects on the current user's instance security dashboard", description: "Vulnerabilities reported on projects on the current user's instance security dashboard",
resolver: ::Resolvers::VulnerabilitiesResolver resolver: ::Resolvers::VulnerabilitiesResolver
field :vulnerabilities_count_by_day_and_severity,
::Types::VulnerabilitiesCountByDayAndSeverityType.connection_type,
null: true,
description: "Number of vulnerabilities per severity level, per day, for the projects on the current user's instance security dashboard",
resolver: ::Resolvers::VulnerabilitiesHistoryResolver
field :design_management, ::Types::DesignManagementType, field :design_management, ::Types::DesignManagementType,
null: false, null: false,
description: 'Fields related to design management' description: 'Fields related to design management'
......
# frozen_string_literal: true
# VulnerabilitiesBaseResolver is an abstract class that is inherited by
# vulnerability related resolvers. It contains the somewhat obtuse logic related
# to finding the object to get vulnerabilities from so that developers writing
# new resolvers don't have to repeat it.
module Resolvers
class VulnerabilitiesBaseResolver < BaseResolver
include Gitlab::Utils::StrongMemoize
protected
# `vulnerable` will be a Project, Group, or InstanceSecurityDashboard
def vulnerable
# A project or group could have been loaded in batch by `BatchLoader`.
# At this point we need the `id` of the project or group to query for vulnerabilities, so
# make sure it's loaded and not `nil` before continuing.
strong_memoize(:vulnerable) do
if resolve_vulnerabilities_for_instance_security_dashboard?
::InstanceSecurityDashboard.new(current_user)
elsif object.respond_to?(:sync)
object.sync
else
object
end
end
end
def resolve_vulnerabilities_for_instance_security_dashboard?
# object will be nil when we're fetching vulnerabilities from QueryType,
# which is the source of vulnerability data for the instance security
# dashboard
object.nil? && current_user.present?
end
end
end
# frozen_string_literal: true
module Resolvers
class VulnerabilitiesHistoryResolver < VulnerabilitiesBaseResolver
include Gitlab::Utils::StrongMemoize
MAX_DAYS = ::Vulnerability::MAX_DAYS_OF_HISTORY
type Types::VulnerabilitiesCountByDayAndSeverityType, null: true
argument :start_date, GraphQL::Types::ISO8601Date, required: true,
description: 'First day for which to fetch vulnerability history'
argument :end_date, GraphQL::Types::ISO8601Date, required: true,
description: 'Last day for which to fetch vulnerability history'
def resolve(**args)
return [] unless vulnerable
start_date = args[:start_date]
end_date = args[:end_date]
days = end_date - start_date + 1
if days > MAX_DAYS
raise ::Vulnerability::TooManyDaysError, "Cannot fetch counts for more than #{MAX_DAYS} days"
else
vulnerable.vulnerabilities.counts_by_day_and_severity(start_date, end_date).to_a
end
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
module Resolvers module Resolvers
class VulnerabilitiesResolver < BaseResolver class VulnerabilitiesResolver < VulnerabilitiesBaseResolver
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
type Types::VulnerabilityType, null: true type Types::VulnerabilityType, null: true
...@@ -30,32 +30,8 @@ module Resolvers ...@@ -30,32 +30,8 @@ module Resolvers
private private
# `vulnerable` will be a Project, Group, or InstanceSecurityDashboard
def vulnerable
# A project or group could have been loaded in batch by `BatchLoader`.
# At this point we need the `id` of the project or group to query for vulnerabilities, so
# make sure it's loaded and not `nil` before continuing.
strong_memoize(:vulnerable) do
if resolve_vulnerabilities_for_instance_security_dashboard?
::InstanceSecurityDashboard.new(current_user)
elsif object.respond_to?(:sync)
object.sync
else
object
end
end
end
def vulnerabilities(filters) def vulnerabilities(filters)
Security::VulnerabilitiesFinder.new(vulnerable, filters).execute Security::VulnerabilitiesFinder.new(vulnerable, filters).execute
end end
def resolve_vulnerabilities_for_instance_security_dashboard?
# object will be nil when we're fetching vulnerabilities from QueryType,
# which is the source of vulnerability data for the instance security
# dashboard
object.nil? && current_user.present?
end
end end
end end
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class VulnerabilitiesCountByDayAndSeverityType < BaseObject
graphql_name 'VulnerabilitiesCountByDayAndSeverity'
description 'Represents the number of vulnerabilities for a particular severity on a particular day'
field :count, GraphQL::INT_TYPE, null: true,
description: 'Number of vulnerabilities'
field :day, GraphQL::Types::ISO8601Date, null: true,
description: 'Date for the count'
field :severity, VulnerabilitySeverityEnum, null: true,
description: 'Severity of the counted vulnerabilities'
end
end
...@@ -9,7 +9,7 @@ class Vulnerability < ApplicationRecord ...@@ -9,7 +9,7 @@ class Vulnerability < ApplicationRecord
TooManyDaysError = Class.new(StandardError) TooManyDaysError = Class.new(StandardError)
MAX_DAYS_IN_PAST = 10 MAX_DAYS_OF_HISTORY = 10
cache_markdown_field :title, pipeline: :single_line cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description, issuable_state_filter_enabled: true cache_markdown_field :description, issuable_state_filter_enabled: true
...@@ -67,19 +67,21 @@ class Vulnerability < ApplicationRecord ...@@ -67,19 +67,21 @@ class Vulnerability < ApplicationRecord
scope :with_states, -> (states) { where(state: states) } scope :with_states, -> (states) { where(state: states) }
scope :counts_by_severity, -> { group(:severity).count } scope :counts_by_severity, -> { group(:severity).count }
def self.counts_by_day_and_severity(num_days_in_past, end_date = Date.current) def self.counts_by_day_and_severity(start_date, end_date)
return [] unless Feature.enabled?(:vulnerability_history, default_enabled: true) return [] unless Feature.enabled?(:vulnerability_history, default_enabled: true)
num_days_of_history = end_date - start_date + 1
# this clause guards against query timeouts # this clause guards against query timeouts
raise TooManyDaysError, "Cannot fetch counts for more than #{MAX_DAYS_IN_PAST} days" if num_days_in_past > MAX_DAYS_IN_PAST raise TooManyDaysError, "Cannot fetch counts for more than #{MAX_DAYS_OF_HISTORY} days" if num_days_of_history > MAX_DAYS_OF_HISTORY
quoted_num_days_in_past = connection.quote(num_days_in_past) quoted_start_date = connection.quote(start_date)
quoted_end_date = connection.quote(end_date) quoted_end_date = connection.quote(end_date)
select( select(
'DATE(calendar.entry) AS day, severity, COUNT(*)' 'DATE(calendar.entry) AS day, severity, COUNT(*)'
).from( ).from(
"generate_series(DATE #{quoted_end_date} - INTERVAL '#{quoted_num_days_in_past} days', DATE #{quoted_end_date}, INTERVAL '1 day') as calendar(entry)" "generate_series(DATE #{quoted_start_date}, DATE #{quoted_end_date}, INTERVAL '1 day') as calendar(entry)"
).joins( ).joins(
'INNER JOIN vulnerabilities ON vulnerabilities.created_at <= calendar.entry' 'INNER JOIN vulnerabilities ON vulnerabilities.created_at <= calendar.entry'
).where( ).where(
......
---
title: Add vulnerability history to graphQL
merge_request: 30674
author:
type: added
...@@ -12,6 +12,7 @@ describe GitlabSchema.types['Group'] do ...@@ -12,6 +12,7 @@ describe GitlabSchema.types['Group'] do
it { expect(described_class).to have_graphql_field(:groupTimelogsEnabled) } it { expect(described_class).to have_graphql_field(:groupTimelogsEnabled) }
it { expect(described_class).to have_graphql_field(:timelogs, complexity: 5) } it { expect(described_class).to have_graphql_field(:timelogs, complexity: 5) }
it { expect(described_class).to have_graphql_field(:vulnerabilities) } it { expect(described_class).to have_graphql_field(:vulnerabilities) }
it { expect(described_class).to have_graphql_field(:vulnerabilities_count_by_day_and_severity) }
describe 'timelogs field' do describe 'timelogs field' do
subject { described_class.fields['timelogs'] } subject { described_class.fields['timelogs'] }
......
# frozen_string_literal: true
require 'spec_helper'
describe Resolvers::VulnerabilitiesHistoryResolver do
include GraphqlHelpers
subject { resolve(described_class, obj: group, args: args, ctx: { current_user: user }) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:user) { create(:user) }
describe '#resolve' do
let(:args) { { start_date: Date.parse('2019-10-15'), end_date: Date.parse('2019-10-21') } }
it "fetches historical vulnerability data from the start date to the end date" do
Timecop.freeze(Date.parse('2019-10-31')) do
create(:vulnerability, :critical, created_at: 15.days.ago, dismissed_at: 10.days.ago, project: project)
create(:vulnerability, :high, created_at: 15.days.ago, dismissed_at: 11.days.ago, project: project)
create(:vulnerability, :critical, created_at: 14.days.ago, resolved_at: 12.days.ago, project: project)
ordered_history = subject.sort_by { |count| [count['day'], count['severity']] }
expect(ordered_history.to_json).to eq([
{ 'id' => nil, 'severity' => 'critical', 'day' => '2019-10-16', 'count' => 1 },
{ 'id' => nil, 'severity' => 'high', 'day' => '2019-10-16', 'count' => 1 },
{ 'id' => nil, 'severity' => 'critical', 'day' => '2019-10-17', 'count' => 2 },
{ 'id' => nil, 'severity' => 'high', 'day' => '2019-10-17', 'count' => 1 },
{ 'id' => nil, 'severity' => 'critical', 'day' => '2019-10-18', 'count' => 2 },
{ 'id' => nil, 'severity' => 'high', 'day' => '2019-10-18', 'count' => 1 },
{ 'id' => nil, 'severity' => 'critical', 'day' => '2019-10-19', 'count' => 1 },
{ 'id' => nil, 'severity' => 'high', 'day' => '2019-10-19', 'count' => 1 },
{ 'id' => nil, 'severity' => 'critical', 'day' => '2019-10-20', 'count' => 1 }
].to_json)
end
end
context 'when given more than 10 days' do
let(:args) { { start_date: Date.parse('2019-10-11'), end_date: Date.parse('2019-10-21') } }
it 'raises an error stating that no more than 10 days can be requested' do
expect { subject }.to raise_error(::Vulnerability::TooManyDaysError, 'Cannot fetch counts for more than 10 days')
end
end
end
end
...@@ -8,7 +8,8 @@ describe GitlabSchema.types['Query'] do ...@@ -8,7 +8,8 @@ describe GitlabSchema.types['Query'] do
:design_management, :design_management,
:geo_node, :geo_node,
:vulnerabilities, :vulnerabilities,
:instance_security_dashboard :instance_security_dashboard,
:vulnerabilities_count_by_day_and_severity
).at_least ).at_least
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['VulnerabilitiesCountByDayAndSeverity'] do
it { expect(described_class).to have_graphql_fields(:count, :day, :severity) }
end
...@@ -178,7 +178,7 @@ describe Vulnerability do ...@@ -178,7 +178,7 @@ describe Vulnerability do
it 'returns an empty array' do it 'returns an empty array' do
create(:vulnerability, created_at: 1.day.ago) create(:vulnerability, created_at: 1.day.ago)
counts_by_day_and_severity = Vulnerability.counts_by_day_and_severity(6) counts_by_day_and_severity = Vulnerability.counts_by_day_and_severity(1.day.ago, Date.current)
expect(counts_by_day_and_severity).to be_empty expect(counts_by_day_and_severity).to be_empty
end end
...@@ -189,38 +189,13 @@ describe Vulnerability do ...@@ -189,38 +189,13 @@ describe Vulnerability do
stub_feature_flags(vulnerability_history: true) stub_feature_flags(vulnerability_history: true)
end end
context 'when not given an end date' do it 'returns the count of unresolved, undismissed vulnerabilities for each severity for each day from the start date to the end date' do
it 'returns the count of unresolved, undismissed vulnerabilities for each severity from the current day to the given number of days in the past' do
Timecop.freeze(Time.zone.parse('2019-10-31')) do Timecop.freeze(Time.zone.parse('2019-10-31')) do
create(:vulnerability, created_at: 5.days.ago, dismissed_at: Date.current, severity: :critical) create(:vulnerability, created_at: 5.days.ago, dismissed_at: Date.current, severity: :critical)
create(:vulnerability, created_at: 5.days.ago, dismissed_at: 1.day.ago, severity: :high) create(:vulnerability, created_at: 5.days.ago, dismissed_at: 1.day.ago, severity: :high)
create(:vulnerability, created_at: 4.days.ago, resolved_at: 2.days.ago, severity: :critical) create(:vulnerability, created_at: 4.days.ago, resolved_at: 2.days.ago, severity: :critical)
counts_by_day_and_severity = Vulnerability.counts_by_day_and_severity(6) counts_by_day_and_severity = Vulnerability.counts_by_day_and_severity(Date.parse('2019-10-22'), Date.parse('2019-10-28'))
expect(counts_by_day_and_severity.order(:day, :severity).to_json).to eq([
{ 'id' => nil, 'severity' => 'high', 'day' => '2019-10-26', 'count' => 1 },
{ 'id' => nil, 'severity' => 'critical', 'day' => '2019-10-26', 'count' => 1 },
{ 'id' => nil, 'severity' => 'high', 'day' => '2019-10-27', 'count' => 1 },
{ 'id' => nil, 'severity' => 'critical', 'day' => '2019-10-27', 'count' => 2 },
{ 'id' => nil, 'severity' => 'high', 'day' => '2019-10-28', 'count' => 1 },
{ 'id' => nil, 'severity' => 'critical', 'day' => '2019-10-28', 'count' => 2 },
{ 'id' => nil, 'severity' => 'high', 'day' => '2019-10-29', 'count' => 1 },
{ 'id' => nil, 'severity' => 'critical', 'day' => '2019-10-29', 'count' => 1 },
{ 'id' => nil, 'severity' => 'critical', 'day' => '2019-10-30', 'count' => 1 }
].to_json)
end
end
end
context 'when given an end date' do
it 'returns the count of unresolved, undismissed vulnerabilities for each severity for each day from the given end date to the given number of days in the past' do
Timecop.freeze(Time.zone.parse('2019-10-31')) do
create(:vulnerability, created_at: 5.days.ago, dismissed_at: Date.current, severity: :critical)
create(:vulnerability, created_at: 5.days.ago, dismissed_at: 1.day.ago, severity: :high)
create(:vulnerability, created_at: 4.days.ago, resolved_at: 2.days.ago, severity: :critical)
counts_by_day_and_severity = Vulnerability.counts_by_day_and_severity(6, Date.parse('2019-10-28'))
expect(counts_by_day_and_severity.order(:day, :severity).to_json).to eq([ expect(counts_by_day_and_severity.order(:day, :severity).to_json).to eq([
{ 'id' => nil, 'severity' => 'high', 'day' => '2019-10-26', 'count' => 1 }, { 'id' => nil, 'severity' => 'high', 'day' => '2019-10-26', 'count' => 1 },
...@@ -232,11 +207,10 @@ describe Vulnerability do ...@@ -232,11 +207,10 @@ describe Vulnerability do
].to_json) ].to_json)
end end
end end
end
context 'when given a number of past days greater than 10' do context 'there are more than 10 days between the start and end dates' do
it 'raises a TooManyDaysError' do it 'raises a TooManyDaysError' do
expect { Vulnerability.counts_by_day_and_severity(11) }.to raise_error( expect { Vulnerability.counts_by_day_and_severity(10.days.ago.to_date, Date.current) }.to raise_error(
Vulnerability::TooManyDaysError, Vulnerability::TooManyDaysError,
'Cannot fetch counts for more than 10 days' 'Cannot fetch counts for more than 10 days'
) )
......
# frozen_string_literal: true
require 'spec_helper'
describe 'group(fullPath).vulnerabilitiesCountByDayAndSeverity' do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, namespace: group) }
let_it_be(:current_user) { create(:user) }
let(:query) { graphql_query_for(:group, { fullPath: group.full_path }, history_field) }
let(:query_result) { graphql_data.dig('group', 'vulnerabilitiesCountByDayAndSeverity', 'nodes') }
let(:history_field) do
query_graphql_field(
:vulnerabilitiesCountByDayAndSeverity,
{
start_date: Date.parse('2019-10-15').iso8601,
end_date: Date.parse('2019-10-21').iso8601
},
history_fields
)
end
let(:history_fields) do
query_graphql_field(:nodes, nil, <<~FIELDS)
count
day
severity
FIELDS
end
it "fetches historical vulnerability data from the start date to the end date for projects in the group and its subgroups" do
Timecop.freeze(Time.zone.parse('2019-10-31')) do
project.add_developer(current_user)
create(:vulnerability, :critical, created_at: 15.days.ago, dismissed_at: 10.days.ago, project: project)
create(:vulnerability, :high, created_at: 15.days.ago, dismissed_at: 11.days.ago, project: project)
create(:vulnerability, :critical, created_at: 14.days.ago, resolved_at: 12.days.ago, project: project)
post_graphql(query, current_user: current_user)
ordered_history = query_result.sort_by { |count| [count['day'], count['severity']] }
expect(ordered_history).to eq([
{ 'severity' => 'CRITICAL', 'day' => '2019-10-16', 'count' => 1 },
{ 'severity' => 'HIGH', 'day' => '2019-10-16', 'count' => 1 },
{ 'severity' => 'CRITICAL', 'day' => '2019-10-17', 'count' => 2 },
{ 'severity' => 'HIGH', 'day' => '2019-10-17', 'count' => 1 },
{ 'severity' => 'CRITICAL', 'day' => '2019-10-18', 'count' => 2 },
{ 'severity' => 'HIGH', 'day' => '2019-10-18', 'count' => 1 },
{ 'severity' => 'CRITICAL', 'day' => '2019-10-19', 'count' => 1 },
{ 'severity' => 'HIGH', 'day' => '2019-10-19', 'count' => 1 },
{ 'severity' => 'CRITICAL', 'day' => '2019-10-20', 'count' => 1 }
])
end
end
end
...@@ -92,4 +92,54 @@ describe 'Query' do ...@@ -92,4 +92,54 @@ describe 'Query' do
end end
end end
end end
describe '.vulnerabilitiesCountByDayAndSeverity' do
let(:query_result) { graphql_data.dig('vulnerabilitiesCountByDayAndSeverity', 'nodes') }
let(:query) do
graphql_query_for(
:vulnerabilitiesCountByDayAndSeverity,
{
start_date: Date.parse('2019-10-15').iso8601,
end_date: Date.parse('2019-10-21').iso8601
},
history_fields
)
end
let(:history_fields) do
query_graphql_field(:nodes, nil, <<~FIELDS)
count
day
severity
FIELDS
end
it "fetches historical vulnerability data from the start date to the end date for projects on the current user's instance security dashboard" do
Timecop.freeze(Time.zone.parse('2019-10-31')) do
current_user.security_dashboard_projects << project
project.add_developer(developer)
create(:vulnerability, :critical, created_at: 15.days.ago, dismissed_at: 10.days.ago, project: project)
create(:vulnerability, :high, created_at: 15.days.ago, dismissed_at: 11.days.ago, project: project)
create(:vulnerability, :critical, created_at: 14.days.ago, resolved_at: 12.days.ago, project: project)
post_graphql(query, current_user: current_user)
ordered_history = query_result.sort_by { |count| [count['day'], count['severity']] }
expect(ordered_history).to eq([
{ 'severity' => 'CRITICAL', 'day' => '2019-10-16', 'count' => 1 },
{ 'severity' => 'HIGH', 'day' => '2019-10-16', 'count' => 1 },
{ 'severity' => 'CRITICAL', 'day' => '2019-10-17', 'count' => 2 },
{ 'severity' => 'HIGH', 'day' => '2019-10-17', 'count' => 1 },
{ 'severity' => 'CRITICAL', 'day' => '2019-10-18', 'count' => 2 },
{ 'severity' => 'HIGH', 'day' => '2019-10-18', 'count' => 1 },
{ 'severity' => 'CRITICAL', 'day' => '2019-10-19', 'count' => 1 },
{ 'severity' => 'HIGH', 'day' => '2019-10-19', 'count' => 1 },
{ 'severity' => 'CRITICAL', 'day' => '2019-10-20', 'count' => 1 }
])
end
end
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