diff --git a/.gitignore b/.gitignore index a097d388..66ddf947 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ Temporary Items # End of https://www.toptal.com/developers/gitignore/api/macos *.env +.vscode \ No newline at end of file diff --git a/app/package-lock.json b/app/package-lock.json index 3fbd5b5d..11f387ad 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,6 +1,6 @@ { "name": "attack-workbench-frontend", - "version": "1.0.2", + "version": "1.0.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/app/package.json b/app/package.json index 0b0e0f39..e551f5f0 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "attack-workbench-frontend", - "version": "1.0.2", + "version": "1.0.3", "description": "An application allowing users to explore, create, annotate, and share extensions of the MITRE ATT&CK® knowledge base. This repository contains an Angular-based web application providing the user interface for the ATT&CK Workbench application.", "keywords": [ "cti", diff --git a/app/src/app/app-routing-stix.module.ts b/app/src/app/app-routing-stix.module.ts index 5ac630a7..a1e00ee6 100644 --- a/app/src/app/app-routing-stix.module.ts +++ b/app/src/app/app-routing-stix.module.ts @@ -13,6 +13,7 @@ import { NgModule } from '@angular/core'; import { environment } from "../environments/environment" import { CollectionImportComponent } from "./views/stix/collection/collection-import/collection-import-workflow/collection-import.component"; import { CollectionIndexImportComponent } from "./views/stix/collection/collection-index/collection-index-import/collection-index-import.component"; +import { DataSourceListComponent } from "./views/stix/data-source/data-source-list/data-source-list.component"; const stixRoutes: Routes = [{ path: 'matrix', @@ -294,7 +295,54 @@ const stixRoutes: Routes = [{ } ] }, - + { + path: 'data-source', + data: { + breadcrumb: 'data sources' + }, + children: [{ + path: '', + data: { + breadcrumb: 'list', + title: "data sources" + }, + component: DataSourceListComponent + }, + { + path: ':id', + data: { + breadcrumb: 'loading...' + }, + children: [{ + path: '', + data: { + breadcrumb: 'view', + editable: true, + title: "view data source" + }, + component: StixPageComponent + } + ] + }, + { + path: ":new", + data: { + breadcrumb: "new data source" + }, + children: [{ + path: '', + data: { + breadcrumb: 'view', + editable: true, + title: "new data source" + }, + component: StixPageComponent + } + ] + } + ] + }, + ] if (environment.integrations.collection_manager.enabled) { diff --git a/app/src/app/app.component.ts b/app/src/app/app.component.ts index 5b0023a8..e6bb9387 100644 --- a/app/src/app/app.component.ts +++ b/app/src/app/app.component.ts @@ -51,15 +51,17 @@ export class AppComponent implements AfterViewInit { // header hiding with scroll ngAfterViewInit() { - this.scrollRef.nativeElement.addEventListener('scroll', (e) => this.scrollEvent(), true); + this.scrollRef.nativeElement.addEventListener('scroll', (e) => this.adjustHeaderPlacement(), true); + //to fix rare cases that the page has resized without scroll events triggering, recompute the offset every 5 seconds + setInterval(() => this.adjustHeaderPlacement(), 5000); } ngOnDestroy() { - this.scrollRef.nativeElement.removeEventListener('scroll', (e) => this.scrollEvent(), true); + this.scrollRef.nativeElement.removeEventListener('scroll', (e) => this.adjustHeaderPlacement(), true); } public hiddenHeaderPX: number = 0; //number of px of the header which is hidden - // when a scroll happens - private scrollEvent(): void { + // adjust the header placement + private adjustHeaderPlacement(): void { let headerHeight = this.header.nativeElement.offsetHeight; // constrain amount of hidden to bounds, round up because decimal scroll causes flicker this.hiddenHeaderPX = Math.floor(Math.min(Math.max(0, this.scrollRef.nativeElement.scrollTop/2), headerHeight)); diff --git a/app/src/app/app.module.ts b/app/src/app/app.module.ts index 1f7b44e9..d620eef3 100644 --- a/app/src/app/app.module.ts +++ b/app/src/app/app.module.ts @@ -114,11 +114,13 @@ import { CollectionManagerComponent } from "./views/stix/collection/collection-m import { CollectionIndexListComponent } from "./views/stix/collection/collection-index/collection-index-list/collection-index-list.component"; import { CollectionIndexViewComponent } from "./views/stix/collection/collection-index/collection-index-view/collection-index-view.component"; import { CollectionIndexImportComponent } from "./views/stix/collection/collection-index/collection-index-import/collection-index-import.component"; -import { CollectionImportReviewComponent } from "./views/stix/collection/collection-import/collection-import-review/collection-import-review.component"; import { CollectionListComponent } from './views/stix/collection/collection-list/collection-list.component'; import { CollectionViewComponent } from './views/stix/collection/collection-view/collection-view.component'; import { CollectionImportComponent } from './views/stix/collection/collection-import/collection-import-workflow/collection-import.component'; +import { CollectionImportReviewComponent } from "./views/stix/collection/collection-import/collection-import-review/collection-import-review.component"; +import { CollectionImportErrorComponent } from './views/stix/collection/collection-import/collection-import-error/collection-import-error.component'; + // import { CollectionExportComponent } from './views/stix/collection/collection-export/collection-export.component'; import { GroupViewComponent } from './views/stix/group/group-view/group-view.component'; @@ -153,6 +155,9 @@ import { ObjectStatusComponent } from './components/object-status/object-status. import { IconViewComponent } from './components/stix/icon-view/icon-view.component'; import { IdentityPropertyComponent } from './components/stix/identity-property/identity-property.component'; import { NgxJdenticonModule, JDENTICON_CONFIG } from 'ngx-jdenticon'; +import { DataSourceViewComponent } from './views/stix/data-source/data-source-view/data-source-view.component'; +import { DataSourceListComponent } from './views/stix/data-source/data-source-list/data-source-list.component'; +import { DataComponentViewComponent } from './views/stix/data-component/data-component-view/data-component-view.component'; @NgModule({ @@ -218,6 +223,7 @@ import { NgxJdenticonModule, JDENTICON_CONFIG } from 'ngx-jdenticon'; CollectionIndexImportComponent, CollectionImportComponent, CollectionImportReviewComponent, + CollectionImportErrorComponent, // CollectionExportComponent, RelationshipViewComponent, @@ -249,7 +255,10 @@ import { NgxJdenticonModule, JDENTICON_CONFIG } from 'ngx-jdenticon'; NotesEditorComponent, ObjectStatusComponent, - IdentityPropertyComponent + IdentityPropertyComponent, + DataSourceViewComponent, + DataSourceListComponent, + DataComponentViewComponent ], imports: [ BreadcrumbModule, diff --git a/app/src/app/classes/stix/collection.ts b/app/src/app/classes/stix/collection.ts index 1e70a70c..ef6092fb 100644 --- a/app/src/app/classes/stix/collection.ts +++ b/app/src/app/classes/stix/collection.ts @@ -9,6 +9,8 @@ import { Software } from './software'; import { StixObject } from './stix-object'; import { Tactic } from './tactic'; import { Technique } from './technique'; +import { DataSource } from './data-source'; +import { DataComponent } from './data-component'; import { logger } from "../../util/logger"; /** @@ -164,6 +166,7 @@ export class Collection extends StixObject { // each sub-property is a list of STIX IDs corresponding to objects in the import public import_categories: CollectionDiffCategories; + public readonly supportsAttackID = false; // collections do not support ATT&CK IDs protected get attackIDValidator() { return null; } //collections do not have ATT&CK IDs constructor(sdo?: any) { @@ -279,6 +282,12 @@ export class Collection extends StixObject { case "intrusion-set": //group this.stix_contents.push(new Group(obj)) break; + case "x-mitre-data-source": // data source + this.stix_contents.push(new DataSource(obj)) + break; + case "x-mitre-data-component": // data component + this.stix_contents.push(new DataComponent(obj)) + break; } } } @@ -289,22 +298,26 @@ export class Collection extends StixObject { * @memberof Collection */ public compareTo(that: Collection): { - technique: CollectionDiffCategories, - tactic: CollectionDiffCategories, - software: CollectionDiffCategories, - relationship: CollectionDiffCategories, - mitigation: CollectionDiffCategories, - matrix: CollectionDiffCategories, - group: CollectionDiffCategories + technique: CollectionDiffCategories, + tactic: CollectionDiffCategories, + software: CollectionDiffCategories, + relationship: CollectionDiffCategories, + mitigation: CollectionDiffCategories, + matrix: CollectionDiffCategories, + group: CollectionDiffCategories, + data_source: CollectionDiffCategories, + data_component: CollectionDiffCategories } { let results = { - technique: new CollectionDiffCategories(), - tactic: new CollectionDiffCategories(), - software: new CollectionDiffCategories(), - relationship: new CollectionDiffCategories(), - mitigation: new CollectionDiffCategories(), - matrix: new CollectionDiffCategories(), - group: new CollectionDiffCategories() + technique: new CollectionDiffCategories(), + tactic: new CollectionDiffCategories(), + software: new CollectionDiffCategories(), + relationship: new CollectionDiffCategories(), + mitigation: new CollectionDiffCategories(), + matrix: new CollectionDiffCategories(), + group: new CollectionDiffCategories(), + data_source: new CollectionDiffCategories(), + data_component: new CollectionDiffCategories() } // build helper lookups to reduce complexity from n^2 to n. let thisStixLookup = new Map(this.stix_contents.map(sdo => [sdo.stixID, sdo])) diff --git a/app/src/app/classes/stix/data-component.ts b/app/src/app/classes/stix/data-component.ts new file mode 100644 index 00000000..68440f33 --- /dev/null +++ b/app/src/app/classes/stix/data-component.ts @@ -0,0 +1,98 @@ +import { Observable } from "rxjs"; +import { RestApiConnectorService } from "src/app/services/connectors/rest-api/rest-api-connector.service"; +import { ValidationData } from "../serializable"; +import { StixObject } from "./stix-object"; +import { logger } from "../../util/logger"; +import { DataSource } from "./data-source"; + +export class DataComponent extends StixObject { + public name: string = ""; + public description: string = ""; + public domains: string[] = ['enterprise-attack']; // default to enterprise + public data_source_ref: string; // stix ID of the data source + + // NOTE: the following field will only be populated when this object is fetched using getDataComponent() + public data_source: DataSource = null; + + public readonly supportsAttackID = false; // data components do not support ATT&CK IDs + protected get attackIDValidator() { return null; } // data components have no ATT&CK ID + + constructor(sdo?: any) { + super(sdo, "x-mitre-data-component"); + if (sdo) { + this.deserialize(sdo); + } + } + + /** + * Transform the current object into a raw object for sending to the back-end + * @abstract + * @returns {*} the raw object to send + */ + public serialize(): any { + let rep = super.base_serialize(); + + rep.stix.name = this.name; + rep.stix.description = this.description; + rep.stix.x_mitre_data_source_ref = this.data_source_ref; + rep.stix.x_mitre_domains = this.domains; + + return rep; + } + + /** + * Parse the object from the record returned from the back-end + * @abstract + * @param {*} raw the raw object to parse + */ + public deserialize(raw: any) { + if (!("stix" in raw)) return; + + let sdo = raw.stix; + + if ("name" in sdo) { + if (typeof(sdo.name) === "string") this.name = sdo.name; + else logger.error("TypeError: name field is not a string:", sdo.name, "(",typeof(sdo.name),")") + } else this.name = ""; + + if ("description" in sdo) { + if (typeof(sdo.description) === "string") this.description = sdo.description; + else logger.error("TypeError: description field is not a string:", sdo.description, "(",typeof(sdo.description),")") + } else this.description = ""; + + if ("x_mitre_data_source_ref" in sdo) { + if (typeof(sdo.x_mitre_data_source_ref) === "string") this.data_source_ref = sdo.x_mitre_data_source_ref; + else logger.error("TypeError: data source ref field is not a string:", sdo.x_mitre_data_source_ref, "(",typeof(sdo.x_mitre_data_source_ref),")") + } else this.data_source_ref = ""; + + if ("x_mitre_domains" in sdo) { + if (this.isStringArray(sdo.x_mitre_domains)) this.domains = sdo.x_mitre_domains; + else logger.error("TypeError: domains field is not a string array."); + } else this.domains = ['enterprise-attack']; // default to enterprise + } + + /** + * Validate the current object state and return information on the result of the validation + * @param {RestApiConnectorService} restAPIService: the REST API connector through which asynchronous validation can be completed + * @returns {Observable} the validation warnings and errors once validation is complete. + */ + public validate(restAPIService: RestApiConnectorService): Observable { + return this.base_validate(restAPIService); + } + + /** + * Save the current state of the STIX object in the database. Update the current object from the response + * @param restAPIService [RestApiConnectorService] the service to perform the POST/PUT through + * @returns {Observable} of the post + */ + public save(restAPIService: RestApiConnectorService): Observable { + // TODO POST if the object was just created (doesn't exist in db yet) + + let postObservable = restAPIService.postDataComponent(this); + let subscription = postObservable.subscribe({ + next: (result) => { this.deserialize(result.serialize()); }, + complete: () => { subscription.unsubscribe(); } + }); + return postObservable; + } +} \ No newline at end of file diff --git a/app/src/app/classes/stix/data-source.ts b/app/src/app/classes/stix/data-source.ts new file mode 100644 index 00000000..c84a0233 --- /dev/null +++ b/app/src/app/classes/stix/data-source.ts @@ -0,0 +1,120 @@ +import { StixObject } from "./stix-object"; +import { logger } from "../../util/logger"; +import { RestApiConnectorService } from "src/app/services/connectors/rest-api/rest-api-connector.service"; +import { Observable } from "rxjs"; +import { ValidationData } from "../serializable"; +import { DataComponent } from "./data-component"; + +export class DataSource extends StixObject { + public name: string = ""; + public description: string = ""; + public platforms: string[] = []; + public collection_layers: string[] = []; + public contributors: string[] = []; + public domains: string[] = ['enterprise-attack']; // default to enterprise + public data_components: DataComponent[] = []; + + public readonly supportsAttackID = true; + protected get attackIDValidator() { + return { + regex: "DS\\d{4}", + format: "DS####" + } + } + + constructor(sdo?: any) { + super(sdo, "x-mitre-data-source"); + if (sdo) { + this.deserialize(sdo); + } + } + + /** + * Transform the current object into a raw object for sending to the back-end + * @abstract + * @returns {*} the raw object to send + */ + public serialize(): any { + let rep = super.base_serialize(); + + rep.stix.name = this.name; + rep.stix.description = this.description; + rep.stix.x_mitre_platforms = this.platforms; + rep.stix.x_mitre_collection_layers = this.collection_layers; + rep.stix.x_mitre_contributors = this.contributors; + rep.stix.x_mitre_domains = this.domains; + + return rep; + } + + /** + * Parse the object from the record returned from the back-end + * @abstract + * @param {*} raw the raw object to parse + */ + public deserialize(raw: any) { + if ("dataComponents" in raw) { + for (let obj of raw.dataComponents) { + this.data_components.push(new DataComponent(obj)); + } + } + + if (!("stix" in raw)) return; + + let sdo = raw.stix; + if ("name" in sdo) { + if (typeof(sdo.name) === "string") this.name = sdo.name; + else logger.error("TypeError: name field is not a string:", sdo.name, "(",typeof(sdo.name),")") + } else this.name = ""; + + if ("description" in sdo) { + if (typeof(sdo.description) === "string") this.description = sdo.description; + else logger.error("TypeError: description field is not a string:", sdo.description, "(",typeof(sdo.description),")") + } else this.description = ""; + + if ("x_mitre_platforms" in sdo) { + if (this.isStringArray(sdo.x_mitre_platforms)) this.platforms = sdo.x_mitre_platforms; + else logger.error("TypeError: platforms field is not a string array.") + } else this.platforms = []; + + if ("x_mitre_collection_layers" in sdo) { + if (this.isStringArray(sdo.x_mitre_collection_layers)) this.collection_layers = sdo.x_mitre_collection_layers; + else logger.error("TypeError: collection layers field is not a string array."); + } else this.collection_layers = []; + + if ("x_mitre_contributors" in sdo) { + if (this.isStringArray(sdo.x_mitre_contributors)) this.contributors = sdo.x_mitre_contributors; + else logger.error("TypeError: x_mitre_contributors is not a string array:", sdo.x_mitre_contributors, "(",typeof(sdo.x_mitre_contributors),")") + } else this.contributors = []; + + if ("x_mitre_domains" in sdo) { + if (this.isStringArray(sdo.x_mitre_domains)) this.domains = sdo.x_mitre_domains; + else logger.error("TypeError: domains field is not a string array."); + } else this.domains = ['enterprise-attack']; // default to enterprise + } + + /** + * Validate the current object state and return information on the result of the validation + * @param {RestApiConnectorService} restAPIService: the REST API connector through which asynchronous validation can be completed + * @returns {Observable} the validation warnings and errors once validation is complete. + */ + public validate(restAPIService: RestApiConnectorService): Observable { + return this.base_validate(restAPIService); + } + + /** + * Save the current state of the STIX object in the database. Update the current object from the response + * @param restAPIService [RestApiConnectorService] the service to perform the POST/PUT through + * @returns {Observable} of the post + */ + public save(restAPIService: RestApiConnectorService): Observable { + // TODO POST if the object was just created (doesn't exist in db yet) + + let postObservable = restAPIService.postDataSource(this); + let subscription = postObservable.subscribe({ + next: (result) => { this.deserialize(result.serialize()); }, + complete: () => { subscription.unsubscribe(); } + }); + return postObservable; + } +} diff --git a/app/src/app/classes/stix/group.ts b/app/src/app/classes/stix/group.ts index f1903242..74e69fc3 100644 --- a/app/src/app/classes/stix/group.ts +++ b/app/src/app/classes/stix/group.ts @@ -10,6 +10,7 @@ export class Group extends StixObject { public aliases: string[] = []; public contributors: string[] = []; + public readonly supportsAttackID = true; protected get attackIDValidator() { return { regex: "G\\d{4}", format: "G####" diff --git a/app/src/app/classes/stix/identity.ts b/app/src/app/classes/stix/identity.ts index 03408e3f..f33d423b 100644 --- a/app/src/app/classes/stix/identity.ts +++ b/app/src/app/classes/stix/identity.ts @@ -10,6 +10,7 @@ export class Identity extends StixObject { public roles?: string[]; // list of roles this identity performs public contact?: string; // contact information for this identity + public readonly supportsAttackID = false; // Identity does not support ATT&CK IDs protected get attackIDValidator() { return null; } // identities do not have an ATT&CK ID constructor(sdo?: any) { diff --git a/app/src/app/classes/stix/marking-definition.ts b/app/src/app/classes/stix/marking-definition.ts index a2adce92..f245d111 100644 --- a/app/src/app/classes/stix/marking-definition.ts +++ b/app/src/app/classes/stix/marking-definition.ts @@ -12,6 +12,7 @@ export class MarkingDefinition extends StixObject { statement: string } = { statement: "" } + public readonly supportsAttackID = false; // marking-defs do not support ATT&CK IDs protected get attackIDValidator() { return null; } //marking-defs do not have ATT&CK IDs constructor(sdo?: any) { diff --git a/app/src/app/classes/stix/matrix.ts b/app/src/app/classes/stix/matrix.ts index 9b92630d..fcf280f2 100644 --- a/app/src/app/classes/stix/matrix.ts +++ b/app/src/app/classes/stix/matrix.ts @@ -8,6 +8,7 @@ export class Matrix extends StixObject { public name: string = ""; public tactic_refs: string[] = []; + public readonly supportsAttackID = true; protected get attackIDValidator() { return { regex: ".*", format: "[domain identifier]" diff --git a/app/src/app/classes/stix/mitigation.ts b/app/src/app/classes/stix/mitigation.ts index 825097e6..3be2f067 100644 --- a/app/src/app/classes/stix/mitigation.ts +++ b/app/src/app/classes/stix/mitigation.ts @@ -8,6 +8,7 @@ export class Mitigation extends StixObject { public name: string = ""; public domains: string[] = []; + public readonly supportsAttackID = true; protected get attackIDValidator() { return { regex: "M\\d{4}", format: "M####" diff --git a/app/src/app/classes/stix/note.ts b/app/src/app/classes/stix/note.ts index 2fd00cfa..ace4f879 100644 --- a/app/src/app/classes/stix/note.ts +++ b/app/src/app/classes/stix/note.ts @@ -10,6 +10,7 @@ export class Note extends StixObject { public object_refs: string[] = []; public editing: boolean = false; + public readonly supportsAttackID = false; // notes do not support ATT&CK IDs protected get attackIDValidator() { return null; } // notes have no ATT&CK ID constructor(sdo?: any) { diff --git a/app/src/app/classes/stix/relationship.ts b/app/src/app/classes/stix/relationship.ts index 8ba44bbf..e46e6b28 100644 --- a/app/src/app/classes/stix/relationship.ts +++ b/app/src/app/classes/stix/relationship.ts @@ -2,26 +2,32 @@ import { Observable, of } from "rxjs"; import { map, switchMap } from "rxjs/operators"; import { RestApiConnectorService } from "src/app/services/connectors/rest-api/rest-api-connector.service"; import { ValidationData } from "../serializable"; -import {StixObject} from "./stix-object"; +import { StixObject } from "./stix-object"; import { logger } from "../../util/logger"; export class Relationship extends StixObject { public source_ref: string = ""; - public get source_name(): string { return this.source_object? this.source_object.stix.name : "[unknown object]"; } + public get source_name(): string { + return `${this.source_parent ? this.source_parent.stix.name + ': ' : ''}${this.source_object ? this.source_object.stix.name : '[unknown object]'}`; + } public source_ID: string = ""; public source_object: any; - + public source_parent: any; public target_ref: string = ""; - public get target_name(): string { return this.target_object? this.target_object.stix.name : "[unknown object]"; } + public get target_name(): string { + return `${this.target_parent ? this.target_parent.stix.name + ': ' : ''}${this.target_object ? this.target_object.stix.name : '[unknown object]'}`; + } public target_ID: string = ""; public target_object: any; + public target_parent: any; public updating_refs: boolean = false; //becomes true while source and target refs are being asynchronously updated. public relationship_type: string = ""; + public readonly supportsAttackID = true; // relationships do not have ATT&CK IDs protected get attackIDValidator() { return null; } // relationships have no ATT&CK ID /** * The valid source types according to relationship_type @@ -34,6 +40,7 @@ export class Relationship extends StixObject { } if (this.relationship_type == "mitigates") return ["mitigation"]; if (this.relationship_type == "subtechnique-of") return ["technique"]; + if (this.relationship_type == "detects") return ["data-component"]; else return null; } /** @@ -47,6 +54,7 @@ export class Relationship extends StixObject { } if (this.relationship_type == "mitigates") return ["technique"]; if (this.relationship_type == "subtechnique-of") return ["technique"]; + if (this.relationship_type == "detects") return ["technique"]; else return null; } @@ -74,8 +82,22 @@ export class Relationship extends StixObject { let serialized = this.serialize(); serialized.source_object = x.find(result => result.stix.id == new_source_ref); this.deserialize(serialized); - this.updating_refs = false; return this; + }), + switchMap(relationship => { + if (relationship.source_object.stix.x_mitre_is_subtechnique || relationship.source_object.stix.type == 'x-mitre-data-component') { + return this.get_parent_object(relationship.source_object, restAPIService).pipe( + map(res => { + this.source_parent = res; + this.updating_refs = false; + return this; + }) + ) + } else { + this.source_parent = undefined; // source object has no parent + this.updating_refs = false; + return of(this); + } }) ) } @@ -83,14 +105,26 @@ export class Relationship extends StixObject { /** * Set the source object * @param {StixObject} new_source_object the object to set + * @param {RestApiConnectorService} restAPIService: the REST API connector through which the parent of the source can be fetched * @returns {Observable} of this object after the data has been updated */ - public set_source_object(new_source_object: StixObject): Observable { + public set_source_object(new_source_object: StixObject, restAPIService: RestApiConnectorService): Observable { this.updating_refs = true; this.source_ref = new_source_object.stixID; let serialized = this.serialize(); serialized.source_object = new_source_object.serialize(); this.deserialize(serialized); + + if (this.source_object.stix.x_mitre_is_subtechnique || this.source_object.stix.type == 'x-mitre-data-component') { + return this.get_parent_object(this.source_object, restAPIService).pipe( + map(result => { + this.source_parent = result; + this.updating_refs = false; + return this; + }) + ); + } else this.source_parent = undefined; // source object has no parent + this.updating_refs = false; return of(this); } @@ -110,26 +144,111 @@ export class Relationship extends StixObject { let serialized = this.serialize(); serialized.target_object = x.find(result => result.stix.id == new_target_ref); this.deserialize(serialized); - this.updating_refs = false; return this; + }), + switchMap(relationship => { + if (relationship.target_object.stix.x_mitre_is_subtechnique || relationship.target_object.stix.type == 'x-mitre-data-component') { + return this.get_parent_object(relationship.target_object, restAPIService).pipe( + map(res => { + this.target_parent = res; + this.updating_refs = false; + return this; + }) + ) + } else { + this.target_parent = undefined; // target object has no parent + this.updating_refs = false; + return of(this); + } }) ) } - /** + /** * Set the target object * @param {StixObject} new_target_object the object to set + * @param {RestApiConnectorService} restAPIService: the REST API connector through which the parent of the target can be fetched * @returns {Observable} of this object after the data has been updated */ - public set_target_object(new_target_object: StixObject): Observable { - this.updating_refs = true; - this.target_ref = new_target_object.stixID; - let serialized = this.serialize(); - serialized.target_object = new_target_object.serialize(); - this.deserialize(serialized); - this.updating_refs = false; - return of(this); + public set_target_object(new_target_object: StixObject, restAPIService: RestApiConnectorService): Observable { + this.updating_refs = true; + this.target_ref = new_target_object.stixID; + let serialized = this.serialize(); + serialized.target_object = new_target_object.serialize(); + this.deserialize(serialized); + + if (this.target_object.stix.x_mitre_is_subtechnique || this.target_object.stix.type == 'x-mitre-data-component') { + return this.get_parent_object(this.target_object, restAPIService).pipe( + map(result => { + this.target_parent = result; + this.updating_refs = false; + return this; + }) + ); + } else this.target_parent = undefined; // target object has no parent + + this.updating_refs = false; + return of(this); + } + + /** + * Retrieve parent object from the REST API, if applicable. + * Fetches the parent technique of a sub-technique and the parent data source of + * a data component. + * @param {any} object the raw source or target object + * @param {RestApiConnectorService} restAPIService the REST API connector through which the parent can be fetched + * @returns {Observable} of the parent object + */ + public get_parent_object(object: any, restAPIService: RestApiConnectorService): Observable { + if (object.stix.x_mitre_is_subtechnique) { // sub-technique + return restAPIService.getRelatedTo({sourceRef: object.stix.id, relationshipType: "subtechnique-of"}).pipe( // fetch parent from REST API + map(relationship => { + if (!relationship || relationship.data.length == 0) return null; // no parent technique found + let p = relationship.data[0] as Relationship; + return p.target_object; + }) + ); + } else { // data component + return restAPIService.getDataSource(object.stix.x_mitre_data_source_ref).pipe( // fetch data source from REST API + map(data_sources => { + if (!data_sources || data_sources.length == 0) return null; // no data source found + return data_sources[0].serialize(); + }) + ); } + } + + /** + * Retrieve the parent object of this source object + * @param {RestApiConnectorService} restAPIService the REST API connector through which the parent can be fetched + * @returns {Observable} of this object after the source parent has been updated + */ + public update_source_parent(restAPIService: RestApiConnectorService): Observable { + this.updating_refs = true; + return this.get_parent_object(this.source_object, restAPIService).pipe( + map(result => { + this.source_parent = result; + this.updating_refs = false; + return this; + }) + ); + } + + /** + * Retrieve the parent object of this target object + * @param {RestApiConnectorService} restAPIService the REST API connector through which the parent can be fetched + * @returns {Observable} of this object after the target parent has been updated + */ + public update_target_parent(restAPIService: RestApiConnectorService): Observable { + this.updating_refs = true; + return this.get_parent_object(this.target_object, restAPIService).pipe( + map(result => { + this.target_parent = result; + this.updating_refs = false; + return this; + }) + ) + } /** * Transform the current object into a raw object for sending to the back-end, stripping any unnecessary fields @@ -199,7 +318,6 @@ export class Relationship extends StixObject { * @returns {Observable} the validation warnings and errors once validation is complete. */ public validate(restAPIService: RestApiConnectorService): Observable { - //TODO verify source and target ref exist return this.base_validate(restAPIService).pipe( map(result => { // presence of source-ref diff --git a/app/src/app/classes/stix/software.ts b/app/src/app/classes/stix/software.ts index b4825649..ce9bcf59 100644 --- a/app/src/app/classes/stix/software.ts +++ b/app/src/app/classes/stix/software.ts @@ -14,6 +14,7 @@ export class Software extends StixObject { public contributors: string[] = []; public domains: string[] = []; + public readonly supportsAttackID = true; protected get attackIDValidator() { return { regex: "S\\d{4}", format: "S####" diff --git a/app/src/app/classes/stix/stix-object.ts b/app/src/app/classes/stix/stix-object.ts index d289752f..8456eba8 100644 --- a/app/src/app/classes/stix/stix-object.ts +++ b/app/src/app/classes/stix/stix-object.ts @@ -19,7 +19,9 @@ let stixTypeToAttackType = { "x-mitre-matrix": "matrix", "x-mitre-tactic": "tactic", "relationship": "relationship", - "marking-definition": "marking-definition" + "marking-definition": "marking-definition", + "x-mitre-data-source": "data-source", + "x-mitre-data-component": "data-component" } export {stixTypeToAttackType}; @@ -37,6 +39,7 @@ export abstract class StixObject extends Serializable { public object_marking_refs: string[] = []; //list of embedded relationships to marking_defs + public abstract readonly supportsAttackID: boolean; // boolean to determine if object supports ATT&CK IDs protected abstract get attackIDValidator(): { regex: string, // regex to validate the ID format: string // format to display to user @@ -50,7 +53,9 @@ export abstract class StixObject extends Serializable { "matrix": "matrices", "tactic": "tactics", "note": "notes", - "marking-definition": "marking-definitions" + "marking-definition": "marking-definitions", + "data-source": "data-sources", + "data-component": "data-components" } public get routes(): any[] { // route to view the object @@ -296,6 +301,8 @@ export abstract class StixObject extends Serializable { this.attackType == "matrix"? restAPIService.getAllMatrices() : this.attackType == "mitigation"? restAPIService.getAllMitigations() : this.attackType == "technique"? restAPIService.getAllTechniques() : + this.attackType == "data-source"? restAPIService.getAllDataSources() : + this.attackType == "data-component"? restAPIService.getAllDataComponents() : restAPIService.getAllTactics(); return accessor.pipe( map(objects => { @@ -321,32 +328,41 @@ export abstract class StixObject extends Serializable { }) } } - // check ATT&CK ID - if (this.hasOwnProperty("attackID") && this.attackID != "") { - if (objects.data.some(x => x.attackID == this.attackID && x.stixID != this.stixID)) { - result.errors.push({ - "result": "error", - "field": "attackID", - "message": "ATT&CK ID is not unique" - }) - } else { - result.successes.push({ - "result": "success", + // check ATT&CK ID and ignore collections + if (this.hasOwnProperty("supportsAttackID") && this.supportsAttackID == true) { + if (this.attackID == "") { + result.warnings.push({ + "result": "warning", "field": "attackID", - "message": "ATT&CK ID is unique" + "message": "Object does not have ATT&CK ID" }) } - // (\S+--)? is an organization prefix, and should probably be improved when that is made an explicit feature - let idRegex = new RegExp("^(\\S+--)?" + this.attackIDValidator.regex + "$"); - let attackIDValid = idRegex.test(this.attackID); - if (!attackIDValid) { - result.errors.push({ - "result": "error", - "field": "attackID", - "message": `ATT&CK ID does not match the format ${this.attackIDValidator.format}` - }) + else { + if (objects.data.some(x => x.attackID == this.attackID && x.stixID != this.stixID)) { + result.errors.push({ + "result": "error", + "field": "attackID", + "message": "ATT&CK ID is not unique" + }) + } else { + result.successes.push({ + "result": "success", + "field": "attackID", + "message": "ATT&CK ID is unique" + }) + } + // (\S+--)? is an organization prefix, and should probably be improved when that is made an explicit feature + let idRegex = new RegExp("^(\\S+--)?" + this.attackIDValidator.regex + "$"); + let attackIDValid = idRegex.test(this.attackID); + if (!attackIDValid) { + result.errors.push({ + "result": "error", + "field": "attackID", + "message": `ATT&CK ID does not match the format ${this.attackIDValidator.format}` + }) + } } - } + } return result; }) ) diff --git a/app/src/app/classes/stix/tactic.ts b/app/src/app/classes/stix/tactic.ts index 550fa9d7..e97dc565 100644 --- a/app/src/app/classes/stix/tactic.ts +++ b/app/src/app/classes/stix/tactic.ts @@ -7,6 +7,8 @@ import { logger } from "../../util/logger"; export class Tactic extends StixObject { public name: string = ""; public domains: string[] = []; + + public readonly supportsAttackID = true; protected get attackIDValidator() { return { regex: "TA\\d{4}", format: "TA####" diff --git a/app/src/app/classes/stix/technique.ts b/app/src/app/classes/stix/technique.ts index 8923288c..fa2196b1 100644 --- a/app/src/app/classes/stix/technique.ts +++ b/app/src/app/classes/stix/technique.ts @@ -24,6 +24,7 @@ export class Technique extends StixObject { public is_subtechnique: boolean = false; + public readonly supportsAttackID = true; protected get attackIDValidator() { return { regex: this.is_subtechnique? "T\\d{4}\\.\\d{3}" : "T\\d{4}", format: this.is_subtechnique? "T####.###" : "T####" diff --git a/app/src/app/components/add-relationship-button/add-relationship-button.component.ts b/app/src/app/components/add-relationship-button/add-relationship-button.component.ts index 0ddf34c7..079ef226 100644 --- a/app/src/app/components/add-relationship-button/add-relationship-button.component.ts +++ b/app/src/app/components/add-relationship-button/add-relationship-button.component.ts @@ -1,10 +1,11 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { MatDialog } from '@angular/material/dialog'; -import { EMPTY, empty, zip } from 'rxjs'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { EMPTY } from 'rxjs'; import { Relationship } from 'src/app/classes/stix/relationship'; import { StixObject } from 'src/app/classes/stix/stix-object'; import { RestApiConnectorService } from 'src/app/services/connectors/rest-api/rest-api-connector.service'; import { StixDialogComponent } from 'src/app/views/stix/stix-dialog/stix-dialog.component'; +import { StixViewConfig } from 'src/app/views/stix/stix-view-page'; @Component({ selector: 'app-add-relationship-button', @@ -24,22 +25,35 @@ export class AddRelationshipButtonComponent implements OnInit { let relationship = new Relationship(); relationship.relationship_type = this.config.relationship_type; let initializer = null; - if (this.config.source_object) initializer = relationship.set_source_object(this.config.source_object); + if (this.config.source_object) initializer = relationship.set_source_object(this.config.source_object, this.restApiService); else if (this.config.source_ref) initializer = relationship.set_source_ref(this.config.source_ref, this.restApiService); - else if (this.config.target_object) initializer = relationship.set_target_object(this.config.target_object); + else if (this.config.target_object) initializer = relationship.set_target_object(this.config.target_object, this.restApiService); else if (this.config.target_ref) initializer = relationship.set_target_ref(this.config.target_ref, this.restApiService); else initializer = EMPTY; this.loading = true; var zip_subscription = initializer.subscribe({ next: () => { this.loading = false; + let config: StixViewConfig = { + object: relationship, + editable: true, + mode: "edit", + sidebarControl: "events" + } + + // if a dialog reference is provided, replace the active + // content with the relationship edit interface. This prevents + // a new dialog from opening over the current dialog. + if (this.config.dialog && this.config.dialog.componentInstance) { + this.config.dialog.componentInstance._config = config; + this.config.dialog.componentInstance.prevObject = this.config.source_object ? this.config.source_object : this.config.target_object; + this.config.dialog.componentInstance.startEditing(); + return; + } + + // open a new dialog let prompt = this.dialog.open(StixDialogComponent, { - data: { - object: relationship, - editable: true, - mode: "edit", - sidebarControl: "events" - }, + data: config, maxHeight: "75vh" }) let subscription = prompt.afterClosed().subscribe({ @@ -50,7 +64,6 @@ export class AddRelationshipButtonComponent implements OnInit { complete: () => { if (zip_subscription) zip_subscription.unsubscribe(); } //for some reason zip_subscription doesn't exist if using set_source_object or set_target_object }) } - } export interface AddRelationshipButtonConfig { @@ -61,4 +74,9 @@ export interface AddRelationshipButtonConfig { source_object?: StixObject; //initial relationship source object. Takes precedence over source_ref if both are specified, and is much faster to execute target_ref?: string; //initial relationship target ref target_object?: StixObject; //initial relationship target object. Takes precedence over target_ref if both are specified, and is much faster to execute + /** + * reference to the current working dialog. This is relevant when adding a new relationship from within the dialog. + * If provided, the 'create relationship' interface will replace the dialog content. + */ + dialog?: MatDialogRef } \ No newline at end of file diff --git a/app/src/app/components/collection-import-summary/collection-import-summary.component.html b/app/src/app/components/collection-import-summary/collection-import-summary.component.html index d11f0e03..c9f2f39e 100644 --- a/app/src/app/components/collection-import-summary/collection-import-summary.component.html +++ b/app/src/app/components/collection-import-summary/collection-import-summary.component.html @@ -1,9 +1,9 @@
- + - {{attackType}} ({{config.object_import_categories[attackType].object_count}}) + {{format(attackType)}} ({{config.object_import_categories[attackType].object_count}}) diff --git a/app/src/app/components/collection-import-summary/collection-import-summary.component.ts b/app/src/app/components/collection-import-summary/collection-import-summary.component.ts index ff823737..2b6bed6c 100644 --- a/app/src/app/components/collection-import-summary/collection-import-summary.component.ts +++ b/app/src/app/components/collection-import-summary/collection-import-summary.component.ts @@ -1,6 +1,8 @@ import { SelectionModel } from '@angular/cdk/collections'; import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; import { CollectionDiffCategories } from 'src/app/classes/stix/collection'; +import { DataComponent } from 'src/app/classes/stix/data-component'; +import { DataSource } from 'src/app/classes/stix/data-source'; import { Group } from 'src/app/classes/stix/group'; import { Matrix } from 'src/app/classes/stix/matrix'; import { Mitigation } from 'src/app/classes/stix/mitigation'; @@ -23,6 +25,9 @@ export class CollectionImportSummaryComponent implements OnInit { ngOnInit(): void { } + public format(attackType: string): string { + return attackType.replace(/_/g, ' '); + } } export interface CollectionImportSummaryConfig { @@ -33,7 +38,9 @@ export interface CollectionImportSummaryConfig { relationship: CollectionDiffCategories, mitigation: CollectionDiffCategories, matrix: CollectionDiffCategories, - group: CollectionDiffCategories + group: CollectionDiffCategories, + data_source: CollectionDiffCategories, + data_component: CollectionDiffCategories }; select?: SelectionModel; } diff --git a/app/src/app/components/object-status/object-status.component.ts b/app/src/app/components/object-status/object-status.component.ts index a3d7e8f1..97343950 100644 --- a/app/src/app/components/object-status/object-status.component.ts +++ b/app/src/app/components/object-status/object-status.component.ts @@ -4,7 +4,6 @@ import { FormControl } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { PopoverContentComponent } from 'ngx-smart-popover'; import { forkJoin } from 'rxjs'; -import { Collection } from 'src/app/classes/stix/collection'; import { Relationship } from 'src/app/classes/stix/relationship'; import { StixObject } from 'src/app/classes/stix/stix-object'; import { RestApiConnectorService } from 'src/app/services/connectors/rest-api/rest-api-connector.service'; @@ -27,7 +26,7 @@ export class ObjectStatusComponent implements OnInit { @ViewChild("objectStatus", {static: false}) public popover: PopoverContentComponent; public objects: StixObject[]; public object: StixObject; - public relationships; + public relationships = []; public revoked: boolean = false; public deprecated: boolean = false; @@ -57,6 +56,8 @@ export class ObjectStatusComponent implements OnInit { else if (this.editorService.type == "tactic") data$ = this.restAPIService.getAllTactics(options); else if (this.editorService.type == "technique") data$ = this.restAPIService.getAllTechniques(options); else if (this.editorService.type == "collection") data$ = this.restAPIService.getAllCollections(options); + else if (this.editorService.type == "data-source") data$ = this.restAPIService.getAllDataSources(options); + else if (this.editorService.type == "data-component") data$ = this.restAPIService.getAllDataComponents(options); let objSubscription = data$.subscribe({ next: (data) => { this.objects = data.data; @@ -72,11 +73,23 @@ export class ObjectStatusComponent implements OnInit { complete: () => { objSubscription.unsubscribe() } }); + if (this.editorService.type == 'data-source') { + // retrieve related data components & their relationships + data$ = this.restAPIService.getAllRelatedToDataSource(this.editorService.stixId); + let dataSubscription = data$.subscribe({ + next: (results) => { + this.relationships = this.relationships.concat(results); + }, + complete: () => { dataSubscription.unsubscribe(); } + }); + } + // retrieve relationships with the object data$ = this.restAPIService.getRelatedTo({sourceOrTargetRef: this.editorService.stixId}); let relSubscription = data$.subscribe({ next: (data) => { - this.relationships = data.data as Relationship[]; + let relationships = data.data as Relationship[]; + this.relationships = this.relationships.concat(relationships) this.loaded = true; setTimeout(() => this.popover.updatePosition()); //after render cycle update popover position since it has new content }, @@ -198,40 +211,41 @@ export class ObjectStatusComponent implements OnInit { let confirmationSub = confirmationPrompt.afterClosed().subscribe({ next: (result) => { - if (result) { - // deprecate object - if (revoked) this.object.revoked = true; - else this.object.deprecated = true; - saves.push(this.object.save(this.restAPIService)); - - // update relationships with the object - for (let relationship of this.relationships) { - if (!relationship.deprecated) { - relationship.deprecated = true; - saves.push(relationship.save(this.restAPIService)); - } - } - - if (revoked_by_id) { - // create a new 'revoked-by' relationship - let revokedRelationship = new Relationship(); - revokedRelationship.relationship_type = 'revoked-by'; - revokedRelationship.source_ref = this.object.stixID; - revokedRelationship.target_ref = revoked_by_id; - saves.push(revokedRelationship.save(this.restAPIService)); - } - - // complete save calls - let saveSubscription = forkJoin(saves).subscribe({ - complete: () => { - this.editorService.onReload.emit(); - saveSubscription.unsubscribe(); - } - }); - } else { // user cancelled + if (!result) { // user cancelled if (revoked) this.revoked = false; else this.deprecated = false; + return; } + + // deprecate or revoke object + if (revoked) this.object.revoked = true; + else this.object.deprecated = true; + saves.push(this.object.save(this.restAPIService)); + + // update relationships with the object + for (let relationship of this.relationships) { + if (!relationship.deprecated) { + relationship.deprecated = true; + saves.push(relationship.save(this.restAPIService)); + } + } + + if (revoked_by_id) { + // create a new 'revoked-by' relationship + let revokedRelationship = new Relationship(); + revokedRelationship.relationship_type = 'revoked-by'; + revokedRelationship.source_ref = this.object.stixID; + revokedRelationship.target_ref = revoked_by_id; + saves.push(revokedRelationship.save(this.restAPIService)); + } + + // complete save calls + let saveSubscription = forkJoin(saves).subscribe({ + complete: () => { + this.editorService.onReload.emit(); + saveSubscription.unsubscribe(); + } + }); }, complete: () => { confirmationSub.unsubscribe(); } }); diff --git a/app/src/app/components/resources-drawer/history-timeline/history-timeline.component.ts b/app/src/app/components/resources-drawer/history-timeline/history-timeline.component.ts index 42817263..65a41ab9 100644 --- a/app/src/app/components/resources-drawer/history-timeline/history-timeline.component.ts +++ b/app/src/app/components/resources-drawer/history-timeline/history-timeline.component.ts @@ -152,6 +152,8 @@ export class HistoryTimelineComponent implements OnInit, OnDestroy { else if (objectType == "tactic") objects$ = this.restAPIConnectorService.getTactic(objectStixID, null, "all"); else if (objectType == "technique") objects$ = this.restAPIConnectorService.getTechnique(objectStixID, null, "all"); else if (objectType == "collection") objects$ = this.restAPIConnectorService.getCollection(objectStixID, null, "all"); + else if (objectType == "data-source") objects$ = this.restAPIConnectorService.getDataSource(objectStixID, null, "all"); + else if (objectType == "data-component") objects$ = this.restAPIConnectorService.getDataComponent(objectStixID, null, "all"); // set up subscribers to get relationships let relationships$ = this.restAPIConnectorService.getRelatedTo({sourceOrTargetRef: objectStixID, versions: "all"}); // join subscribers diff --git a/app/src/app/components/stix/list-property/list-edit/list-edit.component.html b/app/src/app/components/stix/list-property/list-edit/list-edit.component.html index f5efa816..eb07aa35 100644 --- a/app/src/app/components/stix/list-property/list-edit/list-edit.component.html +++ b/app/src/app/components/stix/list-property/list-edit/list-edit.component.html @@ -2,31 +2,32 @@ {{config.label? config.label : config.field}} - + {{value}} cancel + (matChipInputTokenEnd)="add($event)"/> + [matTooltip]="selectControl.disabled && !config.disabled ? 'a valid domain must be selected first' : null"> Loading {{config.label? config.label : config.field}}... {{config.label? config.label : config.field}} - + - {{value}} cancel + {{value}} cancel diff --git a/app/src/app/components/stix/list-property/list-edit/list-edit.component.ts b/app/src/app/components/stix/list-property/list-edit/list-edit.component.ts index 527975f5..a2edb346 100644 --- a/app/src/app/components/stix/list-property/list-edit/list-edit.component.ts +++ b/app/src/app/components/stix/list-property/list-edit/list-edit.component.ts @@ -2,7 +2,7 @@ import { AfterContentChecked, ChangeDetectorRef, Component, Input, OnInit, ViewE import { ListPropertyConfig } from '../list-property.component'; import { COMMA, ENTER } from '@angular/cdk/keycodes'; import { MatChipInputEvent } from '@angular/material/chips'; -import { FormControl } from '@angular/forms'; +import { FormControl, Validators } from '@angular/forms'; import { MatOptionSelectionChange } from '@angular/material/core'; import { MatDialog } from '@angular/material/dialog'; import { RestApiConnectorService } from 'src/app/services/connectors/rest-api/rest-api-connector.service'; @@ -35,7 +35,9 @@ export class ListEditComponent implements OnInit, AfterContentChecked { "tactic_type": "x_mitre_tactic_type", "impact_type": "x_mitre_impact_type", "effective_permissions": "x_mitre_effective_permissions", - "permissions_required": "x_mitre_permissions_required" + "permissions_required": "x_mitre_permissions_required", + "collection_layers": "x_mitre_collection_layers", + "data_sources": "x_mitre_data_sources" } public domains = [ "enterprise-attack", @@ -49,24 +51,28 @@ export class ListEditComponent implements OnInit, AfterContentChecked { public allObjects: StixObject[] = []; // any value (editType: 'any') + public inputControl: FormControl; readonly separatorKeysCodes: number[] = [ENTER, COMMA]; constructor(public dialog: MatDialog, private restAPIConnectorService: RestApiConnectorService, private ref: ChangeDetectorRef) { } ngOnInit(): void { - this.selectControl = new FormControl(this.config.object[this.config.field]); + this.selectControl = new FormControl({value: this.config.object[this.config.field], disabled: this.config.disabled ? this.config.disabled : false}); + this.inputControl = new FormControl(null, this.config.required ? [Validators.required] : undefined); if (this.config.field == 'platforms' || this.config.field == 'tactic_type' || this.config.field == 'permissions_required' || this.config.field == 'effective_permissions' || this.config.field == 'impact_type' - || this.config.field == 'domains') { + || this.config.field == 'domains' + || this.config.field == 'collection_layers' + || this.config.field == 'data_sources') { if (!this.dataLoaded) { let data$ = this.restAPIConnectorService.getAllAllowedValues(); this.sub = data$.subscribe({ next: (data) => { let stixObject = this.config.object as StixObject; - this.allAllowedValues = data.find(obj => { return obj.objectType == stixObject.attackType; }) + this.allAllowedValues = data.find(obj => { return obj.objectType == stixObject.attackType; }); this.dataLoaded = true; }, complete: () => { this.sub.unsubscribe(); } @@ -194,6 +200,13 @@ export class ListEditComponent implements OnInit, AfterContentChecked { }); } + // check for existing data + if (this.selectControl.value) { + for (let value of this.selectControl.value) { + if (!values.includes(value)) values.push(value); + } + } + if (!values.length) { // disable field and reset selection this.selectControl.disable(); @@ -210,6 +223,7 @@ export class ListEditComponent implements OnInit, AfterContentChecked { public add(event: MatChipInputEvent): void { if (event.value && event.value.trim()) { this.config.object[this.config.field].push(event.value.trim()); + this.inputControl.setValue(this.config.object[this.config.field]); } if (event.input) { event.input.value = ''; // reset input value @@ -222,6 +236,7 @@ export class ListEditComponent implements OnInit, AfterContentChecked { if (i >= 0) { this.config.object[this.config.field].splice(i, 1); } + this.inputControl.setValue(this.config.object[this.config.field]) } /** Remove selection from via chip cancel button */ diff --git a/app/src/app/components/stix/list-property/list-property.component.ts b/app/src/app/components/stix/list-property/list-property.component.ts index 5b5c8a05..098051d0 100644 --- a/app/src/app/components/stix/list-property/list-property.component.ts +++ b/app/src/app/components/stix/list-property/list-property.component.ts @@ -29,6 +29,10 @@ export interface ListPropertyConfig { object: StixObject | [StixObject, StixObject]; /* Edit mode. Default: 'any' */ editType?: "select" | "stixList" | "any"; + /* If true, the field will be disabled. Default false if omitted. */ + disabled?: boolean; + /* If true, the field will be required. Default false if omitted. */ + required?: boolean; /* the field of the object(s) to visualize as a list */ field: string; /* if specified, label with this string instead of field */ diff --git a/app/src/app/components/stix/name-property/name-property.component.html b/app/src/app/components/stix/name-property/name-property.component.html index 052df9f6..95337cab 100644 --- a/app/src/app/components/stix/name-property/name-property.component.html +++ b/app/src/app/components/stix/name-property/name-property.component.html @@ -1,12 +1,12 @@

- {{config.object.parentTechnique.name}}: {{config.object[config.field ? config.field : "name"]}} + {{config.parent.name}}: {{config.object[config.field ? config.field : "name"]}} name - {{config.object.parentTechnique.name}}: + {{config.parent.name}}: diff --git a/app/src/app/components/stix/name-property/name-property.component.ts b/app/src/app/components/stix/name-property/name-property.component.ts index 8b594e85..972ff846 100644 --- a/app/src/app/components/stix/name-property/name-property.component.ts +++ b/app/src/app/components/stix/name-property/name-property.component.ts @@ -46,6 +46,10 @@ export interface NamePropertyConfig { * Note: if mode is diff, pass an array of two objects to diff */ object: StixObject | [StixObject, StixObject]; + /* The parent object. If specified, the object name will be + * prefixed with the name of the parent + */ + parent?: StixObject; /* the field of the object(s) to visualize as a name * If unspecified, uses 'name' field as defined on StixObject */ diff --git a/app/src/app/components/stix/stix-list/stix-list.component.ts b/app/src/app/components/stix/stix-list/stix-list.component.ts index 4630c1ae..48433bda 100644 --- a/app/src/app/components/stix/stix-list/stix-list.component.ts +++ b/app/src/app/components/stix/stix-list/stix-list.component.ts @@ -43,6 +43,7 @@ export class StixListComponent implements OnInit, AfterViewInit, OnDestroy { @Input() public config: StixListConfig = {}; @Output() public onRowAction = new EventEmitter(); @Output() public onSelect = new EventEmitter(); + @Output() public refresh = new EventEmitter(); @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild('search') search: ElementRef; public searchQuery: string = ""; @@ -114,7 +115,12 @@ export class StixListComponent implements OnInit, AfterViewInit, OnDestroy { maxHeight: "75vh" }) let subscription = prompt.afterClosed().subscribe({ - next: result => { if (prompt.componentInstance.dirty) this.applyControls(); }, //re-fetch values since an edit occurred + next: result => { + if (prompt.componentInstance.dirty) { //re-fetch values since an edit occurred + this.applyControls(); + this.refresh.emit(); + } + }, complete: () => { subscription.unsubscribe(); } }); } @@ -171,7 +177,7 @@ export class StixListComponent implements OnInit, AfterViewInit, OnDestroy { if ("type" in this.config) { // this.filter.push("type." + this.config.type); // set columns according to type - switch(this.config.type) { + switch(this.config.type.replace(/_/g, '-')) { case "collection": case "collection-created": this.addColumn("name", "name", "plain", sticky_allowed, ["name"]); @@ -250,6 +256,7 @@ export class StixListComponent implements OnInit, AfterViewInit, OnDestroy { "display": "descriptive" }] break; + case "data-source": case "technique": this.addColumn("", "workflow", "icon"); this.addColumn("", "state", "icon"); @@ -265,10 +272,23 @@ export class StixListComponent implements OnInit, AfterViewInit, OnDestroy { "display": "descriptive" }] break; + case "data-component": + this.addColumn("name", "name", "plain", sticky_allowed, ["name"]); + this.addColumn("domain", "domains", "list"); + this.addColumn("version", "version", "version"); + this.addColumn("modified","modified", "timestamp"); + this.addColumn("created", "created", "timestamp"); + this.tableDetail = [{ + "field": "description", + "display": "descriptive" + }] + break; case "relationship": this.addColumn("", "state", "icon"); - this.addColumn("source", "source_ID", "plain"); - this.addColumn("", "source_name", "plain", this.config.targetRef? sticky_allowed: false, ["relationship-name"]);// ["name", "relationship-left"]); + if (this.config.relationshipType && this.config.relationshipType !== "detects") { + this.addColumn("source", "source_ID", "plain"); + this.addColumn("", "source_name", "plain", this.config.targetRef? sticky_allowed: false, ["relationship-name"]);// ["name", "relationship-left"]); + } else this.addColumn("source", "source_name", "plain", this.config.targetRef? sticky_allowed: false, ["relationship-name"]); this.addColumn("type", "relationship_type", "plain", false, ["text-deemphasis", "relationship-joiner"]); this.addColumn("target", "target_ID", "plain"); this.addColumn("", "target_name", "plain", this.config.sourceRef? sticky_allowed: false, ["relationship-name"]);// ["name", "relationship-right"]); @@ -462,6 +482,8 @@ export class StixListComponent implements OnInit, AfterViewInit, OnDestroy { else if (this.config.type == "technique") this.data$ = this.restAPIConnectorService.getAllTechniques(options); else if (this.config.type.includes("collection")) this.data$ = this.restAPIConnectorService.getAllCollections({search: this.searchQuery, versions: "all"}); else if (this.config.type == "relationship") this.data$ = this.restAPIConnectorService.getRelatedTo({sourceRef: this.config.sourceRef, targetRef: this.config.targetRef, sourceType: this.config.sourceType, targetType: this.config.targetType, relationshipType: this.config.relationshipType, excludeSourceRefs: this.config.excludeSourceRefs, excludeTargetRefs: this.config.excludeTargetRefs, limit: limit, offset: offset, includeDeprecated: deprecated}); + else if (this.config.type == "data-source") this.data$ = this.restAPIConnectorService.getAllDataSources(options); + else if (this.config.type == "data-component") this.data$ = this.restAPIConnectorService.getAllDataComponents(options); let subscription = this.data$.subscribe({ next: (data) => { this.totalObjectCount = data.pagination.total; }, complete: () => { subscription.unsubscribe() } @@ -487,7 +509,7 @@ export class StixListComponent implements OnInit, AfterViewInit, OnDestroy { } //allowed types for StixListConfig -type type_attacktype = "collection" | "group" | "matrix" | "mitigation" | "software" | "tactic" | "technique" | "relationship"; +type type_attacktype = "collection" | "group" | "matrix" | "mitigation" | "software" | "tactic" | "technique" | "relationship" | "data-source" | "data-component"; type selection_types = "one" | "many" | "disabled" export interface StixListConfig { /* if specified, shows the given STIX objects in the table instead of loading from the back-end based on other configurations. */ diff --git a/app/src/app/services/connectors/api-connector.ts b/app/src/app/services/connectors/api-connector.ts index 3da308b2..361969ec 100644 --- a/app/src/app/services/connectors/api-connector.ts +++ b/app/src/app/services/connectors/api-connector.ts @@ -23,11 +23,12 @@ export abstract class ApiConnector { /** * Log the error and then raise it to the next level + * @param {boolean} showSnack if true, show the error snackbar */ - protected handleError_raise() { + protected handleError_raise(showSnack: boolean = true) { return (error: any): Observable => { logger.error(error); - this.errorSnack(error); + if (showSnack) this.errorSnack(error); return throwError(error); } } diff --git a/app/src/app/services/connectors/rest-api/rest-api-connector.service.ts b/app/src/app/services/connectors/rest-api/rest-api-connector.service.ts index 831f1afa..317f0f36 100644 --- a/app/src/app/services/connectors/rest-api/rest-api-connector.service.ts +++ b/app/src/app/services/connectors/rest-api/rest-api-connector.service.ts @@ -1,8 +1,8 @@ import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { MatSnackBar } from '@angular/material/snack-bar'; -import { forkJoin, Observable, of, ReplaySubject } from 'rxjs'; -import { tap, catchError, map, share, switchMap } from 'rxjs/operators'; +import { forkJoin, Observable, of } from 'rxjs'; +import { tap, catchError, map, share, switchMap, mergeMap } from 'rxjs/operators'; import { CollectionIndex } from 'src/app/classes/collection-index'; import { ExternalReference } from 'src/app/classes/external-references'; import { Collection } from 'src/app/classes/stix/collection'; @@ -20,9 +20,11 @@ import { Technique } from 'src/app/classes/stix/technique'; import { environment } from "../../../../environments/environment"; import { ApiConnector } from '../api-connector'; import { logger } from "../../../util/logger"; +import { DataSource } from 'src/app/classes/stix/data-source'; +import { DataComponent } from 'src/app/classes/stix/data-component'; //attack types -type AttackType = "collection" | "group" | "matrix" | "mitigation" | "software" | "tactic" | "technique" | "relationship" | "note" | "identity" | "marking-definition"; +type AttackType = "collection" | "group" | "matrix" | "mitigation" | "software" | "tactic" | "technique" | "relationship" | "note" | "identity" | "marking-definition" | "data-source" | "data-component"; // pluralize AttackType const attackTypeToPlural = { "technique": "techniques", @@ -35,7 +37,9 @@ const attackTypeToPlural = { "relationship": "relationships", "note": "notes", "identity": "identities", - "marking-definition": "marking-definitions" + "marking-definition": "marking-definitions", + "data-source": "data-sources", + "data-component": "data-components" } // transform AttackType to the relevant class const attackTypeToClass = { @@ -49,7 +53,9 @@ const attackTypeToClass = { "relationship": Relationship, "note": Note, "identity": Identity, - "marking-definition": MarkingDefinition + "marking-definition": MarkingDefinition, + "data-source": DataSource, + "data-component": DataComponent } // transform AttackType to the relevant class @@ -64,7 +70,9 @@ const stixTypeToClass = { "x-mitre-collection": Collection, "relationship": Relationship, "identity": Identity, - "marking-definition": MarkingDefinition + "marking-definition": MarkingDefinition, + "x-mitre-data-source": DataSource, + "x-mitre-data-component": DataComponent } export interface Paginated { @@ -228,6 +236,28 @@ export class RestApiConnectorService extends ApiConnector { * @returns {Observable} observable of retrieved objects */ public get getAllMitigations() { return this.getStixObjectsFactory("mitigation"); } + /** + * Get all data sources + * @param {number} [limit] the number of data sources to retrieve + * @param {number} [offset] the number of data sources to skip + * @param {string} [state] if specified, only get objects with this state + * @param {boolean} [revoked] if true, get revoked objects + * @param {boolean} [deprecated] if true, get deprecated objects + * @param {string[]} [excludeIDs] if specified, excludes these STIX IDs from the result + * @returns {Observable} observable of retrieved objects + */ + public get getAllDataSources() { return this.getStixObjectsFactory("data-source"); } + /** + * Get all data components + * @param {number} [limit] the number of data components to retrieve + * @param {number} [offset] the number of data components to skip + * @param {string} [state] if specified, only get objects with this state + * @param {boolean} [revoked] if true, get revoked objects + * @param {boolean} [deprecated] if true, get deprecated objects + * @param {string[]} [excludeIDs] if specified, excludes these STIX IDs from the result + * @returns {Observable} observable of retrieved objects + */ + public get getAllDataComponents() { return this.getStixObjectsFactory("data-component"); } /** * Get all matrices * @param {number} [limit] the number of matrices to retrieve @@ -349,7 +379,7 @@ export class RestApiConnectorService extends ApiConnector { private getStixObjectFactory(attackType: AttackType) { let attackClass = attackTypeToClass[attackType]; let plural = attackTypeToPlural[attackType] - return function

(id: string, modified?: Date | string, versions="latest", includeSubs?: boolean, retrieveContents?: boolean): Observable { + return function

(id: string, modified?: Date | string, versions="latest", includeSubs?: boolean, retrieveContents?: boolean, retrieveDataComponents?: boolean): Observable { let url = `${this.baseUrl}/${plural}/${id}`; if (modified) { let modifiedString = typeof(modified) == "string"? modified : modified.toISOString(); @@ -358,6 +388,7 @@ export class RestApiConnectorService extends ApiConnector { let query = new HttpParams(); if (versions != "latest") query = query.set("versions", versions); if (attackType == "collection" && retrieveContents) query = query.set("retrieveContents", "true"); + if (attackType == "data-source" && retrieveDataComponents) query = query.set("retrieveDataComponents", "true"); return this.http.get(url, {headers: this.headers, params: query}).pipe( tap(result => logger.log(`retrieved ${attackType}`, result)), // on success, trigger the success notification map(result => { @@ -406,6 +437,19 @@ export class RestApiConnectorService extends ApiConnector { ); } }), + switchMap(result => { // fetch parent data source of data component + let x = result as any[]; + if (x[0].attackType != "data-component") return of(result); + let d = x[0] as DataComponent; + return this.getDataSource(d.data_source_ref).pipe( // fetch data source from REST API + map(data_source => { + let ds = data_source as DataSource[]; + d.data_source = ds[0]; + return [d]; + }), + tap(data_component => logger.log("fetched data source of", data_component)) + ); + }), catchError(this.handleError_continue([])), // on error, trigger the error notification and continue operation without crashing (returns empty item) share() // multicast so that multiple subscribers don't trigger the call twice. THIS MUST BE THE LAST LINE OF THE PIPE ) @@ -452,6 +496,23 @@ export class RestApiConnectorService extends ApiConnector { * @returns {Observable} the object with the given ID and modified date */ public get getMitigation() { return this.getStixObjectFactory("mitigation"); } + /** + * Get a single data source by STIX ID + * @param {string} id the object STIX ID + * @param {Date} [modified] if specified, get the version modified at the given date + * @param {versions} [string] default "latest", if "all" returns all versions of the object instead of just the latest version. + * @param {retrieveDataComponents} [boolean] if true, include data components with a reference to the given object. Incompatible with versions="all" + * @returns {Observable} the object with the given ID and modified date + */ + public get getDataSource() { return this.getStixObjectFactory("data-source"); } + /** + * Get a single data component by STIX ID + * @param {string} id the object STIX ID + * @param {Date} [modified] if specified, get the version modified at the given date + * @param {versions} [string] default "latest", if "all" returns all versions of the object instead of just the latest version. + * @returns {Observable} the object with the given ID and modified date + */ + public get getDataComponent() { return this.getStixObjectFactory("data-component"); } /** * Get a single matrix by STIX ID * @param {string} id the object STIX ID @@ -540,6 +601,18 @@ export class RestApiConnectorService extends ApiConnector { * @returns {Observable} the created object */ public get postMitigation() { return this.postStixObjectFactory("mitigation"); } + /** + * POST (create) a new data source + * @param {DataSource} object the object to create + * @returns {Observable} the created object + */ + public get postDataSource() { return this.postStixObjectFactory("data-source"); } + /** + * POST (create) a new data component + * @param {DataComponent} object the object to create + * @returns {Observable} the created object + */ + public get postDataComponent() { return this.postStixObjectFactory("data-component"); } /** * POST (create) a new matrix * @param {Matrix} object the object to create @@ -631,6 +704,20 @@ export class RestApiConnectorService extends ApiConnector { * @returns {Observable} the updated object */ public get putMitigation() { return this.putStixObjectFactory("mitigation"); } + /** + * PUT (update) a data source + * @param {DataSource} object the object to update + * @param {Date} [modified] optional, the modified date to overwrite. If omitted, uses the modified field of the object + * @returns {Observable} the updated object + */ + public get putDataSource() { return this.putStixObjectFactory("data-source"); } + /** + * PUT (update) a data component + * @param {DataComponent} object the object to update + * @param {Date} [modified] optional, the modified date to overwrite. If omitted, uses the modified field of the object + * @returns {Observable} the updated object + */ + public get putDataComponent() { return this.putStixObjectFactory("data-component"); } /** * PUT (update) a matrix * @param {Matrix} object the object to update @@ -709,6 +796,20 @@ export class RestApiConnectorService extends ApiConnector { * @returns {Observable<{}>} observable of the response body */ public get deleteMitigation() { return this.deleteStixObjectFactory("mitigation"); } + /** + * DELETE a data source + * @param {string} id the STIX ID of the object to delete + * @param {Date} modified the modified date of the version to delete + * @returns {Observable<{}>} observable of the response body + */ + public get deleteDataSource() { return this.deleteStixObjectFactory("data-source"); } + /** + * DELETE a data component + * @param {string} id the STIX ID of the object to delete + * @param {Date} modified the modified date of the version to delete + * @returns {Observable<{}>} observable of the response body + */ + public get deleteDataComponent() { return this.deleteStixObjectFactory("data-component"); } /** * DELETE a matrix * @param {string} id the STIX ID of the object to delete @@ -823,6 +924,36 @@ export class RestApiConnectorService extends ApiConnector { ) } + /** + * Get all objects related to a data source + * @param id the STIX ID of the data source + * @returns list of data components related to the data source along with the data components' relationships with techniques + */ + public getAllRelatedToDataSource(id: string): Observable { + let dataComponents$ = this.getAllDataComponents(); + return dataComponents$.pipe( + map(result => { // get related data component objects + let dataComponents = result.data as DataComponent[]; + return dataComponents.filter(d => d.data_source_ref == id); + }), + mergeMap(dataComponents => { // get relationships for each data component + let relatedTo = dataComponents.map(dc => this.getRelatedTo({sourceOrTargetRef: dc.stixID})); + if (!relatedTo.length) return of(dataComponents); + return forkJoin(relatedTo).pipe( + map(relationships => { + let all_results: StixObject[] = []; + for(let relationship_result of relationships) { + all_results = all_results.concat(relationship_result.data) + } + return all_results.concat(dataComponents); + }) + ); + }), + catchError(this.handleError_continue([])), + share() + ); + } + // ___ ___ ___ ___ ___ ___ _ _ ___ ___ ___ // | _ \ __| __| __| _ \ __| \| |/ __| __/ __| // | / _|| _|| _|| / _|| .` | (__| _|\__ \ @@ -906,12 +1037,15 @@ export class RestApiConnectorService extends ApiConnector { * POST a collection bundle (including a collection SDO and the objects to which it refers) to the back-end * @param {*} collectionBundle the STIX bundle to write * @param {boolean} [preview] if true, preview the results of the import without actually committing the import + * @param {boolean} [force] if true, force import the collection + * @param {boolean} [suppressErrors] if true, suppress the error snackbar * @returns {Observable} collection object marking the results of the import */ - public postCollectionBundle(collectionBundle: any, preview: boolean = false): Observable { + public postCollectionBundle(collectionBundle: any, preview: boolean = false, force: boolean = false, suppressErrors: boolean = false): Observable { // add query params for preview let query = new HttpParams(); - if (preview) query = query.set("checkOnly", "true"); + if (preview) query = query.set("previewOnly", "true"); + if (force) query = query.set("forceImport", "all"); // perform the request return this.http.post(`${this.baseUrl}/collection-bundles`, collectionBundle, {headers: this.headers, params: query}).pipe( tap(result => { @@ -921,11 +1055,64 @@ export class RestApiConnectorService extends ApiConnector { map(result => { return new Collection(result); }), - catchError(this.handleError_raise()), + catchError(this.handleError_raise(!suppressErrors)), share() ) } + /** + * Preview a collection bundle. + * POST the collection bundle to the back end to retrieve a preview of the import results. A second POST + * call will occur (with ?forceImport='all') if the first POST call results in an overridable import error. + * This is done in order to view the import errors alongside a preview of the import results. + * @param collectionBundle the STIX bundle to preview + * @returns {Observable} the collection object and any import errors as result of the preview import + */ + public previewCollectionBundle(collectionBundle: any): Observable { + // perform preview request + return this.postCollectionBundle(collectionBundle, true, false, true).pipe( + map(result => { + return {error: undefined, preview: result}; + }), + catchError(err => { + // check if import can be forced + if (this.cannotForceImport(err)) { + return of({error: err.error, preview: undefined}); + } + // force request + return this.postCollectionBundle(collectionBundle, true, true, true).pipe( + map(force_result => { + return {error: err.error, preview: force_result}; + }), + catchError(this.handleError_raise()) + ) + }), + share() + ); + } + + /** + * Determine if the user cannot force import a collection when the post collection call fails. + * Users cannot force an import when: + * 1. the collection bundle has more than one collection object + * 2. the collection bundle does not have a collection object + * 3. the collection is badly formatted + * 4. the collection contains duplicate objects + * @param err the resulting error from previewing the collection + * @returns true if the user cannot force import the collection; false otherwise + */ + private cannotForceImport(err: any): boolean { + if (err.status == "400") { + let bundleErrors = err.error.bundleErrors; + let objectErrors = err.error.objectErrors.summary; + if (bundleErrors.noCollection || bundleErrors.moreThanOneCollection || bundleErrors.badlyFormattedCollection || objectErrors.duplicateObjectInBundleCount) { + return true; + } + return false; + } + return true; + } + /** * Get a collection bundle * @param {string} id STIX ID of collection @@ -1076,8 +1263,8 @@ export class RestApiConnectorService extends ApiConnector { * @param data: the data to download. Must be a JSON * @param filename: the name of the file to download */ - private triggerBrowserDownload(data: any, filename: string) { - let url = URL.createObjectURL(new Blob([JSON.stringify(data)], {type: "text/json"})); + public triggerBrowserDownload(data: any, filename: string) { + let url = URL.createObjectURL(new Blob([JSON.stringify(data, null, 4)], {type: "text/json"})); let downloadLink = document.createElement("a"); downloadLink.href = url; downloadLink.download = filename diff --git a/app/src/app/views/stix/collection/collection-export/collection-export-workflow/collection-export.component.html b/app/src/app/views/stix/collection/collection-export/collection-export-workflow/collection-export.component.html deleted file mode 100644 index d84670ea..00000000 --- a/app/src/app/views/stix/collection/collection-export/collection-export-workflow/collection-export.component.html +++ /dev/null @@ -1,36 +0,0 @@ -

- - -
-
- - name - - name is required - - - version - - invalid version number - version must be incremented - -
-
- - description - - -
-
- -
-
-
- - TODO - - - TODO export button - -
-
diff --git a/app/src/app/views/stix/collection/collection-export/collection-export-workflow/collection-export.component.scss b/app/src/app/views/stix/collection/collection-export/collection-export-workflow/collection-export.component.scss deleted file mode 100644 index 85cdf8f2..00000000 --- a/app/src/app/views/stix/collection/collection-export/collection-export-workflow/collection-export.component.scss +++ /dev/null @@ -1,14 +0,0 @@ -@import "../.../../../../../../style/globals"; -.collectionInfo { - mat-form-field {box-sizing: content-box;} - .name { - flex-grow: 1 - } - .version { - width: 25%; - } - .description { - width: 100%; - } - -} \ No newline at end of file diff --git a/app/src/app/views/stix/collection/collection-export/collection-export-workflow/collection-export.component.spec.ts b/app/src/app/views/stix/collection/collection-export/collection-export-workflow/collection-export.component.spec.ts deleted file mode 100644 index 35963e15..00000000 --- a/app/src/app/views/stix/collection/collection-export/collection-export-workflow/collection-export.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { CollectionExportComponent } from './collection-export.component'; - -describe('CollectionExportComponent', () => { - let component: CollectionExportComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ CollectionExportComponent ] - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(CollectionExportComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/app/src/app/views/stix/collection/collection-export/collection-export-workflow/collection-export.component.ts b/app/src/app/views/stix/collection/collection-export/collection-export-workflow/collection-export.component.ts deleted file mode 100644 index 07aa5d52..00000000 --- a/app/src/app/views/stix/collection/collection-export/collection-export-workflow/collection-export.component.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { Collection } from 'src/app/classes/stix/collection'; -import { ActivatedRoute } from '@angular/router'; -import { CollectionService } from 'src/app/services/stix/collection/collection.service'; -import { FormBuilder, FormGroup, Validators } from '@angular/forms'; -import { versionNumberIncrementValidator, versionNumberFormatValidator } from 'src/app/classes/version-number'; -import { GroupService } from 'src/app/services/stix/group/group.service'; -import { MatrixService } from 'src/app/services/stix/matrix/matrix.service'; -import { MitigationService } from 'src/app/services/stix/mitigation/mitigation.service'; -import { SoftwareService } from 'src/app/services/stix/software/software.service'; -import { TacticService } from 'src/app/services/stix/tactic/tactic.service'; -import { TechniqueService } from 'src/app/services/stix/technique/technique.service'; -import { Group } from 'src/app/classes/stix/group'; -import { Software } from 'src/app/classes/stix/software'; -import { Matrix } from 'src/app/classes/stix/matrix'; -import { Mitigation } from 'src/app/classes/stix/mitigation'; -import { Tactic } from 'src/app/classes/stix/tactic'; -import { Technique } from 'src/app/classes/stix/technique'; -import { StixObject } from 'src/app/classes/stix/stix-object'; - -@Component({ - selector: 'app-collection-export', - templateUrl: './collection-export.component.html', - styleUrls: ['./collection-export.component.scss'] -}) -export class CollectionExportComponent implements OnInit { - - public collection: Collection; - - public collectionInformation: FormGroup; - - // this won't actually be here, it's just for the mockups - public contents: StixObject[] = []; - - constructor(private route: ActivatedRoute, - private collectionService: CollectionService, - private formBuilder: FormBuilder, - - private groupService: GroupService, - private matrixService: MatrixService, - private mitigationService: MitigationService, - private softwareService: SoftwareService, - private tacticService: TacticService, - private techniqueService: TechniqueService) { } - - ngOnInit() { - let id = this.route.snapshot.paramMap.get("id"); - if (id) this.collection = this.collectionService.get(id, true); - else this.collection = new Collection(); - - let versionValidators = id ? [ //if existing collection, require increment - Validators.required, //field is required - versionNumberFormatValidator(), //must be formatted correctly - versionNumberIncrementValidator(this.collection.version) //must be incremented compared to previous collection version - ] : [ //if new collection, do not require increment - Validators.required, //field is required - versionNumberFormatValidator(), //must be formatted correctly - ] - - this.collectionInformation = this.formBuilder.group({ - name: [this.collection.name, Validators.required], - description: [this.collection.description], - version: [this.collection.version.toString(), versionValidators], - }) - this.collectionInformation.markAllAsTouched(); // force field validation on page load - for (let service of [this.groupService, - this.matrixService, - this.mitigationService, - this.softwareService, - this.tacticService, - this.techniqueService]) { - this.contents = this.contents.concat(service.getAll()); - } - } - - - -} diff --git a/app/src/app/views/stix/collection/collection-export/collection-exported-list/collection-exported-list.component.html b/app/src/app/views/stix/collection/collection-export/collection-exported-list/collection-exported-list.component.html deleted file mode 100644 index f7dbe07e..00000000 --- a/app/src/app/views/stix/collection/collection-export/collection-exported-list/collection-exported-list.component.html +++ /dev/null @@ -1 +0,0 @@ -

collection-exported-list works!

diff --git a/app/src/app/views/stix/collection/collection-export/collection-exported-list/collection-exported-list.component.scss b/app/src/app/views/stix/collection/collection-export/collection-exported-list/collection-exported-list.component.scss deleted file mode 100644 index e69de29b..00000000 diff --git a/app/src/app/views/stix/collection/collection-export/collection-exported-list/collection-exported-list.component.spec.ts b/app/src/app/views/stix/collection/collection-export/collection-exported-list/collection-exported-list.component.spec.ts deleted file mode 100644 index 27cb8aaa..00000000 --- a/app/src/app/views/stix/collection/collection-export/collection-exported-list/collection-exported-list.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { CollectionExportedListComponent } from './collection-exported-list.component'; - -describe('CollectionExportedListComponent', () => { - let component: CollectionExportedListComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ CollectionExportedListComponent ] - }) - .compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(CollectionExportedListComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/app/src/app/views/stix/collection/collection-export/collection-exported-list/collection-exported-list.component.ts b/app/src/app/views/stix/collection/collection-export/collection-exported-list/collection-exported-list.component.ts deleted file mode 100644 index ccee708c..00000000 --- a/app/src/app/views/stix/collection/collection-export/collection-exported-list/collection-exported-list.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component, OnInit } from '@angular/core'; - -@Component({ - selector: 'app-collection-exported-list', - templateUrl: './collection-exported-list.component.html', - styleUrls: ['./collection-exported-list.component.scss'] -}) -export class CollectionExportedListComponent implements OnInit { - - constructor() { } - - ngOnInit(): void { - } - -} diff --git a/app/src/app/views/stix/collection/collection-import/collection-import-error/collection-import-error.component.html b/app/src/app/views/stix/collection/collection-import/collection-import-error/collection-import-error.component.html new file mode 100644 index 00000000..e422d645 --- /dev/null +++ b/app/src/app/views/stix/collection/collection-import/collection-import-error/collection-import-error.component.html @@ -0,0 +1,48 @@ +
+ +

+ ERROR: the data you are trying to import contains errors and cannot be imported: +

+
+
    +
  • + The collection bundle does not include an x-mitre-collection object. +
  • +
  • + The collection bundle contains multiple x-mitre-collection objects. +
  • +
  • + The collection bundle is improperly formatted. +
  • +
  • + The collection bundle contains {{duplicateObjects}} duplicated {{duplicateObjects > 1 ? 'objects' : 'object'}}. +
  • +
+
+
+ + +

+ WARNING: the data you are trying to import contains some problems which may affect your knowledge base if you proceed with the import: +

+
+
    +
  • + The collection bundle already exists in the database. Some objects may not be imported. +
  • +
  • + {{objSpecVersionViolations}} {{objSpecVersionViolations > 1 ? 'objects use' : 'object uses'}} a newer version of the ATT&CK spec than your Workbench supports and will not be imported. After upgrading Workbench to the latest version you can re-import the data to acquire {{objSpecVersionViolations > 1 ? 'these objects' : 'this object'}}. +
  • +
+
+
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/app/src/app/views/stix/collection/collection-import/collection-import-error/collection-import-error.component.scss b/app/src/app/views/stix/collection/collection-import/collection-import-error/collection-import-error.component.scss new file mode 100644 index 00000000..e33760f9 --- /dev/null +++ b/app/src/app/views/stix/collection/collection-import/collection-import-error/collection-import-error.component.scss @@ -0,0 +1,10 @@ +@import "../../../../../../style/colors"; + +.collection-import-error { + .warn { + color: color(warn); + } + .error { + color: color(error); + } +} diff --git a/app/src/app/views/stix/collection/collection-import/collection-import-error/collection-import-error.component.spec.ts b/app/src/app/views/stix/collection/collection-import/collection-import-error/collection-import-error.component.spec.ts new file mode 100644 index 00000000..7aa1a31d --- /dev/null +++ b/app/src/app/views/stix/collection/collection-import/collection-import-error/collection-import-error.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CollectionImportErrorComponent } from './collection-import-error.component'; + +describe('CollectionImportErrorComponent', () => { + let component: CollectionImportErrorComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CollectionImportErrorComponent] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CollectionImportErrorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/app/src/app/views/stix/collection/collection-import/collection-import-error/collection-import-error.component.ts b/app/src/app/views/stix/collection/collection-import/collection-import-error/collection-import-error.component.ts new file mode 100644 index 00000000..a548171b --- /dev/null +++ b/app/src/app/views/stix/collection/collection-import/collection-import-error/collection-import-error.component.ts @@ -0,0 +1,29 @@ +import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core'; + +@Component({ + selector: 'app-collection-import-error', + templateUrl: './collection-import-error.component.html', + styleUrls: ['./collection-import-error.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class CollectionImportErrorComponent { + @Input() error: any; + @Output() onCancel = new EventEmitter(); + + public get hasWarnings(): boolean { + return this.duplicateCollection || this.objSpecVersionViolations > 0; + } + public get hasErrors(): boolean { + return this.noCollection || this.multipleCollections || this.badlyFormatted || this.duplicateObjects > 0; + } + + // can be overridden + public get duplicateCollection(): boolean { return this.error.bundleErrors.duplicateCollection; } + public get objSpecVersionViolations(): number { return this.error.objectErrors.summary.invalidAttackSpecVersionCount; } + + // cannot be overridden + public get noCollection(): boolean { return this.error.bundleErrors.noCollection; } + public get duplicateObjects(): number { return this.error.objectErrors.summary.duplicateObjectInBundleCount; } + public get multipleCollections(): boolean { return this.error.bundleErrors.moreThanOneCollection; } + public get badlyFormatted(): boolean { return this.error.bundleErrors.badlyFormattedCollection; } +} diff --git a/app/src/app/views/stix/collection/collection-import/collection-import-review/collection-import-review.component.ts b/app/src/app/views/stix/collection/collection-import/collection-import-review/collection-import-review.component.ts index ff6f2ed3..2a1b3f09 100644 --- a/app/src/app/views/stix/collection/collection-import/collection-import-review/collection-import-review.component.ts +++ b/app/src/app/views/stix/collection/collection-import/collection-import-review/collection-import-review.component.ts @@ -8,6 +8,8 @@ import { Relationship } from 'src/app/classes/stix/relationship'; import { Software } from 'src/app/classes/stix/software'; import { Tactic } from 'src/app/classes/stix/tactic'; import { Technique } from 'src/app/classes/stix/technique'; +import { DataSource } from 'src/app/classes/stix/data-source'; +import { DataComponent } from 'src/app/classes/stix/data-component'; import { EditorService } from 'src/app/services/editor/editor.service'; import { StixViewPage } from '../../../stix-view-page'; import { logger } from "../../../../../util/logger"; @@ -24,13 +26,15 @@ export class CollectionImportReviewComponent extends StixViewPage implements OnI public get collection(): Collection { return this.config.object as Collection; } public collection_import_categories = { - technique: new CollectionDiffCategories(), - tactic: new CollectionDiffCategories(), - software: new CollectionDiffCategories(), - relationship: new CollectionDiffCategories(), - mitigation: new CollectionDiffCategories(), - matrix: new CollectionDiffCategories(), - group: new CollectionDiffCategories() + technique: new CollectionDiffCategories(), + tactic: new CollectionDiffCategories(), + software: new CollectionDiffCategories(), + relationship: new CollectionDiffCategories(), + mitigation: new CollectionDiffCategories(), + matrix: new CollectionDiffCategories(), + group: new CollectionDiffCategories(), + data_source: new CollectionDiffCategories(), + data_component: new CollectionDiffCategories() } constructor(private route: ActivatedRoute, public editor: EditorService) { super() } @@ -88,6 +92,12 @@ export class CollectionImportReviewComponent extends StixViewPage implements OnI case "intrusion-set": //group this.collection_import_categories.group[category].push(object); break; + case "x-mitre-data-source": // data source + this.collection_import_categories.data_source[category].push(object); + break; + case "x-mitre-data-component": // data component + this.collection_import_categories.data_component[category].push(object); + break; } } logger.log(this.collection_import_categories); diff --git a/app/src/app/views/stix/collection/collection-import/collection-import-workflow/collection-import.component.html b/app/src/app/views/stix/collection/collection-import/collection-import-workflow/collection-import.component.html index 1c2aa47c..16e02408 100644 --- a/app/src/app/views/stix/collection/collection-import/collection-import-workflow/collection-import.component.html +++ b/app/src/app/views/stix/collection/collection-import/collection-import-workflow/collection-import.component.html @@ -26,6 +26,7 @@ +

Below are the objects of the collection as compared to the current contents of your workbench. Select the objects you want to import into your knowledge base. @@ -45,22 +46,38 @@

- +
-

Success!

+

Success! The collection has been imported into your workbench.

- The collection has been imported into your workbench. {{select.selected.length}} new objects were added. You can review the results of this import at any time on the imported collections section of the collection page. + {{successfully_saved.size}} new objects were added. You can review the imported objects at any time on the imported collections section of the collection page. +

+ +

+ {{save_errors.length}} objects could not be imported. +

+ + + + Import errors + + + +
{{save_errors | json}}
+
+
+

+

-
diff --git a/app/src/app/views/stix/collection/collection-import/collection-import-workflow/collection-import.component.scss b/app/src/app/views/stix/collection/collection-import/collection-import-workflow/collection-import.component.scss index f68dbd6e..76f16f77 100644 --- a/app/src/app/views/stix/collection/collection-import/collection-import-workflow/collection-import.component.scss +++ b/app/src/app/views/stix/collection/collection-import/collection-import-workflow/collection-import.component.scss @@ -20,6 +20,11 @@ flex-grow: 1; } } + .error-log { + overflow-x: scroll; + overflow-y: auto; + max-height: 30em; + } } // .collection-import { // width: 45em; diff --git a/app/src/app/views/stix/collection/collection-import/collection-import-workflow/collection-import.component.ts b/app/src/app/views/stix/collection/collection-import/collection-import-workflow/collection-import.component.ts index efd3ae04..199f8f69 100644 --- a/app/src/app/views/stix/collection/collection-import/collection-import-workflow/collection-import.component.ts +++ b/app/src/app/views/stix/collection/collection-import/collection-import-workflow/collection-import.component.ts @@ -7,6 +7,8 @@ import { MatStepper } from '@angular/material/stepper'; import { ActivatedRoute } from '@angular/router'; import { FileInputComponent } from 'ngx-material-file-input'; import { Collection, CollectionDiffCategories } from 'src/app/classes/stix/collection'; +import { DataComponent } from 'src/app/classes/stix/data-component'; +import { DataSource } from 'src/app/classes/stix/data-source'; import { Group } from 'src/app/classes/stix/group'; import { Matrix } from 'src/app/classes/stix/matrix'; import { Mitigation } from 'src/app/classes/stix/mitigation'; @@ -34,18 +36,24 @@ export class CollectionImportComponent implements OnInit { public select: SelectionModel; // ids of objects which have changed (object-version not already in knowledge base) public changed_ids: string[] = []; - // ids of objects which have nto changed (object-version not already in knowledge base) + // ids of objects which have not changed (object-version not already in knowledge base) public unchanged_ids: string[] = []; + + public import_errors: any; + public save_errors: string[] = []; + public successfully_saved: Set = new Set(); public collectionBundle: any; public object_import_categories = { - technique: new CollectionDiffCategories(), - tactic: new CollectionDiffCategories(), - software: new CollectionDiffCategories(), - relationship: new CollectionDiffCategories(), - mitigation: new CollectionDiffCategories(), - matrix: new CollectionDiffCategories(), - group: new CollectionDiffCategories() + technique: new CollectionDiffCategories(), + tactic: new CollectionDiffCategories(), + software: new CollectionDiffCategories(), + relationship: new CollectionDiffCategories(), + mitigation: new CollectionDiffCategories(), + matrix: new CollectionDiffCategories(), + group: new CollectionDiffCategories(), + data_source: new CollectionDiffCategories(), + data_component: new CollectionDiffCategories() } constructor(public route: ActivatedRoute, public http: HttpClient, public snackbar: MatSnackBar, public restAPIConnectorService: RestApiConnectorService, private dialog: MatDialog) { } @@ -95,19 +103,27 @@ export class CollectionImportComponent implements OnInit { public previewCollection(collectionBundle) { // send the collection bundle to the backend - let subscription_preview = this.restAPIConnectorService.postCollectionBundle(collectionBundle, true).subscribe({ + let subscription_preview = this.restAPIConnectorService.previewCollectionBundle(collectionBundle).subscribe({ next: (preview_results) => { - if (!preview_results) { - this.loadingStep1 = false; + if (preview_results.error) { + // errors occurred when fetching collection preview + this.import_errors = preview_results.error; + } + + if (!preview_results.preview) { + // collection bundle cannot be imported, show errors on next step + this.loadingStep1 = false; + this.stepper.next(); } else { - this.parsePreview(collectionBundle, preview_results) + // successfully fetched preview + this.parsePreview(collectionBundle, preview_results.preview); } }, error: (err) => { this.loadingStep1 = false; }, complete: () => { subscription_preview.unsubscribe() } - }) + }); } public parsePreview(collectionBundle: any, preview: Collection) { @@ -165,6 +181,12 @@ export class CollectionImportComponent implements OnInit { case "intrusion-set": //group this.object_import_categories.group[category].push(new Group(raw)) break; + case "x-mitre-data-source": // data source + this.object_import_categories.data_source[category].push(new DataSource(raw)) + break; + case "x-mitre-data-component": // data component + this.object_import_categories.data_component[category].push(new DataComponent(raw)) + break; } } // set up selection @@ -214,8 +236,19 @@ export class CollectionImportComponent implements OnInit { } } newBundle.objects = objects; - let subscription = this.restAPIConnectorService.postCollectionBundle(newBundle, false).subscribe({ - next: () => { + let force = this.import_errors ? true : false; // force import if the collection bundle has errors + let subscription = this.restAPIConnectorService.postCollectionBundle(newBundle, false, force).subscribe({ + next: (results) => { + if (results.import_categories.errors.length > 0) { + logger.warn("Collection import completed with errors:", results.import_categories.errors); + } + this.save_errors = results.import_categories.errors; + let save_error_ids = new Set(this.save_errors.map(err => err['object_ref'])); + for (let category in results.import_categories) { + if (category == "errors") continue; + for (let id of results.import_categories[category]) if (!save_error_ids.has(id)) this.successfully_saved.add(id); + } + logger.log("Successfully imported the following objects:", Array.from(this.successfully_saved)); this.stepper.next(); }, complete: () => { subscription.unsubscribe(); } //prevent memory leaks @@ -227,4 +260,19 @@ export class CollectionImportComponent implements OnInit { }) } + /** + * Cancel the collection import and revert to previous step + */ + public cancelImport(): void { + this.import_errors = undefined; + this.stepper.previous(); + } + + /** + * Download a log of errors from the import + */ + public downloadErrorLog() { + this.restAPIConnectorService.triggerBrowserDownload(this.save_errors, "import-errors.json") + } + } \ No newline at end of file diff --git a/app/src/app/views/stix/collection/collection-view/collection-view.component.html b/app/src/app/views/stix/collection/collection-view/collection-view.component.html index dcf43221..ae2cc50c 100644 --- a/app/src/app/views/stix/collection/collection-view/collection-view.component.html +++ b/app/src/app/views/stix/collection/collection-view/collection-view.component.html @@ -83,10 +83,10 @@

Objects in Collection

- + -

{{attackType}}

+

{{format(attackType)}}

diff --git a/app/src/app/views/stix/collection/collection-view/collection-view.component.ts b/app/src/app/views/stix/collection/collection-view/collection-view.component.ts index b9ff8182..7ce45554 100644 --- a/app/src/app/views/stix/collection/collection-view/collection-view.component.ts +++ b/app/src/app/views/stix/collection/collection-view/collection-view.component.ts @@ -12,6 +12,8 @@ import { Software } from 'src/app/classes/stix/software'; import { StixObject } from 'src/app/classes/stix/stix-object'; import { Tactic } from 'src/app/classes/stix/tactic'; import { Technique } from 'src/app/classes/stix/technique'; +import { DataSource } from 'src/app/classes/stix/data-source'; +import { DataComponent } from 'src/app/classes/stix/data-component'; import { VersionNumber } from 'src/app/classes/version-number'; import { StixListComponent } from 'src/app/components/stix/stix-list/stix-list.component'; import { RestApiConnectorService } from 'src/app/services/connectors/rest-api/rest-api-connector.service'; @@ -50,23 +52,27 @@ export class CollectionViewComponent extends StixViewPage implements OnInit { public stagedData: VersionReference[] = []; public potentialChanges = { - technique: new CollectionDiffCategories(), - tactic: new CollectionDiffCategories(), - software: new CollectionDiffCategories(), - relationship: new CollectionDiffCategories(), - mitigation: new CollectionDiffCategories(), - matrix: new CollectionDiffCategories(), - group: new CollectionDiffCategories() + technique: new CollectionDiffCategories(), + tactic: new CollectionDiffCategories(), + software: new CollectionDiffCategories(), + relationship: new CollectionDiffCategories(), + mitigation: new CollectionDiffCategories(), + matrix: new CollectionDiffCategories(), + group: new CollectionDiffCategories(), + data_source: new CollectionDiffCategories(), + data_component: new CollectionDiffCategories() } public collectionChanges = { - technique: new CollectionDiffCategories(), - tactic: new CollectionDiffCategories(), - software: new CollectionDiffCategories(), - relationship: new CollectionDiffCategories(), - mitigation: new CollectionDiffCategories(), - matrix: new CollectionDiffCategories(), - group: new CollectionDiffCategories() + technique: new CollectionDiffCategories(), + tactic: new CollectionDiffCategories(), + software: new CollectionDiffCategories(), + relationship: new CollectionDiffCategories(), + mitigation: new CollectionDiffCategories(), + matrix: new CollectionDiffCategories(), + group: new CollectionDiffCategories(), + data_source: new CollectionDiffCategories(), + data_component: new CollectionDiffCategories() } public collection_import_categories = []; @@ -153,7 +159,14 @@ export class CollectionViewComponent extends StixViewPage implements OnInit { //stage data for saving this.stagedData = []; + let missingATTACKIDs = []; for (let object of collectionStixIDToObject.values()) { + // grab name of objects that do not have ATT&CK IDs + if (object.hasOwnProperty("attackID")) { + if (object.attackID == "" && object.hasOwnProperty("name")) { + missingATTACKIDs.push(object["name"]); + } + } // record associated identities/marking defs if (object.created_by_ref) identities.add(object.created_by_ref); if (object.modified_by_ref) identities.add(object.modified_by_ref); @@ -248,6 +261,21 @@ export class CollectionViewComponent extends StixViewPage implements OnInit { field: "contents", message: `${relationship_stats.excluded_both} relationships had neither attached objects in the collection and were therefore excluded` }) + // Check for missing ATT&CK ids + if (missingATTACKIDs.length != 0) { + let customMessage = ""; + if (missingATTACKIDs.length == 1) { + customMessage = `1 object missing ATT&CK ID: ${missingATTACKIDs[0]}` + } + else { + customMessage = `${missingATTACKIDs.length} objects missing ATT&CK IDs: ${missingATTACKIDs}` + } + results.warnings.push({ + result: "warning", + field: "attackID", + message: customMessage + }) + } //must have contents if (this.stagedData.length == 0) results.errors.push({ @@ -498,6 +526,10 @@ export class CollectionViewComponent extends StixViewPage implements OnInit { }) } + public format(attackType: string): string { + return attackType.replace(/_/g, ' '); + } + ngOnInit() { //set up subscription to route query params to reinitialize stix lists this.route.queryParams.subscribe(params => { diff --git a/app/src/app/views/stix/data-component/data-component-view/data-component-view.component.html b/app/src/app/views/stix/data-component/data-component-view/data-component-view.component.html new file mode 100644 index 00000000..9b1ba79b --- /dev/null +++ b/app/src/app/views/stix/data-component/data-component-view/data-component-view.component.html @@ -0,0 +1,99 @@ +
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ +
+
+
+

Techniques Detected

+
+
+
+
+ +
+
+
+
+ +
+
+
+ +
+
+
+

References

+
+
+
+
+ +
+
+
+
+ + + \ No newline at end of file diff --git a/app/src/app/views/stix/data-component/data-component-view/data-component-view.component.spec.ts b/app/src/app/views/stix/data-component/data-component-view/data-component-view.component.spec.ts new file mode 100644 index 00000000..c6867efb --- /dev/null +++ b/app/src/app/views/stix/data-component/data-component-view/data-component-view.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DataComponentViewComponent } from './data-component-view.component'; + +describe('DataComponentViewComponent', () => { + let component: DataComponentViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DataComponentViewComponent] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DataComponentViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/app/src/app/views/stix/data-component/data-component-view/data-component-view.component.ts b/app/src/app/views/stix/data-component/data-component-view/data-component-view.component.ts new file mode 100644 index 00000000..6f92f2a8 --- /dev/null +++ b/app/src/app/views/stix/data-component/data-component-view/data-component-view.component.ts @@ -0,0 +1,37 @@ +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { DataComponent } from 'src/app/classes/stix/data-component'; +import { StixObject } from 'src/app/classes/stix/stix-object'; +import { RestApiConnectorService } from 'src/app/services/connectors/rest-api/rest-api-connector.service'; +import { StixViewPage } from '../../stix-view-page'; + +@Component({ + selector: 'app-data-component-view', + templateUrl: './data-component-view.component.html' +}) +export class DataComponentViewComponent extends StixViewPage implements OnInit { + @Output() onClickRelationship = new EventEmitter(); + public loading = false; + public get data_component(): DataComponent { return this.config.object as DataComponent; } + + constructor(private restAPIConnectorService: RestApiConnectorService) { super(); } + + ngOnInit(): void { + if (!this.data_component.data_source) { + // fetch parent data source + this.loading = true; + let objects$ = this.restAPIConnectorService.getDataComponent(this.data_component.stixID); + let subscription = objects$.subscribe({ + next: (result) => { + let objects = result as DataComponent[]; + this.data_component.data_source = objects[0].data_source; + this.loading = false; + }, + complete: () => { subscription.unsubscribe(); } + }); + } + } + + public viewRelationship(object: StixObject): void { + this.onClickRelationship.emit(object); + } +} diff --git a/app/src/app/views/stix/data-source/data-source-list/data-source-list.component.html b/app/src/app/views/stix/data-source/data-source-list/data-source-list.component.html new file mode 100644 index 00000000..7f623b58 --- /dev/null +++ b/app/src/app/views/stix/data-source/data-source-list/data-source-list.component.html @@ -0,0 +1,11 @@ +
+
+

data sources

+
+ +
+
+ +
\ No newline at end of file diff --git a/app/src/app/views/stix/data-source/data-source-list/data-source-list.component.spec.ts b/app/src/app/views/stix/data-source/data-source-list/data-source-list.component.spec.ts new file mode 100644 index 00000000..b1635cd4 --- /dev/null +++ b/app/src/app/views/stix/data-source/data-source-list/data-source-list.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DataSourceListComponent } from './data-source-list.component'; + +describe('DataSourceListComponent', () => { + let component: DataSourceListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DataSourceListComponent] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DataSourceListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/app/src/app/views/stix/data-source/data-source-list/data-source-list.component.ts b/app/src/app/views/stix/data-source/data-source-list/data-source-list.component.ts new file mode 100644 index 00000000..d3f790ea --- /dev/null +++ b/app/src/app/views/stix/data-source/data-source-list/data-source-list.component.ts @@ -0,0 +1,7 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-data-source-list', + templateUrl: './data-source-list.component.html' +}) +export class DataSourceListComponent { } diff --git a/app/src/app/views/stix/data-source/data-source-view/data-source-view.component.html b/app/src/app/views/stix/data-source/data-source-view/data-source-view.component.html new file mode 100644 index 00000000..0bfb1d7d --- /dev/null +++ b/app/src/app/views/stix/data-source/data-source-view/data-source-view.component.html @@ -0,0 +1,127 @@ +
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ +
+
+
+

Data Components

+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+
+

References

+
+
+
+
+ +
+
+
+
\ No newline at end of file diff --git a/app/src/app/views/stix/data-source/data-source-view/data-source-view.component.scss b/app/src/app/views/stix/data-source/data-source-view/data-source-view.component.scss new file mode 100644 index 00000000..414f1001 --- /dev/null +++ b/app/src/app/views/stix/data-source/data-source-view/data-source-view.component.scss @@ -0,0 +1,4 @@ +.add-button { + text-align: center; + margin-bottom: 20px; +} \ No newline at end of file diff --git a/app/src/app/views/stix/data-source/data-source-view/data-source-view.component.spec.ts b/app/src/app/views/stix/data-source/data-source-view/data-source-view.component.spec.ts new file mode 100644 index 00000000..d8e99a62 --- /dev/null +++ b/app/src/app/views/stix/data-source/data-source-view/data-source-view.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DataSourceViewComponent } from './data-source-view.component'; + +describe('DataSourceViewComponent', () => { + let component: DataSourceViewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [DataSourceViewComponent] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DataSourceViewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/app/src/app/views/stix/data-source/data-source-view/data-source-view.component.ts b/app/src/app/views/stix/data-source/data-source-view/data-source-view.component.ts new file mode 100644 index 00000000..a53ee3c6 --- /dev/null +++ b/app/src/app/views/stix/data-source/data-source-view/data-source-view.component.ts @@ -0,0 +1,59 @@ +import { Component, OnInit } from '@angular/core'; +import { DataSource } from 'src/app/classes/stix/data-source'; +import { DataComponent } from 'src/app/classes/stix/data-component'; +import { RestApiConnectorService } from 'src/app/services/connectors/rest-api/rest-api-connector.service'; +import { StixViewPage } from '../../stix-view-page'; +import { MatDialog } from '@angular/material/dialog'; +import { StixDialogComponent } from '../../stix-dialog/stix-dialog.component'; + +@Component({ + selector: 'app-data-source-view', + templateUrl: './data-source-view.component.html', + styleUrls: ['./data-source-view.component.scss'] +}) +export class DataSourceViewComponent extends StixViewPage implements OnInit { + public get data_source(): DataSource { return this.config.object as DataSource; } + public data_components: DataComponent[] = []; + public loading = false; + + constructor(public dialog: MatDialog, private restAPIConnectorService: RestApiConnectorService) { super(); } + + ngOnInit(): void { + this.data_components = this.data_source.data_components; + } + + public getDataComponents(): void { + this.loading = true; + let data$ = this.restAPIConnectorService.getAllDataComponents(); + let sub = data$.subscribe({ + next: (results) => { + let objects = results.data as DataComponent[]; + this.data_components = objects.filter(obj => obj.data_source_ref == this.data_source.stixID) + this.loading = false; + }, + complete: () => {sub.unsubscribe();} + }) + } + + public createDataComponent(): void { + let data_component = new DataComponent(); + data_component.data_source_ref = this.data_source.stixID; + data_component.data_source = this.data_source; + data_component.workflow = undefined; + let prompt = this.dialog.open(StixDialogComponent, { + data: { + object: data_component, + editable: true, + mode: "edit", + sidebarControl: "events" + }, + maxHeight: "75vh" + }); + let subscription = prompt.afterClosed().subscribe({ + next: (result) => { + if (result) { this.getDataComponents(); } //re-fetch values since an edit occurred + }, + complete: () => { subscription.unsubscribe(); } + }); + } +} diff --git a/app/src/app/views/stix/group/group-list/group-list.component.html b/app/src/app/views/stix/group/group-list/group-list.component.html index d84fb1c9..bc6926be 100644 --- a/app/src/app/views/stix/group/group-list/group-list.component.html +++ b/app/src/app/views/stix/group/group-list/group-list.component.html @@ -1,6 +1,15 @@
-

groups

+

+ groups + + help_outline + +

+
+ +
- + @@ -48,6 +49,17 @@ (onOpenHistory)="openHistory()" (onOpenNotes)="openNotes()"> + + + + { - this.dialogRef.close(this.dirty); + next: (result) => { this.editorService.onEditingStopped.emit(); + if (this.prevObject) this.revertToPreviousObject(); + else if (object.attackType == 'data-component') { // view data component on save + this.validating = false; + this.editing = false; + } + else this.dialogRef.close(this.dirty); }, complete: () => { subscription.unsubscribe(); } }) @@ -66,7 +97,14 @@ export class StixDialogComponent implements OnInit { this.validating = false; } + public discardChanges() { + this.editorService.onEditingStopped.emit(); + if (this.prevObject) this.revertToPreviousObject(); + else this.close(); + } + public close() { + if (this.prevObject) this.prevObject = undefined; // unset previous object this.dialogRef.close(this.dirty); } diff --git a/app/src/app/views/stix/stix-page/stix-page.component.html b/app/src/app/views/stix/stix-page/stix-page.component.html index b061a4a8..cb59a9e5 100644 --- a/app/src/app/views/stix/stix-page/stix-page.component.html +++ b/app/src/app/views/stix/stix-page/stix-page.component.html @@ -11,6 +11,7 @@

¯\_(ツ)_/¯ Nothing her + diff --git a/app/src/app/views/stix/stix-page/stix-page.component.ts b/app/src/app/views/stix/stix-page/stix-page.component.ts index 0cd6a3a3..3a85acfa 100644 --- a/app/src/app/views/stix/stix-page/stix-page.component.ts +++ b/app/src/app/views/stix/stix-page/stix-page.component.ts @@ -4,6 +4,7 @@ import { ActivatedRoute, Router, NavigationEnd } from '@angular/router'; import { BreadcrumbService } from 'angular-crumbs'; import { Observable } from 'rxjs'; import { Collection } from 'src/app/classes/stix/collection'; +import { DataSource } from 'src/app/classes/stix/data-source'; import { Group } from 'src/app/classes/stix/group'; import { Matrix } from 'src/app/classes/stix/matrix'; import { Mitigation } from 'src/app/classes/stix/mitigation'; @@ -125,6 +126,8 @@ export class StixPageComponent implements OnInit, OnDestroy { else if (this.objectType == "tactic") objects$ = this.restAPIConnectorService.getTactic(objectStixID); else if (this.objectType == "technique") objects$ = this.restAPIConnectorService.getTechnique(objectStixID, null, "latest", true); else if (this.objectType == "collection") objects$ = this.restAPIConnectorService.getCollection(objectStixID, objectModified, "latest", false, true); + else if (this.objectType == "data-source") objects$ = this.restAPIConnectorService.getDataSource(objectStixID, null, "latest", false, false, true); + else if (this.objectType == "data-component") objects$ = this.restAPIConnectorService.getDataComponent(objectStixID); let subscription = objects$.subscribe({ next: result => { this.updateBreadcrumbs(result, this.objectType ); @@ -168,6 +171,7 @@ export class StixPageComponent implements OnInit, OnDestroy { this.objectType == "mitigation" ? new Mitigation() : this.objectType == "group" ? new Group(): this.objectType == "collection" ? new Collection() : + this.objectType == "data-source" ? new DataSource() : null // if not any of the above types ); this.initialVersion = new VersionNumber(this.objects[0].version.toString()); diff --git a/app/src/app/views/stix/stix-view-page.ts b/app/src/app/views/stix/stix-view-page.ts index 7119b624..9631d4dc 100644 --- a/app/src/app/views/stix/stix-view-page.ts +++ b/app/src/app/views/stix/stix-view-page.ts @@ -1,5 +1,7 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; import { StixObject } from 'src/app/classes/stix/stix-object'; +import { StixDialogComponent } from './stix-dialog/stix-dialog.component'; @Component({template: ''}) export abstract class StixViewPage { @@ -38,4 +40,5 @@ export interface StixViewConfig { * if "service", use sidebar.service to control the sidebar */ sidebarControl?: "disable" | "events" | "service" + dialog?: MatDialogRef // reference to the current dialog } \ No newline at end of file diff --git a/app/src/app/views/stix/tactic/tactic-list/tactic-list.component.html b/app/src/app/views/stix/tactic/tactic-list/tactic-list.component.html index 33051c67..7239ccda 100644 --- a/app/src/app/views/stix/tactic/tactic-list/tactic-list.component.html +++ b/app/src/app/views/stix/tactic/tactic-list/tactic-list.component.html @@ -1,6 +1,15 @@
-

tactics

+

+ tactics + + help_outline + +

+
- @@ -101,14 +101,14 @@ arrow_forward
- -
+ +
@@ -320,6 +320,32 @@

Procedure Examples

}">
+ +
+
+

Data Sources

+
+
+
+
+ +
+
+
+
+ +
+
diff --git a/app/src/app/views/stix/technique/technique-view/technique-view.component.ts b/app/src/app/views/stix/technique/technique-view/technique-view.component.ts index 1e34dee9..2c2df64b 100644 --- a/app/src/app/views/stix/technique/technique-view/technique-view.component.ts +++ b/app/src/app/views/stix/technique/technique-view/technique-view.component.ts @@ -29,6 +29,18 @@ export class TechniqueViewComponent extends StixViewPage implements OnInit { this.ref.detectChanges(); } + /** + * Get label for the data sources field. + * Appends 'ics' for clarification if the object is cross-domain + */ + public dataSourcesLabel(): string { + let label = 'data sources'; + if (this.technique.domains.includes('ics-attack') && this.technique.domains.length > 1) { + label = 'ics ' + label; + } + return label; + } + public showDomainField(domain: string, field: string): boolean { return this.technique.domains.includes(domain) && (this.technique[field].length > 0 || this.editing); } diff --git a/app/src/style/layouts/expansion-panel.scss b/app/src/style/layouts/expansion-panel.scss index 3e056f89..86d191fe 100644 --- a/app/src/style/layouts/expansion-panel.scss +++ b/app/src/style/layouts/expansion-panel.scss @@ -5,7 +5,7 @@ .dark & { border: 1px solid border-color(dark); } .light & { border: 1px solid border-color(light); } } - &:not(:first-child):not(.mat-expanded) { border-top-width: 0px } + &:not(:first-of-type):not(.mat-expanded) { border-top-width: 0px } &.mat-expanded + .mat-expansion-panel { border-top-width: 1px; } @@ -13,7 +13,6 @@ flex-basis: 0; align-items: center; } - .mat-expansion-panel-header-title { } .mat-expansion-panel-header-description { justify-content: flex-start; } diff --git a/app/src/style/layouts/list-page.scss b/app/src/style/layouts/list-page.scss index fbc386d6..1f5d349e 100644 --- a/app/src/style/layouts/list-page.scss +++ b/app/src/style/layouts/list-page.scss @@ -5,6 +5,27 @@ margin-top: 0; margin-bottom: 16px; } + h1.with-help-icon { + margin-left: 30px; + } margin-bottom: 16px; } + + popover-content div { + text-transform: initial; + font-weight: normal; + width: 30rem; + text-align: justify; + text-align-last: left; + padding: 0px 15px; + margin: 0px; + display: block; + margin-block-start: 1em; + margin-block-end: 1em; + margin-inline-start: 0px; + margin-inline-end: 0px; + font-family: Roboto, serif; + font-size: 18px; + line-height: 1.5; + } } \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index e0b74161..ce929798 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -31,107 +31,171 @@ # Changelog + +## 21 October 2021 + +### ATT&CK Workbench version 1.1.0 + +ATT&CK Workbench v1.1.0 includes support for ATT&CK Spec v2.1.0 and coincides with the ATT&CK v10.0 release. Users who do not upgrade to Workbench v1.1.0 may encounter issues with the new ATT&CK data: + +- If the user added the ATT&CK collection index prior to the ATT&CK v10.0 release, it may lose track of imported Enterprise collections. These collections can still be found in the "imported collections" tab of the collection manager, but won't be reflected in the collection manager. Collection subscriptions for Enterprise may also be lost. Upgrading to ATT&CK Workbench v1.1.0 will fix this issue and restore prior collection subscriptions. +- If the user imports ATT&CK v10.0 using ATT&CK Workbench 1.0.X, data sources and data components will not be imported into their local knowledge base. You can re-import the collection after upgrading Workbench to v1.1.0 to acquire the data sources and data components even if you had already imported it when running a prior version of Workbench. + +ATT&CK Workbench version 1.1.0 includes improvements to how data is imported which should circumvent the above issues for future releases of ATT&CK. + +#### Improvements in 1.1.0 + +- Added object type documentation on list pages. See [frontend#221](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/221). +- Added support for ATT&CK Spec v2.1.0: + - Added support for data sources and data components, and viewing/editing interfaces for these object types and their relationships with techniques. See [frontend#67](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/67), [frontend#66](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/66). + - Added support for `x_mitre_attack_spec_version` on all object types. +- Improved the flexibility and robustness of collection imports: + - Workbench will now check the ATT&CK Spec version of imported data and warn the user if the ATT&CK Spec version is unsupported (ex. if the Workbench instance is too outdated to support the data it is trying to import). The user can choose to bypass this warning. + - Workbench can now import the same collection multiple times in case objects in the initial import could not be imported due to an error. + - The user will now be provided with a downloadable list of objects that could not be saved (and the reason why) in the event of import errors. + - REST API will now log import errors for individual objects to the console when the log level is set to `verbose`. + - Frontend will now log import errors to the console when the application environment is not set to production. +- Added validation for missing ATT&CK IDs on objects that support them. The user will now be warned if they neglect to assign an ATT&CK ID to an object which supports it. When exporting a collection, the user will similarly be warned if any contained objects are missing ATT&CK IDs. See [frontend#231](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/231). +- REST API now supports setting the log level through an environment variable. See [rest-api#108](https://github.com/center-for-threat-informed-defense/attack-workbench-rest-api/issues/108). +- REST API no longer sets the `upgrade-insecure-requests` directive of the `Content-Security-Policy` header in responses. This will facilitate the deployment of ATT&CK Workbench in an internal environment without requiring the system to be configured to support HTTPS. See [rest-api#96](https://github.com/center-for-threat-informed-defense/attack-workbench-rest-api/issues/96). + +#### Fixes in 1.1.0 + +- Fixed an issue where the navigation header could be inaccessible when navigating within the application or when the page resized due to user input. +- Frontend will no longer claim objects were imported when they were actually discarded due to import errors such as spec violations. +- Imported STIX bundles will no longer require (but still allow) the `spec_version` field on the bundle itself. This was causing issues importing collections created by the Workbench. Objects within the bundle still require the `spec_version` field per the STIX 2.1 spec. See [rest-api#103](https://github.com/center-for-threat-informed-defense/attack-workbench-rest-api/issues/103). +- Fixed an issue where the REST API would save references when importing a collection bundle even though the `previewOnly` flag had been set. See [rest-api#120](https://github.com/center-for-threat-informed-defense/attack-workbench-rest-api/issues/120). + ## 20 August 2021 + ### ATT&CK Workbench version 1.0.2 + #### Fixes in 1.0.2 -- Error snackbars will now show appropriate messages instead of `[object ProgressEvent]` when communication with the REST API is interrupted or cannot be established. See [frontend#227](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/227). -- Fixed a bug where tactic shortnames were computed incorrectly for tactics with more than one space in the name (E.g `"Command and Control"`). See [frontend#239](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/239). - - If you have edited a technique under a tactic with more than one space in the name, remove and re-add the tactic under the technique edit interface to ensure that the tactic reference is formatted properly. - - If you have created a tactic with more than one space in the name, save a new version of the tactic and the proper shortname should be saved. You do not need to make any edits when saving the tactic page for the shortname to be fixed. + +- Error snackbars will now show appropriate messages instead of `[object ProgressEvent]` when communication with the REST API is interrupted or cannot be established. See [frontend#227](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/227). +- Fixed a bug where tactic shortnames were computed incorrectly for tactics with more than one space in the name (E.g `"Command and Control"`). See [frontend#239](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/239). + - If you have edited a technique under a tactic with more than one space in the name, remove and re-add the tactic under the technique edit interface to ensure that the tactic reference is formatted properly. + - If you have created a tactic with more than one space in the name, save a new version of the tactic and the proper shortname should be saved. You do not need to make any edits when saving the tactic page for the shortname to be fixed. ## 8 July 2021 + ### ATT&CK Workbench version 1.0.1 + #### Improvements in 1.0.1 -- Added a system for configuring the Collection Manager with self-signed certs when using the docker setup. Documentation for this configuration will be improved in a subsequent release. + +- Added a system for configuring the Collection Manager with self-signed certs when using the docker setup. Documentation for this configuration will be improved in a subsequent release. #### Fixes in 1.0.1 -- Fixed an error encountered when using the `attack-objects` API with large datasets. This error was preventing users from loading the "create a collection" page when Enterprise ATT&CK collections were imported. See [rest-api#87](https://github.com/center-for-threat-informed-defense/attack-workbench-rest-api/issues/87). + +- Fixed an error encountered when using the `attack-objects` API with large datasets. This error was preventing users from loading the "create a collection" page when Enterprise ATT&CK collections were imported. See [rest-api#87](https://github.com/center-for-threat-informed-defense/attack-workbench-rest-api/issues/87). ## 21 June 2021 + ### ATT&CK Workbench version 1.0.0 + #### Improvements in 1.0.0 -- Performance improvements when adding, editing, and validating relationships. -- Improved error messages when importing collections that are too large or malformed. See [frontend#198](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/198). -- Improved page titles and breadcrumb on "object not found" pages. -- User can now import collections from file. See [frontend#207](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/207). -- Collection index update interval is now set in the REST API configuration instead of hardcoded in the frontend. See [frontend#200](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/200). + +- Performance improvements when adding, editing, and validating relationships. +- Improved error messages when importing collections that are too large or malformed. See [frontend#198](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/198). +- Improved page titles and breadcrumb on "object not found" pages. +- User can now import collections from file. See [frontend#207](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/207). +- Collection index update interval is now set in the REST API configuration instead of hardcoded in the frontend. See [frontend#200](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/200). #### Fixes in 1.0.0 -- Fixed vertically misaligned timestamps across several UIs. -- Fixed missing timestamp on collection version lists within collection indexes. -- Fixed object status popover showing the wrong status if opened too soon after the page loads. Also improved performance of the status popover code. -- Collection import UI no longer gets stuck if it runs into a problem fetching/importing/previewing the collection. See [frontend#198](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/198) -- Object status popover now closes properly when the user starts editing the object. See [frontend#199](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/199). + +- Fixed vertically misaligned timestamps across several UIs. +- Fixed missing timestamp on collection version lists within collection indexes. +- Fixed object status popover showing the wrong status if opened too soon after the page loads. Also improved performance of the status popover code. +- Collection import UI no longer gets stuck if it runs into a problem fetching/importing/previewing the collection. See [frontend#198](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/198) +- Object status popover now closes properly when the user starts editing the object. See [frontend#199](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/199). ## 7 May 2021 + ### ATT&CK Workbench version 0.4.0 + #### Improvements in 0.4.0 -- Added a favicon. See [frontend#137](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/137). -- Added dynamic page title to make it easier to distinguish multiple Workbench tabs in the browser. See [frontend#130](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/130). -- Added a list of recommended indexes available when adding a collection index. See [frontend#194](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/194). -- Added ability to set workflow state when objects are saved. See [frontend#184](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/184). -- Updated occurrences of "aliases" to "associated groups" or "associated software" for consistency across the application. See [frontend#176](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/176). -- Improved logging and added log level to environment configuration to suppress unnecessary logs from production deployments. See [frontend#209](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/209). -- Updated the reference editor to enforce correct formatting when creating a new reference. See [frontend#177](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/177). + +- Added a favicon. See [frontend#137](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/137). +- Added dynamic page title to make it easier to distinguish multiple Workbench tabs in the browser. See [frontend#130](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/130). +- Added a list of recommended indexes available when adding a collection index. See [frontend#194](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/194). +- Added ability to set workflow state when objects are saved. See [frontend#184](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/184). +- Updated occurrences of "aliases" to "associated groups" or "associated software" for consistency across the application. See [frontend#176](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/176). +- Improved logging and added log level to environment configuration to suppress unnecessary logs from production deployments. See [frontend#209](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/209). +- Updated the reference editor to enforce correct formatting when creating a new reference. See [frontend#177](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/177). ## 21 April 2021 + ### ATT&CK Workbench version 0.3.0 + #### New Features in 0.3.0 -- Added attribution of edits and tracking of organization identity. See [frontend#61](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/124) and [frontend#182](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/61). -- Added ability to revoke and deprecate objects. See [frontend#164](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/164). -- Added tracking of workflow state. See [frontend#3](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/3). -- Added ability to create and edit collections. See [frontend#4](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/4), [frontend#5](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/5), and [frontend#112](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/112). -- Added support and documentation for [ATT&CK Navigator](https://github.com/mitre-attack/attack-navigator) integration. See [frontend#153](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/153). -- Added support and documentation for [ATT&CK Website](https://github.com/mitre-attack/attack-website/) integration. See [frontend#152](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/152). + +- Added attribution of edits and tracking of organization identity. See [frontend#61](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/124) and [frontend#182](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/61). +- Added ability to revoke and deprecate objects. See [frontend#164](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/164). +- Added tracking of workflow state. See [frontend#3](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/3). +- Added ability to create and edit collections. See [frontend#4](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/4), [frontend#5](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/5), and [frontend#112](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/112). +- Added support and documentation for [ATT&CK Navigator](https://github.com/mitre-attack/attack-navigator) integration. See [frontend#153](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/153). +- Added support and documentation for [ATT&CK Website](https://github.com/mitre-attack/attack-website/) integration. See [frontend#152](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/152). #### Improvements in 0.3.0 -- Improved display of object domains. See [frontend#166](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/166). + +- Improved display of object domains. See [frontend#166](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/166). ## 19 March 2021 ### ATT&CK Workbench version 0.2.0 + #### New Features in 0.2.0 -- Added support for MTC and CAPEC IDs. See [frontend#124](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/124). -- Added ability to create and edit objects. See [frontend#44](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/44) and [frontend#145](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/145). - - Added ability to edit group/software aliases. See [frontend#118](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/118). - - Added ability to edit various list properties such as platforms, tactics, and domains. See [frontend#31](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/31). - - Added rich-text description editor. See [frontend#32](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/32). - - Added ability to convert techniques to sub-techniques, and vice versa. - - Added ability to edit ATT&CK IDs. See [frontend#55](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/55). - - Added validation system to warn user of malformed data. - - Added ability to reorder tactics on matrices. See [frontend#116](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/116). - - Added ability to edit object version numbers, and a UI for incrementing versions when objects are saved. See [frontend#56](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/56). -- Added ability to create and edit notes (annotations) on objects. See [frontend#59](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/59). -- Added citations/references support. - - Added automatic detection of citations on descriptions and aliases. See [frontend#115](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/115). - - Added references manager tool. See [frontend#115](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/115) and [frontend#133](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/133). + +- Added support for MTC and CAPEC IDs. See [frontend#124](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/124). +- Added ability to create and edit objects. See [frontend#44](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/44) and [frontend#145](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/145). + - Added ability to edit group/software aliases. See [frontend#118](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/118). + - Added ability to edit various list properties such as platforms, tactics, and domains. See [frontend#31](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/31). + - Added rich-text description editor. See [frontend#32](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/32). + - Added ability to convert techniques to sub-techniques, and vice versa. + - Added ability to edit ATT&CK IDs. See [frontend#55](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/55). + - Added validation system to warn user of malformed data. + - Added ability to reorder tactics on matrices. See [frontend#116](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/116). + - Added ability to edit object version numbers, and a UI for incrementing versions when objects are saved. See [frontend#56](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/56). +- Added ability to create and edit notes (annotations) on objects. See [frontend#59](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/59). +- Added citations/references support. + - Added automatic detection of citations on descriptions and aliases. See [frontend#115](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/115). + - Added references manager tool. See [frontend#115](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/115) and [frontend#133](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/133). #### Improvements in 0.2.0 -- Lists of objects can now be searched and filtered. See [frontend#128](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/128) and [frontend#127](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/127). -- Lists of objects now display ATT&CK IDs when relevant. See [frontend#119](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/119). -- When viewing an object, fields which have no value(s) will now be hidden. See [frontend#120](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/120). -- Improved display of sub-techniques. See [frontend#125](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/125). -- Layout and formatting improvements to [USAGE](/docs/usage.md) document + +- Lists of objects can now be searched and filtered. See [frontend#128](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/128) and [frontend#127](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/127). +- Lists of objects now display ATT&CK IDs when relevant. See [frontend#119](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/119). +- When viewing an object, fields which have no value(s) will now be hidden. See [frontend#120](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/120). +- Improved display of sub-techniques. See [frontend#125](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/125). +- Layout and formatting improvements to [USAGE](/docs/usage.md) document #### Fixes in 0.2.0 -- Fixed broken pagination on relationship tables. See [frontend#126](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/126). + +- Fixed broken pagination on relationship tables. See [frontend#126](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/126). ## 16 February 2021 + ### ATT&CK Workbench version 0.1.1 + #### New Features in 0.1.1 -- Added Dockerfiles, docker-compose, [and documentation on how to use them](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/tree/master/docs/docker-compose.md). See [frontend#108](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/108), [frontend#109](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/109) [rest-api#14](https://github.com/center-for-threat-informed-defense/attack-workbench-rest-api/issues/14), and [collection-manager#13](https://github.com/center-for-threat-informed-defense/attack-workbench-collection-manager/issues/13). + +- Added Dockerfiles, docker-compose, [and documentation on how to use them](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/tree/master/docs/docker-compose.md). See [frontend#108](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/108), [frontend#109](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/issues/109) [rest-api#14](https://github.com/center-for-threat-informed-defense/attack-workbench-rest-api/issues/14), and [collection-manager#13](https://github.com/center-for-threat-informed-defense/attack-workbench-collection-manager/issues/13). #### Fixes in 0.1.1 -- Fixed a crash that could occur with specific queries on the REST API. See [rest-api#28](https://github.com/center-for-threat-informed-defense/attack-workbench-rest-api/issues/28). + +- Fixed a crash that could occur with specific queries on the REST API. See [rest-api#28](https://github.com/center-for-threat-informed-defense/attack-workbench-rest-api/issues/28). ## 19 January 2021 + ### ATT&CK Workbench version 0.1.0 + #### New Features in 0.1.0 -- Created object view pages for matrix, technique, tactic, mitigation, group, and software objects. -- Added the ability to browse and import collection indexes. - - Collection indexes can be imported via URL. - - A preview of the collection index is shown before confirming the import. -- Added the ability to import, view, and subscribe to collections. - - Collections listed within an index can be subscribed to, which will pull new versions when they are published. - - Collections can also be manually imported via URL. When importing, a preview of the collection and its contents is shown before confirming the import. At this step, users can preview the objects in the collection and select which ones they want to import. Changes in the import are displayed relative to the state of the knowledge base similar to the update pages on the [ATT&CK Website](https://attack.mitre.org/resources/updates/). - - An interface provides the ability to review prior imports, which provides a list of changes at the time of the import identical to that shown during the import of the collection. \ No newline at end of file + +- Created object view pages for matrix, technique, tactic, mitigation, group, and software objects. +- Added the ability to browse and import collection indexes. + - Collection indexes can be imported via URL. + - A preview of the collection index is shown before confirming the import. +- Added the ability to import, view, and subscribe to collections. + - Collections listed within an index can be subscribed to, which will pull new versions when they are published. + - Collections can also be manually imported via URL. When importing, a preview of the collection and its contents is shown before confirming the import. At this step, users can preview the objects in the collection and select which ones they want to import. Changes in the import are displayed relative to the state of the knowledge base similar to the update pages on the [ATT&CK Website](https://attack.mitre.org/resources/updates/). + - An interface provides the ability to review prior imports, which provides a list of changes at the time of the import identical to that shown during the import of the collection. diff --git a/docs/collections.md b/docs/collections.md index 167e7b7e..97c3e974 100644 --- a/docs/collections.md +++ b/docs/collections.md @@ -36,6 +36,7 @@ Collections are represented in STIX using the `x-mitre-collection` type, describ | **modified** (required)| `timestamp` | Represents the time at which the collection was most recently modified. | | **x_mitre_version** (required) | `string` | The version of the collection object, which must follow the MAJOR.MINOR.PATCH pattern. | | **spec_version** (required) | `string` | The version of the STIX specification used to represent the object. This value MUST be `2.1`. +| **x_mitre_attack_spec_version** (required) | `string` | The version of the ATT&CK spec used to represent the object. More information on the ATT&CK spec and the current ATT&CK Spec version can be found [on the attack-stix-data GitHub repository](https://github.com/mitre-attack/attack-stix-data/blob/master/USAGE.md). | **created_by_ref** (required) | `string` | identifier | Specifies the **id** property of the `identity` object that describes the entity that created this collection. | | **object_marking_refs** (required) | `list` of type `identifier` | Specifies a list of **id** properties of `marking-definition` objects that apply to this object. Typically used for copyright statements. | | **x_mitre_contents** (required) | `list` of type _object version reference_ | Specifies the objects contained within the collection. See the _object version reference_ type below. | @@ -52,9 +53,10 @@ Object version references are used to refer to a specific version of a STIX obje ### Collection Example ```json { - "id": "x-mitre-collection--23320f4-22ad-8467-3b73-ed0c869a12838", + "id": "x-mitre-collection--402e24b4-436e-4936-b19b-2038648f489", "type": "x-mitre-collection", "spec_version": "2.1", + "x_mitre_attack_spec_version": "2.1.0", "name": "Enterprise ATT&CK", "x_mitre_version": "6.2", "description": "Version 6.2 of the Enterprise ATT&CK dataset", @@ -148,7 +150,7 @@ Collection version objects describe specific versions of collections within a _c "modified": "2019-07-17T20:04:40.297Z", "collections": [ { - "id": "x-mitre-collection--23320f4-22ad-8467-3b73-ed0c869a12838", + "id": "x-mitre-collection--402e24b4-436e-4936-b19b-2038648f489", "name": "Enterprise ATT&CK", "description": "The Enterprise domain of the ATT&CK dataset", "created": "2019-07-31T00:00:00.000Z", diff --git a/docs/integrations.md b/docs/integrations.md index c79697ee..c4eaa76a 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -2,36 +2,65 @@ The ATT&CK Workbench is designed to integrate with a variety of tools. This page provides documentation on how to set up such integrations. -## ATT&CK Navigator +Many ATT&CK Workbench integrations require that specific fields or values are present on your custom STIX data to use it properly. Depending on what data you have some objects may not be shown in integrations. Objects may not show up in integrations if they: -The [ATT&CK Navigator](https://github.com/mitre-attack/attack-navigator) can be configured to display the contents of your local knowledge base. +- Support ATT&CK IDs but do not have one assigned +- Are revoked or deprecated +- Support platforms or tactics but are not assigned to any +- Support mappings to techniques but do not feature any mappings -### 1. Install the Navigator +## Workbench REST API URLs + +Depending on whether you are using the manual install or the docker install, and where Workbench has been deployed, the URLs you will be setting in your integrations will be different. + +The following API routes are used with integrations to access STIX bundles representing the local knowledge base: + +| Domain | API Route | +| :--------- | :-------------------------------------------- | +| Enterprise | `/api/stix-bundles/?domain=enterprise-attack` | +| Mobile | `/api/stix-bundles/?domain=mobile-attack` | +| ICS | `/api/stix-bundles/?domain=ics-attack` | + +The host the routes are available at depends on how the Workbench has been installed and deployed: + +| Deployment Type | Installation Type | Host (hostname:port) | +| :-------------- | :---------------- | :----------------------- | +| local | manual | `http://localhost:3000` | +| local | docker | `http://localhost` | +| remote | manual | `{remote-hostname}:3000` | +| remote | docker | `{remote-hostname}` | + +For example, the enterprise STIX bundle of a manual installation running locally would be available at `http://localhost:3000/api/stix-bundles/?domain=enterprise-attack`. You can test the URL by opening it in your web browser while the Workbench is running - if it is correct you should see a JSON object as a response. +## ATT&CK Navigator Integration + +The [ATT&CK Navigator](https://github.com/mitre-attack/attack-navigator) can be configured to display the contents of your local knowledge base. + +### 1. Install the Navigator Clone the [ATT&CK Navigator](https://github.com/mitre-attack/attack-navigator) repository: `git clone https://github.com/mitre-attack/attack-navigator.git` ### 2. Update the config -Edit the config file `nav-app/src/assets/config.json` by prepending a new object to the versions array connecting to the ATT&CK Workbench REST API. +Edit the config file `nav-app/src/assets/config.json` by prepending a new object to the versions array connecting to the ATT&CK Workbench REST API. Refer to [Workbench REST API URLs, above](#workbench-rest-api-urls), for the values for the enterprise, mobile and ICS URLs. ```json { "versions": [ { - "name": "ATT&CK Workbench Data", + "name": "ATT&CK Workbench Data", "domains": [ - { - "name": "Enterprise", - "data": ["http://localhost:3000/api/stix-bundles/?domain=enterprise-attack"] + { + "name": "Enterprise", + "data": ["(Enterprise URL)"] }, - { - "name": "Mobile", - "data": ["http://localhost:3000/api/stix-bundles/?domain=mobile-attack"] + { + "name": "Mobile", + "data": ["(Mobile URL)"] }, { "name": "ICS", - "data": ["http://localhost:3000/api/stix-bundles/?domain=ics-attack"] + "data": ["(ICS URL)"] } ] } @@ -43,8 +72,9 @@ Edit the config file `nav-app/src/assets/config.json` by prepending a new object Follow the [install and run](https://github.com/mitre-attack/attack-navigator#install-and-run) instructions on the ATT&CK Navigator to deploy the application. The Navigator will update its data from the Workbench every time it loads, so there is no need to periodically rebuild the application to stay synchronized with Workbench data. -## ATT&CK Website -The [ATT&CK Website](https://github.com/mitre-attack/attack-website) can be configured to display the contents of your local knowledge base. +## ATT&CK Website Integration + +The [ATT&CK Website](https://github.com/mitre-attack/attack-website) can be configured to display the contents of your local knowledge base. ### 1. Install the Website @@ -52,19 +82,19 @@ Clone the [ATT&CK Website](https://github.com/mitre-attack/attack-website) repos ### 2. Update Config -Edit the domain URLs in the config file `modules/site_config.py` as shown below to connect to the Workbench REST API. +Edit the domain URLs in the config file `modules/site_config.py` as shown below to connect to the Workbench REST API. Refer to [Workbench REST API URLs, above](#workbench-rest-api-urls), for the values for the enterprise and mobile URLs. ```python domains = [ { "name" : "enterprise-attack", - "location" : "http://localhost:3000/api/stix-bundles/?domain=enterprise-attack", + "location" : "(Enterprise URL)", "alias" : "Enterprise", "deprecated" : False }, { "name" : "mobile-attack", - "location" : "http://localhost:3000/api/stix-bundles/?domain=mobile-attack", + "location" : "(Mobile URL)", "alias" : "Mobile", "deprecated" : False }, @@ -77,11 +107,9 @@ domains = [ ] ``` -Depending on your deployment of the Workbench the REST API URL may be different. See the environment files, `src/environments/`, for the currently configured REST API URL. - ### 3. Build the site -Run `python3 update-attack.py` to build the website using the current Workbench data. +Run `python3 update-attack.py` to build the website using the current Workbench data. ### 4. Serve the site diff --git a/docs/usage.md b/docs/usage.md index 5928c3eb..c0147881 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -173,10 +173,11 @@ ATT&CK IDs must follow a prescribed format: | Matrix | (domain identifier)* | | Tactic | `TAxxxx` | | Technique | `Txxxx` | -| Sub-Technique | `Txxxx.yyy` | +| Sub-Technique | `Txxxx.yyy` | | Mitigation | `Mxxxx` | | Group | `Gxxxx` | | Software | `Sxxxx` | +| Data Source | `DSxxxx` | _\* Domain identifiers for Matrices are described in the section for editing matrices._ @@ -232,7 +233,7 @@ The set of fields available to edit on a technique differs according to the doma | Field | Domains | Tactics? | Description | |:------|:--------|:---------|:-------------| -| Data Sources | Enterprise, ICS | (All Tactics) | Sources of information that may be used to identify the action or result of the action being performed. | +| Data Sources | ICS | (All Tactics) | Sources of information that may be used to identify the action or result of the action being performed. | | sub-technique? | Enterprise | (All Tactics) | Is this object a sub-technique? This cannot be changed for sub-techniques with assigned parents, or for parent-techniques with assigned sub-techniques. | | System Requirements | Enterprise | (All Tactics) | Additional information on requirements the adversary needs to meet or about the state of the system (software, patch level, etc.) that may be required for the technique to work. | | Permissions Required | Enterprise | Privilege Escalation | The lowest level of permissions the adversary is required to be operating within to perform the technique on a system. | @@ -252,7 +253,7 @@ The set of fields available to edit on a technique differs according to the doma | Sub-techniques / Other Sub-techniques | Sub-techniques of the technique if it is a parent technique, or other sub-techniques of the parent | is a sub-technique. | Mitigations | Mitigations that apply to this technique | | Procedure Examples | Groups and software that use this technique | - +| Data Sources | Data components that detect this technique | #### Editing Tactics @@ -306,6 +307,28 @@ The software type must be selected when creating it and due to limitations of th | Techniques Used | Techniques used by the group | | Associated Groups | Groups that use this software | +#### Editing Data Sources + +Data sources represent relevant information that can be collected by sensors or logs to detect adversary behaviors. Data sources +include data components to provide an additional layer of context and identify the specific properties of a data source +that are relevant to detecting an ATT&CK technique or sub-technique. + +Data sources support the standard set of fields and also define collection layers, which are a description of where the data source may be +physically collected. After a data component has been added to a data source, the user can create relationships with techniques in the +data component dialog window. + +##### Data Source Relationships + +| Relationship Section | Description | +|:-----|:----| +| Data Components | Data components related to this data source | + +##### Data Component Relationships + +| Relationship Section | Description | +|:-----|:----| +| Techniques Detected | Techniques detected by the data component | + #### Editing Relationships Relationships map objects to other objects. Relationships have types, sources, and targets. The source and targets define the objects connected by the relationship, and the type is a verb describing the nature of their relationship. @@ -316,6 +339,7 @@ Relationships map objects to other objects. Relationships have types, sources, a | uses | Group, Software* | Software*, Technique | | mitigates | Mitigation | Technique | | subtechnique-of | Technique | Technique | +| detects | Data Component | Technique | _\* Relationships cannot be created between two software._