diff --git a/CHANGELOG.md b/CHANGELOG.md index 98e4880b9..15d409fa3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/nav-app/src/app/tabs/tabs.component.spec.ts b/nav-app/src/app/tabs/tabs.component.spec.ts index 7aa7d4d48..34855fd77 100755 --- a/nav-app/src/app/tabs/tabs.component.spec.ts +++ b/nav-app/src/app/tabs/tabs.component.spec.ts @@ -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', () => { diff --git a/nav-app/src/app/tabs/tabs.component.ts b/nav-app/src/app/tabs/tabs.component.ts index 4180b9b9d..38a9e95ea 100755 --- a/nav-app/src/app/tabs/tabs.component.ts +++ b/nav-app/src/app/tabs/tabs.component.ts @@ -728,59 +728,53 @@ export class TabsComponent implements AfterViewInit { public async readJSONFile(file: File): Promise { 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); + }); } /** @@ -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);