Skip to content
Merged
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
46 changes: 18 additions & 28 deletions src/app/components/add-dialog/add-dialog.component.html
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
<div class="add-dialog">
<h1 *ngIf="config.title" mat-dialog-title>{{ config.title }}</h1>
<mat-dialog-content>
<div
*ngIf="
config.type === 'user' ||
config.type === 'analytic' ||
config.selectableObjects?.length > 0;
else nothing
">
<div *ngIf="hasSelectableContent; else nothing">
<mat-checkbox
*ngIf="config.showPreserveRelationshipsOption"
color="primary"
Expand All @@ -18,20 +12,22 @@ <h1 *ngIf="config.title" mat-dialog-title>{{ config.title }}</h1>
</mat-checkbox>
<app-stix-list
*ngIf="config.type !== 'user'"
[config]="{
select: config.selectionType
? config.selectionType
: config.select
? 'many'
: 'disabled',
selectionModel: config.select,
type: config.type,
clickBehavior: 'expand',
stixObjects: config.selectableObjects
? config.selectableObjects
: undefined,
showFilters: false,
}"></app-stix-list>
[config]="
config.stixListConfig || {
select: config.selectionType
? config.selectionType
: config.select
? 'many'
: 'disabled',
selectionModel: config.select,
type: config.type,
clickBehavior: 'expand',
stixObjects: config.selectableObjects
? config.selectableObjects
: undefined,
showFilters: false,
}
"></app-stix-list>
<app-users-list
*ngIf="config.type === 'user'"
[config]="{
Expand All @@ -47,13 +43,7 @@ <h3>No available objects to add.</h3>
</ng-template>
</mat-dialog-content>
<mat-dialog-actions align="center">
<ng-container
*ngIf="
config.type === 'user' ||
config.type === 'analytic' ||
config.selectableObjects?.length > 0;
else closeDialog
">
<ng-container *ngIf="hasSelectableContent; else closeDialog">
<button
mat-raised-button
class="extended-button left-button"
Expand Down
13 changes: 12 additions & 1 deletion src/app/components/add-dialog/add-dialog.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Component, Inject, ViewEncapsulation } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { StixObject } from 'src/app/classes/stix/stix-object';
import { SelectionModel } from '@angular/cdk/collections';
import { StixListConfig } from '../stix/stix-list/stix-list.component';

