From b051fa264c183084fd9c1b1f7966f38d6f1bd275 Mon Sep 17 00:00:00 2001 From: Ahmed Abdelsalam Date: Thu, 25 Jan 2024 21:13:41 +0100 Subject: [PATCH 01/37] Add: Added a dedicated view for compliance audit reports - Compliance reports are now listed under Resilience tab and do not appear anymore under Scans tab. - A dedicated view for a compliance report now shows compliance of results. - Delta compliance reports can now highlight changes in compliance. --- src/gmp/capabilities/capabilities.js | 2 + src/gmp/commands/__tests__/reports.js | 2 + src/gmp/commands/auditreports.js | 153 ++++ src/gmp/commands/reports.js | 1 + src/gmp/commands/users.js | 2 + src/gmp/gmp.js | 1 + src/gmp/models/auditreport.js | 79 ++ src/gmp/models/filter.js | 2 + src/gmp/models/filter/keywords.js | 2 + src/gmp/models/report/auditreport.js | 121 +++ src/gmp/models/report/host.js | 36 +- 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 | 2 + src/web/components/bar/compliancebar.js | 58 ++ src/web/components/bar/menubar.jsx | 6 + .../dashboard/display/createDisplay.jsx | 9 +- src/web/components/label/compliancestate.js | 83 ++ .../powerfilter/compliancelevelsgroup.js | 124 +++ src/web/pages/reports/auditdashboard/index.js | 52 ++ .../pages/reports/auditdashboard/loaders.js | 46 + .../reports/auditdashboard/statusdisplay.js | 107 +++ .../pages/reports/auditdeltadetailspage.js | 698 +++++++++++++++ src/web/pages/reports/auditdetailscontent.js | 468 ++++++++++ src/web/pages/reports/auditdetailspage.js | 809 ++++++++++++++++++ src/web/pages/reports/auditfilterdialog.js | 152 ++++ src/web/pages/reports/auditreportrow.js | 160 ++++ src/web/pages/reports/auditreportslistpage.js | 201 +++++ src/web/pages/reports/auditreportstable.js | 142 +++ src/web/pages/reports/deltadetailscontent.jsx | 14 +- .../pages/reports/details/deltaresultstab.jsx | 4 + 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 | 107 +-- .../pages/reports/details/toolbaricons.jsx | 24 +- src/web/pages/reports/detailsfilterdialog.jsx | 52 +- src/web/pages/results/row.jsx | 38 +- src/web/pages/results/table.jsx | 32 +- src/web/pages/start/dashboard.jsx | 1 + src/web/pages/tags/dialog.jsx | 1 + src/web/pages/usersettings/dialog.jsx | 17 +- src/web/pages/usersettings/filterpart.jsx | 13 + .../pages/usersettings/usersettingspage.jsx | 12 + src/web/routes.jsx | 12 + src/web/store/entities/__tests__/reducers.js | 1 + src/web/store/entities/auditreports.js | 76 ++ src/web/store/entities/reducers.js | 2 + src/web/store/entities/report/actions.js | 311 ++++--- src/web/store/entities/report/reducers.js | 2 +- src/web/store/entities/report/selectors.js | 3 + src/web/store/entities/reports/reducers.js | 2 +- 54 files changed, 4284 insertions(+), 297 deletions(-) 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.js create mode 100644 src/web/components/label/compliancestate.js create mode 100644 src/web/components/powerfilter/compliancelevelsgroup.js create mode 100644 src/web/pages/reports/auditdashboard/index.js create mode 100644 src/web/pages/reports/auditdashboard/loaders.js create mode 100644 src/web/pages/reports/auditdashboard/statusdisplay.js create mode 100644 src/web/pages/reports/auditdeltadetailspage.js create mode 100644 src/web/pages/reports/auditdetailscontent.js create mode 100644 src/web/pages/reports/auditdetailspage.js create mode 100644 src/web/pages/reports/auditfilterdialog.js create mode 100644 src/web/pages/reports/auditreportrow.js create mode 100644 src/web/pages/reports/auditreportslistpage.js create mode 100644 src/web/pages/reports/auditreportstable.js create mode 100644 src/web/store/entities/auditreports.js diff --git a/src/gmp/capabilities/capabilities.js b/src/gmp/capabilities/capabilities.js index f8baa0e991..9f08dd8bed 100644 --- a/src/gmp/capabilities/capabilities.js +++ b/src/gmp/capabilities/capabilities.js @@ -11,6 +11,8 @@ import {parseBoolean} from 'gmp/parser'; const types = { audit: 'task', audits: 'task', + auditreport: 'report', + auditreports: 'report', host: 'asset', hosts: 'asset', os: 'asset', 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..a6ae6da2ca --- /dev/null +++ b/src/gmp/commands/auditreports.js @@ -0,0 +1,153 @@ +/* Copyright (C) 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +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); + +// vim: set ts=2 sw=2 tw=80: 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..1957bd40b5 --- /dev/null +++ b/src/gmp/models/auditreport.js @@ -0,0 +1,79 @@ +/* Copyright (C) 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +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'; + +const COMPLIANCE_STATE_TRANSLATIONS = { + yes: _l('Yes'), + no: _l('No'), + incomplete: _l('Incomplete'), + undefined: _l('Undefined'), +}; +/* eslint-disable quote-props */ + +export const getTranslatableReportCompliance = compliance => + `${COMPLIANCE_STATE_TRANSLATIONS[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.report_format = parseModelFromElement(report_format, 'reportformat'); + copy.task = parseModelFromElement(task, 'task'); + + copy.report_type = type; + copy.content_type = 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; + +// vim: set ts=2 sw=2 tw=80: 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..8c1c559280 --- /dev/null +++ b/src/gmp/models/report/auditreport.js @@ -0,0 +1,121 @@ +/* Copyright (C) 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import {isDefined} from '../../utils/identity'; +import {isEmpty} from '../../utils/string'; + +import {parseDate} from '../../parser'; + +import {parseFilter} from '../../collection/parser'; + +import Model from '../../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, scan_start, scan_end, task, scan, timestamp} = + element; + + const filter = parseFilter(element); + + copy.filter = filter; + + copy.report_type = element._type; + + delete copy.filters; + + if (isDefined(compliance)) { + copy.compliance = { + filtered: compliance.filtered, + full: compliance.full, + }; + } + + 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.report_type === 'delta'; + } +} + +export default AuditReportReport; + +// vim: set ts=2 sw=2 tw=80: diff --git a/src/gmp/models/report/host.js b/src/gmp/models/report/host.js index b22be1fe19..c0f7c182a8 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.compliance_counts = { + yes: 0, + no: 0, + incomplete: 0, + total: 0, + }; } static fromElement(element) { @@ -51,7 +57,17 @@ 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.host_compliance = isDefined(host_compliance) + ? host_compliance + : 'undefined'; if (isEmpty(asset._asset_id)) { delete copy.asset; @@ -82,6 +98,24 @@ class Host { }; } + if (isDefined(compliance_count)) { + copy.compliance_counts = { + 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.compliance_counts = { + yes: 0, + no: 0, + incomplete: 0, + undefined: 0, + total: 0, + }; + } + copy.start = parseDate(element.start); copy.end = parseDate(element.end); diff --git a/src/gmp/models/report/os.js b/src/gmp/models/report/os.js index e90ed54b94..07d752b989 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 isNoInCompliance = Object.values(this.hosts.complianceByIp).some( + value => value === 'no', + ); + const isIncompleteInCompliance = Object.values( + this.hosts.complianceByIp, + ).some(value => value === 'incomplete'); + const isYesInCompliance = Object.values(this.hosts.complianceByIp).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..f7d7149ba6 100644 --- a/src/gmp/utils/entitytype.js +++ b/src/gmp/utils/entitytype.js @@ -61,6 +61,7 @@ export const normalizeType = type => { const ENTITY_TYPES = { alert: _l('Alert'), asset: _l('Asset'), + auditreport: _l('Audit Report'), certbund: _l('CERT-Bund Advisory'), cpe: _l('CPE'), credential: _l('Credential'), @@ -108,6 +109,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.js b/src/web/components/bar/compliancebar.js new file mode 100644 index 0000000000..3347de6d92 --- /dev/null +++ b/src/web/components/bar/compliancebar.js @@ -0,0 +1,58 @@ +/* Copyright (C) 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import React from 'react'; + +import {isDefined} from 'gmp/utils/identity'; + +import PropTypes from '../../utils/proptypes.js'; +import Theme from 'web/utils/theme.js'; + +import ProgressBar from './progressbar.js'; +import {getTranslatableReportCompliance} from 'gmp/models/auditreport'; + +const ComplianceBar = ({compliance, toolTip}) => { + const title = getTranslatableReportCompliance(compliance); + + let background; + if (compliance === 'no') { + background = Theme.errorRed; + } else if (compliance === 'incomplete') { + background = Theme.severityWarnYellow; + } else if (compliance === 'yes') { + background = Theme.statusRunGreen; + } else { + background = 'gray'; + } + + const toolTipText = isDefined(toolTip) ? toolTip : title; + + return ( + + {title} + + ); +}; + +ComplianceBar.propTypes = { + compliance: PropTypes.string, + toolTip: PropTypes.string, +}; + +export default ComplianceBar; + +// vim: set ts=2 sw=2 tw=80: diff --git a/src/web/components/bar/menubar.jsx b/src/web/components/bar/menubar.jsx index 8d5e567a03..053a6dd4ec 100644 --- a/src/web/components/bar/menubar.jsx +++ b/src/web/components/bar/menubar.jsx @@ -168,6 +168,12 @@ const MenuBar = ({isLoggedIn, capabilities}) => { {capabilities.mayAccess('audits') && ( )} + {capabilities.mayAccess('audits') && ( + + )} )} 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/label/compliancestate.js b/src/web/components/label/compliancestate.js new file mode 100644 index 0000000000..b144b790e1 --- /dev/null +++ b/src/web/components/label/compliancestate.js @@ -0,0 +1,83 @@ +/* Copyright (C) 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import React from 'react'; + +import styled from 'styled-components'; + +import _ from 'gmp/locale'; +import {styledExcludeProps} from 'web/utils/styledConfig'; + +const Label = styledExcludeProps(styled.div, [ + 'backgroundColor', + 'borderColor', +])` + 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 YesLabel = props => { + return ( + + ); +}; + +const NoLabel = props => { + return ( + + ); +}; + +const IncompleteLabel = props => { + return ( + + ); +}; + +const UndefinedLabel = props => { + return ( + + ); +}; + +export const ComplianceStateLabels = { + Yes: YesLabel, + No: NoLabel, + Incomplete: IncompleteLabel, + Undefined: UndefinedLabel, +}; + +export default ComplianceStateLabels; + +// vim: set ts=2 sw=2 tw=80: diff --git a/src/web/components/powerfilter/compliancelevelsgroup.js b/src/web/components/powerfilter/compliancelevelsgroup.js new file mode 100644 index 0000000000..b9c7673581 --- /dev/null +++ b/src/web/components/powerfilter/compliancelevelsgroup.js @@ -0,0 +1,124 @@ +/* Copyright (C) 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React from 'react'; + +import _ from 'gmp/locale'; + +import {isDefined} from 'gmp/utils/identity'; + +import PropTypes from '../../utils/proptypes.js'; + +import Checkbox from '../form/checkbox.js'; +import FormGroup from '../form/formgroup.js'; + +import IconDivider from '../layout/icondivider.js'; + +import ComplianceStateLabels from '../label/compliancestate.js'; + +class ComplianceLevelsFilterGroup extends React.Component { + constructor(...args) { + super(...args); + + this.handleComplianceChange = this.handleComplianceChange.bind(this); + } + + handleComplianceChange(value, level) { + const {filter, onChange, onRemove, isResult = false} = this.props; + + const filter_name = isResult + ? 'compliance_levels' + : 'report_compliance_levels'; + + let compliance = filter.get(filter_name); + + if (!compliance) { + compliance = ''; + } + + if (value && !compliance.includes(level)) { + compliance += level; + onChange(compliance, filter_name); + } else if (!value && compliance.includes(level)) { + compliance = compliance.replace(level, ''); + + if (compliance.trim().length === 0) { + onRemove(); + } else { + onChange(compliance, filter_name); + } + } + } + + render() { + const {filter, isResult} = this.props; + + let compliance_levels = filter.get( + isResult ? 'compliance_levels' : 'report_compliance_levels', + ); + + if (!isDefined(compliance_levels)) { + compliance_levels = ''; + } + return ( + + + + + + + + + + + + + + + + + ); + } +} + +ComplianceLevelsFilterGroup.propTypes = { + filter: PropTypes.filter.isRequired, + isResult: PropTypes.bool, + onChange: PropTypes.func.isRequired, + onRemove: PropTypes.func.isRequired, +}; + +export default ComplianceLevelsFilterGroup; + +// vim: set ts=2 sw=2 tw=80: diff --git a/src/web/pages/reports/auditdashboard/index.js b/src/web/pages/reports/auditdashboard/index.js new file mode 100644 index 0000000000..3118683ac4 --- /dev/null +++ b/src/web/pages/reports/auditdashboard/index.js @@ -0,0 +1,52 @@ +/* Copyright (C) 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React from 'react'; + +import Dashboard from '../../../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; + +// vim: set ts=2 sw=2 tw=80: diff --git a/src/web/pages/reports/auditdashboard/loaders.js b/src/web/pages/reports/auditdashboard/loaders.js new file mode 100644 index 0000000000..d8ed201bb3 --- /dev/null +++ b/src/web/pages/reports/auditdashboard/loaders.js @@ -0,0 +1,46 @@ +/* Copyright (C) 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import React from 'react'; + +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; + +// vim: set ts=2 sw=2 tw=80: diff --git a/src/web/pages/reports/auditdashboard/statusdisplay.js b/src/web/pages/reports/auditdashboard/statusdisplay.js new file mode 100644 index 0000000000..344c6c1a1d --- /dev/null +++ b/src/web/pages/reports/auditdashboard/statusdisplay.js @@ -0,0 +1,107 @@ +/* Copyright (C) 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import {scaleOrdinal} from 'd3-scale'; + +import {interpolateHcl} from 'd3-interpolate'; + +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 {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 red = interpolateHcl('#d62728', '#ff9896'); +const green = interpolateHcl('#2ca02c', '#98df8a'); +const orange = interpolateHcl('#ff7f0e', '#ffbb78'); + +const ComplianceStatusColorScale = scaleOrdinal() + .domain(['yes', 'no', 'incomplete', 'undefined']) + .range([green(0.25), red(0), orange(0.5), 'silver']); + +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: ComplianceStatusColorScale(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}) => + _('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}) => + _('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: Reports by Compliance'), +}); + +registerDisplay( + ReportComplianceTableDisplay.displayId, + ReportComplianceTableDisplay, + { + title: _l('Table: Reports by Compliance'), + }, +); + +// vim: set ts=2 sw=2 tw=80: diff --git a/src/web/pages/reports/auditdeltadetailspage.js b/src/web/pages/reports/auditdeltadetailspage.js new file mode 100644 index 0000000000..deb15f2d93 --- /dev/null +++ b/src/web/pages/reports/auditdeltadetailspage.js @@ -0,0 +1,698 @@ +/* Copyright (C) 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React from 'react'; + +import {connect} from 'react-redux'; + +import _ from 'gmp/locale'; + +import logger from 'gmp/log'; + +import 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 {loadDeltaReport} from 'web/store/entities/report/actions'; + +import {deltaReportSelector} 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 {loadUserSettingsDefaultFilter} from 'web/store/usersettings/defaultfilters/actions'; +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 withGmp from 'web/utils/withGmp'; + +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 getFilter = (entity = {}) => { + const {report = {}} = entity; + return report.filter; +}; + +class DeltaAuditReportDetails extends React.Component { + constructor(...args) { + super(...args); + + this.state = { + activeTab: 0, + showFilterDialog: false, + showDownloadReportDialog: false, + sorting: { + results: { + sortField: 'compliant', + sortReverse: true, + }, + errors: { + sortField: 'error', + sortReverse: false, + }, + }, + }; + + this.handleActivateTab = this.handleActivateTab.bind(this); + this.handleAddToAssets = this.handleAddToAssets.bind(this); + this.handleChanged = this.handleChanged.bind(this); + this.handleError = this.handleError.bind(this); + this.handleFilterAddLogLevel = this.handleFilterAddLogLevel.bind(this); + this.handleFilterChange = this.handleFilterChange.bind(this); + this.handleFilterDecreaseMinQoD = + this.handleFilterDecreaseMinQoD.bind(this); + this.handleFilterCreated = this.handleFilterCreated.bind(this); + this.handleFilterEditClick = this.handleFilterEditClick.bind(this); + this.handleFilterRemoveSeverity = + this.handleFilterRemoveSeverity.bind(this); + this.handleFilterRemoveClick = this.handleFilterRemoveClick.bind(this); + this.handleFilterResetClick = this.handleFilterResetClick.bind(this); + this.handleRemoveFromAssets = this.handleRemoveFromAssets.bind(this); + this.handleReportDownload = this.handleReportDownload.bind(this); + this.handleTlsCertificateDownload = + this.handleTlsCertificateDownload.bind(this); + this.handleFilterDialogClose = this.handleFilterDialogClose.bind(this); + this.handleSortChange = this.handleSortChange.bind(this); + + this.loadTarget = this.loadTarget.bind(this); + this.handleOpenDownloadReportDialog = + this.handleOpenDownloadReportDialog.bind(this); + this.handleCloseDownloadReportDialog = + this.handleCloseDownloadReportDialog.bind(this); + } + + componentDidMount() { + this.props.loadSettings(); + this.props.loadFilters(); + this.props.loadReportFormats(); + this.props.loadReportComposerDefaults(); + } + + componentDidUpdate(prevProps) { + const {reportFormats} = this.props; + if ( + !isDefined(this.state.reportFormatId) && + isDefined(reportFormats) && + reportFormats.length > 0 + ) { + // set initial report format id if available + const reportFormatId = first(reportFormats).id; + if (isDefined(reportFormatId)) { + // 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 + this.setState({reportFormatId}); + } + } + + if ( + prevProps.reportId !== this.props.reportId || + prevProps.deltaReportId !== this.props.deltaReportId + ) { + this.load(); + } + } + + load(filter) { + log.debug('Loading deleta report', { + filter, + }); + + this.setState(({lastFilter}) => ({ + isUpdating: isDefined(lastFilter) && !lastFilter.equals(filter), // show update indicator if filter has changed + lastFilter: filter, + })); + + this.props.reload(filter).then(() => { + this.setState({isUpdating: false}); + }); + } + + reload() { + // reload data from backend + this.load(this.state.lastFilter); + } + + handleChanged() { + this.reload(); + } + + handleError(error) { + const {showError} = this.props; + log.error(error); + showError(error); + } + + handleFilterChange(filter) { + this.handleInteraction(); + + this.load(filter); + } + + handleFilterRemoveClick() { + this.handleFilterChange(RESET_FILTER); + } + + handleFilterResetClick() { + this.handleFilterChange(this.props.resultDefaultFilter); + } + + handleActivateTab(index) { + this.handleInteraction(); + + this.setState({activeTab: index}); + } + + handleAddToAssets() { + const {gmp, showSuccessMessage, entity, reportFilter: filter} = this.props; + + this.handleInteraction(); + + gmp.auditreport.addAssets(entity, {filter}).then(() => { + showSuccessMessage( + _( + 'Report content added to Assets with QoD>=70% and Overrides enabled.', + ), + ); + this.reload(); + }, this.handleError); + } + + handleRemoveFromAssets() { + const {gmp, showSuccessMessage, entity, reportFilter: filter} = this.props; + + this.handleInteraction(); + + gmp.auditreport.removeAssets(entity, {filter}).then(() => { + showSuccessMessage(_('Report content removed from Assets.')); + this.reload(); + }, this.handleError); + } + + handleFilterEditClick() { + this.handleInteraction(); + + this.setState({showFilterDialog: true}); + } + + handleFilterDialogClose() { + this.handleInteraction(); + + this.setState({showFilterDialog: false}); + } + + handleOpenDownloadReportDialog() { + this.setState({ + showDownloadReportDialog: true, + }); + } + + handleCloseDownloadReportDialog() { + this.setState({showDownloadReportDialog: false}); + } + + handleReportDownload(state) { + const { + deltaReportId, + entity, + gmp, + reportComposerDefaults, + reportExportFileName, + reportFilter, + reportFormats = [], + username, + onDownload, + } = this.props; + 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, + }; + this.props.saveReportComposerDefaults(defaults); + } + + const report_format = reportFormats.find( + format => reportFormatId === format.id, + ); + + const extension = isDefined(report_format) + ? report_format.extension + : 'unknown'; // unknown should never happen but we should be save here + + this.handleInteraction(); + + return gmp.auditreport + .download(entity, { + reportFormatId, + deltaReportId, + filter: newFilter, + }) + .then(response => { + this.setState({showDownloadReportDialog: 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}); + }, this.handleError); + } + + handleTlsCertificateDownload(cert) { + const {onDownload} = this.props; + + const {data, serial} = cert; + + this.handleInteraction(); + + onDownload({ + filename: 'tls-cert-' + serial + '.pem', + data: create_pem_certificate(data), + }); + } + + handleFilterCreated(filter) { + this.handleInteraction(); + this.load(filter); + this.props.loadFilters(); + } + + handleFilterAddLogLevel() { + const {reportFilter} = this.props; + let levels = reportFilter.get('levels', ''); + + this.handleInteraction(); + + if (!levels.includes('g')) { + levels += 'g'; + const lfilter = reportFilter.copy(); + lfilter.set('levels', levels); + this.load(lfilter); + } + } + + handleFilterRemoveSeverity() { + const {reportFilter} = this.props; + + this.handleInteraction(); + + if (reportFilter.has('severity')) { + const lfilter = reportFilter.copy(); + lfilter.delete('severity'); + this.load(lfilter); + } + } + + handleFilterDecreaseMinQoD() { + const {reportFilter} = this.props; + + this.handleInteraction(); + + if (reportFilter.has('min_qod')) { + const lfilter = reportFilter.copy(); + lfilter.set('min_qod', 30); + this.load(lfilter); + } + } + + handleSortChange(name, sortField) { + this.handleInteraction(); + + const prev = this.state.sorting[name]; + + const sortReverse = + sortField === prev.sortField ? !prev.sortReverse : false; + + this.setState({ + sorting: { + ...this.state.sorting, + [name]: { + sortField, + sortReverse, + }, + }, + }); + } + + handleInteraction() { + const {onInteraction} = this.props; + if (isDefined(onInteraction)) { + onInteraction(); + } + } + + loadTarget() { + const {entity} = this.props; + const target = getTarget(entity); + + return this.props.loadTarget(target.id); + } + + render() { + const { + entity, + entityError, + filters = [], + isLoading, + reportFilter, + reportFormats, + reportId, + onInteraction, + reportComposerDefaults, + showError, + showErrorMessage, + showSuccessMessage, + } = this.props; + const { + activeTab, + isUpdating = false, + showFilterDialog, + showDownloadReportDialog, + sorting, + storeAsDefault, + } = this.state; + + const {report} = entity || {}; + + return ( + + + {({edit}) => ( + + this.loadTarget().then(response => edit(response.data)) + } + onTlsCertificateDownloadClick={this.handleTlsCertificateDownload} + showError={showError} + showErrorMessage={showErrorMessage} + showSuccessMessage={showSuccessMessage} + /> + )} + + {showFilterDialog && ( + + )} + {showDownloadReportDialog && ( + + )} + + ); + } +} + +DeltaAuditReportDetails.propTypes = { + defaultFilter: PropTypes.filter, + deltaReportId: PropTypes.id, + entity: PropTypes.model, + entityError: PropTypes.object, + filters: PropTypes.array, + gmp: PropTypes.gmp.isRequired, + isLoading: PropTypes.bool.isRequired, + loadFilters: PropTypes.func.isRequired, + loadReport: PropTypes.func.isRequired, + loadReportComposerDefaults: PropTypes.func.isRequired, + loadReportFormats: PropTypes.func.isRequired, + loadReportIfNeeded: PropTypes.func.isRequired, + loadSettings: PropTypes.func.isRequired, + loadTarget: PropTypes.func.isRequired, + location: PropTypes.object.isRequired, + match: PropTypes.object.isRequired, + reload: PropTypes.func.isRequired, + reportComposerDefaults: PropTypes.object, + reportExportFileName: PropTypes.string, + reportFilter: PropTypes.filter, + reportFormats: PropTypes.array, + reportId: PropTypes.id, + resultDefaultFilter: PropTypes.filter, + saveReportComposerDefaults: PropTypes.func.isRequired, + showError: PropTypes.func.isRequired, + showErrorMessage: PropTypes.func.isRequired, + showSuccessMessage: PropTypes.func.isRequired, + target: PropTypes.model, + username: PropTypes.string, + onDownload: PropTypes.func.isRequired, + onInteraction: PropTypes.func.isRequired, +}; + +const mapDispatchToProps = (dispatch, {gmp}) => { + return { + onInteraction: () => dispatch(renewSessionTimeout(gmp)()), + loadFilters: () => dispatch(loadFilters(gmp)(RESULTS_FILTER_FILTER)), + loadSettings: () => dispatch(loadUserSettingDefaults(gmp)()), + loadTarget: targetId => gmp.target.get({id: targetId}), + loadReportFormats: () => + dispatch(loadReportFormats(gmp)(REPORT_FORMATS_FILTER)), + loadReport: (id, deltaId, filter) => + dispatch(loadDeltaReport(gmp)(id, deltaId, filter)), + loadReportIfNeeded: (id, deltaId, filter) => + dispatch(loadDeltaReport(gmp)(id, deltaId, filter)), + loadReportComposerDefaults: () => + dispatch(loadReportComposerDefaults(gmp)()), + loadUserSettingDefaultFilter: () => + dispatch(loadUserSettingsDefaultFilter(gmp)('result')), + saveReportComposerDefaults: reportComposerDefaults => + dispatch(saveReportComposerDefaults(gmp)(reportComposerDefaults)), + }; +}; + +const mapStateToProps = (rootState, {match}) => { + const {id, deltaid} = match.params; + const filterSel = filterSelector(rootState); + const deltaSel = deltaReportSelector(rootState); + const reportFormatsSel = reportFormatsSelector(rootState); + const userDefaultsSelector = getUserSettingsDefaults(rootState); + const userDefaultFilterSel = getUserSettingsDefaultFilter( + rootState, + 'result', + ); + const username = getUsername(rootState); + const entity = deltaSel.getEntity(id, deltaid); + const entityError = deltaSel.getError(id, deltaid); + + return { + deltaReportId: deltaid, + entity, + entityError, + filters: filterSel.getAllEntities(RESULTS_FILTER_FILTER), + isLoading: !isDefined(entity), + reportExportFileName: userDefaultsSelector.getValueByName( + 'reportexportfilename', + ), + reportFilter: getFilter(entity), + reportFormats: reportFormatsSel.getAllEntities(REPORT_FORMATS_FILTER), + reportId: id, + reportComposerDefaults: getReportComposerDefaults(rootState), + resultDefaultFilter: userDefaultFilterSel.getFilter('result'), + username, + }; +}; + +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, + loadReport, + loadReportIfNeeded, + 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 loadReportIfNeeded(reportId, deltaReportId, filter).then(() => + loadReport(reportId, deltaReportId, filter), + ); + }; + +const DeltaAuditReportDetailsWrapper = ({ + defaultFilter, + reportFilter, + ...props +}) => ( + reloadInterval(props.entity)} + > + {({reload}) => ( + + )} + +); + +DeltaAuditReportDetailsWrapper.propTypes = { + defaultFilter: PropTypes.filter, + entity: PropTypes.model, + gmp: PropTypes.gmp.isRequired, + reportFilter: PropTypes.filter, +}; + +export default compose( + withGmp, + withDialogNotification, + withDownload, + connect(mapStateToProps, mapDispatchToProps), +)(DeltaAuditReportDetailsWrapper); + +// vim: set ts=2 sw=2 tw=80: diff --git a/src/web/pages/reports/auditdetailscontent.js b/src/web/pages/reports/auditdetailscontent.js new file mode 100644 index 0000000000..1b5298bef7 --- /dev/null +++ b/src/web/pages/reports/auditdetailscontent.js @@ -0,0 +1,468 @@ +/* Copyright (C) 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React from 'react'; + +import styled from 'styled-components'; + +import _ from 'gmp/locale'; + +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 withGmp from 'web/utils/withGmp'; + +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 ThresholdPanel from './details/thresholdpanel'; +import TLSCertificatesTab from './details/tlscertificatestab'; +import ToolBarIcons from './details/toolbaricons'; + +const Span = styled.span` + margin-top: 2px; +`; + +const PageContent = ({ + activeTab, + entity, + errorsCounts, + filters, + gmp, + hostsCounts, + isLoading = true, + isLoadingFilters = true, + isUpdating = false, + operatingSystemsCounts, + pageFilter, + reportError, + reportFilter, + reportId, + resetFilter, + resultsCounts, + sorting, + showError, + showErrorMessage, + showSuccessMessage, + task, + tlsCertificatesCounts, + onActivateTab, + onAddToAssetsClick, + onTlsCertificateDownloadClick, + onError, + onFilterAddLogLevelClick, + onFilterChanged, + onFilterCreated, + onFilterDecreaseMinQoDClick, + onFilterEditClick, + onFilterRemoveSeverityClick, + 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 { + 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, + gmp: PropTypes.gmp.isRequired, + 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, + onFilterAddLogLevelClick: PropTypes.func.isRequired, + onFilterChanged: PropTypes.func.isRequired, + onFilterCreated: PropTypes.func.isRequired, + onFilterDecreaseMinQoDClick: PropTypes.func.isRequired, + onFilterEditClick: PropTypes.func.isRequired, + onFilterRemoveClick: PropTypes.func.isRequired, + onFilterRemoveSeverityClick: 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 withGmp(PageContent); + +// vim: set ts=2 sw=2 tw=80: diff --git a/src/web/pages/reports/auditdetailspage.js b/src/web/pages/reports/auditdetailspage.js new file mode 100644 index 0000000000..46894280af --- /dev/null +++ b/src/web/pages/reports/auditdetailspage.js @@ -0,0 +1,809 @@ +/* Copyright (C) 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React from 'react'; + +import {connect} from 'react-redux'; + +import _ from 'gmp/locale'; + +import logger from 'gmp/log'; + +import 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 {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 withGmp from 'web/utils/withGmp'; + +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'; + +const log = logger.getLogger('web.pages.auditreport.detailspage'); + +const DEFAULT_FILTER = Filter.fromString( + 'levels=hmlg rows=100 min_qod=70 first=1 sort-reverse=compliant', +); + +export const REPORT_RESET_FILTER = RESET_FILTER.copy() + .setSortOrder('sort-reverse') + .setSortBy('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 getFilter = (entity = {}) => { + const {report = {}} = entity; + return report.filter; +}; + +class ReportDetails extends React.Component { + constructor(...args) { + super(...args); + + this.state = { + activeTab: 0, + showFilterDialog: false, + showDownloadReportDialog: false, + sorting: { + 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, + }, + }, + }; + + this.handleActivateTab = this.handleActivateTab.bind(this); + this.handleAddToAssets = this.handleAddToAssets.bind(this); + this.handleChanged = this.handleChanged.bind(this); + this.handleError = this.handleError.bind(this); + this.handleFilterAddLogLevel = this.handleFilterAddLogLevel.bind(this); + this.handleFilterChange = this.handleFilterChange.bind(this); + this.handleFilterDecreaseMinQoD = + this.handleFilterDecreaseMinQoD.bind(this); + this.handleFilterCreated = this.handleFilterCreated.bind(this); + this.handleFilterEditClick = this.handleFilterEditClick.bind(this); + this.handleFilterRemoveSeverity = + this.handleFilterRemoveSeverity.bind(this); + this.handleFilterRemoveClick = this.handleFilterRemoveClick.bind(this); + this.handleFilterResetClick = this.handleFilterResetClick.bind(this); + this.handleRemoveFromAssets = this.handleRemoveFromAssets.bind(this); + this.handleReportDownload = this.handleReportDownload.bind(this); + this.handleTlsCertificateDownload = + this.handleTlsCertificateDownload.bind(this); + this.handleFilterDialogClose = this.handleFilterDialogClose.bind(this); + this.handleSortChange = this.handleSortChange.bind(this); + + this.loadTarget = this.loadTarget.bind(this); + this.handleOpenDownloadReportDialog = + this.handleOpenDownloadReportDialog.bind(this); + this.handleCloseDownloadReportDialog = + this.handleCloseDownloadReportDialog.bind(this); + } + + static getDerivedStateFromProps(props, state) { + if (isDefined(props.entity)) { + // update only if a new report is available to avoid having no report + // when the filter changes + + const {report = {}} = props.entity; + const { + results = {}, + hosts = {}, + operatingsystems = {}, + tlsCertificates = {}, + errors = {}, + } = report; + + return { + entity: props.entity, + + resultsCounts: isDefined(results.counts) + ? results.counts + : state.resultsCounts, + hostsCounts: isDefined(hosts.counts) ? hosts.counts : state.hostsCounts, + operatingSystemsCounts: isDefined(operatingsystems.counts) + ? operatingsystems.counts + : state.operatingSystemsCounts, + tlsCertificatesCounts: isDefined(tlsCertificates.counts) + ? tlsCertificates.counts + : state.tlsCertificatesCounts, + errorsCounts: isDefined(errors.counts) + ? errors.counts + : state.errorsCounts, + reportFilter: props.reportFilter, + isUpdating: false, + }; + } + // report is not in the store and is currently loaded + return { + isUpdating: true, + }; + } + + componentDidMount() { + this.props.loadSettings(); + this.props.loadFilters(); + this.props.loadReportFormats(); + this.props.loadReportComposerDefaults(); + } + + componentDidUpdate(prevProps) { + const {reportFormats} = this.props; + if ( + !isDefined(this.state.reportFormatId) && + isDefined(reportFormats) && + reportFormats.length > 0 + ) { + // set initial report format id if available + const reportFormatId = first(reportFormats).id; + if (isDefined(reportFormatId)) { + // 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 + this.setState({reportFormatId}); + } 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 Greenbone Vulnerability Manager 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, or' + + ' contact your system administrator.', + ); + throw new Error(noReportFormatError); + } + } + + if (prevProps.reportId !== this.props.reportId) { + this.load(); + } + } + + load(filter) { + log.debug('Loading report', { + filter, + }); + const {reportFilter} = this.props; + + this.setState({ + isUpdating: !isDefined(reportFilter) || !reportFilter.equals(filter), // show update indicator if filter has changed + }); + + this.props + .reload(filter) + .then(() => { + this.setState({isUpdating: false}); + }) + .catch(() => { + this.setState({isUpdating: false}); + }); + } + + reload() { + // reload data from backend + this.load(this.props.reportFilter); + } + + handleChanged() { + this.reload(); + } + + handleError(error) { + const {showError} = this.props; + log.error(error); + showError(error); + } + + handleFilterChange(filter) { + this.handleInteraction(); + + this.load(filter); + } + + handleFilterRemoveClick() { + this.handleFilterChange(REPORT_RESET_FILTER); + } + + handleFilterResetClick() { + if (hasValue(this.props.resultDefaultFilter)) { + this.handleFilterChange(this.props.resultDefaultFilter); + } else { + this.handleFilterChange(DEFAULT_FILTER); + } + } + + handleActivateTab(index) { + this.handleInteraction(); + + this.setState({activeTab: index}); + } + + handleAddToAssets() { + const {gmp, showSuccessMessage, entity, reportFilter: filter} = this.props; + + this.handleInteraction(); + + gmp.auditreport.addAssets(entity, {filter}).then(() => { + showSuccessMessage( + _( + 'Report content added to Assets with QoD>=70% and Overrides enabled.', + ), + ); + this.reload(); + }, this.handleError); + } + + handleRemoveFromAssets() { + const {gmp, showSuccessMessage, entity, reportFilter: filter} = this.props; + + this.handleInteraction(); + + gmp.auditreport.removeAssets(entity, {filter}).then(() => { + showSuccessMessage(_('Report content removed from Assets.')); + this.reload(); + }, this.handleError); + } + + handleFilterEditClick() { + this.handleInteraction(); + + this.setState({showFilterDialog: true}); + } + + handleFilterDialogClose() { + this.handleInteraction(); + + this.setState({showFilterDialog: false}); + } + + handleOpenDownloadReportDialog() { + this.setState({ + showDownloadReportDialog: true, + }); + } + + handleCloseDownloadReportDialog() { + this.setState({showDownloadReportDialog: false}); + } + + handleReportDownload(state) { + const { + entity, + gmp, + reportComposerDefaults, + reportExportFileName, + reportFilter, + reportFormats = [], + username, + onDownload, + } = this.props; + 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, + }; + this.props.saveReportComposerDefaults(defaults); + } + + const report_format = reportFormats.find( + format => reportFormatId === format.id, + ); + + const extension = isDefined(report_format) + ? report_format.extension + : 'unknown'; // unknown should never happen but we should be save here + + this.handleInteraction(); + + return gmp.auditreport + .download(entity, { + reportFormatId, + filter: newFilter, + }) + .then(response => { + this.setState({showDownloadReportDialog: 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}); + }, this.handleError); + } + + handleTlsCertificateDownload(cert) { + const {onDownload} = this.props; + + const {data, serial} = cert; + + this.handleInteraction(); + + onDownload({ + filename: 'tls-cert-' + serial + '.pem', + mimetype: 'application/x-x509-ca-cert', + data: create_pem_certificate(data), + }); + } + + handleFilterCreated(filter) { + this.handleInteraction(); + this.load(filter); + this.props.loadFilters(); + } + + handleFilterAddLogLevel() { + const {reportFilter} = this.props; + let levels = reportFilter.get('levels', ''); + + this.handleInteraction(); + + if (!levels.includes('g')) { + levels += 'g'; + const lfilter = reportFilter.copy(); + lfilter.set('levels', levels); + this.load(lfilter); + } + } + + handleFilterRemoveSeverity() { + const {reportFilter} = this.props; + + this.handleInteraction(); + + if (reportFilter.has('severity')) { + const lfilter = reportFilter.copy(); + lfilter.delete('severity'); + this.load(lfilter); + } + } + + handleFilterDecreaseMinQoD() { + const {reportFilter} = this.props; + + this.handleInteraction(); + + if (reportFilter.has('min_qod')) { + const lfilter = reportFilter.copy(); + lfilter.set('min_qod', 30); + this.load(lfilter); + } + } + + handleSortChange(name, sortField) { + this.handleInteraction(); + + const prev = this.state.sorting[name]; + + const sortReverse = + sortField === prev.sortField ? !prev.sortReverse : false; + + this.setState({ + sorting: { + ...this.state.sorting, + [name]: { + sortField, + sortReverse, + }, + }, + }); + } + + handleInteraction() { + const {onInteraction} = this.props; + if (isDefined(onInteraction)) { + onInteraction(); + } + } + + loadTarget() { + const {entity} = this.props; + const target = getTarget(entity); + + return this.props.loadTarget(target.id); + } + + render() { + const { + filters = [], + gmp, + isLoading, + isLoadingFilters, + pageFilter, + reportError, + reportFormats, + reportId, + onInteraction, + reportComposerDefaults, + showError, + showErrorMessage, + showSuccessMessage, + } = this.props; + const { + activeTab, + entity, + errorsCounts, + hostsCounts, + isUpdating = false, + operatingSystemsCounts, + reportFilter, + resultsCounts, + showFilterDialog, + showDownloadReportDialog, + sorting, + storeAsDefault, + tlsCertificatesCounts, + } = this.state; + + const report = isDefined(entity) ? entity.report : undefined; + + const threshold = gmp.settings.reportResultsThreshold; + + const showThresholdMessage = + isDefined(report) && report.results.counts.filtered > threshold; + + return ( + + + + {({edit}) => ( + + this.loadTarget().then(response => edit(response.data)) + } + onTlsCertificateDownloadClick={this.handleTlsCertificateDownload} + showError={showError} + showErrorMessage={showErrorMessage} + showSuccessMessage={showSuccessMessage} + /> + )} + + {showFilterDialog && ( + + )} + {showDownloadReportDialog && ( + + )} + + ); + } +} + +ReportDetails.propTypes = { + entity: PropTypes.model, + filter: PropTypes.filter, + filters: PropTypes.array, + gmp: PropTypes.gmp.isRequired, + isLoading: PropTypes.bool, + isLoadingFilters: PropTypes.bool, + loadFilters: PropTypes.func.isRequired, + loadReportComposerDefaults: PropTypes.func.isRequired, + loadReportFormats: PropTypes.func.isRequired, + loadSettings: PropTypes.func.isRequired, + loadTarget: PropTypes.func.isRequired, + location: PropTypes.object.isRequired, + match: PropTypes.object.isRequired, + pageFilter: PropTypes.filter, + reload: PropTypes.func.isRequired, + reportComposerDefaults: PropTypes.object, + reportError: PropTypes.error, + reportExportFileName: PropTypes.string, + reportFilter: PropTypes.filter, + reportFormats: PropTypes.array, + reportId: PropTypes.id, + resultDefaultFilter: PropTypes.filter, + saveReportComposerDefaults: PropTypes.func.isRequired, + showError: PropTypes.func.isRequired, + showErrorMessage: PropTypes.func.isRequired, + showSuccessMessage: PropTypes.func.isRequired, + target: PropTypes.model, + username: PropTypes.string, + onDownload: PropTypes.func.isRequired, + onInteraction: 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 + loadReportWithThreshold, + pageFilter, + reportFilter, + updateFilter, + }) => + 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; + } + + updateFilter(filter); + return loadReportWithThreshold(reportId, {filter}); + }; + +const ReportDetailsWrapper = ({reportFilter, ...props}) => ( + + {({filter}) => ( + reloadInterval(props.entity)} + > + {({reload}) => ( + + )} + + )} + +); + +ReportDetailsWrapper.propTypes = { + entity: PropTypes.model, + gmp: PropTypes.gmp.isRequired, + reportFilter: PropTypes.filter, + reportId: PropTypes.id.isRequired, +}; + +const getReportPageName = id => `report-${id}`; + +const mapDispatchToProps = (dispatch, {gmp, entity, match}) => ({ + onInteraction: () => dispatch(renewSessionTimeout(gmp)()), + loadFilters: () => dispatch(loadFilters(gmp)(RESULTS_FILTER_FILTER)), + loadSettings: () => dispatch(loadUserSettingDefaults(gmp)()), + loadTarget: targetId => gmp.target.get({id: targetId}), + loadReportFormats: () => + dispatch(loadReportFormats(gmp)(REPORT_FORMATS_FILTER)), + loadReportWithThreshold: (id, options) => + dispatch(loadAuditReportWithThreshold(gmp)(id, options)), + loadReportComposerDefaults: () => dispatch(loadReportComposerDefaults(gmp)()), + saveReportComposerDefaults: reportComposerDefaults => + dispatch(saveReportComposerDefaults(gmp)(reportComposerDefaults)), + updateFilter: f => + dispatch(setPageFilter(getReportPageName(match.params.id), f)), +}); + +const mapStateToProps = (rootState, {match}) => { + const {id} = match.params; + const filterSel = filterSelector(rootState); + const reportSel = auditReportSelector(rootState); + const reportFormatsSel = reportFormatsSelector(rootState); + const userDefaultsSelector = getUserSettingsDefaults(rootState); + const userDefaultFilterSel = getUserSettingsDefaultFilter( + rootState, + 'result', + ); + const username = getUsername(rootState); + + const pSelector = getPage(rootState); + const pageFilter = pSelector.getFilter(getReportPageName(id)); + + const entity = reportSel.getEntity(id, pageFilter); + const isLoading = reportSel.isLoadingEntity(id, pageFilter); + const reportError = reportSel.getEntityError(id, pageFilter); + + const filters = filterSel.getAllEntities(RESULTS_FILTER_FILTER); + const isLoadingFilters = filterSel.isLoadingAllEntities( + RESULTS_FILTER_FILTER, + ); + + return { + entity, + filters, + reportError, + pageFilter, + isLoading, + isLoadingFilters, + reportExportFileName: userDefaultsSelector.getValueByName( + 'reportexportfilename', + ), + reportFilter: getFilter(entity), + reportFormats: reportFormatsSel.getAllEntities(REPORT_FORMATS_FILTER), + reportId: id, + reportComposerDefaults: getReportComposerDefaults(rootState), + resultDefaultFilter: userDefaultFilterSel.getFilter(), + username, + }; +}; + +export default compose( + withGmp, + withDialogNotification, + withDownload, + connect(mapStateToProps, mapDispatchToProps), +)(ReportDetailsWrapper); + +// vim: set ts=2 sw=2 tw=80: diff --git a/src/web/pages/reports/auditfilterdialog.js b/src/web/pages/reports/auditfilterdialog.js new file mode 100644 index 0000000000..e9618b1367 --- /dev/null +++ b/src/web/pages/reports/auditfilterdialog.js @@ -0,0 +1,152 @@ +/* Copyright (C) 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import React from 'react'; + +import {_l, _} from 'gmp/locale/lang'; + +import Layout from 'web/components/layout/layout'; + +import compose from 'web/utils/compose'; +import withCapabilities from 'web/utils/withCapabilities'; + +/* 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 SORT_FIELDS = [ + { + name: 'date', + displayName: _l('Date'), + }, + { + name: 'status', + displayName: _l('Status'), + }, + { + name: 'task', + displayName: _l('Task'), + }, + { + name: 'compliant', + displayName: _l('Compliant'), + }, + { + name: 'compliance_yes', + displayName: _l('Compliance: Yes'), + }, + { + name: 'compliance_no', + displayName: _l('Compliance: No'), + }, + { + name: 'compliance_incomplete', + displayName: _l('Compliance: Incomplete'), + }, +]; + +const AuditReportFilterDialogComponent = ({ + capabilities, + filter, + filterName, + filterstring, + onFilterChange, + saveNamedFilter, + onFilterStringChange, + onFilterValueChange, + onSearchTermChange, + onSortByChange, + onSortOrderChange, + onValueChange, +}) => { + const handleRemoveCompliance = () => + onFilterChange(filter.delete('report_compliance_levels')); + + if (!filter) { + return null; + } + + return ( + + + + + + + + + + + + + + + + {capabilities.mayCreate('filter') && ( + + )} + + ); +}; + +AuditReportFilterDialogComponent.propTypes = FilterDialogPropTypes; + +export default compose( + withCapabilities, + withFilterDialog(), +)(AuditReportFilterDialogComponent); + +// vim: set ts=2 sw=2 tw=80: diff --git a/src/web/pages/reports/auditreportrow.js b/src/web/pages/reports/auditreportrow.js new file mode 100644 index 0000000000..b922244eb1 --- /dev/null +++ b/src/web/pages/reports/auditreportrow.js @@ -0,0 +1,160 @@ +/* Copyright (C) 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React from 'react'; + +import _ from 'gmp/locale'; + +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 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.compliance_count.yes.filtered} + {report.compliance_count.no.filtered} + + {report.compliance_count.incomplete.filtered} + + + + ); +}; + +AuditRow.propTypes = { + actionsComponent: PropTypes.component, + entity: PropTypes.model.isRequired, + links: PropTypes.bool, +}; + +export default AuditRow; + +// vim: set ts=2 sw=2 tw=80: diff --git a/src/web/pages/reports/auditreportslistpage.js b/src/web/pages/reports/auditreportslistpage.js new file mode 100644 index 0000000000..99f0dc27fb --- /dev/null +++ b/src/web/pages/reports/auditreportslistpage.js @@ -0,0 +1,201 @@ +/* Copyright (C) 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React from 'react'; + +import _ from 'gmp/locale'; + +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 compose from 'web/utils/compose'; +import PropTypes from 'web/utils/proptypes'; +import withGmp from 'web/utils/withGmp'; + +import AuditFilterDialog from './auditfilterdialog'; +import AuditReportsTable from './auditreportstable'; + +import AuditReportsDashboard, { + AUDIT_REPORTS_DASHBOARD_ID, +} from './auditdashboard'; + +const ToolBarIcons = () => ( + + + +); + +class Page extends React.Component { + constructor(...args) { + super(...args); + + this.state = {}; + + this.handleReportDeltaSelect = this.handleReportDeltaSelect.bind(this); + this.handleReportDeleteClick = this.handleReportDeleteClick.bind(this); + this.handleTaskChange = this.handleTaskChange.bind(this); + } + + static getDerivedStateFromProps(props, state) { + const {filter} = props; + const {selectedDeltaReport} = state; + + if ( + isDefined(selectedDeltaReport) && + (!isDefined(filter) || + filter.get('task_id') !== selectedDeltaReport.task.id) + ) { + // filter has changed. reset delta report selection + return {selectedDeltaReport: undefined}; + } + return null; + } + + handleReportDeltaSelect(report) { + const {onFilterChanged} = this.props; + const {selectedDeltaReport, beforeSelectFilter} = this.state; + + if (isDefined(selectedDeltaReport)) { + const {history} = this.props; + + onFilterChanged(beforeSelectFilter); + + history.push( + '/auditreport/delta/' + selectedDeltaReport.id + '/' + report.id, + ); + } else { + const {filter = new Filter()} = this.props; + + onFilterChanged( + filter + .copy() + .set('first', 1) // reset to first page + .set('task_id', report.task.id), + ); + + this.setState({ + beforeSelectFilter: filter, + selectedDeltaReport: report, + }); + } + } + + handleReportDeleteClick(report) { + const {onDelete} = this.props; + return onDelete(report); + } + + handleTaskChange(task_id) { + this.setState({task_id}); + } + + render() { + const {filter, onFilterChanged, onInteraction} = this.props; + return ( + + + ( + + )} + dashboardControls={() => ( + + )} + filtersFilter={AUDIT_REPORTS_FILTER_FILTER} + filterEditDialog={AuditFilterDialog} + table={AuditReportsTable} + toolBarIcons={ToolBarIcons} + title={_('Audit Reports')} + sectionIcon={} + onInteraction={onInteraction} + onReportDeltaSelect={this.handleReportDeltaSelect} + onReportDeleteClick={this.handleReportDeleteClick} + /> + + ); + } +} + +Page.propTypes = { + filter: PropTypes.filter, + gmp: PropTypes.gmp.isRequired, + 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_REPORT_LIST_FILTER = Filter.fromString( + 'sort-reverse=date first=1', +); + +export default compose( + withGmp, + withEntitiesContainer('auditreport', { + fallbackFilter: FALLBACK_REPORT_LIST_FILTER, + entitiesSelector, + loadEntities, + reloadInterval: reportsReloadInterval, + }), +)(Page); + +// vim: set ts=2 sw=2 tw=80: diff --git a/src/web/pages/reports/auditreportstable.js b/src/web/pages/reports/auditreportstable.js new file mode 100644 index 0000000000..a4660d3c2e --- /dev/null +++ b/src/web/pages/reports/auditreportstable.js @@ -0,0 +1,142 @@ +/* Copyright (C) 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import React from 'react'; + +import {_, _l} from 'gmp/locale/lang'; + +import {isDefined} from 'gmp/utils/identity'; + +import PropTypes from '../../utils/proptypes.js'; + +import {createEntitiesFooter} from '../../entities/footer.js'; +import {createEntitiesTable} from '../../entities/table.js'; + +import ComplianceState from '../../components/label/compliancestate.js'; + +import TableHead from '../../components/table/head.js'; +import TableHeader from '../../components/table/header.js'; +import TableRow from '../../components/table/row.js'; + +import AuditReportRow from './auditreportrow.js'; + +const Header = ({ + actionsColumn, + sort = true, + currentSortBy, + currentSortDir, + onSortChange, +}) => { + 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, +}); + +// vim: set ts=2 sw=2 tw=80: diff --git a/src/web/pages/reports/deltadetailscontent.jsx b/src/web/pages/reports/deltadetailscontent.jsx index 97141d8c48..0f6174ee97 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, @@ -92,7 +93,13 @@ const PageContent = ({ const {userTags = {}} = report; const userTagsCount = userTags.length; - const {results = {}, result_count = {}, timestamp, scan_run_status} = report; + const { + results = {}, + compliance_count = {}, + result_count = {}, + timestamp, + scan_run_status, + } = report; const hasReport = isDefined(entity); @@ -126,12 +133,13 @@ const PageContent = ({ ); - const {filtered} = result_count; + const {filtered} = audit ? compliance_count : result_count; return ( entity.nvt.solution?.type), vulnerability: makeCompareString('vulnerability'), + // TODO: Add filter for compliant }; const ResultsTab = ({ + audit = false, counts, delta = false, filter, @@ -99,6 +101,7 @@ const ResultsTab = ({ onPreviousClick, }) => ( entity.start), end: makeCompareDate(entity => entity.end), total: makeCompareNumber(entity => entity.result_counts.total), + compliance_yes: makeCompareNumber(entity => entity.compliance_counts.yes), + compliance_no: makeCompareNumber(entity => entity.compliance_counts.no), + compliance_incomplete: makeCompareNumber( + entity => entity.compliance_counts.incomplete, + ), + compliance_total: makeCompareNumber(entity => entity.compliance_counts.total), + // TODO Add filter for compliant }; 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, + host_compliance, ip, result_counts = {}, + compliance_counts = {}, 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 ? ( + {compliance_counts.yes} + ) : ( + {result_counts.high} + )} + {audit ? ( + {compliance_counts.no} + ) : ( + {result_counts.warning} + )} + {audit ? ( + {compliance_counts.incomplete} + ) : ( + {result_counts.info} + )} + {!audit && {result_counts.log}} + {!audit && {result_counts.false_positive}} + {audit ? ( + {compliance_counts.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..af21628ca9 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), + // TODO Add filter for compliant }; 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..5f7a6675ae 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 => ( - + {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/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 ? ( + + ) : ( + + )} { - 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 = ({ const FilterPart = ({ alertsFilter, + auditReportsFilter, configsFilter, credentialsFilter, filtersFilter, @@ -64,6 +65,17 @@ const FilterPart = ({ onChange={onChange} /> + + + { 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 +1016,7 @@ const mapStateToProps = rootState => { defaultSchedule, defaultTarget, alertsFilter, + auditReportsFilter, configsFilter, credentialsFilter, filtersFilter, @@ -1046,6 +1057,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..7bd7be35b0 100644 --- a/src/web/routes.jsx +++ b/src/web/routes.jsx @@ -23,6 +23,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 +68,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 +124,15 @@ const Routes = () => ( + + + diff --git a/src/web/store/entities/__tests__/reducers.js b/src/web/store/entities/__tests__/reducers.js index 71e7a11e04..f11f0d884e 100644 --- a/src/web/store/entities/__tests__/reducers.js +++ b/src/web/store/entities/__tests__/reducers.js @@ -24,6 +24,7 @@ describe('entities reducer tests', () => { expect(entitiesReducer(undefined, {})).toEqual({ alert: initState, audit: initState, + auditreport: initState, certbund: initState, cpe: initState, credential: initState, diff --git a/src/web/store/entities/auditreports.js b/src/web/store/entities/auditreports.js new file mode 100644 index 0000000000..c9027660a1 --- /dev/null +++ b/src/web/store/entities/auditreports.js @@ -0,0 +1,76 @@ +/* Copyright (C) 2024 Greenbone AG + * + * SPDX-License-Identifier: AGPL-3.0-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License + * as published by the Free Software Foundation, either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +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 deltaReducer = createReducer('deltaReport'); + +export { + deltaReducer, + loadAllEntities, + loadEntities, + reducer, + reportsSelector as selector, + entitiesActions, +}; + +// vim: set ts=2 sw=2 tw=80: diff --git a/src/web/store/entities/reducers.js b/src/web/store/entities/reducers.js index d12f11332f..79a24926b5 100644 --- a/src/web/store/entities/reducers.js +++ b/src/web/store/entities/reducers.js @@ -7,6 +7,7 @@ import {combineReducers} from 'redux'; import {reducer as alert} from './alerts'; import {reducer as audit} from './audits'; +import {reducer as auditreport} from './auditreports'; import {reducer as certbund} from './certbund'; import {reducer as cpe} from './cpes'; import {reducer as credential} from './credentials'; @@ -41,6 +42,7 @@ import {reducer as vuln} from './vulns'; const entitiesReducer = combineReducers({ alert, audit, + auditreport, certbund, cpe, credential, diff --git a/src/web/store/entities/report/actions.js b/src/web/store/entities/report/actions.js index 264f97e7b4..e72a03b6bd 100644 --- a/src/web/store/entities/report/actions.js +++ b/src/web/store/entities/report/actions.js @@ -11,28 +11,27 @@ import { } from 'web/store/entities/utils/actions'; import { + auditReportSelector, reportSelector, deltaReportSelector, deltaReportIdentifier, } from './selectors'; -const entityType = 'report'; - export const reportActions = { - request: (id, filter) => ({ + request: (id, filter, entityType = 'report') => ({ type: types.ENTITY_LOADING_REQUEST, entityType, filter, id, }), - success: (id, data, filter) => ({ + success: (id, data, filter, entityType = 'report') => ({ type: types.ENTITY_LOADING_SUCCESS, entityType, data, filter, id, }), - error: (id, error, filter) => ({ + error: (id, error, filter, entityType = 'report') => ({ type: types.ENTITY_LOADING_ERROR, entityType, error, @@ -41,116 +40,214 @@ export const reportActions = { }), }; -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 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 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 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(reportActions.request(id, filter, 'auditreport')); + + return gmp.auditreport + .get({id}, {filter, details}) + .then( + response => response.data, + error => { + dispatch(reportActions.error(id, error, filter, 'auditreport')); + return Promise.reject(error); + }, + ) + .then(data => { + dispatch(reportActions.success(id, data, filter, 'auditreport')); + + 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(reportActions.request(id, filter, 'auditreport')); + + const {reportResultsThreshold: threshold} = gmp.settings; + return gmp.auditreport + .get({id}, {filter, details: false}) + .then( + response => response.data, + error => { + dispatch(reportActions.error(id, error, filter, 'auditreport')); + 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, 'auditreport')); + 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 loadDeltaAuditReport = + gmp => (id, deltaId, filter) => (dispatch, getState) => { + const rootState = getState(); + const state = deltaReportSelector(rootState); + + if (state.isLoading(id, deltaId)) { + return Promise.resolve(); + } + + const identifier = deltaReportIdentifier(id, deltaId); + + dispatch(deltaReportActions.request(identifier)); + + return gmp.auditreport.getDelta({id}, {id: deltaId}, {filter}).then( + response => + dispatch(deltaReportActions.success(identifier, response.data)), + error => dispatch(deltaReportActions.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..48314d3557 100644 --- a/src/web/store/entities/report/selectors.js +++ b/src/web/store/entities/report/selectors.js @@ -78,5 +78,8 @@ 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); 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; } From 1059bbfcbc88ab74108c21029d51ab6a22b815a8 Mon Sep 17 00:00:00 2001 From: Ahmed Abdelsalam Date: Wed, 31 Jan 2024 10:11:01 +0100 Subject: [PATCH 02/37] Add audit reports to the display list in dashboards --- src/gmp/utils/entitytype.js | 1 + src/web/pages/filters/component.jsx | 1 + .../pages/reports/auditdashboard/statusdisplay.js | 12 ++++++++---- src/web/pages/reports/auditdetailscontent.js | 2 +- src/web/pages/reports/auditreportslistpage.js | 8 ++++---- src/web/pages/start/dashboard.jsx | 3 ++- 6 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/gmp/utils/entitytype.js b/src/gmp/utils/entitytype.js index f7d7149ba6..a850343d09 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', diff --git a/src/web/pages/filters/component.jsx b/src/web/pages/filters/component.jsx index 3b473ab82e..96f249b421 100644 --- a/src/web/pages/filters/component.jsx +++ b/src/web/pages/filters/component.jsx @@ -20,6 +20,7 @@ import FilterDialog from 'web/pages/filters/dialog'; const FILTER_OPTIONS = [ ['alert', _l('Alert')], + ['audit_report', _l('Audit Report')], ['credential', _l('Credential')], ['filter', _l('Filter')], ['group', _l('Group')], diff --git a/src/web/pages/reports/auditdashboard/statusdisplay.js b/src/web/pages/reports/auditdashboard/statusdisplay.js index 344c6c1a1d..31e2969fd5 100644 --- a/src/web/pages/reports/auditdashboard/statusdisplay.js +++ b/src/web/pages/reports/auditdashboard/statusdisplay.js @@ -73,7 +73,9 @@ export const ReportComplianceDisplay = createDisplay({ filterTerm: 'compliant', displayId: 'report-by-compliance', title: ({data: tdata}) => - _('Reports by Compliance (Total: {{count}})', {count: tdata.total}), + _('Compliance Reports by Compliance (Total: {{count}})', { + count: tdata.total, + }), filtersFilter: AUDIT_REPORTS_FILTER_FILTER, loaderComponent: ReportCompianceLoader, }); @@ -86,21 +88,23 @@ export const ReportComplianceTableDisplay = createDisplay({ dataTitles: [_l('Status'), _l('# of Reports')], dataRow: row => [row.label, row.value], title: ({data: tdata}) => - _('Reports by Compliance (Total: {{count}})', {count: tdata.total}), + _('Compliance 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: Reports by Compliance'), + title: _l('Chart: Compliance Reports by Compliance'), }); registerDisplay( ReportComplianceTableDisplay.displayId, ReportComplianceTableDisplay, { - title: _l('Table: Reports by Compliance'), + title: _l('Table: Compliance Reports by Compliance'), }, ); diff --git a/src/web/pages/reports/auditdetailscontent.js b/src/web/pages/reports/auditdetailscontent.js index 1b5298bef7..ab7bca16f6 100644 --- a/src/web/pages/reports/auditdetailscontent.js +++ b/src/web/pages/reports/auditdetailscontent.js @@ -159,7 +159,7 @@ const PageContent = ({ const header_title = ( - {_('Audit Report:')} + {_('Compliance Report:')} {showIsLoading ? ( {_('Loading')} ) : ( diff --git a/src/web/pages/reports/auditreportslistpage.js b/src/web/pages/reports/auditreportslistpage.js index 99f0dc27fb..ebfa835a0a 100644 --- a/src/web/pages/reports/auditreportslistpage.js +++ b/src/web/pages/reports/auditreportslistpage.js @@ -136,7 +136,7 @@ class Page extends React.Component { const {filter, onFilterChanged, onInteraction} = this.props; return ( - + } onInteraction={onInteraction} onReportDeltaSelect={this.handleReportDeltaSelect} @@ -184,14 +184,14 @@ const reportsReloadInterval = ({entities = []}) => ? USE_DEFAULT_RELOAD_INTERVAL_ACTIVE : USE_DEFAULT_RELOAD_INTERVAL; -const FALLBACK_REPORT_LIST_FILTER = Filter.fromString( +const FALLBACK_AUDIT_REPORT_LIST_FILTER = Filter.fromString( 'sort-reverse=date first=1', ); export default compose( withGmp, withEntitiesContainer('auditreport', { - fallbackFilter: FALLBACK_REPORT_LIST_FILTER, + fallbackFilter: FALLBACK_AUDIT_REPORT_LIST_FILTER, entitiesSelector, loadEntities, reloadInterval: reportsReloadInterval, diff --git a/src/web/pages/start/dashboard.jsx b/src/web/pages/start/dashboard.jsx index 2b50b2e12a..ec07fc07d2 100644 --- a/src/web/pages/start/dashboard.jsx +++ b/src/web/pages/start/dashboard.jsx @@ -27,11 +27,12 @@ import {CVES_DISPLAYS} from '../cves/dashboard'; import {CPES_DISPLAYS} from '../cpes/dashboard'; import {DFNCERT_DISPLAYS} from '../dfncert/dashboard'; import {TICKETS_DISPLAYS} from '../tickets/dashboard'; +import {AUDIT_REPORTS_DISPLAYS} from '../reports/auditdashboard'; import {DEFAULT_DISPLAYS} from './newdashboarddialog'; const ALL_DISPLAYS = [ - // TODO: Add audit reports display + ...AUDIT_REPORTS_DISPLAYS, ...TASKS_DISPLAYS, ...REPORTS_DISPLAYS, ...RESULTS_DISPLAYS, From 36892ffd16f106ff4ce2e0677094de6f3a685058 Mon Sep 17 00:00:00 2001 From: Ahmed Abdelsalam Date: Fri, 2 Feb 2024 11:48:36 +0100 Subject: [PATCH 03/37] code cleanup and refactoring --- src/gmp/capabilities/capabilities.js | 17 +++++++---- src/gmp/models/auditreport.js | 4 +-- src/web/components/bar/compliancebar.js | 8 +++--- src/web/components/bar/menubar.jsx | 2 +- .../components/dashboard/display/utils.jsx | 6 ++++ src/web/components/label/compliancestate.js | 25 ++++++++++++++--- src/web/entities/filterprovider.jsx | 1 + src/web/pages/audits/table.jsx | 2 +- .../reports/auditdashboard/statusdisplay.js | 28 +++++++------------ src/web/pages/reports/auditdetailscontent.js | 2 +- src/web/pages/reports/auditreportslistpage.js | 4 +-- src/web/pages/tags/component.jsx | 1 + src/web/pages/tasks/row.jsx | 2 +- src/web/utils/theme.jsx | 5 ++++ 14 files changed, 67 insertions(+), 40 deletions(-) diff --git a/src/gmp/capabilities/capabilities.js b/src/gmp/capabilities/capabilities.js index 9f08dd8bed..2afd4898ba 100644 --- a/src/gmp/capabilities/capabilities.js +++ b/src/gmp/capabilities/capabilities.js @@ -9,10 +9,8 @@ import {pluralizeType} from 'gmp/utils/entitytype'; import {parseBoolean} from 'gmp/parser'; const types = { - audit: 'task', - audits: 'task', - auditreport: 'report', - auditreports: 'report', + auditreport: 'audit_report', + auditreports: 'audit_reports', host: 'asset', hosts: 'asset', os: 'asset', @@ -44,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] ? subtypes[type] : type; }; class Capabilities { diff --git a/src/gmp/models/auditreport.js b/src/gmp/models/auditreport.js index 1957bd40b5..f666f75496 100644 --- a/src/gmp/models/auditreport.js +++ b/src/gmp/models/auditreport.js @@ -25,7 +25,7 @@ import Model, {parseModelFromElement} from 'gmp/model'; import AuditReportReport from './report/auditreport'; -const COMPLIANCE_STATE_TRANSLATIONS = { +export const COMPLIANCE_STATES = { yes: _l('Yes'), no: _l('No'), incomplete: _l('Incomplete'), @@ -34,7 +34,7 @@ const COMPLIANCE_STATE_TRANSLATIONS = { /* eslint-disable quote-props */ export const getTranslatableReportCompliance = compliance => - `${COMPLIANCE_STATE_TRANSLATIONS[compliance]}`; + `${COMPLIANCE_STATES[compliance]}`; class AuditReport extends Model { static entityType = 'auditreport'; diff --git a/src/web/components/bar/compliancebar.js b/src/web/components/bar/compliancebar.js index 3347de6d92..2c08f44707 100644 --- a/src/web/components/bar/compliancebar.js +++ b/src/web/components/bar/compliancebar.js @@ -30,13 +30,13 @@ const ComplianceBar = ({compliance, toolTip}) => { let background; if (compliance === 'no') { - background = Theme.errorRed; + background = Theme.compliance_no; } else if (compliance === 'incomplete') { - background = Theme.severityWarnYellow; + background = Theme.compliance_incomplete; } else if (compliance === 'yes') { - background = Theme.statusRunGreen; + background = Theme.compliance_yes; } else { - background = 'gray'; + background = Theme.compliance_undefined; } const toolTipText = isDefined(toolTip) ? toolTip : title; diff --git a/src/web/components/bar/menubar.jsx b/src/web/components/bar/menubar.jsx index 053a6dd4ec..830865158d 100644 --- a/src/web/components/bar/menubar.jsx +++ b/src/web/components/bar/menubar.jsx @@ -170,7 +170,7 @@ const MenuBar = ({isLoggedIn, capabilities}) => { )} {capabilities.mayAccess('audits') && ( )} 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.js b/src/web/components/label/compliancestate.js index b144b790e1..9e9ba5badf 100644 --- a/src/web/components/label/compliancestate.js +++ b/src/web/components/label/compliancestate.js @@ -21,6 +21,7 @@ import styled from 'styled-components'; import _ from 'gmp/locale'; import {styledExcludeProps} from 'web/utils/styledConfig'; +import Theme from 'web/utils/theme.js'; const Label = styledExcludeProps(styled.div, [ 'backgroundColor', @@ -41,7 +42,11 @@ const Label = styledExcludeProps(styled.div, [ const YesLabel = props => { return ( -
); -ReportCompianceLoader.propTypes = loaderPropTypes; - -// vim: set ts=2 sw=2 tw=80: +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 index 1d4c45b96e..7305781531 100644 --- a/src/web/pages/reports/auditdashboard/statusdisplay.jsx +++ b/src/web/pages/reports/auditdashboard/statusdisplay.jsx @@ -86,6 +86,4 @@ registerDisplay( { title: _l('Table: Audit Reports by Compliance'), }, -); - -// vim: set ts=2 sw=2 tw=80: +); \ No newline at end of file diff --git a/src/web/pages/reports/auditdeltadetailspage.jsx b/src/web/pages/reports/auditdeltadetailspage.jsx index 67f90d4029..4916895a23 100644 --- a/src/web/pages/reports/auditdeltadetailspage.jsx +++ b/src/web/pages/reports/auditdeltadetailspage.jsx @@ -549,6 +549,4 @@ DeltaAuditReportDetailsWrapper.propTypes = { export default compose( withDialogNotification, withDownload, -)(DeltaAuditReportDetailsWrapper); - -// vim: set ts=2 sw=2 tw=80: +)(DeltaAuditReportDetailsWrapper); \ No newline at end of file diff --git a/src/web/pages/reports/auditdetailscontent.jsx b/src/web/pages/reports/auditdetailscontent.jsx index 7f69f07a9a..c1ed725c89 100644 --- a/src/web/pages/reports/auditdetailscontent.jsx +++ b/src/web/pages/reports/auditdetailscontent.jsx @@ -445,6 +445,4 @@ PageContent.propTypes = { onTlsCertificateDownloadClick: PropTypes.func.isRequired, }; -export default PageContent; - -// vim: set ts=2 sw=2 tw=80: +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 index 088828a4a4..db953b0f72 100644 --- a/src/web/pages/reports/auditdetailspage.jsx +++ b/src/web/pages/reports/auditdetailspage.jsx @@ -689,6 +689,4 @@ const ReportDetailsWrapper = props => { export default compose( withDialogNotification, withDownload, -)(ReportDetailsWrapper); - -// vim: set ts=2 sw=2 tw=80: +)(ReportDetailsWrapper); \ No newline at end of file diff --git a/src/web/pages/reports/auditfilterdialog.jsx b/src/web/pages/reports/auditfilterdialog.jsx index a8f3ddba27..02b05b1883 100644 --- a/src/web/pages/reports/auditfilterdialog.jsx +++ b/src/web/pages/reports/auditfilterdialog.jsx @@ -5,13 +5,12 @@ import React from 'react'; -import {_l, _} from 'gmp/locale/lang'; - import Layout from 'web/components/layout/layout'; -import compose from 'web/utils/compose'; import useCapabilities from 'web/utils/useCapabilities'; +import useTranslation from 'web/hooks/useTranslation'; + /* eslint-disable max-len */ import CreateNamedFilterGroup from 'web/components/powerfilter/createnamedfiltergroup'; @@ -27,37 +26,6 @@ import FilterSearchGroup from 'web/components/powerfilter/filtersearchgroup'; /* eslint-enable */ -const SORT_FIELDS = [ - { - name: 'date', - displayName: _l('Date'), - }, - { - name: 'status', - displayName: _l('Status'), - }, - { - name: 'task', - displayName: _l('Task'), - }, - { - name: 'compliant', - displayName: _l('Compliant'), - }, - { - name: 'compliance_yes', - displayName: _l('Compliance: Yes'), - }, - { - name: 'compliance_no', - displayName: _l('Compliance: No'), - }, - { - name: 'compliance_incomplete', - displayName: _l('Compliance: Incomplete'), - }, -]; - const AuditReportFilterDialogComponent = ({ filter, filterName, @@ -71,11 +39,41 @@ const AuditReportFilterDialogComponent = ({ onSortOrderChange, onValueChange, }) => { + const [_] = useTranslation(); + const capabilities = useCapabilities(); const handleRemoveCompliance = () => onFilterChange(filter.delete('report_compliance_levels')); - - const capabilities = useCapabilities(); - + 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; } @@ -133,6 +131,4 @@ const AuditReportFilterDialogComponent = ({ AuditReportFilterDialogComponent.propTypes = FilterDialogPropTypes; -export default compose(withFilterDialog())(AuditReportFilterDialogComponent); - -// vim: set ts=2 sw=2 tw=80: +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 index 52d8e57c47..28334e9b3d 100644 --- a/src/web/pages/reports/auditreportrow.jsx +++ b/src/web/pages/reports/auditreportrow.jsx @@ -144,6 +144,4 @@ AuditRow.propTypes = { links: PropTypes.bool, }; -export default AuditRow; - -// vim: set ts=2 sw=2 tw=80: +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 index e399e3546a..b12d3739a0 100644 --- a/src/web/pages/reports/auditreportslistpage.jsx +++ b/src/web/pages/reports/auditreportslistpage.jsx @@ -37,7 +37,6 @@ import { selector as entitiesSelector, } from 'web/store/entities/auditreports'; -import compose from 'web/utils/compose'; import PropTypes from 'web/utils/proptypes'; import AuditFilterDialog from './auditfilterdialog'; @@ -162,13 +161,9 @@ const FALLBACK_AUDIT_REPORT_LIST_FILTER = Filter.fromString( 'report_compliance_levels=yniu sort-reverse=date first=1', ); -export default compose( - withEntitiesContainer('auditreport', { +export default withEntitiesContainer('auditreport', { fallbackFilter: FALLBACK_AUDIT_REPORT_LIST_FILTER, entitiesSelector, loadEntities, reloadInterval: reportsReloadInterval, - }), -)(AuditReportsPage); - -// vim: set ts=2 sw=2 tw=80: + })(AuditReportsPage); \ No newline at end of file diff --git a/src/web/pages/reports/auditreportstable.jsx b/src/web/pages/reports/auditreportstable.jsx index 3830594a8f..4608cf114f 100644 --- a/src/web/pages/reports/auditreportstable.jsx +++ b/src/web/pages/reports/auditreportstable.jsx @@ -5,7 +5,8 @@ import React from 'react'; -import {_, _l} from 'gmp/locale/lang'; +import {_l} from 'gmp/locale/lang'; +import useTranslation from 'web/hooks/useTranslation'; import {isDefined} from 'gmp/utils/identity'; @@ -29,6 +30,7 @@ const Header = ({ currentSortDir, onSortChange, }) => { + const [_] = useTranslation(); return ( @@ -125,6 +127,4 @@ export default createEntitiesTable({ footer: Footer, row: AuditReportRow, toggleDetailsIcon: false, -}); - -// vim: set ts=2 sw=2 tw=80: +}); \ No newline at end of file diff --git a/src/web/pages/reports/details/__tests__/resultstab.jsx b/src/web/pages/reports/details/__tests__/resultstab.jsx index e648b4dec9..736267d6f8 100644 --- a/src/web/pages/reports/details/__tests__/resultstab.jsx +++ b/src/web/pages/reports/details/__tests__/resultstab.jsx @@ -5,7 +5,6 @@ import {describe, test, expect, testing} from '@gsa/testing'; import React from 'react'; -import {setLocale} from 'gmp/locale/lang'; import Filter from 'gmp/models/filter'; @@ -20,8 +19,6 @@ import {entitiesLoadingActions} from 'web/store/entities/results'; import ResultsTab from '../resultstab'; -setLocale('en'); - const reloadInterval = 1; const manualUrl = 'test/'; diff --git a/src/web/pages/reports/details/resultstab.jsx b/src/web/pages/reports/details/resultstab.jsx index 420961f883..0acc2e8d24 100644 --- a/src/web/pages/reports/details/resultstab.jsx +++ b/src/web/pages/reports/details/resultstab.jsx @@ -378,6 +378,4 @@ const mapDispatchToProps = (dispatch, {reportId, gmp}) => { 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/store/entities/auditreports.js b/src/web/store/entities/auditreports.js index 3b305c09f9..95febbe2e3 100644 --- a/src/web/store/entities/auditreports.js +++ b/src/web/store/entities/auditreports.js @@ -60,5 +60,3 @@ export { reportsSelector as selector, entitiesActions, }; - -// vim: set ts=2 sw=2 tw=80: From 254dcc25713fc0a90a87fc12ea9eb0270f8c8bf0 Mon Sep 17 00:00:00 2001 From: Ahmed Abdelsalam Date: Wed, 3 Jul 2024 16:33:47 +0200 Subject: [PATCH 31/37] Fixing import path for hooks --- allowedSnakeCase.cjs | 3 +++ src/web/pages/reports/auditdeltadetailspage.jsx | 2 +- src/web/pages/reports/auditdetailscontent.jsx | 2 +- src/web/pages/reports/auditdetailspage.jsx | 2 +- src/web/pages/reports/auditfilterdialog.jsx | 2 +- src/web/pages/results/__tests__/row.jsx | 2 +- 6 files changed, 8 insertions(+), 5 deletions(-) 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/src/web/pages/reports/auditdeltadetailspage.jsx b/src/web/pages/reports/auditdeltadetailspage.jsx index 4916895a23..66f78be296 100644 --- a/src/web/pages/reports/auditdeltadetailspage.jsx +++ b/src/web/pages/reports/auditdeltadetailspage.jsx @@ -63,7 +63,7 @@ import { import compose from 'web/utils/compose'; import {generateFilename} from 'web/utils/render'; import PropTypes from 'web/utils/proptypes'; -import useGmp from 'web/utils/useGmp'; +import useGmp from 'web/hooks/useGmp'; import TargetComponent from '../targets/component'; diff --git a/src/web/pages/reports/auditdetailscontent.jsx b/src/web/pages/reports/auditdetailscontent.jsx index c1ed725c89..9006567e62 100644 --- a/src/web/pages/reports/auditdetailscontent.jsx +++ b/src/web/pages/reports/auditdetailscontent.jsx @@ -44,7 +44,7 @@ import EntityInfo from 'web/entity/info'; import EntityTags from 'web/entity/tags'; import PropTypes from 'web/utils/proptypes'; -import useGmp from 'web/utils/useGmp'; +import useGmp from 'web/hooks/useGmp'; import ErrorsTab from './details/errorstab'; import HostsTab from './details/hoststab'; diff --git a/src/web/pages/reports/auditdetailspage.jsx b/src/web/pages/reports/auditdetailspage.jsx index db953b0f72..1473f15ea7 100644 --- a/src/web/pages/reports/auditdetailspage.jsx +++ b/src/web/pages/reports/auditdetailspage.jsx @@ -74,7 +74,7 @@ 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/utils/useGmp'; +import useGmp from 'web/hooks/useGmp'; const log = logger.getLogger('web.pages.auditreport.detailspage'); diff --git a/src/web/pages/reports/auditfilterdialog.jsx b/src/web/pages/reports/auditfilterdialog.jsx index 02b05b1883..aa0db7183b 100644 --- a/src/web/pages/reports/auditfilterdialog.jsx +++ b/src/web/pages/reports/auditfilterdialog.jsx @@ -7,7 +7,7 @@ import React from 'react'; import Layout from 'web/components/layout/layout'; -import useCapabilities from 'web/utils/useCapabilities'; +import useCapabilities from 'web/hooks/useCapabilities'; import useTranslation from 'web/hooks/useTranslation'; diff --git a/src/web/pages/results/__tests__/row.jsx b/src/web/pages/results/__tests__/row.jsx index 217db7549f..b8f6b103cf 100644 --- a/src/web/pages/results/__tests__/row.jsx +++ b/src/web/pages/results/__tests__/row.jsx @@ -144,7 +144,7 @@ describe('Delta reports V2 with same severity, qod and hostname', () => { }); describe('Audit reports with compliance', () => { - const {render} = rendererWith(); + const {render} = rendererWith({gmp, store: true}); test('should render Audit report with compliance yes', () => { const entity = Result.fromElement({ From 58f1ab75bdf232afc42fe32ba239ef69ac7e6e49 Mon Sep 17 00:00:00 2001 From: Ahmed Abdelsalam Date: Thu, 4 Jul 2024 14:29:43 +0200 Subject: [PATCH 32/37] Apply review comment --- src/gmp/models/report/os.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/gmp/models/report/os.js b/src/gmp/models/report/os.js index 07d752b989..ebc6188e3d 100644 --- a/src/gmp/models/report/os.js +++ b/src/gmp/models/report/os.js @@ -27,13 +27,13 @@ class OperatingSystem { if (!(host.ip in this.hosts.complianceByIp)) { this.hosts.complianceByIp[host.ip] = compliance; } - const isNoInCompliance = Object.values(this.hosts.complianceByIp).some( - value => value === 'no', + const complianceByIpValues = Object.values(this.hosts.complianceByIp); + + const isNoInCompliance = complianceByIpValues.some(value => value === 'no'); + const isIncompleteInCompliance = complianceByIpValues.some( + value => value === 'incomplete', ); - const isIncompleteInCompliance = Object.values( - this.hosts.complianceByIp, - ).some(value => value === 'incomplete'); - const isYesInCompliance = Object.values(this.hosts.complianceByIp).some( + const isYesInCompliance = complianceByIpValues.some( value => value === 'yes', ); From d790468bc75513c4907b7f4f9eafce08eaf4582b Mon Sep 17 00:00:00 2001 From: Ahmed Abdelsalam Date: Tue, 9 Jul 2024 16:27:24 +0200 Subject: [PATCH 33/37] Allow applying report configs to audit reports. --- src/web/pages/reports/auditdeltadetailspage.jsx | 15 ++++++++++++++- src/web/pages/reports/auditdetailspage.jsx | 15 ++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/web/pages/reports/auditdeltadetailspage.jsx b/src/web/pages/reports/auditdeltadetailspage.jsx index 66f78be296..2a65928c1f 100644 --- a/src/web/pages/reports/auditdeltadetailspage.jsx +++ b/src/web/pages/reports/auditdeltadetailspage.jsx @@ -14,7 +14,11 @@ import useTranslation from 'web/hooks/useTranslation'; import logger from 'gmp/log'; -import Filter, {RESET_FILTER, RESULTS_FILTER_FILTER} from 'gmp/models/filter'; +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'; @@ -41,6 +45,11 @@ import { 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'; @@ -113,7 +122,9 @@ const DeltaAuditReportDetails = props => { 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, @@ -143,6 +154,7 @@ const DeltaAuditReportDetails = props => { 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 }, []); @@ -440,6 +452,7 @@ const DeltaAuditReportDetails = props => { includeNotes={reportComposerDefaults.includeNotes} includeOverrides={reportComposerDefaults.includeOverrides} reportFormats={reportFormats} + reportConfigs={reportConfigs} storeAsDefault={storeAsDefault} onClose={handleCloseDownloadReportDialog} onSave={handleReportDownload} diff --git a/src/web/pages/reports/auditdetailspage.jsx b/src/web/pages/reports/auditdetailspage.jsx index 1473f15ea7..b0585d6ffa 100644 --- a/src/web/pages/reports/auditdetailspage.jsx +++ b/src/web/pages/reports/auditdetailspage.jsx @@ -14,7 +14,11 @@ import useTranslation from 'web/hooks/useTranslation'; import logger from 'gmp/log'; -import Filter, {RESET_FILTER, RESULTS_FILTER_FILTER} from 'gmp/models/filter'; +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'; @@ -43,6 +47,11 @@ import { 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'; @@ -164,7 +173,9 @@ const ReportDetails = props => { ); 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'), @@ -177,6 +188,7 @@ const ReportDetails = props => { 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)) { @@ -565,6 +577,7 @@ const ReportDetails = props => { includeNotes={reportComposerDefaults.includeNotes} includeOverrides={reportComposerDefaults.includeOverrides} reportFormats={reportFormats} + reportConfigs={reportConfigs} showThresholdMessage={showThresholdMessage} storeAsDefault={storeAsDefault} threshold={threshold} From 932ac979b3d071e701db4756944d5f75e1974df5 Mon Sep 17 00:00:00 2001 From: Ahmed Abdelsalam Date: Tue, 9 Jul 2024 17:09:09 +0200 Subject: [PATCH 34/37] Add feature toggle for compliance reports. --- src/web/components/bar/menubar.jsx | 97 +++++++++---------- src/web/pages/filters/component.jsx | 10 +- src/web/pages/start/dashboard.jsx | 19 ++-- src/web/pages/tags/component.jsx | 9 +- src/web/pages/usersettings/filterpart.jsx | 30 +++--- .../pages/usersettings/usersettingspage.jsx | 18 ++-- 6 files changed, 104 insertions(+), 79 deletions(-) diff --git a/src/web/components/bar/menubar.jsx b/src/web/components/bar/menubar.jsx index 830865158d..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,33 +144,34 @@ 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') && ( )} - {capabilities.mayAccess('audits') && ( + {caps.featureEnabled('COMPLIANCE_REPORTS') && + caps.mayAccess('audits') && ( { )} - {capabilities.mayAccess('info') && ( + {caps.mayAccess('info') && ( @@ -188,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') && ( )} @@ -232,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') && ( )} @@ -284,8 +287,6 @@ const MenuBar = ({isLoggedIn, capabilities}) => { }; MenuBar.propTypes = { - capabilities: PropTypes.capabilities, - gmp: PropTypes.gmp.isRequired, isLoggedIn: PropTypes.bool.isRequired, }; @@ -294,8 +295,6 @@ const mapStateToProps = rootState => ({ }); export default compose( - withCapabilities, - withGmp, connect(mapStateToProps), )(MenuBar); diff --git a/src/web/pages/filters/component.jsx b/src/web/pages/filters/component.jsx index 96f249b421..1330c349c4 100644 --- a/src/web/pages/filters/component.jsx +++ b/src/web/pages/filters/component.jsx @@ -20,7 +20,6 @@ import FilterDialog from 'web/pages/filters/dialog'; const FILTER_OPTIONS = [ ['alert', _l('Alert')], - ['audit_report', _l('Audit Report')], ['credential', _l('Credential')], ['filter', _l('Filter')], ['group', _l('Group')], @@ -76,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/start/dashboard.jsx b/src/web/pages/start/dashboard.jsx index ec07fc07d2..62b1ff4f03 100644 --- a/src/web/pages/start/dashboard.jsx +++ b/src/web/pages/start/dashboard.jsx @@ -28,11 +28,10 @@ import {CPES_DISPLAYS} from '../cpes/dashboard'; import {DFNCERT_DISPLAYS} from '../dfncert/dashboard'; import {TICKETS_DISPLAYS} from '../tickets/dashboard'; import {AUDIT_REPORTS_DISPLAYS} from '../reports/auditdashboard'; - import {DEFAULT_DISPLAYS} from './newdashboarddialog'; +import useCapabilities from 'web/hooks/useCapabilities'; const ALL_DISPLAYS = [ - ...AUDIT_REPORTS_DISPLAYS, ...TASKS_DISPLAYS, ...REPORTS_DISPLAYS, ...RESULTS_DISPLAYS, @@ -58,14 +57,22 @@ const StartDashboard = ({ onNewDisplay, onResetDashboard, ...props -}) => ( +}) => { + 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 714ab2c5f3..fa868aaa99 100644 --- a/src/web/pages/tags/component.jsx +++ b/src/web/pages/tags/component.jsx @@ -30,7 +30,6 @@ export const MAX_RESOURCES = 40; // concerns listing in "Assigned Resources" tab const TYPES = [ 'alert', 'audit', - 'auditreport', 'host', 'operatingsystem', 'cpe', @@ -109,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/usersettings/filterpart.jsx b/src/web/pages/usersettings/filterpart.jsx index 40e3e14d68..0f6a95f653 100644 --- a/src/web/pages/usersettings/filterpart.jsx +++ b/src/web/pages/usersettings/filterpart.jsx @@ -13,7 +13,7 @@ import Select from 'web/components/form/select'; import PropTypes from 'web/utils/proptypes'; import {renderSelectItems, UNSET_VALUE} from 'web/utils/render'; -import withCapabilities from 'web/utils/withCapabilities'; +import useCapabilities from 'web/hooks/useCapabilities'; const filterFilters = (filters, type) => filters.filter(filter => filter.filter_type === type); @@ -52,6 +52,7 @@ const FilterPart = ({ filters = [], onChange, }) => { + const caps = useCapabilities(); return ( @@ -65,17 +66,20 @@ const FilterPart = ({ onChange={onChange} /> - - + + ) + }