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: 2 additions & 0 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ import { IconViewComponent } from './components/icon-view/icon-view.component';
import { ObjectStatusComponent } from './components/object-status/object-status.component';
import { RecentActivityComponent } from './components/recent-activity/recent-activity.component';
import { NotesEditorComponent } from './components/stix/stix-page-tabs/notes-editor/notes-editor.component';
import { ReleaseReviewDialogComponent } from './components/release-review-dialog/release-review-dialog.component';
import { IdentityPropertyComponent } from './components/stix/identity-property/identity-property.component';

import { CitationEditComponent } from './components/stix/citation-property/citation-edit/citation-edit.component';
Expand Down Expand Up @@ -350,6 +351,7 @@ export function initConfig(appConfigService: AppConfigService) {
OrderedListViewComponent,
OrderedListEditComponent,
NotesEditorComponent,
ReleaseReviewDialogComponent,
ObjectStatusComponent,
RecentActivityComponent,
IdentityPropertyComponent,
Expand Down
3 changes: 2 additions & 1 deletion src/app/classes/release-tracks/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@
}

export type ReleasePreviewSummary =
StandardReleasePreviewSummary | VirtualReleasePreviewSummary;
| StandardReleasePreviewSummary

Check failure on line 143 in src/app/classes/release-tracks/api.ts

View workflow job for this annotation

GitHub Actions / static-checks

Replace `|·StandardReleasePreviewSummary⏎·` with `StandardReleasePreviewSummary`
| VirtualReleasePreviewSummary;

export interface SnapshotBundleHashes {
manifest_id: string;
Expand Down
3 changes: 2 additions & 1 deletion src/app/classes/release-tracks/tiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { WorkflowStatusType } from 'src/app/utils/types';
import { SnapshotTier } from './enums';

export type ReleaseTrackObjectTier =
SnapshotTier.Candidate | SnapshotTier.Staged;
| SnapshotTier.Candidate
| SnapshotTier.Staged;

export type WorkflowRevisionSelector = Date | 'latest';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<section
class="release-review-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="release-review-title">
<header class="release-review-header">
<div>
<h2 id="release-review-title">Review {{ objectName }}</h2>
<p>
Awaiting-review revision compared with the current released member.
<span *ngIf="data.items.length > 1">{{ progressLabel }}</span>
</p>
</div>
<button
mat-icon-button
type="button"
aria-label="Close review"
(click)="cancel()">
<mat-icon aria-hidden="true">close</mat-icon>
</button>
</header>

<main class="release-review-content">
@switch (reviewItem.current.type) {
@case ('relationship') {
<app-relationship-view [config]="config"></app-relationship-view>
}
@case ('intrusion-set') {
<app-group-view [config]="config"></app-group-view>
}
@case ('campaign') {
<app-campaign-view [config]="config"></app-campaign-view>
}
@case ('malware') {
<app-software-view [config]="config"></app-software-view>
}
@case ('tool') {
<app-software-view [config]="config"></app-software-view>
}
@case ('x-mitre-matrix') {
<app-matrix-view [config]="config"></app-matrix-view>
}
@case ('course-of-action') {
<app-mitigation-view [config]="config"></app-mitigation-view>
}
@case ('attack-pattern') {
<app-technique-view [config]="config"></app-technique-view>
}
@case ('x-mitre-data-source') {
<app-data-source-view [config]="config"></app-data-source-view>
}
@case ('x-mitre-data-component') {
<app-data-component-view [config]="config"></app-data-component-view>
}
@case ('x-mitre-asset') {
<app-asset-view [config]="config"></app-asset-view>
}
@case ('x-mitre-tactic') {
<app-tactic-view [config]="config"></app-tactic-view>
}
@case ('x-mitre-collection') {
<app-collection-view [config]="config"></app-collection-view>
}
@case ('x-mitre-detection-strategy') {
<app-detection-strategy-view
[config]="config"></app-detection-strategy-view>
}
@case ('x-mitre-analytic') {
<app-analytic-view [config]="config"></app-analytic-view>
}
}
</main>

<div class="release-review-note" *ngIf="showNote">
<mat-form-field appearance="outline">
<mat-label>Updates requested</mat-label>
<textarea
matInput
rows="3"
maxlength="4000"
[(ngModel)]="note"
aria-describedby="release-review-note-help"></textarea>
<mat-hint id="release-review-note-help">
This note will be attached to the object.
</mat-hint>
</mat-form-field>
</div>

<footer class="release-review-actions">
<button mat-stroked-button type="button" (click)="skip()">
Skip
<mat-icon aria-hidden="true">arrow_forward</mat-icon>
</button>
<span class="action-spacer"></span>
<button
*ngIf="!showNote"
mat-stroked-button
type="button"
(click)="showNote = true">
Request updates
</button>
<button
*ngIf="showNote"
mat-stroked-button
type="button"
(click)="showNote = false">
Cancel note
</button>
<button
*ngIf="showNote"
mat-flat-button
color="warn"
type="button"
[disabled]="!note.trim()"
(click)="requestUpdates()">
Send request
</button>
<button
*ngIf="!showNote"
mat-flat-button
color="primary"
type="button"
(click)="approve()">
Approve
</button>
</footer>
</section>
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
@use '../../../style/colors';

.release-review-dialog-panel .mat-mdc-dialog-surface {
border: 1px solid;
border-radius: 10px;
overflow: hidden;
@include colors.theme-border-color;
}

.release-review-dialog {
display: flex;
width: min(94vw, 1160px);
max-height: min(90vh, 900px);
flex-direction: column;

.release-review-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 24px;
padding: 18px 24px;
border-bottom: 1px solid;
@include colors.theme-border-color;
@include colors.theme-property(
background,
rgba(colors.on-color(dark), 0.045),
rgba(colors.color(mitre-black), 0.025)
);

h2,
p {
margin: 0;
}

p {
margin-top: 4px;
@include colors.theme-text-deemphasis;

span {
margin-left: 12px;
font-weight: 700;
}
}
}

.release-review-content {
min-height: 0;
flex: 1 1 auto;
padding: 20px 24px;
overflow: auto;
}

.release-review-note {
padding: 16px 24px 0;
border-top: 1px solid;
@include colors.theme-border-color;

mat-form-field {
width: 100%;
}
}

.release-review-actions {
display: flex;
align-items: center;
gap: 12px;
padding: 16px 24px;
border-top: 1px solid;
@include colors.theme-border-color;

.action-spacer {
flex: 1 1 auto;
}
}
}

