Skip to content

Commit 0103c18

Browse files
authored
Merge pull request #5595 from mitre/htmlFilteredReports
HTML Reports based on filter data
2 parents ffed7e9 + bccfbbc commit 0103c18

9 files changed

Lines changed: 204 additions & 65 deletions

File tree

apps/frontend/public/static/export/template.html

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -110,28 +110,8 @@
110110
{{/showResultSets}}
111111
</nav>
112112

113-
<!-- Profile info card -->
114-
<div class="px-4 py-6" id="profileInfo">
115-
<div
116-
class="min-w-full rounded-xl border border-gray-300 bg-gray-100 px-4 py-2 print:border-black print:bg-white"
117-
>
118-
<h5 class="mb-2 text-xl font-bold">Profile Info</h5>
119-
<div class="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
120-
<!-- Generate an info report for every profile -->
121-
{{#files}}
122-
<div class="mx-2 my-2 break-words">
123-
<div><strong>Filename:</strong> {{filename}}</div>
124-
<div><strong>Tool Version:</strong> {{toolVersion}}</div>
125-
<div><strong>Platform:</strong> {{platform}}</div>
126-
<div><strong>Duration:</strong> {{duration}}</div>
127-
</div>
128-
{{/files}}
129-
</div>
130-
</div>
131-
</div>
132-
133113
<!-- Profile status card -->
134-
<div class="px-4 pb-6">
114+
<div class="px-4 py-6">
135115
<div
136116
class="min-w-full rounded-xl border border-gray-300 bg-gray-100 px-4 py-2 print:border-black print:bg-white"
137117
>
@@ -217,6 +197,26 @@ <h5 class="mb-2 text-xl font-bold">Profile Status</h5>
217197
</div>
218198
</div>
219199

200+
<!-- Profile info card -->
201+
<div class="px-4 pb-6" id="profileInfo">
202+
<div
203+
class="min-w-full rounded-xl border border-gray-300 bg-gray-100 px-4 py-2 print:border-black print:bg-white"
204+
>
205+
<h5 class="mb-2 text-xl font-bold">Profile Info</h5>
206+
<div class="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4">
207+
<!-- Generate an info report for every profile -->
208+
{{#files}}
209+
<div class="mx-2 my-2 break-words">
210+
<div><strong>Filename:</strong> {{filename}}</div>
211+
<div><strong>Tool Version:</strong> {{toolVersion}}</div>
212+
<div><strong>Platform:</strong> {{platform}}</div>
213+
<div><strong>Duration:</strong> {{duration}}</div>
214+
</div>
215+
{{/files}}
216+
</div>
217+
</div>
218+
</div>
219+
220220
<div class="overflow-hidden">
221221
<!-- Result section -->
222222
<!-- If empty, then this should be an executive report HTML export type -->

apps/frontend/src/components/global/ExportHTMLModal.vue

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@
3333
<pre class="pt-5" v-text="description" />
3434
</v-col>
3535
</v-row>
36+
<v-row>
37+
<v-col cols="1">
38+
<v-icon color="primary">mdi-information-variant-circle</v-icon>
39+
</v-col>
40+
<v-col>
41+
Both Manager and Administrator Reports can be data intensive, which
42+
can take longer for the browser to render the page.
43+
</v-col>
44+
</v-row>
3645
</v-card-text>
3746
<v-divider />
3847
<v-card-actions>
@@ -57,6 +66,7 @@ import {Filter} from '../../store/data_filters';
5766
import {InspecDataModule} from '../../store/data_store';
5867
import {SnackbarModule} from '../../store/snackbar';
5968
import {FromHDFToHTMLMapper} from '@mitre/hdf-converters';
69+
import {SourcedContextualizedEvaluation} from '../../store/report_intake';
6070
6171
// All selectable export types for an HTML export
6272
enum FileExportTypes {
@@ -118,23 +128,38 @@ export default class ExportHTMLModal extends Vue {
118128
return SnackbarModule.failure('No files have been loaded.');
119129
}
120130
131+
document.body.style.cursor = 'wait';
121132
const files = [];
133+
const filteredStatus = this.filter.status!.toString() || '';
134+
const filteredSeverity = this.filter.severity!.toString() || '';
135+
122136
for (const fileId of this.filter.fromFile) {
123137
const file = InspecDataModule.allEvaluationFiles.find(
124138
(f) => f.uniqueId === fileId
125139
);
140+
126141
if (file) {
127142
const data = file.evaluation;
128-
const fileName = file.filename;
129-
const fileID = file.uniqueId;
130-
files.push({data, fileName, fileID});
143+
const filteredControls = this.getFilterControlIds(
144+
data,
145+
filteredStatus,
146+
filteredSeverity
147+
);
148+
149+
// Only show files that have controls that meet
150+
// the status/severity criteria, could be all files
151+
if (filteredControls.length > 0) {
152+
const fileName = file.filename;
153+
const fileID = file.uniqueId;
154+
files.push({data, fileName, fileID, filteredControls});
155+
}
131156
}
132157
}
133-
134158
// Generate and export HTML file
135159
const body = await new FromHDFToHTMLMapper(files, this.exportType).toHTML(
136160
'/static/export/'
137161
);
162+
138163
saveAs(
139164
new Blob([s2ab(body)], {type: 'application/octet-stream'}),
140165
`${this.exportType}_Report_${new Date().toString()}.html`.replace(
@@ -143,7 +168,83 @@ export default class ExportHTMLModal extends Vue {
143168
)
144169
);
145170
171+
document.body.style.cursor = 'default';
146172
this.closeModal();
147173
}
174+
175+
/**
176+
* Generate an array containing the control identification numbers
177+
* (profiles.controls) selected. The Id is extracted for the
178+
* profiles.controls.id, it can be CIS, STIG (V or SV), CWEID, WASCID,
179+
* or any other control Id.
180+
*
181+
* Possible selection permutations are:
182+
* - Status (pass, failed, etc)
183+
* - Severity ( low, medium, etc)
184+
* - Combination of Status and Severity
185+
*/
186+
getFilterControlIds(
187+
data: SourcedContextualizedEvaluation,
188+
status: string,
189+
severity: string
190+
): string[] {
191+
/**
192+
* NOTE: The filterControls array is used to specify what controls
193+
* are selected based on Status and Severity selection.
194+
* If we use the approach of filtering the content from the data object
195+
* (e.g.
196+
* data.data.profiles[0].controls =
197+
* data.data.profiles[0].controls.filter((control) => {
198+
* if (filteredControls.includes(control.id)) {
199+
* return filteredControls.includes(control.id);
200+
* }
201+
* });
202+
* )
203+
* the contextualize object does not get updated and the results
204+
* in data_store get out of sync, there is, when utilizing the
205+
* ".contains" it returns the results object (child of the controls object)
206+
* where the file.evaluations returns the filtered controls.
207+
*/
208+
let filteredControls: string[] = [];
209+
// Both Status and Severity selection
210+
if (status.length > 0 && severity.length > 0) {
211+
data.contains.map((profile) => {
212+
profile.contains.map((result) => {
213+
if (
214+
status?.includes(result.root.hdf.status) &&
215+
severity?.includes(result.root.hdf.severity)
216+
) {
217+
filteredControls.push(result.data.id);
218+
}
219+
});
220+
});
221+
// Status selection
222+
} else if (status.length > 0) {
223+
data.contains.map((profile) => {
224+
profile.contains.map((result) => {
225+
if (status?.includes(result.root.hdf.status)) {
226+
filteredControls.push(result.data.id);
227+
}
228+
});
229+
});
230+
// Severity selection
231+
} else if (severity.length > 0) {
232+
data.contains.map((profile) => {
233+
profile.contains.map((result) => {
234+
if (severity?.includes(result.root.hdf.severity)) {
235+
filteredControls.push(result.data.id);
236+
}
237+
});
238+
});
239+
// No selection
240+
} else {
241+
data.contains.map((profile) => {
242+
profile.contains.map((result) => {
243+
filteredControls.push(result.data.id);
244+
});
245+
});
246+
}
247+
return filteredControls;
248+
}
148249
}
149250
</script>

apps/frontend/src/components/global/upload_tabs/FileReader.vue

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,13 @@
103103
<v-progress-circular
104104
indeterminate
105105
color="#ff5600"
106-
:size="80"
107-
:width="20"
108-
/>
106+
:size="120"
107+
:width="15"
108+
>
109+
<template #default>
110+
<b>{{ percent }}% loaded</b>
111+
</template>
112+
</v-progress-circular>
109113
</div>
110114
</div>
111115
</v-col>
@@ -140,7 +144,7 @@ interface VueFileAgentRecord {
140144
export default class FileReader extends mixins(ServerMixin) {
141145
fileRecords: Array<VueFileAgentRecord> = [];
142146
loading = false;
143-
147+
percent = 0;
144148
isActiveDialog = false;
145149
146150
filesSelected() {
@@ -151,12 +155,18 @@ export default class FileReader extends mixins(ServerMixin) {
151155
152156
/** Callback for our file reader */
153157
commit_files(files: File[]) {
158+
const totalFiles = files.length;
159+
let index = 1;
160+
document.body.style.cursor = 'wait';
154161
Promise.all(
155162
files.map(async (file) => {
156163
try {
157-
return await InspecIntakeModule.loadFile({file});
164+
const fileId = await InspecIntakeModule.loadFile({file});
165+
this.percent = Math.floor((index++ / totalFiles) * 100);
166+
return fileId;
158167
} catch (err) {
159168
SnackbarModule.failure(String(err));
169+
document.body.style.cursor = 'default';
160170
}
161171
})
162172
)
@@ -174,6 +184,8 @@ export default class FileReader extends mixins(ServerMixin) {
174184
})
175185
.finally(() => {
176186
this.loading = false;
187+
this.percent = 0;
188+
document.body.style.cursor = 'default';
177189
});
178190
}
179191

apps/frontend/src/views/Results.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,9 @@ export default class Results extends mixins(RouteMixin, ServerMixin) {
276276
treeFilters: TreeMapState = [];
277277
controlSelection: string | null = null;
278278
279+
gotStatus: boolean = false;
280+
gotSeverity: boolean = false;
281+
279282
/** Model for if all-filtered snackbar should be showing */
280283
filterSnackbar = false;
281284

libs/hdf-converters/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,4 @@
7575
"^.+\\.ts$": "ts-jest"
7676
}
7777
}
78-
}
78+
}

libs/hdf-converters/src/converters-from-hdf/html/reverse-html-mapper.ts

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ type InputData = {
2929
data: ContextualizedEvaluation | string;
3030
fileName: string;
3131
fileID: string;
32+
filteredControls?: string[];
3233
};
3334

3435
type ProcessedData = {
3536
data: ContextualizedEvaluation;
3637
fileName: string;
3738
fileID: string;
39+
filteredControls?: string[];
3840
};
3941

4042
// All selectable export types for an HTML export
@@ -174,7 +176,12 @@ export class FromHDFToHTMLMapper {
174176
}
175177

176178
this.addFiledata(
177-
{data: file.data, fileName: file.fileName, fileID: file.fileID},
179+
{
180+
data: file.data,
181+
fileName: file.fileName,
182+
fileID: file.fileID,
183+
filteredControls: file.filteredControls
184+
},
178185
exportType
179186
);
180187
}
@@ -199,11 +206,24 @@ export class FromHDFToHTMLMapper {
199206

200207
// Pull out results from file
201208
const allResultLevels: ContextualizedControl[] = [];
202-
file.data.contains.map((profile) => {
203-
profile.contains.map((result) => {
204-
allResultLevels.push(result);
209+
if (file.filteredControls === undefined) {
210+
file.data.contains.map((profile) => {
211+
profile.contains.map((result) => {
212+
allResultLevels.push(result);
213+
});
205214
});
206-
});
215+
} else {
216+
file.data.contains.flatMap((profile) => {
217+
profile.contains.flatMap((result) => {
218+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
219+
for (const element of file.filteredControls!) {
220+
if (element === result.data.id) {
221+
allResultLevels.push(result);
222+
}
223+
}
224+
});
225+
});
226+
}
207227

208228
// Begin filling out outpuData object to pass into HTML template
209229
// Set high level generalized profile details
@@ -310,7 +330,7 @@ export class FromHDFToHTMLMapper {
310330
};
311331

312332
// Calculate & set compliance level and color from result statuses
313-
// Set default complaince level and color
333+
// Set default compliance level and color
314334
this.outputData.compliance.level = '0.00%';
315335
this.outputData.compliance.color = 'low';
316336

@@ -324,7 +344,9 @@ export class FromHDFToHTMLMapper {
324344
100
325345
);
326346
// Set compliance level
327-
this.outputData.compliance.level = complianceLevel;
347+
this.outputData.compliance.level = complianceLevel.includes('NaN')
348+
? '0.00%'
349+
: complianceLevel;
328350
// Determine color of compliance level
329351
// High compliance is green, medium is yellow, low is red
330352
this.outputData.compliance.color = translateCompliance(complianceLevel);
@@ -495,7 +517,7 @@ export class FromHDFToHTMLMapper {
495517
return text;
496518
}
497519

498-
// Prompt HTML generation from data pulled from file during constructor intialization
520+
// Prompt HTML generation from data pulled from file during constructor initialization
499521
// Requires path to prompt location of needed files relative to function call location
500522
async toHTML(path: string): Promise<string> {
501523
// Pull export template + styles and create outputData object containing data to fill template with
@@ -517,7 +539,6 @@ export class FromHDFToHTMLMapper {
517539
'//# sourceMappingURL=tw-elements.umd.min.js.map',
518540
''
519541
);
520-
521542
// Render template and return generated HTML file
522543
return Mustache.render(template, this.outputData);
523544
}

0 commit comments

Comments
 (0)