Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Addition: Filter by Data Sources #633

Draft
wants to merge 11 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions nav-app/angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,5 +142,8 @@
"@schematics/angular:directive": {
"prefix": "app"
}
},
"cli": {
"analytics": false
}
}
71 changes: 68 additions & 3 deletions nav-app/src/app/classes/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,21 @@ export class Filter {
selection: string[];
};

public dataSources : {
options: string[];
selection: string[];
}

constructor() {
this.platforms = {
selection: [],
options: [],
};

this.dataSources = {
selection: [],
options: [],
};
}

/**
Expand All @@ -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<string, { name: string; external_references: any[] }>
// 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
Expand All @@ -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
Expand All @@ -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 });
}

/**
Expand All @@ -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 = {
Expand All @@ -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<string, { name: string; external_references: any[] }>');
}
}
}
27 changes: 25 additions & 2 deletions nav-app/src/app/classes/view-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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) => {
Expand All @@ -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;

});
}

Expand Down
52 changes: 36 additions & 16 deletions nav-app/src/app/datatable/data-table.component.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

<!--
oooooooo8 ooooooo oooo oooo ooooooooooo oooooooooo ooooooo ooooo oooooooo8
o888 88 o888 888o 8888o 88 88 888 88 888 888 o888 888o 888 888
Expand Down Expand Up @@ -268,24 +269,43 @@
<span class="material-icons">filter_list</span>
</div>
<div class="dropdown-container filters" *ngIf="currentDropdown === 'filters'" #dropdown [class.left]="checkalign(dropdown)">
<div class="filter" *ngFor="let filter of ['platforms']">
<b class="filter-label">{{ filter }}</b>
<div *ngIf="viewModel.filters[filter].options.length !== 0">
<div class="filter-option" *ngFor="let filterOption of viewModel.filters[filter].options">
<!-- Platforms Filter -->
<div class="filter-platform">
<b class="filter-label">Platforms</b>
<div *ngIf="viewModel.filters.platforms.options.length !== 0">
<div class="filter-option" *ngFor="let filterOption of viewModel.filters.platforms.options">
<input
[id]="filterOption"
class="checkbox-custom"
type="checkbox"
(click)="viewModel.filters.toggleInFilter('platforms', filterOption)"
[checked]="viewModel.filters.inFilter('platforms', filterOption)" />
<label [for]="filterOption" class="checkbox-custom-label noselect">{{ filterOption }}</label>
</div>
</div>
<div *ngIf="viewModel.filters.platforms.options.length == 0">Data does not include Platforms</div>
</div>

<!-- Data Sources Filter -->
<div class="filter-datasource">
<b class="filter-label">Data Sources</b>
<div *ngIf="viewModel.filters.dataSources.options.length !== 0">
<div class="filter-option" *ngFor="let filterOption of viewModel.filters.dataSources.options">
<input
[id]="filterOption"
class="checkbox-custom"
type="checkbox"
(click)="viewModel.filters.toggleInFilter(filter, filterOption)"
[checked]="viewModel.filters.inFilter(filter, filterOption)" />
(click)="viewModel.filters.toggleInFilter('dataSources', filterOption)"
[checked]="viewModel.filters.inFilter('dataSources', filterOption)" />
<label [for]="filterOption" class="checkbox-custom-label noselect">{{ filterOption }}</label>
</div>
</div>
<div *ngIf="viewModel.filters[filter].options.length == 0">Data does not include {{ filter }}</div>
<div *ngIf="viewModel.filters.dataSources.options.length == 0">Data does not include Data Sources</div>
</div>
</div>
</div>


<!-- sorting -->
<div *ngIf="configService.getFeature('sorting')" class="control-row-item">
<div
Expand Down Expand Up @@ -772,11 +792,11 @@

<!--
oooo oooo o ooooooooooo oooooooooo ooooo ooooo oooo
8888o 888 888 88 888 88 888 888 888 888 88
88 888o8 88 8 88 888 888oooo88 888 888
88 888 88 8oooo88 888 888 88o 888 88 888
8888o 888 888 88 888 88 888 888 888 888 88
88 888o8 88 8 88 888 888oooo88 888 888
88 888 88 8oooo88 888 888 88o 888 88 888
o88o 8 o88o o88o o888o o888o o888o 88o8 o888o o88o o888o
-->
-->
<mat-drawer-container class="matrices-content" autosize>
<mat-drawer-content>
<div class="matrices" #scrollRef>
Expand Down Expand Up @@ -817,11 +837,11 @@

<!--
ooooo ooooooooooo ooooooo8 ooooooooooo oooo oooo ooooooooo
888 888 88 o888 88 888 88 8888o 88 888 88o
888 888ooo8 888 oooo 888ooo8 88 888o88 888 888
888 o 888 oo 888o 88 888 oo 88 8888 888 888
888 888 88 o888 88 888 88 8888o 88 888 88o
888 888ooo8 888 oooo 888ooo8 88 888o88 888 888
888 o 888 oo 888o 88 888 oo 88 8888 888 888
o888ooooo88 o888ooo8888 888ooo888 o888ooo8888 o88o 88 o888ooo88
-->
-->

<div class="legendBar" (click)="showingLegend = !showingLegend" *ngIf="!showingLegend && configService.getFeature('legend')">
<span class="material-icons">keyboard_arrow_up</span>
Expand Down Expand Up @@ -850,4 +870,4 @@
<button style="margin-left: 75px; margin-top: 10px; margin-bottom: 10px" class="button" (click)="viewModel.addLegendItem()">Add Item</button>
<button style="margin-top: 10px; margin-bottom: 10px" class="button" (click)="viewModel.clearLegend()">Clear</button>
</div>
</div>
</div>
59 changes: 56 additions & 3 deletions nav-app/src/app/datatable/data-table.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -314,6 +366,7 @@ $cellSize: 15px;
}
}


.warning {
@include adaptive-color('color', #b30000, #ffab0f);
}
Expand Down Expand Up @@ -557,4 +610,4 @@ $cellSize: 15px;
width: 250px !important;
overflow-y: auto;
max-height: 50vh;
}
}
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.