@media (max-width: 700px) {
.release-review-dialog .release-review-actions {
align-items: stretch;
flex-direction: column;

.action-spacer {
display: none;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { ReleaseReviewDialogComponent } from './release-review-dialog.component';

describe('ReleaseReviewDialogComponent', () => {
const createComponent = (count = 2) => {
const dialogRef = { close: vi.fn() } as any;
const items = Array.from({ length: count }, (_, index) => ({
item: {
object_ref: `attack-pattern--${index}`,
name: `Technique ${index}`,
},
current: {
name: `Technique ${index}`,
type: 'attack-pattern',
},
prior: null,
})) as any;
return {
component: new ReleaseReviewDialogComponent(dialogRef, { items }),
dialogRef,
};
};

it('steps through items and returns approved objects', () => {
const { component, dialogRef } = createComponent();

component.approve();

expect(component.index).toBe(1);
expect(dialogRef.close).not.toHaveBeenCalled();

component.skip();

expect(dialogRef.close).toHaveBeenCalledWith({
approved: [expect.objectContaining({ object_ref: 'attack-pattern--0' })],
updateRequests: [],
});
});

it('requires a note before requesting updates', () => {
const { component, dialogRef } = createComponent(1);

component.requestUpdates();
expect(dialogRef.close).not.toHaveBeenCalled();

component.note = 'Please update the description.';
component.requestUpdates();

expect(dialogRef.close).toHaveBeenCalledWith({
approved: [],
updateRequests: [
{
item: expect.objectContaining({ object_ref: 'attack-pattern--0' }),
note: 'Please update the description.',
},
],
});
});

it('exposes the active object metadata and diff config', () => {
const { component } = createComponent();

expect(component.objectName).toBe('Technique 0');
expect(component.progressLabel).toBe('1 of 2');
expect(component.config).toEqual({
mode: 'diff',
object: [component.reviewItem.current, component.reviewItem.prior],
editable: false,
sidebarControl: 'disable',
showRelationships: false,
});

delete (component.reviewItem.item as any).name;
(component.reviewItem.item as any).attack_id = 'T0001';
expect(component.objectName).toBe('T0001');

delete (component.reviewItem.item as any).attack_id;
expect(component.objectName).toBe('Object');
});

it('closes without a result when no actions have been completed', () => {
const { component, dialogRef } = createComponent();

component.cancel();

expect(dialogRef.close).toHaveBeenCalledWith(undefined);
});

it('preserves completed actions when closing before the final item', () => {
const { component, dialogRef } = createComponent();

component.approve();
component.cancel();

expect(dialogRef.close).toHaveBeenLastCalledWith({
approved: [expect.objectContaining({ object_ref: 'attack-pattern--0' })],
updateRequests: [],
});
});
});
Loading
Loading