@Component({
selector: 'app-add-dialog',
Expand All @@ -16,6 +17,15 @@ export class AddDialogComponent {
@Inject(MAT_DIALOG_DATA) public config: AddDialogConfig
) {}

public get hasSelectableContent(): boolean {
return (
this.config.type === 'user' ||
this.config.type === 'analytic' ||
!!this.config.stixListConfig ||
(this.config.selectableObjects?.length ?? 0) > 0
);
}

public clearSelections() {
this.config.select.clear();
this.dialogRef.close(true);
Expand All @@ -26,10 +36,11 @@ export interface AddDialogConfig {
selectableObjects?: StixObject[]; // Stix Object array of selectable objects not in list
type: string; // type to display stix list
select: SelectionModel<string>; // selection model to retrieve list of selected object
selectionType?: string; // 'many', 'one', or 'disabled'; defaults to 'many' if a selection model is given
selectionType?: 'many' | 'one' | 'disabled'; // defaults to 'many' if a selection model is given
buttonLabel?: string; // optional button label, default "add"
title?: string; // dialog text
clearSelection?: boolean; //boolean to add clear selection button
showPreserveRelationshipsOption?: boolean; // show checkbox for preserveRelationships revoke option
preserveRelationships?: boolean; // value passed back through the shared dialog config
stixListConfig?: StixListConfig; // optional full stix-list config
}
57 changes: 55 additions & 2 deletions src/app/components/stix/stix-list/stix-list.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { provideHttpClient } from '@angular/common/http';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { of } from 'rxjs';
import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router';

import { StixListComponent, StixListConfig } from './stix-list.component';
import { RestApiConnectorService } from 'src/app/services/connectors/rest-api/rest-api-connector.service';
import { AuthenticationService } from 'src/app/services/connectors/authentication/authentication.service';

describe('StixListComponent', () => {
let component: StixListComponent;
Expand All @@ -15,7 +20,39 @@ describe('StixListComponent', () => {
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [StixListComponent],
providers: [provideHttpClient()],
imports: [NoopAnimationsModule],
providers: [
{
provide: RestApiConnectorService,
useValue: {
getAllAllowedValues: vi.fn(() => of([])),
getAllObjects: vi.fn(() =>
of({ data: [], pagination: { total: 0, limit: 0, offset: 0 } })
),
getAllTechniques: vi.fn(() =>
of({ data: [], pagination: { total: 0, limit: 0, offset: 0 } })
),
},
},
{
provide: AuthenticationService,
useValue: {
canEdit: vi.fn(() => true),
},
},
{
provide: MatDialog,
useValue: {
open: vi.fn(),
},
},
{
provide: Router,
useValue: {
navigate: vi.fn(),
},
},
],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
}));
Expand All @@ -30,4 +67,20 @@ describe('StixListComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});

it('should order all objects columns as ID, Name, Type, Domain, Modified', () => {
component.config = { columnsPreset: 'all-objects' };
component.tableColumns = [];
component.tableColumns_settings = new Map<string, any>();

(component as any).buildTable();

expect(component.tableColumns).toEqual([
'attackID',
'name',
'attackType',
'domains',
'modified',
]);
});
});
10 changes: 9 additions & 1 deletion src/app/components/stix/stix-list/stix-list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,14 @@ export class StixListComponent implements OnInit, AfterViewInit, OnDestroy {
this.tableDetail = [];
return;
}
if (this.config.columnsPreset === 'all-objects') {
this.addIdAndNameColumns(sticky_allowed);
this.addColumn('type', 'attackType', 'plain');
this.addDomainColumn();
this.addColumn('modified', 'modified', 'timestamp');
this.tableDetail = [];
return;
}
if ('type' in this.config) {
// set columns according to type
switch (this.config.type.replace(/_/g, '-')) {
Expand Down Expand Up @@ -1256,7 +1264,7 @@ export interface StixListConfig {
/** default true, if false hides all search/filter/control options */
showControls?: boolean;
/** Optional preset to override default columns */
columnsPreset?: 'id-name';
columnsPreset?: 'id-name' | 'all-objects';
/** display the 'show deprecated' filter, default false
* this may be relevant when displaying a list of embedded relationships, where
* the list of STIX objects is provided in the 'stixObjects' configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { MatDialog } from '@angular/material/dialog';
import { BreadcrumbService } from 'src/app/services/helpers/breadcrumb.service';
import { RestApiConnectorService } from 'src/app/services/connectors/rest-api/rest-api-connector.service';
import { MultipleChoiceDialogComponent } from 'src/app/components/multiple-choice-dialog/multiple-choice-dialog.component';
import { AddDialogComponent } from 'src/app/components/add-dialog/add-dialog.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import {
ConflictPolicy,
Expand Down Expand Up @@ -44,6 +45,7 @@ describe('ReleaseTrackPageComponent', () => {
updateConfig: vi.fn(() => createAsyncObservable({})),
reviewCandidates: vi.fn(() => createAsyncObservable({})),
updateMetadataByLatest: vi.fn(() => createAsyncObservable({})),
addCandidates: vi.fn(() => createAsyncObservable({})),
});
mockDialog = {
open: vi.fn(),
Expand Down Expand Up @@ -273,6 +275,39 @@ describe('ReleaseTrackPageComponent', () => {
).not.toHaveBeenCalled();
});

it('should open the all objects table to add candidates', () => {
mockDialog.open.mockImplementation((_component: any, config: any) => {
config.data.select.select('attack-pattern--1234');
return {
afterClosed: () => of(true),
};
});
mockReleaseTrackApiConnector.addCandidates.mockReturnValue(of({}));
component.id = 'release-track--123';
component.releaseTrack = { id: 'release-track--123' } as any;

component.onAddCandidate();

expect(mockRestApiConnector.getAllObjects).not.toHaveBeenCalled();
expect(mockDialog.open).toHaveBeenCalledWith(
AddDialogComponent,
expect.objectContaining({
data: expect.objectContaining({
title: 'Add candidates',
stixListConfig: expect.objectContaining({
showUserSearch: true,
excludeAttackTypes: ['relationship', 'note', 'collection'],
select: 'many',
}),
}),
})
);
expect(mockReleaseTrackApiConnector.addCandidates).toHaveBeenCalledWith(
'release-track--123',
['attack-pattern--1234']
);
});

it('should preview and tag the latest draft release', () => {
const refreshSpy = vi
.spyOn(component, 'getReleaseTrack')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { MultipleChoiceDialogComponent } from 'src/app/components/multiple-choic
import { finalize, take } from 'rxjs/operators';
import { FormBuilder, FormGroup } from '@angular/forms';
import { ReleaseTrackObjectItem } from 'src/app/components/release-track-object-card/release-track-object-card.component';
import { ALL_OBJECTS_STIX_LIST_CONFIG } from 'src/app/views/stix/all-objects-page/all-objects-page.component';

type ReleaseTrackLaneType = 'candidate' | 'staged' | 'member';

Expand Down Expand Up @@ -354,67 +355,40 @@ export class ReleaseTrackPageComponent implements OnInit {
if (!this.releaseTrack) return;

const selection = new SelectionModel<string>(true);

const sub = this.restApiConnectorService
.getAllObjects({ deserialize: true })
.subscribe({
next: results => {
const objects = (results as any).data || [];

const dialogRef = this.dialog.open(AddDialogComponent, {
data: {
selectableObjects: objects,
select: selection,
type: 'all',
selectionType: 'many',
buttonLabel: 'Add',
title: 'Add candidates',
clearSelection: true,
},
maxWidth: '70em',
minWidth: '40vw',
maxHeight: '75vh',
});

const closeSub = dialogRef.afterClosed().subscribe({
next: result => {
if (!result) return; // user cancelled

const objectRefs: any[] = selection.selected.map(stixId => {
const obj = objects.find(
(o: any) =>
o.stixID === stixId || (o.stix && o.stix.id === stixId)
);
const id = obj ? obj.stixID || obj.stix?.id : stixId;
let modified: string | undefined;
if (obj) {
if (obj.modified instanceof Date)
modified = obj.modified.toISOString();
else if (obj.modified) modified = obj.modified;
else if (obj.stix && obj.stix.modified)
modified = obj.stix.modified;
}
return modified ? { id, modified } : id;
});

const apiSub = this.connector
.addCandidates(this.id, objectRefs)
.subscribe({
next: () => {
// refresh snapshot from server so local state reflects saved candidates
this.refreshReleaseTrackState();
},
error: err => {
console.error('Failed to add candidates', err);
},
complete: () => apiSub.unsubscribe(),
});
},
complete: () => closeSub.unsubscribe(),
});
const dialogRef = this.dialog.open(AddDialogComponent, {
data: {
select: selection,
type: 'all',
selectionType: 'many',
buttonLabel: 'Add',
title: 'Add candidates',
clearSelection: true,
stixListConfig: {
...ALL_OBJECTS_STIX_LIST_CONFIG,
select: 'many',
selectionModel: selection,
clickBehavior: 'expand',
},
complete: () => sub.unsubscribe(),
});
},
maxWidth: '90vw',
width: '80vw',
maxHeight: '85vh',
});

dialogRef.afterClosed().subscribe({
next: result => {
if (!result || !selection.selected.length) return;

this.connector.addCandidates(this.id, selection.selected).subscribe({
next: () => {
this.refreshReleaseTrackState();
},
error: err => {
console.error('Failed to add candidates', err);
},
});
},
});
}

public getViewUrl(stixId: string): string {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';

import { AllObjectsPageComponent } from './all-objects-page.component';

Expand All @@ -9,6 +10,7 @@ describe('AllObjectsPageComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [AllObjectsPageComponent],
schemas: [NO_ERRORS_SCHEMA],
}).compileComponents();
});

Expand All @@ -21,4 +23,8 @@ describe('AllObjectsPageComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});

it('should use the all objects column preset', () => {
expect(component.stixListConfig.columnsPreset).toBe('all-objects');
});
});
Loading
Loading