From d9c6be40e95caafed3c2d340ad5357d7a41e144e Mon Sep 17 00:00:00 2001 From: Charissa Miller <48832936+clemiller@users.noreply.github.com> Date: Sun, 25 Feb 2024 08:35:11 -0500 Subject: [PATCH 01/39] load collection index --- nav-app/src/app/services/config.service.ts | 70 ++++--- nav-app/src/assets/config.json | 213 +-------------------- 2 files changed, 43 insertions(+), 240 deletions(-) diff --git a/nav-app/src/app/services/config.service.ts b/nav-app/src/app/services/config.service.ts index 373895720..1a5b350bc 100644 --- a/nav-app/src/app/services/config.service.ts +++ b/nav-app/src/app/services/config.service.ts @@ -1,11 +1,14 @@ import { Injectable } from '@angular/core'; import { ContextMenuItem } from '../classes/context-menu-item'; import { HttpClient } from '@angular/common/http'; +import { map, switchMap } from 'rxjs/operators'; +import { of } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class ConfigService { + public collectionIndex: string; public versions: any[] = []; public contextMenuItems: ContextMenuItem[] = []; public defaultLayers: any; @@ -172,34 +175,43 @@ export class ConfigService { * Note: this is done at startup */ public loadConfig() { - return this.http - .get('./assets/config.json') - .toPromise() - .then((config) => { - console.debug(`loaded app configuration settings`); - - this.versions = config['versions']; - config['custom_context_menu_items'].forEach((item) => { - this.contextMenuItems.push(new ContextMenuItem(item.label, item.url, item.subtechnique_url)); - }); - this.defaultLayers = config['default_layers']; - this.commentColor = config['comment_color']; - this.linkColor = config['link_color']; - this.metadataColor = config['metadata_color']; - this.banner = config['banner']; - - // parse feature preferences - this.featureList = config['features']; - config['features'].forEach((feature) => { - this.setFeature_object(feature); - }); - - // override preferences with preferences from URL fragments - this.getAllFragments().forEach((value: string, key: string) => { - if (this.isFeature(key) || this.isFeatureGroup(key)) { - this.setFeature(key, value == 'true'); - } - }); - }); + return this.http.get('./assets/config.json').pipe( + map((config) => { + console.debug('loaded app configuration settings'); + + this.versions = config['versions']; + + config['custom_context_menu_items'].forEach((item) => { + this.contextMenuItems.push(new ContextMenuItem(item.label, item.url, item.subtechnique_url)); + }); + this.defaultLayers = config['default_layers']; + this.commentColor = config['comment_color']; + this.linkColor = config['link_color']; + this.metadataColor = config['metadata_color']; + this.banner = config['banner']; + + // parse feature preferences + this.featureList = config['features']; + config['features'].forEach((feature) => { + this.setFeature_object(feature); + }); + + // override preferences with preferences from URL fragments + this.getAllFragments().forEach((value: string, key: string) => { + if (this.isFeature(key) || this.isFeatureGroup(key)) { + this.setFeature(key, value == 'true'); + } + }); + return config['collection_index_url']; + }), + switchMap((collectionIndexUrl: string) => { + if (collectionIndexUrl) return this.http.get(collectionIndexUrl); + return of({}); + }), + map((collectionIndex: any) => { + console.log('loaded collection index'); + this.collectionIndex = collectionIndex; + }) + ).toPromise() } } diff --git a/nav-app/src/assets/config.json b/nav-app/src/assets/config.json index 44271d80e..66248cb08 100755 --- a/nav-app/src/assets/config.json +++ b/nav-app/src/assets/config.json @@ -1,216 +1,7 @@ { + "collection_index_url": "https://raw.githubusercontent.com/mitre-attack/attack-stix-data/master/index.json", + "versions": [ - { - "name": "ATT&CK v14", - "version": "14", - "domains": [ - { - "name": "Enterprise", - "identifier": "enterprise-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v14.1/enterprise-attack/enterprise-attack.json"] - }, - { - "name": "Mobile", - "identifier": "mobile-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v14.1/mobile-attack/mobile-attack.json"] - }, - { - "name": "ICS", - "identifier": "ics-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v14.1/ics-attack/ics-attack.json"] - } - ] - }, - { - "name": "ATT&CK v13", - "version": "13", - "domains": [ - { - "name": "Enterprise", - "identifier": "enterprise-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v13.1/enterprise-attack/enterprise-attack.json"] - }, - { - "name": "Mobile", - "identifier": "mobile-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v13.1/mobile-attack/mobile-attack.json"] - }, - { - "name": "ICS", - "identifier": "ics-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v13.1/ics-attack/ics-attack.json"] - } - ] - }, - { - "name": "ATT&CK v12", - "version": "12", - "domains": [ - { - "name": "Enterprise", - "identifier": "enterprise-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v12.1/enterprise-attack/enterprise-attack.json"] - }, - { - "name": "Mobile", - "identifier": "mobile-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v12.1/mobile-attack/mobile-attack.json"] - }, - { - "name": "ICS", - "identifier": "ics-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v12.1/ics-attack/ics-attack.json"] - } - ] - }, - { - "name": "ATT&CK v11", - "version": "11", - "domains": [ - { - "name": "Enterprise", - "identifier": "enterprise-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v11.3/enterprise-attack/enterprise-attack.json"] - }, - { - "name": "Mobile", - "identifier": "mobile-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v11.3/mobile-attack/mobile-attack.json"] - }, - { - "name": "ICS", - "identifier": "ics-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v11.3/ics-attack/ics-attack.json"] - } - ] - }, - { - "name": "ATT&CK v10", - "version": "10", - "domains": [ - { - "name": "Enterprise", - "identifier": "enterprise-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v10.1/enterprise-attack/enterprise-attack.json"] - }, - { - "name": "Mobile", - "identifier": "mobile-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v10.1/mobile-attack/mobile-attack.json"] - }, - { - "name": "ICS", - "identifier": "ics-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v10.1/ics-attack/ics-attack.json"] - } - ] - }, - { - "name": "ATT&CK v9", - "version": "9", - "domains": [ - { - "name": "Enterprise", - "identifier": "enterprise-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v9.0/enterprise-attack/enterprise-attack.json"] - }, - { - "name": "Mobile", - "identifier": "mobile-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v9.0/mobile-attack/mobile-attack.json"] - }, - { - "name": "ICS", - "identifier": "ics-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v9.0/ics-attack/ics-attack.json"] - } - ] - }, - { - "name": "ATT&CK v8", - "version": "8", - "domains": [ - { - "name": "Enterprise", - "identifier": "enterprise-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v8.2/enterprise-attack/enterprise-attack.json"] - }, - { - "name": "Mobile", - "identifier": "mobile-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v8.2/mobile-attack/mobile-attack.json"] - }, - { - "name": "ICS", - "identifier": "ics-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v8.2/ics-attack/ics-attack.json"] - } - ] - }, - { - "name": "ATT&CK v7", - "version": "7", - "domains": [ - { - "name": "Enterprise", - "identifier": "enterprise-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v7.2/enterprise-attack/enterprise-attack.json"] - }, - { - "name": "Mobile", - "identifier": "mobile-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v7.2/mobile-attack/mobile-attack.json"] - } - ] - }, - { - "name": "ATT&CK v6", - "version": "6", - "domains": [ - { - "name": "Enterprise", - "identifier": "enterprise-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v6.3/enterprise-attack/enterprise-attack.json"] - }, - { - "name": "Mobile", - "identifier": "mobile-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v6.3/mobile-attack/mobile-attack.json"] - } - ] - }, - { - "name": "ATT&CK v5", - "version": "5", - "domains": [ - { - "name": "Enterprise", - "identifier": "enterprise-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v5.2/enterprise-attack/enterprise-attack.json"] - }, - { - "name": "Mobile", - "identifier": "mobile-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v5.2/mobile-attack/mobile-attack.json"] - } - ] - }, - { - "name": "ATT&CK v4", - "version": "4", - "domains": [ - { - "name": "Enterprise", - "identifier": "enterprise-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v4.0/enterprise-attack/enterprise-attack.json"] - }, - { - "name": "Mobile", - "identifier": "mobile-attack", - "data": ["https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v4.0/mobile-attack/mobile-attack.json"] - } - ] - } ], "custom_context_menu_items": [ From 5f2c5e0aac40e0479aee8c1ccbff5d9b0c73cc4a Mon Sep 17 00:00:00 2001 From: Charissa Miller <48832936+clemiller@users.noreply.github.com> Date: Tue, 5 Mar 2024 13:30:57 -0500 Subject: [PATCH 02/39] rename function --- .../tooltip/tooltip.component.spec.ts | 2 +- nav-app/src/app/services/data.service.spec.ts | 12 ++++---- nav-app/src/app/services/data.service.ts | 8 +++--- nav-app/src/app/tabs/tabs.component.spec.ts | 28 +++++++++---------- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/nav-app/src/app/matrix/technique-cell/tooltip/tooltip.component.spec.ts b/nav-app/src/app/matrix/technique-cell/tooltip/tooltip.component.spec.ts index 259ff26fa..38838aa2b 100644 --- a/nav-app/src/app/matrix/technique-cell/tooltip/tooltip.component.spec.ts +++ b/nav-app/src/app/matrix/technique-cell/tooltip/tooltip.component.spec.ts @@ -83,7 +83,7 @@ describe('TooltipComponent', () => { innerWidth: { get: () => 100 }, innerHeight: { get: () => 100 }, }); - component.dataService.setUpURLs(versions); + component.dataService.setUpDomains(versions); component.dataService.domains[0].notes = [new Note(stixSDO)]; component.viewModel = new ViewModel('layer', '33', 'enterprise-attack-13', null); component.viewModel.domainVersionID = 'enterprise-attack-13'; diff --git a/nav-app/src/app/services/data.service.spec.ts b/nav-app/src/app/services/data.service.spec.ts index 84f49e011..f37ab8e64 100755 --- a/nav-app/src/app/services/data.service.spec.ts +++ b/nav-app/src/app/services/data.service.spec.ts @@ -85,7 +85,7 @@ describe('DataService', () => { describe('setup with Workbench integration', () => { beforeEach(() => { configService.versions = MockData.workbenchData; - spyOn(DataService.prototype, 'setUpURLs').and.callThrough(); + spyOn(DataService.prototype, 'setUpDomains').and.callThrough(); mockService = new DataService(http, configService); }); @@ -107,7 +107,7 @@ describe('DataService', () => { describe('setup with TAXII', () => { beforeEach(() => { configService.versions = MockData.taxiiData; - spyOn(DataService.prototype, 'setUpURLs').and.callThrough(); + spyOn(DataService.prototype, 'setUpDomains').and.callThrough(); mockService = new DataService(http, configService); }); @@ -133,7 +133,7 @@ describe('DataService', () => { describe('setup with config data', () => { beforeEach(() => { configService.versions = MockData.configData; - spyOn(DataService.prototype, 'setUpURLs').and.callThrough(); + spyOn(DataService.prototype, 'setUpDomains').and.callThrough(); mockService = new DataService(http, configService); }); @@ -223,7 +223,7 @@ describe('DataService', () => { it('should update domain watchers', () => { let functionSpy = spyOn(dataService, 'getCurrentVersion'); - dataService.setUpURLs(MockData.configData); + dataService.setUpDomains(MockData.configData); let domain = dataService.domains[0]; domain.dataLoadedCallbacks = [dataService.getCurrentVersion]; dataService.parseBundle(domain, MockData.stixBundleSDO); @@ -239,7 +239,7 @@ describe('DataService', () => { domains: MockData.configData[0].domains, }; configService.versions = [MockData.configData[0], newVersion]; - spyOn(DataService.prototype, 'setUpURLs').and.callThrough(); + spyOn(DataService.prototype, 'setUpDomains').and.callThrough(); mockService = new DataService(http, configService); }); @@ -342,7 +342,7 @@ describe('DataService', () => { describe('StixObject tests', () => { beforeEach(() => { configService.versions = MockData.configData; - spyOn(DataService.prototype, 'setUpURLs').and.callThrough(); + spyOn(DataService.prototype, 'setUpDomains').and.callThrough(); mockService = new DataService(http, configService); }); diff --git a/nav-app/src/app/services/data.service.ts b/nav-app/src/app/services/data.service.ts index 98ecb08c2..3f374f853 100755 --- a/nav-app/src/app/services/data.service.ts +++ b/nav-app/src/app/services/data.service.ts @@ -17,7 +17,7 @@ export class DataService { private configService: ConfigService ) { console.debug('initializing data service'); - this.setUpURLs(configService.versions); + this.setUpDomains(configService.versions); } public domain_backwards_compatibility = { @@ -268,11 +268,11 @@ export class DataService { public icsAttackURL: string = 'https://raw.githubusercontent.com/mitre/cti/master/ics-attack/ics-attack.json'; /** - * Set up the URLs for data - * @param {versions} list of versions and domains defined in the configuration file + * Set up the URLs for domains in the list defined in the config file + * @param {versions} list of versions and domains * @memberof DataService */ - public setUpURLs(versions: any[]) { + public setUpDomains(versions: any[]) { versions.forEach((version: any) => { let v: Version = new Version(version['name'], version['version'].match(/\d+/g)[0]); this.versions.push(v); diff --git a/nav-app/src/app/tabs/tabs.component.spec.ts b/nav-app/src/app/tabs/tabs.component.spec.ts index 53a64d71b..7593ed55c 100755 --- a/nav-app/src/app/tabs/tabs.component.spec.ts +++ b/nav-app/src/app/tabs/tabs.component.spec.ts @@ -466,7 +466,7 @@ describe('TabsComponent', () => { })); it('should read and open json file', waitForAsync(() => { - component.dataService.setUpURLs(MockData.configData); + component.dataService.setUpDomains(MockData.configData); let mockedDocElement = document.createElement('input'); mockedDocElement.id = 'uploader'; mockedDocElement.value = 'test1'; @@ -525,7 +525,7 @@ describe('TabsComponent', () => { component.openTab('layer', vm1, true, true, true, true); component.openTab('layer1', vm2, true, true, true, true); expect(component.getScoreExpressionError()).toEqual('Layer b does not match the chosen domain'); - component.dataService.setUpURLs(MockData.configData); // set up data + component.dataService.setUpDomains(MockData.configData); // set up data component.opSettings.domain = 'enterprise-attack-13'; expect(component.getFilteredVMs()).toEqual(component.viewModelsService.viewModels); spyOn(component.dataService, 'loadDomainData').and.returnValue(Promise.resolve()); @@ -539,7 +539,7 @@ describe('TabsComponent', () => { let vm1 = component.viewModelsService.newViewModel('layer', 'enterprise-attack-13'); component.openTab('layer', vm1, true, true, true, true); expect(component.getScoreExpressionError()).toEqual(null); - component.dataService.setUpURLs(MockData.configData); // set up data + component.dataService.setUpDomains(MockData.configData); // set up data component.dataService.parseBundle(component.dataService.getDomain('enterprise-attack-13'), MockData.stixBundleSDO); //load the data component.opSettings.domain = 'enterprise-attack-13'; spyOn(component.dataService, 'loadDomainData').and.returnValue(Promise.resolve()); @@ -555,7 +555,7 @@ describe('TabsComponent', () => { component.openTab('layer', vm1, true, true, true, true); component.openTab('layer2', vm2, true, true, true, true); - component.dataService.setUpURLs(MockData.configDataExtended); // set up data + component.dataService.setUpDomains(MockData.configDataExtended); // set up data component.dataService.parseBundle(component.dataService.getDomain('enterprise-attack-13'), MockData.stixBundleSDO); //load the data component.opSettings.domain = 'enterprise-attack-13'; let alertSpy = spyOn(window, 'alert'); @@ -568,7 +568,7 @@ describe('TabsComponent', () => { describe('versionUpgradeDialog', () => { it('should upgrade layer', waitForAsync(() => { - component.dataService.setUpURLs(MockData.configDataExtended); + component.dataService.setUpDomains(MockData.configDataExtended); let layer = JSON.parse(JSON.stringify(MockLayers.layerFile1)); let vm1 = component.viewModelsService.newViewModel('layer2', 'enterprise-attack-12'); let versionUpgradeSpy = spyOn(component, 'versionUpgradeDialog').and.returnValue( @@ -584,7 +584,7 @@ describe('TabsComponent', () => { })); it('should not upgrade layer', waitForAsync(() => { - component.dataService.setUpURLs(MockData.configDataExtended); + component.dataService.setUpDomains(MockData.configDataExtended); let layer = JSON.parse(JSON.stringify(MockLayers.layerFile1)); let vm1 = component.viewModelsService.newViewModel('layer2', 'enterprise-attack-12'); let versionUpgradeSpy = spyOn(component, 'versionUpgradeDialog').and.returnValue(Promise.resolve(null)); @@ -598,7 +598,7 @@ describe('TabsComponent', () => { })); it('should not upgrade layer with domain data loaded', waitForAsync(() => { - component.dataService.setUpURLs(MockData.configDataExtended); + component.dataService.setUpDomains(MockData.configDataExtended); component.dataService.parseBundle(component.dataService.getDomain('enterprise-attack-13'), MockData.stixBundleSDO); let layer = JSON.parse(JSON.stringify(MockLayers.layerFile1)); let vm1 = component.viewModelsService.newViewModel('layer2', 'enterprise-attack-13'); @@ -623,7 +623,7 @@ describe('TabsComponent', () => { describe('upgradeLayer', () => { it('should upgrade layer', waitForAsync(() => { - component.dataService.setUpURLs(MockData.configDataExtended); + component.dataService.setUpDomains(MockData.configDataExtended); let layer = JSON.parse(JSON.stringify(MockLayers.layerFile1)); let vm1 = component.viewModelsService.newViewModel('layer2', 'enterprise-attack-12'); let versionUpgradeSpy = spyOn(component, 'versionUpgradeDialog').and.returnValue( @@ -639,7 +639,7 @@ describe('TabsComponent', () => { })); it('should not upgrade layer', waitForAsync(() => { - component.dataService.setUpURLs(MockData.configDataExtended); + component.dataService.setUpDomains(MockData.configDataExtended); let layer = JSON.parse(JSON.stringify(MockLayers.layerFile1)); let vm1 = component.viewModelsService.newViewModel('layer2', 'enterprise-attack-12'); let versionUpgradeSpy = spyOn(component, 'versionUpgradeDialog').and.returnValue(Promise.resolve(null)); @@ -653,7 +653,7 @@ describe('TabsComponent', () => { })); it('should not upgrade layer with default layer enabled', waitForAsync(() => { - component.dataService.setUpURLs(MockData.configDataExtended); + component.dataService.setUpDomains(MockData.configDataExtended); let layer = JSON.parse(JSON.stringify(MockLayers.layerFile1)); let vm1 = component.viewModelsService.newViewModel('layer2', 'enterprise-attack-12'); spyOn(component.dataService, 'loadDomainData').and.returnValue(Promise.resolve()); @@ -664,7 +664,7 @@ describe('TabsComponent', () => { })); it('should not upgrade layer with default layer enabled and domain data loaded', waitForAsync(() => { - component.dataService.setUpURLs(MockData.configDataExtended); + component.dataService.setUpDomains(MockData.configDataExtended); component.dataService.parseBundle(component.dataService.getDomain('enterprise-attack-13'), MockData.stixBundleSDO); let bb = JSON.parse(JSON.stringify(MockLayers.layerFile1)); let vm1 = component.viewModelsService.newViewModel('layer2', 'enterprise-attack-13'); @@ -676,7 +676,7 @@ describe('TabsComponent', () => { })); it('should not upgrade layer with domain data loaded', waitForAsync(() => { - component.dataService.setUpURLs(MockData.configDataExtended); + component.dataService.setUpDomains(MockData.configDataExtended); component.dataService.parseBundle(component.dataService.getDomain('enterprise-attack-13'), MockData.stixBundleSDO); let layer = JSON.parse(JSON.stringify(MockLayers.layerFile1)); let vm1 = component.viewModelsService.newViewModel('layer2', 'enterprise-attack-13'); @@ -701,7 +701,7 @@ describe('TabsComponent', () => { describe('loadLayerFromURL', () => { it('should load from url', waitForAsync(() => { - component.dataService.setUpURLs(MockData.configData); + component.dataService.setUpDomains(MockData.configData); component.http = http; spyOn(component.http, 'get').and.returnValue(of(MockLayers.layerFile1)); let versionMismatchSpy = spyOn(component, 'versionMismatchWarning').and.returnValue(Promise.resolve(true)); @@ -728,7 +728,7 @@ describe('TabsComponent', () => { ], }, ]; - component.dataService.setUpURLs(versions); + component.dataService.setUpDomains(versions); component.http = http; spyOn(component.http, 'get').and.returnValue(of(MockLayers.layerFile1)); let versionMismatchSpy = spyOn(component, 'versionMismatchWarning').and.returnValue(Promise.resolve(true)); From a46e40399f98c4a65b79794457cbd065aac76bf7 Mon Sep 17 00:00:00 2001 From: Charissa Miller <48832936+clemiller@users.noreply.github.com> Date: Tue, 5 Mar 2024 13:42:06 -0500 Subject: [PATCH 03/39] move domain class from stix directory --- nav-app/src/app/classes/{stix => }/domain.ts | 24 +++++++++---------- nav-app/src/app/classes/filter.ts | 2 +- nav-app/src/app/classes/index.ts | 1 + nav-app/src/app/classes/stix/index.ts | 1 - nav-app/src/app/services/data.service.spec.ts | 4 ++-- nav-app/src/app/services/data.service.ts | 6 ++--- nav-app/src/app/tabs/tabs.component.spec.ts | 4 ++-- nav-app/src/app/tabs/tabs.component.ts | 3 +-- 8 files changed, 22 insertions(+), 23 deletions(-) rename nav-app/src/app/classes/{stix => }/domain.ts (86%) diff --git a/nav-app/src/app/classes/stix/domain.ts b/nav-app/src/app/classes/domain.ts similarity index 86% rename from nav-app/src/app/classes/stix/domain.ts rename to nav-app/src/app/classes/domain.ts index da91b53f6..49f4d076f 100644 --- a/nav-app/src/app/classes/stix/domain.ts +++ b/nav-app/src/app/classes/domain.ts @@ -1,15 +1,15 @@ -import { ServiceAuth } from '../../services/data.service'; -import { Campaign } from './campaign'; -import { DataComponent } from './data-component'; -import { Group } from './group'; -import { Matrix } from './matrix'; -import { Mitigation } from './mitigation'; -import { Note } from './note'; -import { Software } from './software'; -import { Tactic } from './tactic'; -import { Technique } from './technique'; -import { Version } from '../version'; -import { Asset } from './asset'; +import { ServiceAuth } from '../services/data.service'; +import { Campaign } from './stix/campaign'; +import { DataComponent } from './stix/data-component'; +import { Group } from './stix/group'; +import { Matrix } from './stix/matrix'; +import { Mitigation } from './stix/mitigation'; +import { Note } from './stix/note'; +import { Software } from './stix/software'; +import { Tactic } from './stix/tactic'; +import { Technique } from './stix/technique'; +import { Version } from './version'; +import { Asset } from './stix/asset'; export class Domain { public readonly id: string; // domain ID diff --git a/nav-app/src/app/classes/filter.ts b/nav-app/src/app/classes/filter.ts index 7a665ec9f..b11dc7135 100644 --- a/nav-app/src/app/classes/filter.ts +++ b/nav-app/src/app/classes/filter.ts @@ -1,4 +1,4 @@ -import { Domain } from './stix/domain'; +import { Domain } from './domain'; export class Filter { // the data for a specific filter diff --git a/nav-app/src/app/classes/index.ts b/nav-app/src/app/classes/index.ts index d37f8af3f..80f3cbbab 100644 --- a/nav-app/src/app/classes/index.ts +++ b/nav-app/src/app/classes/index.ts @@ -1,5 +1,6 @@ export { VersionChangelog } from './version-changelog'; export { Version } from './version'; +export { Domain } from './domain'; export { ContextMenuItem } from './context-menu-item'; export { Gradient, Gcolor } from './gradient'; export { Filter } from './filter'; diff --git a/nav-app/src/app/classes/stix/index.ts b/nav-app/src/app/classes/stix/index.ts index 086431d65..4c81a3f22 100644 --- a/nav-app/src/app/classes/stix/index.ts +++ b/nav-app/src/app/classes/stix/index.ts @@ -1,7 +1,6 @@ export { StixObject } from './stix-object'; export { Campaign } from './campaign'; export { DataComponent } from './data-component'; -export { Domain } from './domain'; export { Group } from './group'; export { Matrix } from './matrix'; export { Mitigation } from './mitigation'; diff --git a/nav-app/src/app/services/data.service.spec.ts b/nav-app/src/app/services/data.service.spec.ts index f37ab8e64..aa55302d7 100755 --- a/nav-app/src/app/services/data.service.spec.ts +++ b/nav-app/src/app/services/data.service.spec.ts @@ -1,7 +1,7 @@ import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { Version, VersionChangelog } from '../classes'; -import { Asset, Campaign, DataComponent, Domain, Group, Matrix, Mitigation, Note, Software, Tactic, Technique } from '../classes/stix'; +import { Domain, Version, VersionChangelog } from '../classes'; +import { Asset, Campaign, DataComponent, Group, Matrix, Mitigation, Note, Software, Tactic, Technique } from '../classes/stix'; import { of } from 'rxjs'; import { HttpClient } from '@angular/common/http'; import { Collection } from '../utils/taxii2lib'; diff --git a/nav-app/src/app/services/data.service.ts b/nav-app/src/app/services/data.service.ts index 3f374f853..e60caa2e8 100755 --- a/nav-app/src/app/services/data.service.ts +++ b/nav-app/src/app/services/data.service.ts @@ -3,9 +3,9 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Buffer } from 'buffer'; import { Observable } from 'rxjs/Rx'; import { fromPromise } from 'rxjs/observable/fromPromise'; -import { Asset, Campaign, Domain, DataComponent, Group, Software, Matrix, Technique, Mitigation, Note } from '../classes/stix'; +import { Asset, Campaign, DataComponent, Group, Software, Matrix, Technique, Mitigation, Note } from '../classes/stix'; import { TaxiiConnect, Collection } from '../utils/taxii2lib'; -import { Version, VersionChangelog } from '../classes'; +import { Domain, Version, VersionChangelog } from '../classes'; import { ConfigService } from './config.service'; @Injectable({ @@ -17,7 +17,7 @@ export class DataService { private configService: ConfigService ) { console.debug('initializing data service'); - this.setUpDomains(configService.versions); + this.setUpDomains(configService.versions); } public domain_backwards_compatibility = { diff --git a/nav-app/src/app/tabs/tabs.component.spec.ts b/nav-app/src/app/tabs/tabs.component.spec.ts index 7593ed55c..0df956013 100755 --- a/nav-app/src/app/tabs/tabs.component.spec.ts +++ b/nav-app/src/app/tabs/tabs.component.spec.ts @@ -3,7 +3,7 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { TabsComponent } from './tabs.component'; import { MatDialog, MatDialogModule } from '@angular/material/dialog'; import { DataService } from '../services/data.service'; -import { Tab, TechniqueVM, Version, ViewModel } from '../classes'; +import { Tab, TechniqueVM, Domain, Version, ViewModel } from '../classes'; import { HelpComponent } from '../help/help.component'; import { SvgExportComponent } from '../svg-export/svg-export.component'; import { MatSnackBar } from '@angular/material/snack-bar'; @@ -12,7 +12,7 @@ import { LayerInformationComponent } from '../layer-information/layer-informatio import * as is from 'is_js'; import { HttpClient } from '@angular/common/http'; import { of } from 'rxjs'; -import { Domain, Technique } from '../classes/stix'; +import { Technique } from '../classes/stix'; import { ConfigService } from '../services/config.service'; import * as MockLayers from '../../tests/utils/mock-layers'; import * as MockData from '../../tests/utils/mock-data'; diff --git a/nav-app/src/app/tabs/tabs.component.ts b/nav-app/src/app/tabs/tabs.component.ts index 4877508cb..b223c0991 100755 --- a/nav-app/src/app/tabs/tabs.component.ts +++ b/nav-app/src/app/tabs/tabs.component.ts @@ -1,7 +1,6 @@ import { Component, ViewChild, TemplateRef, AfterViewInit, ViewEncapsulation, Input, Output, EventEmitter } from '@angular/core'; import { DataService } from '../services/data.service'; -import { Domain } from '../classes/stix'; -import { Tab, Version, ViewModel } from '../classes'; +import { Tab, Domain, Version, ViewModel } from '../classes'; import { ConfigService } from '../services/config.service'; import { VersionUpgradeComponent } from '../version-upgrade/version-upgrade.component'; import { HelpComponent } from '../help/help.component'; From 4a8d635b85d4f5ac62cfe597304a8cd647fcd83c Mon Sep 17 00:00:00 2001 From: Charissa Miller <48832936+clemiller@users.noreply.github.com> Date: Tue, 5 Mar 2024 13:48:14 -0500 Subject: [PATCH 04/39] remove unused code --- nav-app/src/app/classes/gradient.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/nav-app/src/app/classes/gradient.ts b/nav-app/src/app/classes/gradient.ts index 074fd4190..4d55d8bf9 100644 --- a/nav-app/src/app/classes/gradient.ts +++ b/nav-app/src/app/classes/gradient.ts @@ -31,7 +31,6 @@ export class Gradient { */ public serialize(): string { let colorList: string[] = []; - let self = this; this.colors.forEach(function (gColor: Gcolor) { let hexstring = tinycolor(gColor.color).toHex8String(); // include the alpha channel colorList.push(hexstring); From cb2366dbe11d04d938094e81bfb6314e33a67da3 Mon Sep 17 00:00:00 2001 From: Charissa Miller <48832936+clemiller@users.noreply.github.com> Date: Tue, 5 Mar 2024 13:52:58 -0500 Subject: [PATCH 05/39] code smell --- nav-app/src/app/services/config.service.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/nav-app/src/app/services/config.service.ts b/nav-app/src/app/services/config.service.ts index 1a5b350bc..372d14db4 100644 --- a/nav-app/src/app/services/config.service.ts +++ b/nav-app/src/app/services/config.service.ts @@ -124,8 +124,10 @@ export class ConfigService { this.features.set(featureObject.name, enabled); return [featureObject.name]; } else { - //has subfeatures - override = override ? override : !featureObject.enabled ? false : null; + // has subfeatures + if (!override) { + override = !featureObject.enabled ? false : null; + } let subfeatures = []; featureObject.subfeatures.forEach(function (subfeature) { subfeatures = Array.prototype.concat(subfeatures, self.setFeature_object(subfeature, override)); @@ -206,6 +208,7 @@ export class ConfigService { }), switchMap((collectionIndexUrl: string) => { if (collectionIndexUrl) return this.http.get(collectionIndexUrl); + // TODO: return properly if no url is provided return of({}); }), map((collectionIndex: any) => { From d968cbbeb15cab18f090d1d2ad4c88a7c4982d06 Mon Sep 17 00:00:00 2001 From: Charissa Miller <48832936+clemiller@users.noreply.github.com> Date: Tue, 5 Mar 2024 14:23:00 -0500 Subject: [PATCH 06/39] parse collection index into domain/version structure --- nav-app/src/app/services/config.service.ts | 2 +- nav-app/src/app/services/data.service.ts | 32 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/nav-app/src/app/services/config.service.ts b/nav-app/src/app/services/config.service.ts index 372d14db4..1dd706da2 100644 --- a/nav-app/src/app/services/config.service.ts +++ b/nav-app/src/app/services/config.service.ts @@ -208,7 +208,7 @@ export class ConfigService { }), switchMap((collectionIndexUrl: string) => { if (collectionIndexUrl) return this.http.get(collectionIndexUrl); - // TODO: return properly if no url is provided + // TODO return properly if no url is provided return of({}); }), map((collectionIndex: any) => { diff --git a/nav-app/src/app/services/data.service.ts b/nav-app/src/app/services/data.service.ts index 12a9ecb65..58ada11c2 100755 --- a/nav-app/src/app/services/data.service.ts +++ b/nav-app/src/app/services/data.service.ts @@ -17,6 +17,13 @@ export class DataService { private configService: ConfigService ) { console.debug('initializing data service'); + if (configService.collectionIndex) { + this.parseCollectionIndex(configService.collectionIndex); + } + // TODO restrict to one or both? + if (configService.versions) { + this.setUpDomains(configService.versions); + } this.setUpDomains(configService.versions); } @@ -313,9 +320,34 @@ export class DataService { this.domains.push(...[enterpriseDomain, mobileDomain, icsDomain]); } + //TODO check this with introduction of collection index this.lowestSupportedVersion = this.versions[this.versions.length - 1]; } + public parseCollectionIndex(collectionIndex: any) { + for (let collection of collectionIndex.collections) { + for (let version of collection.versions) { + let versionNumber = version.version; + let versionName = `${collectionIndex.name} v${versionNumber}`; + let v = this.addVersion(versionName, versionNumber); + this.domains.push(new Domain(collection.id, collection.name, v, [version.url])); + } + } + console.log('**', this.domains, this.versions); + } + + public addVersion(versionName: string, versionNumber: string): Version { + // check if version already exists + let existingVersion = this.versions.find(v => v.name === versionName && v.number === versionNumber); + if (!existingVersion) { + // create and add new version + let version = new Version(versionName, versionNumber); + this.versions.push(version); + return version; + } + return existingVersion; + } + /** * Fetch the domain data from the endpoint */ From a032cc889d9be28e4d41edd9132180977e5e55ab Mon Sep 17 00:00:00 2001 From: Charissa Miller <48832936+clemiller@users.noreply.github.com> Date: Tue, 5 Mar 2024 15:03:22 -0500 Subject: [PATCH 07/39] add mimimum supported version to globals --- nav-app/src/app/tabs/tabs.component.html | 4 ++-- nav-app/src/app/tabs/tabs.component.ts | 11 ++++++----- nav-app/src/app/utils/globals.ts | 1 + 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/nav-app/src/app/tabs/tabs.component.html b/nav-app/src/app/tabs/tabs.component.html index dbe6dd590..467727a67 100755 --- a/nav-app/src/app/tabs/tabs.component.html +++ b/nav-app/src/app/tabs/tabs.component.html @@ -160,7 +160,7 @@

MITRE ATT&CK® Navigator

none - + {{ nVersion.name }} @@ -176,7 +176,7 @@

MITRE ATT&CK® Navigator

- *Note: ATT&CK Versions prior to {{ dataService.lowestSupportedVersion.name }} are not supported by Navigator v{{ + *Note: ATT&CK Versions prior to {{ minimumSupportedVersion }} are not supported by Navigator v{{ navVersion }}. diff --git a/nav-app/src/app/tabs/tabs.component.ts b/nav-app/src/app/tabs/tabs.component.ts index b223c0991..64cfb963c 100755 --- a/nav-app/src/app/tabs/tabs.component.ts +++ b/nav-app/src/app/tabs/tabs.component.ts @@ -66,6 +66,10 @@ export class TabsComponent implements AfterViewInit { return this.filterDomains(this.dataService.versions[0]); } + public get minimumSupportedVersion(): string { + return globals.minimumSupportedVersion; + } + constructor( public dialog: MatDialog, public viewModelsService: ViewModelsService, @@ -438,11 +442,8 @@ export class TabsComponent implements AfterViewInit { * Create a new layer in the given domain and version */ public newLayer(domainVersionID: string, obj: any = undefined): void { - // load domain data, if not yet loaded - let domain = this.dataService.getDomain(domainVersionID); - if (!domain.dataLoaded) { - this.dataService.loadDomainData(domainVersionID, true); - } + // load domain data + this.dataService.loadDomainData(domainVersionID, true); // find non conflicting name let name; diff --git a/nav-app/src/app/utils/globals.ts b/nav-app/src/app/utils/globals.ts index d29ae30e3..d74410da8 100755 --- a/nav-app/src/app/utils/globals.ts +++ b/nav-app/src/app/utils/globals.ts @@ -3,3 +3,4 @@ import appPackageInfo from '../../../package.json'; export const navVersion: string = appPackageInfo.version; export const layerVersion: string = '4.5'; +export const minimumSupportedVersion: string = '4.0'; From 07a41e2f359852e9ab471ce57b791d1f7b7ef0c7 Mon Sep 17 00:00:00 2001 From: Charissa Miller <48832936+clemiller@users.noreply.github.com> Date: Tue, 5 Mar 2024 15:08:24 -0500 Subject: [PATCH 08/39] only parse valid versions --- nav-app/src/app/services/data.service.spec.ts | 10 +++--- nav-app/src/app/services/data.service.ts | 32 ++++++++----------- nav-app/src/app/tabs/tabs.component.spec.ts | 10 +++--- 3 files changed, 23 insertions(+), 29 deletions(-) diff --git a/nav-app/src/app/services/data.service.spec.ts b/nav-app/src/app/services/data.service.spec.ts index aa55302d7..8347a10ea 100755 --- a/nav-app/src/app/services/data.service.spec.ts +++ b/nav-app/src/app/services/data.service.spec.ts @@ -72,7 +72,7 @@ describe('DataService', () => { it('should compare the same version as unchanged', () => { let domain = dataService.domains[0]; - dataService.parseBundle(domain, MockData.stixBundleSDO); + dataService.parseBundles(domain, MockData.stixBundleSDO); let result = dataService.compareVersions(domain.id, domain.id); expect(result).toBeInstanceOf(VersionChangelog); @@ -183,7 +183,7 @@ describe('DataService', () => { mockService.domains[0].relationships['campaigns_attributed_to'].set('intrusion-set-0', ['attack-pattern-0']); mockService.domains[0].relationships['targeted_assets'].set('asset-0', ['attack-pattern-0']); let domain = mockService.domains[0]; - mockService.parseBundle(domain, MockData.stixBundleSDO); + mockService.parseBundles(domain, MockData.stixBundleSDO); // check data loaded expect(domain.dataLoaded).toBeTrue(); expect(domain.platforms).toEqual(MockData.T0000.x_mitre_platforms); @@ -226,7 +226,7 @@ describe('DataService', () => { dataService.setUpDomains(MockData.configData); let domain = dataService.domains[0]; domain.dataLoadedCallbacks = [dataService.getCurrentVersion]; - dataService.parseBundle(domain, MockData.stixBundleSDO); + dataService.parseBundles(domain, MockData.stixBundleSDO); expect(functionSpy).toHaveBeenCalled(); }); }); @@ -249,7 +249,7 @@ describe('DataService', () => { // parse stix bundles into old domain let [oldDomain, newDomain] = mockService.domains; - mockService.parseBundle(oldDomain, MockData.stixBundleSDO); + mockService.parseBundles(oldDomain, MockData.stixBundleSDO); expect(oldDomain.dataLoaded).toBeTrue(); // deprecation @@ -274,7 +274,7 @@ describe('DataService', () => { let revokedSubtechnique = { ...MockData.T0000_001 }; revokedSubtechnique['modified'] = '2002-01-01T01:01:00.000Z'; // parse stix bundle into new domain - mockService.parseBundle(newDomain, [ + mockService.parseBundles(newDomain, [ { type: 'bundle', id: 'bundle--1', diff --git a/nav-app/src/app/services/data.service.ts b/nav-app/src/app/services/data.service.ts index 58ada11c2..d938a32ef 100755 --- a/nav-app/src/app/services/data.service.ts +++ b/nav-app/src/app/services/data.service.ts @@ -7,6 +7,7 @@ import { Asset, Campaign, DataComponent, Group, Software, Matrix, Technique, Mit import { TaxiiConnect, Collection } from '../utils/taxii2lib'; import { Domain, Version, VersionChangelog } from '../classes'; import { ConfigService } from './config.service'; +import * as globals from '../utils/globals'; @Injectable({ providedIn: 'root', @@ -24,7 +25,7 @@ export class DataService { if (configService.versions) { this.setUpDomains(configService.versions); } - this.setUpDomains(configService.versions); + this.versions.sort((a, b) => +a.number > +b.number ? -1 : 1); } public domain_backwards_compatibility = { @@ -44,11 +45,11 @@ export class DataService { } /** - * Parse the given stix bundle into the relevant data holders + * Parse the given stix bundles into the relevant data holders * @param domain * @param stixBundles */ - public parseBundle(domain: Domain, stixBundles: any[]): void { + public parseBundles(domain: Domain, stixBundles: any[]): void { let platforms = new Set(); let seenIDs = new Set(); let matrixSDOs = []; @@ -58,18 +59,8 @@ export class DataService { let techniqueSDOs = []; let bundleMatrices = []; let idToTechniqueSDO = new Map(); + // iterate through stix domain objects in the bundle for (let sdo of bundle.objects) { - // iterate through stix domain objects in the bundle - // Filter out object not included in this domain if domains field is available - if ( - !domain.isCustom && - sdo.x_mitre_domains?.length > 0 && - domain.urls.length == 1 && - !sdo.x_mitre_domains.includes(domain.domain_identifier) - ) { - continue; - } - // filter out duplicates, except for matrices // which are needed to properly build the datatables if (sdo.type != 'x-mitre-matrix') { @@ -275,9 +266,6 @@ export class DataService { } } - // Observable for data in config.json - private configData$: Observable; - // Observable for data private domainData$: Observable; @@ -327,13 +315,19 @@ export class DataService { public parseCollectionIndex(collectionIndex: any) { for (let collection of collectionIndex.collections) { for (let version of collection.versions) { + // TODO only parse most recent minor versions of a major release? + // TODO ignore beta versions? let versionNumber = version.version; let versionName = `${collectionIndex.name} v${versionNumber}`; + if (+versionNumber < +globals.minimumSupportedVersion) { + console.debug(`version ${versionNumber} is not supported, skipping ${versionName}`); + continue; + } + // create version & domain let v = this.addVersion(versionName, versionNumber); this.domains.push(new Domain(collection.id, collection.name, v, [version.url])); } } - console.log('**', this.domains, this.versions); } public addVersion(versionName: string, versionNumber: string): Version { @@ -395,7 +389,7 @@ export class DataService { let subscription; subscription = this.getDomainData(domain, refresh).subscribe({ next: (data: Object[]) => { - this.parseBundle(domain, data); + this.parseBundles(domain, data); resolve(null); }, complete: () => { diff --git a/nav-app/src/app/tabs/tabs.component.spec.ts b/nav-app/src/app/tabs/tabs.component.spec.ts index ac939daab..aabeeabb0 100755 --- a/nav-app/src/app/tabs/tabs.component.spec.ts +++ b/nav-app/src/app/tabs/tabs.component.spec.ts @@ -540,7 +540,7 @@ describe('TabsComponent', () => { component.openTab('layer', vm1, true, true, true, true); expect(component.getScoreExpressionError()).toEqual(null); component.dataService.setUpDomains(MockData.configData); // set up data - component.dataService.parseBundle(component.dataService.getDomain('enterprise-attack-13'), MockData.stixBundleSDO); //load the data + component.dataService.parseBundles(component.dataService.getDomain('enterprise-attack-13'), MockData.stixBundleSDO); //load the data component.opSettings.domain = 'enterprise-attack-13'; spyOn(component.dataService, 'loadDomainData').and.returnValue(Promise.resolve()); component.layerByOperation(); @@ -556,7 +556,7 @@ describe('TabsComponent', () => { component.openTab('layer2', vm2, true, true, true, true); component.dataService.setUpDomains(MockData.configDataExtended); // set up data - component.dataService.parseBundle(component.dataService.getDomain('enterprise-attack-13'), MockData.stixBundleSDO); //load the data + component.dataService.parseBundles(component.dataService.getDomain('enterprise-attack-13'), MockData.stixBundleSDO); //load the data component.opSettings.domain = 'enterprise-attack-13'; let alertSpy = spyOn(window, 'alert'); let consoleSpy = spyOn(console, 'error'); @@ -599,7 +599,7 @@ describe('TabsComponent', () => { it('should not upgrade layer with domain data loaded', waitForAsync(() => { component.dataService.setUpDomains(MockData.configDataExtended); - component.dataService.parseBundle(component.dataService.getDomain('enterprise-attack-13'), MockData.stixBundleSDO); + component.dataService.parseBundles(component.dataService.getDomain('enterprise-attack-13'), MockData.stixBundleSDO); let layer = JSON.parse(JSON.stringify(MockLayers.layerFile1)); let vm1 = component.viewModelsService.newViewModel('layer2', 'enterprise-attack-13'); let st1 = new Technique(MockData.T0000_000, [], null); @@ -665,7 +665,7 @@ describe('TabsComponent', () => { it('should not upgrade layer with default layer enabled and domain data loaded', waitForAsync(() => { component.dataService.setUpDomains(MockData.configDataExtended); - component.dataService.parseBundle(component.dataService.getDomain('enterprise-attack-13'), MockData.stixBundleSDO); + component.dataService.parseBundles(component.dataService.getDomain('enterprise-attack-13'), MockData.stixBundleSDO); let bb = JSON.parse(JSON.stringify(MockLayers.layerFile1)); let vm1 = component.viewModelsService.newViewModel('layer2', 'enterprise-attack-13'); spyOn(component.dataService, 'loadDomainData').and.returnValue(Promise.resolve()); @@ -677,7 +677,7 @@ describe('TabsComponent', () => { it('should not upgrade layer with domain data loaded', waitForAsync(() => { component.dataService.setUpDomains(MockData.configDataExtended); - component.dataService.parseBundle(component.dataService.getDomain('enterprise-attack-13'), MockData.stixBundleSDO); + component.dataService.parseBundles(component.dataService.getDomain('enterprise-attack-13'), MockData.stixBundleSDO); let layer = JSON.parse(JSON.stringify(MockLayers.layerFile1)); let vm1 = component.viewModelsService.newViewModel('layer2', 'enterprise-attack-13'); let st1 = new Technique(MockData.T0000_000, [], null); From 98512d81bebd65b8b73dadc88d3fa1b899084934 Mon Sep 17 00:00:00 2001 From: Charissa Miller <48832936+clemiller@users.noreply.github.com> Date: Tue, 5 Mar 2024 15:16:44 -0500 Subject: [PATCH 09/39] cleanup styles --- nav-app/src/styles.scss | 76 ++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 47 deletions(-) diff --git a/nav-app/src/styles.scss b/nav-app/src/styles.scss index 7b09e2829..b37cbf6ab 100755 --- a/nav-app/src/styles.scss +++ b/nav-app/src/styles.scss @@ -65,11 +65,26 @@ body { @include adaptive-color-dark-only('border-color', on-color(dark-1)); } - .content code { - @include adaptive-color-dark-only('background-color', color(dark-1)); - @include adaptive-color-dark-only('border-color', color(panel-dark) !important); - @include adaptive-color-dark-only('color', on-color(dark) !important); - } + .content { + overflow-y: scroll; + max-height: 60vh; + font-size: 11pt; + + p, + ul { + margin-block-start: 1em; + margin-block-end: 1em; + } + + code { + @include adaptive-color-dark-only('background-color', color(dark-1)); + @include adaptive-color-dark-only('border-color', color(panel-dark) !important); + @include adaptive-color-dark-only('color', on-color(dark) !important); + color: black; + padding: 1px 2px; + word-break: break-word; + } + } a { @include adaptive-color-dark-only('color', color(dark-link) !important); @@ -79,6 +94,15 @@ body { a:visited { @include adaptive-color-dark-only('color', color(dark-link-active)); } + + .top-button { + margin-top: 25px; + text-align: right; + } + + .close-button { + text-align: center; + } } .noselect { @@ -97,15 +121,11 @@ body { // | (_| (_) | .` | | | | / (_) | |__\__ \ // \___\___/|_|\_| |_| |_|_\\___/|____|___/ -// $inputFontSize: 10pt; // panel with controls .controlsContainer { @include adaptive-color('background-color', color(panel-dark), color(dark-2)); - // width: 100%; text-align: right; - // padding: 0 3px; - // display: flex; display: block; @media print { display: none; @@ -115,9 +135,6 @@ body { margin: 0; } - // justify-content: space-between; - // font: 400 12px system-ui; - .control-sections > li { list-style: none; display: inline-block; @@ -126,18 +143,13 @@ body { padding: 0 5px 0 5px; position: relative; - // text-align: center; // label for a section of controls, e.g layer controls or technique controls .section-label { @include adaptive-color('background-color', color(panel-dark), color(dark-2)); @include adaptive-color('color', color(tab-text-color), on-color(dark)); font-size: 8pt; - // height: 8px; top: -12px; position: absolute; - // border: 1px 1px 0 1px solid black; - // width:50%; - // left: 25%; padding: 0 5px; border-radius: 2px 2px 0 0; text-align: center; @@ -145,7 +157,6 @@ body { user-select: none; white-space: nowrap; z-index: 1; - // bottom: 10px; } .control-row-item { @@ -357,35 +368,6 @@ body { } } -.mat-dialog-container { - .top-button { - margin-top: 25px; - text-align: right; - } - - .close-button { - text-align: center; - } -} - -.mat-dialog-container .content { - overflow-y: scroll; - max-height: 60vh; - font-size: 11pt; - - p, - ul { - margin-block-start: 1em; - margin-block-end: 1em; - } - - code { - color: black; - padding: 1px 2px; - word-break: break-word; - } -} - .mat-dialog-container .markdown { padding: 25px; } From a5f09d934098a2c3a35ae56b00b10a936e116123 Mon Sep 17 00:00:00 2001 From: Charissa Miller <48832936+clemiller@users.noreply.github.com> Date: Tue, 5 Mar 2024 15:21:28 -0500 Subject: [PATCH 10/39] update tabs note --- nav-app/src/app/tabs/tabs.component.html | 9 ++++----- nav-app/src/app/tabs/tabs.component.ts | 5 ++++- nav-app/src/styles.scss | 5 +++++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/nav-app/src/app/tabs/tabs.component.html b/nav-app/src/app/tabs/tabs.component.html index 467727a67..0399e78bd 100755 --- a/nav-app/src/app/tabs/tabs.component.html +++ b/nav-app/src/app/tabs/tabs.component.html @@ -175,11 +175,10 @@

MITRE ATT&CK® Navigator

- - *Note: ATT&CK Versions prior to {{ minimumSupportedVersion }} are not supported by Navigator v{{ - navVersion - }}. - + + *Note: + ATT&CK Versions prior to v{{minimumSupportedVersion}} are not supported by Navigator v{{navVersion}}. +
OR diff --git a/nav-app/src/app/tabs/tabs.component.ts b/nav-app/src/app/tabs/tabs.component.ts index 64cfb963c..ce5459de2 100755 --- a/nav-app/src/app/tabs/tabs.component.ts +++ b/nav-app/src/app/tabs/tabs.component.ts @@ -32,7 +32,6 @@ export class TabsComponent implements AfterViewInit { public dropdownEnabled: string = ''; public layerTabs: Tab[] = []; public adjustedHeaderHeight: number = 0; - public navVersion = globals.navVersion; public safariDialogRef; public versionDialogRef; public versionMinorSnackbarRef; @@ -70,6 +69,10 @@ export class TabsComponent implements AfterViewInit { return globals.minimumSupportedVersion; } + public get navVersion(): string { + return globals.navVersion; + } + constructor( public dialog: MatDialog, public viewModelsService: ViewModelsService, diff --git a/nav-app/src/styles.scss b/nav-app/src/styles.scss index b37cbf6ab..b82a2c768 100755 --- a/nav-app/src/styles.scss +++ b/nav-app/src/styles.scss @@ -47,6 +47,11 @@ font-feature-settings: 'liga'; } +.text-deemphasis { + color: on-color-deemphasis(body); + font-size: 12px !important; +} + body { margin: 0; overflow-y: scroll; From 83f04958a5b7dc54edbd7215dfdb1ba162db056f Mon Sep 17 00:00:00 2001 From: Charissa Miller <48832936+clemiller@users.noreply.github.com> Date: Tue, 5 Mar 2024 15:22:06 -0500 Subject: [PATCH 11/39] use domain identifier for layer compatibility --- nav-app/src/app/services/data.service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nav-app/src/app/services/data.service.ts b/nav-app/src/app/services/data.service.ts index d938a32ef..de3849f5c 100755 --- a/nav-app/src/app/services/data.service.ts +++ b/nav-app/src/app/services/data.service.ts @@ -314,6 +314,7 @@ export class DataService { public parseCollectionIndex(collectionIndex: any) { for (let collection of collectionIndex.collections) { + let domainIdentifier = collection.name.replace(' ', '-').replace('&', 'a').toLowerCase(); for (let version of collection.versions) { // TODO only parse most recent minor versions of a major release? // TODO ignore beta versions? @@ -325,7 +326,7 @@ export class DataService { } // create version & domain let v = this.addVersion(versionName, versionNumber); - this.domains.push(new Domain(collection.id, collection.name, v, [version.url])); + this.domains.push(new Domain(domainIdentifier, collection.name, v, [version.url])); } } } From 73f6cddf67bc921daaf780b71cb90d5aece0fd7f Mon Sep 17 00:00:00 2001 From: Charissa Miller <48832936+clemiller@users.noreply.github.com> Date: Tue, 5 Mar 2024 15:25:36 -0500 Subject: [PATCH 12/39] code cleanup --- .../src/app/matrix/matrix-flat/matrix-flat.component.scss | 1 - .../src/app/matrix/matrix-side/matrix-side.component.html | 1 - nav-app/src/colors.scss | 6 ------ 3 files changed, 8 deletions(-) diff --git a/nav-app/src/app/matrix/matrix-flat/matrix-flat.component.scss b/nav-app/src/app/matrix/matrix-flat/matrix-flat.component.scss index 40e22d8b0..7f64e2d7f 100644 --- a/nav-app/src/app/matrix/matrix-flat/matrix-flat.component.scss +++ b/nav-app/src/app/matrix/matrix-flat/matrix-flat.component.scss @@ -94,7 +94,6 @@ .more-icon { transition: all ease 0.125s; vertical-align: top; - // TODO icon transform: scale(0.5) rotate(-90deg); width: 12px; height: 12px; diff --git a/nav-app/src/app/matrix/matrix-side/matrix-side.component.html b/nav-app/src/app/matrix/matrix-side/matrix-side.component.html index f1d1427e7..475b2435b 100644 --- a/nav-app/src/app/matrix/matrix-side/matrix-side.component.html +++ b/nav-app/src/app/matrix/matrix-side/matrix-side.component.html @@ -49,7 +49,6 @@ - Date: Tue, 26 Mar 2024 12:20:42 -0400 Subject: [PATCH 23/39] update existing tests --- .../src/app/classes/stix/data-component.ts | 3 +- nav-app/src/app/services/data.service.spec.ts | 27 ++- nav-app/src/app/tabs/tabs.component.spec.ts | 43 +++-- nav-app/src/tests/utils/mock-data.ts | 158 +++++++++--------- 4 files changed, 115 insertions(+), 116 deletions(-) diff --git a/nav-app/src/app/classes/stix/data-component.ts b/nav-app/src/app/classes/stix/data-component.ts index efdb2fd16..edfc9ccbc 100644 --- a/nav-app/src/app/classes/stix/data-component.ts +++ b/nav-app/src/app/classes/stix/data-component.ts @@ -1,5 +1,6 @@ import { DataService } from '../../services/data.service'; import { StixObject } from './stix-object'; +import { Technique } from './technique'; export class DataComponent extends StixObject { public readonly url: string; @@ -15,7 +16,7 @@ export class DataComponent extends StixObject { * @param domainVersionID the ID of the domain and version * @returns {Technique[]} list of techniques used by the data component */ - public techniques(domainVersionID): string[] { + public techniques(domainVersionID): Technique[] { const techniques = []; const domain = this.dataService.getDomain(domainVersionID); diff --git a/nav-app/src/app/services/data.service.spec.ts b/nav-app/src/app/services/data.service.spec.ts index c99f16af7..ce17881b6 100755 --- a/nav-app/src/app/services/data.service.spec.ts +++ b/nav-app/src/app/services/data.service.spec.ts @@ -21,7 +21,7 @@ describe('DataService', () => { providers: [DataService], }); configService = TestBed.inject(ConfigService); - configService.versions = []; + configService.versions = MockData.configData; dataService = TestBed.inject(DataService); http = TestBed.inject(HttpClient); }); @@ -31,14 +31,6 @@ describe('DataService', () => { expect(dataService).toBeTruthy(); }); - it('should set up data on failure to load config', () => { - expect(dataService.versions.length).toEqual(1); - expect(dataService.versions[0]).toEqual(dataService.latestVersion); - expect(dataService.domains.length).toEqual(3); - let last = dataService.domains[dataService.domains.length - 1]; - expect(last).toBeInstanceOf(Domain); - }); - it('should get domainVersionID with latest version', () => { let domainIdentifier = 'enterprise-attack'; let result = dataService.getDomainVersionID(domainIdentifier, ''); @@ -88,7 +80,7 @@ describe('DataService', () => { expect(mockService.versions.length).toEqual(1); expect(domain).toBeInstanceOf(Domain); expect(domain.authentication).toBeTruthy(); - expect(domain.authentication).toEqual(MockData.workbenchData[0].authentication); + expect(domain.authentication).toEqual(MockData.workbenchData.data[0].authentication); }); it('should fetch domain data via Workbench', () => { @@ -110,9 +102,9 @@ describe('DataService', () => { expect(mockService.versions.length).toEqual(1); expect(domain).toBeInstanceOf(Domain); expect(domain.taxii_url).toBeTruthy(); - expect(domain.taxii_url).toEqual(MockData.taxiiData[0].domains[0].taxii_url); + expect(domain.taxii_url).toEqual(MockData.taxiiData.data[0].domains[0].taxii_url); expect(domain.taxii_collection).toBeTruthy(); - expect(domain.taxii_collection).toEqual(MockData.taxiiData[0].domains[0].taxii_collection); + expect(domain.taxii_collection).toEqual(MockData.taxiiData.data[0].domains[0].taxii_collection); }); it('should fetch domain data via TAXII', () => { @@ -194,7 +186,7 @@ describe('DataService', () => { testObjectType(domain.dataComponents, 1, DataComponent); testObjectType(domain.groups, 1, Group); testObjectType(domain.matrices, 1, Matrix); - testObjectType(domain.mitigations, 1, Mitigation); + testObjectType(domain.mitigations, 2, Mitigation); testObjectType(domain.notes, 1, Note); testObjectType(domain.software, 2, Software); testObjectType(domain.tactics, 1, Tactic); @@ -221,9 +213,12 @@ describe('DataService', () => { let newVersion = { name: 'ATT&CK v14', version: '14', - domains: MockData.configData[0].domains, + domains: MockData.configData.data[0].domains, }; - configService.versions = [MockData.configData[0], newVersion]; + configService.versions = { + enabled: true, + data: [MockData.configData.data[0], newVersion] + }; spyOn(DataService.prototype, 'setUpDomains').and.callThrough(); mockService = new DataService(http, configService); }); @@ -386,7 +381,7 @@ describe('DataService', () => { }); expect(data_component_test.source('enterprise-attack-13')).toEqual({ name: 'Name', url: 'test-url.com' }); mockService.domains[0].relationships['component_rel'].set('data-component-0', ['attack-pattern-0']); - expect(data_component_test.techniques('enterprise-attack-13')).toEqual(['attack-pattern-0']); + expect(data_component_test.techniques('enterprise-attack-13')[0].id).toEqual('attack-pattern-0'); }); it('should test group', () => { diff --git a/nav-app/src/app/tabs/tabs.component.spec.ts b/nav-app/src/app/tabs/tabs.component.spec.ts index aabeeabb0..ac6d4e0c2 100755 --- a/nav-app/src/app/tabs/tabs.component.spec.ts +++ b/nav-app/src/app/tabs/tabs.component.spec.ts @@ -40,7 +40,7 @@ describe('TabsComponent', () => { }).compileComponents(); dialog = TestBed.inject(MatDialog); configService = TestBed.inject(ConfigService); - configService.versions = []; + configService.versions = {enabled: true, data: []}; configService.banner = 'test banner'; configService.defaultLayers = MockData.defaultLayersDisabled; dataService = TestBed.inject(DataService); @@ -457,16 +457,16 @@ describe('TabsComponent', () => { }); it('should create new layer from url', waitForAsync(() => { - component.dataService.domains[0].dataLoaded = true; + component.dataService.setUpDomains(MockData.configData.data); component.http = http; spyOn(component.http, 'get').and.returnValue(of(MockLayers.layerFile1)); spyOn(component.dataService, 'loadDomainData').and.returnValue(Promise.resolve()); component.newLayerFromURL(loadData, JSON.parse(JSON.stringify(MockLayers.layerFile1))); - expect(component.dataService.domains.length).toEqual(3); + expect(component.dataService.domains.length).toEqual(2); })); it('should read and open json file', waitForAsync(() => { - component.dataService.setUpDomains(MockData.configData); + component.dataService.setUpDomains(MockData.configData.data); let mockedDocElement = document.createElement('input'); mockedDocElement.id = 'uploader'; mockedDocElement.value = 'test1'; @@ -525,7 +525,7 @@ describe('TabsComponent', () => { component.openTab('layer', vm1, true, true, true, true); component.openTab('layer1', vm2, true, true, true, true); expect(component.getScoreExpressionError()).toEqual('Layer b does not match the chosen domain'); - component.dataService.setUpDomains(MockData.configData); // set up data + component.dataService.setUpDomains(MockData.configData.data); // set up data component.opSettings.domain = 'enterprise-attack-13'; expect(component.getFilteredVMs()).toEqual(component.viewModelsService.viewModels); spyOn(component.dataService, 'loadDomainData').and.returnValue(Promise.resolve()); @@ -539,7 +539,7 @@ describe('TabsComponent', () => { let vm1 = component.viewModelsService.newViewModel('layer', 'enterprise-attack-13'); component.openTab('layer', vm1, true, true, true, true); expect(component.getScoreExpressionError()).toEqual(null); - component.dataService.setUpDomains(MockData.configData); // set up data + component.dataService.setUpDomains(MockData.configData.data); // set up data component.dataService.parseBundles(component.dataService.getDomain('enterprise-attack-13'), MockData.stixBundleSDO); //load the data component.opSettings.domain = 'enterprise-attack-13'; spyOn(component.dataService, 'loadDomainData').and.returnValue(Promise.resolve()); @@ -555,7 +555,7 @@ describe('TabsComponent', () => { component.openTab('layer', vm1, true, true, true, true); component.openTab('layer2', vm2, true, true, true, true); - component.dataService.setUpDomains(MockData.configDataExtended); // set up data + component.dataService.setUpDomains(MockData.configDataExtended.data); // set up data component.dataService.parseBundles(component.dataService.getDomain('enterprise-attack-13'), MockData.stixBundleSDO); //load the data component.opSettings.domain = 'enterprise-attack-13'; let alertSpy = spyOn(window, 'alert'); @@ -568,7 +568,7 @@ describe('TabsComponent', () => { describe('versionUpgradeDialog', () => { it('should upgrade layer', waitForAsync(() => { - component.dataService.setUpDomains(MockData.configDataExtended); + component.dataService.setUpDomains(MockData.configDataExtended.data); let layer = JSON.parse(JSON.stringify(MockLayers.layerFile1)); let vm1 = component.viewModelsService.newViewModel('layer2', 'enterprise-attack-12'); let versionUpgradeSpy = spyOn(component, 'versionUpgradeDialog').and.returnValue( @@ -579,12 +579,12 @@ describe('TabsComponent', () => { expect(versionUpgradeSpy).toHaveBeenCalled(); }); fixture.whenStable().then(() => { - expect(component.layerTabs.length).toEqual(2); + expect(component.layerTabs.length).toEqual(1); }); })); it('should not upgrade layer', waitForAsync(() => { - component.dataService.setUpDomains(MockData.configDataExtended); + component.dataService.setUpDomains(MockData.configDataExtended.data); let layer = JSON.parse(JSON.stringify(MockLayers.layerFile1)); let vm1 = component.viewModelsService.newViewModel('layer2', 'enterprise-attack-12'); let versionUpgradeSpy = spyOn(component, 'versionUpgradeDialog').and.returnValue(Promise.resolve(null)); @@ -598,7 +598,7 @@ describe('TabsComponent', () => { })); it('should not upgrade layer with domain data loaded', waitForAsync(() => { - component.dataService.setUpDomains(MockData.configDataExtended); + component.dataService.setUpDomains(MockData.configDataExtended.data); component.dataService.parseBundles(component.dataService.getDomain('enterprise-attack-13'), MockData.stixBundleSDO); let layer = JSON.parse(JSON.stringify(MockLayers.layerFile1)); let vm1 = component.viewModelsService.newViewModel('layer2', 'enterprise-attack-13'); @@ -623,7 +623,7 @@ describe('TabsComponent', () => { describe('upgradeLayer', () => { it('should upgrade layer', waitForAsync(() => { - component.dataService.setUpDomains(MockData.configDataExtended); + component.dataService.setUpDomains(MockData.configDataExtended.data); let layer = JSON.parse(JSON.stringify(MockLayers.layerFile1)); let vm1 = component.viewModelsService.newViewModel('layer2', 'enterprise-attack-12'); let versionUpgradeSpy = spyOn(component, 'versionUpgradeDialog').and.returnValue( @@ -634,12 +634,12 @@ describe('TabsComponent', () => { expect(versionUpgradeSpy).toHaveBeenCalled(); }); fixture.whenStable().then(() => { - expect(component.layerTabs.length).toEqual(2); + expect(component.layerTabs.length).toEqual(1); }); })); it('should not upgrade layer', waitForAsync(() => { - component.dataService.setUpDomains(MockData.configDataExtended); + component.dataService.setUpDomains(MockData.configDataExtended.data); let layer = JSON.parse(JSON.stringify(MockLayers.layerFile1)); let vm1 = component.viewModelsService.newViewModel('layer2', 'enterprise-attack-12'); let versionUpgradeSpy = spyOn(component, 'versionUpgradeDialog').and.returnValue(Promise.resolve(null)); @@ -653,7 +653,7 @@ describe('TabsComponent', () => { })); it('should not upgrade layer with default layer enabled', waitForAsync(() => { - component.dataService.setUpDomains(MockData.configDataExtended); + component.dataService.setUpDomains(MockData.configDataExtended.data); let layer = JSON.parse(JSON.stringify(MockLayers.layerFile1)); let vm1 = component.viewModelsService.newViewModel('layer2', 'enterprise-attack-12'); spyOn(component.dataService, 'loadDomainData').and.returnValue(Promise.resolve()); @@ -664,7 +664,7 @@ describe('TabsComponent', () => { })); it('should not upgrade layer with default layer enabled and domain data loaded', waitForAsync(() => { - component.dataService.setUpDomains(MockData.configDataExtended); + component.dataService.setUpDomains(MockData.configDataExtended.data); component.dataService.parseBundles(component.dataService.getDomain('enterprise-attack-13'), MockData.stixBundleSDO); let bb = JSON.parse(JSON.stringify(MockLayers.layerFile1)); let vm1 = component.viewModelsService.newViewModel('layer2', 'enterprise-attack-13'); @@ -676,7 +676,7 @@ describe('TabsComponent', () => { })); it('should not upgrade layer with domain data loaded', waitForAsync(() => { - component.dataService.setUpDomains(MockData.configDataExtended); + component.dataService.setUpDomains(MockData.configDataExtended.data); component.dataService.parseBundles(component.dataService.getDomain('enterprise-attack-13'), MockData.stixBundleSDO); let layer = JSON.parse(JSON.stringify(MockLayers.layerFile1)); let vm1 = component.viewModelsService.newViewModel('layer2', 'enterprise-attack-13'); @@ -701,16 +701,13 @@ describe('TabsComponent', () => { describe('loadLayerFromURL', () => { it('should load from url', waitForAsync(() => { - component.dataService.setUpDomains(MockData.configData); + component.dataService.setUpDomains(MockData.configData.data); component.http = http; spyOn(component.http, 'get').and.returnValue(of(MockLayers.layerFile1)); - let versionMismatchSpy = spyOn(component, 'versionMismatchWarning').and.returnValue(Promise.resolve(true)); - let upgradeLayerSpy = spyOn(component, 'upgradeLayer').and.returnValue(Promise.resolve([])); component .loadLayerFromURL('https://raw.githubusercontent.com/mitre-attack/attack-navigator/master/layers/data/samples/Bear_APT.json', false) .then(() => { - expect(versionMismatchSpy).toHaveBeenCalled(); - expect(upgradeLayerSpy).toHaveBeenCalled(); + expect(component.loadTabs.length).toEqual(1); }); })); @@ -731,7 +728,6 @@ describe('TabsComponent', () => { component.dataService.setUpDomains(versions); component.http = http; spyOn(component.http, 'get').and.returnValue(of(MockLayers.layerFile1)); - let versionMismatchSpy = spyOn(component, 'versionMismatchWarning').and.returnValue(Promise.resolve(true)); let alertSpy = spyOn(window, 'alert'); let consoleSpy = spyOn(console, 'error'); component @@ -739,7 +735,6 @@ describe('TabsComponent', () => { .then(() => { expect(consoleSpy).toHaveBeenCalled(); expect(alertSpy).toHaveBeenCalled(); - expect(versionMismatchSpy).toHaveBeenCalled(); }); })); }); diff --git a/nav-app/src/tests/utils/mock-data.ts b/nav-app/src/tests/utils/mock-data.ts index 663e413df..51b0d6d6e 100644 --- a/nav-app/src/tests/utils/mock-data.ts +++ b/nav-app/src/tests/utils/mock-data.ts @@ -1,57 +1,49 @@ // Mock Test Data // mock data configurations -export const configData = [ - { - name: 'ATT&CK v13', - version: '13', - domains: [ - { - name: 'Enterprise', - identifier: 'enterprise-attack', - data: ['https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v13.1/enterprise-attack/enterprise-attack.json'], - }, - ], - }, -]; - -export const mobileDomainData = [ - { - name: 'ATT&CK v13', - version: '13', - domains: [ - { - name: 'Mobile', - identifier: 'mobile-attack', - data: ['https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v14.1/mobile-attack/mobile-attack.json'], - }, - ], - }, -]; -export const configDataExtended = [ - { - name: 'ATT&CK v13', - version: '13', - domains: [ - { - name: 'Enterprise', - identifier: 'enterprise-attack', - data: ['https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v13.1/enterprise-attack/enterprise-attack.json'], - }, - ], - }, - { - name: 'ATT&CK v12', - version: '12', - domains: [ - { - name: 'Enterprise', - identifier: 'enterprise-attack', - data: ['https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v12.1/enterprise-attack/enterprise-attack.json'], - }, - ], - }, -]; +export const configData = { + "enabled": true, + "data": [ + { + name: 'ATT&CK v13', + version: '13', + domains: [ + { + name: 'Enterprise', + identifier: 'enterprise-attack', + data: ['https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v13.1/enterprise-attack/enterprise-attack.json'], + }, + ], + }, + ] +} +export const configDataExtended = { + enabled: true, + data: [ + { + name: 'ATT&CK v13', + version: '13', + domains: [ + { + name: 'Enterprise', + identifier: 'enterprise-attack', + data: ['https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v13.1/enterprise-attack/enterprise-attack.json'], + }, + ], + }, + { + name: 'ATT&CK v12', + version: '12', + domains: [ + { + name: 'Enterprise', + identifier: 'enterprise-attack', + data: ['https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v12.1/enterprise-attack/enterprise-attack.json'], + }, + ], + }, + ] +}; export const defaultLayersEnabled = { enabled: true, urls: ['https://raw.githubusercontent.com/mitre-attack/attack-navigator/master/layers/data/samples/Bear_APT.json'], @@ -61,30 +53,46 @@ export const defaultLayersDisabled = { urls: ['https://raw.githubusercontent.com/mitre-attack/attack-navigator/master/layers/data/samples/Bear_APT.json'], }; -export const taxiiData = [ - { - name: 'ATT&CK v13', - version: '13', - domains: [ - { - name: 'Enterprise', - identifier: 'enterprise-attack', - taxii_url: 'https://cti-taxii.mitre.org/', - taxii_collection: '95ecc380-afe9-11e4-9b6c-751b66dd541e', - }, - ], - }, -]; -export const workbenchData = [ - { - ...configData[0], - authentication: { - enabled: true, - serviceName: 'navigator', - apiKey: 'sample-navigator-apikey', - }, - }, -]; +export const taxiiData = { + enabled: true, + data: [ + { + name: 'ATT&CK v13', + version: '13', + domains: [ + { + name: 'Enterprise', + identifier: 'enterprise-attack', + taxii_url: 'https://cti-taxii.mitre.org/', + taxii_collection: '95ecc380-afe9-11e4-9b6c-751b66dd541e', + }, + ], + }, + ] +}; + +export const workbenchData = { + enabled: true, + data: [ + { + name: 'ATT&CK v13', + version: '13', + domains: [ + { + name: 'Enterprise', + identifier: 'enterprise-attack', + data: ['https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v13.1/enterprise-attack/enterprise-attack.json'], + }, + ], + authentication: { + enabled: true, + serviceName: 'navigator', + apiKey: 'sample-navigator-apikey', + }, + }, + ] +}; + export const configTechniqueControls = { name: 'technique_controls', enabled: true, From fcb043ddb92332d1e3b727635dceaab5e701d209 Mon Sep 17 00:00:00 2001 From: Charissa Miller <48832936+clemiller@users.noreply.github.com> Date: Tue, 26 Mar 2024 12:40:02 -0400 Subject: [PATCH 24/39] config validation tests --- .../src/app/services/config.service.spec.ts | 21 ++++++++++++ nav-app/src/app/services/config.service.ts | 8 ++++- nav-app/src/tests/utils/mock-data.ts | 32 +++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/nav-app/src/app/services/config.service.spec.ts b/nav-app/src/app/services/config.service.spec.ts index 4be440d0f..750f87923 100644 --- a/nav-app/src/app/services/config.service.spec.ts +++ b/nav-app/src/app/services/config.service.spec.ts @@ -73,4 +73,25 @@ describe('ConfigService', () => { it('should set up data in constructor', () => { expect(service).toBeTruthy(); }); + + it('should pass validation if only versions is configured', () => { + expect(() => service.validateConfig(MockData.versionsConfig)).not.toThrow(); + }); + + it('should pass validation if only collection index is configured', () => { + expect(() => service.validateConfig(MockData.collectionIndexConfig)).not.toThrow(); + }); + + it('should fail validation if collection_index_url is not a string', () => { + expect(() => service.validateConfig(MockData.invalidTypeConfig)).toThrowError(); + }); + + it('should fail validation if neither versions or collection index are configured', () => { + expect(() => service.validateConfig(MockData.invalidConfig)).toThrowError(); + }); + + it('should return config if validation passes', () => { + expect(() => service.validateConfig(MockData.customConfig)).not.toThrow(); + expect(service.validateConfig(MockData.customConfig)).toEqual(MockData.customConfig); + }); }); diff --git a/nav-app/src/app/services/config.service.ts b/nav-app/src/app/services/config.service.ts index 371e5b631..21f487cdd 100644 --- a/nav-app/src/app/services/config.service.ts +++ b/nav-app/src/app/services/config.service.ts @@ -178,8 +178,14 @@ export class ConfigService { return fragments; } + /** + * Validate that the configuration file specifies a collection index URL + * or a list of versions/domains + * @param config the configuration to validate + * @returns the configuration, if valid, otherwise throws an error + */ public validateConfig(config: any): any { - if (!config.collection_index_url && !config.versions?.length) { + if (!config.collection_index_url && !config.versions?.data?.length) { throw new Error(`'collection_index_url' or 'versions' must be defined`); } if (config.collection_index_url && typeof config.collection_index_url !== typeof 'string') { diff --git a/nav-app/src/tests/utils/mock-data.ts b/nav-app/src/tests/utils/mock-data.ts index 51b0d6d6e..6a4d234eb 100644 --- a/nav-app/src/tests/utils/mock-data.ts +++ b/nav-app/src/tests/utils/mock-data.ts @@ -17,6 +17,38 @@ export const configData = { }, ] } +export const collectionIndexConfig = { + "collection_index_url": "https://raw.githubusercontent.com/mitre-attack/attack-stix-data/master/index.json", +} +export const versionsConfig = { + "versions": { + "enabled": true, + "data": [ + { + name: 'ATT&CK v13', + version: '13', + domains: [ + { + name: 'Enterprise', + identifier: 'enterprise-attack', + data: ['https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v13.1/enterprise-attack/enterprise-attack.json'], + }, + ], + }, + ] + } +} +export const customConfig = { + ...collectionIndexConfig, + ...versionsConfig +} +export const invalidTypeConfig = { + "collection_index_url": false +} +export const invalidConfig = { + "collection_index_url": "", + "versions": {} +} export const configDataExtended = { enabled: true, data: [ From d8efb4e4d14163a50d356ca18e1dc3d3206517e2 Mon Sep 17 00:00:00 2001 From: Charissa Miller <48832936+clemiller@users.noreply.github.com> Date: Tue, 26 Mar 2024 12:54:34 -0400 Subject: [PATCH 25/39] add dataservice tests --- nav-app/src/app/services/data.service.spec.ts | 43 ++++++++++++++++++- nav-app/src/app/services/data.service.ts | 18 +++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/nav-app/src/app/services/data.service.spec.ts b/nav-app/src/app/services/data.service.spec.ts index ce17881b6..3e723700f 100755 --- a/nav-app/src/app/services/data.service.spec.ts +++ b/nav-app/src/app/services/data.service.spec.ts @@ -26,7 +26,7 @@ describe('DataService', () => { http = TestBed.inject(HttpClient); }); - describe('setup with default versions', () => { + describe('helper functions', () => { it('should be created', () => { expect(dataService).toBeTruthy(); }); @@ -66,8 +66,49 @@ describe('DataService', () => { expect(result.oldDomainVersionID).toEqual(domain.id); expect(result.unchanged.length).toEqual(3); }); + + it('should get domain identifier from name', () => { + const domainName = "Enterprise ATT&CK"; + expect(dataService.getDomainIdentifier(domainName)).toEqual('enterprise-attack'); + }); + + it('should handle empty string', () => { + expect(dataService.getDomainIdentifier('')).toEqual(''); + }); }); + describe('set up via collection index', () => { + beforeEach(() => { + dataService.versions = []; + }); + + it('should add new version when it does not already exist', () => { + const versionName = "Enterprise ATT&CK v14"; + const versionNumber = "14"; + + const result = dataService.addVersion(versionName, versionNumber); + + expect(result instanceof Version).toBeTruthy(); + expect(result.name).toBe(versionName); + expect(result.number).toBe(versionNumber); + expect(dataService.versions.length).toBe(1); + expect(dataService.versions[0]).toBe(result); + }); + + it('should return existing version if it already exists', () => { + const versionName = "Enterprise ATT&CK v14"; + const versionNumber = "14"; + + const existingVersion = new Version(versionName, versionNumber); + dataService.versions.push(existingVersion); + + const result = dataService.addVersion(versionName, versionNumber); + + expect(result).toBe(existingVersion); + expect(dataService.versions.length).toBe(1); + }); + }); + describe('setup with Workbench integration', () => { beforeEach(() => { configService.versions = MockData.workbenchData; diff --git a/nav-app/src/app/services/data.service.ts b/nav-app/src/app/services/data.service.ts index 46e5ad79b..23e57f2ca 100755 --- a/nav-app/src/app/services/data.service.ts +++ b/nav-app/src/app/services/data.service.ts @@ -276,7 +276,6 @@ export class DataService { /** * Set up the URLs for domains in the list defined in the config file * @param {versions} list of versions and domains - * @memberof DataService */ public setUpDomains(versions: any[]) { versions.forEach((version: any) => { @@ -296,6 +295,10 @@ export class DataService { }); } + /** + * Parses the collection index for domains/versions + * @param collectionIndex the collection index + */ public parseCollectionIndex(collectionIndex: any) { for (let collection of collectionIndex.collections) { let domainIdentifier = this.getDomainIdentifier(collection.name); @@ -324,10 +327,23 @@ export class DataService { } } + /** + * Retrieves the domain identifier from the domain name + * Helper function for parseCollectionIndex() + * @param domainName the name of the domain + * @returns the domain identifier (e.g. 'enterprise-attack') + */ public getDomainIdentifier(domainName: string): string { return domainName.replace(/ /g, '-').replace(/&/g, 'a').toLowerCase(); } + /** + * Adds a new version to the list of versions, checking if + * one already exists. + * @param versionName the name of the version + * @param versionNumber the version number + * @returns the existing or created Version object + */ public addVersion(versionName: string, versionNumber: string): Version { // check if version already exists let existingVersion = this.versions.find(v => v.name === versionName && v.number === versionNumber); From 0f304c7fa27ca32d2c942cd7b9ffc764251ca1bd Mon Sep 17 00:00:00 2001 From: Charissa Miller <48832936+clemiller@users.noreply.github.com> Date: Wed, 27 Mar 2024 09:06:54 -0400 Subject: [PATCH 26/39] data service tests --- nav-app/src/app/services/data.service.spec.ts | 14 ++++++ nav-app/src/tests/utils/mock-data.ts | 46 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/nav-app/src/app/services/data.service.spec.ts b/nav-app/src/app/services/data.service.spec.ts index 3e723700f..717813977 100755 --- a/nav-app/src/app/services/data.service.spec.ts +++ b/nav-app/src/app/services/data.service.spec.ts @@ -107,6 +107,20 @@ describe('DataService', () => { expect(result).toBe(existingVersion); expect(dataService.versions.length).toBe(1); }); + + it('should parse collection index correctly', () => { + spyOn(dataService, 'getDomainIdentifier').and.callThrough(); + spyOn(dataService, 'addVersion').and.callThrough(); + spyOn(console, 'debug'); + + dataService.parseCollectionIndex(MockData.collectionIndex); + + expect(dataService.getDomainIdentifier).toHaveBeenCalledTimes(3); // once for each collection + expect(dataService.addVersion).toHaveBeenCalledTimes(3); // once for each valid defined MAJOR version + expect(console.debug).toHaveBeenCalledTimes(1); // once for v1.0 + expect(dataService.versions.length).toBe(1); // for each uniquely defined MAJOR version + expect(dataService.domains.length).toBe(4); // for each supported domain + }); }); describe('setup with Workbench integration', () => { diff --git a/nav-app/src/tests/utils/mock-data.ts b/nav-app/src/tests/utils/mock-data.ts index 6a4d234eb..bc2305aea 100644 --- a/nav-app/src/tests/utils/mock-data.ts +++ b/nav-app/src/tests/utils/mock-data.ts @@ -142,6 +142,52 @@ export const configToolbarControls = { subfeatures: [{ name: 'sticky_toolbar', enabled: true, description: 'Disable to remove the ability to enable/disable the sticky toolbar.' }], }; +// mock collection index +export const collectionIndex = { + "name": "MITRE ATT&CK", + "collections": [ + { + "name": "Enterprise ATT&CK", + "id": "x-mitre-collection--1f5f1533-f617-4ca8-9ab4-6a02367fa019", + "versions": [ + { + "version": "14.1", + "url": "https://raw.githubusercontent.com/mitre-attack/attack-stix-data/master/enterprise-attack/enterprise-attack-14.1.json", + }, + { + "version": "14.0", + "url": "https://raw.githubusercontent.com/mitre-attack/attack-stix-data/master/enterprise-attack/enterprise-attack-14.0.json", + }, + ] + }, + { + "name": "Mobile ATT&CK", + "id": "x-mitre-collection--dac0d2d7-8653-445c-9bff-82f934c1e858", + "versions": [ + { + "version": "14.1", + "url": "https://raw.githubusercontent.com/mitre-attack/attack-stix-data/master/mobile-attack/mobile-attack-14.1.json", + }, + { + "version": "1.0", + "url": "https://raw.githubusercontent.com/mitre-attack/attack-stix-data/master/enterprise-attack/enterprise-attack-1.0.json", + }, + ] + }, + { + "name": "ICS ATT&CK", + "id": "x-mitre-collection--90c00720-636b-4485-b342-8751d232bf09", + "versions": [ + { + "version": "14.1", + "url": "https://raw.githubusercontent.com/mitre-attack/attack-stix-data/master/ics-attack/ics-attack-14.1.json", + "modified": "2023-11-14T14:00:00.188Z" + }, + ] + } + ] +} + // mock base STIX SDOs export const stixSDO = { name: 'Name', From 4bb18d942a6213652c919a922adecf181d18bbb7 Mon Sep 17 00:00:00 2001 From: Charissa Miller <48832936+clemiller@users.noreply.github.com> Date: Wed, 27 Mar 2024 09:13:10 -0400 Subject: [PATCH 27/39] tabs component tests --- nav-app/src/app/tabs/tabs.component.spec.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/nav-app/src/app/tabs/tabs.component.spec.ts b/nav-app/src/app/tabs/tabs.component.spec.ts index ac6d4e0c2..7f4617b58 100755 --- a/nav-app/src/app/tabs/tabs.component.spec.ts +++ b/nav-app/src/app/tabs/tabs.component.spec.ts @@ -24,7 +24,6 @@ describe('TabsComponent', () => { let dataService: DataService; let configService: ConfigService; let http: HttpClient; - let snackBar: MatSnackBar; let testTab = new Tab('test tab', true, false, 'enterprise-attack', true); let loadData = { @@ -45,7 +44,6 @@ describe('TabsComponent', () => { configService.defaultLayers = MockData.defaultLayersDisabled; dataService = TestBed.inject(DataService); http = TestBed.inject(HttpClient); - snackBar = TestBed.inject(MatSnackBar); fixture = TestBed.createComponent(TabsComponent); component = fixture.debugElement.componentInstance; }); @@ -494,6 +492,18 @@ describe('TabsComponent', () => { expect(component.layerTabs.length).toEqual(3); }); })); + + it('should retrieve the minimum supported version', () => { + const result = component.minimumSupportedVersion; + expect(result).toBeDefined(); + expect(result).toBe('4.0'); + }); + + it('should retrieve the current navigator version', () => { + const result = component.navVersion; + expect(result).toBeDefined(); + expect(typeof result).toBe('string'); + }); }); describe('validateInput', () => { From c36592e790ed5ab2dee385caac94930231966972 Mon Sep 17 00:00:00 2001 From: Charissa Miller <48832936+clemiller@users.noreply.github.com> Date: Wed, 27 Mar 2024 09:15:12 -0400 Subject: [PATCH 28/39] autoformatter --- nav-app/angular.json | 12 +- nav-app/redirects/enterprise/index.html | 80 +++--- nav-app/redirects/mobile/index.html | 80 +++--- nav-app/src/app/classes/view-model.ts | 2 +- .../layer-upgrade.component.html | 14 +- .../layer-upgrade/layer-upgrade.component.ts | 24 +- .../src/app/services/config.service.spec.ts | 40 +-- nav-app/src/app/services/config.service.ts | 145 +++++----- nav-app/src/app/services/data.service.spec.ts | 92 +++---- nav-app/src/app/services/data.service.ts | 148 +++++----- nav-app/src/app/tabs/tabs.component.html | 7 +- nav-app/src/app/tabs/tabs.component.spec.ts | 28 +- nav-app/src/app/tabs/tabs.component.ts | 12 +- nav-app/src/index.google-analytics.html | 14 +- nav-app/src/styles.scss | 46 ++-- nav-app/src/tests/utils/mock-data.ts | 260 +++++++++--------- 16 files changed, 505 insertions(+), 499 deletions(-) diff --git a/nav-app/angular.json b/nav-app/angular.json index 5d91d2f9e..f68ada734 100644 --- a/nav-app/angular.json +++ b/nav-app/angular.json @@ -63,12 +63,12 @@ } ] }, - "googleAnalytics": { - "index": { - "input": "src/index.google-analytics.html", - "output": "index.html" - } - } + "googleAnalytics": { + "index": { + "input": "src/index.google-analytics.html", + "output": "index.html" + } + } }, "defaultConfiguration": "" }, diff --git a/nav-app/redirects/enterprise/index.html b/nav-app/redirects/enterprise/index.html index 10de3091c..9ad7a56de 100644 --- a/nav-app/redirects/enterprise/index.html +++ b/nav-app/redirects/enterprise/index.html @@ -1,39 +1,43 @@ - + - - - - - - ATT&CK® Navigator - - -
-

ATT&CK Navigator's Enterprise instance has moved to the new multi-domain instance.

-

This page should automatically redirect. If it does not, please use the following link:

-

https://mitre-attack.github.io/attack-navigator/

-
- - - - - \ No newline at end of file + + + + + + ATT&CK® Navigator + + +
+

ATT&CK Navigator's Enterprise instance has moved to the new multi-domain instance.

+

This page should automatically redirect. If it does not, please use the following link:

+

https://mitre-attack.github.io/attack-navigator/

+
+ + + + + diff --git a/nav-app/redirects/mobile/index.html b/nav-app/redirects/mobile/index.html index 78391a79e..2acc2a2a1 100644 --- a/nav-app/redirects/mobile/index.html +++ b/nav-app/redirects/mobile/index.html @@ -1,39 +1,43 @@ - + - - - - - - ATT&CK® Navigator - - -
-

ATT&CK Navigator's Mobile instance has moved to the new multi-domain instance.

-

This page should automatically redirect. If it does not, please use the following link:

-

https://mitre-attack.github.io/attack-navigator/

-
- - - - - \ No newline at end of file + + + + + + ATT&CK® Navigator + + +
+

ATT&CK Navigator's Mobile instance has moved to the new multi-domain instance.

+

This page should automatically redirect. If it does not, please use the following link:

+

https://mitre-attack.github.io/attack-navigator/

+
+ + + + + diff --git a/nav-app/src/app/classes/view-model.ts b/nav-app/src/app/classes/view-model.ts index 9bcd9be2d..3dcf34861 100644 --- a/nav-app/src/app/classes/view-model.ts +++ b/nav-app/src/app/classes/view-model.ts @@ -1078,7 +1078,7 @@ export class ViewModel { let versionNumber = ''; let obj = typeof rep == 'string' ? JSON.parse(rep) : rep; this.name = obj.name; - // layer with no specified version defaults to current version + // layer with no specified version defaults to current version this.version = this.dataService.latestVersion.number; if ('versions' in obj) { if ('attack' in obj.versions) { diff --git a/nav-app/src/app/layer-upgrade/layer-upgrade.component.html b/nav-app/src/app/layer-upgrade/layer-upgrade.component.html index fac2f5d8f..1c1d51015 100644 --- a/nav-app/src/app/layer-upgrade/layer-upgrade.component.html +++ b/nav-app/src/app/layer-upgrade/layer-upgrade.component.html @@ -51,11 +51,7 @@

Options

show annotated techniques only
- + - +