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 { ...@@ -5926,7 +5926,43 @@ type Group {
): VulnerabilityConnection ): 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( vulnerabilitiesCountByDayAndSeverity(
""" """
...@@ -5958,7 +5994,7 @@ type Group { ...@@ -5958,7 +5994,7 @@ type Group {
First day for which to fetch vulnerability history First day for which to fetch vulnerability history
""" """
startDate: ISO8601Date! startDate: ISO8601Date!
): VulnerabilitiesCountByDayAndSeverityConnection ): VulnerabilitiesCountByDayAndSeverityConnection @deprecated(reason: "Use `vulnerabilitiesCountByDay`. Deprecated in 13.3")
""" """
Represents vulnerable project counts for each grade Represents vulnerable project counts for each grade
...@@ -11552,7 +11588,44 @@ type Query { ...@@ -11552,7 +11588,44 @@ type Query {
): VulnerabilityConnection ): 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( vulnerabilitiesCountByDayAndSeverity(
""" """
...@@ -11584,7 +11657,7 @@ type Query { ...@@ -11584,7 +11657,7 @@ type Query {
First day for which to fetch vulnerability history First day for which to fetch vulnerability history
""" """
startDate: ISO8601Date! startDate: ISO8601Date!
): VulnerabilitiesCountByDayAndSeverityConnection ): VulnerabilitiesCountByDayAndSeverityConnection @deprecated(reason: "Use `vulnerabilitiesCountByDay`. Deprecated in 13.3")
} }
""" """
...@@ -15460,6 +15533,51 @@ enum VisibilityScopesEnum { ...@@ -15460,6 +15533,51 @@ enum VisibilityScopesEnum {
public 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 Represents the number of vulnerabilities for a particular severity on a particular day
""" """
...@@ -15515,6 +15633,41 @@ type VulnerabilitiesCountByDayAndSeverityEdge { ...@@ -15515,6 +15633,41 @@ type VulnerabilitiesCountByDayAndSeverityEdge {
node: VulnerabilitiesCountByDayAndSeverity 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. Represents a vulnerability.
""" """
......
...@@ -16325,8 +16325,8 @@ ...@@ -16325,8 +16325,8 @@
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "vulnerabilitiesCountByDayAndSeverity", "name": "vulnerabilitiesCountByDay",
"description": "Number of vulnerabilities per severity level, per day, for the projects in the group and its subgroups", "description": "Number of vulnerabilities per day for the projects in the group and its subgroups",
"args": [ "args": [
{ {
"name": "startDate", "name": "startDate",
...@@ -16399,12 +16399,93 @@ ...@@ -16399,12 +16399,93 @@
], ],
"type": { "type": {
"kind": "OBJECT", "kind": "OBJECT",
"name": "VulnerabilitiesCountByDayAndSeverityConnection", "name": "VulnerabilitiesCountByDayConnection",
"ofType": null "ofType": null
}, },
"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. 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", "name": "vulnerabilityGrades",
"description": "Represents vulnerable project counts for each grade", "description": "Represents vulnerable project counts for each grade",
...@@ -33983,8 +34064,8 @@ ...@@ -33983,8 +34064,8 @@
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "vulnerabilitiesCountByDayAndSeverity", "name": "vulnerabilitiesCountByDay",
"description": "Number of vulnerabilities per severity level, per day, for the projects on the current user's instance security dashboard", "description": "Number of vulnerabilities per day for the projects on the current user's instance security dashboard",
"args": [ "args": [
{ {
"name": "startDate", "name": "startDate",
...@@ -34057,11 +34138,92 @@ ...@@ -34057,11 +34138,92 @@
], ],
"type": { "type": {
"kind": "OBJECT", "kind": "OBJECT",
"name": "VulnerabilitiesCountByDayAndSeverityConnection", "name": "VulnerabilitiesCountByDayConnection",
"ofType": null "ofType": null
}, },
"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. 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, "inputFields": null,
...@@ -45482,6 +45644,163 @@ ...@@ -45482,6 +45644,163 @@
], ],
"possibleTypes": null "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", "kind": "OBJECT",
"name": "VulnerabilitiesCountByDayAndSeverity", "name": "VulnerabilitiesCountByDayAndSeverity",
...@@ -45649,6 +45968,118 @@ ...@@ -45649,6 +45968,118 @@
"enumValues": null, "enumValues": null,
"possibleTypes": 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", "kind": "OBJECT",
"name": "Vulnerability", "name": "Vulnerability",
...@@ -2294,6 +2294,21 @@ Autogenerated return type of UpdateSnippet ...@@ -2294,6 +2294,21 @@ 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 |
## 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 ## VulnerabilitiesCountByDayAndSeverity
Represents the number of vulnerabilities for a particular severity on a particular day 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_ ...@@ -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 VulnerabilityChart from 'ee/security_dashboard/components/first_class_vulnerability_chart.vue';
import CsvExportButton from './csv_export_button.vue'; import CsvExportButton from './csv_export_button.vue';
import VulnerabilitySeverity from './vulnerability_severity.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'; import DashboardNotConfigured from './empty_states/group_dashboard_not_configured.vue';
export default { export default {
......
...@@ -10,7 +10,7 @@ import Filters from 'ee/security_dashboard/components/first_class_vulnerability_ ...@@ -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 projectsQuery from 'ee/security_dashboard/graphql/get_instance_security_dashboard_projects.query.graphql';
import ProjectManager from './first_class_project_manager/project_manager.vue'; import ProjectManager from './first_class_project_manager/project_manager.vue';
import CsvExportButton from './csv_export_button.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'; import DashboardNotConfigured from './empty_states/instance_dashboard_not_configured.vue';
export default { export default {
......
...@@ -12,7 +12,6 @@ import { SEVERITY_LEVELS } from '../store/constants'; ...@@ -12,7 +12,6 @@ import { SEVERITY_LEVELS } from '../store/constants';
const ISO_DATE = 'isoDate'; const ISO_DATE = 'isoDate';
const DAYS = { thirty: 30, sixty: 60, ninety: 90 }; const DAYS = { thirty: 30, sixty: 60, ninety: 90 };
const MAX_DAY_INTERVAL_ALLOWED_BY_BACKEND = 10;
export default { export default {
components: { components: {
...@@ -33,7 +32,6 @@ export default { ...@@ -33,7 +32,6 @@ export default {
vulnerabilitiesHistory: {}, vulnerabilitiesHistory: {},
vulnerabilitiesHistoryDayRange: DAYS.thirty, vulnerabilitiesHistoryDayRange: DAYS.thirty,
errorLoadingVulnerabilitiesHistory: false, errorLoadingVulnerabilitiesHistory: false,
currentStartDateCursor: undefined,
}; };
}, },
days: Object.values(DAYS), days: Object.values(DAYS),
...@@ -62,18 +60,13 @@ export default { ...@@ -62,18 +60,13 @@ export default {
variables() { variables() {
return { return {
fullPath: this.groupFullPath, fullPath: this.groupFullPath,
startDate: formatDate(new Date(this.startDateCursor), ISO_DATE), startDate: formatDate(new Date(this.startDate), ISO_DATE),
endDate: this.formattedEndDateCursor, endDate: this.formattedEndDateCursor,
}; };
}, },
update(results) { update(results) {
return this.processRawData(results); return this.processRawData(results);
}, },
result() {
if (this.formattedEndDateCursor !== formatDate(new Date(), ISO_DATE)) {
this.currentStartDateCursor = this.endDateCursor;
}
},
error() { error() {
this.errorLoadingVulnerabilitiesHistory = true; this.errorLoadingVulnerabilitiesHistory = true;
}, },
...@@ -83,14 +76,8 @@ export default { ...@@ -83,14 +76,8 @@ export default {
startDate() { startDate() {
return Date.now() - DAY_IN_MS * this.vulnerabilitiesHistoryDayRange; return Date.now() - DAY_IN_MS * this.vulnerabilitiesHistoryDayRange;
}, },
startDateCursor() {
return this.currentStartDateCursor || this.startDate;
},
endDateCursor() { endDateCursor() {
return Math.min( return Date.now();
this.startDateCursor + DAY_IN_MS * (MAX_DAY_INTERVAL_ALLOWED_BY_BACKEND - 1),
Date.now(),
);
}, },
formattedEndDateCursor() { formattedEndDateCursor() {
return formatDate(new Date(this.endDateCursor), ISO_DATE); return formatDate(new Date(this.endDateCursor), ISO_DATE);
...@@ -124,7 +111,7 @@ export default { ...@@ -124,7 +111,7 @@ export default {
}, },
}, },
watch: { watch: {
startDateCursor() { startDate() {
this.$apollo.queries.vulnerabilitiesHistory.refetch(); this.$apollo.queries.vulnerabilitiesHistory.refetch();
}, },
}, },
...@@ -132,20 +119,22 @@ export default { ...@@ -132,20 +119,22 @@ export default {
setVulnerabilitiesHistoryDayRange(days) { setVulnerabilitiesHistoryDayRange(days) {
this.vulnerabilitiesHistory = {}; this.vulnerabilitiesHistory = {};
this.vulnerabilitiesHistoryDayRange = days; this.vulnerabilitiesHistoryDayRange = days;
this.currentStartDateCursor = undefined;
}, },
processRawData(results) { processRawData(results) {
let { vulnerabilitiesCountByDayAndSeverity } = results; let { vulnerabilitiesCountByDay } = results;
if (this.groupFullPath) { if (this.groupFullPath) {
vulnerabilitiesCountByDayAndSeverity = results.group.vulnerabilitiesCountByDayAndSeverity; vulnerabilitiesCountByDay = results.group.vulnerabilitiesCountByDay;
} }
const vulnerabilitiesData = vulnerabilitiesCountByDayAndSeverity.nodes.reduce( const vulnerabilitiesData = vulnerabilitiesCountByDay.nodes.reduce(
(acc, v) => { (acc, v) => {
const severity = v.severity.toLowerCase(); const { date, ...severities } = v;
acc[severity] = acc[severity] || {}; Object.keys(severities).forEach(severity => {
acc[severity][v.day] = v.count; acc[severity] = acc[severity] || {};
acc[severity][date] = v[severity];
}, {});
return acc; return acc;
}, },
{ ...this.vulnerabilitiesHistory }, { ...this.vulnerabilitiesHistory },
......
query groupVulnerabilityHistory($fullPath: ID!, $startDate: ISO8601Date!, $endDate: ISO8601Date!) { query groupVulnerabilityHistory($fullPath: ID!, $startDate: ISO8601Date!, $endDate: ISO8601Date!) {
group(fullPath: $fullPath) { group(fullPath: $fullPath) {
vulnerabilitiesCountByDayAndSeverity(startDate: $startDate, endDate: $endDate) { vulnerabilitiesCountByDay(startDate: $startDate, endDate: $endDate) {
nodes { nodes {
day date
count critical
severity high
medium
low
} }
} }
} }
......
query instanceVulnerabilityHistory($startDate: ISO8601Date!, $endDate: ISO8601Date!) { query instanceVulnerabilityHistory($startDate: ISO8601Date!, $endDate: ISO8601Date!) {
vulnerabilitiesCountByDayAndSeverity(startDate: $startDate, endDate: $endDate) { vulnerabilitiesCountByDay(startDate: $startDate, endDate: $endDate) {
nodes { nodes {
day date
count critical
severity high
medium
low
} }
} }
} }
...@@ -42,11 +42,18 @@ module EE ...@@ -42,11 +42,18 @@ module EE
description: 'Vulnerability scanners reported on the project vulnerabilties of the group and its subgroups', description: 'Vulnerability scanners reported on the project vulnerabilties of the group and its subgroups',
resolver: ::Resolvers::Vulnerabilities::ScannersResolver 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, field :vulnerabilities_count_by_day_and_severity,
::Types::VulnerabilitiesCountByDayAndSeverityType.connection_type, ::Types::VulnerabilitiesCountByDayAndSeverityType.connection_type,
null: true, null: true,
description: 'Number of vulnerabilities per severity level, per day, for the projects in the group and its subgroups', 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, field :vulnerability_grades,
[::Types::VulnerableProjectsByGradeType], [::Types::VulnerableProjectsByGradeType],
......
...@@ -12,11 +12,18 @@ module EE ...@@ -12,11 +12,18 @@ 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,
::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, field :vulnerabilities_count_by_day_and_severity,
::Types::VulnerabilitiesCountByDayAndSeverityType.connection_type, ::Types::VulnerabilitiesCountByDayAndSeverityType.connection_type,
null: true, null: true,
description: "Number of vulnerabilities per severity level, per day, for the projects on the current user's instance security dashboard", 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, field :geo_node, ::Types::Geo::GeoNodeType,
null: true, 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 ...@@ -342,6 +342,11 @@ module EE
) )
end 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 def max_personal_access_token_lifetime_from_now
if max_personal_access_token_lifetime.present? if max_personal_access_token_lifetime.present?
max_personal_access_token_lifetime.days.from_now max_personal_access_token_lifetime.days.from_now
......
...@@ -38,6 +38,12 @@ class InstanceSecurityDashboard ...@@ -38,6 +38,12 @@ class InstanceSecurityDashboard
Vulnerabilities::Scanner.for_projects(projects) Vulnerabilities::Scanner.for_projects(projects)
end end
def vulnerability_historical_statistics
return Vulnerabilities::Scanner.none if projects.empty?
Vulnerabilities::HistoricalStatistic.for_project(projects)
end
private private
attr_reader :project_ids, :user attr_reader :project_ids, :user
......
...@@ -21,5 +21,18 @@ module Vulnerabilities ...@@ -21,5 +21,18 @@ module Vulnerabilities
enum letter_grade: Vulnerabilities::Statistic.letter_grades enum letter_grade: Vulnerabilities::Statistic.letter_grades
scope :older_than, ->(days:) { where('"vulnerability_historical_statistics"."date" < now() - interval ?', "#{days} days") } 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
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', () => { ...@@ -7,11 +7,11 @@ describe('First class vulnerability chart component', () => {
let wrapper; let wrapper;
const responseData = { const responseData = {
vulnerabilitiesCountByDayAndSeverity: { vulnerabilitiesCountByDay: {
nodes: [ nodes: [
{ day: '2020-05-18', count: 5, severity: 'MEDIUM' }, { date: '2020-05-18', medium: 5 },
{ day: '2020-05-19', count: 2, severity: 'MEDIUM' }, { date: '2020-05-19', medium: 2 },
{ day: '2020-05-18', count: 2, severity: 'CRITICAL' }, { 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 ...@@ -335,6 +335,24 @@ RSpec.describe Group do
end end
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 describe '#mark_ldap_sync_as_failed' do
it 'sets the state to failed' do it 'sets the state to failed' do
group.start_ldap_sync group.start_ldap_sync
......
...@@ -138,6 +138,25 @@ RSpec.describe InstanceSecurityDashboard do ...@@ -138,6 +138,25 @@ RSpec.describe InstanceSecurityDashboard do
end end
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 describe '#full_path' do
let(:user) { create(:user) } let(:user) { create(:user) }
......
...@@ -29,4 +29,70 @@ RSpec.describe Vulnerabilities::HistoricalStatistic do ...@@ -29,4 +29,70 @@ RSpec.describe Vulnerabilities::HistoricalStatistic do
it { is_expected.to match_array([statistic_2, statistic_3]) } it { is_expected.to match_array([statistic_2, statistic_3]) }
end 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 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