Skip to content

Commit

Permalink
624 bug loading multi-layer JSON via embedded links (#634)
Browse files Browse the repository at this point in the history
* fix loading layer(s) from URL

* update changelog

* minor change

* fix loading layer(s) from file

* fix jasmine tests
  • Loading branch information
clemiller committed Apr 30, 2024
1 parent 4f4c514 commit 510dc71
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 74 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
- Added support for loading content from a TAXII 2.1 server. See issue [#277](https://github.com/mitre-attack/attack-navigator/issues/277). For more information on how to load content from TAXII 2.1 see _Loading content from a TAXII server_ in the [README](README.md).
- Improved error handling when there is an issue loading the configuration file. See issue [#398](https://github.com/mitre-attack/attack-navigator/issues/398).

## Fixes
- Fixed an issue where loading a multi-layer JSON file through embedded links would throw an error and prevent the layers from loading. See issue [#624](https://github.com/mitre-attack/attack-navigator/issues/624).

# 4.9.5 - 23 April 2024

Adds support for ATT&CK v15.0.
Expand Down
15 changes: 1 addition & 14 deletions nav-app/src/app/tabs/tabs.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,23 +474,10 @@ describe('TabsComponent', () => {
component.openUploadPrompt();
expect(logSpy).toHaveBeenCalled();
let blob = new Blob([JSON.stringify(MockLayers.layerFile2)], { type: 'text/json' });
let file = new File([blob], 'layer-2.json');
let file = new File([blob], 'layer-1.json');
component.readJSONFile(file).then(() => {
expect(component.layerTabs.length).toEqual(1);
});
let layer = MockLayers.layerFile2;
layer.viewMode = 2;
blob = new Blob([JSON.stringify(layer)], { type: 'text/json' });
file = new File([blob], 'layer-2.json');
component.readJSONFile(file).then(() => {
expect(component.layerTabs.length).toEqual(2);
});
layer.viewMode = 0;
blob = new Blob([JSON.stringify(layer)], { type: 'text/json' });
file = new File([blob], 'layer-2.json');
component.readJSONFile(file).then(() => {
expect(component.layerTabs.length).toEqual(3);
});
}));

it('should retrieve the minimum supported version', () => {
Expand Down
126 changes: 66 additions & 60 deletions nav-app/src/app/tabs/tabs.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -728,59 +728,53 @@ export class TabsComponent implements AfterViewInit {
public async readJSONFile(file: File): Promise<void> {
return new Promise((resolve, reject) => {
let reader = new FileReader();
let viewModel: ViewModel;
reader.onload = (e) => {
let result = String(reader.result);
try {
let objList = typeof result == 'string' ? JSON.parse(result) : result;
if ('length' in objList) {
for (let obj of objList) {
this.loadObjAsLayer(this, obj);
let self = this;

reader.onload = async (e) => {
let loadObjAsLayer = async function(layerObj) {
let viewModel = self.viewModelsService.newViewModel('loading layer...', undefined);
try {
let layerVersionStr = viewModel.deserializeDomainVersionID(layerObj);
await self.versionMismatchWarning(layerVersionStr);
self.versionMismatchWarning(layerVersionStr);
if (!self.dataService.getDomain(viewModel.domainVersionID)) {
throw new Error(`Error: '${viewModel.domain}' (v${viewModel.version}) is an invalid domain.`);
}
} else {
let obj = typeof result == 'string' ? JSON.parse(result) : result;
this.loadObjAsLayer(this, obj);

let isCustom = 'customDataURL' in layerObj;
if (!isCustom) {
await self.upgradeLayer(viewModel, layerObj, true);
console.debug(`loaded layer "${viewModel.name}"`);
} else {
// load as custom data
viewModel.deserialize(layerObj);
let url = layerObj['customDataURL'];
self.newLayerFromURL(
{url: url, version: viewModel.version, identifier: viewModel.domain},
layerObj
);
}
} catch (err) {
console.error(err);
alert(`ERROR parsing layer, check the javascript console for more information.`);
self.viewModelsService.destroyViewModel(viewModel);
resolve(null); // continue
}
} catch (err) {
viewModel = this.viewModelsService.newViewModel('loading layer...', undefined);
console.error('ERROR: Either the file is not JSON formatted, or the file structure is invalid.', err);
alert('ERROR: Either the file is not JSON formatted, or the file structure is invalid.');
this.viewModelsService.destroyViewModel(viewModel);
}
};
reader.readAsText(file);
});
}

public loadObjAsLayer(self, obj): void {
let viewModel: ViewModel;
viewModel = self.viewModelsService.newViewModel('loading layer...', undefined);
let layerVersionStr = viewModel.deserializeDomainVersionID(obj);
self.versionMismatchWarning(layerVersionStr)
.then((res) => {
let isCustom = 'customDataURL' in obj;
if (!isCustom) {
if (!self.dataService.getDomain(viewModel.domainVersionID)) {
throw new Error(`Error: '${viewModel.domain}' (v${viewModel.version}) is an invalid domain.`);
let result = String(reader.result);
let layerFile = typeof result == 'string' ? JSON.parse(result) : result;
if (layerFile?.length) {
console.debug('loading file with multiple layers');
for (let layer of layerFile) {
await loadObjAsLayer(layer);
}
self.upgradeLayer(viewModel, obj, true);
} else {
// load as custom data
viewModel.deserialize(obj);
self.openTab('new layer', viewModel, true, true, true, true);
self.newLayerFromURL(
{
url: obj['customDataURL'],
version: viewModel.version,
identifier: viewModel.domain,
},
obj
);
await loadObjAsLayer(layerFile);
}
})
.catch((error) => {
console.log(error);
});
};
reader.readAsText(file);
});
}

/**
Expand Down Expand Up @@ -837,22 +831,34 @@ export class TabsComponent implements AfterViewInit {
let self = this;
subscription = self.http.get(loadURL).subscribe({
next: async (res) => {
let viewModel = self.viewModelsService.newViewModel('loading layer...', undefined);
try {
let layerVersionStr = viewModel.deserializeDomainVersionID(res);
await self.versionMismatchWarning(layerVersionStr);
if (!self.dataService.getDomain(viewModel.domainVersionID)) {
throw new Error(`Error: '${viewModel.domain}' (v${viewModel.version}) is an invalid domain.`);
let loadLayerAsync = async function(layerObj) {
let viewModel = self.viewModelsService.newViewModel('loading layer...', undefined);
try {
let layerVersionStr = viewModel.deserializeDomainVersionID(layerObj);
await self.versionMismatchWarning(layerVersionStr);
if (!self.dataService.getDomain(viewModel.domainVersionID)) {
throw new Error(`Error: '${viewModel.domain}' (v${viewModel.version}) is an invalid domain.`);
}
await self.upgradeLayer(viewModel, layerObj, replace, defaultLayers);
console.debug(`loaded layer "${viewModel.name}" from ${loadURL}`);
} catch (err) {
console.error(err);
alert(`ERROR parsing layer from ${loadURL}, check the javascript console for more information.`);
self.viewModelsService.destroyViewModel(viewModel);
resolve(null); // continue
}
await self.upgradeLayer(viewModel, res, replace, defaultLayers);
console.debug('loaded layer from', loadURL);
resolve(null); //continue
} catch (err) {
console.error(err);
this.viewModelsService.destroyViewModel(viewModel);
alert(`ERROR parsing layer from ${loadURL}, check the javascript console for more information.`);
resolve(null); // continue
};

let layerFile = typeof res == 'string' ? JSON.parse(res) : res;
if (layerFile?.length) {
console.debug('loading file with multiple layers');
for (let layer of layerFile) {
await loadLayerAsync(layer);
}
} else {
await loadLayerAsync(layerFile);
}
resolve(null); //continue
},
error: (err) => {
console.error(err);
Expand Down

0 comments on commit 510dc71

Please sign in to comment.