Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Heimdall
# Heimdall

[![Run E2E Backend + Frontend Tests](https://github.com/mitre/heimdall2/workflows/Run%20E2E%20Backend%20+%20Frontend%20Tests/badge.svg)](https://github.com/mitre/heimdall2/actions/workflows/e2e-ui-tests.yml)
[![Run Frontend Tests](https://github.com/mitre/heimdall2/workflows/Run%20Frontend%20Tests/badge.svg)](https://github.com/mitre/heimdall2/actions/workflows/frontend-tests.yml)
Expand Down
22 changes: 22 additions & 0 deletions apps/frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
import Footer from '@/components/global/Footer.vue';
import Snackbar from '@/components/global/Snackbar.vue';
import Spinner from '@/components/global/Spinner.vue';
import {
InspecDataModule,
UNSAVED_CHANGES_MESSAGE
} from '@/store/data_store';
import Vue from 'vue';
import Component from 'vue-class-component';
import {ServerModule} from './store/server';
Expand All @@ -33,6 +37,24 @@ import {ServerModule} from './store/server';
}
})
export default class App extends Vue {
mounted() {
window.addEventListener('beforeunload', this.confirmBeforeUnload);
}

beforeDestroy() {
window.removeEventListener('beforeunload', this.confirmBeforeUnload);
}

confirmBeforeUnload(event: BeforeUnloadEvent) {
if (!InspecDataModule.hasUnsavedFiles) {
return;
}

event.preventDefault();
event.returnValue = UNSAVED_CHANGES_MESSAGE;
return UNSAVED_CHANGES_MESSAGE;
}

get classificationStyle() {
return {
background: ServerModule.classificationBannerColor,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
caveat ||
justification ||
rationale ||
comments ||
errorMessage
"
>
Expand All @@ -38,10 +37,20 @@
<br />
</span>
<span v-if="rationale">Rationale: {{ rationale }}<br /></span>
<span v-if="comments">Comments: {{ comments }}<br /></span>
<v-divider />
<br />
</div>
<h3>Comments:</h3>
<v-textarea
v-model="localComments"
auto-grow
class="mb-4"
dense
hide-details
outlined
rows="3"
@input="updateComments"
/>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="sanitize_html(main_desc)" />
</div>
Expand All @@ -61,7 +70,17 @@
<v-row :key="'tab' + index" :class="zebra(index)">
<v-col cols="12" :class="detail.class">
<h3>{{ detail.name }}:</h3>
<h4>
<v-textarea
v-if="detail.editable"
v-model="localComments"
auto-grow
dense
hide-details
outlined
rows="3"
@input="updateComments"
/>
<h4 v-else>
<!-- eslint-disable vue/no-v-html -->
<pre class="mono" v-html="sanitize_html(detail.value)" />
<!-- eslint-enable vue/no-v-html -->
Expand Down Expand Up @@ -91,6 +110,7 @@
<script lang="ts">
import ControlRowCol from '@/components/cards/controltable/ControlRowCol.vue';
import HtmlSanitizeMixin from '@/mixins/HtmlSanitizeMixin';
import {InspecDataModule} from '@/store/data_store';
import {ContextualizedControl} from 'inspecjs';
import * as _ from 'lodash';
//TODO: add line numbers
Expand All @@ -108,8 +128,16 @@ interface Detail {
name: string;
value: string;
class?: string;
editable?: boolean;
}

type EditableDetail = {
value: string;
editable: true;
};

type DetailValue = string | number | null | undefined | EditableDetail;

@Component({
components: {
ControlRowCol,
Expand All @@ -122,6 +150,7 @@ export default class ControlRowDetails extends mixins(HtmlSanitizeMixin) {
readonly control!: ContextualizedControl;

localTab = this.tab;
localComments = this.comments;

@Watch('tab')
onTabChanged(newTab?: string, _oldVal?: string) {
Expand Down Expand Up @@ -195,8 +224,13 @@ export default class ControlRowDetails extends mixins(HtmlSanitizeMixin) {
return this.control.hdf.descriptions.justification;
}

get comments(): string | undefined {
return this.control.hdf.descriptions.comments;
get comments(): string {
return this.control.hdf.descriptions.comments ?? '';
}

updateComments(comments: string) {
this.localComments = comments;
InspecDataModule.updateControlComments({comments, control: this.control});
}

get errorMessage(): string {
Expand All @@ -206,13 +240,17 @@ export default class ControlRowDetails extends mixins(HtmlSanitizeMixin) {
}

get details(): Detail[] {
const detailsMap = new Map();
const detailsMap = new Map<string, DetailValue>();

detailsMap.set('Control', this.control.data.id);
detailsMap.set('Title', this.control.data.title);
detailsMap.set('Caveat', this.control.hdf.descriptions.caveat);
detailsMap.set('Desc', this.control.data.desc);
detailsMap.set('Rationale', this.control.hdf.descriptions.rationale);
detailsMap.set('Comments', {
editable: true,
value: this.localComments
});
// default to showing severity tag, otherwise show the computed severity (based on impact or severityoverride)
detailsMap.set(
'Severity',
Expand Down Expand Up @@ -270,7 +308,7 @@ export default class ControlRowDetails extends mixins(HtmlSanitizeMixin) {
});

for (const prop in this.control.hdf.descriptions) {
if (!detailsMap.has(_.capitalize(prop))) {
if (prop !== 'comments' && !detailsMap.has(_.capitalize(prop))) {
detailsMap.set(_.startCase(prop), this.control.hdf.descriptions[prop]);
}
}
Expand All @@ -285,9 +323,15 @@ export default class ControlRowDetails extends mixins(HtmlSanitizeMixin) {
);
}

return Array.from(detailsMap, ([name, value]) => ({name, value})).filter(
(v) => v.value !== undefined
);
return Array.from(detailsMap, ([name, value]) => ({name, value}))
.filter((detail) => detail.value !== undefined)
.map((detail): Detail => {
const {name, value} = detail;
if (typeof value === 'object' && value !== null && 'value' in value) {
return {editable: value.editable, name, value: value.value};
}
return {name, value: value === null ? '' : String(value)};
});
}

//for zebra background
Expand Down
8 changes: 6 additions & 2 deletions apps/frontend/src/components/global/ExportCKLModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,7 @@
this.originalProfileTitle.set(originalTitleIndex, name);
}
// Get the name value up to the index, replace dashes with spaces
newName = name.substring(0, index).split('-').join(' ');

Check warning on line 623 in apps/frontend/src/components/global/ExportCKLModal.vue

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `String#replaceAll()` over `String#split().join()`.

See more on https://sonarcloud.io/project/issues?id=mitre_heimdall2&issues=AZ7wLa0bxHPsQcQXshUd&open=AZ7wLa0bxHPsQcQXshUd&pullRequest=8278
// Convert the first letter of each word into uppercase
newName = newName.replaceAll(/^\w|[A-Z]|\b\w/gv, function (word) {
return word.toUpperCase();
Expand Down Expand Up @@ -738,8 +738,12 @@
});
}
}
saveSingleOrMultipleFiles(fileData, 'ckl');
this.closeModal();
saveSingleOrMultipleFiles(fileData, 'ckl').then(() => {
InspecDataModule.markFilesSaved(
this.selected.map((file) => file.uniqueId)
);
this.closeModal();
});
}

validateInputMetadata(metadata: ChecklistMetadata): Result<true, string> {
Expand Down
6 changes: 5 additions & 1 deletion apps/frontend/src/components/global/ExportJson.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<script lang="ts">
import IconLinkItem from '@/components/global/sidebaritems/IconLinkItem.vue';
import {FilteredDataModule} from '@/store/data_filters';
import {InspecDataModule} from '@/store/data_store';
import {saveSingleOrMultipleFiles} from '@/utilities/export_util';
import Vue from 'vue';
import Component from 'vue-class-component';
Expand Down Expand Up @@ -51,8 +52,11 @@ export default class ExportJSON extends Vue {

//exports .zip of jsons if multiple are selected, if one is selected it will export that .json file
export_json() {
const ids = FilteredDataModule.selected_file_ids;
const files = this.populate_files();
saveSingleOrMultipleFiles(files, 'json');
saveSingleOrMultipleFiles(files, 'json').then(() => {
InspecDataModule.markFilesSaved(ids);
});
}

cleanup_filename(filename: string): string {
Expand Down
32 changes: 29 additions & 3 deletions apps/frontend/src/components/global/Sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import DropdownContent from '@/components/global/sidebaritems/DropdownContent.vu
import {Trinary} from '@/enums/Trinary';
import RouteMixin from '@/mixins/RouteMixin';
import {FilteredDataModule} from '@/store/data_filters';
import {InspecDataModule} from '@/store/data_store';
import {InspecDataModule, UNSAVED_CHANGES_MESSAGE} from '@/store/data_store';
import {EvaluationFile, ProfileFile} from '@/store/report_intake';
import Component, {mixins} from 'vue-class-component';
import {Prop} from 'vue-property-decorator';
Expand Down Expand Up @@ -141,7 +141,15 @@ export default class Sidebar extends mixins(RouteMixin) {

removeSelectedEvaluations(): void {
const selectedFiles = FilteredDataModule.selected_evaluation_ids;
selectedFiles.forEach((fileId) => {
const files = this.visible_evaluation_files.filter((file) =>
selectedFiles.includes(file.uniqueId)
);
if (!this.confirmRemoveUnsavedFiles(files)) {
return;
}

files.forEach((file) => {
const fileId = file.uniqueId;
EvaluationModule.removeEvaluation(fileId);
InspecDataModule.removeFile(fileId);
// Remove any database files that may have been in the URL
Expand All @@ -153,7 +161,15 @@ export default class Sidebar extends mixins(RouteMixin) {

removeSelectedProfiles(): void {
const selectedFiles = FilteredDataModule.selected_profile_ids;
selectedFiles.forEach((fileId) => {
const files = this.visible_profile_files.filter((file) =>
selectedFiles.includes(file.uniqueId)
);
if (!this.confirmRemoveUnsavedFiles(files)) {
return;
}

files.forEach((file) => {
const fileId = file.uniqueId;
EvaluationModule.removeEvaluation(fileId);
InspecDataModule.removeFile(fileId);
// Remove any database files that may have been in the URL
Expand All @@ -162,6 +178,16 @@ export default class Sidebar extends mixins(RouteMixin) {
this.navigateWithNoErrors(`/${this.current_route}`);
});
}

confirmRemoveUnsavedFiles(files: (EvaluationFile | ProfileFile)[]): boolean {
if (!files.some((file) => file.hasUnsavedChanges)) {
return true;
}

return globalThis.confirm(
`${UNSAVED_CHANGES_MESSAGE}\n\nRemove them anyway?`
);
}
}
</script>

Expand Down
Loading
Loading