From 52940b5cd0ad2a797cee68979e2eb9f9c7037cfb Mon Sep 17 00:00:00 2001 From: Ahmed <144101267+a-h-abdelsalam@users.noreply.github.com> Date: Fri, 23 Aug 2024 15:10:26 +0200 Subject: [PATCH] Add: Added a dedicated view for compliance audit reports (#3952) * Compliance reports are now listed under Resilience tab and do not appear anymore under Scans tab. A dedicated view for a compliance report shows compliance of results. * This feature is currently behind a feature toggle: COMPLIANCE_REPORTS --- allowedSnakeCase.cjs | 3 + public/locales/gsa-de.json | 9 + src/gmp/capabilities/capabilities.js | 15 +- src/gmp/commands/__tests__/auditreport.js | 33 + src/gmp/commands/__tests__/auditreports.js | 92 ++ src/gmp/commands/__tests__/reports.js | 2 + src/gmp/commands/auditreports.js | 139 +++ src/gmp/commands/reports.js | 1 + src/gmp/commands/users.js | 2 + src/gmp/gmp.js | 1 + src/gmp/models/auditreport.js | 65 ++ src/gmp/models/filter.js | 2 + src/gmp/models/filter/keywords.js | 2 + src/gmp/models/report/auditreport.js | 140 +++ src/gmp/models/report/host.js | 39 +- src/gmp/models/report/os.js | 26 + src/gmp/models/report/parser.js | 13 +- src/gmp/models/result.js | 5 + src/gmp/utils/entitytype.js | 5 + src/web/components/bar/compliancebar.jsx | 44 + .../components/bar/compliancestatusbar.jsx | 3 +- src/web/components/bar/menubar.jsx | 101 +- .../dashboard/display/createDisplay.jsx | 9 +- .../components/dashboard/display/utils.jsx | 6 + src/web/components/label/compliancestate.jsx | 55 + .../__tests__/compliancelevelsgroup.jsx | 239 +++++ .../powerfilter/compliancelevelsgroup.jsx | 105 ++ src/web/pages/audits/__tests__/listpage.jsx | 8 +- src/web/pages/audits/__tests__/row.jsx | 106 +- src/web/pages/audits/__tests__/table.jsx | 2 +- src/web/pages/audits/row.jsx | 30 +- src/web/pages/audits/table.jsx | 4 +- src/web/pages/filters/component.jsx | 9 +- src/web/pages/policies/__tests__/listpage.jsx | 8 +- src/web/pages/policies/table.jsx | 2 +- .../__mocks__/mockauditdeltareport.jsx | 388 ++++++++ .../reports/__mocks__/mockauditreport.jsx | 370 +++++++ .../__tests__/auditdeltadetailspage.jsx | 377 +++++++ .../reports/__tests__/auditdetailscontent.jsx | 394 ++++++++ .../reports/__tests__/auditfilterdialog.jsx | 66 ++ .../reports/__tests__/auditreportrow.jsx | 54 + .../__tests__/auditreportslistpage.jsx | 267 +++++ .../reports/__tests__/detailsfilterdialog.jsx | 119 +++ .../pages/reports/auditdashboard/index.jsx | 36 + .../pages/reports/auditdashboard/loaders.jsx | 30 + .../reports/auditdashboard/statusdisplay.jsx | 89 ++ .../pages/reports/auditdeltadetailspage.jsx | 565 +++++++++++ src/web/pages/reports/auditdetailscontent.jsx | 448 +++++++++ src/web/pages/reports/auditdetailspage.jsx | 705 +++++++++++++ src/web/pages/reports/auditfilterdialog.jsx | 134 +++ src/web/pages/reports/auditreportrow.jsx | 147 +++ .../pages/reports/auditreportslistpage.jsx | 169 ++++ src/web/pages/reports/auditreportstable.jsx | 130 +++ src/web/pages/reports/deltadetailscontent.jsx | 20 +- src/web/pages/reports/deltadetailspage.jsx | 17 - .../reports/details/__tests__/hoststab.jsx | 105 ++ .../details/__tests__/operatingsystemstab.jsx | 83 ++ .../reports/details/__tests__/resultstab.jsx | 369 +++++++ .../reports/details/auditthresholdpanel.jsx | 150 +++ .../pages/reports/details/deltaresultstab.jsx | 4 + .../reports/details/emptyresultsreport.jsx | 8 +- src/web/pages/reports/details/hoststab.jsx | 10 + src/web/pages/reports/details/hoststable.jsx | 226 +++-- .../reports/details/operatingsystemstab.jsx | 4 + .../reports/details/operatingsystemstable.jsx | 55 +- src/web/pages/reports/details/resultstab.jsx | 115 ++- .../pages/reports/details/toolbaricons.jsx | 24 +- src/web/pages/reports/detailsfilterdialog.jsx | 52 +- src/web/pages/results/__tests__/row.jsx | 144 +++ src/web/pages/results/row.jsx | 38 +- src/web/pages/results/table.jsx | 32 +- src/web/pages/start/dashboard.jsx | 19 +- src/web/pages/tags/component.jsx | 10 +- src/web/pages/tags/dialog.jsx | 1 + src/web/pages/tasks/status.jsx | 15 +- src/web/pages/usersettings/dialog.jsx | 17 +- src/web/pages/usersettings/filterpart.jsx | 21 +- .../pages/usersettings/usersettingspage.jsx | 18 + src/web/routes.jsx | 16 + src/web/store/entities/__tests__/reducers.js | 2 + src/web/store/entities/auditreports.js | 62 ++ src/web/store/entities/reducers.js | 6 + .../entities/report/__tests__/actions.js | 941 +++++++++++++++++- src/web/store/entities/report/actions.js | 332 ++++-- src/web/store/entities/report/reducers.js | 2 +- src/web/store/entities/report/selectors.js | 6 + src/web/store/entities/reports/reducers.js | 2 +- src/web/utils/theme.jsx | 5 + 88 files changed, 8335 insertions(+), 409 deletions(-) create mode 100644 src/gmp/commands/__tests__/auditreport.js create mode 100644 src/gmp/commands/__tests__/auditreports.js create mode 100644 src/gmp/commands/auditreports.js create mode 100644 src/gmp/models/auditreport.js create mode 100644 src/gmp/models/report/auditreport.js create mode 100644 src/web/components/bar/compliancebar.jsx create mode 100644 src/web/components/label/compliancestate.jsx create mode 100644 src/web/components/powerfilter/__tests__/compliancelevelsgroup.jsx create mode 100644 src/web/components/powerfilter/compliancelevelsgroup.jsx create mode 100644 src/web/pages/reports/__mocks__/mockauditdeltareport.jsx create mode 100644 src/web/pages/reports/__mocks__/mockauditreport.jsx create mode 100644 src/web/pages/reports/__tests__/auditdeltadetailspage.jsx create mode 100644 src/web/pages/reports/__tests__/auditdetailscontent.jsx create mode 100644 src/web/pages/reports/__tests__/auditfilterdialog.jsx create mode 100644 src/web/pages/reports/__tests__/auditreportrow.jsx create mode 100644 src/web/pages/reports/__tests__/auditreportslistpage.jsx create mode 100644 src/web/pages/reports/__tests__/detailsfilterdialog.jsx create mode 100644 src/web/pages/reports/auditdashboard/index.jsx create mode 100644 src/web/pages/reports/auditdashboard/loaders.jsx create mode 100644 src/web/pages/reports/auditdashboard/statusdisplay.jsx create mode 100644 src/web/pages/reports/auditdeltadetailspage.jsx create mode 100644 src/web/pages/reports/auditdetailscontent.jsx create mode 100644 src/web/pages/reports/auditdetailspage.jsx create mode 100644 src/web/pages/reports/auditfilterdialog.jsx create mode 100644 src/web/pages/reports/auditreportrow.jsx create mode 100644 src/web/pages/reports/auditreportslistpage.jsx create mode 100644 src/web/pages/reports/auditreportstable.jsx create mode 100644 src/web/pages/reports/details/__tests__/resultstab.jsx create mode 100644 src/web/pages/reports/details/auditthresholdpanel.jsx create mode 100644 src/web/store/entities/auditreports.js diff --git a/allowedSnakeCase.cjs b/allowedSnakeCase.cjs index b257daf6f7..8bc6e4a50e 100644 --- a/allowedSnakeCase.cjs +++ b/allowedSnakeCase.cjs @@ -29,6 +29,8 @@ module.exports = [ '_asset_id', 'asset_id', 'assigned_to', + 'audit_report', + 'audit_reports', 'auth_algorithm', 'auth_conf_setting', 'auth_method', @@ -248,6 +250,7 @@ module.exports = [ 'highest_severity', 'high_per_host', 'host_allow', + 'host_compliance', 'host_cves', 'hostnames_by_ip', 'hosts_allow', diff --git a/public/locales/gsa-de.json b/public/locales/gsa-de.json index 24374558ee..a302ea7032 100644 --- a/public/locales/gsa-de.json +++ b/public/locales/gsa-de.json @@ -158,6 +158,9 @@ "Audit made visible for:\n{{user}}\n{{role}}\n{{group}}": "Audit sichtbar gemacht für:\n{{user}}\n{{role}}\n{{group}}", "Audit: {{name}}": "Audit: {{name}}", "Audits": "Audits", + "Audit Reports by Compliance (Total: {{count}})": "Audit-Berichte nach Compliance (Gesamt: {{count}})", + "Audit Reports": "Audit-Berichte", + "Audit Report": "Audit-Bericht", "Audits using this Policy": "Audits, die diese Richtlinie verwenden", "Auth": "Auth.", "Auth Algorithm": "Auth-Algorithmus", @@ -243,6 +246,7 @@ "Certificate in use will expire at {{date}}": "Aktuelles Zertifikat wird am {{date}} ablaufen", "Change Password": "Passwort ändern", "Changed": "Verändert", + "Chart: Audit Reports by Compliance": "Diagramm: Audit-Berichte nach Compliance", "Chart: CERT-Bund Advisories by CVSS": "Diagramm: CERT-Bund-Advisories nach CVSS", "Chart: CERT-Bund Advisories by Creation Time": "Diagramm: CERT-Bund-Advisories nach Erstellungszeit", "Chart: CERT-Bund Advisories by Severity Class": "Diagramm: CERT-Bund-Advisories nach Schweregradklasse", @@ -323,6 +327,8 @@ "Complete": "Vollständig", "Complexity": "Komplexität", "Compliance Audits": "Compliance Audits", + "Compliance Audit Reports": "Compliance-Audit-Berichte", + "Compliance Percent": "Compliance-Prozent", "Compliance Policies": "Compliance Richtlinien", "Compliance Status": "Compliance Status", "Compose": "Zusammenstellen", @@ -841,6 +847,7 @@ "Include log messages in your filter settings.": "Log-Nachrichten in die Filtereinstellungen einbeziehen.", "Include report": "Bericht einfügen", "Included": "Beinhaltet", + "Incomplete": "Unvollständig", "Info": "Info", "Information": "Informationen", "Inheriting user": "Erbender Benutzer", @@ -1543,6 +1550,7 @@ "TLS Certificates by Modification Time (Total: {{count}})": "TLS-Zertifikate nach Änderungszeit (Gesamt: {{count}})", "TLS Certificates by Status (Total: {{count}})": "TLS-Zertifikate nach Status (Gesamt: {{count}})", "TLS Certificates for this Host": "TLS-Zertifikate für diesen Host", + "Table: Audit Reports by Compliance": "Tabelle: Audit-Berichte nach Compliance", "Table: CERT-Bund Advisories by CVSS": "Tabelle: CERT-Bund-Advisories nach CVSS", "Table: CERT-Bund Advisories by Creation Time": "Tabelle: CERT-Bund-Advisories nach Erstellungszeit", "Table: CERT-Bund Advisories by Severity Class": "Tabelle: CERT-Bund-Advisories nach Schweregradklasse", @@ -1908,6 +1916,7 @@ "task": "Aufgabe", "to": "für", "undefined": "undefiniert", + "Undefined": "Undefiniert", "until {{- enddate}}": "bis {{- enddate}}", "verinice Connector": "verinice-Konnektor", "verinice.PRO Connector": "verinice.PRO-Konnektor", diff --git a/src/gmp/capabilities/capabilities.js b/src/gmp/capabilities/capabilities.js index f8baa0e991..297f9e8fc9 100644 --- a/src/gmp/capabilities/capabilities.js +++ b/src/gmp/capabilities/capabilities.js @@ -9,8 +9,8 @@ import {pluralizeType} from 'gmp/utils/entitytype'; import {parseBoolean} from 'gmp/parser'; const types = { - audit: 'task', - audits: 'task', + auditreport: 'audit_report', + auditreports: 'audit_reports', host: 'asset', hosts: 'asset', os: 'asset', @@ -42,12 +42,19 @@ const types = { tlscertificates: 'tls_certificate', }; +const subtypes = { + audit: 'task', + audits: 'task', + audit_report: 'report', + audit_reports: 'reports', +}; + const convertType = type => { const ctype = types[type]; if (isDefined(ctype)) { - return ctype; + type = ctype; } - return type; + return subtypes[type] || type; }; class Capabilities { diff --git a/src/gmp/commands/__tests__/auditreport.js b/src/gmp/commands/__tests__/auditreport.js new file mode 100644 index 0000000000..d6eef9ec28 --- /dev/null +++ b/src/gmp/commands/__tests__/auditreport.js @@ -0,0 +1,33 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect} from '@gsa/testing'; +import {createHttp, createEntityResponse} from 'gmp/commands/testing'; +import {AuditReportCommand} from 'gmp/commands/auditreports'; + +describe('AuditReportCommand tests', () => { + test('should request single audit report', () => { + const response = createEntityResponse('report', {_id: 'foo'}); + const fakeHttp = createHttp(response); + + expect.hasAssertions(); + + const cmd = new AuditReportCommand(fakeHttp); + return cmd.get({id: 'foo'}).then(resp => { + expect(fakeHttp.request).toHaveBeenCalledWith('get', { + args: { + cmd: 'get_report', + report_id: 'foo', + ignore_pagination: 1, + details: 1, + lean: 1, + }, + }); + + const {data} = resp; + expect(data.id).toEqual('foo'); + }); + }); +}); diff --git a/src/gmp/commands/__tests__/auditreports.js b/src/gmp/commands/__tests__/auditreports.js new file mode 100644 index 0000000000..1169cfd917 --- /dev/null +++ b/src/gmp/commands/__tests__/auditreports.js @@ -0,0 +1,92 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect} from '@gsa/testing'; +import {ALL_FILTER} from 'gmp/models/filter'; + +import { + createHttp, + createEntitiesResponse, + createAggregatesResponse, +} from '../testing'; +import {AuditReportsCommand} from 'gmp/commands/auditreports'; + +describe('AuditReportsCommand tests', () => { + test('should return all audit reports', () => { + const response = createEntitiesResponse('report', [ + { + _id: '1', + }, + { + _id: '2', + }, + ]); + + const fakeHttp = createHttp(response); + + expect.hasAssertions(); + + const cmd = new AuditReportsCommand(fakeHttp); + return cmd.getAll().then(resp => { + expect(fakeHttp.request).toHaveBeenCalledWith('get', { + args: { + cmd: 'get_reports', + details: 0, + filter: ALL_FILTER.toFilterString(), + usage_type: 'audit', + }, + }); + const {data} = resp; + expect(data.length).toEqual(2); + }); + }); + + test('should return results', () => { + const response = createEntitiesResponse('report', [ + { + _id: '1', + }, + { + _id: '2', + }, + ]); + + const fakeHttp = createHttp(response); + + expect.hasAssertions(); + + const cmd = new AuditReportsCommand(fakeHttp); + return cmd.get().then(resp => { + expect(fakeHttp.request).toHaveBeenCalledWith('get', { + args: { + cmd: 'get_reports', + details: 0, + usage_type: 'audit', + }, + }); + const {data} = resp; + expect(data.length).toEqual(2); + }); + }); + + test('should aggregate compliance counts', () => { + const response = createAggregatesResponse(); + const fakeHttp = createHttp(response); + + expect.hasAssertions(); + + const cmd = new AuditReportsCommand(fakeHttp); + return cmd.getComplianceAggregates().then(resp => { + expect(fakeHttp.request).toHaveBeenCalledWith('get', { + args: { + cmd: 'get_aggregate', + aggregate_type: 'report', + group_column: 'compliant', + usage_type: 'audit', + }, + }); + }); + }); +}); diff --git a/src/gmp/commands/__tests__/reports.js b/src/gmp/commands/__tests__/reports.js index 07d7c03968..97cc6d922b 100644 --- a/src/gmp/commands/__tests__/reports.js +++ b/src/gmp/commands/__tests__/reports.js @@ -32,6 +32,7 @@ describe('ReportsCommand tests', () => { cmd: 'get_reports', details: 0, filter: ALL_FILTER.toFilterString(), + usage_type: 'scan', }, }); const {data} = resp; @@ -59,6 +60,7 @@ describe('ReportsCommand tests', () => { args: { cmd: 'get_reports', details: 0, + usage_type: 'scan', }, }); const {data} = resp; diff --git a/src/gmp/commands/auditreports.js b/src/gmp/commands/auditreports.js new file mode 100644 index 0000000000..dcb6ccbebd --- /dev/null +++ b/src/gmp/commands/auditreports.js @@ -0,0 +1,139 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {isDefined} from 'gmp/utils/identity'; + +import registerCommand from 'gmp/command'; + +import AuditReport from 'gmp/models/auditreport'; + +import {ALL_FILTER} from 'gmp/models/filter'; + +import DefaultTransform from 'gmp/http/transform/default'; + +import {convertBoolean} from './convert'; +import EntitiesCommand from './entities'; +import EntityCommand from './entity'; + +export class AuditReportsCommand extends EntitiesCommand { + constructor(http) { + super(http, 'report', AuditReport); + } + + getEntitiesResponse(root) { + return root.get_reports.get_reports_response; + } + + getComplianceAggregates({filter} = {}) { + return this.getAggregates({ + aggregate_type: 'report', + group_column: 'compliant', + usage_type: 'audit', + filter, + }); + } + + get(params, options) { + return super.get( + { + details: 0, + ...params, + usage_type: 'audit', + }, + options, + ); + } +} + +export class AuditReportCommand extends EntityCommand { + constructor(http) { + super(http, 'report', AuditReport); + } + + download({id}, {reportFormatId, deltaReportId, filter}) { + return this.httpGet( + { + cmd: 'get_report', + delta_report_id: deltaReportId, + details: 1, + report_id: id, + report_format_id: reportFormatId, + filter: isDefined(filter) ? filter.all() : ALL_FILTER, + }, + {transform: DefaultTransform, responseType: 'arraybuffer'}, + ); + } + + addAssets({id}, {filter = ''}) { + return this.httpPost({ + cmd: 'create_asset', + report_id: id, + filter, + }); + } + + removeAssets({id}, {filter = ''}) { + return this.httpPost({ + cmd: 'delete_asset', + report_id: id, + filter, + }); + } + + alert({alert_id, report_id, filter}) { + return this.httpPost({ + cmd: 'report_alert', + alert_id, + report_id, + filter, + }); + } + + getDelta( + {id}, + {id: delta_report_id}, + {filter, details = true, ...options} = {}, + ) { + return this.httpGet( + { + id, + delta_report_id, + filter, + ignore_pagination: 1, + details: convertBoolean(details), + }, + options, + ).then(this.transformResponse); + } + + get( + {id}, + { + filter, + details = true, + ignorePagination = true, + lean = true, + ...options + } = {}, + ) { + return this.httpGet( + { + id, + filter, + lean: convertBoolean(lean), + ignore_pagination: convertBoolean(ignorePagination), + details: convertBoolean(details), + }, + options, + ).then(this.transformResponse); + } + + getElementFromRoot(root) { + return root.get_report.get_reports_response.report; + } +} + +registerCommand('auditreport', AuditReportCommand); +registerCommand('auditreports', AuditReportsCommand); diff --git a/src/gmp/commands/reports.js b/src/gmp/commands/reports.js index a1027e9160..b46fe58a23 100644 --- a/src/gmp/commands/reports.js +++ b/src/gmp/commands/reports.js @@ -52,6 +52,7 @@ export class ReportsCommand extends EntitiesCommand { { details: 0, // ensure to request no details by default ...params, + usage_type: 'scan', }, options, ); diff --git a/src/gmp/commands/users.js b/src/gmp/commands/users.js index c94c4077e7..9a70662bcb 100644 --- a/src/gmp/commands/users.js +++ b/src/gmp/commands/users.js @@ -38,6 +38,7 @@ export const ROWS_PER_PAGE_SETTING_ID = '5f5a8712-8017-11e1-8556-406186ea4fc5'; export const DEFAULT_FILTER_SETTINGS = { alert: 'b833a6f2-dcdc-4535-bfb0-a5154b5b5092', asset: '0f040d06-abf9-43a2-8f94-9de178b0e978', + auditreport: '45414da7-55f0-44c1-abbb-6b7d1126fbdf', certbund: 'e4cf514a-17e2-4ab9-9c90-336f15e24750', cpe: '3414a107-ae46-4dea-872d-5c4479a48e8f', credential: '186a5ac8-fe5a-4fb1-aa22-44031fb339f3', @@ -286,6 +287,7 @@ export class UserCommand extends EntityCommand { data.defaultTarget, [saveDefaultFilterSettingId('alert')]: data.alertsFilter, [saveDefaultFilterSettingId('asset')]: data.assetsFilter, + [saveDefaultFilterSettingId('auditreport')]: data.auditReportsFilter, [saveDefaultFilterSettingId('scanconfig')]: data.configsFilter, [saveDefaultFilterSettingId('credential')]: data.credentialsFilter, [saveDefaultFilterSettingId('filter')]: data.filtersFilter, diff --git a/src/gmp/gmp.js b/src/gmp/gmp.js index fa52a2b89a..b233734851 100644 --- a/src/gmp/gmp.js +++ b/src/gmp/gmp.js @@ -11,6 +11,7 @@ import logger from 'gmp/log'; import 'gmp/commands/alerts'; import 'gmp/commands/audits'; +import 'gmp/commands/auditreports'; import 'gmp/commands/auth'; import 'gmp/commands/certbund'; import 'gmp/commands/credentials'; diff --git a/src/gmp/models/auditreport.js b/src/gmp/models/auditreport.js new file mode 100644 index 0000000000..62438d2071 --- /dev/null +++ b/src/gmp/models/auditreport.js @@ -0,0 +1,65 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {_l} from 'gmp/locale/lang'; + +import {isDefined} from 'gmp/utils/identity'; + +import {parseDate} from 'gmp/parser'; + +import Model, {parseModelFromElement} from 'gmp/model'; + +import AuditReportReport from './report/auditreport'; + +export const COMPLIANCE_STATES = { + yes: _l('Yes'), + no: _l('No'), + incomplete: _l('Incomplete'), + undefined: _l('Undefined'), +}; +/* eslint-disable quote-props */ + +export const getTranslatableReportCompliance = compliance => + `${COMPLIANCE_STATES[compliance]}`; + +class AuditReport extends Model { + static entityType = 'auditreport'; + + static parseElement(element) { + const copy = super.parseElement(element); + + const { + report, + report_format, + _type: type, + _content_type: content_type, + task, + scan_start, + scan_end, + timestamp, + } = element; + + if (isDefined(report)) { + copy.report = AuditReportReport.fromElement(report); + } + + copy.reportFormat = parseModelFromElement(report_format, 'reportformat'); + copy.task = parseModelFromElement(task, 'task'); + + copy.reportType = type; + copy.contentType = content_type; + + copy.scan_start = parseDate(scan_start); + copy.timestamp = parseDate(timestamp); + + if (isDefined(scan_end)) { + copy.scan_end = parseDate(scan_end); + } + + return copy; + } +} + +export default AuditReport; diff --git a/src/gmp/models/filter.js b/src/gmp/models/filter.js index 5be78cc63f..09a3a6a9a3 100644 --- a/src/gmp/models/filter.js +++ b/src/gmp/models/filter.js @@ -768,6 +768,8 @@ class Filter extends Model { export const ALL_FILTER = new Filter().all(); export const ALERTS_FILTER_FILTER = Filter.fromString('type=alert'); +export const AUDIT_REPORTS_FILTER_FILTER = + Filter.fromString('type=audit_report'); export const CERTBUND_FILTER_FILTER = Filter.fromString('type=info'); export const CPES_FILTER_FILTER = Filter.fromString('type=info'); export const CREDENTIALS_FILTER_FILTER = Filter.fromString('type=credential'); diff --git a/src/gmp/models/filter/keywords.js b/src/gmp/models/filter/keywords.js index 739bc84973..f3468d7870 100644 --- a/src/gmp/models/filter/keywords.js +++ b/src/gmp/models/filter/keywords.js @@ -6,12 +6,14 @@ export const EXTRA_KEYWORDS = [ 'apply_overrides', + 'compliance_levels', 'delta_states', 'first', 'levels', 'min_qod', 'notes', 'overrides', + 'report_compliance_levels', 'result_hosts_only', 'rows', 'solution_type', diff --git a/src/gmp/models/report/auditreport.js b/src/gmp/models/report/auditreport.js new file mode 100644 index 0000000000..18e69b4558 --- /dev/null +++ b/src/gmp/models/report/auditreport.js @@ -0,0 +1,140 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {isDefined} from 'gmp/utils/identity'; +import {isEmpty} from 'gmp/utils/string'; + +import {parseDate} from 'gmp/parser'; + +import {parseFilter} from 'gmp/collection/parser'; + +import Model from 'gmp/model'; + +import ReportTask from './task'; + +import { + parse_errors, + parseHosts, + parseOperatingSystems, + parseResults, + parseTlsCertificates, +} from './parser'; + +class AuditReportReport extends Model { + static entityType = 'auditreport'; + + parseProperties(element) { + return AuditReportReport.parseElement(element); + } + + static parseElement(element) { + const copy = super.parseElement(element); + + const { + delta, + compliance, + compliance_count, + scan_start, + scan_end, + task, + scan, + timestamp, + } = element; + + const filter = parseFilter(element); + + copy.filter = filter; + + copy.reportType = element._type; + + delete copy.filters; + + if (isDefined(compliance)) { + copy.compliance = { + filtered: compliance.filtered, + full: compliance.full, + }; + } + + if (isDefined(compliance_count)) { + copy.complianceCounts = { + filtered: parseInt(compliance_count.filtered), + full: parseInt(compliance_count.full), + incomplete: { + filtered: parseInt(compliance_count.incomplete.filtered), + full: parseInt(compliance_count.incomplete.full), + }, + no: { + filtered: parseInt(compliance_count.no.filtered), + full: parseInt(compliance_count.no.full), + }, + undefined: { + filtered: parseInt(compliance_count.undefined.filtered), + full: parseInt(compliance_count.undefined.full), + }, + yes: { + filtered: parseInt(compliance_count.yes.filtered), + full: parseInt(compliance_count.yes.full), + }, + }; + } + + delete copy.compliance_count; + + copy.task = ReportTask.fromElement(task); + + copy.results = parseResults(element, filter); + + copy.hosts = parseHosts(element, filter); + + copy.tlsCertificates = parseTlsCertificates(element, filter); + + delete copy.host; + + copy.operatingSystems = parseOperatingSystems(element, filter); + + copy.errors = parse_errors(element, filter); + + copy.scan_start = parseDate(scan_start); + + if (isDefined(scan_end)) { + copy.scan_end = parseDate(scan_end); + } + + if (isDefined(timestamp)) { + copy.timestamp = parseDate(timestamp); + } + + if (isDefined(scan) && isDefined(scan.task) && isDefined(scan.task.slave)) { + if (isEmpty(scan.task.slave._id)) { + delete copy.scan.task.slave; + } else { + copy.slave = { + ...scan.task.slave, + }; + } + } + + if (isDefined(delta) && isDefined(delta.report)) { + copy.delta_report = { + id: delta.report._id, + scan_run_status: delta.report.scan_run_status, + scan_end: parseDate(delta.report.scan_end), + scan_start: parseDate(delta.report.scan_start), + timestamp: parseDate(delta.report.timestamp), + }; + + delete copy.delta; + } + + return copy; + } + + isDeltaReport() { + return this.reportType === 'delta'; + } +} + +export default AuditReportReport; diff --git a/src/gmp/models/report/host.js b/src/gmp/models/report/host.js index b22be1fe19..098e851886 100644 --- a/src/gmp/models/report/host.js +++ b/src/gmp/models/report/host.js @@ -38,6 +38,12 @@ class Host { warning: 0, total: 0, }; + this.complianceCounts = { + yes: 0, + no: 0, + incomplete: 0, + total: 0, + }; } static fromElement(element) { @@ -51,7 +57,19 @@ class Host { static parseElement(element = {}) { const copy = {...element}; - const {asset = {}, port_count = {}, result_count} = element; + const { + asset = {}, + port_count = {}, + result_count, + compliance_count, + host_compliance, + } = element; + + copy.hostCompliance = isDefined(host_compliance) + ? host_compliance + : 'undefined'; + + delete copy.host_compliance; if (isEmpty(asset._asset_id)) { delete copy.asset; @@ -82,10 +100,29 @@ class Host { }; } + if (isDefined(compliance_count)) { + copy.complianceCounts = { + yes: parse_page_count(compliance_count.yes), + no: parse_page_count(compliance_count.no), + incomplete: parse_page_count(compliance_count.incomplete), + undefined: parse_page_count(compliance_count.undefined), + total: parse_page_count(compliance_count), + }; + } else { + copy.complianceCounts = { + yes: 0, + no: 0, + incomplete: 0, + undefined: 0, + total: 0, + }; + } + copy.start = parseDate(element.start); copy.end = parseDate(element.end); delete copy.result_count; + delete copy.compliance_count; copy.authSuccess = {}; copy.details = {}; diff --git a/src/gmp/models/report/os.js b/src/gmp/models/report/os.js index e90ed54b94..ebc6188e3d 100644 --- a/src/gmp/models/report/os.js +++ b/src/gmp/models/report/os.js @@ -11,6 +11,7 @@ class OperatingSystem { constructor() { this.hosts = { hostsByIp: {}, + complianceByIp: {}, count: 0, }; } @@ -22,6 +23,31 @@ class OperatingSystem { } } + addHostCompliance(host, compliance) { + if (!(host.ip in this.hosts.complianceByIp)) { + this.hosts.complianceByIp[host.ip] = compliance; + } + const complianceByIpValues = Object.values(this.hosts.complianceByIp); + + const isNoInCompliance = complianceByIpValues.some(value => value === 'no'); + const isIncompleteInCompliance = complianceByIpValues.some( + value => value === 'incomplete', + ); + const isYesInCompliance = complianceByIpValues.some( + value => value === 'yes', + ); + + if (isNoInCompliance) { + this.compliance = 'no'; + } else if (isIncompleteInCompliance) { + this.compliance = 'incomplete'; + } else if (isYesInCompliance) { + this.compliance = 'yes'; + } else { + this.compliance = 'undefined'; + } + } + setSeverity(severity) { if (!isDefined(this.severity) || this.severity < severity) { this.severity = severity; diff --git a/src/gmp/models/report/parser.js b/src/gmp/models/report/parser.js index 65cec48744..da86443613 100644 --- a/src/gmp/models/report/parser.js +++ b/src/gmp/models/report/parser.js @@ -306,7 +306,7 @@ export const parseOperatingSystems = (report, filter) => { const severities = parseHostSeverities(results); forEach(hosts, host => { - const {detail: details, ip} = host; + const {detail: details, ip, host_compliance} = host; let best_os_cpe; let best_os_txt; @@ -335,6 +335,7 @@ export const parseOperatingSystems = (report, filter) => { os.addHost(host); os.setSeverity(severity); + os.addHostCompliance(host, host_compliance); } } }); @@ -398,7 +399,7 @@ export const parseHosts = (report, filter) => { const parse_report_report_counts = elem => { const es = isDefined(elem.results) ? elem.results : {}; - const ec = elem.result_count; + const ec = elem.result_count ? elem.result_count : elem.compliance_count; const length = isDefined(es.result) ? es.result.length : 0; @@ -413,9 +414,13 @@ const parse_report_report_counts = elem => { }; export const parseResults = (report, filter) => { - const {results, result_count} = report; + const {results, result_count, compliance_count} = report; - if (!isDefined(results) && !isDefined(result_count)) { + if ( + !isDefined(results) && + !isDefined(result_count) && + !isDefined(compliance_count) + ) { return undefined; // instead of returning empty_collection_list(filter) we return an undefined // in order to query if results have been loaded and make a difference to diff --git a/src/gmp/models/result.js b/src/gmp/models/result.js index a6291aa5f2..73cdf7901c 100644 --- a/src/gmp/models/result.js +++ b/src/gmp/models/result.js @@ -42,6 +42,7 @@ class Result extends Model { const copy = super.parseElement(element); const { + compliance, description, detection, host = {}, @@ -88,6 +89,10 @@ class Result extends Model { copy.description = description; } + if (isDefined(compliance)) { + copy.compliance = compliance; + } + if (isDefined(severity)) { copy.severity = parseSeverity(severity); } diff --git a/src/gmp/utils/entitytype.js b/src/gmp/utils/entitytype.js index 6938a59eb1..815cd00d42 100644 --- a/src/gmp/utils/entitytype.js +++ b/src/gmp/utils/entitytype.js @@ -34,6 +34,7 @@ export const pluralizeType = type => { return type + 's'; }; const TYPES = { + audit_report: 'auditreport', config: 'scanconfig', cert_bund_adv: 'certbund', dfn_cert_adv: 'dfncert', @@ -61,6 +62,8 @@ export const normalizeType = type => { const ENTITY_TYPES = { alert: _l('Alert'), asset: _l('Asset'), + audit: _l('Audit'), + auditreport: _l('Audit Report'), certbund: _l('CERT-Bund Advisory'), cpe: _l('CPE'), credential: _l('Credential'), @@ -75,6 +78,7 @@ const ENTITY_TYPES = { note: _l('Note'), nvt: _l('NVT'), permission: _l('Permission'), + policy: _l('Policy'), portlist: _l('Port List'), portrange: _l('Port Range'), report: _l('Report'), @@ -108,6 +112,7 @@ export const typeName = type => { }; const CMD_TYPES = { + auditreport: 'audit_report', scanconfig: 'config', certbund: 'cert_bund_adv', dfncert: 'dfn_cert_adv', diff --git a/src/web/components/bar/compliancebar.jsx b/src/web/components/bar/compliancebar.jsx new file mode 100644 index 0000000000..9f17e89fc9 --- /dev/null +++ b/src/web/components/bar/compliancebar.jsx @@ -0,0 +1,44 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import React from 'react'; + +import {isDefined} from 'gmp/utils/identity'; + +import PropTypes from 'web/utils/proptypes'; +import Theme from 'web/utils/theme'; + +import ProgressBar from './progressbar'; +import {getTranslatableReportCompliance} from 'gmp/models/auditreport'; + +const ComplianceBar = ({compliance, toolTip}) => { + const title = getTranslatableReportCompliance(compliance); + + let background; + if (compliance === 'no') { + background = Theme.complianceNo; + } else if (compliance === 'incomplete') { + background = Theme.complianceIncomplete; + } else if (compliance === 'yes') { + background = Theme.complianceYes; + } else { + background = Theme.complianceUndefined; + } + + const toolTipText = isDefined(toolTip) ? toolTip : title; + + return ( + + {title} + + ); +}; + +ComplianceBar.propTypes = { + compliance: PropTypes.string, + toolTip: PropTypes.string, +}; + +export default ComplianceBar; \ No newline at end of file diff --git a/src/web/components/bar/compliancestatusbar.jsx b/src/web/components/bar/compliancestatusbar.jsx index a3fbe98afe..16917b57f2 100644 --- a/src/web/components/bar/compliancestatusbar.jsx +++ b/src/web/components/bar/compliancestatusbar.jsx @@ -5,7 +5,7 @@ import React from 'react'; -import _ from 'gmp/locale'; +import useTranslation from 'web/hooks/useTranslation'; import PropTypes from 'web/utils/proptypes'; import Theme from 'web/utils/theme'; @@ -15,6 +15,7 @@ import ProgressBar from 'web/components/bar/progressbar'; const ComplianceStatusBar = ({complianceStatus}) => { let text; let boxBackground; + const [_] = useTranslation(); if (complianceStatus < 0 || complianceStatus > 100) { text = _('N/A'); boxBackground = Theme.darkGrey; diff --git a/src/web/components/bar/menubar.jsx b/src/web/components/bar/menubar.jsx index 8d5e567a03..9d1315365b 100644 --- a/src/web/components/bar/menubar.jsx +++ b/src/web/components/bar/menubar.jsx @@ -25,8 +25,7 @@ import {isLoggedIn} from 'web/store/usersettings/selectors'; import compose from 'web/utils/compose'; import PropTypes from 'web/utils/proptypes'; import Theme from 'web/utils/theme'; -import withGmp from 'web/utils/withGmp'; -import withCapabilities from 'web/utils/withCapabilities'; +import useCapabilities from 'web/hooks/useCapabilities'; const MENU_BAR_HEIGHT = '35px'; @@ -54,8 +53,11 @@ const MenuBarPlaceholder = styled.div` `; // eslint-disable-next-line no-shadow -const MenuBar = ({isLoggedIn, capabilities}) => { - if (!isLoggedIn || !isDefined(capabilities)) { +const MenuBar = ({isLoggedIn}) => { + + const caps = useCapabilities(); + + if (!isLoggedIn || !isDefined(caps)) { return null; } @@ -66,7 +68,7 @@ const MenuBar = ({isLoggedIn, capabilities}) => { 'vulns', 'overrides', 'notes', - ].reduce((sum, cur) => sum || capabilities.mayAccess(cur), false); + ].reduce((sum, cur) => sum || caps.mayAccess(cur), false); const may_op_configuration = [ 'targets', @@ -80,10 +82,10 @@ const MenuBar = ({isLoggedIn, capabilities}) => { 'scanners', 'filters', 'tags', - ].reduce((sum, cur) => sum || capabilities.mayAccess(cur), false); + ].reduce((sum, cur) => sum || caps.mayAccess(cur), false); const mayOpNotesOverrides = ['notes', 'overrides'].reduce( - (sum, cur) => sum || capabilities.mayAccess(cur), + (sum, cur) => sum || caps.mayAccess(cur), false, ); @@ -91,20 +93,20 @@ const MenuBar = ({isLoggedIn, capabilities}) => { 'alerts', 'schedules', 'report_formats', - ].reduce((sum, cur) => sum || capabilities.mayAccess(cur), false); + ].reduce((sum, cur) => sum || caps.mayAccess(cur), false); const mayOpScannersFiltersTags = ['scanners', 'filters', 'tags'].reduce( - (sum, cur) => sum || capabilities.mayAccess(cur), + (sum, cur) => sum || caps.mayAccess(cur), false, ); const mayOpResilience = ['tickets', 'policies', 'audits'].reduce( - (sum, cur) => sum || capabilities.mayAccess(cur), + (sum, cur) => sum || caps.mayAccess(cur), false, ); const mayOpAssets = ['assets', 'tls_certificates'].reduce( - (sum, cur) => sum || capabilities.mayAccess(cur), + (sum, cur) => sum || caps.mayAccess(cur), false, ); @@ -116,24 +118,24 @@ const MenuBar = ({isLoggedIn, capabilities}) => { {may_op_scans && ( - {capabilities.mayAccess('tasks') && ( + {caps.mayAccess('tasks') && ( )} - {capabilities.mayAccess('reports') && ( + {caps.mayAccess('reports') && ( )} - {capabilities.mayAccess('results') && ( + {caps.mayAccess('results') && ( )} - {capabilities.mayAccess('vulns') && ( + {caps.mayAccess('vulns') && ( )} {mayOpNotesOverrides && ( - {capabilities.mayAccess('notes') && ( + {caps.mayAccess('notes') && ( )} - {capabilities.mayAccess('overrides') && ( + {caps.mayAccess('overrides') && ( )} @@ -142,36 +144,43 @@ const MenuBar = ({isLoggedIn, capabilities}) => { )} {mayOpAssets && ( - {capabilities.mayAccess('assets') && ( + {caps.mayAccess('assets') && ( )} - {capabilities.mayAccess('assets') && ( + {caps.mayAccess('assets') && ( )} - {capabilities.mayAccess('tls_certificates') && ( + {caps.mayAccess('tls_certificates') && ( )} )} {mayOpResilience && ( - {capabilities.mayAccess('tickets') && ( + {caps.mayAccess('tickets') && ( )} - {capabilities.mayAccess('policies') && ( + {caps.mayAccess('policies') && ( )} - {capabilities.mayAccess('audits') && ( + {caps.mayAccess('audits') && ( )} + {caps.featureEnabled('COMPLIANCE_REPORTS') && + caps.mayAccess('audits') && ( + + )} )} - {capabilities.mayAccess('info') && ( + {caps.mayAccess('info') && ( @@ -182,43 +191,43 @@ const MenuBar = ({isLoggedIn, capabilities}) => { )} {may_op_configuration && ( - {capabilities.mayAccess('targets') && ( + {caps.mayAccess('targets') && ( )} - {capabilities.mayAccess('port_lists') && ( + {caps.mayAccess('port_lists') && ( )} - {capabilities.mayAccess('credentials') && ( + {caps.mayAccess('credentials') && ( )} - {capabilities.mayAccess('configs') && ( + {caps.mayAccess('configs') && ( )} {mayOpAlertsSchedulesReportFormats && ( - {capabilities.mayAccess('alerts') && ( + {caps.mayAccess('alerts') && ( )} - {capabilities.mayAccess('schedules') && ( + {caps.mayAccess('schedules') && ( )} - {capabilities.mayAccess('report_configs') && ( + {caps.mayAccess('report_configs') && ( )} - {capabilities.mayAccess('report_formats') && ( + {caps.mayAccess('report_formats') && ( )} )} {mayOpScannersFiltersTags && ( - {capabilities.mayAccess('scanners') && ( + {caps.mayAccess('scanners') && ( )} - {capabilities.mayAccess('filters') && ( + {caps.mayAccess('filters') && ( )} - {capabilities.mayAccess('tags') && ( + {caps.mayAccess('tags') && ( )} @@ -226,20 +235,20 @@ const MenuBar = ({isLoggedIn, capabilities}) => { )} - {capabilities.mayAccess('users') && ( + {caps.mayAccess('users') && ( )} - {capabilities.mayAccess('groups') && ( + {caps.mayAccess('groups') && ( )} - {capabilities.mayAccess('roles') && ( + {caps.mayAccess('roles') && ( )} - {capabilities.mayAccess('permissions') && ( + {caps.mayAccess('permissions') && ( )} - {capabilities.mayAccess('system_reports') && ( + {caps.mayAccess('system_reports') && ( { /> )} - {capabilities.mayAccess('feeds') && ( + {caps.mayAccess('feeds') && ( { /> )} - {capabilities.mayOp('describe_auth') && ( + {caps.mayOp('describe_auth') && ( - {capabilities.mayOp('modify_auth') && ( + {caps.mayOp('modify_auth') && ( )} - {capabilities.mayOp('modify_auth') && ( + {caps.mayOp('modify_auth') && ( )} @@ -278,8 +287,6 @@ const MenuBar = ({isLoggedIn, capabilities}) => { }; MenuBar.propTypes = { - capabilities: PropTypes.capabilities, - gmp: PropTypes.gmp.isRequired, isLoggedIn: PropTypes.bool.isRequired, }; @@ -288,8 +295,6 @@ const mapStateToProps = rootState => ({ }); export default compose( - withCapabilities, - withGmp, connect(mapStateToProps), )(MenuBar); diff --git a/src/web/components/dashboard/display/createDisplay.jsx b/src/web/components/dashboard/display/createDisplay.jsx index b6b43b1b29..520777b8ae 100644 --- a/src/web/components/dashboard/display/createDisplay.jsx +++ b/src/web/components/dashboard/display/createDisplay.jsx @@ -16,13 +16,20 @@ const createDisplay = ({ displayId, displayName, filtersFilter, + filterTerm, loaderComponent: Loader, ...other }) => { const DisplayComponent = ({filter, ...props}) => ( {loaderProps => ( - + {isDefined(Chart) ? displayProps => : undefined} diff --git a/src/web/components/dashboard/display/utils.jsx b/src/web/components/dashboard/display/utils.jsx index 60a8e98838..24a76501bf 100644 --- a/src/web/components/dashboard/display/utils.jsx +++ b/src/web/components/dashboard/display/utils.jsx @@ -10,6 +10,8 @@ import {scaleOrdinal, scaleLinear} from 'd3-scale'; import {parseInt} from 'gmp/parser'; +import {COMPLIANCE_STATES} from 'gmp/models/auditreport'; + import { ERROR, DEBUG, @@ -149,4 +151,8 @@ export const secInfoTypeColorScale = scaleOrdinal() '#80c674', // Nvts ]); +export const complianceColorScale = scaleOrdinal() + .domain(Object.keys(COMPLIANCE_STATES)) + .range(['#4cb045', '#D80000', 'orange', 'silver']); + // vim: set ts=2 sw=2 tw=80: diff --git a/src/web/components/label/compliancestate.jsx b/src/web/components/label/compliancestate.jsx new file mode 100644 index 0000000000..065d371f13 --- /dev/null +++ b/src/web/components/label/compliancestate.jsx @@ -0,0 +1,55 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import styled from 'styled-components'; +import PropTypes from 'web/utils/proptypes'; +import useTranslation from 'web/hooks/useTranslation'; +import Theme from 'web/utils/theme'; + +const Label = styled.div` + text-align: center; + font-weight: normal; + font-style: normal; + color: white; + padding: 1px; + display: inline-block; + width: 70px; + height: 1.5em; + font-size: 0.8em; + background-color: ${props => props.$backgroundColor}; + border-color: ${props => props.$borderColor}; +`; + +const ComplianceLabel = ({text, color, ...props}) => { + const [_] = useTranslation(); + return ( + + ) +}; + +ComplianceLabel.propTypes = { + text: PropTypes.string, + color: PropTypes.string, +}; + +const YesLabel = props => ; +const NoLabel = props => ; +const IncompleteLabel = props => ; +const UndefinedLabel = props => ; + +export const ComplianceStateLabels = { + Yes: YesLabel, + No: NoLabel, + Incomplete: IncompleteLabel, + Undefined: UndefinedLabel, +}; + +export default ComplianceStateLabels; \ No newline at end of file diff --git a/src/web/components/powerfilter/__tests__/compliancelevelsgroup.jsx b/src/web/components/powerfilter/__tests__/compliancelevelsgroup.jsx new file mode 100644 index 0000000000..3454b78e68 --- /dev/null +++ b/src/web/components/powerfilter/__tests__/compliancelevelsgroup.jsx @@ -0,0 +1,239 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect, testing} from '@gsa/testing'; +import {render, fireEvent} from 'web/utils/testing'; + +import ComplianceLevelsFilterGroup from 'web/components/powerfilter/compliancelevelsgroup'; + +import Filter from 'gmp/models/filter'; + +describe('ComplianceLevelsFilterGroup audit reports tests', () => { + test('should call change handler', () => { + const filter = Filter.fromString('report_compliance_levels='); + const handleChange = testing.fn(); + const handleRemove = testing.fn(); + const {element} = render( + , + ); + + const checkbox = element.querySelectorAll('input'); + fireEvent.click(checkbox[0]); + + expect(handleChange).toHaveBeenCalled(); + expect(handleChange).toHaveBeenCalledWith('y', 'report_compliance_levels'); + }); + + test('should check checkbox', () => { + const filter = Filter.fromString('report_compliance_levels=yn'); + const handleChange = testing.fn(); + const handleRemove = testing.fn(); + const {element} = render( + , + ); + + const checkbox = element.querySelectorAll('input'); + + expect(checkbox[0].checked).toEqual(true); + expect(checkbox[1].checked).toEqual(true); + }); + + test('should uncheck checkbox', () => { + const filter1 = Filter.fromString('report_compliance_levels=yni'); + const filter2 = Filter.fromString('report_compliance_levels=yn'); + const handleChange = testing.fn(); + const handleRemove = testing.fn(); + const {element, rerender} = render( + , + ); + + const checkbox = element.querySelectorAll('input'); + + expect(checkbox[0].checked).toEqual(true); + expect(checkbox[1].checked).toEqual(true); + expect(checkbox[1].checked).toEqual(true); + + rerender( + , + ); + + expect(checkbox[0].checked).toEqual(true); + expect(checkbox[1].checked).toEqual(true); + expect(checkbox[2].checked).toEqual(false); + }); + + test('should be unchecked by default', () => { + const filter = Filter.fromString(); + const handleChange = testing.fn(); + const handleRemove = testing.fn(); + const {element} = render( + , + ); + + const checkbox = element.querySelectorAll('input'); + + expect(checkbox[0].checked).toEqual(false); + expect(checkbox[1].checked).toEqual(false); + expect(checkbox[2].checked).toEqual(false); + expect(checkbox[3].checked).toEqual(false); + }); + + test('should call remove handler', () => { + const filter = Filter.fromString('report_compliance_levels=y'); + const handleChange = testing.fn(); + const handleRemove = testing.fn(); + const {element} = render( + , + ); + + const checkbox = element.querySelectorAll('input'); + expect(checkbox[0].checked).toEqual(true); + + fireEvent.click(checkbox[0]); + + expect(handleRemove).toHaveBeenCalled(); + }); +}); + +describe('ComplianceLevelsFilterGroup audit results tests', () => { + test('should call change handler', () => { + const filter = Filter.fromString('compliance_levels='); + const handleChange = testing.fn(); + const handleRemove = testing.fn(); + const {element} = render( + , + ); + + const checkbox = element.querySelectorAll('input'); + fireEvent.click(checkbox[0]); + + expect(handleChange).toHaveBeenCalled(); + expect(handleChange).toHaveBeenCalledWith('y', 'compliance_levels'); + }); + + test('should check checkbox', () => { + const filter = Filter.fromString('compliance_levels=yn'); + const handleChange = testing.fn(); + const handleRemove = testing.fn(); + const {element} = render( + , + ); + + const checkbox = element.querySelectorAll('input'); + + expect(checkbox[0].checked).toEqual(true); + expect(checkbox[1].checked).toEqual(true); + }); + + test('should uncheck checkbox', () => { + const filter1 = Filter.fromString('compliance_levels=yni'); + const filter2 = Filter.fromString('compliance_levels=yn'); + const handleChange = testing.fn(); + const handleRemove = testing.fn(); + const {element, rerender} = render( + , + ); + + const checkbox = element.querySelectorAll('input'); + + expect(checkbox[0].checked).toEqual(true); + expect(checkbox[1].checked).toEqual(true); + expect(checkbox[1].checked).toEqual(true); + + rerender( + , + ); + + expect(checkbox[0].checked).toEqual(true); + expect(checkbox[1].checked).toEqual(true); + expect(checkbox[2].checked).toEqual(false); + }); + + test('should be unchecked by default', () => { + const filter = Filter.fromString(); + const handleChange = testing.fn(); + const handleRemove = testing.fn(); + const {element} = render( + , + ); + + const checkbox = element.querySelectorAll('input'); + + expect(checkbox[0].checked).toEqual(false); + expect(checkbox[1].checked).toEqual(false); + expect(checkbox[2].checked).toEqual(false); + expect(checkbox[3].checked).toEqual(false); + }); + + test('should call remove handler', () => { + const filter = Filter.fromString('compliance_levels=y'); + const handleChange = testing.fn(); + const handleRemove = testing.fn(); + const {element} = render( + , + ); + + const checkbox = element.querySelectorAll('input'); + expect(checkbox[0].checked).toEqual(true); + + fireEvent.click(checkbox[0]); + + expect(handleRemove).toHaveBeenCalled(); + }); +}); diff --git a/src/web/components/powerfilter/compliancelevelsgroup.jsx b/src/web/components/powerfilter/compliancelevelsgroup.jsx new file mode 100644 index 0000000000..272b4882d6 --- /dev/null +++ b/src/web/components/powerfilter/compliancelevelsgroup.jsx @@ -0,0 +1,105 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + + +import React from 'react'; + +import useTranslation from 'web/hooks/useTranslation'; + +import {isDefined} from 'gmp/utils/identity'; + +import PropTypes from 'web/utils/proptypes'; + +import Checkbox from 'web/components/form/checkbox'; +import FormGroup from 'web/components/form/formgroup'; + +import IconDivider from 'web/components/layout/icondivider'; + +import ComplianceStateLabels from 'web/components/label/compliancestate'; + +const ComplianceLevelsFilterGroup = ({ + filter, + onChange, + onRemove, + isResult = false, +}) => { + const [_] = useTranslation(); + + const handleComplianceChange = (value, level) => { + const filterName = isResult + ? 'compliance_levels' + : 'report_compliance_levels'; + + let compliance = filter.get(filterName); + + if (!compliance) { + compliance = ''; + } + + if (value && !compliance.includes(level)) { + compliance += level; + onChange(compliance, filterName); + } else if (!value && compliance.includes(level)) { + compliance = compliance.replace(level, ''); + + if (compliance.trim().length === 0) { + onRemove(); + } else { + onChange(compliance, filterName); + } + } + }; + + let complianceLevels = filter.get( + isResult ? 'compliance_levels' : 'report_compliance_levels', + ); + + if (!isDefined(complianceLevels)) { + complianceLevels = ''; + } + return ( + + + + + + + + + + + + + + + + + ); +}; + +ComplianceLevelsFilterGroup.propTypes = { + filter: PropTypes.filter.isRequired, + isResult: PropTypes.bool, + onChange: PropTypes.func.isRequired, + onRemove: PropTypes.func.isRequired, +}; + +export default ComplianceLevelsFilterGroup; \ No newline at end of file diff --git a/src/web/pages/audits/__tests__/listpage.jsx b/src/web/pages/audits/__tests__/listpage.jsx index 574fcf8053..4b9b7942a1 100644 --- a/src/web/pages/audits/__tests__/listpage.jsx +++ b/src/web/pages/audits/__tests__/listpage.jsx @@ -200,15 +200,15 @@ describe('AuditPage tests', () => { const icons = getAllByTestId('svg-icon'); await act(async () => { - expect(icons[19]).toHaveAttribute( + expect(icons[20]).toHaveAttribute( 'title', 'Move page contents to trashcan', ); - fireEvent.click(icons[19]); + fireEvent.click(icons[20]); expect(deleteByFilter).toHaveBeenCalled(); - expect(icons[20]).toHaveAttribute('title', 'Export page contents'); - fireEvent.click(icons[20]); + expect(icons[21]).toHaveAttribute('title', 'Export page contents'); + fireEvent.click(icons[21]); expect(exportByFilter).toHaveBeenCalled(); }); }); diff --git a/src/web/pages/audits/__tests__/row.jsx b/src/web/pages/audits/__tests__/row.jsx index be31ea28e5..d4599b2ac1 100644 --- a/src/web/pages/audits/__tests__/row.jsx +++ b/src/web/pages/audits/__tests__/row.jsx @@ -21,7 +21,10 @@ import Row from '../row'; setLocale('en'); const gmp = {settings: {}}; -const caps = new Capabilities(['everything']); +const featureList = [ + {name: 'COMPLIANCE_REPORTS', _enabled: 0}, +]; +const caps = new Capabilities(['everything'], featureList); const lastReport = { report: { @@ -760,3 +763,104 @@ describe('Audit Row tests', () => { console.warn = consoleError; }); + +describe('Audit Row tests - compliance reports enabled', () => { + // deactivate console.error for tests + // to make it possible to test a row without a table + console.error = () => {}; + const featureList = [ + {name: 'COMPLIANCE_REPORTS', _enabled: 1}, + ]; + const caps = new Capabilities(['everything'], featureList); + + test('should render', () => { + const audit = Audit.fromElement({ + _id: '314', + owner: {name: 'username'}, + name: 'foo', + comment: 'bar', + status: AUDIT_STATUS.done, + alterable: '0', + last_report: lastReport, + permissions: {permission: [{name: 'everything'}]}, + target: {_id: '5678', name: 'target'}, + usage_type: 'audit', + }); + + const handleAuditClone = testing.fn(); + const handleAuditDelete = testing.fn(); + const handleAuditDownload = testing.fn(); + const handleAuditEdit = testing.fn(); + const handleAuditResume = testing.fn(); + const handleAuditStart = testing.fn(); + const handleAuditStop = testing.fn(); + const handleReportDownload = testing.fn(); + const handleToggleDetailsClick = testing.fn(); + + const {render, store} = rendererWith({ + gmp, + capabilities: caps, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('username')); + + const {baseElement, getAllByTestId} = render( + , + ); + + expect(baseElement).toBeVisible(); + + // Name + expect(baseElement).toHaveTextContent('foo'); + expect(baseElement).toHaveTextContent('(bar)'); + + // Status + const bars = getAllByTestId('progressbar-box'); + + expect(bars[0]).toHaveAttribute('title', AUDIT_STATUS.done); + expect(bars[0]).toHaveTextContent(AUDIT_STATUS.done); + + const detailsLinks = getAllByTestId('details-link'); + + expect(detailsLinks[0]).toHaveTextContent('Done'); + expect(detailsLinks[0]).toHaveAttribute('href', '/auditreport/1234'); + + // Report + expect(detailsLinks[1]).toHaveTextContent('Wed, Jul 10, 2019 2:51 PM CEST'); + expect(detailsLinks[1]).toHaveAttribute('href', '/auditreport/1234'); + + // Compliance Status + expect(bars[1]).toHaveAttribute('title', '50%'); + expect(bars[1]).toHaveTextContent('50%'); + + // Actions + const icons = getAllByTestId('svg-icon'); + + expect(icons[0]).toHaveAttribute('title', 'Start'); + expect(icons[1]).toHaveAttribute('title', 'Audit is not stopped'); + expect(icons[2]).toHaveAttribute('title', 'Move Audit to trashcan'); + expect(icons[3]).toHaveAttribute('title', 'Edit Audit'); + expect(icons[4]).toHaveAttribute('title', 'Clone Audit'); + expect(icons[5]).toHaveAttribute('title', 'Export Audit'); + expect(icons[6]).toHaveAttribute( + 'title', + 'Download Greenbone Compliance Report', + ); + }); +}); \ No newline at end of file diff --git a/src/web/pages/audits/__tests__/table.jsx b/src/web/pages/audits/__tests__/table.jsx index 2fe481bc42..89748c8423 100644 --- a/src/web/pages/audits/__tests__/table.jsx +++ b/src/web/pages/audits/__tests__/table.jsx @@ -137,7 +137,7 @@ describe('Audits table tests', () => { expect(header[0]).toHaveTextContent('Name'); expect(header[1]).toHaveTextContent('Status'); expect(header[2]).toHaveTextContent('Report'); - expect(header[3]).toHaveTextContent('Compliance Status'); + expect(header[3]).toHaveTextContent('Compliance Percent'); expect(header[4]).toHaveTextContent('Actions'); }); diff --git a/src/web/pages/audits/row.jsx b/src/web/pages/audits/row.jsx index 7eca63a339..d8f46e3b77 100644 --- a/src/web/pages/audits/row.jsx +++ b/src/web/pages/audits/row.jsx @@ -37,6 +37,22 @@ import {GREENBONE_SENSOR_SCANNER_TYPE} from 'gmp/models/scanner'; import ComplianceStatusBar from 'web/components/bar/compliancestatusbar'; import {renderReport} from 'web/pages/tasks/row'; +import DateTime from 'web/components/date/datetime'; +import DetailsLink from 'web/components/link/detailslink'; +import useCapabilities from 'web/hooks/useCapabilities'; + +const renderAuditReport = (report, links) => { + if (!isDefined(report)) { + return null; + } + return ( + + + + + + ); +}; const getComplianceStatus = report => { if (!isDefined(report)) { @@ -69,8 +85,8 @@ const Row = ({ ...props }) => { const {scanner, observers} = entity; - const obs = []; + const caps = useCapabilities(); if (isDefined(observers)) { if (isDefined(observers.user)) { @@ -129,9 +145,17 @@ const Row = ({ {entity.comment && ({entity.comment})} - + + + + {caps.featureEnabled('COMPLIANCE_REPORTS') + ? renderAuditReport(entity.last_report, links) + : renderReport(entity.last_report, links) + } - {renderReport(entity.last_report, links)} {isDefined(entity.last_report) && ( - + {actionsColumn} @@ -79,7 +79,7 @@ export default createEntitiesTable({ footer: createEntitiesFooter({ span: 5, trash: true, - tags: false, + tags: true, download: 'audits.xml', }), }); diff --git a/src/web/pages/filters/component.jsx b/src/web/pages/filters/component.jsx index 3b473ab82e..1330c349c4 100644 --- a/src/web/pages/filters/component.jsx +++ b/src/web/pages/filters/component.jsx @@ -75,8 +75,13 @@ class FilterComponent extends React.Component { openFilterDialog(filter) { const {capabilities} = this.props; - - let types = FILTER_OPTIONS.filter(option => + const filterOptions = [ + ...(capabilities.featureEnabled('COMPLIANCE_REPORTS') + ? [['audit_report', _l('Audit Report')]] + : []), + ...FILTER_OPTIONS + ]; + let types = filterOptions.filter(option => filter_types(capabilities, option[0]), ); diff --git a/src/web/pages/policies/__tests__/listpage.jsx b/src/web/pages/policies/__tests__/listpage.jsx index 5ad6df42d8..dd95e816c7 100644 --- a/src/web/pages/policies/__tests__/listpage.jsx +++ b/src/web/pages/policies/__tests__/listpage.jsx @@ -178,15 +178,15 @@ describe('PoliciesPage tests', () => { const icons = getAllByTestId('svg-icon'); await act(async () => { - expect(icons[18]).toHaveAttribute( + expect(icons[19]).toHaveAttribute( 'title', 'Move page contents to trashcan', ); - fireEvent.click(icons[18]); + fireEvent.click(icons[19]); expect(deleteByFilter).toHaveBeenCalled(); - expect(icons[19]).toHaveAttribute('title', 'Export page contents'); - fireEvent.click(icons[19]); + expect(icons[20]).toHaveAttribute('title', 'Export page contents'); + fireEvent.click(icons[20]); expect(exportByFilter).toHaveBeenCalled(); }); }); diff --git a/src/web/pages/policies/table.jsx b/src/web/pages/policies/table.jsx index d1611fafea..92baddf16b 100644 --- a/src/web/pages/policies/table.jsx +++ b/src/web/pages/policies/table.jsx @@ -29,7 +29,7 @@ const PoliciesTable = createEntitiesTable({ download: 'policies.xml', span: 2, trash: true, - tags: false, + tags: true, }), }); diff --git a/src/web/pages/reports/__mocks__/mockauditdeltareport.jsx b/src/web/pages/reports/__mocks__/mockauditdeltareport.jsx new file mode 100644 index 0000000000..2e9507c304 --- /dev/null +++ b/src/web/pages/reports/__mocks__/mockauditdeltareport.jsx @@ -0,0 +1,388 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import AuditReport from 'gmp/models/auditreport'; + +// Task +const task1 = { + _id: '314', + name: 'foo', + comment: 'bar', + target: {_id: '159'}, +}; + +// Results +const result1 = { + _id: '101', + name: 'Result 1', + owner: {name: 'admin'}, + comment: 'Comment 1', + creation_time: '2019-06-03T11:06:31Z', + modification_time: '2019-06-03T11:06:31Z', + host: {__text: '123.456.78.910'}, + port: '80/tcp', + nvt: { + _oid: '201', + type: 'nvt', + name: 'nvt1', + tags: 'solution_type=Mitigation', + refs: {ref: [{_type: 'cve', _id: 'CVE-2019-1234'}]}, + }, + threat: 'High', + severity: 10.0, + qod: {value: 80}, + detection: { + result: { + details: { + detail: [{name: 'product', value: 'cpe:/a: 123'}], + }, + }, + }, + compliance: 'yes', + delta: 'same', +}; + +const result2 = { + _id: '102', + name: 'Result 2', + owner: {name: 'admin'}, + comment: 'Comment 2', + creation_time: '2019-06-03T11:06:31Z', + modification_time: '2019-06-03T11:06:31Z', + host: {__text: '109.876.54.321'}, + port: '80/tcp', + nvt: { + _oid: '202', + type: 'nvt', + name: 'nvt2', + tags: 'solution_type=VendorFix', + refs: {ref: [{_type: 'cve', _id: 'CVE-2019-5678'}]}, + }, + threat: 'Medium', + severity: 5.0, + qod: {value: 70}, + detection: { + result: { + details: { + detail: [{name: 'product', value: 'cpe:/a: 456'}], + }, + }, + }, + compliance: 'incomplete', + delta: 'same', +}; + +const result3 = { + _id: '103', + name: 'Result 3', + owner: {name: 'admin'}, + comment: 'Comment 3', + creation_time: '2019-06-03T11:06:31Z', + modification_time: '2019-06-03T11:06:31Z', + host: {__text: '109.876.54.321'}, + port: '80/tcp', + nvt: { + _oid: '201', + type: 'nvt', + name: 'nvt1', + tags: 'solution_type=Mitigation', + refs: {ref: [{_type: 'cve', _id: 'CVE-2019-1234'}]}, + solution: { + _type: 'Mitigation', + }, + }, + threat: 'Medium', + severity: 5.0, + qod: {value: 80}, + compliance: 'no', + delta: 'same', +}; + +// Hosts +export const host1 = { + ip: '123.456.78.910', + asset: {_asset_id: '123'}, + start: '2019-06-03T11:00:22Z', + end: '2019-06-03T11:15:14Z', + port_count: {page: 10}, + compliance_count: { + page: 50, + incomplete: {page: 14}, + no: {page: 30}, + undefined: {page: 5}, + yes: {page: 7}, + }, + host_compliance: 'no', + detail: [ + {name: 'best_os_cpe', value: 'cpe:/foo/bar'}, + {name: 'best_os_txt', value: 'Foo OS'}, + {name: 'App', value: 'cpe:/a: 123'}, + {name: 'App', value: 'cpe:/a: 789'}, + {name: 'App', value: 'cpe:/a: 101'}, + {name: 'cpe:/a: 123', value: 'ab'}, + {name: 'cpe:/a: 123', value: 'cd'}, + {name: 'traceroute', value: '1.1.1.1,2.2.2.2,3.3.3.3'}, + {name: 'hostname', value: 'foo.bar'}, + {name: 'Auth-SSH-Success'}, + {name: 'SSLInfo', value: '1234::123456'}, + { + name: 'SSLDetails:123456', + value: + 'issuer:CN=foo|serial:abcd|notBefore:20190130T201714|notAfter:20190801T201714', + }, + { + name: 'Closed CVE', + value: 'CVE-2000-1234', + source: { + type: 'openvas', + name: '201', + description: 'This is a description', + }, + extra: '10.0', + }, + ], +}; + +export const host2 = { + ip: '109.876.54.321', + start: '2019-06-03T11:15:14Z', + end: '2019-06-03T11:31:23Z', + port_count: {page: 15}, + compliance_count: { + page: 40, + incomplete: {page: 5}, + no: {page: 0}, + undefined: {page: 6}, + yes: {page: 17}, + }, + host_compliance: 'incomplete', + detail: [ + {name: 'best_os_cpe', value: 'cpe:/lorem/ipsum'}, + {name: 'best_os_txt', value: 'Lorem OS'}, + {name: 'App', value: 'cpe:/a: 123'}, + {name: 'App', value: 'cpe:/a: 456'}, + {name: 'traceroute', value: '1.1.1.1,2.2.2.2'}, + {name: 'hostname', value: 'lorem.ipsum'}, + {name: 'Auth-SSH-Failure'}, + {name: 'SSLInfo', value: '5678::654321'}, + { + name: 'SSLDetails:654321', + value: + 'issuer:CN=bar|serial:dcba|notBefore:20190330T201714|notAfter:20191001T201714', + }, + { + name: 'Closed CVE', + value: 'CVE-2000-5678', + source: { + type: 'openvas', + name: '202', + description: 'This is another description', + }, + extra: '5.0', + }, + ], +}; + +// Hosts +export const host3 = { + ip: '123.456.78.810', + asset: {_asset_id: '123'}, + start: '2019-06-03T11:00:22Z', + end: '2019-06-03T11:15:14Z', + port_count: {page: 10}, + compliance_count: { + page: 20, + incomplete: {page: 0}, + no: {page: 0}, + undefined: {page: 0}, + yes: {page: 20}, + }, + host_compliance: 'yes', + detail: [ + {name: 'best_os_cpe', value: 'cpe:/foo/bar'}, + {name: 'best_os_txt', value: 'Foo OS'}, + {name: 'App', value: 'cpe:/a: 123'}, + {name: 'App', value: 'cpe:/a: 789'}, + {name: 'App', value: 'cpe:/a: 101'}, + {name: 'cpe:/a: 123', value: 'ab'}, + {name: 'cpe:/a: 123', value: 'cd'}, + {name: 'traceroute', value: '1.1.1.1,2.2.2.2,3.3.3.3'}, + {name: 'hostname', value: 'foo.bar'}, + {name: 'Auth-SSH-Success'}, + {name: 'SSLInfo', value: '1234::123456'}, + { + name: 'SSLDetails:123456', + value: + 'issuer:CN=foo|serial:abcd|notBefore:20190130T201714|notAfter:20190801T201714', + }, + { + name: 'Closed CVE', + value: 'CVE-2000-1234', + source: { + type: 'openvas', + name: '201', + description: 'This is a description', + }, + extra: '10.0', + }, + ], +}; + +// Ports +const port1 = { + host: '1.1.1.1', + __text: '123/tcp', + severity: 10.0, + threat: 'High', +}; +const port2 = { + host: '2.2.2.2', + __text: '456/tcp', + severity: 5.0, + threat: 'Medium', +}; + +// Errors +const error1 = { + host: { + __text: '123.456.78.910', + asset: {_asset_id: '123'}, + }, + port: '123/tcp', + description: 'This is an error.', + nvt: { + _oid: '314', + name: 'NVT1', + }, +}; + +const error2 = { + host: { + __text: '109.876.54.321', + asset: {_asset_id: '109'}, + }, + port: '456/tcp', + description: 'This is another error.', + nvt: { + _oid: '159', + name: 'NVT2', + }, +}; + +// TLS certificates +const tlsCertificate1 = { + name: '57610B6A3C73866870678E638C7825743145B24', + certificate: { + __text: '66870678E638C7825743145B247554E0D92C94', + _format: 'DER', + }, + data: 'MIIDSzCCAjOgAwIBAgIJALScVB/zqOLZMA0GCSqGSIb3DQ', + sha256_fingerprint: '57610B6A3C73866870678E638C78', + md5_fingerprint: 'fa:a9:9d:f2:28:cc:2c:c0:80:16', + activation_time: '2019-08-10T12:51:27Z', + expiration_time: '2019-09-10T12:51:27Z', + valid: true, + subject_dn: 'CN=LoremIpsumSubject1 C=Dolor', + issuer_dn: 'CN=LoremIpsumIssuer1 C=Dolor', + serial: '00B49C541FF5A8E1D9', + host: {ip: '192.168.9.90', hostname: 'foo.bar'}, + ports: {port: ['4021', '4023']}, +}; + +const tlsCertificate2 = { + name: 'C137E9D559CC95ED130011FE4012DE56CAE2F8', + certificate: { + __text: 'MIICGTCCAYICCQDDh8Msu4YfXDANBgkqhkiG9w0B', + _format: 'DER', + }, + sha256_fingerprint: 'C137E9D559CC95ED130011FE4012', + md5_fingerprint: '63:70:d6:65:17:32:01:66:9e:7d:c4', + activation_time: 'unlimited', + expiration_time: 'undefined', + valid: false, + subject_dn: 'CN=LoremIpsumSubject2 C=Dolor', + issuer_dn: 'CN=LoremIpsumIssuer2 C=Dolor', + serial: '00C387C32CBB861F5C', + host: {ip: '191.164.9.93', hostname: ''}, + ports: {port: ['8445', '5061']}, +}; + +export const getMockAuditDeltaReport = () => { + const report = { + _type: 'delta', + _id: '1234', + delta: { + report: { + _id: '5678', + scan_run_status: 'Done', + scan_start: '2019-05-20T12:00:15Z', + scan_end: '2019-05-20T12:30:46Z', + }, + }, + scan_run_status: 'Done', + scan_start: '2019-06-03T11:00:22Z', + scan_end: '2019-06-03T11:31:23Z', + timestamp: '2019-06-03T11:00:22Z', + timezone: 'UTC', + timezone_abbrev: 'UTC', + task: task1, + closed_cves: {count: 0}, + vulns: {count: 0}, + apps: {count: 4}, + os: {count: 2}, + ssl_certs: {count: 2}, + compliance: {filtered: 'no', full: 'no'}, + compliance_count: { + full: 3, + filtered: 2, + incomplete: { + full: 5, + filtered: 1, + }, + yes: { + full: 2, + filtered: 3, + }, + no: { + full: 1, + filtered: 2, + }, + undefined: { + full: 2, + filtered: 0, + }, + }, + results: {result: [result1, result2, result3]}, + hosts: {count: 3}, + host: [host1, host2, host3], + tls_certificates: { + tls_certificate: [tlsCertificate1, tlsCertificate2], + }, + ports: { + count: 2, + port: [port1, port2], + }, + errors: { + count: 2, + error: [error1, error2], + }, + }; + + const entity = AuditReport.fromElement({ + report: report, + creation_time: '2019-06-02T12:00:22Z', + modification_time: '2019-06-03T11:00:22Z', + name: '2019-06-03T11:00:22Z', + owner: {name: 'admin'}, + _id: '1234', + }); + + return { + entity, + report: entity.report, + results: entity.report.results, + task: entity.report.task, + }; +}; diff --git a/src/web/pages/reports/__mocks__/mockauditreport.jsx b/src/web/pages/reports/__mocks__/mockauditreport.jsx new file mode 100644 index 0000000000..a723ab74bd --- /dev/null +++ b/src/web/pages/reports/__mocks__/mockauditreport.jsx @@ -0,0 +1,370 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import AuditReport from 'gmp/models/auditreport'; + +// Task +const task1 = { + _id: '314', + name: 'foo', + comment: 'bar', + target: {_id: '159'}, +}; + +// Results +const result1 = { + _id: '101', + name: 'Result 1', + owner: {name: 'admin'}, + comment: 'Comment 1', + creation_time: '2019-06-03T11:06:31Z', + modification_time: '2019-06-03T11:06:31Z', + host: {__text: '123.456.78.910'}, + port: '80/tcp', + nvt: { + _oid: '201', + type: 'nvt', + name: 'nvt1', + tags: 'solution_type=Mitigation', + refs: {ref: [{_type: 'cve', _id: 'CVE-2019-1234'}]}, + }, + threat: 'High', + severity: 10.0, + qod: {value: 80}, + detection: { + result: { + details: { + detail: [{name: 'product', value: 'cpe:/a: 123'}], + }, + }, + }, + compliance: 'yes', +}; + +const result2 = { + _id: '102', + name: 'Result 2', + owner: {name: 'admin'}, + comment: 'Comment 2', + creation_time: '2019-06-03T11:06:31Z', + modification_time: '2019-06-03T11:06:31Z', + host: {__text: '109.876.54.321'}, + port: '80/tcp', + nvt: { + _oid: '202', + type: 'nvt', + name: 'nvt2', + tags: 'solution_type=VendorFix', + refs: {ref: [{_type: 'cve', _id: 'CVE-2019-5678'}]}, + }, + threat: 'Medium', + severity: 5.0, + qod: {value: 70}, + detection: { + result: { + details: { + detail: [{name: 'product', value: 'cpe:/a: 456'}], + }, + }, + }, + compliance: 'incomplete', +}; + +const result3 = { + _id: '103', + name: 'Result 3', + owner: {name: 'admin'}, + comment: 'Comment 3', + creation_time: '2019-06-03T11:06:31Z', + modification_time: '2019-06-03T11:06:31Z', + host: {__text: '109.876.54.321'}, + port: '80/tcp', + nvt: { + _oid: '201', + type: 'nvt', + name: 'nvt1', + tags: 'solution_type=Mitigation', + refs: {ref: [{_type: 'cve', _id: 'CVE-2019-1234'}]}, + solution: { + _type: 'Mitigation', + }, + }, + threat: 'Medium', + severity: 5.0, + qod: {value: 80}, + compliance: 'no', +}; + +// Hosts +export const host1 = { + ip: '123.456.78.910', + asset: {_asset_id: '123'}, + start: '2019-06-03T11:00:22Z', + end: '2019-06-03T11:15:14Z', + port_count: {page: 10}, + compliance_count: { + page: 50, + incomplete: {page: 14}, + no: {page: 30}, + undefined: {page: 5}, + yes: {page: 7}, + }, + host_compliance: 'no', + detail: [ + {name: 'best_os_cpe', value: 'cpe:/foo/bar'}, + {name: 'best_os_txt', value: 'Foo OS'}, + {name: 'App', value: 'cpe:/a: 123'}, + {name: 'App', value: 'cpe:/a: 789'}, + {name: 'App', value: 'cpe:/a: 101'}, + {name: 'cpe:/a: 123', value: 'ab'}, + {name: 'cpe:/a: 123', value: 'cd'}, + {name: 'traceroute', value: '1.1.1.1,2.2.2.2,3.3.3.3'}, + {name: 'hostname', value: 'foo.bar'}, + {name: 'Auth-SSH-Success'}, + {name: 'SSLInfo', value: '1234::123456'}, + { + name: 'SSLDetails:123456', + value: + 'issuer:CN=foo|serial:abcd|notBefore:20190130T201714|notAfter:20190801T201714', + }, + { + name: 'Closed CVE', + value: 'CVE-2000-1234', + source: { + type: 'openvas', + name: '201', + description: 'This is a description', + }, + extra: '10.0', + }, + ], +}; + +export const host2 = { + ip: '109.876.54.321', + start: '2019-06-03T11:15:14Z', + end: '2019-06-03T11:31:23Z', + port_count: {page: 15}, + compliance_count: { + page: 40, + incomplete: {page: 5}, + no: {page: 0}, + undefined: {page: 6}, + yes: {page: 17}, + }, + host_compliance: 'incomplete', + detail: [ + {name: 'best_os_cpe', value: 'cpe:/lorem/ipsum'}, + {name: 'best_os_txt', value: 'Lorem OS'}, + {name: 'App', value: 'cpe:/a: 123'}, + {name: 'App', value: 'cpe:/a: 456'}, + {name: 'traceroute', value: '1.1.1.1,2.2.2.2'}, + {name: 'hostname', value: 'lorem.ipsum'}, + {name: 'Auth-SSH-Failure'}, + {name: 'SSLInfo', value: '5678::654321'}, + { + name: 'SSLDetails:654321', + value: + 'issuer:CN=bar|serial:dcba|notBefore:20190330T201714|notAfter:20191001T201714', + }, + { + name: 'Closed CVE', + value: 'CVE-2000-5678', + source: { + type: 'openvas', + name: '202', + description: 'This is another description', + }, + extra: '5.0', + }, + ], +}; + +// Hosts +export const host3 = { + ip: '123.456.78.810', + asset: {_asset_id: '123'}, + start: '2019-06-03T11:00:22Z', + end: '2019-06-03T11:15:14Z', + port_count: {page: 10}, + compliance_count: { + page: 20, + incomplete: {page: 0}, + no: {page: 0}, + undefined: {page: 0}, + yes: {page: 20}, + }, + host_compliance: 'yes', + detail: [ + {name: 'best_os_cpe', value: 'cpe:/foo/bar'}, + {name: 'best_os_txt', value: 'Foo OS'}, + {name: 'App', value: 'cpe:/a: 123'}, + {name: 'App', value: 'cpe:/a: 789'}, + {name: 'App', value: 'cpe:/a: 101'}, + {name: 'cpe:/a: 123', value: 'ab'}, + {name: 'cpe:/a: 123', value: 'cd'}, + {name: 'traceroute', value: '1.1.1.1,2.2.2.2,3.3.3.3'}, + {name: 'hostname', value: 'foo.bar'}, + {name: 'Auth-SSH-Success'}, + {name: 'SSLInfo', value: '1234::123456'}, + { + name: 'SSLDetails:123456', + value: + 'issuer:CN=foo|serial:abcd|notBefore:20190130T201714|notAfter:20190801T201714', + }, + { + name: 'Closed CVE', + value: 'CVE-2000-1234', + source: { + type: 'openvas', + name: '201', + description: 'This is a description', + }, + extra: '10.0', + }, + ], +}; + +// Ports +const port1 = { + host: '1.1.1.1', + __text: '123/tcp', + severity: 10.0, + threat: 'High', +}; +const port2 = { + host: '2.2.2.2', + __text: '456/tcp', + severity: 5.0, + threat: 'Medium', +}; + +// Errors +const error1 = { + host: { + __text: '123.456.78.910', + asset: {_asset_id: '123'}, + }, + port: '123/tcp', + description: 'This is an error.', + nvt: { + _oid: '314', + name: 'NVT1', + }, +}; + +const error2 = { + host: { + __text: '109.876.54.321', + asset: {_asset_id: '109'}, + }, + port: '456/tcp', + description: 'This is another error.', + nvt: { + _oid: '159', + name: 'NVT2', + }, +}; + +// TLS certificates +const tlsCertificate1 = { + name: '57610B6A3C73866870678E638C7825743145B24', + certificate: { + __text: '66870678E638C7825743145B247554E0D92C94', + _format: 'DER', + }, + data: 'MIIDSzCCAjOgAwIBAgIJALScVB/zqOLZMA0GCSqGSIb3DQ', + sha256_fingerprint: '57610B6A3C73866870678E638C78', + md5_fingerprint: 'fa:a9:9d:f2:28:cc:2c:c0:80:16', + activation_time: '2019-08-10T12:51:27Z', + expiration_time: '2019-09-10T12:51:27Z', + valid: true, + subject_dn: 'CN=LoremIpsumSubject1 C=Dolor', + issuer_dn: 'CN=LoremIpsumIssuer1 C=Dolor', + serial: '00B49C541FF5A8E1D9', + host: {ip: '192.168.9.90', hostname: 'foo.bar'}, + ports: {port: ['4021', '4023']}, +}; + +const tlsCertificate2 = { + name: 'C137E9D559CC95ED130011FE4012DE56CAE2F8', + certificate: { + __text: 'MIICGTCCAYICCQDDh8Msu4YfXDANBgkqhkiG9w0B', + _format: 'DER', + }, + sha256_fingerprint: 'C137E9D559CC95ED130011FE4012', + md5_fingerprint: '63:70:d6:65:17:32:01:66:9e:7d:c4', + activation_time: 'unlimited', + expiration_time: 'undefined', + valid: false, + subject_dn: 'CN=LoremIpsumSubject2 C=Dolor', + issuer_dn: 'CN=LoremIpsumIssuer2 C=Dolor', + serial: '00C387C32CBB861F5C', + host: {ip: '191.164.9.93', hostname: ''}, + ports: {port: ['8445', '5061']}, +}; + +export const getMockAuditReport = () => { + const report = { + _id: '1234', + scan_run_status: 'Done', + scan_start: '2019-06-03T11:00:22Z', + scan_end: '2019-06-03T11:31:23Z', + timestamp: '2019-06-03T11:00:22Z', + timezone: 'UTC', + timezone_abbrev: 'UTC', + task: task1, + closed_cves: {count: 0}, + vulns: {count: 0}, + apps: {count: 4}, + os: {count: 2}, + ssl_certs: {count: 2}, + compliance: {filtered: 'no', full: 'no'}, + compliance_count: { + __text: 3, + full: 3, + filtered: 2, + yes: {full: 5, filtered: 3}, + no: {full: 3, filtered: 2}, + incomplete: {full: 2, filtered: 1}, + undefined: {full: 0, filtered: 0}, + }, + results: {result: [result1, result2, result3]}, + hosts: {count: 3}, + host: [host1, host2, host3], + tls_certificates: { + tls_certificate: [tlsCertificate1, tlsCertificate2], + }, + ports: { + count: 2, + port: [port1, port2], + }, + errors: { + count: 2, + error: [error1, error2], + }, + }; + + const entity = AuditReport.fromElement({ + report: report, + creation_time: '2019-06-02T12:00:22Z', + modification_time: '2019-06-03T11:00:22Z', + name: '2019-06-03T11:00:22Z', + owner: {name: 'admin'}, + _id: '1234', + task: task1, + }); + + return { + entity, + report: entity.report, + results: entity.report.results, + hosts: entity.report.hosts, + operatingsystems: entity.report.operatingSystems, + tlsCertificates: entity.report.tlsCertificates, + errors: entity.report.errors, + task: entity.report.task, + }; +}; diff --git a/src/web/pages/reports/__tests__/auditdeltadetailspage.jsx b/src/web/pages/reports/__tests__/auditdeltadetailspage.jsx new file mode 100644 index 0000000000..fbd2849f14 --- /dev/null +++ b/src/web/pages/reports/__tests__/auditdeltadetailspage.jsx @@ -0,0 +1,377 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect, testing} from '@gsa/testing'; +import React from 'react'; + +import Capabilities from 'gmp/capabilities/capabilities'; + +import Filter from 'gmp/models/filter'; + +import {setTimezone, setUsername} from 'web/store/usersettings/actions'; + +import {rendererWith} from 'web/utils/testing'; + +import {getMockAuditDeltaReport} from 'web/pages/reports/__mocks__/mockauditdeltareport'; + +import DeltaDetailsContent from 'web/pages/reports/deltadetailscontent'; + +const filter = Filter.fromString( + 'apply_overrides=0 compliance_levels=ynui rows=10 min_qod=70 first=1 sort=compliant', +); + +const filterWithName = Filter.fromElement({ + term: 'apply_overrides=0 compliance_levels=ynui rows=10 min_qod=70 first=1 sort=compliant', + name: 'foo', + id: '123', +}); + +const caps = new Capabilities(['everything']); + +const manualUrl = 'test/'; + +const currentSettings = testing.fn().mockResolvedValue({ + foo: 'bar', +}); + +const getReportComposerDefaults = testing.fn().mockResolvedValue({ + foo: 'bar', +}); + +describe('Audit Detla Report Details Content tests', () => { + test('should render Audit Delta Report Details Content', () => { + const onActivateTab = testing.fn(); + const onAddToAssetsClick = testing.fn(); + const onError = testing.fn(); + const onFilterAddLogLevelClick = testing.fn(); + const onFilterDecreaseMinQoDClick = testing.fn(); + const onFilterChanged = testing.fn(); + const onFilterCreated = testing.fn(); + const onFilterEditClick = testing.fn(); + const onFilterRemoveSeverityClick = testing.fn(); + const onFilterResetClick = testing.fn(); + const onFilterRemoveClick = testing.fn(); + const onInteraction = testing.fn(); + const onRemoveFromAssetsClick = testing.fn(); + const onReportDownloadClick = testing.fn(); + const showError = testing.fn(); + const showErrorMessage = testing.fn(); + const showSuccessMessage = testing.fn(); + const onSortChange = testing.fn(); + const onTagSuccess = testing.fn(); + const onTargetEditClick = testing.fn(); + const onTlsCertificateDownloadClick = testing.fn(); + + const sorting = { + errors: {sortField: 'error', sortReverse: true}, + hosts: {sortField: 'compliant', sortReverse: true}, + os: {sortField: 'compliant', sortReverse: true}, + results: {sortField: 'compliant', sortReverse: true}, + tlscerts: {sortField: 'dn', sortReverse: true}, + }; + + const {entity} = getMockAuditDeltaReport(); + + const filters = [filterWithName]; + + const gmp = { + settings: {manualUrl, reportResultsThreshold: 10}, + user: {currentSettings, getReportComposerDefaults}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: caps, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const {baseElement, getAllByTestId} = render( + , + ); + + const icons = baseElement.querySelectorAll('svg'); + const inputs = baseElement.querySelectorAll('input'); + const links = baseElement.querySelectorAll('a'); + const tableData = baseElement.querySelectorAll('td'); + const selects = getAllByTestId('select-selected-value'); + const bars = getAllByTestId('progressbar-box'); + + // Toolbar Icons + expect(icons.length).toEqual(14) + + // Powerfilter + expect(inputs[0]).toHaveAttribute('name', 'userFilterString'); + expect(selects[0]).toHaveAttribute('title', 'Loaded filter'); + expect(selects[0]).toHaveTextContent('foo'); + + // Header + expect(baseElement).toHaveTextContent( + 'Report:Mon, Jun 3, 2019 1:00 PM CEST', + ); + expect(bars[0]).toHaveAttribute('title', 'Done'); + expect(bars[0]).toHaveTextContent('Done'); + expect(baseElement).toHaveTextContent( + 'Created:Sun, Jun 2, 2019 2:00 PM CEST', + ); + expect(baseElement).toHaveTextContent( + 'Modified:Mon, Jun 3, 2019 1:00 PM CEST', + ); + expect(baseElement).toHaveTextContent('Owner:admin'); + + // Tabs + expect(baseElement).toHaveTextContent('Information'); + expect(baseElement).toHaveTextContent('Results(2)'); + expect(baseElement).toHaveTextContent('User Tags(0)'); + + // Summary + expect(tableData[0]).toHaveTextContent('Task Name'); + expect(links[6]).toHaveAttribute('href', '/task/314'); + expect(tableData[1]).toHaveTextContent('foo'); + + expect(tableData[2]).toHaveTextContent('Comment'); + expect(tableData[3]).toHaveTextContent('bar'); + + expect(tableData[4]).toHaveTextContent('Report 1'); + expect(links[7]).toHaveAttribute('href', '/report/1234'); + expect(tableData[5]).toHaveTextContent('1234'); + + expect(tableData[6]).toHaveTextContent('Scan Time Report 1'); + expect(tableData[7]).toHaveTextContent( + 'Mon, Jun 3, 2019 1:00 PM CEST - Mon, Jun 3, 2019 1:31 PM CEST', + ); + + expect(tableData[8]).toHaveTextContent('Scan Duration Report 1'); + expect(tableData[9]).toHaveTextContent('0:31 h'); + + expect(tableData[10]).toHaveTextContent('Scan Status Report 1'); + expect(bars[1]).toHaveTextContent('Done'); + + expect(tableData[12]).toHaveTextContent('Report 2'); + expect(links[8]).toHaveAttribute('href', '/report/5678'); + expect(tableData[13]).toHaveTextContent('5678'); + + expect(tableData[14]).toHaveTextContent('Scan Time Report 2'); + expect(tableData[15]).toHaveTextContent( + 'Mon, May 20, 2019 2:00 PM CEST - Mon, May 20, 2019 2:30 PM CEST', + ); + + expect(tableData[16]).toHaveTextContent('Scan Duration Report 2'); + expect(tableData[17]).toHaveTextContent('0:30 h'); + + expect(tableData[18]).toHaveTextContent('Scan Status Report 2'); + expect(bars[2]).toHaveTextContent('Done'); + + expect(tableData[20]).toHaveTextContent('Hosts scanned'); + expect(tableData[21]).toHaveTextContent('3'); + + expect(tableData[22]).toHaveTextContent('Filter'); + expect(tableData[23]).toHaveTextContent( + 'apply_overrides=0 compliance_levels=ynui min_qod=70', + ); + + expect(tableData[24]).toHaveTextContent('Timezone'); + expect(tableData[25]).toHaveTextContent('UTC (UTC)'); + }); + + test('should render results tab', () => { + const onActivateTab = testing.fn(); + const onAddToAssetsClick = testing.fn(); + const onError = testing.fn(); + const onFilterAddLogLevelClick = testing.fn(); + const onFilterDecreaseMinQoDClick = testing.fn(); + const onFilterChanged = testing.fn(); + const onFilterCreated = testing.fn(); + const onFilterEditClick = testing.fn(); + const onFilterRemoveSeverityClick = testing.fn(); + const onFilterResetClick = testing.fn(); + const onFilterRemoveClick = testing.fn(); + const onInteraction = testing.fn(); + const onRemoveFromAssetsClick = testing.fn(); + const onReportDownloadClick = testing.fn(); + const showError = testing.fn(); + const showErrorMessage = testing.fn(); + const showSuccessMessage = testing.fn(); + const onSortChange = testing.fn(); + const onTagSuccess = testing.fn(); + const onTargetEditClick = testing.fn(); + const onTlsCertificateDownloadClick = testing.fn(); + + const sorting = { + apps: {sortField: 'severity', sortReverse: true}, + closedcves: {sortField: 'severity', sortReverse: true}, + cves: {sortField: 'severity', sortReverse: true}, + errors: {sortField: 'error', sortReverse: true}, + hosts: {sortField: 'severity', sortReverse: true}, + os: {sortField: 'severity', sortReverse: true}, + ports: {sortField: 'severity', sortReverse: true}, + results: {sortField: 'severity', sortReverse: true}, + tlscerts: {sortField: 'dn', sortReverse: true}, + }; + + const {entity} = getMockAuditDeltaReport(); + + const filters = [filterWithName]; + + const gmp = { + settings: {manualUrl, reportResultsThreshold: 10}, + user: {currentSettings, getReportComposerDefaults}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: caps, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const {baseElement, getAllByTestId} = render( + , + ); + + const icons = baseElement.querySelectorAll('svg'); + const inputs = baseElement.querySelectorAll('input'); + const header = baseElement.querySelectorAll('th'); + const rows = baseElement.querySelectorAll('tr'); + const selects = getAllByTestId('select-selected-value'); + const bars = getAllByTestId('progressbar-box'); + + // Toolbar Icons + expect(icons.length).toEqual(24) + + // Powerfilter + expect(inputs[0]).toHaveAttribute('name', 'userFilterString'); + expect(selects[0]).toHaveAttribute('title', 'Loaded filter'); + expect(selects[0]).toHaveTextContent('--'); + + // Header + expect(baseElement).toHaveTextContent( + 'Report:Mon, Jun 3, 2019 1:00 PM CEST', + ); + expect(bars[0]).toHaveAttribute('title', 'Done'); + expect(bars[0]).toHaveTextContent('Done'); + expect(baseElement).toHaveTextContent( + 'Created:Sun, Jun 2, 2019 2:00 PM CEST', + ); + expect(baseElement).toHaveTextContent( + 'Modified:Mon, Jun 3, 2019 1:00 PM CEST', + ); + expect(baseElement).toHaveTextContent('Owner:admin'); + + // Tabs + expect(baseElement).toHaveTextContent('Information'); + expect(baseElement).toHaveTextContent('Results(2)'); + expect(baseElement).toHaveTextContent('User Tags(0)'); + + // Results + + // Headings + expect(header[0]).toHaveTextContent('Delta'); + expect(header[1]).toHaveTextContent('Vulnerability'); + expect(header[3]).toHaveTextContent('Compliant'); + expect(header[4]).toHaveTextContent('QoD'); + expect(header[5]).toHaveTextContent('Host'); + expect(header[6]).toHaveTextContent('Location'); + expect(header[7]).toHaveTextContent('Created'); + expect(header[8]).toHaveTextContent('IP'); + expect(header[9]).toHaveTextContent('Name'); + + // Row 1 + expect(rows[2]).toHaveTextContent('[ = ]'); + expect(rows[2]).toHaveTextContent('Result 1'); + expect(bars[1]).toHaveAttribute('title', 'Yes'); + expect(bars[1]).toHaveTextContent('Yes'); + expect(rows[2]).toHaveTextContent('80 %'); + expect(rows[2]).toHaveTextContent('123.456.78.910'); + expect(rows[2]).toHaveTextContent('80/tcp'); + expect(rows[2]).toHaveTextContent('Mon, Jun 3, 2019 1:06 PM CEST'); + + // Row 2 + expect(rows[3]).toHaveTextContent('[ = ]'); + expect(rows[3]).toHaveTextContent('Result 2'); + expect(bars[2]).toHaveAttribute('title', 'Incomplete'); + expect(bars[2]).toHaveTextContent('Incomplete'); + expect(rows[3]).toHaveTextContent('70 %'); + expect(rows[3]).toHaveTextContent('109.876.54.321'); + expect(rows[3]).toHaveTextContent('80/tcp'); + expect(rows[3]).toHaveTextContent('Mon, Jun 3, 2019 1:06 PM CEST'); + + // Filter + expect(baseElement).toHaveTextContent( + '(Applied filter: apply_overrides=0 compliance_levels=ynui rows=10 min_qod=70 first=1 sort=compliant)', + ); + }); +}); diff --git a/src/web/pages/reports/__tests__/auditdetailscontent.jsx b/src/web/pages/reports/__tests__/auditdetailscontent.jsx new file mode 100644 index 0000000000..fa8ccd693f --- /dev/null +++ b/src/web/pages/reports/__tests__/auditdetailscontent.jsx @@ -0,0 +1,394 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect, testing} from '@gsa/testing'; +import React from 'react'; + +import Capabilities from 'gmp/capabilities/capabilities'; + +import Filter from 'gmp/models/filter'; + +import {setTimezone, setUsername} from 'web/store/usersettings/actions'; + +import {rendererWith} from 'web/utils/testing'; + +import {getMockAuditReport} from 'web/pages/reports/__mocks__/mockauditreport'; + +import DetailsContent from 'web/pages/reports/auditdetailscontent'; + +const filter = Filter.fromString( + 'apply_overrides=0 compliance_levels=ynui rows=10 min_qod=70 first=1 sort=compliant', +); + +const filterWithName = Filter.fromElement({ + term: 'apply_overrides=0 compliance_levels=ynui rows=10 min_qod=70 first=1 sort=compliant', + name: 'foo', + id: '123', +}); + +const resetFilter = Filter.fromString( + 'first=1 compliance_levels=ynui sort=compliant', +); + +const caps = new Capabilities(['everything']); + +const manualUrl = 'test/'; + +const currentSettings = testing.fn().mockResolvedValue({ + foo: 'bar', +}); + +const getReportComposerDefaults = testing.fn().mockResolvedValue({ + foo: 'bar', +}); + +describe('Audit Report Details Content tests', () => { + test('should render Audit Report Details Content', () => { + const onActivateTab = testing.fn(); + const onAddToAssetsClick = testing.fn(); + const onError = testing.fn(); + const onFilterAddLogLevelClick = testing.fn(); + const onFilterDecreaseMinQoDClick = testing.fn(); + const onFilterChanged = testing.fn(); + const onFilterCreated = testing.fn(); + const onFilterEditClick = testing.fn(); + const onFilterRemoveSeverityClick = testing.fn(); + const onFilterResetClick = testing.fn(); + const onFilterRemoveClick = testing.fn(); + const onInteraction = testing.fn(); + const onRemoveFromAssetsClick = testing.fn(); + const onReportDownloadClick = testing.fn(); + const showError = testing.fn(); + const showErrorMessage = testing.fn(); + const showSuccessMessage = testing.fn(); + const onSortChange = testing.fn(); + const onTagSuccess = testing.fn(); + const onTargetEditClick = testing.fn(); + const onTlsCertificateDownloadClick = testing.fn(); + + const sorting = { + errors: {sortField: 'error', sortReverse: true}, + hosts: {sortField: 'compliant', sortReverse: true}, + os: {sortField: 'compliant', sortReverse: true}, + results: {sortField: 'compliant', sortReverse: true}, + tlscerts: {sortField: 'dn', sortReverse: true}, + }; + + const {entity} = getMockAuditReport(); + + const filters = [filterWithName]; + + const gmp = { + settings: {manualUrl, reportResultsThreshold: 10}, + user: {currentSettings, getReportComposerDefaults}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: caps, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const {baseElement, getAllByTestId} = render( + , + ); + + const icons = baseElement.querySelectorAll('svg'); + const inputs = baseElement.querySelectorAll('input'); + const links = baseElement.querySelectorAll('a'); + const tableData = baseElement.querySelectorAll('td'); + const selects = getAllByTestId('select-selected-value'); + const bars = getAllByTestId('progressbar-box'); + + // Toolbar Icons + expect(icons.length).toEqual(16) + + // Powerfilter + expect(inputs[0]).toHaveAttribute('name', 'userFilterString'); + expect(selects[0]).toHaveAttribute('title', 'Loaded filter'); + expect(selects[0]).toHaveTextContent('Loading...'); + + // Header + expect(baseElement).toHaveTextContent( + 'Report:Mon, Jun 3, 2019 1:00 PM CEST', + ); + expect(bars[0]).toHaveAttribute('title', 'Done'); + expect(bars[0]).toHaveTextContent('Done'); + expect(baseElement).toHaveTextContent( + 'Created:Sun, Jun 2, 2019 2:00 PM CEST', + ); + expect(baseElement).toHaveTextContent( + 'Modified:Mon, Jun 3, 2019 1:00 PM CEST', + ); + expect(baseElement).toHaveTextContent('Owner:admin'); + + // Tabs + expect(baseElement).toHaveTextContent('Information'); + expect(baseElement).toHaveTextContent('Results(2 of 3)'); + expect(baseElement).toHaveTextContent('Hosts(2 of 2)'); + expect(baseElement).toHaveTextContent('Operating Systems(2 of 2)'); + expect(baseElement).toHaveTextContent('TLS Certificates(2 of 2)'); + expect(baseElement).toHaveTextContent('Error Messages(2 of 2)'); + expect(baseElement).toHaveTextContent('User Tags(0)'); + + // Summary + expect(tableData[0]).toHaveTextContent('Task Name'); + expect(links[7]).toHaveAttribute('href', '/task/314'); + expect(tableData[1]).toHaveTextContent('foo'); + + expect(tableData[2]).toHaveTextContent('Comment'); + expect(tableData[3]).toHaveTextContent('bar'); + + expect(tableData[4]).toHaveTextContent('Scan Time'); + expect(tableData[5]).toHaveTextContent( + 'Mon, Jun 3, 2019 1:00 PM CEST - Mon, Jun 3, 2019 1:31 PM CEST', + ); + + expect(tableData[6]).toHaveTextContent('Scan Duration'); + expect(tableData[7]).toHaveTextContent('0:31 h'); + + expect(tableData[8]).toHaveTextContent('Scan Status'); + expect(bars[1]).toHaveAttribute('title', 'Done'); + expect(bars[1]).toHaveTextContent('Done'); + + expect(tableData[10]).toHaveTextContent('Hosts scanned'); + expect(tableData[11]).toHaveTextContent('3'); + + expect(tableData[12]).toHaveTextContent('Filter'); + expect(tableData[13]).toHaveTextContent( + 'apply_overrides=0 compliance_levels=ynui min_qod=70', + ); + + expect(tableData[14]).toHaveTextContent('Timezone'); + expect(tableData[15]).toHaveTextContent('UTC (UTC)'); + }); + + test('should render audit threshold panel', () => { + const onActivateTab = testing.fn(); + const onAddToAssetsClick = testing.fn(); + const onError = testing.fn(); + const onFilterAddLogLevelClick = testing.fn(); + const onFilterDecreaseMinQoDClick = testing.fn(); + const onFilterChanged = testing.fn(); + const onFilterCreated = testing.fn(); + const onFilterEditClick = testing.fn(); + const onFilterRemoveSeverityClick = testing.fn(); + const onFilterResetClick = testing.fn(); + const onFilterRemoveClick = testing.fn(); + const onInteraction = testing.fn(); + const onRemoveFromAssetsClick = testing.fn(); + const onReportDownloadClick = testing.fn(); + const showError = testing.fn(); + const showErrorMessage = testing.fn(); + const showSuccessMessage = testing.fn(); + const onSortChange = testing.fn(); + const onTagSuccess = testing.fn(); + const onTargetEditClick = testing.fn(); + const onTlsCertificateDownloadClick = testing.fn(); + + const sorting = { + errors: {sortField: 'error', sortReverse: true}, + hosts: {sortField: 'compliant', sortReverse: true}, + os: {sortField: 'compliant', sortReverse: true}, + results: {sortField: 'compliant', sortReverse: true}, + tlscerts: {sortField: 'dn', sortReverse: true}, + }; + + const {entity} = getMockAuditReport(); + + const filters = [filterWithName]; + + const gmp = { + settings: {manualUrl, reportResultsThreshold: 1}, + user: {currentSettings, getReportComposerDefaults}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: caps, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const {baseElement, getAllByTestId} = render( + , + ); + + const icons = baseElement.querySelectorAll('svg'); + const inputs = baseElement.querySelectorAll('input'); + const selects = getAllByTestId('select-selected-value'); + const bars = getAllByTestId('progressbar-box'); + + // Toolbar Icons + expect(icons.length).toEqual(20) + + // Powerfilter + expect(inputs[0]).toHaveAttribute('name', 'userFilterString'); + expect(selects[0]).toHaveAttribute('title', 'Loaded filter'); + expect(selects[0]).toHaveTextContent('Loading...'); + + // Header + expect(baseElement).toHaveTextContent( + 'Report:Mon, Jun 3, 2019 1:00 PM CEST', + ); + expect(bars[0]).toHaveAttribute('title', 'Done'); + expect(bars[0]).toHaveTextContent('Done'); + expect(baseElement).toHaveTextContent( + 'Created:Sun, Jun 2, 2019 2:00 PM CEST', + ); + expect(baseElement).toHaveTextContent( + 'Modified:Mon, Jun 3, 2019 1:00 PM CEST', + ); + expect(baseElement).toHaveTextContent('Owner:admin'); + + // Tabs + expect(baseElement).toHaveTextContent('Information'); + expect(baseElement).toHaveTextContent('Results(2 of 3)'); + expect(baseElement).toHaveTextContent('Hosts(2 of 2)'); + expect(baseElement).toHaveTextContent('Operating Systems(2 of 2)'); + expect(baseElement).toHaveTextContent('TLS Certificates(2 of 2)'); + expect(baseElement).toHaveTextContent('Error Messages(2 of 2)'); + expect(baseElement).toHaveTextContent('User Tags(0)'); + + // Should include + expect(baseElement).toHaveTextContent( + "The Hosts cannot be displayed in order to maintain the performance within the browser's capabilities.", + ); + expect(baseElement).toHaveTextContent( + 'Please decrease the number of results below the threshold of 1 by applying a more refined filter.', + ); + + expect(baseElement).toHaveTextContent( + 'Results with compliance "Yes" are currently included.', + ); + + expect(baseElement).toHaveTextContent( + 'Filter out results with compliance "Yes".', + ); + + expect(baseElement).toHaveTextContent( + 'Results with compliance "Undefined" are currently included.', + ); + + expect(baseElement).toHaveTextContent( + 'Filter out results with compliance "Undefined".', + ); + + expect(baseElement).toHaveTextContent( + 'Results with compliance "Incomplete" are currently included.', + ); + expect(baseElement).toHaveTextContent( + 'Filter out results with compliance "Incomplete".', + ); + + expect(baseElement).toHaveTextContent( + 'Your filter settings may be too unrefined.', + ); + + expect(baseElement).toHaveTextContent( + 'Adjust and update your filter settings.', + ); + expect(baseElement).toHaveTextContent( + '(Applied filter: apply_overrides=0 compliance_levels=ynui min_qod=70)', + ); + + // // Should not Include + expect(baseElement).not.toHaveTextContent('IP-Adress'); + expect(baseElement).not.toHaveTextContent('Hostname'); + expect(baseElement).not.toHaveTextContent('Apps'); + expect(baseElement).not.toHaveTextContent('Distance'); + }); +}); diff --git a/src/web/pages/reports/__tests__/auditfilterdialog.jsx b/src/web/pages/reports/__tests__/auditfilterdialog.jsx new file mode 100644 index 0000000000..f9d29e5809 --- /dev/null +++ b/src/web/pages/reports/__tests__/auditfilterdialog.jsx @@ -0,0 +1,66 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect, testing} from '@gsa/testing'; +import React from 'react'; + +import Capabilities from 'gmp/capabilities/capabilities'; + +import Filter from 'gmp/models/filter'; + +import {rendererWith} from 'web/utils/testing'; + +import AuditReportFilter from 'web/pages/reports/auditfilterdialog'; + +const caps = new Capabilities(['everything']); + +const manualUrl = 'test/'; + +describe('Filter Dialog for Audit report', () => { + test('should render filter with compliance level group', () => { + const onFilterChanged = testing.fn(); + const onFilterCreated = testing.fn(); + const onCloseClick = testing.fn(); + + const filter = Filter.fromString( + 'apply_overrides=0 levels=hmlg rows=100 min_qod=70 first=1 sort=compliant', + ); + + const gmp = { + settings: {manualUrl, reportResultsThreshold: 10}, + }; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + }); + + const {getAllByTestId} = render( + , + ); + + const formgroups = getAllByTestId('formgroup-title'); + const content = getAllByTestId('formgroup-content'); + const radioTitles = getAllByTestId('radio-title'); + + expect(formgroups[0]).toHaveTextContent('Filter'); + expect(formgroups[1]).toHaveTextContent('Compliance'); + expect(content[1]).toHaveTextContent('YesNoIncompleteUndefined'); + expect(formgroups[2]).toHaveTextContent('QoD'); + expect(formgroups[3]).toHaveTextContent('From Task (name)'); + expect(formgroups[4]).toHaveTextContent('First result'); + expect(formgroups[5]).toHaveTextContent('Results per page'); + expect(formgroups[6]).toHaveTextContent('Sort by'); + expect(radioTitles[0]).toHaveTextContent('Ascending'); + expect(radioTitles[1]).toHaveTextContent('Descending'); + }); +}); diff --git a/src/web/pages/reports/__tests__/auditreportrow.jsx b/src/web/pages/reports/__tests__/auditreportrow.jsx new file mode 100644 index 0000000000..ff49d98ac5 --- /dev/null +++ b/src/web/pages/reports/__tests__/auditreportrow.jsx @@ -0,0 +1,54 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect, testing} from '@gsa/testing'; +import React from 'react'; +import {rendererWith} from 'web/utils/testing'; +import AuditRow from '../auditreportrow'; +import {setTimezone} from 'web/store/usersettings/actions'; + +import {getMockAuditReport} from 'web/pages/reports/__mocks__/mockauditreport'; + +describe('Audit report row', () => { + test('should render row for Audit report', () => { + const {entity} = getMockAuditReport(); + const onReportDeleteClick = testing.fn(); + const onReportDeltaSelect = testing.fn(); + + const {render, store} = rendererWith({ + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + + const {baseElement, getAllByTestId} = render( + + + + +
, + ); + + const bars = getAllByTestId('progressbar-box'); + const links = baseElement.querySelectorAll('a'); + const rows = baseElement.querySelectorAll('tr'); + + expect(links[0]).toHaveAttribute('href', '/auditreport/1234'); + expect(rows[0]).toHaveTextContent('Mon, Jun 3, 2019 1:00 PM'); + expect(bars[0]).toHaveAttribute('title', 'Done'); + expect(bars[0]).toHaveTextContent('Done'); + expect(rows[0]).toHaveTextContent('foo'); + expect(links[1]).toHaveAttribute('href', '/task/314'); + expect(bars[1]).toHaveAttribute('title', 'No'); + expect(bars[1]).toHaveTextContent('No'); + expect(rows[0]).toHaveTextContent('321'); // yes: 3, no: 2, incomplete: 1 + }); +}); diff --git a/src/web/pages/reports/__tests__/auditreportslistpage.jsx b/src/web/pages/reports/__tests__/auditreportslistpage.jsx new file mode 100644 index 0000000000..0539914828 --- /dev/null +++ b/src/web/pages/reports/__tests__/auditreportslistpage.jsx @@ -0,0 +1,267 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect, testing} from '@gsa/testing'; +import React from 'react'; +import {act} from 'react-dom/test-utils'; + +import CollectionCounts from 'gmp/collection/collectioncounts'; + +import Filter from 'gmp/models/filter'; + +import {setTimezone, setUsername} from 'web/store/usersettings/actions'; +import {entitiesActions} from 'web/store/entities/auditreports'; +import {loadingActions} from 'web/store/usersettings/defaults/actions'; +import {defaultFilterLoadingActions} from 'web/store/usersettings/defaultfilters/actions'; + +import {rendererWith, waitFor, fireEvent} from 'web/utils/testing'; +import {getMockAuditReport} from 'web/pages/reports/__mocks__/mockauditreport'; +import AuditReportsPage from '../auditreportslistpage'; + +window.URL.createObjectURL = testing.fn(); + +const {entity} = getMockAuditReport(); + +const reloadInterval = 1; +const manualUrl = 'test/'; + +const currentSettings = testing.fn().mockResolvedValue({ + foo: 'bar', +}); + +const getFilters = testing.fn().mockReturnValue( + Promise.resolve({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }), +); + +const getDashboardSetting = testing.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, +}); + +const getUserSetting = testing.fn().mockResolvedValue({ + filter: null, +}); + +const getComplianceAggregates = testing.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, +}); + +const getReports = testing.fn().mockResolvedValue({ + data: [entity], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, +}); + +const getAll = testing.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, +}); + +const getReportFormats = testing.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, +}); + +describe('AuditReportsPage tests', () => { + test('should render full AuditReports Page', async () => { + const gmp = { + auditreports: { + get: getReports, + getComplianceAggregates: getComplianceAggregates, + }, + filters: { + get: getFilters, + }, + reportformats: { + get: getReportFormats, + }, + dashboard: { + getSetting: getDashboardSetting, + }, + reloadInterval, + settings: {manualUrl}, + user: {currentSettings, getSetting: getUserSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('auditreport', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesActions.success([entity], filter, loadedFilter, counts), + ); + + const {baseElement, getAllByTestId} = render(); + + await waitFor(() => baseElement.querySelectorAll('table')); + + const display = getAllByTestId('grid-item'); + const icons = getAllByTestId('svg-icon'); + const inputs = baseElement.querySelectorAll('input'); + const header = baseElement.querySelectorAll('th'); + const row = baseElement.querySelectorAll('tr'); + const selects = getAllByTestId('select-selected-value'); + + // Toolbar Icons + expect(icons[0]).toHaveAttribute('title', 'Help: Audit Reports'); + + // Powerfilter + expect(inputs[0]).toHaveAttribute('name', 'userFilterString'); + expect(icons[1]).toHaveAttribute('title', 'Update Filter'); + expect(icons[2]).toHaveAttribute('title', 'Remove Filter'); + expect(icons[3]).toHaveAttribute('title', 'Reset to Default Filter'); + expect(icons[4]).toHaveAttribute('title', 'Help: Powerfilter'); + expect(icons[5]).toHaveAttribute('title', 'Edit Filter'); + expect(selects[0]).toHaveAttribute('title', 'Loaded filter'); + expect(selects[0]).toHaveTextContent('--'); + + // // Dashboard + expect(icons[7]).toHaveAttribute('title', 'Add new Dashboard Display'); + expect(icons[8]).toHaveAttribute('title', 'Reset to Defaults'); + expect(display[0]).toHaveTextContent( + 'Audit Reports by Compliance (Total: 0)', + ); + expect(display[1]).toHaveTextContent( + 'Audit Reports by Compliance (Total: 0)', + ); + + // Table + expect(header[0]).toHaveTextContent('Date'); + expect(header[1]).toHaveTextContent('Status'); + expect(header[2]).toHaveTextContent('Task'); + expect(header[3]).toHaveTextContent('Compliant'); + expect(header[4]).toHaveTextContent('Yes'); + expect(header[5]).toHaveTextContent('No'); + expect(header[6]).toHaveTextContent('Incomplete'); + expect(header[7]).toHaveTextContent('Actions'); + + expect(row[1]).toHaveTextContent('Mon, Jun 3, 2019 1:00 PM'); + expect(row[1]).toHaveTextContent('Done'); + expect(row[1]).toHaveTextContent('foo'); + expect(row[1]).toHaveTextContent('No'); + expect(row[1]).toHaveTextContent('321'); // yes: 3, no: 2, incomplete: 1 + }); + + test('should call commands for bulk actions', async () => { + const deleteByFilter = testing.fn().mockResolvedValue({ + foo: 'bar', + }); + + const exportByFilter = testing.fn().mockResolvedValue({ + foo: 'bar', + }); + + const gmp = { + auditreports: { + get: getReports, + getComplianceAggregates: getComplianceAggregates, + deleteByFilter, + exportByFilter, + }, + filters: { + get: getFilters, + }, + reportformats: { + get: getReportFormats, + }, + dashboard: { + getSetting: getDashboardSetting, + }, + tags: { + getAll: getAll, + }, + reloadInterval, + settings: {manualUrl}, + user: {currentSettings, getSetting: getUserSetting}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('auditreport', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesActions.success([entity], filter, loadedFilter, counts), + ); + + const {baseElement, getAllByTestId} = render(); + + await waitFor(() => baseElement.querySelectorAll('table')); + + const icons = getAllByTestId('svg-icon'); + + await act(async () => { + expect(icons[19]).toHaveAttribute('title', 'Add tag to page contents'); + fireEvent.click(icons[19]); + expect(getAll).toHaveBeenCalled(); + + expect(icons[20]).toHaveAttribute('title', 'Delete page contents'); + fireEvent.click(icons[20]); + expect(deleteByFilter).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/web/pages/reports/__tests__/detailsfilterdialog.jsx b/src/web/pages/reports/__tests__/detailsfilterdialog.jsx new file mode 100644 index 0000000000..4c26cc56bb --- /dev/null +++ b/src/web/pages/reports/__tests__/detailsfilterdialog.jsx @@ -0,0 +1,119 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect, testing} from '@gsa/testing'; +import React from 'react'; + +import Capabilities from 'gmp/capabilities/capabilities'; + +import Filter from 'gmp/models/filter'; + +import {rendererWith} from 'web/utils/testing'; + +import FilterDialog from 'web/pages/reports/detailsfilterdialog'; + +const caps = new Capabilities(['everything']); + +const manualUrl = 'test/'; + +describe('Details Filter Dialog for Audit report', () => { + test('should render compliance levels filter group', () => { + const onFilterChanged = testing.fn(); + const onFilterCreated = testing.fn(); + const onCloseClick = testing.fn(); + + const filter = Filter.fromString( + 'apply_overrides=0 levels=hmlg rows=100 min_qod=70 first=1 sort=compliant', + ); + + const gmp = { + settings: {manualUrl, reportResultsThreshold: 10}, + }; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + }); + + const {getAllByTestId} = render( + , + ); + + const formgroups = getAllByTestId('formgroup-title'); + const content = getAllByTestId('formgroup-content'); + + expect(formgroups[0]).toHaveTextContent('Filter'); + expect(formgroups[1]).toHaveTextContent( + 'Only show hosts that have results', + ); + expect(formgroups[2]).toHaveTextContent('QoD'); + expect(formgroups[3]).toHaveTextContent('Compliance'); + expect(content[3]).toHaveTextContent('YesNoIncompleteUndefined'); + expect(formgroups[4]).toHaveTextContent('Solution Type'); + expect(formgroups[5]).toHaveTextContent('Vulnerability'); + expect(formgroups[6]).toHaveTextContent('Host (IP)'); + expect(formgroups[7]).toHaveTextContent('Location (eg. port/protocol)'); + expect(formgroups[8]).toHaveTextContent('First result'); + expect(formgroups[9]).toHaveTextContent('Results per page'); + }); + + test('should render severity levels filter group', () => { + const onFilterChanged = testing.fn(); + const onFilterCreated = testing.fn(); + const onCloseClick = testing.fn(); + + const filter = Filter.fromString( + 'apply_overrides=0 levels=hmlg rows=100 min_qod=70 first=1 sort=severity', + ); + + const gmp = { + settings: {manualUrl, reportResultsThreshold: 10}, + }; + + const {render} = rendererWith({ + gmp, + capabilities: caps, + }); + + const {getAllByTestId} = render( + , + ); + + const formgroups = getAllByTestId('formgroup-title'); + const content = getAllByTestId('formgroup-content'); + + expect(formgroups[0]).toHaveTextContent('Filter'); + expect(formgroups[1]).toHaveTextContent('Apply Overrides'); + expect(formgroups[2]).toHaveTextContent( + 'Only show hosts that have results', + ); + expect(formgroups[3]).toHaveTextContent('QoD'); + expect(formgroups[4]).toHaveTextContent('Severity (Class)'); + expect(content[4]).toHaveTextContent('HighMediumLowLogFalse Pos.'); + expect(formgroups[5]).toHaveTextContent('Severity'); + expect(formgroups[6]).toHaveTextContent('Solution Type'); + expect(formgroups[7]).toHaveTextContent('Vulnerability'); + expect(formgroups[8]).toHaveTextContent('Host (IP)'); + expect(formgroups[9]).toHaveTextContent('Location (eg. port/protocol)'); + expect(formgroups[10]).toHaveTextContent('First result'); + expect(formgroups[11]).toHaveTextContent('Results per page'); + }); +}); diff --git a/src/web/pages/reports/auditdashboard/index.jsx b/src/web/pages/reports/auditdashboard/index.jsx new file mode 100644 index 0000000000..ab590e00b1 --- /dev/null +++ b/src/web/pages/reports/auditdashboard/index.jsx @@ -0,0 +1,36 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + + +import Dashboard from 'web/components/dashboard/dashboard'; + +import { + ReportComplianceDisplay, + ReportComplianceTableDisplay, +} from './statusdisplay'; + +export const AUDIT_REPORTS_DASHBOARD_ID = + '8083d77b-05bb-4b17-ab39-c81175cb512c'; + +export const AUDIT_REPORTS_DISPLAYS = [ + ReportComplianceDisplay.displayId, + ReportComplianceTableDisplay.displayId, +]; + +const AuditReportsDashboard = props => ( + +); + +export default AuditReportsDashboard; \ No newline at end of file diff --git a/src/web/pages/reports/auditdashboard/loaders.jsx b/src/web/pages/reports/auditdashboard/loaders.jsx new file mode 100644 index 0000000000..54ddad147f --- /dev/null +++ b/src/web/pages/reports/auditdashboard/loaders.jsx @@ -0,0 +1,30 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import Loader, { + loadFunc, + loaderPropTypes, +} from 'web/store/dashboard/data/loader'; + +export const REPORTS_COMPLIANCE = 'reports-compliance'; + +export const reportComplianceLoader = loadFunc( + ({gmp, filter}) => + gmp.auditreports.getComplianceAggregates({filter}).then(r => r.data), + REPORTS_COMPLIANCE, +); + +export const ReportCompianceLoader = ({children, filter}) => ( + + {children} + +); + +ReportCompianceLoader.propTypes = loaderPropTypes; \ No newline at end of file diff --git a/src/web/pages/reports/auditdashboard/statusdisplay.jsx b/src/web/pages/reports/auditdashboard/statusdisplay.jsx new file mode 100644 index 0000000000..7305781531 --- /dev/null +++ b/src/web/pages/reports/auditdashboard/statusdisplay.jsx @@ -0,0 +1,89 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + + +import {_, _l} from 'gmp/locale/lang'; + +import {AUDIT_REPORTS_FILTER_FILTER} from 'gmp/models/filter'; +import {getTranslatableReportCompliance} from 'gmp/models/auditreport'; + +import {registerDisplay} from 'web/components/dashboard/registry'; +import { + complianceColorScale, + totalCount, + percent, +} from 'web/components/dashboard/display/utils'; + +import createDisplay from 'web/components/dashboard/display/createDisplay'; +import DataTable from 'web/components/dashboard/display/datatable'; +import DataTableDisplay from 'web/components/dashboard/display/datatabledisplay'; // eslint-disable-line max-len + +import StatusDisplay from 'web/components/dashboard/display/status/statusdisplay'; // eslint-disable-line max-len + +import {ReportCompianceLoader} from './loaders'; + +const transformStatusData = (data = {}) => { + const {groups = []} = data; + + const sum = totalCount(groups); + + const tdata = groups.map(group => { + const {count, value} = group; + const translatableValue = getTranslatableReportCompliance(value); + const perc = percent(count, sum); + return { + value: count, + label: translatableValue, + toolTip: `${translatableValue}: ${perc}% (${count})`, + color: complianceColorScale(value), + filterValue: value, + }; + }); + + tdata.total = sum; + + return tdata; +}; + +export const ReportComplianceDisplay = createDisplay({ + dataTransform: transformStatusData, + displayComponent: StatusDisplay, + filterTerm: 'compliant', + displayId: 'report-by-compliance', + title: ({data: tdata}) => + _('Audit Reports by Compliance (Total: {{count}})', { + count: tdata.total, + }), + filtersFilter: AUDIT_REPORTS_FILTER_FILTER, + loaderComponent: ReportCompianceLoader, +}); + +export const ReportComplianceTableDisplay = createDisplay({ + chartComponent: DataTable, + displayComponent: DataTableDisplay, + loaderComponent: ReportCompianceLoader, + dataTransform: transformStatusData, + dataTitles: [_l('Status'), _l('# of Reports')], + dataRow: row => [row.label, row.value], + title: ({data: tdata}) => + _('Audit Reports by Compliance (Total: {{count}})', { + count: tdata.total, + }), + displayId: 'report-by-compliance-table', + displayName: 'ReportComplianceTableDisplay', + filtersFilter: AUDIT_REPORTS_FILTER_FILTER, +}); + +registerDisplay(ReportComplianceDisplay.displayId, ReportComplianceDisplay, { + title: _l('Chart: Audit Reports by Compliance'), +}); + +registerDisplay( + ReportComplianceTableDisplay.displayId, + ReportComplianceTableDisplay, + { + title: _l('Table: Audit Reports by Compliance'), + }, +); \ No newline at end of file diff --git a/src/web/pages/reports/auditdeltadetailspage.jsx b/src/web/pages/reports/auditdeltadetailspage.jsx new file mode 100644 index 0000000000..2a65928c1f --- /dev/null +++ b/src/web/pages/reports/auditdeltadetailspage.jsx @@ -0,0 +1,565 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + + +import React, {useEffect, useState} from 'react'; + +import {useDispatch, useSelector, shallowEqual} from 'react-redux'; + +import {useRouteMatch} from 'react-router-dom'; + +import useTranslation from 'web/hooks/useTranslation'; + +import logger from 'gmp/log'; + +import Filter, { + ALL_FILTER, + RESET_FILTER, + RESULTS_FILTER_FILTER +} from 'gmp/models/filter'; +import {isActive} from 'gmp/models/task'; + +import {first} from 'gmp/utils/array'; +import {isDefined, hasValue} from 'gmp/utils/identity'; + +import withDownload from 'web/components/form/withDownload'; + +import Reload, { + NO_RELOAD, + USE_DEFAULT_RELOAD_INTERVAL_ACTIVE, +} from 'web/components/loading/reload'; + +import withDialogNotification from 'web/components/notification/withDialogNotifiaction'; // eslint-disable-line max-len + +import DownloadReportDialog from 'web/pages/reports/downloadreportdialog'; + +import { + loadAllEntities as loadFilters, + selector as filterSelector, +} from 'web/store/entities/filters'; + +import { + loadAllEntities as loadReportFormats, + selector as reportFormatsSelector, +} from 'web/store/entities/reportformats'; + +import { + loadAllEntities as loadReportConfigs, + selector as reportConfigsSelector, +} from 'web/store/entities/reportconfigs'; + +import {loadDeltaAuditReport} from 'web/store/entities/report/actions'; + +import {deltaAuditReportSelector} from 'web/store/entities/report/selectors'; + +import { + loadReportComposerDefaults, + renewSessionTimeout, + saveReportComposerDefaults, +} from 'web/store/usersettings/actions'; + +import {loadUserSettingDefaults} from 'web/store/usersettings/defaults/actions'; +import {getUserSettingsDefaults} from 'web/store/usersettings/defaults/selectors'; +import {getUserSettingsDefaultFilter} from 'web/store/usersettings/defaultfilters/selectors'; + +import { + getReportComposerDefaults, + getUsername, +} from 'web/store/usersettings/selectors'; + +import compose from 'web/utils/compose'; +import {generateFilename} from 'web/utils/render'; +import PropTypes from 'web/utils/proptypes'; +import useGmp from 'web/hooks/useGmp'; + +import TargetComponent from '../targets/component'; + +import Page from './deltadetailscontent'; +import FilterDialog from './detailsfilterdialog'; + +const log = logger.getLogger('web.pages.report.deltadetailspage'); + +const DEFAULT_FILTER = Filter.fromString( + 'levels=hmlg rows=100 min_qod=70 first=1 sort-reverse=compliant', +); + +const REPORT_FORMATS_FILTER = Filter.fromString('active=1 and trust=1 rows=-1'); + +const getTarget = (entity = {}) => { + const {report = {}} = entity; + const {task = {}} = report; + return task.target; +}; + +const DeltaAuditReportDetails = props => { + const [activeTab, setActiveTab] = useState(0); + const [showFilterDialog, setShowFilterDialog] = useState(false); + const [showDownloadReportDialog, setShowDownloadReportDialog] = + useState(false); + const [reportFormatId, setReportFormatId] = useState(); + const [isUpdating, setIsUpdating] = useState(false); + // storeAsDefault is set in SaveDialogContent + // eslint-disable-next-line no-unused-vars + const [storeAsDefault, setStoreAsDefault] = useState(); + + const [sorting, setSorting] = useState({ + results: { + sortField: 'compliant', + sortReverse: true, + }, + errors: { + sortField: 'error', + sortReverse: false, + }, + }); + + const [_] = useTranslation(); + const gmp = useGmp(); + const dispatch = useDispatch(); + const match = useRouteMatch(); + const {id: reportId, deltaid: deltaReportId} = match.params; + + const reportFormatsSel = useSelector(reportFormatsSelector); + const reportConfigsSel = useSelector(reportConfigsSelector); + const reportFormats = reportFormatsSel?.getAllEntities(REPORT_FORMATS_FILTER); + const reportConfigs = reportConfigsSel?.getAllEntities(ALL_FILTER); + const userDefaultFilterSel = useSelector( + rootState => getUserSettingsDefaultFilter(rootState, 'result'), + shallowEqual, + ); + const resultDefaultFilter = userDefaultFilterSel?.getFilter('result'); + const reportComposerDefaults = useSelector(getReportComposerDefaults); + const userDefaultsSelector = useSelector( + getUserSettingsDefaults, + shallowEqual, + ); + const reportExportFileName = userDefaultsSelector?.getValueByName( + 'reportexportfilename', + ); + const username = useSelector(getUsername); + const filterSel = useSelector(filterSelector); + const filters = filterSel?.getAllEntities(RESULTS_FILTER_FILTER); + const [entity, entityError] = useSelector(state => { + const deltaSel = deltaAuditReportSelector(state); + return [ + deltaSel?.getEntity(reportId, deltaReportId), + deltaSel?.getError(reportId, deltaReportId), + ]; + }); + const isLoading = !isDefined(entity); + + useEffect(() => { + dispatch(loadUserSettingDefaults(gmp)()); + dispatch(loadFilters(gmp)(RESULTS_FILTER_FILTER)); + dispatch(loadReportFormats(gmp)(REPORT_FORMATS_FILTER)); + dispatch(loadReportConfigs(gmp)(ALL_FILTER)); + dispatch(loadReportComposerDefaults(gmp)()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if ( + !isDefined(reportFormatId) && + isDefined(reportFormats) && + reportFormats.length > 0 + ) { + // set initial report format id if available + const initialReportFormatId = first(reportFormats).id; + if (isDefined(initialReportFormatId)) { + // ensure the report format id is only set if we really have one + // if no report format id is available we would create an infinite + // render loop here + setReportFormatId({initialReportFormatId}); + } else { + // if there is no report format at all, throw a proper error message + // instead of just showing x is undefined JS stacktrace + const noReportFormatError = _( + 'The report cannot be displayed because' + + ' no report format is available.' + + ' This could be due to a missing gvmd data feed. Please update' + + ' the gvmd data feed, check the "feed import owner" setting, the' + + ' feed status page, or contact your system administrator.', + ); + throw new Error(noReportFormatError); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reportFormats, reportFormatId]); + + useEffect(() => { + load(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reportId, deltaReportId]); + + const load = filter => { + log.debug('Loading report', { + filter, + }); + const {reportFilter: prevFilter} = props; + + setIsUpdating(!isDefined(prevFilter) || !prevFilter.equals(filter)); + + props + .reload(filter) + .then(() => { + setIsUpdating(false); + }) + .catch(() => { + setIsUpdating(false); + }); + }; + + const reload = () => { + // reload data from backend + load(props.reportFilter); + }; + + const handleChanged = () => { + reload(); + }; + + const handleError = error => { + const {showError} = props; + log.error(error); + showError(error); + }; + + const handleFilterChange = filter => { + handleInteraction(); + load(filter); + }; + + const handleFilterRemoveClick = () => { + handleFilterChange(RESET_FILTER); + }; + + const handleFilterResetClick = () => { + if (hasValue(resultDefaultFilter)) { + handleFilterChange(resultDefaultFilter); + } else { + handleFilterChange(DEFAULT_FILTER); + } + }; + + const handleActivateTab = index => { + handleInteraction(); + setActiveTab(index); + }; + + const handleAddToAssets = () => { + const {showSuccessMessage, reportFilter: filter} = props; + + handleInteraction(); + + gmp.auditreport.addAssets(entity, {filter}).then(() => { + showSuccessMessage( + _( + 'Report content added to Assets with QoD>=70% and Overrides enabled.', + ), + ); + reload(); + }, handleError); + }; + + const handleRemoveFromAssets = () => { + const {showSuccessMessage, reportFilter: filter} = props; + + handleInteraction(); + + gmp.auditreport.removeAssets(entity, {filter}).then(() => { + showSuccessMessage(_('Report content removed from Assets.')); + reload(); + }, handleError); + }; + + const handleFilterEditClick = () => { + handleInteraction(); + + setShowFilterDialog(true); + }; + + const handleFilterDialogClose = () => { + handleInteraction(); + setShowFilterDialog(false); + }; + + const handleOpenDownloadReportDialog = () => { + setShowDownloadReportDialog(true); + }; + + const handleCloseDownloadReportDialog = () => { + setShowDownloadReportDialog(false); + }; + + const handleReportDownload = state => { + const {reportFilter, onDownload} = props; + // eslint-disable-next-line no-shadow + const {includeNotes, includeOverrides, reportFormatId, storeAsDefault} = + state; + + const newFilter = reportFilter.copy(); + newFilter.set('notes', includeNotes); + newFilter.set('overrides', includeOverrides); + + if (storeAsDefault) { + const defaults = { + ...reportComposerDefaults, + defaultReportFormatId: reportFormatId, + includeNotes, + includeOverrides, + }; + dispatch(saveReportComposerDefaults(gmp)(defaults)); + } + + const report_format = reportFormats + ? reportFormats.find(format => reportFormatId === format.id) + : undefined; + + const extension = isDefined(report_format) + ? report_format.extension + : 'unknown'; // unknown should never happen but we should be save here + + handleInteraction(); + + return gmp.auditreport + .download(entity, { + reportFormatId, + deltaReportId, + filter: newFilter, + }) + .then(response => { + setShowDownloadReportDialog(false); + const {data} = response; + const filename = generateFilename({ + creationTime: entity.creationTime, + extension, + fileNameFormat: reportExportFileName, + id: entity.id, + modificationTime: entity.modificationTime, + reportFormat: report_format?.name, + resourceName: entity.task.name, + resourceType: 'report', + username, + }); + + onDownload({filename, data}); + }, handleError); + }; + + const handleFilterCreated = filter => { + handleInteraction(); + load(filter); + dispatch(loadFilters(gmp)(RESULTS_FILTER_FILTER)); + }; + + const handleFilterDecreaseMinQoD = () => { + const {reportFilter} = props; + + handleInteraction(); + + if (reportFilter.has('min_qod')) { + const lfilter = reportFilter.copy(); + lfilter.set('min_qod', 30); + load(lfilter); + } + }; + + const handleSortChange = (name, sortField) => { + handleInteraction(); + + const prev = sorting[name]; + + const sortReverse = + sortField === prev.sortField ? !prev.sortReverse : false; + + const newSorting = { + ...sorting, + [name]: { + sortField, + sortReverse, + }, + }; + setSorting(newSorting); + }; + + const handleInteraction = () => dispatch(renewSessionTimeout(gmp)()); + + const loadTarget = () => { + const target = getTarget(entity); + return gmp.target.get({id: target.id}); + }; + + const {reportFilter, showError, showErrorMessage, showSuccessMessage} = props; + + const {report} = entity || {}; + + return ( + + + {({edit}) => ( + + loadTarget().then(response => edit(response.data)) + } + showError={showError} + showErrorMessage={showErrorMessage} + showSuccessMessage={showSuccessMessage} + /> + )} + + {showFilterDialog && ( + + )} + {showDownloadReportDialog && ( + + )} + + ); +}; + +DeltaAuditReportDetails.propTypes = { + defaultFilter: PropTypes.filter, + location: PropTypes.object.isRequired, + reload: PropTypes.func.isRequired, + reportFilter: PropTypes.filter, + showError: PropTypes.func.isRequired, + showErrorMessage: PropTypes.func.isRequired, + showSuccessMessage: PropTypes.func.isRequired, + target: PropTypes.model, + username: PropTypes.string, + onDownload: PropTypes.func.isRequired, +}; + +const reloadInterval = report => + isDefined(report) && isActive(report.report.scan_run_status) + ? USE_DEFAULT_RELOAD_INTERVAL_ACTIVE + : NO_RELOAD; // report doesn't change anymore. no need to reload + +const load = + ({defaultFilter, reportId, deltaReportId, dispatch, gmp, reportFilter}) => + filter => { + if (!hasValue(filter)) { + // use loaded filter after initial loading + filter = reportFilter; + } + + if (!hasValue(filter)) { + // use filter from user setting + filter = defaultFilter; + } + + if (!hasValue(filter)) { + // use fallback filter + filter = DEFAULT_FILTER; + } + + // to avoid confusion of loaded results with different sort terms and + // directions, always load the report with sort=name from gvmd (the user's + // sort term will be handled by GSA in the browser) + filter.delete('sort-reverse'); + filter.set('sort', 'name'); + return dispatch( + loadDeltaAuditReport(gmp)(reportId, deltaReportId, filter), + ).then(() => + dispatch(loadDeltaAuditReport(gmp)(reportId, deltaReportId, filter)), + ); + }; + +const DeltaAuditReportDetailsWrapper = ({defaultFilter, ...props}) => { + const gmp = useGmp(); + const dispatch = useDispatch(); + const match = useRouteMatch(); + + const {id: reportId, deltaid: deltaReportId} = match.params; + const deltaSel = useSelector(deltaAuditReportSelector, shallowEqual); + const entity = deltaSel.getEntity(reportId, deltaReportId); + const reportFilter = entity?.report?.filter; + + return ( + reloadInterval(entity)} + > + {({reload}) => ( + + )} + + ); +}; + +DeltaAuditReportDetailsWrapper.propTypes = { + defaultFilter: PropTypes.filter, +}; + +export default compose( + withDialogNotification, + withDownload, +)(DeltaAuditReportDetailsWrapper); \ No newline at end of file diff --git a/src/web/pages/reports/auditdetailscontent.jsx b/src/web/pages/reports/auditdetailscontent.jsx new file mode 100644 index 0000000000..9006567e62 --- /dev/null +++ b/src/web/pages/reports/auditdetailscontent.jsx @@ -0,0 +1,448 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + + +import React from 'react'; + +import styled from 'styled-components'; + +import useTranslation from 'web/hooks/useTranslation'; + +import {TASK_STATUS} from 'gmp/models/task'; + +import {isDefined} from 'gmp/utils/identity'; + +import StatusBar from 'web/components/bar/statusbar'; +import ToolBar from 'web/components/bar/toolbar'; + +import DateTime from 'web/components/date/datetime'; + +import ErrorPanel from 'web/components/error/errorpanel'; + +import ReportIcon from 'web/components/icon/reporticon'; + +import Divider from 'web/components/layout/divider'; +import Layout from 'web/components/layout/layout'; + +import Loading from 'web/components/loading/loading'; + +import Powerfilter from 'web/components/powerfilter/powerfilter'; + +import Tab from 'web/components/tab/tab'; +import TabLayout from 'web/components/tab/tablayout'; +import TabList from 'web/components/tab/tablist'; +import TabPanel from 'web/components/tab/tabpanel'; +import TabPanels from 'web/components/tab/tabpanels'; +import Tabs from 'web/components/tab/tabs'; + +import Section from 'web/components/section/section'; +import SectionHeader from 'web/components/section/header'; + +import EntityInfo from 'web/entity/info'; +import EntityTags from 'web/entity/tags'; + +import PropTypes from 'web/utils/proptypes'; +import useGmp from 'web/hooks/useGmp'; + +import ErrorsTab from './details/errorstab'; +import HostsTab from './details/hoststab'; +import OperatingSystemsTab from './details/operatingsystemstab'; +import ResultsTab from './details/resultstab'; +import Summary from './details/summary'; +import TabTitle from './details/tabtitle'; +import AuditThresholdPanel from './details/auditthresholdpanel'; +import TLSCertificatesTab from './details/tlscertificatestab'; +import ToolBarIcons from './details/toolbaricons'; + +const Span = styled.span` + margin-top: 2px; +`; + +const PageContent = ({ + activeTab, + entity, + errorsCounts, + filters, + hostsCounts, + isLoading = true, + isLoadingFilters = true, + isUpdating = false, + operatingSystemsCounts, + pageFilter, + reportError, + reportFilter, + reportId, + resetFilter, + resultsCounts, + sorting, + showError, + showErrorMessage, + showSuccessMessage, + task, + tlsCertificatesCounts, + onActivateTab, + onAddToAssetsClick, + onTlsCertificateDownloadClick, + onError, + onFilterChanged, + onFilterCreated, + onFilterDecreaseMinQoDClick, + onFilterEditClick, + onFilterRemoveClick, + onFilterResetClick, + onInteraction, + onRemoveFromAssetsClick, + onReportDownloadClick, + onSortChange, + onTagSuccess, + onTargetEditClick, +}) => { + const hasReport = isDefined(entity); + + const report = hasReport ? entity.report : undefined; + + const userTags = hasReport ? report.userTags : undefined; + const userTagsCount = isDefined(userTags) ? userTags.length : 0; + const gmp = useGmp(); + const [_] = useTranslation(); + + const { + errors = {}, + hosts = {}, + operatingSystems = {}, + results = {}, + tlsCertificates = {}, + timestamp, + scan_run_status, + } = report || {}; + + if (!hasReport && isDefined(reportError)) { + return ( + + ); + } + + const threshold = gmp.settings.reportResultsThreshold; + + const showThresholdMessage = + !isLoading && hasReport && results.counts.filtered > threshold; + + const isContainer = isDefined(task) && task.isContainer(); + const status = isContainer ? TASK_STATUS.container : scan_run_status; + const progress = isDefined(task) ? task.progress : 0; + + const showIsLoading = isLoading && !hasReport; + + const showInitialLoading = + isLoading && + !isDefined(reportError) && + !showThresholdMessage && + (!isDefined(results.entities) || results.entities.length === 0); + + const header_title = ( + + {_('Audit Report:')} + {showIsLoading ? ( + {_('Loading')} + ) : ( + + + + + + + )} + + ); + + const header = ( + } title={header_title}> + {hasReport && } + + ); + + return ( + + + + + + + + +
+ {showIsLoading ? ( + + ) : ( + + + + {_('Information')} + + + + + + + + + + + + + + + + + + + + + + {hasReport ? ( + + + + + + + + onSortChange('results', sortField) + } + onTargetEditClick={onTargetEditClick} + /> + + + {showInitialLoading ? ( + + ) : showThresholdMessage ? ( + + ) : ( + + onSortChange('hosts', sortField) + } + /> + )} + + + {showInitialLoading ? ( + + ) : showThresholdMessage ? ( + + ) : ( + + onSortChange('os', sortField) + } + /> + )} + + + {showInitialLoading ? ( + + ) : showThresholdMessage ? ( + + ) : ( + + onSortChange('tlscerts', sortField) + } + onTlsCertificateDownloadClick={ + onTlsCertificateDownloadClick + } + /> + )} + + + + onSortChange('errors', sortField) + } + /> + + + + + + + ) : ( + + )} + + )} +
+
+ ); +}; + +PageContent.propTypes = { + activeTab: PropTypes.number, + applicationsCounts: PropTypes.counts, + closedCvesCounts: PropTypes.counts, + cvesCounts: PropTypes.counts, + entity: PropTypes.model, + errorsCounts: PropTypes.counts, + filters: PropTypes.array, + hostsCounts: PropTypes.counts, + isLoading: PropTypes.bool, + isLoadingFilters: PropTypes.bool, + isUpdating: PropTypes.bool, + operatingSystemsCounts: PropTypes.counts, + pageFilter: PropTypes.filter, + portsCounts: PropTypes.counts, + reportError: PropTypes.error, + reportFilter: PropTypes.filter, + reportId: PropTypes.id.isRequired, + resetFilter: PropTypes.filter, + resultsCounts: PropTypes.counts, + showError: PropTypes.func.isRequired, + showErrorMessage: PropTypes.func.isRequired, + showSuccessMessage: PropTypes.func.isRequired, + sorting: PropTypes.object, + task: PropTypes.model, + tlsCertificatesCounts: PropTypes.counts, + onActivateTab: PropTypes.func.isRequired, + onAddToAssetsClick: PropTypes.func.isRequired, + onError: PropTypes.func.isRequired, + onFilterChanged: PropTypes.func.isRequired, + onFilterCreated: PropTypes.func.isRequired, + onFilterDecreaseMinQoDClick: PropTypes.func.isRequired, + onFilterEditClick: PropTypes.func.isRequired, + onFilterRemoveClick: PropTypes.func.isRequired, + onFilterResetClick: PropTypes.func.isRequired, + onInteraction: PropTypes.func.isRequired, + onRemoveFromAssetsClick: PropTypes.func.isRequired, + onReportDownloadClick: PropTypes.func.isRequired, + onSortChange: PropTypes.func.isRequired, + onTagSuccess: PropTypes.func.isRequired, + onTargetEditClick: PropTypes.func.isRequired, + onTlsCertificateDownloadClick: PropTypes.func.isRequired, +}; + +export default PageContent; \ No newline at end of file diff --git a/src/web/pages/reports/auditdetailspage.jsx b/src/web/pages/reports/auditdetailspage.jsx new file mode 100644 index 0000000000..b0585d6ffa --- /dev/null +++ b/src/web/pages/reports/auditdetailspage.jsx @@ -0,0 +1,705 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + + +import React, {useEffect, useState} from 'react'; + +import {useDispatch, useSelector, shallowEqual} from 'react-redux'; + +import {useRouteMatch} from 'react-router-dom'; + +import useTranslation from 'web/hooks/useTranslation'; + +import logger from 'gmp/log'; + +import Filter, { + ALL_FILTER, + RESET_FILTER, + RESULTS_FILTER_FILTER +} from 'gmp/models/filter'; +import {isActive} from 'gmp/models/task'; + +import {first} from 'gmp/utils/array'; +import {isDefined, hasValue} from 'gmp/utils/identity'; + +import withDownload from 'web/components/form/withDownload'; + +import Reload, { + NO_RELOAD, + USE_DEFAULT_RELOAD_INTERVAL_ACTIVE, +} from 'web/components/loading/reload'; + +import withDialogNotification from 'web/components/notification/withDialogNotifiaction'; // eslint-disable-line max-len + +import FilterProvider from 'web/entities/filterprovider'; + +import DownloadReportDialog from 'web/pages/reports/downloadreportdialog'; + +import { + loadAllEntities as loadFilters, + selector as filterSelector, +} from 'web/store/entities/filters'; + +import { + loadAllEntities as loadReportFormats, + selector as reportFormatsSelector, +} from 'web/store/entities/reportformats'; + +import { + loadAllEntities as loadReportConfigs, + selector as reportConfigsSelector, +} from 'web/store/entities/reportconfigs'; + +import {loadAuditReportWithThreshold} from 'web/store/entities/report/actions'; +import {auditReportSelector} from 'web/store/entities/report/selectors'; + +import { + loadReportComposerDefaults, + renewSessionTimeout, + saveReportComposerDefaults, +} from 'web/store/usersettings/actions'; + +import {loadUserSettingDefaults} from 'web/store/usersettings/defaults/actions'; +import {getUserSettingsDefaults} from 'web/store/usersettings/defaults/selectors'; +import {getUserSettingsDefaultFilter} from 'web/store/usersettings/defaultfilters/selectors'; + +import { + getReportComposerDefaults, + getUsername, +} from 'web/store/usersettings/selectors'; + +import {create_pem_certificate} from 'web/utils/cert'; +import compose from 'web/utils/compose'; +import {generateFilename} from 'web/utils/render'; +import PropTypes from 'web/utils/proptypes'; + +import TargetComponent from '../targets/component'; +import PageTitle from 'web/components/layout/pagetitle'; + +import Page from './auditdetailscontent'; + +import FilterDialog from './detailsfilterdialog'; +import {pageFilter as setPageFilter} from 'web/store/pages/actions'; +import getPage from 'web/store/pages/selectors'; +import useGmp from 'web/hooks/useGmp'; + +const log = logger.getLogger('web.pages.auditreport.detailspage'); + +const DEFAULT_FILTER = Filter.fromString( + 'levels=hmlg rows=100 min_qod=70 first=1 compliance_levels=yniu sort=compliant', +); + +export const AUDIT_REPORT_RESET_FILTER = RESET_FILTER.copy() + .setSortOrder('sort') + .setSortBy('compliant'); + +const REPORT_FORMATS_FILTER = Filter.fromString('active=1 and trust=1 rows=-1'); + +const getReportPageName = id => `report-${id}`; + +const getTarget = (entity = {}) => { + const {report = {}} = entity; + const {task = {}} = report; + return task.target; +}; + +const ReportDetails = props => { + const [activeTab, setActiveTab] = useState(0); + const [showFilterDialog, setShowFilterDialog] = useState(false); + const [showDownloadReportDialog, setShowDownloadReportDialog] = + useState(false); + const [sorting, setSorting] = useState({ + results: { + sortField: 'compliant', + sortReverse: true, + }, + hosts: { + sortField: 'compliant', + sortReverse: true, + }, + os: { + sortField: 'compliant', + sortReverse: true, + }, + tlscerts: { + sortField: 'dn', + sortReverse: false, + }, + errors: { + sortField: 'error', + sortReverse: false, + }, + }); + + const [entity, setEntity] = useState(); + const [resultsCounts, setResultsCounts] = useState(); + const [hostsCounts, setHostsCounts] = useState(); + const [operatingSystemsCounts, setOperatingSystemsCounts] = useState(); + const [tlsCertificatesCounts, setTlsCertificatesCounts] = useState(); + const [reportFormatId, setReportFormatId] = useState(); + const [errorsCounts, setErrorsCounts] = useState(); + const [reportFilter, setReportFilter] = useState(); + const [isUpdating, setIsUpdating] = useState(false); + // storeAsDefault is set in SaveDialogContent + // eslint-disable-next-line no-unused-vars + const [storeAsDefault, setStoreAsDefault] = useState(); + + const [_] = useTranslation(); + const gmp = useGmp(); + const dispatch = useDispatch(); + const match = useRouteMatch(); + const {id: reportId} = match.params; + + const pSelector = useSelector(getPage, shallowEqual); + const pageFilter = pSelector?.getFilter(getReportPageName(reportId)); + + const [selectedEntity, reportError, isLoading] = useSelector(state => { + const reportSel = auditReportSelector(state); + return [ + reportSel?.getEntity(reportId, pageFilter), + reportSel?.getEntityError(reportId, pageFilter), + reportSel?.isLoadingEntity(reportId, pageFilter), + ]; + }, shallowEqual); + + const userDefaultsSelector = useSelector( + getUserSettingsDefaults, + shallowEqual, + ); + const reportExportFileName = userDefaultsSelector?.getValueByName( + 'reportexportfilename', + ); + + const reportFormatsSel = useSelector(reportFormatsSelector); + const reportConfigsSel = useSelector(reportConfigsSelector); + const reportFormats = reportFormatsSel?.getAllEntities(REPORT_FORMATS_FILTER); + const reportConfigs = reportConfigsSel?.getAllEntities(ALL_FILTER); + const reportComposerDefaults = useSelector(getReportComposerDefaults); + const userDefaultFilterSel = useSelector( + rootState => getUserSettingsDefaultFilter(rootState, 'result'), + shallowEqual, + ); + const resultDefaultFilter = userDefaultFilterSel?.getFilter(); + const username = useSelector(getUsername); + + useEffect(() => { + dispatch(loadUserSettingDefaults(gmp)()); + dispatch(loadFilters(gmp)(RESULTS_FILTER_FILTER)); + dispatch(loadReportFormats(gmp)(REPORT_FORMATS_FILTER)); + dispatch(loadReportConfigs(gmp)(ALL_FILTER)); + dispatch(loadReportComposerDefaults(gmp)()); + + if (isDefined(selectedEntity)) { + setEntity(entity); + updateReportCounts(selectedEntity); + setReportFilter(props.reportFilter); + setIsUpdating(false); + } else { + setIsUpdating(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (isDefined(selectedEntity)) { + // update only if a new report is available to avoid having no report + // when the filter changes + setEntity(selectedEntity); + updateReportCounts(selectedEntity); + setReportFilter(props.reportFilter); + setIsUpdating(false); + } else { + // report is not in the store and is currently loaded + setIsUpdating(true); + } + }, [selectedEntity, props.reportFilter]); + + useEffect(() => { + if ( + !isDefined(reportFormatId) && + isDefined(reportFormats) && + reportFormats.length > 0 + ) { + // set initial report format id if available + const initialReportFormatId = first(reportFormats).id; + if (isDefined(initialReportFormatId)) { + // ensure the report format id is only set if we really have one + // if no report format id is available we would create an infinite + // render loop here + setReportFormatId({initialReportFormatId}); + } else { + // if there is no report format at all, throw a proper error message + // instead of just showing x is undefined JS stacktrace + const noReportFormatError = _( + 'The report cannot be displayed because' + + ' no report format is available.' + + ' This could be due to a missing gvmd data feed. Please update' + + ' the gvmd data feed, check the "feed import owner" setting, the' + + ' feed status page, or contact your system administrator.', + ); + throw new Error(noReportFormatError); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reportFormats, reportFormatId]); + + useEffect(() => { + load(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reportId]); + + const updateReportCounts = reportEntity => { + const {report = {}} = reportEntity; + const { + results = {}, + hosts = {}, + operatingSystems = {}, + tlsCertificates = {}, + errors = {}, + } = report; + + if (isDefined(results.counts)) { + setResultsCounts(results.counts); + } + if (isDefined(hosts.counts)) { + setHostsCounts(hosts.counts); + } + if (isDefined(operatingSystems.counts)) { + setOperatingSystemsCounts(operatingSystems.counts); + } + if (isDefined(tlsCertificates.counts)) { + setTlsCertificatesCounts(tlsCertificates.counts); + } + if (isDefined(errors.counts)) { + setErrorsCounts(errors.counts); + } + }; + + const load = newFilter => { + log.debug('Loading report', { + newFilter, + }); + const {reportFilter: filter} = props; + + setIsUpdating(!isDefined(filter) || !filter.equals(newFilter)); // show update indicator if filter has changed + + props + .reload(newFilter) + .then(() => { + setIsUpdating(false); + }) + .catch(() => { + setIsUpdating(false); + }); + }; + + const reload = () => { + // reload data from backend + load(props.reportFilter); + }; + + const handleChanged = () => { + reload(); + }; + + const handleError = error => { + const {showError} = props; + log.error(error); + showError(error); + }; + + const handleFilterChange = filter => { + handleInteraction(); + load(filter); + }; + + const handleFilterRemoveClick = () => { + handleFilterChange(AUDIT_REPORT_RESET_FILTER); + }; + + const handleFilterResetClick = () => { + if (hasValue(resultDefaultFilter)) { + handleFilterChange(resultDefaultFilter); + } else { + handleFilterChange(DEFAULT_FILTER); + } + }; + + const handleActivateTab = index => { + handleInteraction(); + setActiveTab(index); + }; + + const handleAddToAssets = () => { + const {showSuccessMessage, reportFilter: filter} = props; + + handleInteraction(); + + gmp.auditreport.addAssets(selectedEntity, {filter}).then(() => { + showSuccessMessage( + _( + 'Report content added to Assets with QoD>=70% and Overrides enabled.', + ), + ); + reload(); + }, handleError); + }; + + const handleRemoveFromAssets = () => { + const {showSuccessMessage, reportFilter: filter} = props; + + handleInteraction(); + + gmp.auditreport.removeAssets(selectedEntity, {filter}).then(() => { + showSuccessMessage(_('Report content removed from Assets.')); + reload(); + }, handleError); + }; + + const handleFilterEditClick = () => { + handleInteraction(); + setShowFilterDialog(true); + }; + + const handleFilterDialogClose = () => { + handleInteraction(); + setShowFilterDialog(false); + }; + + const handleOpenDownloadReportDialog = () => { + setShowDownloadReportDialog(true); + }; + + const handleCloseDownloadReportDialog = () => { + setShowDownloadReportDialog(false); + }; + + const handleReportDownload = state => { + const {reportFilter: filter, onDownload} = props; + + const { + includeNotes, + includeOverrides, + // eslint-disable-next-line no-shadow + reportFormatId, + // eslint-disable-next-line no-shadow + storeAsDefault, + } = state; + + const newFilter = filter.copy(); + newFilter.set('notes', includeNotes); + newFilter.set('overrides', includeOverrides); + + if (storeAsDefault) { + const defaults = { + ...reportComposerDefaults, + defaultReportFormatId: reportFormatId, + includeNotes, + includeOverrides, + }; + dispatch(saveReportComposerDefaults(gmp)(defaults)); + } + + const report_format = reportFormats + ? reportFormats.find(format => reportFormatId === format.id) + : undefined; + + const extension = isDefined(report_format) + ? report_format.extension + : 'unknown'; // unknown should never happen but we should be save here + + handleInteraction(); + + return gmp.auditreport + .download(selectedEntity, { + reportFormatId, + filter: newFilter, + }) + .then(response => { + setShowDownloadReportDialog(false); + const {data} = response; + const filename = generateFilename({ + creationTime: selectedEntity.creationTime, + extension, + fileNameFormat: reportExportFileName, + id: selectedEntity.id, + modificationTime: selectedEntity.modificationTime, + reportFormat: report_format?.name, + resourceName: selectedEntity.task.name, + resourceType: 'report', + username, + }); + + onDownload({filename, data}); + }, handleError); + }; + + const handleTlsCertificateDownload = cert => { + const {onDownload} = props; + + const {data, serial} = cert; + + handleInteraction(); + + onDownload({ + filename: 'tls-cert-' + serial + '.pem', + mimetype: 'application/x-x509-ca-cert', + data: create_pem_certificate(data), + }); + }; + + const handleFilterCreated = filter => { + handleInteraction(); + load(filter); + dispatch(loadFilters(gmp)(RESULTS_FILTER_FILTER)); + }; + + const handleFilterDecreaseMinQoD = () => { + const {reportFilter: filter} = props; + + handleInteraction(); + + if (filter.has('min_qod')) { + const lfilter = filter.copy(); + lfilter.set('min_qod', 30); + load(lfilter); + } + }; + + const handleSortChange = (name, sortField) => { + handleInteraction(); + + const prev = sorting[name]; + + const sortReverse = + sortField === prev.sortField ? !prev.sortReverse : false; + + const newSort = { + ...sorting, + [name]: { + sortField, + sortReverse, + }, + }; + setSorting(newSort); + }; + + const handleInteraction = () => dispatch(renewSessionTimeout(gmp)()); + + const loadTarget = () => { + const target = getTarget(selectedEntity); + return gmp.target.get({id: target.id}); + }; + + const {showError, showErrorMessage, showSuccessMessage} = props; + + const report = isDefined(entity) ? entity.report : undefined; + + const threshold = gmp.settings.reportResultsThreshold; + + const showThresholdMessage = + isDefined(report) && report.results.counts.filtered > threshold; + + const [filters, isLoadingFilters] = useSelector(state => { + const filterSel = filterSelector(state); + return [ + filterSel?.getAllEntities(RESULTS_FILTER_FILTER), + filterSel?.isLoadingAllEntities(RESULTS_FILTER_FILTER), + ]; + }); + + return ( + + + + {({edit}) => ( + + loadTarget().then(response => edit(response.data)) + } + onTlsCertificateDownloadClick={handleTlsCertificateDownload} + showError={showError} + showErrorMessage={showErrorMessage} + showSuccessMessage={showSuccessMessage} + /> + )} + + {showFilterDialog && ( + + )} + {showDownloadReportDialog && ( + + )} + + ); +}; + +ReportDetails.propTypes = { + location: PropTypes.object.isRequired, + reload: PropTypes.func.isRequired, + reportFilter: PropTypes.filter, + showError: PropTypes.func.isRequired, + showErrorMessage: PropTypes.func.isRequired, + showSuccessMessage: PropTypes.func.isRequired, + target: PropTypes.model, + username: PropTypes.string, + onDownload: PropTypes.func.isRequired, +}; + +const reloadInterval = report => + isDefined(report) && isActive(report.report.scan_run_status) + ? USE_DEFAULT_RELOAD_INTERVAL_ACTIVE + : NO_RELOAD; // report doesn't change anymore. no need to reload + +const load = + ({ + defaultFilter, + reportId, + // eslint-disable-next-line no-shadow + dispatch, + gmp, + match, + pageFilter, + reportFilter, + }) => + filter => { + if (!hasValue(filter)) { + // use loaded filter after initial loading + filter = reportFilter; + } + + if (!hasValue(filter)) { + // use filter from store + filter = pageFilter; + } + + if (!hasValue(filter)) { + // use filter from user setting + filter = defaultFilter; + } + + if (!hasValue(filter)) { + // use fallback filter + filter = DEFAULT_FILTER; + } + dispatch(setPageFilter(getReportPageName(match.params.id), filter)); + return dispatch(loadAuditReportWithThreshold(gmp)(reportId, {filter})); + }; + +const ReportDetailsWrapper = props => { + const dispatch = useDispatch(); + const gmp = useGmp(); + const match = useRouteMatch(); + + const {id: reportId} = match.params; + const reportSel = useSelector(auditReportSelector, shallowEqual); + const pSelector = useSelector(getPage, shallowEqual); + + const pageFilter = pSelector.getFilter(getReportPageName(reportId)); + const entity = reportSel.getEntity(reportId, pageFilter); + const reportFilter = entity?.report?.filter; + + return ( + + {({filter}) => ( + reloadInterval(entity)} + > + {({reload}) => ( + + )} + + )} + + ); +}; + +export default compose( + withDialogNotification, + withDownload, +)(ReportDetailsWrapper); \ No newline at end of file diff --git a/src/web/pages/reports/auditfilterdialog.jsx b/src/web/pages/reports/auditfilterdialog.jsx new file mode 100644 index 0000000000..aa0db7183b --- /dev/null +++ b/src/web/pages/reports/auditfilterdialog.jsx @@ -0,0 +1,134 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import React from 'react'; + +import Layout from 'web/components/layout/layout'; + +import useCapabilities from 'web/hooks/useCapabilities'; + +import useTranslation from 'web/hooks/useTranslation'; + +/* eslint-disable max-len */ + +import CreateNamedFilterGroup from 'web/components/powerfilter/createnamedfiltergroup'; +import FilterStringGroup from 'web/components/powerfilter/filterstringgroup'; +import FirstResultGroup from 'web/components/powerfilter/firstresultgroup'; +import MinQodGroup from 'web/components/powerfilter/minqodgroup'; +import ResultsPerPageGroup from 'web/components/powerfilter/resultsperpagegroup'; +import SortByGroup from 'web/components/powerfilter/sortbygroup'; +import withFilterDialog from 'web/components/powerfilter/withFilterDialog'; +import FilterDialogPropTypes from 'web/components/powerfilter/dialogproptypes'; +import ComplianceLevelFilterGroup from 'web/components/powerfilter/compliancelevelsgroup'; +import FilterSearchGroup from 'web/components/powerfilter/filtersearchgroup'; + +/* eslint-enable */ + +const AuditReportFilterDialogComponent = ({ + filter, + filterName, + filterstring, + onFilterChange, + saveNamedFilter, + onFilterStringChange, + onFilterValueChange, + onSearchTermChange, + onSortByChange, + onSortOrderChange, + onValueChange, +}) => { + const [_] = useTranslation(); + const capabilities = useCapabilities(); + const handleRemoveCompliance = () => + onFilterChange(filter.delete('report_compliance_levels')); + const SORT_FIELDS = [ + { + name: 'date', + displayName: _('Date'), + }, + { + name: 'status', + displayName: _('Status'), + }, + { + name: 'task', + displayName: _('Task'), + }, + { + name: 'compliant', + displayName: _('Compliant'), + }, + { + name: 'compliance_yes', + displayName: _('Compliance: Yes'), + }, + { + name: 'compliance_no', + displayName: _('Compliance: No'), + }, + { + name: 'compliance_incomplete', + displayName: _('Compliance: Incomplete'), + }, + ]; + + if (!filter) { + return null; + } + + return ( + + + + + + + + + + + + + + + + {capabilities.mayCreate('filter') && ( + + )} + + ); +}; + +AuditReportFilterDialogComponent.propTypes = FilterDialogPropTypes; + +export default withFilterDialog()(AuditReportFilterDialogComponent); \ No newline at end of file diff --git a/src/web/pages/reports/auditreportrow.jsx b/src/web/pages/reports/auditreportrow.jsx new file mode 100644 index 0000000000..28334e9b3d --- /dev/null +++ b/src/web/pages/reports/auditreportrow.jsx @@ -0,0 +1,147 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + + +import React from 'react'; + +import useTranslation from 'web/hooks/useTranslation'; + +import {isDefined} from 'gmp/utils/identity'; + +import {TASK_STATUS, isActive} from 'gmp/models/task'; + +import StatusBar from 'web/components/bar/statusbar'; + +import DateTime from 'web/components/date/datetime'; + +import DeleteIcon from 'web/components/icon/deleteicon'; +import DeltaIcon from 'web/components/icon/deltaicon'; +import DeltaSecondIcon from 'web/components/icon/deltasecondicon'; + +import IconDivider from 'web/components/layout/icondivider'; + +import DetailsLink from 'web/components/link/detailslink'; + +import TableData from 'web/components/table/data'; +import TableRow from 'web/components/table/row'; + +import withEntitiesActions from 'web/entities/withEntitiesActions'; + +import ComplianceBar from 'web/components/bar/compliancebar'; + +import PropTypes from 'web/utils/proptypes'; + +const Actions = withEntitiesActions( + ({entity, selectedDeltaReport, onReportDeleteClick, onReportDeltaSelect}) => { + const {report} = entity; + + const scanActive = isActive(report.scan_run_status); + + const [_] = useTranslation(); + const title = scanActive ? _('Scan is active') : _('Delete Report'); + + return ( + + {isDefined(selectedDeltaReport) ? ( + entity.id === selectedDeltaReport.id ? ( + + ) : ( + + ) + ) : ( + + )} + + + ); + }, +); + +Actions.propTypes = { + entity: PropTypes.model.isRequired, + selectedDeltaReport: PropTypes.model, + onReportDeleteClick: PropTypes.func.isRequired, + onReportDeltaSelect: PropTypes.func.isRequired, +}; + +const AuditRow = ({ + actionsComponent: ActionsComponent = Actions, + entity, + links = true, + ...props +}) => { + const {report} = entity; + const {scan_run_status, task} = report; + + let status = scan_run_status; + let progress; + + if (isDefined(task)) { + if (task.isContainer() && status !== TASK_STATUS.processing) { + status = + status === TASK_STATUS.interrupted + ? TASK_STATUS.uploadinginterrupted + : status === TASK_STATUS.running || status === TASK_STATUS.processing + ? TASK_STATUS.uploading + : TASK_STATUS.container; + } + progress = task.progress; + } + + return ( + + + + + + + + + + + + + + + {entity.task.name} + + + + + + + {report.complianceCounts.yes.filtered} + {report.complianceCounts.no.filtered} + + {report.complianceCounts.incomplete.filtered} + + + + ); +}; + +AuditRow.propTypes = { + actionsComponent: PropTypes.component, + entity: PropTypes.model.isRequired, + links: PropTypes.bool, +}; + +export default AuditRow; \ No newline at end of file diff --git a/src/web/pages/reports/auditreportslistpage.jsx b/src/web/pages/reports/auditreportslistpage.jsx new file mode 100644 index 0000000000..b12d3739a0 --- /dev/null +++ b/src/web/pages/reports/auditreportslistpage.jsx @@ -0,0 +1,169 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + + +import React, {useEffect, useState} from 'react'; + +import {useHistory} from 'react-router-dom'; + +import useTranslation from 'web/hooks/useTranslation'; + +import Filter, {AUDIT_REPORTS_FILTER_FILTER} from 'gmp/models/filter'; + +import {isActive} from 'gmp/models/task'; + +import {isDefined} from 'gmp/utils/identity'; + +import EntitiesPage from 'web/entities/page'; +import withEntitiesContainer from 'web/entities/withEntitiesContainer'; + +import DashboardControls from 'web/components/dashboard/controls'; + +import ManualIcon from 'web/components/icon/manualicon'; +import ReportIcon from 'web/components/icon/reporticon'; + +import IconDivider from 'web/components/layout/icondivider'; +import PageTitle from 'web/components/layout/pagetitle'; + +import { + USE_DEFAULT_RELOAD_INTERVAL, + USE_DEFAULT_RELOAD_INTERVAL_ACTIVE, +} from 'web/components/loading/reload'; + +import { + loadEntities, + selector as entitiesSelector, +} from 'web/store/entities/auditreports'; + +import PropTypes from 'web/utils/proptypes'; + +import AuditFilterDialog from './auditfilterdialog'; +import AuditReportsTable from './auditreportstable'; + +import AuditReportsDashboard, { + AUDIT_REPORTS_DASHBOARD_ID, +} from './auditdashboard'; + +const ToolBarIcons = () => { + const [_] = useTranslation(); + return ( + + + + ) +}; + +const AuditReportsPage = ({ + filter, + onFilterChanged, + onInteraction, + onDelete, + ...props +}) => { + const [selectedDeltaReport, setSelectedDeltaReport] = useState(); + const [beforeSelectFilter, setBeforeSelectFilter] = useState(); + const history = useHistory(); + const [_] = useTranslation(); + + useEffect(() => { + if ( + isDefined(selectedDeltaReport) && + (!isDefined(filter) || + filter.get('task_id') !== selectedDeltaReport.task.id) + ) { + // filter has changed. reset delta report selection + setSelectedDeltaReport(); + } + }, [filter, selectedDeltaReport]); + + const handleReportDeleteClick = onDelete; + + const handleReportDeltaSelect = report => { + if (isDefined(selectedDeltaReport)) { + onFilterChanged(beforeSelectFilter); + + history.push( + '/auditreport/delta/' + selectedDeltaReport.id + '/' + report.id, + ); + } else { + if (!isDefined(filter)) { + filter = new Filter(); + } + + onFilterChanged( + filter + .copy() + .set('first', 1) // reset to first page + .set('task_id', report.task.id), + ); + setSelectedDeltaReport(report); + setBeforeSelectFilter(filter); + } + }; + + return ( + + + ( + + )} + dashboardControls={() => ( + + )} + filtersFilter={AUDIT_REPORTS_FILTER_FILTER} + filterEditDialog={AuditFilterDialog} + table={AuditReportsTable} + toolBarIcons={ToolBarIcons} + title={_('Audit Reports')} + sectionIcon={} + filter={filter} + onFilterChanged={onFilterChanged} + onInteraction={onInteraction} + onReportDeltaSelect={handleReportDeltaSelect} + onReportDeleteClick={handleReportDeleteClick} + /> + + ); +}; + +AuditReportsPage.propTypes = { + filter: PropTypes.filter, + history: PropTypes.object.isRequired, + onChanged: PropTypes.func.isRequired, + onDelete: PropTypes.func.isRequired, + onError: PropTypes.func.isRequired, + onFilterChanged: PropTypes.func.isRequired, + onInteraction: PropTypes.func.isRequired, +}; + +const reportsReloadInterval = ({entities = []}) => + entities.some(entity => isActive(entity.report.scan_run_status)) + ? USE_DEFAULT_RELOAD_INTERVAL_ACTIVE + : USE_DEFAULT_RELOAD_INTERVAL; + +const FALLBACK_AUDIT_REPORT_LIST_FILTER = Filter.fromString( + 'report_compliance_levels=yniu sort-reverse=date first=1', +); + +export default withEntitiesContainer('auditreport', { + fallbackFilter: FALLBACK_AUDIT_REPORT_LIST_FILTER, + entitiesSelector, + loadEntities, + reloadInterval: reportsReloadInterval, + })(AuditReportsPage); \ No newline at end of file diff --git a/src/web/pages/reports/auditreportstable.jsx b/src/web/pages/reports/auditreportstable.jsx new file mode 100644 index 0000000000..4608cf114f --- /dev/null +++ b/src/web/pages/reports/auditreportstable.jsx @@ -0,0 +1,130 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import React from 'react'; + +import {_l} from 'gmp/locale/lang'; +import useTranslation from 'web/hooks/useTranslation'; + +import {isDefined} from 'gmp/utils/identity'; + +import PropTypes from 'web/utils/proptypes'; + +import {createEntitiesFooter} from 'web/entities/footer'; +import {createEntitiesTable} from 'web/entities/table'; + +import ComplianceState from 'web/components/label/compliancestate'; + +import TableHead from 'web/components/table/head'; +import TableHeader from 'web/components/table/header'; +import TableRow from 'web/components/table/row'; + +import AuditReportRow from './auditreportrow'; + +const Header = ({ + actionsColumn, + sort = true, + currentSortBy, + currentSortDir, + onSortChange, +}) => { + const [_] = useTranslation(); + return ( + + + + + + + + + + + + + + + + {isDefined(actionsColumn) ? ( + actionsColumn + ) : ( + + {_('Actions')} + + )} + + + ); +}; + +Header.propTypes = { + actionsColumn: PropTypes.element, + currentSortBy: PropTypes.string, + currentSortDir: PropTypes.string, + sort: PropTypes.bool, + onSortChange: PropTypes.func, +}; + +const Footer = createEntitiesFooter({ + span: 10, + delete: true, +}); + +export default createEntitiesTable({ + emptyTitle: _l('No reports available'), + header: Header, + footer: Footer, + row: AuditReportRow, + toggleDetailsIcon: false, +}); \ No newline at end of file diff --git a/src/web/pages/reports/deltadetailscontent.jsx b/src/web/pages/reports/deltadetailscontent.jsx index 97141d8c48..b2379a4258 100644 --- a/src/web/pages/reports/deltadetailscontent.jsx +++ b/src/web/pages/reports/deltadetailscontent.jsx @@ -56,6 +56,7 @@ const Span = styled.span` const PageContent = ({ activeTab, + audit = false, entity, entityError, filter, @@ -70,7 +71,6 @@ const PageContent = ({ task, onActivateTab, onAddToAssetsClick, - onTlsCertificateDownloadClick, onError, onFilterAddLogLevelClick, onFilterChanged, @@ -92,7 +92,13 @@ const PageContent = ({ const {userTags = {}} = report; const userTagsCount = userTags.length; - const {results = {}, result_count = {}, timestamp, scan_run_status} = report; + const { + results = {}, + complianceCounts = {}, + result_count = {}, + timestamp, + scan_run_status, + } = report; const hasReport = isDefined(entity); @@ -126,12 +132,13 @@ const PageContent = ({ ); - const {filtered} = result_count; + const {filtered} = audit ? complianceCounts : result_count; return ( this.loadTarget().then(response => edit(response.data)) } - onTlsCertificateDownloadClick={this.handleTlsCertificateDownload} showError={showError} showErrorMessage={showErrorMessage} showSuccessMessage={showSuccessMessage} diff --git a/src/web/pages/reports/details/__tests__/hoststab.jsx b/src/web/pages/reports/details/__tests__/hoststab.jsx index f96cd8716c..d1ab375865 100644 --- a/src/web/pages/reports/details/__tests__/hoststab.jsx +++ b/src/web/pages/reports/details/__tests__/hoststab.jsx @@ -14,6 +14,7 @@ import {setTimezone, setUsername} from 'web/store/usersettings/actions'; import {rendererWith} from 'web/utils/testing'; import {getMockReport} from 'web/pages/reports/__mocks__/mockreport'; +import {getMockAuditReport} from 'web/pages/reports/__mocks__/mockauditreport'; import HostsTab from '../hoststab'; @@ -114,3 +115,107 @@ describe('Report Hosts Tab tests', () => { ); }); }); + +const auditfilter = Filter.fromString( + 'apply_overrides=0 levels=hmlg rows=3 min_qod=70 first=1 sort=compliant', +); + +describe('Audit Report Hosts Tab tests', () => { + test('should render Audit Report Hosts Tab', () => { + const {hosts} = getMockAuditReport(); + + const onSortChange = testing.fn(); + const onInteraction = testing.fn(); + + const {render, store} = rendererWith({ + gmp, + capabilities: caps, + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const {baseElement, getAllByTestId} = render( + onSortChange('hosts', sortField)} + />, + ); + + const images = baseElement.querySelectorAll('img'); + const links = baseElement.querySelectorAll('a'); + const header = baseElement.querySelectorAll('th'); + const rows = baseElement.querySelectorAll('tr'); + + const bars = getAllByTestId('progressbar-box'); + + // Headings + expect(header[0]).toHaveTextContent('IP Address'); + expect(header[1]).toHaveTextContent('Hostname'); + expect(header[2]).toHaveTextContent('OS'); + expect(header[3]).toHaveTextContent('Ports'); + expect(header[4]).toHaveTextContent('Apps'); + expect(header[5]).toHaveTextContent('Distance'); + expect(header[6]).toHaveTextContent('Auth'); + expect(header[7]).toHaveTextContent('Start'); + expect(header[8]).toHaveTextContent('End'); + expect(header[9]).toHaveTextContent('Yes'); + expect(header[10]).toHaveTextContent('No'); + expect(header[11]).toHaveTextContent('Incomplete'); + expect(header[12]).toHaveTextContent('Total'); + expect(header[13]).toHaveTextContent('Compliant'); + + // Row 1 + expect(links[13]).toHaveAttribute( + 'href', + '/hosts?filter=name%3D109.876.54.321', + ); // filter by name because host has no asset id + expect(links[13]).toHaveTextContent('109.876.54.321'); + expect(rows[1]).toHaveTextContent('lorem.ipsum'); + expect(images[0]).toHaveAttribute('src', '/img/os_unknown.svg'); + expect(rows[1]).toHaveTextContent('1521'); // 15 Ports, 2 Apps, 1 Distance + expect(rows[1]).toHaveTextContent('Mon, Jun 3, 2019 1:15 PM CEST'); + expect(rows[1]).toHaveTextContent('Mon, Jun 3, 2019 1:31 PM CEST'); + expect(rows[1]).toHaveTextContent('170540'); // 17 Yes, 0 No, 5 Incomplete, 40 Total + expect(bars[0]).toHaveAttribute('title', 'Incomplete'); + expect(bars[0]).toHaveTextContent('Incomplete'); + + // Row 2 + expect(links[14]).toHaveAttribute('href', '/host/123'); + expect(links[14]).toHaveTextContent('123.456.78.910'); + expect(rows[2]).toHaveTextContent('foo.bar'); + expect(images[0]).toHaveAttribute('src', '/img/os_unknown.svg'); + expect(rows[2]).toHaveTextContent('1032'); // 10 Ports, 3 Apps, 2 Distance + expect(rows[2]).toHaveTextContent('Mon, Jun 3, 2019 1:00 PM CEST'); + expect(rows[2]).toHaveTextContent('Mon, Jun 3, 2019 1:15 PM CEST'); + expect(rows[2]).toHaveTextContent('7301450'); // 7 Yes, 30 No, 14 Incomplete, 50 Total + expect(bars[1]).toHaveAttribute('title', 'No'); + expect(bars[1]).toHaveTextContent('No'); + + // Row 3 + expect(links[15]).toHaveAttribute('href', '/host/123'); + expect(links[15]).toHaveTextContent('123.456.78.810'); + expect(rows[3]).toHaveTextContent('foo.bar'); + expect(images[0]).toHaveAttribute('src', '/img/os_unknown.svg'); + expect(rows[3]).toHaveTextContent('1032'); // 10 Ports, 3 Apps, 2 Distance + expect(rows[3]).toHaveTextContent('Mon, Jun 3, 2019 1:00 PM CEST'); + expect(rows[3]).toHaveTextContent('Mon, Jun 3, 2019 1:15 PM CEST'); + expect(rows[3]).toHaveTextContent('200020'); // 20 Yes, 0 No, 0 Incomplete, 20 Total + expect(bars[2]).toHaveAttribute('title', 'Yes'); + expect(bars[2]).toHaveTextContent('Yes'); + + // Filter + expect(baseElement).toHaveTextContent( + '(Applied filter: apply_overrides=0 levels=hmlg rows=3 min_qod=70 first=1 sort=compliant)', + ); + }); +}); diff --git a/src/web/pages/reports/details/__tests__/operatingsystemstab.jsx b/src/web/pages/reports/details/__tests__/operatingsystemstab.jsx index cb382d0b38..fe335de140 100644 --- a/src/web/pages/reports/details/__tests__/operatingsystemstab.jsx +++ b/src/web/pages/reports/details/__tests__/operatingsystemstab.jsx @@ -13,6 +13,8 @@ import {rendererWith} from 'web/utils/testing'; import {getMockReport} from 'web/pages/reports/__mocks__/mockreport'; +import {getMockAuditReport} from 'web/pages/reports/__mocks__/mockauditreport'; + import OperatingSystemsTab from '../operatingsystemstab'; const filter = Filter.fromString( @@ -94,3 +96,84 @@ describe('Report Operating Systems Tab tests', () => { ); }); }); + +const auditfilter = Filter.fromString( + 'apply_overrides=0 levels=hmlg rows=3 min_qod=70 first=1 sort=compliant', +); + +describe('Audit Report Operating Systems Tab tests', () => { + test('should render Audit Report Operating Systems Tab', () => { + const {operatingsystems} = getMockAuditReport(); + + const onSortChange = testing.fn(); + const onInteraction = testing.fn(); + + const {render, store} = rendererWith({ + router: true, + store: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const {baseElement, getAllByTestId} = render( + , + ); + + const images = baseElement.querySelectorAll('img'); + const links = baseElement.querySelectorAll('a'); + const header = baseElement.querySelectorAll('th'); + const bars = getAllByTestId('progressbar-box'); + + // Headings + expect(header[0]).toHaveTextContent('Operating System'); + expect(header[1]).toHaveTextContent('CPE'); + expect(header[2]).toHaveTextContent('Hosts'); + expect(header[3]).toHaveTextContent('Compliant'); + + // Row 1 + expect(images[0]).toHaveAttribute('src', '/img/os_unknown.svg'); + expect(links[4]).toHaveAttribute( + 'href', + '/operatingsystems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', + ); + expect(links[4]).toHaveTextContent('Foo OS'); + expect(links[5]).toHaveAttribute( + 'href', + '/operatingsystems?filter=name%3Dcpe%3A%2Ffoo%2Fbar', + ); + expect(links[5]).toHaveTextContent('cpe:/foo/bar'); + expect(bars[0]).toHaveAttribute('title', 'No'); + expect(bars[0]).toHaveTextContent('No'); + + // Row 2 + expect(images[1]).toHaveAttribute('src', '/img/os_unknown.svg'); + expect(links[6]).toHaveAttribute( + 'href', + '/operatingsystems?filter=name%3Dcpe%3A%2Florem%2Fipsum', + ); + expect(links[6]).toHaveTextContent('Lorem OS'); + expect(links[7]).toHaveAttribute( + 'href', + '/operatingsystems?filter=name%3Dcpe%3A%2Florem%2Fipsum', + ); + expect(links[7]).toHaveTextContent('cpe:/lorem/ipsum'); + expect(bars[1]).toHaveAttribute('title', 'Incomplete'); + expect(bars[1]).toHaveTextContent('Incomplete'); + + // Filter + expect(baseElement).toHaveTextContent( + '(Applied filter: apply_overrides=0 levels=hmlg rows=3 min_qod=70 first=1 sort=compliant)', + ); + }); +}); diff --git a/src/web/pages/reports/details/__tests__/resultstab.jsx b/src/web/pages/reports/details/__tests__/resultstab.jsx new file mode 100644 index 0000000000..736267d6f8 --- /dev/null +++ b/src/web/pages/reports/details/__tests__/resultstab.jsx @@ -0,0 +1,369 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import {describe, test, expect, testing} from '@gsa/testing'; +import React from 'react'; + +import Filter from 'gmp/models/filter'; + +import {setTimezone, setUsername} from 'web/store/usersettings/actions'; +import CollectionCounts from 'gmp/collection/collectioncounts'; + +import {rendererWith, wait} from 'web/utils/testing'; +import Result from 'gmp/models/result'; +import {loadingActions} from 'web/store/usersettings/defaults/actions'; +import {defaultFilterLoadingActions} from 'web/store/usersettings/defaultfilters/actions'; +import {entitiesLoadingActions} from 'web/store/entities/results'; + +import ResultsTab from '../resultstab'; + +const reloadInterval = 1; +const manualUrl = 'test/'; + +// mock entities +export const result1 = Result.fromElement({ + _id: '101', + name: 'Result 1', + owner: {name: 'admin'}, + comment: 'Comment 1', + creation_time: '2019-06-03T11:06:31Z', + modification_time: '2019-06-03T11:06:31Z', + host: {__text: '123.456.78.910', hostname: 'foo'}, + port: '80/tcp', + nvt: { + _oid: '201', + type: 'nvt', + name: 'nvt1', + tags: 'solution_type=Mitigation', + refs: {ref: [{_type: 'cve', _id: 'CVE-2019-1234'}]}, + }, + threat: 'High', + severity: 10.0, + qod: {value: 80}, + compliance: 'yes', +}); + +export const result2 = Result.fromElement({ + _id: '102', + name: 'Result 2', + owner: {name: 'admin'}, + comment: 'Comment 2', + creation_time: '2019-06-03T11:06:31Z', + modification_time: '2019-06-03T11:06:31Z', + host: {__text: '109.876.54.321'}, + port: '80/tcp', + nvt: { + _oid: '202', + type: 'nvt', + name: 'nvt2', + tags: 'solution_type=VendorFix', + refs: {ref: [{_type: 'cve', _id: 'CVE-2019-5678'}]}, + }, + threat: 'Medium', + severity: 5.0, + qod: {value: 70}, + compliance: 'no', +}); + +export const result3 = Result.fromElement({ + _id: '103', + name: 'Result 3', + owner: {name: 'admin'}, + comment: 'Comment 3', + creation_time: '2019-06-03T11:06:31Z', + modification_time: '2019-06-03T11:06:31Z', + host: {__text: '109.876.54.321', hostname: 'bar'}, + port: '80/tcp', + nvt: { + _oid: '201', + type: 'nvt', + name: 'nvt1', + tags: 'solution_type=Mitigation', + refs: {ref: [{_type: 'cve', _id: 'CVE-2019-1234'}]}, + solution: { + _type: 'Mitigation', + }, + }, + threat: 'Medium', + severity: 5.0, + qod: {value: 80}, + compliance: 'incomplete', +}); + +const results = [result1, result2, result3]; + +let currentSettings; +let getAggregates; +let getDashboardSetting; +let getFilters; +let getResults; + +beforeEach(() => { + // mock gmp commands + + getResults = testing.fn().mockResolvedValue({ + data: results, + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }); + + getFilters = testing.fn().mockReturnValue( + Promise.resolve({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }), + ); + + getDashboardSetting = testing.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }); + + getAggregates = testing.fn().mockResolvedValue({ + data: [], + meta: { + filter: Filter.fromString(), + counts: new CollectionCounts(), + }, + }); + + currentSettings = testing.fn().mockResolvedValue({ + foo: 'bar', + }); +}); + +describe('Report Results Tab tests', () => { + test('should render Results Tab with compliance information', async () => { + const reload = testing.fn(); + const onFilterAddLogLevelClick = testing.fn(); + const onFilterDecreaseMinQoDClick = testing.fn(); + const onFilterEditClick = testing.fn(); + const onFilterRemoveClick = testing.fn(); + const onFilterRemoveSeverityClick = testing.fn(); + const onTargetEditClick = testing.fn(); + + const gmp = { + results: { + get: getResults, + getSeverityAggregates: getAggregates, + getWordCountsAggregates: getAggregates, + }, + filters: { + get: getFilters, + }, + dashboard: { + getSetting: getDashboardSetting, + }, + settings: {manualUrl, reloadInterval}, + user: {currentSettings}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('result', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success(results, filter, loadedFilter, counts), + ); + + const {baseElement} = render( + , + ); + + await wait(); + + const header = baseElement.querySelectorAll('th'); + const row = baseElement.querySelectorAll('tr'); + + expect(header[0]).toHaveTextContent('Vulnerability'); + expect(header[2]).toHaveTextContent('Compliant'); + expect(header[3]).toHaveTextContent('QoD'); + expect(header[4]).toHaveTextContent('Host'); + expect(header[5]).toHaveTextContent('Location'); + expect(header[6]).toHaveTextContent('Created'); + expect(header[7]).toHaveTextContent('IP'); + expect(header[8]).toHaveTextContent('Name'); + + expect(row[2]).toHaveTextContent('Result 1'); + expect(row[2]).toHaveTextContent('Yes'); + expect(row[2]).toHaveTextContent('80 %'); + expect(row[2]).toHaveTextContent('123.456.78.910'); + expect(row[2]).toHaveTextContent('foo'); + expect(row[2]).toHaveTextContent('80/tcp'); + expect(row[2]).toHaveTextContent('Mon, Jun 3, 2019 1:06 PM CEST'); + + expect(row[3]).toHaveTextContent('Result 2'); + expect(row[3]).toHaveTextContent('No'); + expect(row[3]).toHaveTextContent('70 %'); + expect(row[3]).toHaveTextContent('109.876.54.321'); + expect(row[3]).toHaveTextContent('80/tcp'); + expect(row[3]).toHaveTextContent('Mon, Jun 3, 2019 1:06 PM CEST'); + + expect(row[4]).toHaveTextContent('Result 3'); + expect(row[4]).toHaveTextContent('Incomplete'); + expect(row[4]).toHaveTextContent('80 %'); + expect(row[4]).toHaveTextContent('109.876.54.321'); + expect(row[4]).toHaveTextContent('bar'); + expect(row[4]).toHaveTextContent('80/tcp'); + expect(row[4]).toHaveTextContent('Mon, Jun 3, 2019 1:06 PM CEST'); + }); + + test('should render Results Tab with compliance information', async () => { + const reload = testing.fn(); + const onFilterAddLogLevelClick = testing.fn(); + const onFilterDecreaseMinQoDClick = testing.fn(); + const onFilterEditClick = testing.fn(); + const onFilterRemoveClick = testing.fn(); + const onFilterRemoveSeverityClick = testing.fn(); + const onTargetEditClick = testing.fn(); + + const gmp = { + results: { + get: getResults, + getSeverityAggregates: getAggregates, + getWordCountsAggregates: getAggregates, + }, + filters: { + get: getFilters, + }, + dashboard: { + getSetting: getDashboardSetting, + }, + settings: {manualUrl, reloadInterval}, + user: {currentSettings}, + }; + + const {render, store} = rendererWith({ + gmp, + capabilities: true, + store: true, + router: true, + }); + + store.dispatch(setTimezone('CET')); + store.dispatch(setUsername('admin')); + + const defaultSettingfilter = Filter.fromString('foo=bar'); + store.dispatch(loadingActions.success({rowsperpage: {value: '2'}})); + store.dispatch( + defaultFilterLoadingActions.success('result', defaultSettingfilter), + ); + + const counts = new CollectionCounts({ + first: 1, + all: 1, + filtered: 1, + length: 1, + rows: 10, + }); + const filter = Filter.fromString('first=1 rows=10'); + const loadedFilter = Filter.fromString('first=1 rows=10'); + store.dispatch( + entitiesLoadingActions.success(results, filter, loadedFilter, counts), + ); + + const {baseElement} = render( + , + ); + + await wait(); + + const header = baseElement.querySelectorAll('th'); + const row = baseElement.querySelectorAll('tr'); + + expect(header[0]).toHaveTextContent('Vulnerability'); + expect(header[2]).toHaveTextContent('Severity'); + expect(header[3]).toHaveTextContent('QoD'); + expect(header[4]).toHaveTextContent('Host'); + expect(header[5]).toHaveTextContent('Location'); + expect(header[6]).toHaveTextContent('Created'); + expect(header[7]).toHaveTextContent('IP'); + expect(header[8]).toHaveTextContent('Name'); + + expect(row[2]).toHaveTextContent('Result 1'); + expect(row[2]).toHaveTextContent('10.0 (High)'); + expect(row[2]).toHaveTextContent('80 %'); + expect(row[2]).toHaveTextContent('123.456.78.910'); + expect(row[2]).toHaveTextContent('foo'); + expect(row[2]).toHaveTextContent('80/tcp'); + expect(row[2]).toHaveTextContent('Mon, Jun 3, 2019 1:06 PM CEST'); + + expect(row[3]).toHaveTextContent('Result 2'); + expect(row[3]).toHaveTextContent('5.0 (Medium)'); + expect(row[3]).toHaveTextContent('70 %'); + expect(row[3]).toHaveTextContent('109.876.54.321'); + expect(row[3]).toHaveTextContent('80/tcp'); + expect(row[3]).toHaveTextContent('Mon, Jun 3, 2019 1:06 PM CEST'); + + expect(row[4]).toHaveTextContent('Result 3'); + expect(row[4]).toHaveTextContent('5.0 (Medium)'); + expect(row[4]).toHaveTextContent('80 %'); + expect(row[4]).toHaveTextContent('109.876.54.321'); + expect(row[4]).toHaveTextContent('bar'); + expect(row[4]).toHaveTextContent('80/tcp'); + expect(row[4]).toHaveTextContent('Mon, Jun 3, 2019 1:06 PM CEST'); + }); +}); diff --git a/src/web/pages/reports/details/auditthresholdpanel.jsx b/src/web/pages/reports/details/auditthresholdpanel.jsx new file mode 100644 index 0000000000..d9b141a1b7 --- /dev/null +++ b/src/web/pages/reports/details/auditthresholdpanel.jsx @@ -0,0 +1,150 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import React from 'react'; + +import styled from 'styled-components'; + +import useTranslation from 'web/hooks/useTranslation'; + +import Divider from 'web/components/layout/divider'; + +import InfoPanel from 'web/components/panel/infopanel'; + +import FilterIcon from 'web/components/icon/filtericon'; + +import PropTypes from 'web/utils/proptypes'; + +import ReportPanel from './reportpanel'; +import EditIcon from 'web/components/icon/editicon'; +import FootNote from 'web/components/footnote/footnote'; +import Layout from 'web/components/layout/layout'; + +const UpdatingDivider = styled(({isUpdating, ...props}) => ( + +))` + opacity: ${props => (props.isUpdating ? '0.2' : '1.0')}; +`; + +const AuditThresholdPanel = ({ + entityType, + filter, + isUpdating = false, + threshold, + onFilterChanged, + onFilterEditClick, +}) => { + const [_] = useTranslation(); + + const compliance = filter.get('compliance_levels', ''); + + const handleRemoveComplianceYes = () => { + if (compliance.includes('y')) { + const newCompliance = compliance.replace('y', ''); + const lfilter = filter.copy(); + lfilter.set('compliance_levels', newCompliance); + + onFilterChanged(lfilter); + } + }; + + const handleRemoveComplianceUndefined = () => { + if (compliance.includes('u')) { + const newCompliance = compliance.replace('u', ''); + const lfilter = filter.copy(); + lfilter.set('compliance_levels', newCompliance); + + onFilterChanged(lfilter); + } + }; + + const handleRemoveComplianceIncomplete = () => { + if (compliance.includes('i')) { + const newCompliance = compliance.replace('i', ''); + const lfilter = filter.copy(); + lfilter.set('compliance_levels', newCompliance); + + onFilterChanged(lfilter); + } + }; + + return ( + + + + {compliance.includes('y') && ( + } + title={_('Results with compliance "Yes" are currently included.')} + onClick={handleRemoveComplianceYes} + > + {_('Filter out results with compliance "Yes".')} + + )} + {compliance.includes('u') && ( + } + title={_( + 'Results with compliance "Undefined" are currently included.', + )} + onClick={handleRemoveComplianceUndefined} + > + {_('Filter out results with compliance "Undefined".')} + + )} + {compliance.includes('i') && ( + } + title={_( + 'Results with compliance "Incomplete" are currently included.', + )} + onClick={handleRemoveComplianceIncomplete} + > + {_('Filter out results with compliance "Incomplete".')} + + )} + } + title={_('Your filter settings may be too unrefined.')} + onClick={onFilterEditClick} + > + {_('Adjust and update your filter settings.')} + + + + + {_('(Applied filter: {{- filter}})', { + filter: filter.simple().toFilterString(), + })} + + + + ); +}; + +AuditThresholdPanel.propTypes = { + entityType: PropTypes.string.isRequired, + filter: PropTypes.filter.isRequired, + isUpdating: PropTypes.bool, + threshold: PropTypes.number.isRequired, + onFilterChanged: PropTypes.func.isRequired, + onFilterEditClick: PropTypes.func.isRequired, +}; + +export default AuditThresholdPanel; diff --git a/src/web/pages/reports/details/deltaresultstab.jsx b/src/web/pages/reports/details/deltaresultstab.jsx index 4b3a52a4ba..95d37996df 100644 --- a/src/web/pages/reports/details/deltaresultstab.jsx +++ b/src/web/pages/reports/details/deltaresultstab.jsx @@ -32,9 +32,11 @@ const resultsSortFunctions = { severity: makeCompareSeverity(), solution_type: makeCompareString(entity => entity.nvt.solution?.type), vulnerability: makeCompareString('vulnerability'), + compliant: makeCompareString('compliance'), }; const ResultsTab = ({ + audit = false, counts, delta = false, filter, @@ -99,6 +101,7 @@ const ResultsTab = ({ onPreviousClick, }) => ( - {!levels.includes('g') && ( + {!levels.includes('g') && isDefined(onFilterAddLogLevelClick) && ( } title={_('Log messages are currently excluded.')} @@ -66,7 +66,7 @@ const EmptyResultsReport = ({ )} - {has_severity_filter && ( + {has_severity_filter && isDefined(onFilterRemoveSeverityClick) && ( } title={_( @@ -117,11 +117,11 @@ const EmptyResultsReport = ({ EmptyResultsReport.propTypes = { all: PropTypes.number.isRequired, filter: PropTypes.filter.isRequired, - onFilterAddLogLevelClick: PropTypes.func.isRequired, + onFilterAddLogLevelClick: PropTypes.func, onFilterDecreaseMinQoDClick: PropTypes.func.isRequired, onFilterEditClick: PropTypes.func.isRequired, onFilterRemoveClick: PropTypes.func.isRequired, - onFilterRemoveSeverityClick: PropTypes.func.isRequired, + onFilterRemoveSeverityClick: PropTypes.func, }; export default EmptyResultsReport; diff --git a/src/web/pages/reports/details/hoststab.jsx b/src/web/pages/reports/details/hoststab.jsx index 5da00be099..60095d019e 100644 --- a/src/web/pages/reports/details/hoststab.jsx +++ b/src/web/pages/reports/details/hoststab.jsx @@ -37,9 +37,17 @@ const hostsSortFunctions = { start: makeCompareDate(entity => entity.start), end: makeCompareDate(entity => entity.end), total: makeCompareNumber(entity => entity.result_counts.total), + complianceYes: makeCompareNumber(entity => entity.complianceCounts.yes), + complianceNo: makeCompareNumber(entity => entity.complianceCounts.no), + complianceIncomplete: makeCompareNumber( + entity => entity.complianceCounts.incomplete, + ), + complianceTotal: makeCompareNumber(entity => entity.complianceCounts.total), + compliant: makeCompareString('hostCompliance'), }; const HostsTab = ({ + audit = false, counts, hosts, filter, @@ -69,6 +77,7 @@ const HostsTab = ({ onPreviousClick, }) => ( ( +const Header = ({ + audit = false, + currentSortBy, + currentSortDir, + sort = true, + onSortChange, +}) => ( ( onSortChange={onSortChange} title={_('End')} /> - - - - - - - + {audit ? ( + + ) : ( + + )} + {audit ? ( + + ) : ( + + )} + {audit ? ( + + ) : ( + + )} + {!audit && ( + + )} + {!audit && ( + + )} + {audit ? ( + + ) : ( + + )} + {audit ? ( + + ) : ( + + )} ); Header.propTypes = { + audit: PropTypes.bool, currentSortBy: PropTypes.string, currentSortDir: PropTypes.string, sort: PropTypes.bool, @@ -206,14 +273,16 @@ const renderAuthIcons = authSuccess => { ); }; -const Row = ({entity, links = true}) => { +const Row = ({entity, links = true, audit = false}) => { const { asset = {}, authSuccess, details = {}, end, + hostCompliance, ip, result_counts = {}, + complianceCounts = {}, severity, start, portsCount, @@ -251,20 +320,43 @@ const Row = ({entity, links = true}) => { - {result_counts.high} - {result_counts.warning} - {result_counts.info} - {result_counts.log} - {result_counts.false_positive} - {result_counts.total} - - - + {audit ? ( + {complianceCounts.yes} + ) : ( + {result_counts.high} + )} + {audit ? ( + {complianceCounts.no} + ) : ( + {result_counts.warning} + )} + {audit ? ( + {complianceCounts.incomplete} + ) : ( + {result_counts.info} + )} + {!audit && {result_counts.log}} + {!audit && {result_counts.false_positive}} + {audit ? ( + {complianceCounts.total} + ) : ( + {result_counts.total} + )} + {audit ? ( + + + + ) : ( + + + + )} ); }; Row.propTypes = { + audit: PropTypes.bool, entity: PropTypes.object.isRequired, links: PropTypes.bool, }; diff --git a/src/web/pages/reports/details/operatingsystemstab.jsx b/src/web/pages/reports/details/operatingsystemstab.jsx index 66dca4b897..86f04fa524 100644 --- a/src/web/pages/reports/details/operatingsystemstab.jsx +++ b/src/web/pages/reports/details/operatingsystemstab.jsx @@ -18,9 +18,11 @@ const operatingssystemsSortFunctions = { cpe: makeCompareString('id'), hosts: makeCompareNumber(entity => entity.hosts.count), severity: makeCompareNumber('severity', 0), + compliant: makeCompareString('compliance'), }; const OperatingSystemsTab = ({ + audit = false, counts, filter, operatingsystems, @@ -50,6 +52,7 @@ const OperatingSystemsTab = ({ onPreviousClick, }) => ( ( +const Header = ({ + audit = false, + currentSortDir, + currentSortBy, + sort = true, + onSortChange, +}) => ( ( onSortChange={onSortChange} title={_('Hosts')} /> - + {audit ? ( + + ) : ( + + )} ); Header.propTypes = { + audit: PropTypes.bool, currentSortBy: PropTypes.string, currentSortDir: PropTypes.string, sort: PropTypes.bool, onSortChange: PropTypes.func, }; -const Row = ({entity, links = true}) => { - const {name, cpe, hosts, severity} = entity; +const Row = ({audit = false, entity, links = true}) => { + const {name, cpe, hosts, severity, compliance} = entity; return ( @@ -86,14 +106,21 @@ const Row = ({entity, links = true}) => { {hosts.count} - - - + {audit && isDefined(compliance) ? ( + + + + ) : ( + + + + )} ); }; Row.propTypes = { + audit: PropTypes.bool, entity: PropTypes.object.isRequired, links: PropTypes.bool, }; diff --git a/src/web/pages/reports/details/resultstab.jsx b/src/web/pages/reports/details/resultstab.jsx index 37371e83b5..0acc2e8d24 100644 --- a/src/web/pages/reports/details/resultstab.jsx +++ b/src/web/pages/reports/details/resultstab.jsx @@ -148,6 +148,7 @@ class ResultsTab extends React.Component { render() { const {isUpdating, results, resultsCounts} = this.state; const { + audit = false, hasTarget, isLoading = true, progress, @@ -225,6 +226,7 @@ class ResultsTab extends React.Component { } return ( isActive(status) ? USE_DEFAULT_RELOAD_INTERVAL_ACTIVE : NO_RELOAD; // report doesn't change anymore. no need to reload -const loadInitial = ({ - reportFilter, - reportId, - resultsFilter, - // eslint-disable-next-line no-shadow - loadResults, - updateFilter, -}) => () => { - let newFilter = resultsFilter; - - if (isDefined(resultsFilter) && isDefined(reportFilter)) { - const simplifiedResultsFilter = resultsFilter - .copy() - .delete(resultsFilter.getSortOrder()) - .delete('first') - .delete('_and_report_id'); - const simplifiedReportFilter = reportFilter - .copy() - .delete(reportFilter.getSortOrder()) - .delete('first'); - - if (!simplifiedReportFilter.equals(simplifiedResultsFilter)) { - // report filter has changed +const loadInitial = + ({ + reportFilter, + reportId, + resultsFilter, + // eslint-disable-next-line no-shadow + loadResults, + updateFilter, + }) => + () => { + let newFilter = resultsFilter; + + if (isDefined(resultsFilter) && isDefined(reportFilter)) { + const simplifiedResultsFilter = resultsFilter + .copy() + .delete(resultsFilter.getSortOrder()) + .delete('first') + .delete('_and_report_id'); + const simplifiedReportFilter = reportFilter + .copy() + .delete(reportFilter.getSortOrder()) + .delete('first'); + + if (!simplifiedReportFilter.equals(simplifiedResultsFilter)) { + // report filter has changed + newFilter = reportFilter; + } + } else if (isDefined(resultsFilter)) { + newFilter = resultsFilter; + } else { newFilter = reportFilter; } - } else if (isDefined(resultsFilter)) { - newFilter = resultsFilter; - } else { - newFilter = reportFilter; - } - newFilter = filterWithReportId(newFilter, reportId); - updateFilter(newFilter); + newFilter = filterWithReportId(newFilter, reportId); + updateFilter(newFilter); - return loadResults(newFilter); -}; + return loadResults(newFilter); + }; -const load = ({ - reportFilter, - reportId, - resultsFilter, - // eslint-disable-next-line no-shadow - loadResults, - updateFilter, -}) => newFilter => { - if (!hasValue(newFilter)) { - newFilter = resultsFilter; - } +const load = + ({ + reportFilter, + reportId, + resultsFilter, + // eslint-disable-next-line no-shadow + loadResults, + updateFilter, + }) => + newFilter => { + if (!hasValue(newFilter)) { + newFilter = resultsFilter; + } - if (!hasValue(newFilter)) { - newFilter = reportFilter; - } + if (!hasValue(newFilter)) { + newFilter = reportFilter; + } - newFilter = filterWithReportId(newFilter, reportId); - updateFilter(newFilter); + newFilter = filterWithReportId(newFilter, reportId); + updateFilter(newFilter); - return loadResults(newFilter); -}; + return loadResults(newFilter); + }; const ResultsTabWrapper = props => ( { export default compose( withGmp, connect(mapStateToProps, mapDispatchToProps), -)(ResultsTabWrapper); - -// vim: set ts=2 sw=2 tw=80: +)(ResultsTabWrapper); \ No newline at end of file diff --git a/src/web/pages/reports/details/toolbaricons.jsx b/src/web/pages/reports/details/toolbaricons.jsx index 3caadaef46..7a61def7f5 100644 --- a/src/web/pages/reports/details/toolbaricons.jsx +++ b/src/web/pages/reports/details/toolbaricons.jsx @@ -33,6 +33,7 @@ import PropTypes from 'web/utils/proptypes'; import AlertActions from './alertactions'; const ToolBarIcons = ({ + audit = false, delta = false, filter, isLoading, @@ -56,7 +57,11 @@ const ToolBarIcons = ({ anchor="reading-a-report" title={_('Help: Reading Reports')} /> - + {audit ? ( + + ) : ( + + )} {!isLoading && ( @@ -86,13 +91,15 @@ const ToolBarIcons = ({ > - - - + {!audit && ( + + + + )} {!delta && ( { const result_hosts_only = filter.get('result_hosts_only'); const handleRemoveLevels = () => onFilterChange(filter.delete('levels')); + const handleRemoveCompliance = () => + onFilterChange(filter.delete('compliance_levels')); + return ( )} - + {!audit && ( + + )} - + {audit ? ( + + ) : ( + + )} - + {!audit && ( + + )} diff --git a/src/web/pages/results/__tests__/row.jsx b/src/web/pages/results/__tests__/row.jsx index e061adf30e..b8f6b103cf 100644 --- a/src/web/pages/results/__tests__/row.jsx +++ b/src/web/pages/results/__tests__/row.jsx @@ -142,3 +142,147 @@ describe('Delta reports V2 with same severity, qod and hostname', () => { expect(icons.length).toBe(0); }); }); + +describe('Audit reports with compliance', () => { + const {render} = rendererWith({gmp, store: true}); + + test('should render Audit report with compliance yes', () => { + const entity = Result.fromElement({ + _id: '101', + name: 'Result 1', + host: {__text: '123.456.78.910', hostname: 'foo'}, + port: '80/tcp', + severity: 10.0, + qod: {value: 80}, + notes: [], + overrides: [], + tickets: [], + compliance: 'yes', + }); + + const {getAllByTestId} = render( + + + + +
, + ); + const bars = getAllByTestId('progressbar-box'); + + expect(bars[0]).toHaveAttribute('title', 'Yes'); + expect(bars[0]).toHaveTextContent('Yes'); + }); + + test('should render Audit report with compliance no', () => { + const entity = Result.fromElement({ + _id: '101', + name: 'Result 1', + host: {__text: '123.456.78.910', hostname: 'foo'}, + port: '80/tcp', + severity: 10.0, + qod: {value: 80}, + notes: [], + overrides: [], + tickets: [], + compliance: 'no', + }); + + const {getAllByTestId} = render( + + + + +
, + ); + const bars = getAllByTestId('progressbar-box'); + expect(bars[0]).toHaveAttribute('title', 'No'); + expect(bars[0]).toHaveTextContent('No'); + }); + + test('should render Audit report with compliance incomplete', () => { + const entity = Result.fromElement({ + _id: '101', + name: 'Result 1', + host: {__text: '123.456.78.910', hostname: 'foo'}, + port: '80/tcp', + severity: 10.0, + qod: {value: 80}, + notes: [], + overrides: [], + tickets: [], + compliance: 'incomplete', + }); + + const {getAllByTestId} = render( + + + + +
, + ); + const bars = getAllByTestId('progressbar-box'); + expect(bars[0]).toHaveAttribute('title', 'Incomplete'); + expect(bars[0]).toHaveTextContent('Incomplete'); + }); + + test('should render Audit report with compliance undefined', () => { + const entity = Result.fromElement({ + _id: '101', + name: 'Result 1', + host: {__text: '123.456.78.910', hostname: 'foo'}, + port: '80/tcp', + severity: 10.0, + qod: {value: 80}, + notes: [], + overrides: [], + tickets: [], + compliance: 'undefined', + }); + + const {getAllByTestId} = render( + + + + +
, + ); + const bars = getAllByTestId('progressbar-box'); + expect(bars[0]).toHaveAttribute('title', 'Undefined'); + expect(bars[0]).toHaveTextContent('Undefined'); + }); + + test('Delta audit report with changed compliance', () => { + const entity = Result.fromElement({ + _id: '101', + name: 'Result 1', + host: {__text: '123.456.78.910', hostname: 'foo'}, + port: '80/tcp', + severity: 10.0, + qod: {value: 80}, + notes: [], + overrides: [], + tickets: [], + compliance: 'undefined', + delta: { + delta_type: 'changed', + result: { + compliance: 'yes', + }, + }, + }); + + const {getAllByTestId} = render( + + + + +
, + ); + const icons = getAllByTestId('svg-icon'); + expect(icons.length).toEqual(1); + expect(icons[0]).toHaveAttribute( + 'title', + 'Compliance is changed from yes.', + ); + }); +}); diff --git a/src/web/pages/results/row.jsx b/src/web/pages/results/row.jsx index addef1f093..18ee6ad6ed 100644 --- a/src/web/pages/results/row.jsx +++ b/src/web/pages/results/row.jsx @@ -13,6 +13,7 @@ import {isDefined, isNumber} from 'gmp/utils/identity'; import {shorten} from 'gmp/utils/string'; import SeverityBar from 'web/components/bar/severitybar'; +import ComplianceBar from 'web/components/bar/compliancebar'; import DateTime from 'web/components/date/datetime'; @@ -42,6 +43,7 @@ import useGmp from "web/hooks/useGmp"; const Row = ({ actionsComponent: ActionsComponent = EntitiesActions, + audit = false, delta = false, entity, links = true, @@ -59,6 +61,7 @@ const Row = ({ entity.overrides.filter(override => override.isActive()).length > 0; const hasTickets = entity.tickets.length > 0; const deltaSeverity = entity.delta?.result?.severity; + const deltaCompliance = entity.delta?.result?.compliance; const deltaHostname = entity.delta?.result?.host?.hostname; const deltaQoD = entity.delta?.result?.qod?.value; const epssScore = entity?.information?.epss?.max_severity?.score @@ -95,16 +98,30 @@ const Row = ({ )}
- - - {isDefined(deltaSeverity) && entity.severity !== deltaSeverity && ( - - )} - + {audit ? ( + + + {isDefined(deltaCompliance) && + entity.compliance !== deltaCompliance && ( + + )} + + ) : ( + + {} + {isDefined(deltaSeverity) && entity.severity !== deltaSeverity && ( + + )} + + )} @@ -165,6 +182,7 @@ const Row = ({ Row.propTypes = { actionsComponent: PropTypes.component, + audit: PropTypes.bool, delta: PropTypes.bool, entity: PropTypes.model.isRequired, links: PropTypes.bool, diff --git a/src/web/pages/results/table.jsx b/src/web/pages/results/table.jsx index 30e4344b04..ff936cba57 100644 --- a/src/web/pages/results/table.jsx +++ b/src/web/pages/results/table.jsx @@ -30,6 +30,7 @@ import useGmp from "web/hooks/useGmp"; const Header = ({ actionsColumn, + audit = false, delta = false, links = true, sort = true, @@ -72,15 +73,27 @@ const Header = ({ )} - + {audit ? ( + + ) : ( + + )} ( +}) => { + const caps = useCapabilities(); + const displayIds = [ + ...ALL_DISPLAYS, + ...(caps.featureEnabled('COMPLIANCE_REPORTS') + ? AUDIT_REPORTS_DISPLAYS + : []) + ]; + return ( -); +)}; StartDashboard.propTypes = { id: PropTypes.id.isRequired, diff --git a/src/web/pages/tags/component.jsx b/src/web/pages/tags/component.jsx index 870f8eaf47..fa868aaa99 100644 --- a/src/web/pages/tags/component.jsx +++ b/src/web/pages/tags/component.jsx @@ -29,6 +29,7 @@ export const MAX_RESOURCES = 40; // concerns listing in "Assigned Resources" tab const TYPES = [ 'alert', + 'audit', 'host', 'operatingsystem', 'cpe', @@ -42,6 +43,7 @@ const TYPES = [ 'nvt', 'override', 'permission', + 'policy', 'portlist', 'report', 'reportconfig', @@ -106,7 +108,13 @@ class TagComponent extends React.Component { getResourceTypes() { const {capabilities} = this.props; - return TYPES.map(type => + const types = [ + ...TYPES, + ...(capabilities.featureEnabled('COMPLIANCE_REPORTS') + ? ['auditreport'] + : []) + ].sort(); + return types.map(type => capabilities.mayAccess(type) ? [type, typeName(type)] : undefined, ).filter(isDefined); } diff --git a/src/web/pages/tags/dialog.jsx b/src/web/pages/tags/dialog.jsx index 7499f94073..003d738620 100644 --- a/src/web/pages/tags/dialog.jsx +++ b/src/web/pages/tags/dialog.jsx @@ -42,6 +42,7 @@ const ScrollableContent = styled.div` `; const types = { + auditreport: 'audit_report', operatingsystem: 'os', certbund: 'cert_bund_adv', dfncert: 'dfn_cert_adv', diff --git a/src/web/pages/tasks/status.jsx b/src/web/pages/tasks/status.jsx index 03a6778574..8fcf3cb9d8 100644 --- a/src/web/pages/tasks/status.jsx +++ b/src/web/pages/tasks/status.jsx @@ -23,7 +23,7 @@ const StyledDetailsLink = styled(DetailsLink)` } `; -const TaskStatus = ({task, links = true}) => { +const TaskStatus = ({task, links = true, isAudit = false}) => { let report_id; if (isDefined(task.current_report)) { report_id = task.current_report.id; @@ -35,16 +35,20 @@ const TaskStatus = ({task, links = true}) => { } return ( - + { }; TaskStatus.propTypes = { + isAudit: PropTypes.bool, links: PropTypes.bool, task: PropTypes.model.isRequired, }; diff --git a/src/web/pages/usersettings/dialog.jsx b/src/web/pages/usersettings/dialog.jsx index d458e546eb..eebb0c6ca2 100644 --- a/src/web/pages/usersettings/dialog.jsx +++ b/src/web/pages/usersettings/dialog.jsx @@ -69,6 +69,7 @@ let UserSettingsDialog = ({ defaultSchedule, defaultTarget, alertsFilter, + auditReportsFilter, configsFilter, credentialsFilter, filtersFilter, @@ -125,6 +126,7 @@ let UserSettingsDialog = ({ defaultSchedule, defaultTarget, alertsFilter, + auditReportsFilter, configsFilter, credentialsFilter, filtersFilter, @@ -157,11 +159,14 @@ let UserSettingsDialog = ({ const [error, setError] = useState(); const [formValues, handleValueChange] = useFormValues(settings); - const handleSave = useCallback(values => { - onSave(values).catch(err => { - setError(err.message); - }) - }, [onSave]); + const handleSave = useCallback( + values => { + onSave(values).catch(err => { + setError(err.message); + }); + }, + [onSave], + ); const {hasError, errors, validate} = useFormValidation( userSettingsRules, @@ -243,6 +248,7 @@ let UserSettingsDialog = ({ filters.filter(filter => filter.filter_type === type); const FilterPart = ({ alertsFilter, + auditReportsFilter, configsFilter, credentialsFilter, filtersFilter, @@ -51,6 +52,7 @@ const FilterPart = ({ filters = [], onChange, }) => { + const caps = useCapabilities(); return ( @@ -64,6 +66,20 @@ const FilterPart = ({ onChange={onChange} /> + {caps.featureEnabled('COMPLIANCE_REPORTS') && ( + + + {capabilities.featureEnabled('COMPLIANCE_REPORTS') && ( + + ) + } { const defaultSchedule = schedulesSel.getEntity(defaultScheduleId); const defaultTarget = targetsSel.getEntity(defaultTargetId); const alertsFilter = userDefaultFilterSelector.getFilter('alert'); + const auditReportsFilter = userDefaultFilterSelector.getFilter('auditreport'); const configsFilter = userDefaultFilterSelector.getFilter('scanconfig'); const credentialsFilter = userDefaultFilterSelector.getFilter('credential'); const filtersFilter = userDefaultFilterSelector.getFilter('filter'); @@ -1006,6 +1022,7 @@ const mapStateToProps = rootState => { defaultSchedule, defaultTarget, alertsFilter, + auditReportsFilter, configsFilter, credentialsFilter, filtersFilter, @@ -1046,6 +1063,7 @@ const mapDispatchToProps = (dispatch, {gmp}) => ({ loadFilterDefaults: () => Promise.all([ dispatch(loadUserSettingsDefaultFilter(gmp)('alert')), + dispatch(loadUserSettingsDefaultFilter(gmp)('auditreport')), dispatch(loadUserSettingsDefaultFilter(gmp)('scanconfig')), dispatch(loadUserSettingsDefaultFilter(gmp)('credential')), dispatch(loadUserSettingsDefaultFilter(gmp)('filter')), diff --git a/src/web/routes.jsx b/src/web/routes.jsx index 63d0825356..80b99c7fad 100644 --- a/src/web/routes.jsx +++ b/src/web/routes.jsx @@ -11,6 +11,7 @@ import {createBrowserHistory} from 'history'; import {stringify, parse} from 'qs'; import qhistory from 'qhistory'; +import ConditionalRoute from 'web/components/conditionalRoute/ConditionalRoute'; import LocationObserver from 'web/components/observer/locationobserver'; import SessionObserver from 'web/components/observer/sessionobserver'; @@ -23,6 +24,8 @@ import AboutPage from './pages/help/about'; import AlertsPage from './pages/alerts/listpage'; import AlertDetailsPage from './pages/alerts/detailspage'; import AuditsPage from './pages/audits/listpage'; +import AuditReportDetailsPage from './pages/reports/auditdetailspage'; +import AuditReportsPage from './pages/reports/auditreportslistpage'; import AuditsDetailsPage from './pages/audits/detailspage'; import CertBundsPage from './pages/certbund/listpage'; import CertBundDetailsPage from './pages/certbund/detailspage'; @@ -66,6 +69,7 @@ import ReportFormatsPage from './pages/reportformats/listpage'; import ReportFormatDetailsPage from './pages/reportformats/detailspage'; import ReportsPage from './pages/reports/listpage'; import ReportDetailsPage from './pages/reports/detailspage'; +import DeltaAuditReportDetailsPage from './pages/reports/auditdeltadetailspage'; import DeltaReportDetailsPage from './pages/reports/deltadetailspage'; import ResultsPage from './pages/results/listpage'; import ResultDetailsPage from './pages/results/detailspage'; @@ -121,6 +125,18 @@ const Routes = () => ( + + + diff --git a/src/web/store/entities/__tests__/reducers.js b/src/web/store/entities/__tests__/reducers.js index 71e7a11e04..e9112b0c86 100644 --- a/src/web/store/entities/__tests__/reducers.js +++ b/src/web/store/entities/__tests__/reducers.js @@ -24,10 +24,12 @@ describe('entities reducer tests', () => { expect(entitiesReducer(undefined, {})).toEqual({ alert: initState, audit: initState, + auditreport: initState, certbund: initState, cpe: initState, credential: initState, cve: initState, + deltaAuditReport: initState, deltaReport: initState, dfncert: initState, filter: initState, diff --git a/src/web/store/entities/auditreports.js b/src/web/store/entities/auditreports.js new file mode 100644 index 0000000000..95febbe2e3 --- /dev/null +++ b/src/web/store/entities/auditreports.js @@ -0,0 +1,62 @@ +/* SPDX-FileCopyrightText: 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { + createEntitiesLoadingActions, + createLoadAllEntities, + createLoadEntities, + types, +} from 'web/store/entities/utils/actions'; + +import {createReducer, initialState} from 'web/store/entities/utils/reducers'; +import {createEntitiesSelector} from 'web/store/entities/utils/selectors'; + +import {reportReducer} from './report/reducers'; +import {reportsReducer} from './reports/reducers'; + +const reportsSelector = createEntitiesSelector('auditreport'); +const entitiesActions = createEntitiesLoadingActions('auditreport'); +const loadAllEntities = createLoadAllEntities({ + selector: reportsSelector, + actions: entitiesActions, + entityType: 'auditreport', +}); +const loadEntities = createLoadEntities({ + selector: reportsSelector, + actions: entitiesActions, + entityType: 'auditreport', +}); + +const reducer = (state = initialState, action) => { + if (action.entityType !== 'auditreport') { + return state; + } + + switch (action.type) { + case types.ENTITIES_LOADING_REQUEST: + case types.ENTITIES_LOADING_SUCCESS: + case types.ENTITIES_LOADING_ERROR: + return reportsReducer(state, action); + + case types.ENTITY_LOADING_REQUEST: + case types.ENTITY_LOADING_SUCCESS: + case types.ENTITY_LOADING_ERROR: + return reportReducer(state, action); + + default: + return state; + } +}; + +const deltaAuditReducer = createReducer('deltaAuditReport'); + +export { + deltaAuditReducer, + loadAllEntities, + loadEntities, + reducer, + reportsSelector as selector, + entitiesActions, +}; diff --git a/src/web/store/entities/reducers.js b/src/web/store/entities/reducers.js index d12f11332f..7ec854db10 100644 --- a/src/web/store/entities/reducers.js +++ b/src/web/store/entities/reducers.js @@ -7,6 +7,10 @@ import {combineReducers} from 'redux'; import {reducer as alert} from './alerts'; import {reducer as audit} from './audits'; +import { + reducer as auditreport, + deltaAuditReducer as deltaAuditReport, +} from './auditreports'; import {reducer as certbund} from './certbund'; import {reducer as cpe} from './cpes'; import {reducer as credential} from './credentials'; @@ -41,10 +45,12 @@ import {reducer as vuln} from './vulns'; const entitiesReducer = combineReducers({ alert, audit, + auditreport, certbund, cpe, credential, cve, + deltaAuditReport, deltaReport, dfncert, filter, diff --git a/src/web/store/entities/report/__tests__/actions.js b/src/web/store/entities/report/__tests__/actions.js index 5712358259..ca9bb8c2e5 100644 --- a/src/web/store/entities/report/__tests__/actions.js +++ b/src/web/store/entities/report/__tests__/actions.js @@ -14,18 +14,24 @@ import {types} from 'web/store/entities/utils/actions'; import {createState, testEntityActions} from 'web/store/entities/utils/testing'; import { + auditReportActions, deltaReportActions, loadReport, + loadAuditReport, + loadAuditReportIfNeeded, + loadAuditReportWithThreshold, + loadDeltaReport, + loadDeltaAuditReport, loadReportIfNeeded, loadReportWithThreshold, reportActions, - loadDeltaReport, } from '../actions'; import {reportIdentifier} from '../selectors'; testEntityActions('report', reportActions); testEntityActions('deltaReport', deltaReportActions); +testEntityActions('auditreport', auditReportActions); describe('loadReport function tests', () => { test('should load report successfully', () => { @@ -936,3 +942,936 @@ describe('loadDeltaReport function tests', () => { }); }); }); + +describe('loadAuditReport function tests', () => { + test('should load audit report successfully', () => { + const id = 'a1'; + const rootState = createState('auditreport', { + isLoading: { + [id]: false, + }, + }); + const getState = testing.fn().mockReturnValue(rootState); + + const dispatch = testing.fn(); + + const get = testing.fn().mockResolvedValue({ + data: {foo: 'bar'}, + }); + + const gmp = { + auditreport: { + get, + }, + }; + + expect(loadAuditReport).toBeDefined(); + expect(isFunction(loadAuditReport)).toBe(true); + + expect.assertions(7); + + return loadAuditReport(gmp)(id)(dispatch, getState).then(() => { + expect(getState).toBeCalled(); + expect(get).toBeCalledWith({id}, {details: true, filter: undefined}); + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenNthCalledWith(1, { + type: types.ENTITY_LOADING_REQUEST, + entityType: 'auditreport', + id, + }); + expect(dispatch).toHaveBeenNthCalledWith(2, { + type: types.ENTITY_LOADING_SUCCESS, + entityType: 'auditreport', + data: {foo: 'bar'}, + id, + }); + }); + }); + + test('should load audit report with results filter successfully', () => { + const id = 'a1'; + const rootState = createState('auditreport', { + isLoading: { + [id]: false, + }, + }); + const getState = testing.fn().mockReturnValue(rootState); + + const dispatch = testing.fn(); + + const get = testing.fn().mockResolvedValue({ + data: {foo: 'bar'}, + }); + + const gmp = { + auditreport: { + get, + }, + }; + + const filter = Filter.fromString('foo=bar'); + + expect(loadAuditReport).toBeDefined(); + expect(isFunction(loadAuditReport)).toBe(true); + + expect.assertions(7); + + return loadAuditReport(gmp)(id, {filter})(dispatch, getState).then(() => { + expect(getState).toBeCalled(); + expect(get).toBeCalledWith({id}, {details: true, filter}); + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenNthCalledWith(1, { + type: types.ENTITY_LOADING_REQUEST, + entityType: 'auditreport', + filter, + id, + }); + expect(dispatch).toHaveBeenNthCalledWith(2, { + type: types.ENTITY_LOADING_SUCCESS, + entityType: 'auditreport', + filter, + data: {foo: 'bar'}, + id, + }); + }); + }); + + test('should not load audit report if isLoading is true', () => { + const id = 'a1'; + const rootState = createState('auditreport', { + isLoading: { + [id]: true, + }, + }); + + const getState = testing.fn().mockReturnValue(rootState); + + const dispatch = testing.fn(); + + const get = testing.fn().mockResolvedValue([{id: 'foo'}]); + + const gmp = { + auditreport: { + get, + }, + }; + + expect.assertions(3); + + return loadAuditReport(gmp)(id)(dispatch, getState).then(() => { + expect(getState).toBeCalled(); + expect(dispatch).not.toBeCalled(); + expect(get).not.toBeCalled(); + }); + }); + + test('should fail loading audit report with an error', () => { + const id = 'a1'; + const rootState = createState('auditreport', { + [id]: { + isLoading: false, + }, + }); + + const getState = testing.fn().mockReturnValue(rootState); + + const dispatch = testing.fn(); + + const get = testing.fn().mockRejectedValue('An Error'); + + const gmp = { + auditreport: { + get, + }, + }; + + expect.assertions(5); + + return loadAuditReport(gmp)(id)(dispatch, getState).catch(() => { + expect(getState).toBeCalled(); + expect(get).toBeCalledWith({id}, {details: true, filter: undefined}); + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenNthCalledWith(1, { + type: types.ENTITY_LOADING_REQUEST, + entityType: 'auditreport', + id, + }); + expect(dispatch).toHaveBeenNthCalledWith(2, { + type: types.ENTITY_LOADING_ERROR, + entityType: 'auditreport', + error: 'An Error', + id, + }); + }); + }); +}); + +describe('report loadAuditReportIfNeeded function tests', () => { + test('should load audit report successfully if needed', () => { + const id = 'a1'; + const rootState = createState('auditreport', { + isLoading: { + [id]: false, + }, + }); + const getState = testing.fn().mockReturnValue(rootState); + + const dispatch = testing.fn(); + + const get = testing.fn().mockResolvedValue({data: {foo: 'bar'}}); + + const gmp = { + auditreport: { + get, + }, + }; + + expect.assertions(7); + + expect(loadAuditReportIfNeeded).toBeDefined(); + expect(isFunction(loadAuditReportIfNeeded)).toBe(true); + + return loadAuditReportIfNeeded(gmp)(id)(dispatch, getState).then(() => { + expect(getState).toBeCalled(); + expect(get).toBeCalledWith({id}, {details: false, filter: undefined}); + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenNthCalledWith(1, { + type: types.ENTITY_LOADING_REQUEST, + entityType: 'auditreport', + id, + }); + expect(dispatch).toHaveBeenNthCalledWith(2, { + type: types.ENTITY_LOADING_SUCCESS, + entityType: 'auditreport', + data: {foo: 'bar'}, + id, + }); + }); + }); + + test('should not load audit report if report is already in store', () => { + const id = 'a1'; + const rootState = createState('auditreport', { + isLoading: { + [id]: false, + }, + byId: { + [id]: 'a1', + }, + }); + + const getState = testing.fn().mockReturnValue(rootState); + + const dispatch = testing.fn(); + + const get = testing.fn().mockResolvedValue([{id: 'foo'}]); + + const gmp = { + auditreport: { + get, + }, + }; + + expect.assertions(3); + + return loadAuditReportIfNeeded(gmp)(id)(dispatch, getState).then(() => { + expect(getState).toBeCalled(); + expect(dispatch).not.toBeCalled(); + expect(get).not.toBeCalled(); + }); + }); + + test('should load audit report with results filter successfully if needed', () => { + const id = 'a1'; + const rootState = createState('auditreport', { + isLoading: { + [id]: false, + }, + }); + const getState = testing.fn().mockReturnValue(rootState); + + const dispatch = testing.fn(); + + const get = testing.fn().mockResolvedValue({data: {foo: 'bar'}}); + + const gmp = { + auditreport: { + get, + }, + }; + + const filter = Filter.fromString('foo=bar'); + + expect.assertions(7); + + expect(loadAuditReportIfNeeded).toBeDefined(); + expect(isFunction(loadAuditReportIfNeeded)).toBe(true); + + return loadAuditReportIfNeeded(gmp)(id, {filter})(dispatch, getState).then( + () => { + expect(getState).toBeCalled(); + expect(get).toBeCalledWith({id}, {details: false, filter}); + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenNthCalledWith(1, { + type: types.ENTITY_LOADING_REQUEST, + entityType: 'auditreport', + filter, + id, + }); + expect(dispatch).toHaveBeenNthCalledWith(2, { + type: types.ENTITY_LOADING_SUCCESS, + entityType: 'auditreport', + filter, + data: {foo: 'bar'}, + id, + }); + }, + ); + }); + + test('should not audit load report if isLoading is true', () => { + const id = 'a1'; + const rootState = createState('auditreport', { + isLoading: { + [id]: true, + }, + }); + + const getState = testing.fn().mockReturnValue(rootState); + + const dispatch = testing.fn(); + + const get = testing.fn().mockResolvedValue([{id: 'foo'}]); + + const gmp = { + auditreport: { + get, + }, + }; + + expect.assertions(3); + + return loadAuditReportIfNeeded(gmp)(id)(dispatch, getState).then(() => { + expect(getState).toBeCalled(); + expect(dispatch).not.toBeCalled(); + expect(get).not.toBeCalled(); + }); + }); + + test('should fail loading audit report with an error', () => { + const id = 'a1'; + const rootState = createState('auditreport', { + [id]: { + isLoading: false, + }, + }); + + const getState = testing.fn().mockReturnValue(rootState); + + const dispatch = testing.fn(); + + const get = testing.fn().mockRejectedValue('An Error'); + + const gmp = { + auditreport: { + get, + }, + }; + + expect.assertions(5); + + return loadAuditReportIfNeeded(gmp)(id)(dispatch, getState).catch(() => { + expect(getState).toBeCalled(); + expect(get).toBeCalledWith({id}, {details: false, filter: undefined}); + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch.mock.calls[0]).toEqual([ + { + type: types.ENTITY_LOADING_REQUEST, + entityType: 'auditreport', + id, + }, + ]); + expect(dispatch.mock.calls[1]).toEqual([ + { + type: types.ENTITY_LOADING_ERROR, + entityType: 'auditreport', + error: 'An Error', + id, + }, + ]); + }); + }); +}); + +describe('loadAuditReportWithThreshold tests', () => { + test('should only load "simple" audit report', () => { + const id = 'a1'; + const rootState = createState('auditreport', { + isLoading: { + [id]: false, + }, + }); + const getState = testing.fn().mockReturnValue(rootState); + + const dispatch = testing.fn(); + + const auditreport = { + report: { + results: { + counts: { + filtered: 10000, + }, + }, + }, + }; + + const get = testing.fn().mockResolvedValue({ + data: auditreport, + }); + + const gmp = { + auditreport: { + get, + }, + settings: { + reportResultsThreshold: 1000, + }, + }; + + expect(loadAuditReportWithThreshold).toBeDefined(); + expect(isFunction(loadAuditReportWithThreshold)).toBe(true); + + expect.assertions(7); + + return loadAuditReportWithThreshold(gmp)(id)(dispatch, getState).then( + () => { + expect(getState).toBeCalled(); + expect(get).toBeCalledWith({id}, {details: false, filter: undefined}); + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenNthCalledWith(1, { + type: types.ENTITY_LOADING_REQUEST, + entityType: 'auditreport', + id, + }); + expect(dispatch).toHaveBeenNthCalledWith(2, { + type: types.ENTITY_LOADING_SUCCESS, + entityType: 'auditreport', + data: auditreport, + id, + }); + }, + ); + }); + + test('should load "full" audit report', () => { + const id = 'a1'; + const rootState = createState('auditreport', { + isLoading: { + [id]: false, + }, + }); + const getState = testing.fn().mockReturnValue(rootState); + + const dispatch = testing.fn(); + + const auditreport = { + report: { + results: { + counts: { + filtered: 10000, + }, + }, + }, + }; + + const get = testing.fn().mockResolvedValue({ + data: auditreport, + }); + + const gmp = { + auditreport: { + get, + }, + settings: { + reportResultsThreshold: 100001, + }, + }; + + expect(loadAuditReportWithThreshold).toBeDefined(); + expect(isFunction(loadAuditReportWithThreshold)).toBe(true); + + expect.assertions(11); + + return loadAuditReportWithThreshold(gmp)(id)(dispatch, getState).then( + () => { + expect(getState).toBeCalled(); + expect(get).toHaveBeenCalledTimes(2); + expect(get).toHaveBeenNthCalledWith( + 1, + {id}, + {details: false, filter: undefined}, + ); + expect(get).toHaveBeenNthCalledWith( + 2, + {id}, + {details: true, filter: undefined}, + ); + expect(dispatch).toHaveBeenCalledTimes(4); + expect(dispatch).toHaveBeenNthCalledWith(1, { + type: types.ENTITY_LOADING_REQUEST, + entityType: 'auditreport', + id, + }); + expect(dispatch).toHaveBeenNthCalledWith(2, { + type: types.ENTITY_LOADING_SUCCESS, + entityType: 'auditreport', + data: auditreport, + id, + }); + expect(dispatch).toHaveBeenNthCalledWith(3, { + type: types.ENTITY_LOADING_REQUEST, + entityType: 'auditreport', + id, + }); + expect(dispatch).toHaveBeenNthCalledWith(4, { + type: types.ENTITY_LOADING_SUCCESS, + entityType: 'auditreport', + data: auditreport, + id, + }); + }, + ); + }); + + test('should only load "simple" audit report with filter', () => { + const id = 'a1'; + const filter = Filter.fromString('foo=bar rows=10'); + const rootState = createState('auditreport', { + isLoading: { + [reportIdentifier(id, filter)]: false, + }, + }); + const getState = testing.fn().mockReturnValue(rootState); + + const dispatch = testing.fn(); + + const auditreport = { + report: { + results: { + counts: { + filtered: 10000, + }, + }, + }, + }; + + const get = testing.fn().mockResolvedValue({ + data: auditreport, + }); + + const gmp = { + auditreport: { + get, + }, + settings: { + reportResultsThreshold: 1000, + }, + }; + + expect(loadAuditReportWithThreshold).toBeDefined(); + expect(isFunction(loadAuditReportWithThreshold)).toBe(true); + + expect.assertions(7); + + return loadAuditReportWithThreshold(gmp)(id, {filter})( + dispatch, + getState, + ).then(() => { + expect(getState).toBeCalled(); + expect(get).toBeCalledWith({id}, {details: false, filter}); + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch).toHaveBeenNthCalledWith(1, { + type: types.ENTITY_LOADING_REQUEST, + entityType: 'auditreport', + filter, + id, + }); + expect(dispatch).toHaveBeenNthCalledWith(2, { + type: types.ENTITY_LOADING_SUCCESS, + entityType: 'auditreport', + filter, + data: auditreport, + id, + }); + }); + }); + + test('should load "full" audit report with filter', () => { + const id = 'a1'; + const filter = Filter.fromString('foo=bar rows=10'); + const rootState = createState('auditreport', { + isLoading: { + [reportIdentifier(id, filter)]: false, + }, + }); + const getState = testing.fn().mockReturnValue(rootState); + + const dispatch = testing.fn(); + + const auditreport = { + report: { + results: { + counts: { + filtered: 10000, + }, + }, + }, + }; + + const get = testing.fn().mockResolvedValue({ + data: auditreport, + }); + + const gmp = { + auditreport: { + get, + }, + settings: { + reportResultsThreshold: 100001, + }, + }; + + expect(loadAuditReportWithThreshold).toBeDefined(); + expect(isFunction(loadAuditReportWithThreshold)).toBe(true); + + expect.assertions(11); + + return loadAuditReportWithThreshold(gmp)(id, {filter})( + dispatch, + getState, + ).then(() => { + expect(getState).toBeCalled(); + expect(get).toHaveBeenCalledTimes(2); + expect(get).toHaveBeenNthCalledWith(1, {id}, {details: false, filter}); + expect(get).toHaveBeenNthCalledWith(2, {id}, {details: true, filter}); + expect(dispatch).toHaveBeenCalledTimes(4); + expect(dispatch).toHaveBeenNthCalledWith(1, { + type: types.ENTITY_LOADING_REQUEST, + entityType: 'auditreport', + filter, + id, + }); + expect(dispatch).toHaveBeenNthCalledWith(2, { + type: types.ENTITY_LOADING_SUCCESS, + entityType: 'auditreport', + filter, + data: auditreport, + id, + }); + expect(dispatch).toHaveBeenNthCalledWith(3, { + type: types.ENTITY_LOADING_REQUEST, + entityType: 'auditreport', + filter, + id, + }); + expect(dispatch).toHaveBeenNthCalledWith(4, { + type: types.ENTITY_LOADING_SUCCESS, + entityType: 'auditreport', + filter, + data: auditreport, + id, + }); + }); + }); + + test('should not load audit report if already loading', () => { + const id = 'a1'; + const rootState = createState('auditreport', { + isLoading: { + [id]: true, + }, + }); + const getState = testing.fn().mockReturnValue(rootState); + + const dispatch = testing.fn(); + + const auditreport = { + report: { + results: { + counts: { + filtered: 10000, + }, + }, + }, + }; + + const get = testing.fn().mockResolvedValue({ + data: auditreport, + }); + + const gmp = { + auditreport: { + get, + }, + settings: { + reportResultsThreshold: 1000, + }, + }; + + expect(loadAuditReportWithThreshold).toBeDefined(); + expect(isFunction(loadAuditReportWithThreshold)).toBe(true); + + expect.assertions(5); + + return loadAuditReportWithThreshold(gmp)(id)(dispatch, getState).then( + () => { + expect(getState).toBeCalled(); + expect(get).not.toHaveBeenCalled(); + expect(dispatch).not.toHaveBeenCalled(); + }, + ); + }); + + test('should not audit load report if already loading with filter', () => { + const id = 'a1'; + const filter = Filter.fromString('foo=bar rows=10'); + const rootState = createState('auditreport', { + isLoading: { + [reportIdentifier(id, filter)]: true, + }, + }); + const getState = testing.fn().mockReturnValue(rootState); + + const dispatch = testing.fn(); + + const auditreport = { + report: { + results: { + counts: { + filtered: 10000, + }, + }, + }, + }; + + const get = testing.fn().mockResolvedValue({ + data: auditreport, + }); + + const gmp = { + auditreport: { + get, + }, + settings: { + reportResultsThreshold: 1000, + }, + }; + + expect(loadAuditReportWithThreshold).toBeDefined(); + expect(isFunction(loadAuditReportWithThreshold)).toBe(true); + + expect.assertions(5); + + return loadAuditReportWithThreshold(gmp)(id, {filter})( + dispatch, + getState, + ).then(() => { + expect(getState).toBeCalled(); + expect(get).not.toHaveBeenCalled(); + expect(dispatch).not.toHaveBeenCalled(); + }); + }); +}); + +describe('loadDeltaAuditReport function tests', () => { + test('should load delta audit report successfully', () => { + const id = 'a1'; + const deltaId = 'a2'; + const identifier = `${id}+${deltaId}`; + + const rootState = createState('deltaAuditReport', { + isLoading: { + [identifier]: false, + }, + }); + const getState = testing.fn().mockReturnValue(rootState); + + const dispatch = testing.fn(); + + const getDelta = testing.fn().mockResolvedValue({ + data: {foo: 'bar'}, + }); + + const gmp = { + auditreport: { + getDelta, + }, + }; + + expect(loadDeltaAuditReport).toBeDefined(); + expect(isFunction(loadDeltaAuditReport)).toBe(true); + + return loadDeltaAuditReport(gmp)(id, deltaId)(dispatch, getState).then( + () => { + expect(getState).toBeCalled(); + expect(getDelta).toBeCalledWith( + {id}, + {id: deltaId}, + {filter: undefined}, + ); + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch.mock.calls[0]).toEqual([ + { + type: types.ENTITY_LOADING_REQUEST, + entityType: 'deltaAuditReport', + id: identifier, + }, + ]); + expect(dispatch.mock.calls[1]).toEqual([ + { + type: types.ENTITY_LOADING_SUCCESS, + entityType: 'deltaAuditReport', + data: {foo: 'bar'}, + id: identifier, + }, + ]); + }, + ); + }); + + test('should load delta audit report with results filter successfully', () => { + const id = 'a1'; + const deltaId = 'a2'; + const identifier = `${id}+${deltaId}`; + + const rootState = createState('deltaAuditReport', { + isLoading: { + [identifier]: false, + }, + }); + const getState = testing.fn().mockReturnValue(rootState); + + const dispatch = testing.fn(); + + const getDelta = testing.fn().mockResolvedValue({ + data: {foo: 'bar'}, + }); + + const gmp = { + auditreport: { + getDelta, + }, + }; + + const filter = Filter.fromString('foo=bar'); + + expect(loadDeltaAuditReport).toBeDefined(); + expect(isFunction(loadDeltaReport)).toBe(true); + + return loadDeltaAuditReport(gmp)( + id, + deltaId, + filter, + )(dispatch, getState).then(() => { + expect(getState).toBeCalled(); + expect(getDelta).toBeCalledWith({id}, {id: deltaId}, {filter}); + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch.mock.calls[0]).toEqual([ + { + type: types.ENTITY_LOADING_REQUEST, + entityType: 'deltaAuditReport', + id: identifier, + }, + ]); + expect(dispatch.mock.calls[1]).toEqual([ + { + type: types.ENTITY_LOADING_SUCCESS, + entityType: 'deltaAuditReport', + data: {foo: 'bar'}, + id: identifier, + }, + ]); + }); + }); + + test('should not load audit delta report if isLoading is true', () => { + const id = 'a1'; + const deltaId = 'a2'; + const identifier = `${id}+${deltaId}`; + const rootState = createState('deltaAuditReport', { + isLoading: { + [identifier]: true, + }, + }); + + const getState = testing.fn().mockReturnValue(rootState); + + const dispatch = testing.fn(); + + const getDelta = testing.fn().mockResolvedValue([{id: 'foo'}]); + + const gmp = { + auditreport: { + getDelta, + }, + }; + + return loadDeltaAuditReport(gmp)(id, deltaId)(dispatch, getState).then( + () => { + expect(getState).toBeCalled(); + expect(dispatch).not.toBeCalled(); + expect(getDelta).not.toBeCalled(); + }, + ); + }); + + test('should fail loading audit delta report with an error', () => { + const id = 'a1'; + const deltaId = 'a2'; + const identifier = `${id}+${deltaId}`; + const rootState = createState('deltaAuditReport', { + [identifier]: { + isLoading: false, + }, + }); + + const getState = testing.fn().mockReturnValue(rootState); + + const dispatch = testing.fn(); + + const getDelta = testing.fn().mockRejectedValue('An Error'); + + const gmp = { + auditreport: { + getDelta, + }, + }; + + return loadDeltaAuditReport(gmp)(id, deltaId)(dispatch, getState).then( + () => { + expect(getState).toBeCalled(); + expect(getDelta).toBeCalledWith( + {id}, + {id: deltaId}, + {filter: undefined}, + ); + expect(dispatch).toHaveBeenCalledTimes(2); + expect(dispatch.mock.calls[0]).toEqual([ + { + type: types.ENTITY_LOADING_REQUEST, + entityType: 'deltaAuditReport', + id: identifier, + }, + ]); + expect(dispatch.mock.calls[1]).toEqual([ + { + type: types.ENTITY_LOADING_ERROR, + entityType: 'deltaAuditReport', + error: 'An Error', + id: identifier, + }, + ]); + }, + ); + }); +}); diff --git a/src/web/store/entities/report/actions.js b/src/web/store/entities/report/actions.js index 264f97e7b4..a667af5cb4 100644 --- a/src/web/store/entities/report/actions.js +++ b/src/web/store/entities/report/actions.js @@ -11,146 +11,270 @@ import { } from 'web/store/entities/utils/actions'; import { + auditReportSelector, reportSelector, deltaReportSelector, + deltaAuditReportSelector, deltaReportIdentifier, } from './selectors'; -const entityType = 'report'; - export const reportActions = { request: (id, filter) => ({ type: types.ENTITY_LOADING_REQUEST, - entityType, + entityType: 'report', filter, id, }), success: (id, data, filter) => ({ type: types.ENTITY_LOADING_SUCCESS, - entityType, + entityType: 'report', data, filter, id, }), error: (id, error, filter) => ({ type: types.ENTITY_LOADING_ERROR, - entityType, + entityType: 'report', error, filter, id, }), }; -export const loadReport = gmp => ( - id, - {filter, details = true, force = false} = {}, -) => (dispatch, getState) => { - const rootState = getState(); - const state = reportSelector(rootState); - - if (!force && state.isLoadingEntity(id, filter)) { - // we are already loading data - return Promise.resolve(); - } - - dispatch(reportActions.request(id, filter)); - - return gmp.report - .get({id}, {filter, details}) - .then( - response => response.data, - error => { - dispatch(reportActions.error(id, error, filter)); - return Promise.reject(error); - }, - ) - .then(data => { - dispatch(reportActions.success(id, data, filter)); - return data; - }); +export const auditReportActions = { + request: (id, filter) => ({ + type: types.ENTITY_LOADING_REQUEST, + entityType: 'auditreport', + filter, + id, + }), + success: (id, data, filter) => ({ + type: types.ENTITY_LOADING_SUCCESS, + entityType: 'auditreport', + data, + filter, + id, + }), + error: (id, error, filter) => ({ + type: types.ENTITY_LOADING_ERROR, + entityType: 'auditreport', + error, + filter, + id, + }), }; -export const loadReportWithThreshold = gmp => (id, {filter} = {}) => ( - dispatch, - getState, -) => { - const rootState = getState(); - const state = reportSelector(rootState); - - if (state.isLoadingEntity(id, filter)) { - // we are already loading data - return Promise.resolve(); - } - - dispatch(reportActions.request(id, filter)); - - const {reportResultsThreshold: threshold} = gmp.settings; - return gmp.report - .get({id}, {filter, details: false}) - .then( - response => response.data, - error => { - dispatch(reportActions.error(id, error, filter)); - return Promise.reject(error); - }, - ) - .then(report => { - const fullReport = - isDefined(report) && - isDefined(report.report) && - isDefined(report.report.results) && - report.report.results.counts.filtered < threshold; - - dispatch(reportActions.success(id, report, filter)); - - if (fullReport) { - return loadReport(gmp)(id, {filter, details: true, force: true})( - dispatch, - getState, - ); - } - }); -}; +export const loadReport = + gmp => + (id, {filter, details = true, force = false} = {}) => + (dispatch, getState) => { + const rootState = getState(); + const state = reportSelector(rootState); -export const loadReportIfNeeded = gmp => ( - id, - {filter, details = false} = {}, -) => (dispatch, getState) => { - // loads the small report (without details) if these information are not - // yet in the store. resolve() otherwise - const rootState = getState(); - const state = reportSelector(rootState); - - if (isDefined(state.getEntity(id, filter))) { - // we are already loading data or have it in the store - return Promise.resolve(); - } - return loadReport(gmp)(id, {filter, details})(dispatch, getState); -}; + if (!force && state.isLoadingEntity(id, filter)) { + // we are already loading data + return Promise.resolve(); + } + + dispatch(reportActions.request(id, filter)); + + return gmp.report + .get({id}, {filter, details}) + .then( + response => response.data, + error => { + dispatch(reportActions.error(id, error, filter)); + return Promise.reject(error); + }, + ) + .then(data => { + dispatch(reportActions.success(id, data, filter)); + return data; + }); + }; + +export const loadReportWithThreshold = + gmp => + (id, {filter} = {}) => + (dispatch, getState) => { + const rootState = getState(); + const state = reportSelector(rootState); + + if (state.isLoadingEntity(id, filter)) { + // we are already loading data + return Promise.resolve(); + } + + dispatch(reportActions.request(id, filter)); + + const {reportResultsThreshold: threshold} = gmp.settings; + return gmp.report + .get({id}, {filter, details: false}) + .then( + response => response.data, + error => { + dispatch(reportActions.error(id, error, filter)); + return Promise.reject(error); + }, + ) + .then(report => { + const fullReport = + isDefined(report) && + isDefined(report.report) && + isDefined(report.report.results) && + report.report.results.counts.filtered < threshold; + + dispatch(reportActions.success(id, report, filter)); + + if (fullReport) { + return loadReport(gmp)(id, {filter, details: true, force: true})( + dispatch, + getState, + ); + } + }); + }; + +export const loadReportIfNeeded = + gmp => + (id, {filter, details = false} = {}) => + (dispatch, getState) => { + // loads the small report (without details) if these information are not + // yet in the store. resolve() otherwise + const rootState = getState(); + const state = reportSelector(rootState); + + if (isDefined(state.getEntity(id, filter))) { + // we are already loading data or have it in the store + return Promise.resolve(); + } + return loadReport(gmp)(id, {filter, details})(dispatch, getState); + }; export const deltaReportActions = createEntityLoadingActions('deltaReport'); -export const loadDeltaReport = gmp => (id, deltaId, filter) => ( - dispatch, - getState, -) => { - const rootState = getState(); - const state = deltaReportSelector(rootState); +export const loadDeltaReport = + gmp => (id, deltaId, filter) => (dispatch, getState) => { + const rootState = getState(); + const state = deltaReportSelector(rootState); - if (state.isLoading(id, deltaId)) { - // we are already loading data - return Promise.resolve(); - } + if (state.isLoading(id, deltaId)) { + // we are already loading data + return Promise.resolve(); + } - const identifier = deltaReportIdentifier(id, deltaId); + const identifier = deltaReportIdentifier(id, deltaId); - dispatch(deltaReportActions.request(identifier)); + dispatch(deltaReportActions.request(identifier)); - return gmp.report - .getDelta({id}, {id: deltaId}, {filter}) - .then( + return gmp.report.getDelta({id}, {id: deltaId}, {filter}).then( response => dispatch(deltaReportActions.success(identifier, response.data)), error => dispatch(deltaReportActions.error(identifier, error)), ); -}; + }; + +export const loadAuditReport = + gmp => + (id, {filter, details = true, force = false} = {}) => + (dispatch, getState) => { + const rootState = getState(); + const state = auditReportSelector(rootState); + + if (!force && state.isLoadingEntity(id, filter)) { + return Promise.resolve(); + } + + dispatch(auditReportActions.request(id, filter)); + + return gmp.auditreport + .get({id}, {filter, details}) + .then( + response => response.data, + error => { + dispatch(auditReportActions.error(id, error, filter)); + return Promise.reject(error); + }, + ) + .then(data => { + dispatch(auditReportActions.success(id, data, filter)); + + return data; + }); + }; + +export const loadAuditReportWithThreshold = + gmp => + (id, {filter} = {}) => + (dispatch, getState) => { + const rootState = getState(); + const state = auditReportSelector(rootState); + + if (state.isLoadingEntity(id, filter)) { + return Promise.resolve(); + } + + dispatch(auditReportActions.request(id, filter)); + + const {reportResultsThreshold: threshold} = gmp.settings; + return gmp.auditreport + .get({id}, {filter, details: false}) + .then( + response => response.data, + error => { + dispatch(auditReportActions.error(id, error, filter)); + return Promise.reject(error); + }, + ) + .then(report => { + const fullReport = + isDefined(report) && + isDefined(report.report) && + isDefined(report.report.results) && + report.report.results.counts.filtered < threshold; + + dispatch(auditReportActions.success(id, report, filter)); + if (fullReport) { + return loadAuditReport(gmp)(id, {filter, details: true, force: true})( + dispatch, + getState, + ); + } + }); + }; + +export const loadAuditReportIfNeeded = + gmp => + (id, {filter, details = false} = {}) => + (dispatch, getState) => { + const rootState = getState(); + const state = auditReportSelector(rootState); + + if (isDefined(state.getEntity(id, filter))) { + return Promise.resolve(); + } + return loadAuditReport(gmp)(id, {filter, details})(dispatch, getState); + }; + +export const deltaAuditReportActions = + createEntityLoadingActions('deltaAuditReport'); + +export const loadDeltaAuditReport = + gmp => (id, deltaId, filter) => (dispatch, getState) => { + const rootState = getState(); + const state = deltaAuditReportSelector(rootState); + + if (state.isLoading(id, deltaId)) { + return Promise.resolve(); + } + + const identifier = deltaReportIdentifier(id, deltaId); + + dispatch(deltaAuditReportActions.request(identifier)); + + return gmp.auditreport.getDelta({id}, {id: deltaId}, {filter}).then( + response => + dispatch(deltaAuditReportActions.success(identifier, response.data)), + error => dispatch(deltaAuditReportActions.error(identifier, error)), + ); + }; diff --git a/src/web/store/entities/report/reducers.js b/src/web/store/entities/report/reducers.js index 0166bac722..6a6dd2a4c8 100644 --- a/src/web/store/entities/report/reducers.js +++ b/src/web/store/entities/report/reducers.js @@ -70,7 +70,7 @@ const byId = (state = {}, action) => { }; export const reportReducer = (state = {}, action) => { - if (action.entityType !== 'report') { + if (action.entityType !== 'report' && action.entityType !== 'auditreport') { return state; } diff --git a/src/web/store/entities/report/selectors.js b/src/web/store/entities/report/selectors.js index 228b0d241a..26d7b3da4d 100644 --- a/src/web/store/entities/report/selectors.js +++ b/src/web/store/entities/report/selectors.js @@ -78,5 +78,11 @@ class DeltaReportSelector { export const reportSelector = rootState => new ReportSelector(rootState.entities.report); +export const auditReportSelector = rootState => + new ReportSelector(rootState.entities.auditreport); + export const deltaReportSelector = rootState => new DeltaReportSelector(rootState.entities.deltaReport); + +export const deltaAuditReportSelector = rootState => + new DeltaReportSelector(rootState.entities.deltaAuditReport); diff --git a/src/web/store/entities/reports/reducers.js b/src/web/store/entities/reports/reducers.js index cd66b54b5f..0ee6e3c88b 100644 --- a/src/web/store/entities/reports/reducers.js +++ b/src/web/store/entities/reports/reducers.js @@ -77,7 +77,7 @@ const byId = (state = {}, action) => { }; export const reportsReducer = (state = {}, action) => { - if (action.entityType !== 'report') { + if (action.entityType !== 'report' && action.entityType !== 'auditreport') { return state; } diff --git a/src/web/utils/theme.jsx b/src/web/utils/theme.jsx index 3627648ac1..df008c9a99 100644 --- a/src/web/utils/theme.jsx +++ b/src/web/utils/theme.jsx @@ -28,6 +28,11 @@ const Theme = { darkRed: '#c12c30', // used by: dialog errors font errorRed: '#c83814', // used by: progressbar + complianceYes: '#4cb045', + complianceNo: '#D80000', + complianceIncomplete: 'orange', + complianceUndefined: 'silver', + lightBlue: '#d6e6fd', // used by InfoPanel and dashboard hovering mediumBlue: '#77acf7', // used by active/hovered items in Select blue: '#0a53b8', // used by: links