Skip to content

Commit 69fbb4a

Browse files
authored
Add support for new detections schema (#702)
* feat: update search to support new detections schema * refactor: address sonarqube issues * test: fix stix bundle parse test
1 parent a61ce80 commit 69fbb4a

File tree

9 files changed

+161
-83
lines changed

9 files changed

+161
-83
lines changed

nav-app/src/app/classes/domain.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
11
import { ServiceAuth } from '../services/data.service';
2-
import { Campaign } from './stix/campaign';
3-
import { DataComponent } from './stix/data-component';
4-
import { Group } from './stix/group';
5-
import { Matrix } from './stix/matrix';
6-
import { Mitigation } from './stix/mitigation';
7-
import { Note } from './stix/note';
8-
import { Software } from './stix/software';
9-
import { Tactic } from './stix/tactic';
10-
import { Technique } from './stix/technique';
112
import { Version } from './version';
12-
import { Asset } from './stix/asset';
3+
import { Asset, Campaign, DataComponent, DetectionStrategy, Group, Matrix, Mitigation, Note, Software, Tactic, Technique } from './stix';
134

145
export class Domain {
156
public readonly id: string; // domain ID
@@ -43,8 +34,10 @@ export class Domain {
4334
public assets: Asset[] = [];
4435
public dataComponents: DataComponent[] = [];
4536
public dataSources = new Map<string, { name: string; external_references: any[] }>(); // Map data source ID to name and urls to be used by data components
37+
public dataComponentsToTechniques = new Map<string, Technique[]>(); // Map data component ID to list of related Techniques
4638
public groups: Group[] = [];
4739
public mitigations: Mitigation[] = [];
40+
public detectionStrategies: DetectionStrategy[] = [];
4841
public notes: Note[] = [];
4942
public relationships: any = {
5043
// subtechnique subtechnique-of technique
@@ -53,6 +46,9 @@ export class Domain {
5346
// data component related to technique
5447
// ID of data component to [] of technique IDs
5548
component_rel: new Map<string, string[]>(),
49+
// detection strategy related to technique
50+
// ID of detection strategy to [] of technique IDs
51+
detection_strategies_detect: new Map<string, string[]>(),
5652
// group uses technique
5753
// ID of group to [] of technique IDs
5854
group_uses: new Map<string, string[]>(),
@@ -76,6 +72,10 @@ export class Domain {
7672
targeted_assets: new Map<string, string[]>(),
7773
};
7874

75+
public get supportsLegacyDataSources(): boolean {
76+
return !!this.dataSources.size && !!this.relationships.component_rel.size;
77+
}
78+
7979
constructor(domain_identifier: string, name: string, version: Version, urls?: string[]) {
8080
this.id = `${domain_identifier}-${version.number}`;
8181
this.domain_identifier = domain_identifier;
@@ -96,4 +96,8 @@ export class Domain {
9696
callback();
9797
}
9898
}
99+
100+
public getTechniqueById(stixId: string): Technique {
101+
return this.techniques.find(t => t.id === stixId) ?? null;
102+
}
99103
}

nav-app/src/app/classes/stix/data-component.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,22 @@ export class DataComponent extends StixObject {
1717
* @returns {Technique[]} list of techniques used by the data component
1818
*/
1919
public techniques(domainVersionID): Technique[] {
20-
const techniques = [];
20+
let techniques = [];
2121
const domain = this.dataService.getDomain(domainVersionID);
2222

23-
let relationships = domain.relationships.component_rel;
24-
if (relationships.has(this.id)) {
25-
relationships.get(this.id).forEach((targetID) => {
26-
const technique = domain.techniques.find((t) => t.id === targetID);
27-
if (technique) techniques.push(technique);
28-
});
23+
if (domain.supportsLegacyDataSources) {
24+
// backwards compatibility with data sources
25+
let relationships = domain.relationships.component_rel;
26+
if (relationships.has(this.id)) {
27+
relationships.get(this.id).forEach((targetID) => {
28+
const technique = domain.techniques.find((t) => t.id === targetID);
29+
if (technique) techniques.push(technique);
30+
});
31+
}
32+
} else {
33+
techniques = domain.dataComponentsToTechniques.get(this.id) ?? [];
2934
}
35+
3036
return techniques;
3137
}
3238
/**
@@ -39,7 +45,7 @@ export class DataComponent extends StixObject {
3945
if (dataSources.has(this.dataSource)) {
4046
const source = dataSources.get(this.dataSource);
4147
let url = '';
42-
if (source.external_references && source.external_references[0] && source.external_references[0].url)
48+
if (source.external_references?.[0]?.url)
4349
url = source.external_references[0].url;
4450
return { name: source.name, url: url };
4551
} else return { name: '', url: '' };
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { DataService } from 'src/app/services/data.service';
2+
import { StixObject } from './stix-object';
3+
4+
export class DetectionStrategy extends StixObject {
5+
public analyticRefs: string[] = [];
6+
7+
/**
8+
* Creates an instance of DetectionStrategy.
9+
* @param {any} stixSDO for the detection strategy
10+
* @param {Technique[]} subtechniques occuring under the technique
11+
*/
12+
constructor(stixSDO: any, dataService: DataService) {
13+
super(stixSDO, dataService);
14+
this.analyticRefs = stixSDO.x_mitre_analytic_refs ?? [];
15+
}
16+
17+
/**
18+
* Get techniques detected by this detection strategy
19+
* @returns {string[]} technique IDs detected by this detection strategy
20+
*/
21+
public detects(domainVersionID): string[] {
22+
let rels = this.dataService.getDomain(domainVersionID).relationships.detection_strategies_detect;
23+
if (rels.has(this.id)) {
24+
return rels.get(this.id);
25+
} else {
26+
return [];
27+
}
28+
}
29+
30+
/**
31+
* Get all techniques related to the detection strategy
32+
*/
33+
public relatedTechniques(domainVersionID): string[] {
34+
return this.detects(domainVersionID);
35+
}
36+
}

nav-app/src/app/classes/stix/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export { Software } from './software';
99
export { Tactic } from './tactic';
1010
export { Technique } from './technique';
1111
export { Asset } from './asset';
12+
export { DetectionStrategy } from './detection-strategy';

nav-app/src/app/classes/stix/stix-object.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export abstract class StixObject {
1717
// Properties
1818
this.id = stixSDO.id;
1919
this.name = stixSDO.name;
20-
this.description = stixSDO.description;
20+
this.description = stixSDO?.description ?? "";
2121
this.created = stixSDO.created;
2222
this.modified = stixSDO.modified;
2323
this.revoked = stixSDO.revoked ? stixSDO.revoked : false;

nav-app/src/app/search-and-multiselect/search-and-multiselect.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ <h4>{{ stixType.label | titlecase }} ({{ stixType.objects.length }})</h4>
109109
<mat-expansion-panel [expanded]="expandedPanels[4]" (click)="userClickedExpand = true">
110110
<mat-expansion-panel-header>
111111
<mat-panel-title>
112-
<h4>Data Sources ({{ stixDataComponentLabels.length }})</h4>
112+
<h4>Data Components ({{ stixDataComponentLabels.length }})</h4>
113113
</mat-panel-title>
114114
<mat-panel-description></mat-panel-description>
115115
</mat-expansion-panel-header>

nav-app/src/app/search-and-multiselect/search-and-multiselect.component.ts

Lines changed: 45 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Component, OnInit, Input, ViewEncapsulation } from '@angular/core';
2-
import { StixObject, Group, Mitigation, Software, Technique, Campaign, Asset } from '../classes/stix';
2+
import { StixObject, Group, Mitigation, Software, Technique, Campaign, Asset, DetectionStrategy } from '../classes/stix';
33
import { ViewModelsService } from '../services/viewmodels.service';
44
import { DataService } from '../services/data.service';
55
import { ViewModel } from '../classes';
@@ -13,6 +13,7 @@ import { ViewModel } from '../classes';
1313
export class SearchAndMultiselectComponent implements OnInit {
1414
@Input() viewModel: ViewModel;
1515

16+
public domain;
1617
public stixTypes: any[] = [];
1718
public techniqueResults: Technique[] = [];
1819
// Data Components is a map mainly because it is a collection of labels that map to
@@ -25,15 +26,15 @@ export class SearchAndMultiselectComponent implements OnInit {
2526
0: true, // techniques panel
2627
1: false, // groups panel
2728
2: false, // software panel
28-
3: false, // campaign panel
29-
4: false, // mitigations panel
30-
5: false, // data components panel
31-
6: false, // assets panel
29+
3: false, // mitigations panel
30+
4: false, // campaigns panel
31+
5: false, // assets panel
32+
6: false, // data sources OR detection strategies panel
3233
};
3334

3435
public fields = [
3536
{
36-
label: 'name',
37+
label: 'Name',
3738
field: 'name',
3839
enabled: true,
3940
},
@@ -43,15 +44,10 @@ export class SearchAndMultiselectComponent implements OnInit {
4344
enabled: true,
4445
},
4546
{
46-
label: 'description',
47+
label: 'Description',
4748
field: 'description',
4849
enabled: true,
4950
},
50-
{
51-
label: 'data sources',
52-
field: 'datasources',
53-
enabled: true,
54-
},
5551
];
5652

5753
private debounceFunction;
@@ -92,6 +88,15 @@ export class SearchAndMultiselectComponent implements OnInit {
9288
}
9389

9490
ngOnInit() {
91+
this.domain = this.dataService.getDomain(this.viewModel.domainVersionID);
92+
// backwards compatibility for old data sources
93+
if (this.domain.supportsLegacyDataSources) {
94+
this.fields.push({
95+
label: 'Data Sources',
96+
field: 'datasources',
97+
enabled: true,
98+
});
99+
}
95100
this.getResults();
96101
}
97102

@@ -202,8 +207,8 @@ export class SearchAndMultiselectComponent implements OnInit {
202207
* Retrieve master list of techniques and sub-techniques
203208
*/
204209
public getTechniques(): void {
205-
let allTechniques = this.dataService.getDomain(this.viewModel.domainVersionID).techniques;
206-
for (let technique of allTechniques) {
210+
let allTechniques = this.domain.techniques;
211+
for (let technique of this.domain.techniques) {
207212
allTechniques = allTechniques.concat(technique.subtechniques);
208213
}
209214
this.techniqueResults = this.filterAndSort(allTechniques, this._query, true);
@@ -213,40 +218,46 @@ export class SearchAndMultiselectComponent implements OnInit {
213218
* Retrieve master list of STIX objects
214219
*/
215220
public getStixData(): void {
216-
let domain = this.dataService.getDomain(this.viewModel.domainVersionID);
217-
218221
this.stixTypes = [
219222
{
220223
label: 'threat groups',
221-
objects: this.filterAndSort(domain.groups, this._query),
224+
objects: this.filterAndSort(this.domain.groups, this._query),
222225
},
223226
{
224227
label: 'software',
225-
objects: this.filterAndSort(domain.software, this._query),
228+
objects: this.filterAndSort(this.domain.software, this._query),
226229
},
227230
{
228231
label: 'mitigations',
229-
objects: this.filterAndSort(domain.mitigations, this._query),
232+
objects: this.filterAndSort(this.domain.mitigations, this._query),
230233
},
231234
{
232235
label: 'campaigns',
233-
objects: this.filterAndSort(domain.campaigns, this._query),
236+
objects: this.filterAndSort(this.domain.campaigns, this._query),
234237
},
235238
{
236239
label: 'assets',
237-
objects: this.filterAndSort(domain.assets, this._query),
240+
objects: this.filterAndSort(this.domain.assets, this._query),
238241
},
239242
];
240243

241-
domain.dataComponents.forEach((c) => {
242-
const source = c.source(this.viewModel.domainVersionID);
243-
const label = `${source.name}: ${c.name}`;
244+
if (this.domain.detectionStrategies.length) {
245+
this.stixTypes.push({
246+
label: 'detection strategies',
247+
objects: this.filterAndSort(this.domain.detectionStrategies, this._query),
248+
})
249+
}
250+
251+
const legacyFormat = this.domain.supportsLegacyDataSources;
252+
for (let dc of this.domain.dataComponents) {
253+
const source = legacyFormat ? dc.source(this.viewModel.domainVersionID) : dc;
254+
const label = legacyFormat ? `${source.name}: ${dc.name}` : source.name;
244255
const obj = {
245-
objects: c.techniques(this.viewModel.domainVersionID),
256+
objects: dc.techniques(this.viewModel.domainVersionID),
246257
url: source.url,
247-
};
258+
}
248259
this.stixDataComponents.set(label, obj);
249-
});
260+
}
250261
this.stixDataComponentLabels = this.filterAndSortLabels(Array.from(this.stixDataComponents.keys()), this._query);
251262
}
252263

@@ -323,22 +334,15 @@ export class SearchAndMultiselectComponent implements OnInit {
323334

324335
public getRelated(stixObject: StixObject): Technique[] {
325336
// master list of all techniques and sub-techniques
326-
let techniques = this.dataService.getDomain(this.viewModel.domainVersionID).techniques;
327-
let allTechniques = techniques.concat(this.dataService.getDomain(this.viewModel.domainVersionID).subtechniques);
337+
let techniques = this.domain.techniques;
338+
let allTechniques = techniques.concat(this.domain.subtechniques);
328339
let domainVersionID = this.viewModel.domainVersionID;
329340

330-
if (stixObject instanceof Group) {
331-
return allTechniques.filter((technique: Technique) => (stixObject as Group).relatedTechniques(domainVersionID).includes(technique.id));
332-
} else if (stixObject instanceof Software) {
333-
return allTechniques.filter((technique: Technique) => (stixObject as Software).relatedTechniques(domainVersionID).includes(technique.id));
334-
} else if (stixObject instanceof Mitigation) {
335-
return allTechniques.filter((technique: Technique) =>
336-
(stixObject as Mitigation).relatedTechniques(domainVersionID).includes(technique.id)
337-
);
338-
} else if (stixObject instanceof Campaign) {
339-
return allTechniques.filter((technique: Technique) => (stixObject as Campaign).relatedTechniques(domainVersionID).includes(technique.id));
340-
} else if (stixObject instanceof Asset) {
341-
return allTechniques.filter((technique: Technique) => (stixObject as Asset).relatedTechniques(domainVersionID).includes(technique.id));
341+
const types = [Group, Software, Mitigation, Campaign, Asset, DetectionStrategy];
342+
const matchedType = types.find(StixType => stixObject instanceof StixType);
343+
if (matchedType) {
344+
return allTechniques.filter((technique: Technique) => (stixObject as any).relatedTechniques(domainVersionID).includes(technique.id));
342345
}
346+
return [];
343347
}
344348
}

nav-app/src/app/services/data.service.spec.ts

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ describe('DataService', () => {
253253
let relationships = domain.relationships;
254254
expect(relationships['campaign_uses'].size).toEqual(1);
255255
expect(relationships['campaigns_attributed_to'].size).toEqual(1);
256-
expect(relationships['component_rel'].size).toEqual(2);
256+
expect(relationships['component_rel'].size).toEqual(1);
257257
expect(relationships['group_uses'].size).toEqual(1);
258258
expect(relationships['mitigates'].size).toEqual(1);
259259
expect(relationships['revoked_by'].size).toEqual(1);
@@ -421,24 +421,6 @@ describe('DataService', () => {
421421
expect(campaign_test.relatedTechniques('enterprise-attack-13')).toEqual([]);
422422
});
423423

424-
it('should test data components', () => {
425-
let t1 = new Technique(MockData.T0000, [], mockService);
426-
mockService.domains[0].techniques = [t1];
427-
let data_component_test = new DataComponent(MockData.DC0000, mockService);
428-
mockService.domains[0].dataSources.set(MockData.DC0000.id, {
429-
name: MockData.stixSDO.name,
430-
external_references: MockData.DC0000.external_references,
431-
});
432-
expect(data_component_test.source('enterprise-attack-13')).toEqual({ name: '', url: '' });
433-
mockService.domains[0].dataSources.set(MockData.DS0000.id, {
434-
name: MockData.stixSDO.name,
435-
external_references: MockData.DC0001.external_references,
436-
});
437-
expect(data_component_test.source('enterprise-attack-13')).toEqual({ name: 'Name', url: 'test-url.com' });
438-
mockService.domains[0].relationships['component_rel'].set('data-component-0', ['attack-pattern-0']);
439-
expect(data_component_test.techniques('enterprise-attack-13')[0].id).toEqual('attack-pattern-0');
440-
});
441-
442424
it('should test group', () => {
443425
mockService.domains[0].relationships['group_uses'].set('intrusion-set-0', ['attack-pattern-0']);
444426
mockService.domains[0].relationships['campaign_uses'].set('intrusion-set-0', ['attack-pattern-0']);

0 commit comments

Comments
 (0)