Commit 47c03fbd authored by Robert Speicher's avatar Robert Speicher

Merge branch '217811-add-new-type-for-historical-statistics' into 'master'

Add vulnerabilitiesCountByDay with vulnerability statistics

See merge request gitlab-org/gitlab!38197
parents cfff41ab a2d26234
......@@ -5926,7 +5926,43 @@ type Group {
): VulnerabilityConnection
"""
Number of vulnerabilities per severity level, per day, for the projects in the group and its subgroups
Number of vulnerabilities per day for the projects in the group and its subgroups
"""
vulnerabilitiesCountByDay(
"""
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!
): VulnerabilitiesCountByDayConnection
"""
Number of vulnerabilities per severity level, per day, for the projects in the
group and its subgroups. Deprecated in 13.3: Use `vulnerabilitiesCountByDay`
"""
vulnerabilitiesCountByDayAndSeverity(
"""
......@@ -5958,7 +5994,7 @@ type Group {
First day for which to fetch vulnerability history
"""
startDate: ISO8601Date!
): VulnerabilitiesCountByDayAndSeverityConnection
): VulnerabilitiesCountByDayAndSeverityConnection @deprecated(reason: "Use `vulnerabilitiesCountByDay`. Deprecated in 13.3")
"""
Represents vulnerable project counts for each grade
......@@ -11552,7 +11588,44 @@ type Query {
): VulnerabilityConnection
"""
Number of vulnerabilities per severity level, per day, for the projects on the current user's instance security dashboard
Number of vulnerabilities per day for the projects on the current user's instance security dashboard
"""
vulnerabilitiesCountByDay(
"""
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!
): VulnerabilitiesCountByDayConnection
"""
Number of vulnerabilities per severity level, per day, for the projects on the
current user's instance security dashboard. Deprecated in 13.3: Use
`vulnerabilitiesCountByDay`
"""
vulnerabilitiesCountByDayAndSeverity(
"""
......@@ -11584,7 +11657,7 @@ type Query {
First day for which to fetch vulnerability history
"""
startDate: ISO8601Date!
): VulnerabilitiesCountByDayAndSeverityConnection
): VulnerabilitiesCountByDayAndSeverityConnection @deprecated(reason: "Use `vulnerabilitiesCountByDay`. Deprecated in 13.3")
}
"""
......@@ -15460,6 +15533,51 @@ enum VisibilityScopesEnum {
public
}
"""
Represents the count of vulnerabilities by severity on a particular day
"""
type VulnerabilitiesCountByDay {
"""
Total number of vulnerabilities on a particular day with critical severity
"""
critical: Int!
"""
Date for the count
"""
date: ISO8601Date!
"""
Total number of vulnerabilities on a particular day with high severity
"""
high: Int!
"""
Total number of vulnerabilities on a particular day with info severity
"""
info: Int!
"""
Total number of vulnerabilities on a particular day with low severity
"""
low: Int!
"""
Total number of vulnerabilities on a particular day with medium severity
"""
medium: Int!
"""
Total number of vulnerabilities on a particular day
"""
total: Int!
"""
Total number of vulnerabilities on a particular day with unknown severity
"""
unknown: Int!
}
"""
Represents the number of vulnerabilities for a particular severity on a particular day
"""
......@@ -15515,6 +15633,41 @@ type VulnerabilitiesCountByDayAndSeverityEdge {
node: VulnerabilitiesCountByDayAndSeverity
}
"""
The connection type for VulnerabilitiesCountByDay.
"""
type VulnerabilitiesCountByDayConnection {
"""
A list of edges.
"""
edges: [VulnerabilitiesCountByDayEdge]
"""
A list of nodes.
"""
nodes: [VulnerabilitiesCountByDay]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type VulnerabilitiesCountByDayEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: VulnerabilitiesCountByDay
}
"""
Represents a vulnerability.
"""
......
......@@ -16325,8 +16325,8 @@
"deprecationReason": null
},
{
"name": "vulnerabilitiesCountByDayAndSeverity",
"description": "Number of vulnerabilities per severity level, per day, for the projects in the group and its subgroups",
"name": "vulnerabilitiesCountByDay",
"description": "Number of vulnerabilities per day for the projects in the group and its subgroups",
"args": [
{
"name": "startDate",
......@@ -16399,12 +16399,93 @@
],
"type": {
"kind": "OBJECT",
"name": "VulnerabilitiesCountByDayAndSeverityConnection",
"name": "VulnerabilitiesCountByDayConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "vulnerabilitiesCountByDayAndSeverity",
"description": "Number of vulnerabilities per severity level, per day, for the projects in the group and its subgroups. Deprecated in 13.3: Use `vulnerabilitiesCountByDay`",
"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": true,
"deprecationReason": "Use `vulnerabilitiesCountByDay`. Deprecated in 13.3"
},
{
"name": "vulnerabilityGrades",
"description": "Represents vulnerable project counts for each grade",
......@@ -33983,8 +34064,8 @@
"deprecationReason": null
},
{
"name": "vulnerabilitiesCountByDayAndSeverity",
"description": "Number of vulnerabilities per severity level, per day, for the projects on the current user's instance security dashboard",
"name": "vulnerabilitiesCountByDay",
"description": "Number of vulnerabilities per day for the projects on the current user's instance security dashboard",
"args": [
{
"name": "startDate",
......@@ -34057,11 +34138,92 @@
],
"type": {
"kind": "OBJECT",
"name": "VulnerabilitiesCountByDayAndSeverityConnection",
"name": "VulnerabilitiesCountByDayConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "vulnerabilitiesCountByDayAndSeverity",
"description": "Number of vulnerabilities per severity level, per day, for the projects on the current user's instance security dashboard. Deprecated in 13.3: Use `vulnerabilitiesCountByDay`",
"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": true,
"deprecationReason": "Use `vulnerabilitiesCountByDay`. Deprecated in 13.3"
}
],
"inputFields": null,
......@@ -45482,6 +45644,163 @@
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "VulnerabilitiesCountByDay",
"description": "Represents the count of vulnerabilities by severity on a particular day",
"fields": [
{
"name": "critical",
"description": "Total number of vulnerabilities on a particular day with critical severity",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "date",
"description": "Date for the count",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ISO8601Date",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "high",
"description": "Total number of vulnerabilities on a particular day with high severity",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "info",
"description": "Total number of vulnerabilities on a particular day with info severity",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "low",
"description": "Total number of vulnerabilities on a particular day with low severity",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "medium",
"description": "Total number of vulnerabilities on a particular day with medium severity",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "total",
"description": "Total number of vulnerabilities on a particular day",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "unknown",
"description": "Total number of vulnerabilities on a particular day with unknown severity",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "VulnerabilitiesCountByDayAndSeverity",
......@@ -45649,6 +45968,118 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "VulnerabilitiesCountByDayConnection",
"description": "The connection type for VulnerabilitiesCountByDay.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "VulnerabilitiesCountByDayEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "VulnerabilitiesCountByDay",
"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": "VulnerabilitiesCountByDayEdge",
"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": "VulnerabilitiesCountByDay",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Vulnerability",
......@@ -2294,6 +2294,21 @@ Autogenerated return type of UpdateSnippet
| --- | ---- | ---------- |
| `createSnippet` | Boolean! | Indicates the user can perform `create_snippet` on this resource |
## VulnerabilitiesCountByDay
Represents the count of vulnerabilities by severity on a particular day
| Name | Type | Description |
| --- | ---- | ---------- |
| `critical` | Int! | Total number of vulnerabilities on a particular day with critical severity |
| `date` | ISO8601Date! | Date for the count |
| `high` | Int! | Total number of vulnerabilities on a particular day with high severity |
| `info` | Int! | Total number of vulnerabilities on a particular day with info severity |
| `low` | Int! | Total number of vulnerabilities on a particular day with low severity |
| `medium` | Int! | Total number of vulnerabilities on a particular day with medium severity |
| `total` | Int! | Total number of vulnerabilities on a particular day |
| `unknown` | Int! | Total number of vulnerabilities on a particular day with unknown severity |
## VulnerabilitiesCountByDayAndSeverity
Represents the number of vulnerabilities for a particular severity on a particular day
......
......@@ -6,7 +6,7 @@ import Filters from 'ee/security_dashboard/components/first_class_vulnerability_
import VulnerabilityChart from 'ee/security_dashboard/components/first_class_vulnerability_chart.vue';
import CsvExportButton from './csv_export_button.vue';
import VulnerabilitySeverity from './vulnerability_severity.vue';
import vulnerabilityHistoryQuery from '../graphql/group_vulnerability_history.graphql';
import vulnerabilityHistoryQuery from '../graphql/group_vulnerability_history.query.graphql';
import DashboardNotConfigured from './empty_states/group_dashboard_not_configured.vue';
export default {
......
......@@ -10,7 +10,7 @@ import Filters from 'ee/security_dashboard/components/first_class_vulnerability_
import projectsQuery from 'ee/security_dashboard/graphql/get_instance_security_dashboard_projects.query.graphql';
import ProjectManager from './first_class_project_manager/project_manager.vue';
import CsvExportButton from './csv_export_button.vue';
import vulnerabilityHistoryQuery from '../graphql/instance_vulnerability_history.graphql';
import vulnerabilityHistoryQuery from '../graphql/instance_vulnerability_history.query.graphql';
import DashboardNotConfigured from './empty_states/instance_dashboard_not_configured.vue';
export default {
......
......@@ -12,7 +12,6 @@ import { SEVERITY_LEVELS } from '../store/constants';
const ISO_DATE = 'isoDate';
const DAYS = { thirty: 30, sixty: 60, ninety: 90 };
const MAX_DAY_INTERVAL_ALLOWED_BY_BACKEND = 10;
export default {
components: {
......@@ -33,7 +32,6 @@ export default {
vulnerabilitiesHistory: {},
vulnerabilitiesHistoryDayRange: DAYS.thirty,
errorLoadingVulnerabilitiesHistory: false,
currentStartDateCursor: undefined,
};
},
days: Object.values(DAYS),
......@@ -62,18 +60,13 @@ export default {
variables() {
return {
fullPath: this.groupFullPath,
startDate: formatDate(new Date(this.startDateCursor), ISO_DATE),
startDate: formatDate(new Date(this.startDate), ISO_DATE),
endDate: this.formattedEndDateCursor,
};
},
update(results) {
return this.processRawData(results);
},
result() {
if (this.formattedEndDateCursor !== formatDate(new Date(), ISO_DATE)) {
this.currentStartDateCursor = this.endDateCursor;
}
},
error() {
this.errorLoadingVulnerabilitiesHistory = true;
},
......@@ -83,14 +76,8 @@ export default {
startDate() {
return Date.now() - DAY_IN_MS * this.vulnerabilitiesHistoryDayRange;
},
startDateCursor() {
return this.currentStartDateCursor || this.startDate;
},
endDateCursor() {
return Math.min(
this.startDateCursor + DAY_IN_MS * (MAX_DAY_INTERVAL_ALLOWED_BY_BACKEND - 1),
Date.now(),
);
return Date.now();
},
formattedEndDateCursor() {
return formatDate(new Date(this.endDateCursor), ISO_DATE);
......@@ -124,7 +111,7 @@ export default {
},
},
watch: {
startDateCursor() {
startDate() {
this.$apollo.queries.vulnerabilitiesHistory.refetch();
},
},
......@@ -132,20 +119,22 @@ export default {
setVulnerabilitiesHistoryDayRange(days) {
this.vulnerabilitiesHistory = {};
this.vulnerabilitiesHistoryDayRange = days;
this.currentStartDateCursor = undefined;
},
processRawData(results) {
let { vulnerabilitiesCountByDayAndSeverity } = results;
let { vulnerabilitiesCountByDay } = results;
if (this.groupFullPath) {
vulnerabilitiesCountByDayAndSeverity = results.group.vulnerabilitiesCountByDayAndSeverity;
vulnerabilitiesCountByDay = results.group.vulnerabilitiesCountByDay;
}
const vulnerabilitiesData = vulnerabilitiesCountByDayAndSeverity.nodes.reduce(
const vulnerabilitiesData = vulnerabilitiesCountByDay.nodes.reduce(
(acc, v) => {
const severity = v.severity.toLowerCase();
acc[severity] = acc[severity] || {};
acc[severity][v.day] = v.count;
const { date, ...severities } = v;
Object.keys(severities).forEach(severity => {
acc[severity] = acc[severity] || {};
acc[severity][date] = v[severity];
}, {});
return acc;
},
{ ...this.vulnerabilitiesHistory },
......
query groupVulnerabilityHistory($fullPath: ID!, $startDate: ISO8601Date!, $endDate: ISO8601Date!) {
group(fullPath: $fullPath) {
vulnerabilitiesCountByDayAndSeverity(startDate: $startDate, endDate: $endDate) {
vulnerabilitiesCountByDay(startDate: $startDate, endDate: $endDate) {
nodes {
day
count
severity
date
critical
high
medium
low
}
}
}
......
query instanceVulnerabilityHistory($startDate: ISO8601Date!, $endDate: ISO8601Date!) {
vulnerabilitiesCountByDayAndSeverity(startDate: $startDate, endDate: $endDate) {
vulnerabilitiesCountByDay(startDate: $startDate, endDate: $endDate) {
nodes {
day
count
severity
date
critical
high
medium
low
}
}
}
......@@ -42,11 +42,18 @@ module EE
description: 'Vulnerability scanners reported on the project vulnerabilties of the group and its subgroups',
resolver: ::Resolvers::Vulnerabilities::ScannersResolver
field :vulnerabilities_count_by_day,
::Types::VulnerabilitiesCountByDayType.connection_type,
null: true,
description: 'Number of vulnerabilities per day for the projects in the group and its subgroups',
resolver: ::Resolvers::VulnerabilitiesCountPerDayResolver
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
resolver: ::Resolvers::VulnerabilitiesHistoryResolver,
deprecated: { reason: 'Use `vulnerabilitiesCountByDay`', milestone: '13.3' }
field :vulnerability_grades,
[::Types::VulnerableProjectsByGradeType],
......
......@@ -12,11 +12,18 @@ module EE
description: "Vulnerabilities reported on projects on the current user's instance security dashboard",
resolver: ::Resolvers::VulnerabilitiesResolver
field :vulnerabilities_count_by_day,
::Types::VulnerabilitiesCountByDayType.connection_type,
null: true,
description: "Number of vulnerabilities per day for the projects on the current user's instance security dashboard",
resolver: ::Resolvers::VulnerabilitiesCountPerDayResolver
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
resolver: ::Resolvers::VulnerabilitiesHistoryResolver,
deprecated: { reason: 'Use `vulnerabilitiesCountByDay`', milestone: '13.3' }
field :geo_node, ::Types::Geo::GeoNodeType,
null: true,
......
# frozen_string_literal: true
module Resolvers
class VulnerabilitiesCountPerDayResolver < VulnerabilitiesBaseResolver
type Types::VulnerabilitiesCountByDayType, 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
vulnerable
.vulnerability_historical_statistics
.grouped_by_date
.aggregated_by_date
.between_dates(args[:start_date], args[:end_date])
.index_by(&:date)
.then { |calendar_entries| generate_missing_dates(calendar_entries, args[:start_date], args[:end_date]) }
end
private
def generate_missing_dates(calendar_entries, start_date, end_date)
severities = ::Vulnerabilities::Finding::SEVERITY_LEVELS.keys
(start_date..end_date)
.each_with_object({}) { |date, result| result[date] = build_calendar_entry(date, calendar_entries[date], result[date - 1.day]) }
.values
.map { |calendar_entry| calendar_entry.attributes.slice('date', 'total', *severities) }
end
def build_calendar_entry(date, result_from_current_day, result_from_previous_day)
result_from_current_day || build_missing_calendar_entry(date, result_from_previous_day)
end
def build_missing_calendar_entry(date, result_from_previous_day)
::Vulnerabilities::HistoricalStatistic.new(result_from_previous_day&.attributes.to_h.merge(date: date))
end
end
end
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class VulnerabilitiesCountByDayType < BaseObject
graphql_name 'VulnerabilitiesCountByDay'
description 'Represents the count of vulnerabilities by severity on a particular day'
field :date, GraphQL::Types::ISO8601Date, null: false,
description: 'Date for the count'
field :total, GraphQL::INT_TYPE, null: false,
description: 'Total number of vulnerabilities on a particular day'
::Vulnerabilities::Finding::SEVERITY_LEVELS.keys.each do |severity|
field severity.to_s, GraphQL::INT_TYPE, null: false,
description: "Total number of vulnerabilities on a particular day with #{severity} severity"
end
end
end
......@@ -342,6 +342,11 @@ module EE
)
end
def vulnerability_historical_statistics
::Vulnerabilities::HistoricalStatistic
.for_project(::Project.for_group_and_its_subgroups(self).non_archived.without_deleted)
end
def max_personal_access_token_lifetime_from_now
if max_personal_access_token_lifetime.present?
max_personal_access_token_lifetime.days.from_now
......
......@@ -38,6 +38,12 @@ class InstanceSecurityDashboard
Vulnerabilities::Scanner.for_projects(projects)
end
def vulnerability_historical_statistics
return Vulnerabilities::Scanner.none if projects.empty?
Vulnerabilities::HistoricalStatistic.for_project(projects)
end
private
attr_reader :project_ids, :user
......
......@@ -21,5 +21,18 @@ module Vulnerabilities
enum letter_grade: Vulnerabilities::Statistic.letter_grades
scope :older_than, ->(days:) { where('"vulnerability_historical_statistics"."date" < now() - interval ?', "#{days} days") }
scope :between_dates, -> (start_date, end_date) { where(arel_table[:date].between(start_date..end_date)) }
scope :for_project, -> (project) { where(project: project) }
scope :aggregated_by_date, -> do
select(
arel_table[:date],
arel_table[:total].sum.as('total'),
*Finding::SEVERITY_LEVELS.map { |severity, _| arel_table[severity].sum.as(severity.to_s) }
)
end
scope :grouped_by_date, -> (sort = :asc) do
group(:date)
.order(date: sort)
end
end
end
---
title: Add vulnerabilitiesCountByDay to Group and InstanceSecurityDashboard GraphQL
API
merge_request: 38197
author:
type: added
---
title: Deprecate vulnerabilitiesCountByDayAndSeverity on Group and InstanceSecurityDashboard GraphQL API
merge_request: 38197
author:
type: deprecated
......@@ -7,11 +7,11 @@ describe('First class vulnerability chart component', () => {
let wrapper;
const responseData = {
vulnerabilitiesCountByDayAndSeverity: {
vulnerabilitiesCountByDay: {
nodes: [
{ day: '2020-05-18', count: 5, severity: 'MEDIUM' },
{ day: '2020-05-19', count: 2, severity: 'MEDIUM' },
{ day: '2020-05-18', count: 2, severity: 'CRITICAL' },
{ date: '2020-05-18', medium: 5 },
{ date: '2020-05-19', medium: 2 },
{ date: '2020-05-18', critical: 2 },
],
},
};
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::VulnerabilitiesCountPerDayResolver do
include GraphqlHelpers
subject(:ordered_history) { 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(:project_2) { create(:project, namespace: group) }
let_it_be(:user) { create(:user) }
describe '#resolve' do
let(:start_date) { Date.new(2019, 10, 15) }
let(:end_date) { Date.new(2019, 10, 21) }
let(:args) { { start_date: start_date, end_date: end_date } }
it 'fetches historical vulnerability data from the start date to the end date' do
Timecop.freeze(Date.new(2019, 10, 31)) do
create(:vulnerability_historical_statistic, date: start_date + 1.day, total: 2, critical: 1, high: 1, project: project)
create(:vulnerability_historical_statistic, date: start_date + 2.days, total: 3, critical: 2, high: 1, project: project)
create(:vulnerability_historical_statistic, date: start_date + 4.days, total: 1, critical: 1, high: 0, project: project_2)
expected_history = [
{ 'total' => 0, 'critical' => 0, 'high' => 0, 'medium' => 0, 'low' => 0, 'unknown' => 0, 'info' => 0, 'date' => start_date },
{ 'total' => 2, 'critical' => 1, 'high' => 1, 'medium' => 0, 'low' => 0, 'unknown' => 0, 'info' => 0, 'date' => start_date + 1.day },
{ 'total' => 3, 'critical' => 2, 'high' => 1, 'medium' => 0, 'low' => 0, 'unknown' => 0, 'info' => 0, 'date' => start_date + 2.days },
{ 'total' => 3, 'critical' => 2, 'high' => 1, 'medium' => 0, 'low' => 0, 'unknown' => 0, 'info' => 0, 'date' => start_date + 3.days },
{ 'total' => 1, 'critical' => 1, 'high' => 0, 'medium' => 0, 'low' => 0, 'unknown' => 0, 'info' => 0, 'date' => start_date + 4.days },
{ 'total' => 1, 'critical' => 1, 'high' => 0, 'medium' => 0, 'low' => 0, 'unknown' => 0, 'info' => 0, 'date' => start_date + 5.days },
{ 'total' => 1, 'critical' => 1, 'high' => 0, 'medium' => 0, 'low' => 0, 'unknown' => 0, 'info' => 0, 'date' => end_date }
].as_json
expect(ordered_history.as_json).to match_array(expected_history)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['VulnerabilitiesCountByDay'] do
it { expect(described_class).to have_graphql_fields(:total, :date, :info, :unknown, :low, :medium, :high, :critical) }
end
......@@ -335,6 +335,24 @@ RSpec.describe Group do
end
end
describe '#vulnerability_historical_statistics' do
subject { group.vulnerability_historical_statistics }
let(:subgroup) { create(:group, parent: group) }
let(:group_project) { create(:project, namespace: group) }
let(:subgroup_project) { create(:project, namespace: subgroup) }
let(:archived_project) { create(:project, :archived, namespace: group) }
let(:deleted_project) { create(:project, pending_delete: true, namespace: group) }
let!(:group_vulnerability_historical_statistic) { create(:vulnerability_historical_statistic, project: group_project) }
let!(:subgroup_vulnerability_historical_statistic) { create(:vulnerability_historical_statistic, project: subgroup_project) }
let!(:archived_vulnerability_historical_statistic) { create(:vulnerability_historical_statistic, project: archived_project) }
let!(:deleted_vulnerability_historical_statistic) { create(:vulnerability_historical_statistic, project: deleted_project) }
it 'returns vulnerability scanners for all non-archived, non-deleted projects in the group and its subgroups' do
is_expected.to contain_exactly(group_vulnerability_historical_statistic, subgroup_vulnerability_historical_statistic)
end
end
describe '#mark_ldap_sync_as_failed' do
it 'sets the state to failed' do
group.start_ldap_sync
......
......@@ -138,6 +138,25 @@ RSpec.describe InstanceSecurityDashboard do
end
end
describe '#vulnerability_historical_statistics' do
let_it_be(:vulnerability_historical_statistic_1) { create(:vulnerability_historical_statistic, project: project1) }
let_it_be(:vulnerability_historical_statistic_2) { create(:vulnerability_historical_statistic, project: project2) }
context 'when the user cannot read all resources' do
it 'returns only vulnerability scanners from projects on their dashboard that they can read' do
expect(subject.vulnerability_historical_statistics).to contain_exactly(vulnerability_historical_statistic_1)
end
end
context 'when the user can read all resources' do
let(:user) { create(:auditor) }
it "returns vulnerability scanners from all projects on the user's dashboard" do
expect(subject.vulnerability_historical_statistics).to contain_exactly(vulnerability_historical_statistic_1, vulnerability_historical_statistic_2)
end
end
end
describe '#full_path' do
let(:user) { create(:user) }
......
......@@ -29,4 +29,70 @@ RSpec.describe Vulnerabilities::HistoricalStatistic do
it { is_expected.to match_array([statistic_2, statistic_3]) }
end
describe '.between_dates' do
let_it_be(:start_date) { Date.new(2020, 8, 11) }
let_it_be(:end_date) { Date.new(2020, 8, 13) }
let_it_be(:historical_statistic_1) { create(:vulnerability_historical_statistic, date: start_date - 1.day) }
let_it_be(:historical_statistic_2) { create(:vulnerability_historical_statistic, date: start_date) }
let_it_be(:historical_statistic_3) { create(:vulnerability_historical_statistic, date: start_date + 1.day) }
let_it_be(:historical_statistic_4) { create(:vulnerability_historical_statistic, date: end_date) }
subject { described_class.between_dates(start_date, end_date) }
it { is_expected.to match_array([historical_statistic_2, historical_statistic_3, historical_statistic_4]) }
it { is_expected.not_to include(historical_statistic_1) }
end
describe '.for_project' do
let_it_be(:project) { create(:project) }
let_it_be(:other_project) { create(:project) }
let_it_be(:historical_statistic_1) { create(:vulnerability_historical_statistic, project: project) }
let_it_be(:historical_statistic_2) { create(:vulnerability_historical_statistic, project: other_project) }
subject { described_class.for_project(project) }
it { is_expected.to match_array([historical_statistic_1]) }
end
describe '.grouped_by_date' do
subject { described_class.grouped_by_date(:asc).count }
let_it_be(:date_1) { Date.new(2020, 8, 10) }
let_it_be(:date_2) { Date.new(2020, 8, 11) }
let_it_be(:historical_statistic_1) { create(:vulnerability_historical_statistic, date: date_1) }
let_it_be(:historical_statistic_2) { create(:vulnerability_historical_statistic, date: date_1) }
let_it_be(:historical_statistic_3) { create(:vulnerability_historical_statistic, date: date_2) }
let(:expected_collection) do
{
Date.new(2020, 8, 10) => 2,
Date.new(2020, 8, 11) => 1
}
end
it { is_expected.to match_array(expected_collection) }
end
describe '.aggregated_by_date' do
let_it_be(:date_1) { Date.new(2020, 8, 10) }
let_it_be(:date_2) { Date.new(2020, 8, 11) }
let_it_be(:historical_statistic_1) { create(:vulnerability_historical_statistic, date: date_1, total: 21, info: 1, unknown: 2, low: 3, medium: 4, high: 5, critical: 6) }
let_it_be(:historical_statistic_2) { create(:vulnerability_historical_statistic, date: date_1, total: 21, info: 1, unknown: 2, low: 3, medium: 4, high: 5, critical: 6) }
let_it_be(:historical_statistic_3) { create(:vulnerability_historical_statistic, date: date_2, total: 57, info: 7, unknown: 8, low: 9, medium: 10, high: 11, critical: 12) }
subject { described_class.grouped_by_date(:asc).aggregated_by_date.as_json }
let(:expected_collection) do
[
{ 'id' => nil, 'date' => '2020-08-10', 'info' => 2, 'unknown' => 4, 'low' => 6, 'medium' => 8, 'high' => 10, 'critical' => 12, 'total' => 42 },
{ 'id' => nil, 'date' => '2020-08-11', 'info' => 7, 'unknown' => 8, 'low' => 9, 'medium' => 10, 'high' => 11, 'critical' => 12, 'total' => 57 }
]
end
it { is_expected.to match_array(expected_collection) }
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