diff --git a/app/api/definitions/openapi.yml b/app/api/definitions/openapi.yml index 6469bea1..b211d0ea 100644 --- a/app/api/definitions/openapi.yml +++ b/app/api/definitions/openapi.yml @@ -120,6 +120,9 @@ paths: /api/matrices/{stixId}/modified/{modified}: $ref: 'paths/matrices-paths.yml#/paths/~1api~1matrices~1{stixId}~1modified~1{modified}' + /api/matrices/{stixId}/modified/{modified}/techniques: + $ref: 'paths/matrices-paths.yml#/paths/~1api~1matrices~1{stixId}~1modified~1{modified}~1techniques' + # Identities /api/identities: $ref: 'paths/identities-paths.yml#/paths/~1api~1identities' diff --git a/app/api/definitions/paths/matrices-paths.yml b/app/api/definitions/paths/matrices-paths.yml index 43c7d6cf..d4d03625 100644 --- a/app/api/definitions/paths/matrices-paths.yml +++ b/app/api/definitions/paths/matrices-paths.yml @@ -90,7 +90,6 @@ paths: type: array items: $ref: '../components/matrices.yml#/components/schemas/matrix' - post: summary: 'Create a matrix' operationId: 'matrix-create' @@ -269,3 +268,30 @@ paths: description: 'The matrix was successfully deleted.' '404': description: 'A matrix with the requested STIX id and modified date was not found.' + + /api/matrices/{stixId}/modified/{modified}/techniques: + get: + summary: 'Retrieves techniques and subtechniques for the supplied matrix' + operationId: 'techniques-get-by-id-and-modified' + description: | + This endpoint retrieves all the techniques and subtechniques found in the supplied matrix. + tags: + - 'Matrices' + parameters: + - name: stixId + in: path + description: 'STIX id of the matrix to retrieve techniques and subtechniques' + required: true + schema: + type: string + - name: modified + in: path + description: 'modified date of the matrix to retrieve' + required: true + schema: + type: string + responses: + '200': + description: 'The techniques and subtechniques of a matrix matching the STIX id and modified date.' + '404': + description: 'A matrix with the requested STIX id and modified date was not found.' diff --git a/app/controllers/matrices-controller.js b/app/controllers/matrices-controller.js index b45a156e..b969fbd2 100644 --- a/app/controllers/matrices-controller.js +++ b/app/controllers/matrices-controller.js @@ -168,3 +168,26 @@ exports.deleteById = function(req, res) { } }); }; + +exports.retrieveTechniquesForMatrix = function(req, res) { + matricesService.retrieveTechniquesForMatrix(req.params.stixId, req.params.modified, function (err, techniquesByTactic) { + if (err) { + if (err.message === matricesService.errors.badlyFormattedParameter) { + logger.warn('Badly formatted stix id: ' + req.params.stixId); + return res.status(400).send('Stix id is badly formatted.'); + } + else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get techniques for matrix. Server error.'); + } + } else { + if (!techniquesByTactic) { + return res.status(404).send('Matrix not found.'); + } + else { + logger.debug(`Success: Retrieved techniques for matrix with id ${ req.params.stixId }`); + return res.status(200).send(techniquesByTactic); + } + } + }); +}; diff --git a/app/routes/matrices-routes.js b/app/routes/matrices-routes.js index 5191820f..c666f09b 100644 --- a/app/routes/matrices-routes.js +++ b/app/routes/matrices-routes.js @@ -49,4 +49,11 @@ router.route('/matrices/:stixId/modified/:modified') matricesController.deleteVersionById ); +router.route('/matrices/:stixId/modified/:modified/techniques') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + matricesController.retrieveTechniquesForMatrix + ); + module.exports = router; diff --git a/app/services/matrices-service.js b/app/services/matrices-service.js index 72ebbb04..b5dd6312 100644 --- a/app/services/matrices-service.js +++ b/app/services/matrices-service.js @@ -1,6 +1,7 @@ 'use strict'; const uuid = require('uuid'); +const util = require('util'); const Matrix = require('../models/matrix-model'); const systemConfigurationService = require('./system-configuration-service'); const identitiesService = require('./identities-service'); @@ -213,6 +214,86 @@ exports.retrieveVersionById = function(stixId, modified, callback) { }); }; +let retrieveTacticById; +let retrieveTechniquesForTactic; +exports.retrieveTechniquesForMatrix = function(stixId, modified, callback) { + // Retrieve the versions of the matrix techniques with the matching stixId and modified date + + // Late binding to avoid circular dependency between modules + if (!retrieveTacticById) { + const tacticsService = require('./tactics-service'); + retrieveTacticById = util.promisify(tacticsService.retrieveById); + } + if (!retrieveTechniquesForTactic) { + const tacticsService = require('./tactics-service'); + retrieveTechniquesForTactic = tacticsService.retrieveTechniquesForTactic; + } + + if (!stixId) { + const error = new Error(errors.missingParameter); + error.parameterName = 'stixId'; + return callback(error); + } + if (!modified) { + const error = new Error(errors.missingParameter); + error.parameterName = 'modified'; + return callback(error); + } + + Matrix.findOne({ 'stix.id': stixId, 'stix.modified': modified }, async function(err, matrix) { + if (err) { + if (err.name === 'CastError') { + const error = new Error(errors.badlyFormattedParameter); + error.parameterName = 'stixId'; + return callback(error); + } + else { + return callback(err); + } + } + else { + if (matrix) { + // get tactics, then query for techniques and sub-techniques + const options = { versions: 'latest', offset: 0, limit: 0 }; + const tacticsTechniques = {}; + for (const tacticId of matrix.stix.tactic_refs) { + const tactics = await retrieveTacticById(tacticId, options); + if (tactics.length) { + const tactic = tactics[0]; + const techniques = await retrieveTechniquesForTactic(tacticId, tactic.stix.modified, options); + // Organize sub-techniques under parent techniques + const parentTechniques = []; + const subtechniques = []; + for (const technique of techniques) { + if (!technique.stix.x_mitre_is_subtechnique) { + parentTechniques.push(technique); + } + else { + subtechniques.push(technique); + } + } + for (const parentTechnique of parentTechniques) { + parentTechnique.subtechniques = []; + for (const subtechnique of subtechniques) { + if (subtechnique.workspace.attack_id.split(".")[0] === parentTechnique.workspace.attack_id) { + parentTechnique.subtechniques.push(subtechnique); + } + } + } + // Add techniques to tactic & store tactic + tactic.techniques = parentTechniques; + tacticsTechniques[tactic.stix.name] = tactic; + } + } + return callback(null, tacticsTechniques); + } + else { + return callback(); + } + } + }); +}; + exports.createIsAsync = true; exports.create = async function(data, options) { // This function handles two use cases: @@ -368,4 +449,3 @@ exports.deleteById = function (stixId, callback) { } }); }; -