diff --git a/nav-app/angular.json b/nav-app/angular.json index dd5a16ba8..f504fac5c 100644 --- a/nav-app/angular.json +++ b/nav-app/angular.json @@ -142,5 +142,8 @@ "@schematics/angular:directive": { "prefix": "app" } + }, + "cli": { + "analytics": false } } diff --git a/nav-app/src/app/classes/filter.ts b/nav-app/src/app/classes/filter.ts index 7a665ec9f..d69c100a6 100644 --- a/nav-app/src/app/classes/filter.ts +++ b/nav-app/src/app/classes/filter.ts @@ -8,11 +8,21 @@ export class Filter { selection: string[]; }; + public dataSources : { + options: string[]; + selection: string[]; + } + constructor() { this.platforms = { selection: [], options: [], }; + + this.dataSources = { + selection: [], + options: [], + }; } /** @@ -21,12 +31,31 @@ export class Filter { */ public initPlatformOptions(domain: Domain): void { this.platforms.options = JSON.parse(JSON.stringify(domain.platforms)); + if (!this.platforms.selection.length) { // prevent overwriting current selection this.platforms.selection = JSON.parse(JSON.stringify(domain.platforms)); } } + /** + * Initialize the data source options according to the data in the domain + * @param {Domain} domain the domain to parse for data source options + */ + + public initDataSourcesOptions(domain: Domain): void { + // dataSourcesMap is a Map + // We want to store the name field in the options array as well as the selection array + + // Iterate over the entries of the Map + for (const [key, value] of domain.dataSources.entries()) { + // Store the name field in the options array + this.dataSources.options.push(value.name); + this.dataSources.selection.push(value.name); + } + + } + /** * toggle the given value in the given filter * @param {*} filterName the name of the filter @@ -42,8 +71,7 @@ export class Filter { this[filterName].selection.splice(index, 1); } else { this[filterName].selection.push(value); - } - } + } } /** * determine if the given value is active in the filter @@ -60,7 +88,7 @@ export class Filter { * @return stringified filter */ public serialize(): string { - return JSON.stringify({ platforms: this.platforms.selection }); + return JSON.stringify({ platforms: this.platforms.selection, dataSources: this.dataSources.selection }); } /** @@ -78,6 +106,29 @@ export class Filter { return true; }; + let isDataSourcesMap = function (obj: any): boolean { + // Check if obj is an instance of Map + if (!(obj instanceof Map)) { + return false; + } + + // Iterate over the entries of the Map + for (const [key, value] of obj.entries()) { + // Check if key is a string and value is an object with 'name' and 'external_references' properties + if (typeof key !== 'string' || + typeof value !== 'object' || + value === null || + !('name' in value) || + !('external_references' in value)) { + return false; + } + } + + return true; + } + + + // Deserialize platforms if (rep.platforms) { if (isStringArray(rep.platforms)) { let backwards_compatibility_mappings = { @@ -101,5 +152,19 @@ export class Filter { this.platforms.selection = Array.from(selection); } else console.error('TypeError: filter platforms field is not a string[]'); } + + // Deserialize data sources + + if(rep.dataSources) { + if (isDataSourcesMap(rep.dataSources)) { + this.dataSources.selection = Array.from(rep.dataSources.keys()); + // show debug message + console.log('Data Sources:', this.dataSources.selection); + // assert that selections is an array + if (!Array.isArray(this.dataSources.selection)) { + console.error('TypeError: filter dataSources selection field is not a string[]'); + } + } else console.error('TypeError: filter dataSources field is not a Map'); + } } } diff --git a/nav-app/src/app/classes/view-model.ts b/nav-app/src/app/classes/view-model.ts index b42f3bf32..91f6e3770 100644 --- a/nav-app/src/app/classes/view-model.ts +++ b/nav-app/src/app/classes/view-model.ts @@ -146,10 +146,12 @@ export class ViewModel { this.dataService.onDataLoad(this.domainVersionID, function () { self.initTechniqueVMs(); self.filters.initPlatformOptions(self.dataService.getDomain(self.domainVersionID)); + self.filters.initDataSourcesOptions(self.dataService.getDomain(self.domainVersionID)); }); } else { this.initTechniqueVMs(); this.filters.initPlatformOptions(domain); + this.filters.initDataSourcesOptions(domain); } this.loaded = true; } @@ -817,6 +819,9 @@ export class ViewModel { return techniques.filter((technique: Technique) => { let techniqueVM = this.getTechniqueVM(technique, tactic); // filter by enabled + let in_platform = false; + let in_ds = false; + if (this.hideDisabled && !this.isSubtechniqueEnabled(technique, techniqueVM, tactic)) { techniqueVM.setIsVisible(false); technique.subtechniques.forEach((subtechnique) => { @@ -842,15 +847,33 @@ export class ViewModel { let subtechniqueVM = this.getTechniqueVM(subtechnique, tactic); subtechniqueVM.setIsVisible(true); }); - return true; //platform match + in_platform = true; + } + } + + let ds_mid = technique.datasources.split(',').map((ds) => ds.split(':')[0]); + let datasources = new Set(ds_mid); + + if (ds_mid.length==1 && ds_mid[0]=='') return in_platform; + for (let ds of this.filters.dataSources.selection) { + if (datasources.has(ds)) { + techniqueVM.setIsVisible(true); + technique.subtechniques.forEach((subtechnique) => { + let subtechniqueVM = this.getTechniqueVM(subtechnique, tactic); + subtechniqueVM.setIsVisible(true); + }); + in_ds = true; } } + techniqueVM.setIsVisible(false); technique.subtechniques.forEach((subtechnique) => { let subtechniqueVM = this.getTechniqueVM(subtechnique, tactic); subtechniqueVM.setIsVisible(false); }); - return false; // no platform match + + return in_platform && in_ds; + }); } diff --git a/nav-app/src/app/datatable/data-table.component.html b/nav-app/src/app/datatable/data-table.component.html index e264caab8..f0bac4d11 100755 --- a/nav-app/src/app/datatable/data-table.component.html +++ b/nav-app/src/app/datatable/data-table.component.html @@ -1,3 +1,4 @@ + +
+ Platforms +
+
+ + +
+
+
Data does not include Platforms
+
+ + +
+ Data Sources +
+
+ (click)="viewModel.filters.toggleInFilter('dataSources', filterOption)" + [checked]="viewModel.filters.inFilter('dataSources', filterOption)" />
-
Data does not include {{ filter }}
+
Data does not include Data Sources
+
+ -->
@@ -817,11 +837,11 @@ + -->
keyboard_arrow_up @@ -850,4 +870,4 @@
-
+
\ No newline at end of file diff --git a/nav-app/src/app/datatable/data-table.component.scss b/nav-app/src/app/datatable/data-table.component.scss index 9c6b56db2..96c1207bc 100755 --- a/nav-app/src/app/datatable/data-table.component.scss +++ b/nav-app/src/app/datatable/data-table.component.scss @@ -296,11 +296,63 @@ $cellSize: 15px; } } +// .filters { +// padding: 4px; + +// .filter { +// text-align: left; + +// &:not(:first-child) { +// margin-top: 4px; +// } + +// .filter-option { +// &:hover { +// @include adaptive-color('background', color(cell-highlight-color), color(cell-highlight-dark-color)); +// } +// } +// } +// } .filters { - padding: 4px; + display: block; + overflow-y: auto; // Prevents additional space when content is too large + max-height: 60vh; + //border: 1px solid color(panel-dark); + + .filter-platform { + text-align: left; + flex-direction: column; /* Ensures elements are stacked vertically */ + // max-height: 20em; /* Set a fixed height */ + // overflow-y: scroll; /* Add scrollbar for vertical overflow */ + // max-height: 30vh; + // border: 1px solid color(panel-dark); + + overflow-y: scroll; // Prevents additional space when content is too large + // max-height: 30vh; + border: 1px solid color(panel-dark); + + &:not(:first-child) { + margin-top: 4px; + } - .filter { + .filter-option { + &:hover { + @include adaptive-color('background', color(cell-highlight-color), color(cell-highlight-dark-color)); + } + } + } + + .filter-datasource { text-align: left; + flex-direction: column; /* Ensures elements are stacked vertically */ + // max-height: 20em; /* Set a fixed height */ + //overflow-y: scroll; /* Add scrollbar for vertical overflow */ + // max-height: 30vh; + // border: 1px solid color(panel-dark); + + overflow-y: scroll; // Prevents additional space when content is too large + //max-height: 30vh; + border: 1px solid color(panel-dark); &:not(:first-child) { margin-top: 4px; @@ -314,6 +366,7 @@ $cellSize: 15px; } } + .warning { @include adaptive-color('color', #b30000, #ffab0f); } @@ -557,4 +610,4 @@ $cellSize: 15px; width: 250px !important; overflow-y: auto; max-height: 50vh; -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..a9cb9e746 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "attack-navigator", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}