From 7363cf8aca0762bd5e4e1c8ea39e62a97a535dab Mon Sep 17 00:00:00 2001 From: Sun Date: Fri, 8 Dec 2023 16:27:36 -0500 Subject: [PATCH 01/26] refactoring service and repo --- app/repository/relationships-repository.js | 159 ++++++++ app/services/relationships-service.js | 417 +-------------------- 2 files changed, 163 insertions(+), 413 deletions(-) create mode 100644 app/repository/relationships-repository.js diff --git a/app/repository/relationships-repository.js b/app/repository/relationships-repository.js new file mode 100644 index 00000000..b9adfbe4 --- /dev/null +++ b/app/repository/relationships-repository.js @@ -0,0 +1,159 @@ +'use strict'; + +const BaseRepository = require('./_base.repository'); +const Relationship = require('../models/relationship-model'); + +class RelationshipsRepository extends BaseRepository { + + async retrieveAll(options) { + // Build the query + const query = {}; + if (!options.includeRevoked) { + query['stix.revoked'] = { $in: [null, false] }; + } + if (!options.includeDeprecated) { + query['stix.x_mitre_deprecated'] = { $in: [null, false] }; + } + if (typeof options.state !== 'undefined') { + if (Array.isArray(options.state)) { + query['workspace.workflow.state'] = { $in: options.state }; + } + else { + query['workspace.workflow.state'] = options.state; + } + } + if (typeof options.sourceRef !== 'undefined') { + query['stix.source_ref'] = options.sourceRef; + } + if (typeof options.targetRef !== 'undefined') { + query['stix.target_ref'] = options.targetRef; + } + if (typeof options.sourceOrTargetRef !== 'undefined') { + query.$or = [{ 'stix.source_ref': options.sourceOrTargetRef }, { 'stix.target_ref': options.sourceOrTargetRef }] + } + if (typeof options.relationshipType !== 'undefined') { + query['stix.relationship_type'] = options.relationshipType; + } + if (typeof options.lastUpdatedBy !== 'undefined') { + query['workspace.workflow.created_by_user_account'] = lastUpdatedByQueryHelper(options.lastUpdatedBy); + } + + // Build the aggregation + const aggregation = []; + if (options.versions === 'latest') { + // - Group the documents by stix.id, sorted by stix.modified + // - Use the first document in each group (according to the value of stix.modified) + aggregation.push({ $sort: { 'stix.id': 1, 'stix.modified': -1 } }); + aggregation.push({ $group: { _id: '$stix.id', document: { $first: '$$ROOT' } } }); + aggregation.push({ $replaceRoot: { newRoot: '$document' } }); + } + + // Add stages to the aggregation to sort (for pagination), apply the query, and add source and target object data + aggregation.push({ $sort: { 'stix.id': 1 } }); + aggregation.push({ $match: query }); + if (options.lookupRefs) { + aggregation.push({ + $lookup: { + from: 'attackObjects', + localField: 'stix.source_ref', + foreignField: 'stix.id', + as: 'source_objects' + } + }); + aggregation.push({ + $lookup: { + from: 'attackObjects', + localField: 'stix.target_ref', + foreignField: 'stix.id', + as: 'target_objects' + } + }); + } + // Retrieve the documents + let results = await Relationship.aggregate(aggregation); + + // Filter out relationships that don't reference the source type + if (options.sourceType) { + results = results.filter(document => { + if (document.source_objects.length === 0) { + return false; + } + else { + document.source_objects.sort((a, b) => b.stix.modified - a.stix.modified); + return objectTypeMap.get(document.source_objects[0].stix.type) === options.sourceType; + } + }); + } + + // Filter out relationships that don't reference the target type + if (options.targetType) { + results = results.filter(document => { + if (document.target_objects.length === 0) { + return false; + } + else { + document.target_objects.sort((a, b) => b.stix.modified - a.stix.modified); + return objectTypeMap.get(document.target_objects[0].stix.type) === options.targetType; + } + }); + } + + const prePaginationTotal = results.length; + + // Apply pagination parameters + if (options.offset || options.limit) { + const start = options.offset || 0; + if (options.limit) { + const end = start + options.limit; + results = results.slice(start, end); + } + else { + results = results.slice(start); + } + } + + // Move latest source and target objects to a non-array property, then remove array of source and target objects + for (const document of results) { + if (Array.isArray(document.source_objects)) { + if (document.source_objects.length === 0) { + document.source_objects = undefined; + } + else { + document.source_object = document.source_objects[0]; + document.source_objects = undefined; + } + } + + if (Array.isArray(document.target_objects)) { + if (document.target_objects.length === 0) { + document.target_objects = undefined; + } + else { + document.target_object = document.target_objects[0]; + document.target_objects = undefined; + } + } + } + + if (options.includeIdentities) { + await identitiesService.addCreatedByAndModifiedByIdentitiesToAll(results); + } + + if (options.includePagination) { + return { + pagination: { + total: prePaginationTotal, + offset: options.offset, + limit: options.limit + }, + data: results + }; + } + else { + return results; + } + }; + +} + +module.exports = new RelationshipsRepository(Relationship); \ No newline at end of file diff --git a/app/services/relationships-service.js b/app/services/relationships-service.js index 20b87b5b..1897199d 100644 --- a/app/services/relationships-service.js +++ b/app/services/relationships-service.js @@ -1,12 +1,6 @@ 'use strict'; -const uuid = require('uuid'); -const Relationship = require('../models/relationship-model'); -const systemConfigurationService = require('./system-configuration-service'); -const identitiesService = require('./identities-service'); -const attackObjectsService = require('./attack-objects-service'); -const config = require('../config/config'); - +const relationshipsRepository = require('../repository/relationships-repository'); const { lastUpdatedByQueryHelper } = require('../lib/request-parameter-helper'); const errors = { @@ -31,412 +25,9 @@ objectTypeMap.set('x-mitre-tactic', 'tactic'); objectTypeMap.set('x-mitre-matrix', 'matrix'); objectTypeMap.set('x-mitre-data-component', 'data-component'); -exports.retrieveAll = async function(options) { - // Build the query - const query = {}; - if (!options.includeRevoked) { - query['stix.revoked'] = { $in: [null, false] }; - } - if (!options.includeDeprecated) { - query['stix.x_mitre_deprecated'] = { $in: [null, false] }; - } - if (typeof options.state !== 'undefined') { - if (Array.isArray(options.state)) { - query['workspace.workflow.state'] = { $in: options.state }; - } - else { - query['workspace.workflow.state'] = options.state; - } - } - if (typeof options.sourceRef !== 'undefined') { - query['stix.source_ref'] = options.sourceRef; - } - if (typeof options.targetRef !== 'undefined') { - query['stix.target_ref'] = options.targetRef; - } - if (typeof options.sourceOrTargetRef !== 'undefined') { - query.$or = [{ 'stix.source_ref': options.sourceOrTargetRef }, { 'stix.target_ref': options.sourceOrTargetRef }] - } - if (typeof options.relationshipType !== 'undefined') { - query['stix.relationship_type'] = options.relationshipType; - } - if (typeof options.lastUpdatedBy !== 'undefined') { - query['workspace.workflow.created_by_user_account'] = lastUpdatedByQueryHelper(options.lastUpdatedBy); - } - - // Build the aggregation - const aggregation = []; - if (options.versions === 'latest') { - // - Group the documents by stix.id, sorted by stix.modified - // - Use the first document in each group (according to the value of stix.modified) - aggregation.push({ $sort: { 'stix.id': 1, 'stix.modified': -1 } }); - aggregation.push({ $group: { _id: '$stix.id', document: { $first: '$$ROOT' } } }); - aggregation.push({ $replaceRoot: { newRoot: '$document' } }); - } - - // Add stages to the aggregation to sort (for pagination), apply the query, and add source and target object data - aggregation.push({ $sort: { 'stix.id': 1 } }); - aggregation.push({ $match: query }); - if (options.lookupRefs) { - aggregation.push({ - $lookup: { - from: 'attackObjects', - localField: 'stix.source_ref', - foreignField: 'stix.id', - as: 'source_objects' - } - }); - aggregation.push({ - $lookup: { - from: 'attackObjects', - localField: 'stix.target_ref', - foreignField: 'stix.id', - as: 'target_objects' - } - }); - } - // Retrieve the documents - let results = await Relationship.aggregate(aggregation); - - // Filter out relationships that don't reference the source type - if (options.sourceType) { - results = results.filter(document => { - if (document.source_objects.length === 0) { - return false; - } - else { - document.source_objects.sort((a, b) => b.stix.modified - a.stix.modified); - return objectTypeMap.get(document.source_objects[0].stix.type) === options.sourceType; - } - }); - } - - // Filter out relationships that don't reference the target type - if (options.targetType) { - results = results.filter(document => { - if (document.target_objects.length === 0) { - return false; - } - else { - document.target_objects.sort((a, b) => b.stix.modified - a.stix.modified); - return objectTypeMap.get(document.target_objects[0].stix.type) === options.targetType; - } - }); - } - - const prePaginationTotal = results.length; - - // Apply pagination parameters - if (options.offset || options.limit) { - const start = options.offset || 0; - if (options.limit) { - const end = start + options.limit; - results = results.slice(start, end); - } - else { - results = results.slice(start); - } - } - - // Move latest source and target objects to a non-array property, then remove array of source and target objects - for (const document of results) { - if (Array.isArray(document.source_objects)) { - if (document.source_objects.length === 0) { - document.source_objects = undefined; - } - else { - document.source_object = document.source_objects[0]; - document.source_objects = undefined; - } - } - - if (Array.isArray(document.target_objects)) { - if (document.target_objects.length === 0) { - document.target_objects = undefined; - } - else { - document.target_object = document.target_objects[0]; - document.target_objects = undefined; - } - } - } - - if (options.includeIdentities) { - await identitiesService.addCreatedByAndModifiedByIdentitiesToAll(results); - } - - if (options.includePagination) { - return { - pagination: { - total: prePaginationTotal, - offset: options.offset, - limit: options.limit - }, - data: results - }; - } - else { - return results; - } -}; - -exports.retrieveById = function(stixId, options, callback) { - // versions=all Retrieve all relationships with the stixId - // versions=latest Retrieve the relationship with the latest modified date for this stixId - - if (!stixId) { - const error = new Error(errors.missingParameter); - error.parameterName = 'stixId'; - return callback(error); - } - - if (options.versions === 'all') { - Relationship - .find({'stix.id': stixId}) - .sort('-stix.modified') - .lean() - .exec(function (err, relationships) { - if (err) { - if (err.name === 'CastError') { - const error = new Error(errors.badlyFormattedParameter); - error.parameterName = 'stixId'; - return callback(error); - } - else { - return callback(err); - } - } - else { - identitiesService.addCreatedByAndModifiedByIdentitiesToAll(relationships) - .then(() => callback(null, relationships)); - } - }); - } - else if (options.versions === 'latest') { - Relationship.findOne({ 'stix.id': stixId }) - .sort('-stix.modified') - .lean() - .exec(function(err, relationship) { - if (err) { - if (err.name === 'CastError') { - const error = new Error(errors.badlyFormattedParameter); - error.parameterName = 'stixId'; - return callback(error); - } - else { - return callback(err); - } - } - else { - // Note: document is null if not found - if (relationship) { - identitiesService.addCreatedByAndModifiedByIdentities(relationship) - .then(() => callback(null, [ relationship ])); - } - else { - return callback(null, []); - } - } - }); - } - else { - const error = new Error(errors.invalidQueryStringParameter); - error.parameterName = 'versions'; - return callback(error); - } -}; - -exports.retrieveVersionById = function(stixId, modified, callback) { - // Retrieve the version of the relationship with the matching stixId and modified date - - 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); - } - - Relationship.findOne({ 'stix.id': stixId, 'stix.modified': modified }, function(err, relationship) { - if (err) { - if (err.name === 'CastError') { - const error = new Error(errors.badlyFormattedParameter); - error.parameterName = 'stixId'; - return callback(error); - } - else { - return callback(err); - } - } - else { - // Note: document is null if not found - if (relationship) { - identitiesService.addCreatedByAndModifiedByIdentities(relationship) - .then(() => callback(null, relationship)); - } - else { - return callback(); - } - } - }); -}; - -exports.createIsAsync = true; -exports.create = async function(data, options) { - // This function handles two use cases: - // 1. This is a completely new object. Create a new object and generate the stix.id if not already - // provided. Set both stix.created_by_ref and stix.x_mitre_modified_by_ref to the organization identity. - // 2. This is a new version of an existing object. Create a new object with the specified id. - // Set stix.x_mitre_modified_by_ref to the organization identity. - - // Create the document - const relationship = new Relationship(data); - - options = options || {}; - if (!options.import) { - // Set the ATT&CK Spec Version - relationship.stix.x_mitre_attack_spec_version = relationship.stix.x_mitre_attack_spec_version ?? config.app.attackSpecVersion; - - // Record the user account that created the object - if (options.userAccountId) { - relationship.workspace.workflow.created_by_user_account = options.userAccountId; - } - - // Set the default marking definitions - await attackObjectsService.setDefaultMarkingDefinitions(relationship); - - // Get the organization identity - const organizationIdentityRef = await systemConfigurationService.retrieveOrganizationIdentityRef(); - - // Check for an existing object - let existingObject; - if (relationship.stix.id) { - existingObject = await Relationship.findOne({ 'stix.id': relationship.stix.id }); - } - - if (existingObject) { - // New version of an existing object - // Only set the x_mitre_modified_by_ref property - relationship.stix.x_mitre_modified_by_ref = organizationIdentityRef; - } - else { - // New object - // Assign a new STIX id if not already provided - relationship.stix.id = relationship.stix.id || `relationship--${uuid.v4()}`; - - // Set the created_by_ref and x_mitre_modified_by_ref properties - relationship.stix.created_by_ref = organizationIdentityRef; - relationship.stix.x_mitre_modified_by_ref = organizationIdentityRef; - } - } +class RelationshipsService extends BaseService { - // Save the document in the database - try { - const savedRelationshipe = await relationship.save(); - return savedRelationshipe; - } - catch (err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - // 11000 = Duplicate index - const error = new Error(errors.duplicateId); - throw error; - } - else { - throw err; - } - } -}; - -exports.updateFull = function(stixId, stixModified, data, callback) { - if (!stixId) { - const error = new Error(errors.missingParameter); - error.parameterName = 'stixId'; - return callback(error); - } - - if (!stixModified) { - const error = new Error(errors.missingParameter); - error.parameterName = 'modified'; - return callback(error); - } - - Relationship.findOne({ 'stix.id': stixId, 'stix.modified': stixModified }, function(err, document) { - if (err) { - if (err.name === 'CastError') { - var error = new Error(errors.badlyFormattedParameter); - error.parameterName = 'stixId'; - return callback(error); - } - else { - return callback(err); - } - } - else if (!document) { - // document not found - return callback(null); - } - else { - // Copy data to found document and save - Object.assign(document, data); - document.save(function(err, savedDocument) { - if (err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - // 11000 = Duplicate index - var error = new Error(errors.duplicateId); - return callback(error); - } - else { - return callback(err); - } - } - else { - return callback(null, savedDocument); - } - }); - } - }); -}; +} -exports.deleteVersionById = function (stixId, stixModified, callback) { - if (!stixId) { - const error = new Error(errors.missingParameter); - error.parameterName = 'stixId'; - return callback(error); - } +module.exports = new RelationshipsService('relationship', relationshipsRepository); - if (!stixModified) { - const error = new Error(errors.missingParameter); - error.parameterName = 'modified'; - return callback(error); - } - - Relationship.findOneAndRemove({ 'stix.id': stixId, 'stix.modified': stixModified }, function (err, relationship) { - if (err) { - return callback(err); - } else { - //Note: relationship is null if not found - return callback(null, relationship); - } - }); -}; - -exports.deleteById = function (stixId, callback) { - if (!stixId) { - const error = new Error(errors.missingParameter); - error.parameterName = 'stixId'; - return callback(error); - } - - Relationship.deleteMany({ 'stix.id': stixId }, function (err, relationship) { - if (err) { - return callback(err); - } else { - //Note: relationship is null if not found - return callback(null, relationship); - } - }); -}; From 627368aaec70f70e1c8f8b707c2b091c4dc7dc27 Mon Sep 17 00:00:00 2001 From: Sun Date: Fri, 8 Dec 2023 16:31:09 -0500 Subject: [PATCH 02/26] refactored tests --- .../api/relationships/relationships.spec.js | 690 +++++++----------- 1 file changed, 259 insertions(+), 431 deletions(-) diff --git a/app/tests/api/relationships/relationships.spec.js b/app/tests/api/relationships/relationships.spec.js index f52ffcc4..1bea63aa 100644 --- a/app/tests/api/relationships/relationships.spec.js +++ b/app/tests/api/relationships/relationships.spec.js @@ -62,198 +62,146 @@ describe('Relationships API', function () { passportCookie = await login.loginAnonymous(app); }); - it('GET /api/relationships returns an empty array of relationships', function (done) { - request(app) + it('GET /api/relationships returns an empty array of relationships', async function () { + const res = await request(app) .get('/api/relationships') .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get an empty array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(0); - done(); - } - }); + .expect('Content-Type', /json/); + // We expect to get an empty array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(0); + }); - it('POST /api/relationships does not create an empty relationship', function (done) { + it('POST /api/relationships does not create an empty relationship', async function () { const body = { }; - request(app) + const res = await request(app) .post('/api/relationships') .send(body) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); + .expect(400); + }); let relationship1a; - it('POST /api/relationships creates a relationship', function (done) { + it('POST /api/relationships creates a relationship', async function () { const timestamp = new Date().toISOString(); initialObjectData.stix.created = timestamp; initialObjectData.stix.modified = timestamp; const body = initialObjectData; - request(app) + const res = await request(app) .post('/api/relationships') .send(body) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(201) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the created relationship - relationship1a = res.body; - expect(relationship1a).toBeDefined(); - expect(relationship1a.stix).toBeDefined(); - expect(relationship1a.stix.id).toBeDefined(); - expect(relationship1a.stix.created).toBeDefined(); - expect(relationship1a.stix.modified).toBeDefined(); - expect(relationship1a.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); - - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get the created relationship + relationship1a = res.body; + expect(relationship1a).toBeDefined(); + expect(relationship1a.stix).toBeDefined(); + expect(relationship1a.stix.id).toBeDefined(); + expect(relationship1a.stix.created).toBeDefined(); + expect(relationship1a.stix.modified).toBeDefined(); + expect(relationship1a.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); + + }); - it('GET /api/relationships returns the added relationship', function (done) { - request(app) + it('GET /api/relationships returns the added relationship', async function () { + const res = await request(app) .get('/api/relationships') .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get one relationship in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(1); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(1); + }); - it('GET /api/relationships/:id should not return a relationship when the id cannot be found', function (done) { - request(app) + it('GET /api/relationships/:id should not return a relationship when the id cannot be found', async function () { + const res = await request(app) .get('/api/relationships/not-an-id') .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); + .expect(404); + }); - it('GET /api/relationships/:id returns the added relationship', function (done) { - request(app) + it('GET /api/relationships/:id returns the added relationship', async function () { + const res = await request(app) .get('/api/relationships/' + relationship1a.stix.id) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get one relationship in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(1); - - const relationship = relationships[0]; - expect(relationship).toBeDefined(); - expect(relationship.stix).toBeDefined(); - expect(relationship.stix.id).toBe(relationship1a.stix.id); - expect(relationship.stix.type).toBe(relationship1a.stix.type); - expect(relationship.stix.name).toBe(relationship1a.stix.name); - expect(relationship.stix.description).toBe(relationship1a.stix.description); - expect(relationship.stix.spec_version).toBe(relationship1a.stix.spec_version); - expect(relationship.stix.object_marking_refs).toEqual(expect.arrayContaining(relationship1a.stix.object_marking_refs)); - expect(relationship.stix.created_by_ref).toBe(relationship1a.stix.created_by_ref); - expect(relationship.stix.x_mitre_attack_spec_version).toBe(relationship1a.stix.x_mitre_attack_spec_version); - - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(1); + + const relationship = relationships[0]; + expect(relationship).toBeDefined(); + expect(relationship.stix).toBeDefined(); + expect(relationship.stix.id).toBe(relationship1a.stix.id); + expect(relationship.stix.type).toBe(relationship1a.stix.type); + expect(relationship.stix.name).toBe(relationship1a.stix.name); + expect(relationship.stix.description).toBe(relationship1a.stix.description); + expect(relationship.stix.spec_version).toBe(relationship1a.stix.spec_version); + expect(relationship.stix.object_marking_refs).toEqual(expect.arrayContaining(relationship1a.stix.object_marking_refs)); + expect(relationship.stix.created_by_ref).toBe(relationship1a.stix.created_by_ref); + expect(relationship.stix.x_mitre_attack_spec_version).toBe(relationship1a.stix.x_mitre_attack_spec_version); + + }); - it('PUT /api/relationships updates a relationship', function (done) { + it('PUT /api/relationships updates a relationship', async function () { const originalModified = relationship1a.stix.modified; const timestamp = new Date().toISOString(); relationship1a.stix.modified = timestamp; relationship1a.stix.description = 'This is an updated relationship.' const body = relationship1a; - request(app) + const res = await request(app) .put('/api/relationships/' + relationship1a.stix.id + '/modified/' + originalModified) .send(body) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the updated relationship - const relationship = res.body; - expect(relationship).toBeDefined(); - expect(relationship.stix.id).toBe(relationship1a.stix.id); - expect(relationship.stix.modified).toBe(relationship1a.stix.modified); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get the updated relationship + const relationship = res.body; + expect(relationship).toBeDefined(); + expect(relationship.stix.id).toBe(relationship1a.stix.id); + expect(relationship.stix.modified).toBe(relationship1a.stix.modified); + }); - it('POST /api/relationships does not create a relationship with the same id and modified date', function (done) { + it('POST /api/relationships does not create a relationship with the same id and modified date', async function () { const body = relationship1a; - request(app) + const res = await request(app) .post('/api/relationships') .send(body) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(409) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); + .expect(409); }); let relationship1b; - it('POST /api/relationships should create a new version of a relationship with a duplicate stix.id but different stix.modified date', function (done) { + it('POST /api/relationships should create a new version of a relationship with a duplicate stix.id but different stix.modified date', async function () { relationship1b = _.cloneDeep(relationship1a); relationship1b._id = undefined; relationship1b.__t = undefined; @@ -261,28 +209,22 @@ describe('Relationships API', function () { const timestamp = new Date().toISOString(); relationship1b.stix.modified = timestamp; const body = relationship1b; - request(app) + const res = await request(app) .post('/api/relationships') .send(body) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(201) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the created relationship - const relationship = res.body; - expect(relationship).toBeDefined(); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get the created relationship + const relationship = res.body; + expect(relationship).toBeDefined(); + }); let relationship1c; - it('POST /api/relationships should create a new version of a relationship with a duplicate stix.id but different stix.modified date', function (done) { + it('POST /api/relationships should create a new version of a relationship with a duplicate stix.id but different stix.modified date', async function () { relationship1c = _.cloneDeep(relationship1a); relationship1c._id = undefined; relationship1c.__t = undefined; @@ -290,407 +232,293 @@ describe('Relationships API', function () { const timestamp = new Date().toISOString(); relationship1c.stix.modified = timestamp; const body = relationship1c; - request(app) + const res = await request(app) .post('/api/relationships') .send(body) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(201) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the created relationship - const relationship = res.body; - expect(relationship).toBeDefined(); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get the created relationship + const relationship = res.body; + expect(relationship).toBeDefined(); + }); - it('GET /api/relationships returns the latest added relationship', function (done) { - request(app) + it('GET /api/relationships returns the latest added relationship', async function () { + const res = await request(app) .get('/api/relationships') .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get one relationship in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(1); - const relationship = relationships[0]; - expect(relationship.stix.id).toBe(relationship1c.stix.id); - expect(relationship.stix.modified).toBe(relationship1c.stix.modified); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(1); + const relationship = relationships[0]; + expect(relationship.stix.id).toBe(relationship1c.stix.id); + expect(relationship.stix.modified).toBe(relationship1c.stix.modified); + }); - it('GET /api/relationships returns all added relationships', function (done) { - request(app) + it('GET /api/relationships returns all added relationships', async function () { + const res = await request(app) .get('/api/relationships?versions=all') .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get two relationships in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(3); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get two relationships in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(3); + }); - it('GET /api/relationships/:stixId returns the latest added relationship', function (done) { - request(app) + it('GET /api/relationships/:stixId returns the latest added relationship', async function () { + const res = await request(app) .get('/api/relationships/' + relationship1b.stix.id) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get one relationship in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(1); - const relationship = relationships[0]; - expect(relationship.stix.id).toBe(relationship1c.stix.id); - expect(relationship.stix.modified).toBe(relationship1c.stix.modified); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(1); + const relationship = relationships[0]; + expect(relationship.stix.id).toBe(relationship1c.stix.id); + expect(relationship.stix.modified).toBe(relationship1c.stix.modified); + }); - it('GET /api/relationships/:stixId returns all added relationships', function (done) { - request(app) + it('GET /api/relationships/:stixId returns all added relationships', async function () { + const res = await request(app) .get('/api/relationships/' + relationship1a.stix.id + '?versions=all') .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get two relationships in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(3); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get two relationships in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(3); + }); - it('GET /api/relationships/:id/modified/:modified returns the first added relationship', function (done) { - request(app) + it('GET /api/relationships/:id/modified/:modified returns the first added relationship', async function () { + const res = await request(app) .get('/api/relationships/' + relationship1a.stix.id + '/modified/' + relationship1a.stix.modified) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get one relationship in an array - const relationship = res.body; - expect(relationship).toBeDefined(); - expect(relationship.stix).toBeDefined(); - expect(relationship.stix.id).toBe(relationship1a.stix.id); - expect(relationship.stix.modified).toBe(relationship1a.stix.modified); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationship = res.body; + expect(relationship).toBeDefined(); + expect(relationship.stix).toBeDefined(); + expect(relationship.stix.id).toBe(relationship1a.stix.id); + expect(relationship.stix.modified).toBe(relationship1a.stix.modified); + }); - it('GET /api/relationships/:id/modified/:modified returns the second added relationship', function (done) { - request(app) + it('GET /api/relationships/:id/modified/:modified returns the second added relationship', async function () { + const res = await request(app) .get('/api/relationships/' + relationship1b.stix.id + '/modified/' + relationship1b.stix.modified) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get one relationship in an array - const relationship = res.body; - expect(relationship).toBeDefined(); - expect(relationship.stix).toBeDefined(); - expect(relationship.stix.id).toBe(relationship1b.stix.id); - expect(relationship.stix.modified).toBe(relationship1b.stix.modified); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationship = res.body; + expect(relationship).toBeDefined(); + expect(relationship.stix).toBeDefined(); + expect(relationship.stix.id).toBe(relationship1b.stix.id); + expect(relationship.stix.modified).toBe(relationship1b.stix.modified); + }); let relationship2; - it('POST /api/relationships creates a relationship', function (done) { + it('POST /api/relationships creates a relationship', async function () { const timestamp = new Date().toISOString(); initialObjectData.stix.created = timestamp; initialObjectData.stix.modified = timestamp; initialObjectData.stix.source_ref = sourceRef2; initialObjectData.stix.target_ref = targetRef2; const body = initialObjectData; - request(app) + const res = await request(app) .post('/api/relationships') .send(body) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(201) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the created relationship - relationship2 = res.body; - expect(relationship2).toBeDefined(); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get the created relationship + relationship2 = res.body; + expect(relationship2).toBeDefined(); + }); - it('GET /api/relationships returns the (latest) relationship matching a source_ref', function (done) { - request(app) + it('GET /api/relationships returns the (latest) relationship matching a source_ref', async function () { + const res = await request(app) .get('/api/relationships?sourceRef=' + sourceRef1) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get one relationship in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(1); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(1); + }); - it('GET /api/relationships returns the (latest) relationship matching a target_ref', function (done) { - request(app) + it('GET /api/relationships returns the (latest) relationship matching a target_ref', async function () { + const res = await request(app) .get('/api/relationships?targetRef=' + targetRef1) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get one relationship in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(1); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(1); + }); - it('GET /api/relationships returns the (latest) relationship matching a sourceOrTargetRef', function (done) { - request(app) + it('GET /api/relationships returns the (latest) relationship matching a sourceOrTargetRef', async function () { + const res = await request(app) .get('/api/relationships?sourceOrTargetRef=' + sourceRef1) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get one relationship in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(1); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(1); + }); - it('GET /api/relationships returns zero relationships for a non-matching source_ref', function (done) { - request(app) + it('GET /api/relationships returns zero relationships for a non-matching source_ref', async function () { + const res = await request(app) .get('/api/relationships?sourceRef=' + sourceRef3) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get zero relationships in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(0); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get zero relationships in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(0); + }); - it('GET /api/relationships returns zero relationships for a non-matching target_ref', function (done) { - request(app) + it('GET /api/relationships returns zero relationships for a non-matching target_ref', async function () { + const res = await request(app) .get('/api/relationships?targetRef=' + targetRef3) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get one relationship in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(0); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(0); + }); - it('GET /api/relationships returns zero relationships for a non-matching sourceOrTargetRef', function (done) { - request(app) + it('GET /api/relationships returns zero relationships for a non-matching sourceOrTargetRef', async function () { + const res = await request(app) .get('/api/relationships?sourceOrTargetRef=' + sourceRef3) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get one relationship in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(0); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(0); + }); - it('DELETE /api/relationships/:id should not delete a relationship when the id cannot be found', function (done) { - request(app) + it('DELETE /api/relationships/:id should not delete a relationship when the id cannot be found', async function () { + const res = await request(app) .delete('/api/relationships/not-an-id') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); + .expect(404); + + }); - it('DELETE /api/relationships/:id/modified/:modified deletes a relationship', function (done) { - request(app) + it('DELETE /api/relationships/:id/modified/:modified deletes a relationship', async function () { + const res = await request(app) .delete('/api/relationships/' + relationship1a.stix.id + '/modified/' + relationship1a.stix.modified) .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); + .expect(204); + + }); - it('DELETE /api/relationships/:id should delete all the relationships with the same stix id', function (done) { - request(app) + it('DELETE /api/relationships/:id should delete all the relationships with the same stix id', async function () { + const res = await request(app) .delete('/api/relationships/' + relationship1b.stix.id) .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); + .expect(204); + + }); - it('DELETE /api/relationships should delete the third relationship', function (done) { - request(app) + it('DELETE /api/relationships should delete the third relationship', async function () { + const res = await request(app) .delete('/api/relationships/' + relationship2.stix.id + '/modified/' + relationship2.stix.modified) .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); + .expect(204); + + }); - it('GET /api/relationships returns an empty array of relationships', function (done) { - request(app) + it('GET /api/relationships returns an empty array of relationships', async function () { + const res = await request(app) .get('/api/relationships') .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get an empty array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(0); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get an empty array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(0); + }); after(async function() { From 92592cefac9f41be84475fc17eede167e7f5305a Mon Sep 17 00:00:00 2001 From: Sun Date: Fri, 8 Dec 2023 17:06:31 -0500 Subject: [PATCH 03/26] refactored controller --- app/controllers/relationships-controller.js | 125 ++++++++++---------- 1 file changed, 60 insertions(+), 65 deletions(-) diff --git a/app/controllers/relationships-controller.js b/app/controllers/relationships-controller.js index 9ae3fe6c..1370d6eb 100644 --- a/app/controllers/relationships-controller.js +++ b/app/controllers/relationships-controller.js @@ -2,8 +2,9 @@ const relationshipsService = require('../services/relationships-service'); const logger = require('../lib/logger'); +const { BadlyFormattedParameterError, InvalidQueryStringParameterError, DuplicateIdError } = require('../exceptions'); -exports.retrieveAll = async function(req, res) { +exports.retrieveAll = async async function(req, res) { const options = { offset: req.query.offset || 0, limit: req.query.limit || 0, @@ -39,18 +40,26 @@ exports.retrieveAll = async function(req, res) { } }; -exports.retrieveById = function(req, res) { +exports.retrieveById = async function(req, res) { const options = { versions: req.query.versions || 'latest' } + try { + const relationships = await relationshipsService.retrieveById(req.params.stixId, options); - relationshipsService.retrieveById(req.params.stixId, options, function (err, relationships) { - if (err) { - if (err.message === relationshipsService.errors.badlyFormattedParameter) { + if (relationships.length === 0) { + return res.status(404).send('Relationship not found.'); + } + else { + logger.debug(`Success: Retrieved ${ relationships.length } relationship(s) with id ${ req.params.stixId }`); + return res.status(200).send(relationships); + } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { logger.warn('Badly formatted stix id: ' + req.params.stixId); return res.status(400).send('Stix id is badly formatted.'); } - else if (err.message === relationshipsService.errors.invalidQueryStringParameter) { + else if (err instanceof InvalidQueryStringParameterError) { logger.warn('Invalid query string: versions=' + req.query.versions); return res.status(400).send('Query string parameter versions is invalid.'); } @@ -59,22 +68,21 @@ exports.retrieveById = function(req, res) { return res.status(500).send('Unable to get relationships. Server error.'); } } - else { - if (relationships.length === 0) { +}; + +exports.retrieveVersionById = async function(req, res) { + + try { + const relationship = await relationshipsService.retrieveVersionById(req.params.stixId, req.params.modified); + if (!relationship) { return res.status(404).send('Relationship not found.'); } else { - logger.debug(`Success: Retrieved ${ relationships.length } relationship(s) with id ${ req.params.stixId }`); - return res.status(200).send(relationships); + logger.debug(`Success: Retrieved relationship with id ${relationship.id}`); + return res.status(200).send(relationship); } - } - }); -}; - -exports.retrieveVersionById = function(req, res) { - relationshipsService.retrieveVersionById(req.params.stixId, req.params.modified, function (err, relationship) { - if (err) { - if (err.message === relationshipsService.errors.badlyFormattedParameter) { + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { logger.warn('Badly formatted stix id: ' + req.params.stixId); return res.status(400).send('Stix id is badly formatted.'); } @@ -82,16 +90,7 @@ exports.retrieveVersionById = function(req, res) { logger.error('Failed with error: ' + err); return res.status(500).send('Unable to get relationship. Server error.'); } - } else { - if (!relationship) { - return res.status(404).send('Relationship not found.'); - } - else { - logger.debug(`Success: Retrieved relationship with id ${relationship.id}`); - return res.status(200).send(relationship); - } } - }); }; exports.create = async function(req, res) { @@ -109,7 +108,7 @@ exports.create = async function(req, res) { return res.status(201).send(relationship); } catch(err) { - if (err.message === relationshipsService.errors.duplicateId) { + if (err instanceof DuplicateIdError) { logger.warn("Duplicate stix.id and stix.modified"); return res.status(409).send('Unable to create relationship. Duplicate stix.id and stix.modified properties.'); } @@ -120,58 +119,54 @@ exports.create = async function(req, res) { } }; -exports.updateFull = function(req, res) { +exports.updateFull = async function(req, res) { // Get the data from the request const relationshipData = req.body; // Create the relationship - relationshipsService.updateFull(req.params.stixId, req.params.modified, relationshipData, function(err, relationship) { - if (err) { + try { + const relationship = relationshipsService.updateFull(req.params.stixId, req.params.modified, relationshipData); + if (!relationship) { + return res.status(404).send('Relationship not found.'); + } else { + logger.debug("Success: Updated relationship with id " + relationship.stix.id); + return res.status(200).send(relationship); + } + } catch (err) { logger.error("Failed with error: " + err); return res.status(500).send("Unable to update relationship. Server error."); } - else { - if (!relationship) { - return res.status(404).send('Relationship not found.'); - } else { - logger.debug("Success: Updated relationship with id " + relationship.stix.id); - return res.status(200).send(relationship); - } - } - }); + }; -exports.deleteVersionById = function(req, res) { - relationshipsService.deleteVersionById(req.params.stixId, req.params.modified, function (err, relationship) { - if (err) { +exports.deleteVersionById = async function(req, res) { + try { + const relationship = await relationshipsService.deleteVersionById(req.params.stixId, req.params.modified); + if (!relationship) { + return res.status(404).send('Relationship not found.'); + } else { + logger.debug("Success: Deleted relationship with id " + relationship.stix.id); + return res.status(204).end(); + } + } catch (err) { logger.error('Delete relationship failed. ' + err); return res.status(500).send('Unable to delete relationship. Server error.'); } - else { - if (!relationship) { - return res.status(404).send('Relationship not found.'); - } else { - logger.debug("Success: Deleted relationship with id " + relationship.stix.id); - return res.status(204).end(); - } - } - }); + }; -exports.deleteById = function(req, res) { - relationshipsService.deleteById(req.params.stixId, function (err, relationships) { - if (err) { - logger.error('Delete relationship failed. ' + err); - return res.status(500).send('Unable to delete relationship. Server error.'); +exports.deleteById = async function(req, res) { + try { + const relationship = await relationshipsService.deleteById(req.params.stixId); + if (relationships.deletedCount === 0) { + return res.status(404).send('Relationship not found.'); } else { - if (relationships.deletedCount === 0) { - return res.status(404).send('Relationship not found.'); - } - else { - logger.debug(`Success: Deleted relationship with id ${ req.params.stixId }`); - return res.status(204).end(); - } + logger.debug(`Success: Deleted relationship with id ${ req.params.stixId }`); + return res.status(204).end(); + } + } catch (err) { + logger.error('Delete relationship failed. ' + err); + return res.status(500).send('Unable to delete relationship. Server error.'); } - }); }; From 528b4e73e36a7f6e77909d06f3d070a741af4df2 Mon Sep 17 00:00:00 2001 From: Sun Date: Fri, 8 Dec 2023 17:07:28 -0500 Subject: [PATCH 04/26] added base service call --- app/services/relationships-service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/relationships-service.js b/app/services/relationships-service.js index 1897199d..bdf9cde2 100644 --- a/app/services/relationships-service.js +++ b/app/services/relationships-service.js @@ -2,7 +2,7 @@ const relationshipsRepository = require('../repository/relationships-repository'); const { lastUpdatedByQueryHelper } = require('../lib/request-parameter-helper'); - +const BaseService = require('./_base.service'); const errors = { missingParameter: 'Missing required parameter', badlyFormattedParameter: 'Badly formatted parameter', From 8de55ce44f0e4f11aa2d6233a395d27c71509623 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Tue, 7 Jan 2025 09:14:54 -0500 Subject: [PATCH 05/26] feat: add relationships-repository --- app/repository/relationships-repository.js | 114 +++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 app/repository/relationships-repository.js diff --git a/app/repository/relationships-repository.js b/app/repository/relationships-repository.js new file mode 100644 index 00000000..ca2e99de --- /dev/null +++ b/app/repository/relationships-repository.js @@ -0,0 +1,114 @@ +'use strict'; + +const BaseRepository = require('./_base.repository'); +const Relationship = require('../models/relationship-model'); +const { DatabaseError } = require('../exceptions'); + +class RelationshipsRepository extends BaseRepository { + /** + * Extends BaseRepository.retrieveAll() to include relationship-specific query parameters + * and data lookup functionality. + * + * Additional options supported beyond base implementation: + * @param {Object} options + * @param {string} options.sourceRef - Filter by source reference + * @param {string} options.targetRef - Filter by target reference + * @param {string} options.sourceOrTargetRef - Filter by either source or target reference + * @param {string} options.relationshipType - Filter by relationship type + * @param {boolean} options.lookupRefs - Include source/target object data via $lookup + * + * @returns {Promise} Array of relationship documents with optional source/target data + */ + async retrieveAll(options) { + try { + const query = this._buildBaseQuery(options); + const aggregation = RelationshipsRepository._buildAggregation(options, query); + return await this.model.aggregate(aggregation).exec(); + } catch (err) { + throw new DatabaseError(err); + } + } + + _buildBaseQuery(options) { + const query = super._buildBaseQuery(options); + + if (options.sourceRef) { + query['stix.source_ref'] = options.sourceRef; + } + if (options.targetRef) { + query['stix.target_ref'] = options.targetRef; + } + if (options.sourceOrTargetRef) { + query.$or = [ + { 'stix.source_ref': options.sourceOrTargetRef }, + { 'stix.target_ref': options.sourceOrTargetRef } + ]; + } + if (options.relationshipType) { + query['stix.relationship_type'] = options.relationshipType; + } + return query; + } + + static _buildAggregation(options, query) { + const aggregation = [ + { $sort: { 'stix.id': 1, 'stix.modified': -1 } } + ]; + + if (options.versions === 'latest') { + aggregation.push( + { $group: { _id: '$stix.id', document: { $first: '$$ROOT' } } }, + { $replaceRoot: { newRoot: '$document' } } + ); + } + + aggregation.push( + { $sort: { 'stix.id': 1 } }, + { $match: query } + ); + + if (options.lookupRefs) { + aggregation.push( + { + $lookup: { + from: 'attackObjects', + localField: 'stix.source_ref', + foreignField: 'stix.id', + as: 'source_objects' + } + }, + { + $lookup: { + from: 'attackObjects', + localField: 'stix.target_ref', + foreignField: 'stix.id', + as: 'target_objects' + } + } + ); + } + + return RelationshipsRepository._addPaginationStages(aggregation, options); + } + + static _addPaginationStages(aggregation, options) { + const facet = { + $facet: { + totalCount: [{ $count: 'totalCount' }], + documents: [] + } + }; + + if (options.offset) { + facet.$facet.documents.push({ $skip: options.offset }); + } + if (options.limit) { + facet.$facet.documents.push({ $limit: options.limit }); + } + + aggregation.push(facet); + return aggregation; + } +} + +module.exports = new RelationshipsRepository(Relationship); \ No newline at end of file From 100617fb1b436970f697b12607284abc648bed22 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Tue, 7 Jan 2025 09:20:12 -0500 Subject: [PATCH 06/26] fix: move attackObjectsService.setDefaultMarkingDefinitions to systemConfigurationService.setDefaultMarkingDefinitionsForObject - I believe this was the source of the circular dependencies issue. There are no more circuluar imports between the AttackObjectsService, RelationshipsService, IdentitiesService, UserAccountsService, and SystemsConfigurationService. - This method makes more sense in the systems-configuration-service module anyways, considering other similar functions are implemented there. - Had to rename the function because an identically named function that processes STIX IDs instead of STIX objects already exists in the system-configuration-service module. We may consolidate these in the future. --- app/services/_base.service.js | 8 +++++--- app/services/attack-objects-service.js | 11 ----------- app/services/collections-service.js | 2 +- app/services/identities-service.js | 4 ++-- app/services/software-service.js | 3 +-- app/services/system-configuration-service.js | 15 ++++++++++++++- 6 files changed, 23 insertions(+), 20 deletions(-) diff --git a/app/services/_base.service.js b/app/services/_base.service.js index 55481e8b..0d80d98e 100644 --- a/app/services/_base.service.js +++ b/app/services/_base.service.js @@ -22,13 +22,15 @@ class BaseService extends AbstractService { static identitiesService; + static systemConfigurationService; + static requireServices() { // Late binding to avoid circular dependencies if (!BaseService.identitiesService) { BaseService.identitiesService = require('./identities-service'); } - if (!BaseService.attackObjectsService) { - BaseService.attackObjectsService = require('./attack-objects-service'); + if (!BaseService.systemConfigurationService) { + BaseService.systemConfigurationService = require('./system-configuration-service'); } } @@ -258,7 +260,7 @@ class BaseService extends AbstractService { } // Set the default marking definitions - await BaseService.attackObjectsService.setDefaultMarkingDefinitions(data); + await BaseService.systemConfigurationService.setDefaultMarkingDefinitionsForObject(data); // Get the organization identity const organizationIdentityRef = await systemConfigurationService.retrieveOrganizationIdentityRef(); diff --git a/app/services/attack-objects-service.js b/app/services/attack-objects-service.js index f6430afb..6f362967 100644 --- a/app/services/attack-objects-service.js +++ b/app/services/attack-objects-service.js @@ -136,17 +136,6 @@ class AttackObjectsService extends BaseService { throw new Error(this.errors.notFound); } } - - async setDefaultMarkingDefinitions(attackObject) { - // Add any default marking definitions that are not in the current list for this object - const defaultMarkingDefinitions = await this.systemConfigurationService.retrieveDefaultMarkingDefinitions({ refOnly: true }); - if (attackObject.stix.object_marking_refs) { - attackObject.stix.object_marking_refs = attackObject.stix.object_marking_refs.concat(defaultMarkingDefinitions.filter(e => !attackObject.stix.object_marking_refs.includes(e))); - } - else { - attackObject.stix.object_marking_refs = defaultMarkingDefinitions; - } - } } module.exports = new AttackObjectsService(null, attackObjectsRepository); \ No newline at end of file diff --git a/app/services/collections-service.js b/app/services/collections-service.js index c00dd360..055620b4 100644 --- a/app/services/collections-service.js +++ b/app/services/collections-service.js @@ -293,7 +293,7 @@ exports.create = async function(data, options) { } // Set the default marking definitions - await attackObjectsService.setDefaultMarkingDefinitions(collection); + await systemConfigurationService.setDefaultMarkingDefinitionsForObject(collection); // Get the organization identity const organizationIdentityRef = await systemConfigurationService.retrieveOrganizationIdentityRef(); diff --git a/app/services/identities-service.js b/app/services/identities-service.js index f54f09ca..0a7b92fa 100644 --- a/app/services/identities-service.js +++ b/app/services/identities-service.js @@ -1,7 +1,7 @@ 'use strict'; const uuid = require('uuid'); -const attackObjectsService = require('./attack-objects-service'); +const systemConfigurationService = require('./system-configuration-service'); const config = require('../config/config'); const userAccountsService = require('./user-accounts-service'); const identitiesRepository = require('../repository/identities-repository'); @@ -50,7 +50,7 @@ class IdentitiesService extends BaseService { } // Set the default marking definitions - await attackObjectsService.setDefaultMarkingDefinitions(data); + await systemConfigurationService.setDefaultMarkingDefinitionsForObject(data); // Assign a new STIX id if not already provided data.stix.id = data.stix.id || `identity--${uuid.v4()}`; diff --git a/app/services/software-service.js b/app/services/software-service.js index 18899a6e..f54516d9 100644 --- a/app/services/software-service.js +++ b/app/services/software-service.js @@ -2,7 +2,6 @@ const uuid = require('uuid'); const systemConfigurationService = require('./system-configuration-service'); -const attackObjectsService = require('./attack-objects-service'); const config = require('../config/config'); const { PropertyNotAllowedError, InvalidTypeError } = require('../exceptions'); @@ -42,7 +41,7 @@ class SoftwareService extends BaseService { } // Set the default marking definitions - await attackObjectsService.setDefaultMarkingDefinitions(data); + await systemConfigurationService.setDefaultMarkingDefinitionsForObject(data); // Get the organization identity const organizationIdentityRef = await systemConfigurationService.retrieveOrganizationIdentityRef(); diff --git a/app/services/system-configuration-service.js b/app/services/system-configuration-service.js index 94760a43..5bb2768c 100644 --- a/app/services/system-configuration-service.js +++ b/app/services/system-configuration-service.js @@ -115,7 +115,7 @@ exports.setOrganizationIdentity = async function(stixId) { } } -exports.retrieveDefaultMarkingDefinitions = async function(options) { +const retrieveDefaultMarkingDefinitions = async function(options) { if (!markingDefinitionsService) { markingDefinitionsService = require('./marking-definitions-service'); } @@ -154,6 +154,19 @@ exports.retrieveDefaultMarkingDefinitions = async function(options) { return []; } } +exports.retrieveDefaultMarkingDefinitions = retrieveDefaultMarkingDefinitions; + +// ** migrated from attack-objects-service ** +exports.setDefaultMarkingDefinitionsForObject = async function(attackObject) { + // Add any default marking definitions that are not in the current list for this object + const defaultMarkingDefinitions = await retrieveDefaultMarkingDefinitions({ refOnly: true }); + if (attackObject.stix.object_marking_refs) { + attackObject.stix.object_marking_refs = attackObject.stix.object_marking_refs.concat(defaultMarkingDefinitions.filter(e => !attackObject.stix.object_marking_refs.includes(e))); + } + else { + attackObject.stix.object_marking_refs = defaultMarkingDefinitions; + } +} exports.setDefaultMarkingDefinitions = async function(stixIds) { // There should be exactly one system configuration document From 1162b24c0e567931b8d0f5e60a77ccf1164d207c Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Tue, 7 Jan 2025 09:20:40 -0500 Subject: [PATCH 07/26] feat: refactored relationships-service --- app/services/relationships-service.js | 510 ++++++-------------------- 1 file changed, 107 insertions(+), 403 deletions(-) diff --git a/app/services/relationships-service.js b/app/services/relationships-service.js index 80045dee..d3865a08 100644 --- a/app/services/relationships-service.js +++ b/app/services/relationships-service.js @@ -1,444 +1,148 @@ 'use strict'; -const uuid = require('uuid'); -const Relationship = require('../models/relationship-model'); -const systemConfigurationService = require('./system-configuration-service'); +//** core dependencies **/ +const BaseService = require('./_base.service'); +const relationshipsRepository = require('../repository/relationships-repository'); + +// ** service dependencies **/ const identitiesService = require('./identities-service'); +const systemConfigService = require('./system-configuration-service'); const config = require('../config/config'); -const { lastUpdatedByQueryHelper } = require('../lib/request-parameter-helper'); - -const errors = { - missingParameter: 'Missing required parameter', - badlyFormattedParameter: 'Badly formatted parameter', - duplicateId: 'Duplicate id', - notFound: 'Document not found', - invalidQueryStringParameter: 'Invalid query string parameter' -}; -exports.errors = errors; - -// Map STIX types to ATT&CK types -const objectTypeMap = new Map(); -objectTypeMap.set('malware', 'software'); -objectTypeMap.set('tool', 'software'); -objectTypeMap.set('attack-pattern', 'technique'); -objectTypeMap.set('intrusion-set', 'group'); -objectTypeMap.set('campaign', 'campaign'); -objectTypeMap.set('x-mitre-asset', 'asset'); -objectTypeMap.set('course-of-action', 'mitigation'); -objectTypeMap.set('x-mitre-tactic', 'tactic'); -objectTypeMap.set('x-mitre-matrix', 'matrix'); -objectTypeMap.set('x-mitre-data-component', 'data-component'); - -exports.retrieveAll = async function(options) { - // Build the query - const query = {}; - if (!options.includeRevoked) { - query['stix.revoked'] = { $in: [null, false] }; - } - if (!options.includeDeprecated) { - query['stix.x_mitre_deprecated'] = { $in: [null, false] }; - } - if (typeof options.state !== 'undefined') { - if (Array.isArray(options.state)) { - query['workspace.workflow.state'] = { $in: options.state }; - } - else { - query['workspace.workflow.state'] = options.state; - } - } - if (typeof options.sourceRef !== 'undefined') { - query['stix.source_ref'] = options.sourceRef; - } - if (typeof options.targetRef !== 'undefined') { - query['stix.target_ref'] = options.targetRef; - } - if (typeof options.sourceOrTargetRef !== 'undefined') { - query.$or = [{ 'stix.source_ref': options.sourceOrTargetRef }, { 'stix.target_ref': options.sourceOrTargetRef }] - } - if (typeof options.relationshipType !== 'undefined') { - query['stix.relationship_type'] = options.relationshipType; - } - if (typeof options.lastUpdatedBy !== 'undefined') { - query['workspace.workflow.created_by_user_account'] = lastUpdatedByQueryHelper(options.lastUpdatedBy); - } - - // Build the aggregation - const aggregation = []; - if (options.versions === 'latest') { - // - Group the documents by stix.id, sorted by stix.modified - // - Use the first document in each group (according to the value of stix.modified) - aggregation.push({ $sort: { 'stix.id': 1, 'stix.modified': -1 } }); - aggregation.push({ $group: { _id: '$stix.id', document: { $first: '$$ROOT' } } }); - aggregation.push({ $replaceRoot: { newRoot: '$document' } }); - } - - // Add stages to the aggregation to sort (for pagination), apply the query, and add source and target object data - aggregation.push({ $sort: { 'stix.id': 1 } }); - aggregation.push({ $match: query }); - if (options.lookupRefs) { - aggregation.push({ - $lookup: { - from: 'attackObjects', - localField: 'stix.source_ref', - foreignField: 'stix.id', - as: 'source_objects' - } - }); - aggregation.push({ - $lookup: { - from: 'attackObjects', - localField: 'stix.target_ref', - foreignField: 'stix.id', - as: 'target_objects' +// ** misc **/ +const uuid = require('uuid'); +const { InvalidTypeError } = require('../exceptions'); + +class RelationshipsService extends BaseService { + + static objectTypeMap = new Map([ + ['malware', 'software'], + ['tool', 'software'], + ['attack-pattern', 'technique'], + ['intrusion-set', 'group'], + ['campaign', 'campaign'], + ['x-mitre-asset', 'asset'], + ['course-of-action', 'mitigation'], + ['x-mitre-tactic', 'tactic'], + ['x-mitre-matrix', 'matrix'], + ['x-mitre-data-component', 'data-component'] + ]); + + async retrieveAll(options) { + // First get results from repository + const results = await this.repository.retrieveAll(options); + + // Apply source/target type filtering if needed + if (options.sourceType || options.targetType) { + const [{ documents, totalCount }] = results; + let filteredDocs = documents; + + // Filter by source type if specified + if (options.sourceType) { + filteredDocs = filteredDocs.filter(document => { + if (!document.source_objects?.length) { + return false; + } + // Sort by modified date to get the latest version + document.source_objects.sort((a, b) => b.stix.modified - a.stix.modified); + return RelationshipsService.objectTypeMap.get(document.source_objects[0].stix.type) === options.sourceType; + }); } - }); - } - // Retrieve the documents - let results = await Relationship.aggregate(aggregation); - // Filter out relationships that don't reference the source type - if (options.sourceType) { - results = results.filter(document => { - if (document.source_objects.length === 0) { - return false; - } - else { - document.source_objects.sort((a, b) => b.stix.modified - a.stix.modified); - return objectTypeMap.get(document.source_objects[0].stix.type) === options.sourceType; + // Filter by target type if specified + if (options.targetType) { + filteredDocs = filteredDocs.filter(document => { + if (!document.target_objects?.length) { + return false; + } + // Sort by modified date to get the latest version + document.target_objects.sort((a, b) => b.stix.modified - a.stix.modified); + return RelationshipsService.objectTypeMap.get(document.target_objects[0].stix.type) === options.targetType; + }); } - }); - } - // Filter out relationships that don't reference the target type - if (options.targetType) { - results = results.filter(document => { - if (document.target_objects.length === 0) { - return false; - } - else { - document.target_objects.sort((a, b) => b.stix.modified - a.stix.modified); - return objectTypeMap.get(document.target_objects[0].stix.type) === options.targetType; + // Update results with filtered documents and recalculate total count + results[0].documents = filteredDocs; + if (totalCount?.length > 0) { + results[0].totalCount[0].totalCount = filteredDocs.length; } - }); - } - - const prePaginationTotal = results.length; - - // Apply pagination parameters - if (options.offset || options.limit) { - const start = options.offset || 0; - if (options.limit) { - const end = start + options.limit; - results = results.slice(start, end); } - else { - results = results.slice(start); + + // Add identity information if requested + if (options.includeIdentities) { + await identitiesService.addCreatedByAndModifiedByIdentitiesToAll(results[0].documents); } + + // Format and return results + return RelationshipsService._formatResults(results, options); } - // Move latest source and target objects to a non-array property, then remove array of source and target objects - for (const document of results) { - if (Array.isArray(document.source_objects)) { - if (document.source_objects.length === 0) { - document.source_objects = undefined; - } - else { - document.source_object = document.source_objects[0]; - document.source_objects = undefined; - } - } + static _formatResults(results, options) { + const [{ documents, totalCount }] = results; - if (Array.isArray(document.target_objects)) { - if (document.target_objects.length === 0) { - document.target_objects = undefined; + // Move source/target objects to single properties + for (const document of documents) { + if (Array.isArray(document.source_objects)) { + document.source_object = document.source_objects[0]; + delete document.source_objects; } - else { + if (Array.isArray(document.target_objects)) { document.target_object = document.target_objects[0]; - document.target_objects = undefined; + delete document.target_objects; } } - } - - if (options.includeIdentities) { - await identitiesService.addCreatedByAndModifiedByIdentitiesToAll(results); - } - if (options.includePagination) { - return { + // Return with or without pagination wrapper + return options.includePagination ? { pagination: { - total: prePaginationTotal, + total: totalCount[0]?.totalCount || 0, offset: options.offset, limit: options.limit }, - data: results - }; - } - else { - return results; - } -}; - -exports.retrieveById = function(stixId, options, callback) { - // versions=all Retrieve all relationships with the stixId - // versions=latest Retrieve the relationship with the latest modified date for this stixId - - if (!stixId) { - const error = new Error(errors.missingParameter); - error.parameterName = 'stixId'; - return callback(error); - } - - if (options.versions === 'all') { - Relationship - .find({'stix.id': stixId}) - .sort('-stix.modified') - .lean() - .exec(function (err, relationships) { - if (err) { - if (err.name === 'CastError') { - const error = new Error(errors.badlyFormattedParameter); - error.parameterName = 'stixId'; - return callback(error); - } - else { - return callback(err); - } - } - else { - identitiesService.addCreatedByAndModifiedByIdentitiesToAll(relationships) - .then(() => callback(null, relationships)); - } - }); - } - else if (options.versions === 'latest') { - Relationship.findOne({ 'stix.id': stixId }) - .sort('-stix.modified') - .lean() - .exec(function(err, relationship) { - if (err) { - if (err.name === 'CastError') { - const error = new Error(errors.badlyFormattedParameter); - error.parameterName = 'stixId'; - return callback(error); - } - else { - return callback(err); - } - } - else { - // Note: document is null if not found - if (relationship) { - identitiesService.addCreatedByAndModifiedByIdentities(relationship) - .then(() => callback(null, [ relationship ])); - } - else { - return callback(null, []); - } - } - }); - } - else { - const error = new Error(errors.invalidQueryStringParameter); - error.parameterName = 'versions'; - return callback(error); + data: documents + } : documents; } -}; -exports.retrieveVersionById = function(stixId, modified, callback) { - // Retrieve the version of the relationship with the matching stixId and modified date + async create(data, options = {}) { + if (data?.stix?.type !== 'relationship') { + throw new InvalidTypeError(); + } - if (!stixId) { - const error = new Error(errors.missingParameter); - error.parameterName = 'stixId'; - return callback(error); - } + if (!options.import) { + await this._enrichMetadata(data, options); + } - if (!modified) { - const error = new Error(errors.missingParameter); - error.parameterName = 'modified'; - return callback(error); + return this.repository.save(data); } - Relationship.findOne({ 'stix.id': stixId, 'stix.modified': modified }, function(err, relationship) { - if (err) { - if (err.name === 'CastError') { - const error = new Error(errors.badlyFormattedParameter); - error.parameterName = 'stixId'; - return callback(error); - } - else { - return callback(err); - } - } - else { - // Note: document is null if not found - if (relationship) { - identitiesService.addCreatedByAndModifiedByIdentities(relationship) - .then(() => callback(null, relationship)); - } - else { - return callback(); - } - } - }); -}; - -exports.createIsAsync = true; -exports.create = async function(data, options) { - // This function handles two use cases: - // 1. This is a completely new object. Create a new object and generate the stix.id if not already - // provided. Set both stix.created_by_ref and stix.x_mitre_modified_by_ref to the organization identity. - // 2. This is a new version of an existing object. Create a new object with the specified id. - // Set stix.x_mitre_modified_by_ref to the organization identity. + async _enrichMetadata(data, options) { + data.stix.x_mitre_attack_spec_version = data.stix.x_mitre_attack_spec_version ?? config.app.attackSpecVersion; - // Avoid circular dependency - const attackObjectsService = require('./attack-objects-service'); - - // Create the document - const relationship = new Relationship(data); - - options = options || {}; - if (!options.import) { - // Set the ATT&CK Spec Version - relationship.stix.x_mitre_attack_spec_version = relationship.stix.x_mitre_attack_spec_version ?? config.app.attackSpecVersion; - - // Record the user account that created the object if (options.userAccountId) { - relationship.workspace.workflow.created_by_user_account = options.userAccountId; + data.workspace.workflow.created_by_user_account = options.userAccountId; } - + // Set the default marking definitions - await attackObjectsService.setDefaultMarkingDefinitions(relationship); - - // Get the organization identity - const organizationIdentityRef = await systemConfigurationService.retrieveOrganizationIdentityRef(); - - // Check for an existing object + await systemConfigService.setDefaultMarkingDefinitionsForObject(data); + + const organizationIdentityRef = await this.systemConfigService.retrieveOrganizationIdentityRef(); + + // Check for existing object let existingObject; - if (relationship.stix.id) { - existingObject = await Relationship.findOne({ 'stix.id': relationship.stix.id }); + if (data.stix.id) { + existingObject = await this.repository.retrieveOneById(data.stix.id); } - + if (existingObject) { - // New version of an existing object - // Only set the x_mitre_modified_by_ref property - relationship.stix.x_mitre_modified_by_ref = organizationIdentityRef; - } - else { - // New object - // Assign a new STIX id if not already provided - relationship.stix.id = relationship.stix.id || `relationship--${uuid.v4()}`; - - // Set the created_by_ref and x_mitre_modified_by_ref properties - relationship.stix.created_by_ref = organizationIdentityRef; - relationship.stix.x_mitre_modified_by_ref = organizationIdentityRef; - } - } - - // Save the document in the database - try { - const savedRelationshipe = await relationship.save(); - return savedRelationshipe; - } - catch (err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - // 11000 = Duplicate index - const error = new Error(errors.duplicateId); - throw error; - } - else { - throw err; - } - } -}; - -exports.updateFull = function(stixId, stixModified, data, callback) { - if (!stixId) { - const error = new Error(errors.missingParameter); - error.parameterName = 'stixId'; - return callback(error); - } - - if (!stixModified) { - const error = new Error(errors.missingParameter); - error.parameterName = 'modified'; - return callback(error); - } - - Relationship.findOne({ 'stix.id': stixId, 'stix.modified': stixModified }, function(err, document) { - if (err) { - if (err.name === 'CastError') { - var error = new Error(errors.badlyFormattedParameter); - error.parameterName = 'stixId'; - return callback(error); - } - else { - return callback(err); - } - } - else if (!document) { - // document not found - return callback(null); - } - else { - // Copy data to found document and save - Object.assign(document, data); - document.save(function(err, savedDocument) { - if (err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - // 11000 = Duplicate index - var error = new Error(errors.duplicateId); - return callback(error); - } - else { - return callback(err); - } - } - else { - return callback(null, savedDocument); - } - }); - } - }); -}; - -exports.deleteVersionById = function (stixId, stixModified, callback) { - if (!stixId) { - const error = new Error(errors.missingParameter); - error.parameterName = 'stixId'; - return callback(error); - } - - if (!stixModified) { - const error = new Error(errors.missingParameter); - error.parameterName = 'modified'; - return callback(error); - } - - Relationship.findOneAndRemove({ 'stix.id': stixId, 'stix.modified': stixModified }, function (err, relationship) { - if (err) { - return callback(err); + // New version - only set modified_by_ref + data.stix.x_mitre_modified_by_ref = organizationIdentityRef; } else { - //Note: relationship is null if not found - return callback(null, relationship); + // New object - set both refs and generate id if needed + data.stix.id = data.stix.id || `relationship--${uuid.v4()}`; + data.stix.created_by_ref = organizationIdentityRef; + data.stix.x_mitre_modified_by_ref = organizationIdentityRef; } - }); -}; - -exports.deleteById = function (stixId, callback) { - if (!stixId) { - const error = new Error(errors.missingParameter); - error.parameterName = 'stixId'; - return callback(error); } +} - Relationship.deleteMany({ 'stix.id': stixId }, function (err, relationship) { - if (err) { - return callback(err); - } else { - //Note: relationship is null if not found - return callback(null, relationship); - } - }); -}; +module.exports = new RelationshipsService(relationshipsRepository); \ No newline at end of file From e487260fc419b7a70f6ffe11ba0a48473660ce50 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Tue, 7 Jan 2025 09:34:03 -0500 Subject: [PATCH 08/26] fix: no-unused-vars in relationships.spec.js --- app/tests/api/relationships/relationships.spec.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/tests/api/relationships/relationships.spec.js b/app/tests/api/relationships/relationships.spec.js index 1bea63aa..e9914399 100644 --- a/app/tests/api/relationships/relationships.spec.js +++ b/app/tests/api/relationships/relationships.spec.js @@ -79,7 +79,7 @@ describe('Relationships API', function () { it('POST /api/relationships does not create an empty relationship', async function () { const body = { }; - const res = await request(app) + await request(app) .post('/api/relationships') .send(body) .set('Accept', 'application/json') @@ -131,7 +131,7 @@ describe('Relationships API', function () { }); it('GET /api/relationships/:id should not return a relationship when the id cannot be found', async function () { - const res = await request(app) + await request(app) .get('/api/relationships/not-an-id') .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) @@ -192,7 +192,7 @@ describe('Relationships API', function () { it('POST /api/relationships does not create a relationship with the same id and modified date', async function () { const body = relationship1a; - const res = await request(app) + await request(app) .post('/api/relationships') .send(body) .set('Accept', 'application/json') @@ -470,7 +470,7 @@ describe('Relationships API', function () { }); it('DELETE /api/relationships/:id should not delete a relationship when the id cannot be found', async function () { - const res = await request(app) + await request(app) .delete('/api/relationships/not-an-id') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(404); @@ -479,7 +479,7 @@ describe('Relationships API', function () { }); it('DELETE /api/relationships/:id/modified/:modified deletes a relationship', async function () { - const res = await request(app) + await request(app) .delete('/api/relationships/' + relationship1a.stix.id + '/modified/' + relationship1a.stix.modified) .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(204); @@ -488,7 +488,7 @@ describe('Relationships API', function () { }); it('DELETE /api/relationships/:id should delete all the relationships with the same stix id', async function () { - const res = await request(app) + await request(app) .delete('/api/relationships/' + relationship1b.stix.id) .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(204); @@ -497,7 +497,7 @@ describe('Relationships API', function () { }); it('DELETE /api/relationships should delete the third relationship', async function () { - const res = await request(app) + await request(app) .delete('/api/relationships/' + relationship2.stix.id + '/modified/' + relationship2.stix.modified) .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(204); From a0224df3d8f091c2fddec1d55096a5069f6a2539 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Tue, 7 Jan 2025 09:34:56 -0500 Subject: [PATCH 09/26] fix: syntax error/typo in relationships-controller.js --- app/controllers/relationships-controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/relationships-controller.js b/app/controllers/relationships-controller.js index 1370d6eb..bbf8a1bc 100644 --- a/app/controllers/relationships-controller.js +++ b/app/controllers/relationships-controller.js @@ -4,7 +4,7 @@ const relationshipsService = require('../services/relationships-service'); const logger = require('../lib/logger'); const { BadlyFormattedParameterError, InvalidQueryStringParameterError, DuplicateIdError } = require('../exceptions'); -exports.retrieveAll = async async function(req, res) { +exports.retrieveAll = async function(req, res) { const options = { offset: req.query.offset || 0, limit: req.query.limit || 0, From 1ca35c19f66522df34a3dc261454d4aa3432bcaa Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Tue, 7 Jan 2025 09:41:34 -0500 Subject: [PATCH 10/26] fix: resolve linting issues in relationships-controller.js - missing 'await' keyword in updateFull function - assigned variable 'relationship' should be 'relationships' in deleteById, causing no-unused-vars and no-undef --- app/controllers/relationships-controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/relationships-controller.js b/app/controllers/relationships-controller.js index bbf8a1bc..37f8b4b9 100644 --- a/app/controllers/relationships-controller.js +++ b/app/controllers/relationships-controller.js @@ -125,7 +125,7 @@ exports.updateFull = async function(req, res) { // Create the relationship try { - const relationship = relationshipsService.updateFull(req.params.stixId, req.params.modified, relationshipData); + const relationship = await relationshipsService.updateFull(req.params.stixId, req.params.modified, relationshipData); if (!relationship) { return res.status(404).send('Relationship not found.'); } else { @@ -157,7 +157,7 @@ exports.deleteVersionById = async function(req, res) { exports.deleteById = async function(req, res) { try { - const relationship = await relationshipsService.deleteById(req.params.stixId); + const relationships = await relationshipsService.deleteById(req.params.stixId); if (relationships.deletedCount === 0) { return res.status(404).send('Relationship not found.'); } From d0682588b14c91f2c83fe73e3a7c5d34ed481fea Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:54:02 -0500 Subject: [PATCH 11/26] fix: remove lazy loading from attack-objects-service - lazy loading not essential now that circular dependencies are eliminated - call services from globals instead of static variables --- app/services/attack-objects-service.js | 29 +++++++------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/app/services/attack-objects-service.js b/app/services/attack-objects-service.js index 6f362967..475e5605 100644 --- a/app/services/attack-objects-service.js +++ b/app/services/attack-objects-service.js @@ -1,30 +1,17 @@ 'use strict'; -const { NotImplementedError } = require('../exceptions'); const Relationship = require('../models/relationship-model'); const attackObjectsRepository = require('../repository/attack-objects-repository'); const BaseService = require('./_base.service'); -const { DatabaseError, - IdentityServiceError } = require('../exceptions'); -class AttackObjectsService extends BaseService { - - systemConfigurationService = require('./system-configuration-service'); +const identitiesService = require('./identities-service'); +const relationshipsService = require('./relationships-service'); - relationshipPrefix = 'relationship'; +const { DatabaseError, IdentityServiceError, NotImplementedError } = require('../exceptions'); - static requireServices() { - // Late binding to avoid circular dependencies - if (!AttackObjectsService.identitiesService) { - AttackObjectsService.identitiesService = require('./identities-service'); - } - if (!AttackObjectsService.relationshipsService) { - AttackObjectsService.relationshipsService = require('./relationships-service'); - } - } +class AttackObjectsService extends BaseService { async retrieveAll(options, callback) { - AttackObjectsService.requireServices(); if (AttackObjectsService.isCallback(arguments[arguments.length - 1])) { callback = arguments[arguments.length - 1]; } @@ -41,7 +28,7 @@ class AttackObjectsService extends BaseService { } try { - await AttackObjectsService.identitiesService.addCreatedByAndModifiedByIdentitiesToAll(results[0].documents); + await identitiesService.addCreatedByAndModifiedByIdentitiesToAll(results[0].documents); } catch (err) { const identityError = new IdentityServiceError({ details: err.message, @@ -63,7 +50,7 @@ class AttackObjectsService extends BaseService { includeIdentities: false, lastUpdatedBy: options.lastUpdatedBy }; - const relationships = await AttackObjectsService.relationshipsService.retrieveAll(relationshipsOptions); + const relationships = await relationshipsService.retrieveAll(relationshipsOptions); if (relationships.length > 0) { results[0].documents = results[0].documents.concat(relationships); results[0].totalCount[0].totalCount += 1; @@ -100,7 +87,7 @@ class AttackObjectsService extends BaseService { // Record that this object is part of a collection async insertCollection(stixId, modified, collectionId, collectionModified) { let attackObject; - if (stixId.startsWith(this.relationshipPrefix)) { + if (stixId.startsWith('relationship')) { // TBD: Use relationships service when that is converted to async attackObject = await Relationship.findOne({ 'stix.id': stixId, 'stix.modified': modified }); } @@ -138,4 +125,4 @@ class AttackObjectsService extends BaseService { } } -module.exports = new AttackObjectsService(null, attackObjectsRepository); \ No newline at end of file +module.exports = new AttackObjectsService('not-a-valid-type', attackObjectsRepository); \ No newline at end of file From 3d99040b58fcc86afed728b1c248e3e209f24d60 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:54:41 -0500 Subject: [PATCH 12/26] feat: add prettier for automated formatting --- .eslintrc.yml | 4 +- .prettierrc | 5 ++ package-lock.json | 169 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 5 ++ 4 files changed, 181 insertions(+), 2 deletions(-) create mode 100644 .prettierrc diff --git a/.eslintrc.yml b/.eslintrc.yml index 9d8eff76..6f95cbef 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -2,7 +2,9 @@ env: es6: true mocha: true node: true -extends: 'eslint:recommended' +extends: + - 'eslint:recommended' + - 'plugin:prettier/recommended' globals: Atomics: readonly SharedArrayBuffer: readonly diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..16f48f3f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": true, + "printWidth": 100, + "trailingComma": "all" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 48e9e1fe..22d722e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,8 @@ "c8": "^7.13.0", "cookie": "^0.5.0", "eslint": "^8.39.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", "expect": "^29.5.0", "json-diff": "^1.0.3", "mocha": "^10.2.0", @@ -51,9 +53,10 @@ "openapi-schema-validator": "^12.1.0", "parse5": "^7.1.2", "parse5-query-domtree": "^1.0.2", + "prettier": "^3.4.2", "read-json-lines-sync": "^2.2.5", "set-cookie-parser": "^2.6.0", - "snyk": "^1.1293.1", + "snyk": "^1.1150.0", "supertest": "^6.3.3" } }, @@ -1503,6 +1506,19 @@ "node": ">= 8" } }, + "node_modules/@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/@sentry-internal/tracing": { "version": "7.46.0", "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.46.0.tgz", @@ -3032,6 +3048,50 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": "*", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, "node_modules/eslint-scope": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", @@ -3485,6 +3545,13 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", @@ -5895,6 +5962,35 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/pretty-format": { "version": "29.5.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", @@ -6635,6 +6731,23 @@ "express": ">=4.0.0 || >=5.0.0-beta" } }, + "node_modules/synckit": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, "node_modules/tar-stream": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", @@ -8298,6 +8411,12 @@ "fastq": "^1.6.0" } }, + "@pkgr/core": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", + "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "dev": true + }, "@sentry-internal/tracing": { "version": "7.46.0", "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.46.0.tgz", @@ -9574,6 +9693,23 @@ } } }, + "eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "requires": {} + }, + "eslint-plugin-prettier": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.9.1" + } + }, "eslint-scope": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", @@ -9808,6 +9944,12 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true + }, "fast-fifo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", @@ -11586,6 +11728,21 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "prettier": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", + "dev": true + }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, "pretty-format": { "version": "29.5.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.5.0.tgz", @@ -12135,6 +12292,16 @@ "swagger-ui-dist": ">=4.11.0" } }, + "synckit": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", + "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "dev": true, + "requires": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + } + }, "tar-stream": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", diff --git a/package.json b/package.json index 84f0a59c..641bea02 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "coverage:cobertura": "c8 --reporter=cobertura npm test", "coverage:text": "c8 npm test", "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write .", "snyk": "snyk test --insecure --fail-on=upgradable", "start": "node ./bin/www", "test": "npm run test:openapi && npm run test:config && npm run test:api", @@ -66,6 +68,8 @@ "c8": "^7.13.0", "cookie": "^0.5.0", "eslint": "^8.39.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", "expect": "^29.5.0", "json-diff": "^1.0.3", "mocha": "^10.2.0", @@ -73,6 +77,7 @@ "openapi-schema-validator": "^12.1.0", "parse5": "^7.1.2", "parse5-query-domtree": "^1.0.2", + "prettier": "^3.4.2", "read-json-lines-sync": "^2.2.5", "set-cookie-parser": "^2.6.0", "snyk": "^1.1150.0", From deddee979ca9181badd9d9676e13943f7a2e88f0 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Tue, 7 Jan 2025 16:07:25 -0500 Subject: [PATCH 13/26] style: apply prettier formatting --- .c8rc | 8 +- .github/workflows/ci-workflow.yml | 2 +- .github/workflows/publish.yml | 25 +- .prettierrc | 9 +- .vscode/launch.json | 15 +- README.md | 82 +- app/api/definitions/components/assets.yml | 2 +- app/api/definitions/components/sessions.yml | 1 - .../definitions/components/stix-common.yml | 20 +- app/api/definitions/components/tactics.yml | 2 +- .../definitions/components/user-accounts.yml | 4 +- app/api/definitions/openapi.yml | 2 +- .../definitions/paths/collections-paths.yml | 132 +- .../paths/data-components-paths.yml | 2 +- .../definitions/paths/data-sources-paths.yml | 2 +- app/api/definitions/paths/groups-paths.yml | 2 +- .../definitions/paths/identities-paths.yml | 2 +- app/api/definitions/paths/matrices-paths.yml | 26 +- .../definitions/paths/mitigations-paths.yml | 2 +- .../definitions/paths/relationships-paths.yml | 2 +- app/api/definitions/paths/software-paths.yml | 2 +- app/api/definitions/paths/tactics-paths.yml | 2 +- app/api/definitions/paths/teams-paths.yml | 2 +- .../definitions/paths/user-accounts-paths.yml | 4 +- app/config/allowed-values.json | 64 +- app/config/config.js | 551 +++-- app/controllers/assets-controller.js | 302 ++- app/controllers/attack-objects-controller.js | 54 +- app/controllers/authn-anonymous-controller.js | 47 +- app/controllers/authn-oidc-controller.js | 44 +- app/controllers/authn-service-controller.js | 215 +- app/controllers/campaigns-controller.js | 300 +-- .../collection-bundles-controller.js | 299 +-- .../collection-indexes-controller.js | 251 ++- app/controllers/collections-controller.js | 258 +-- app/controllers/data-components-controller.js | 291 +-- app/controllers/data-sources-controller.js | 296 +-- app/controllers/groups-controller.js | 287 ++- app/controllers/health-controller.js | 29 +- app/controllers/identities-controller.js | 283 +-- .../marking-definitions-controller.js | 212 +- app/controllers/matrices-controller.js | 294 +-- app/controllers/mitigations-controller.js | 292 +-- app/controllers/notes-controller.js | 265 ++- app/controllers/recent-activity-controller.js | 46 +- app/controllers/references-controller.js | 142 +- app/controllers/relationships-controller.js | 303 +-- app/controllers/session-controller.js | 17 +- app/controllers/software-controller.js | 328 +-- app/controllers/stix-bundles-controller.js | 62 +- .../system-configuration-controller.js | 207 +- app/controllers/tactics-controller.js | 344 ++- app/controllers/teams-controller.js | 205 +- app/controllers/techniques-controller.js | 339 +-- app/controllers/user-accounts-controller.js | 403 ++-- app/exceptions/index.js | 212 +- app/index.js | 231 +- app/lib/authenticated-request.js | 86 +- app/lib/authn-anonymous.js | 88 +- app/lib/authn-basic.js | 139 +- app/lib/authn-bearer.js | 259 ++- app/lib/authn-configuration.js | 111 +- app/lib/authn-middleware.js | 55 +- app/lib/authn-oidc.js | 182 +- app/lib/authz-middleware.js | 119 +- app/lib/database-configuration.js | 415 ++-- app/lib/database-connection.js | 32 +- app/lib/database-in-memory.js | 86 +- app/lib/error-handler.js | 38 +- app/lib/linkById.js | 141 +- app/lib/logger.js | 36 +- app/lib/migration/migrate-database.js | 46 +- app/lib/migration/migration-config.js | 28 +- app/lib/model-names.js | 30 +- app/lib/regex.js | 27 +- app/lib/request-parameter-helper.js | 12 +- app/lib/requestId.js | 6 +- app/models/asset-model.js | 42 +- app/models/attack-object-model.js | 32 +- app/models/campaign-model.js | 36 +- app/models/collection-index-model.js | 60 +- app/models/collection-model.js | 36 +- app/models/data-component-model.js | 28 +- app/models/data-source-model.js | 32 +- app/models/group-model.js | 30 +- app/models/identity-model.js | 32 +- app/models/marking-definition-model.js | 31 +- app/models/matrix-model.js | 28 +- app/models/mitigation-model.js | 28 +- app/models/note-model.js | 26 +- app/models/reference-model.js | 8 +- app/models/relationship-model.js | 46 +- app/models/software-model.js | 36 +- app/models/subschemas/attack-pattern.js | 50 +- app/models/subschemas/stix-core.js | 68 +- app/models/subschemas/workspace.js | 99 +- app/models/system-configuration-model.js | 18 +- app/models/tactic-model.js | 30 +- app/models/team-model.js | 20 +- app/models/technique-model.js | 6 +- app/models/user-account-model.js | 25 +- app/repository/_abstract.repository.js | 148 +- app/repository/_base.repository.js | 359 ++-- app/repository/assets-repository.js | 163 +- app/repository/attack-objects-repository.js | 164 +- app/repository/campaigns-repository.js | 4 +- app/repository/data-components-repository.js | 2 +- app/repository/data-sources-repository.js | 2 +- app/repository/groups-repository.js | 2 +- app/repository/identities-repository.js | 4 +- .../marking-definitions-repository.js | 16 +- app/repository/matrix-repository.js | 4 +- app/repository/mitigations-repository.js | 4 +- app/repository/notes-repository.js | 130 +- app/repository/references-repository.js | 186 +- app/repository/relationships-repository.js | 179 +- app/repository/software-repository.js | 4 +- .../system-configurations-repository.js | 43 +- app/repository/tactics-repository.js | 2 +- app/repository/teams-repository.js | 253 +-- app/repository/techniques-repository.js | 4 +- app/repository/user-accounts-repository.js | 323 ++- app/routes/assets-routes.js | 63 +- app/routes/attack-objects-routes.js | 13 +- app/routes/authn-anonymous-routes.js | 25 +- app/routes/authn-oidc-routes.js | 32 +- app/routes/authn-service-routes.js | 22 +- app/routes/campaigns-routes.js | 67 +- app/routes/collection-bundles-routes.js | 23 +- app/routes/collection-indexes-routes.js | 68 +- app/routes/collections-routes.js | 64 +- app/routes/data-components-routes.js | 75 +- app/routes/data-sources-routes.js | 71 +- app/routes/groups-routes.js | 63 +- app/routes/health-routes.js | 22 +- app/routes/identities-routes.js | 67 +- app/routes/index.js | 22 +- app/routes/marking-definitions-routes.js | 52 +- app/routes/matrices-routes.js | 82 +- app/routes/mitigations-routes.js | 71 +- app/routes/notes-routes.js | 67 +- app/routes/recent-activity-routes.js | 13 +- app/routes/references-routes.js | 35 +- app/routes/relationships-routes.js | 75 +- app/routes/session-routes.js | 6 +- app/routes/software-routes.js | 63 +- app/routes/stix-bundles-routes.js | 16 +- app/routes/system-configuration-routes.js | 104 +- app/routes/tactics-routes.js | 82 +- app/routes/teams-routes.js | 47 +- app/routes/techniques-routes.js | 86 +- app/routes/user-accounts-routes.js | 68 +- app/scheduler/scheduler.js | 408 ++-- app/services/_abstract.service.js | 30 +- app/services/_base.service.js | 743 +++---- app/services/assets-service.js | 4 +- app/services/attack-objects-service.js | 209 +- app/services/authentication-service.js | 243 ++- app/services/campaigns-service.js | 4 +- app/services/collection-bundles-service.js | 1601 +++++++------- app/services/collection-indexes-service.js | 288 ++- app/services/collections-service.js | 846 ++++---- app/services/data-components-service.js | 2 +- app/services/data-sources-service.js | 266 +-- app/services/groups-service.js | 4 +- app/services/identities-service.js | 224 +- app/services/marking-definitions-service.js | 210 +- app/services/matrices-service.js | 184 +- app/services/mitigations-service.js | 4 +- app/services/notes-service.js | 75 +- app/services/recent-activity-service.js | 220 +- app/services/references-service.js | 51 +- app/services/relationships-service.js | 239 ++- app/services/software-service.js | 124 +- app/services/stix-bundles-service.js | 1114 +++++----- app/services/system-configuration-service.js | 417 ++-- app/services/tactics-service.js | 128 +- app/services/teams-service.js | 306 ++- app/services/techniques-service.js | 130 +- app/services/user-accounts-service.js | 458 ++-- app/tests/api/assets/assets.spec.js | 702 +++--- .../api/attack-objects/attack-objects-1.json | 163 +- .../api/attack-objects/attack-objects-2.json | 24 +- .../attack-objects-pagination.spec.js | 50 +- .../api/attack-objects/attack-objects.spec.js | 501 +++-- app/tests/api/campaigns/campaigns.spec.js | 883 ++++---- .../collection-bundles.spec.js | 1886 ++++++++--------- .../collection-indexes.spec.js | 523 ++--- app/tests/api/collections/collections.spec.js | 1359 ++++++------ .../data-components-pagination.spec.js | 34 +- .../data-components/data-components.spec.js | 666 +++--- .../data-sources-pagination.spec.js | 34 +- .../api/data-sources/data-sources.spec.js | 815 ++++--- .../groups/groups-input-validation.spec.js | 287 ++- .../api/groups/groups-pagination.spec.js | 34 +- app/tests/api/groups/groups.query.json | 6 +- app/tests/api/groups/groups.query.spec.js | 616 +++--- app/tests/api/groups/groups.spec.js | 909 ++++---- app/tests/api/identities/identities.spec.js | 626 +++--- .../marking-definitions.spec.js | 373 ++-- app/tests/api/matrices/matrices.spec.js | 647 +++--- .../mitigations-pagination.spec.js | 36 +- app/tests/api/mitigations/mitigations.spec.js | 663 +++--- app/tests/api/notes/notes.spec.js | 884 ++++---- app/tests/api/references/references.spec.js | 548 ++--- .../relationships-pagination.spec.js | 40 +- .../api/relationships/relationships.spec.js | 980 +++++---- app/tests/api/session/session.spec.js | 98 +- .../api/software/software-pagination.spec.js | 55 +- app/tests/api/software/software.spec.js | 772 ++++--- .../api/stix-bundles/stix-bundles.spec.js | 1557 +++++++------- .../create-object-identity.spec.js | 376 ++-- .../system-configuration.spec.js | 526 ++--- app/tests/api/tactics/tactics.spec.js | 720 ++++--- app/tests/api/tactics/tactics.techniques.json | 120 +- .../api/tactics/tactics.techniques.spec.js | 203 +- app/tests/api/teams/teams-invalid.spec.js | 62 +- app/tests/api/teams/teams.spec.js | 372 ++-- app/tests/api/teams/teams.valid.json | 2 +- .../techniques/techniques-pagination.spec.js | 48 +- .../api/techniques/techniques.query.json | 20 +- .../api/techniques/techniques.query.spec.js | 586 +++-- app/tests/api/techniques/techniques.spec.js | 831 ++++---- .../api/techniques/techniques.tactics.json | 88 +- .../api/techniques/techniques.tactics.spec.js | 238 +-- .../user-accounts-invalid.spec.js | 62 +- .../api/user-accounts/user-accounts.spec.js | 541 +++-- app/tests/authn/README.md | 1 - app/tests/authn/anonymous-authn.spec.js | 266 ++- app/tests/authn/basic-apikey-service.spec.js | 174 +- .../authn/challenge-apikey-service.spec.js | 341 ++- app/tests/authn/oidc-authn.spec.js | 529 +++-- .../oidc-client-credentials-service.spec.js | 208 +- app/tests/authn/oidc-register.spec.js | 624 +++--- app/tests/config/config.spec.js | 77 +- app/tests/config/test-config.json | 2 +- app/tests/fuzz/user-accounts-fuzz.spec.js | 232 +- .../collection-bundles-enterprise.spec.js | 240 +-- app/tests/integration-test/README.md | 13 +- app/tests/integration-test/initialize-data.js | 81 +- .../integration-test/mock-data/blue-v1.json | 16 +- .../integration-test/mock-data/blue-v2.json | 20 +- .../integration-test/mock-data/green-v1.json | 16 +- .../integration-test/mock-data/red-v1.json | 16 +- .../mock-remote-host/index.js | 87 +- .../integration-test/update-subscription.js | 71 +- app/tests/openapi/validate-open-api.spec.js | 24 +- app/tests/shared/keycloak.js | 361 ++-- app/tests/shared/login.js | 24 +- app/tests/shared/pagination.js | 514 ++--- bin/www | 223 +- docs/authentication.md | 43 +- docs/data-model.md | 6 +- docs/docker.md | 19 +- docs/link-by-id.md | 90 +- docs/user-management.md | 41 +- .../20220705205741-move-relationships.js | 128 +- migrations/sample-migration.js | 22 +- resources/postman-exports/README.md | 1 + ...workbench-rest-api.postman_collection.json | 498 ++--- ...ce-authentication.postman_environment.json | 76 +- .../navigator-basic-apikey.json | 22 +- scripts/clearDatabase.js | 28 +- scripts/configureKeycloak.js | 115 +- scripts/loadBundle.js | 93 +- scripts/validateBundle.js | 79 +- 266 files changed, 24651 insertions(+), 25036 deletions(-) diff --git a/.c8rc b/.c8rc index d9e3c703..e092ff2f 100644 --- a/.c8rc +++ b/.c8rc @@ -1,9 +1,5 @@ { "all": true, - "include": [ - "app/**/*.js" - ], - "exclude": [ - "app/tests/**/*.js" - ] + "include": ["app/**/*.js"], + "exclude": ["app/tests/**/*.js"] } diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 3a5cf55d..d7c745b2 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -1,6 +1,6 @@ name: run static checks -on: +on: push: branches: [master, develop] pull_request: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6019e830..b9ffb4d8 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,20 +3,18 @@ name: Build and Publish Docker Image on: push: branches: - - "master" - - "develop" + - 'master' + - 'develop' tags: - - "v*.*.*" + - 'v*.*.*' jobs: docker: runs-on: ubuntu-latest steps: - - - name: Checkout + - name: Checkout uses: actions/checkout@v3 - - - name: Set Docker Image and Tags + - name: Set Docker Image and Tags id: meta uses: docker/metadata-action@v4 with: @@ -31,27 +29,24 @@ jobs: type=semver,pattern=v{{major}}.{{minor}}.{{patch}} # set git short commit as Docker tag (e.g., sha-ad132f5) type=sha - - - name: Set up Docker Buildx + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - - - name: Login to GHCR + - name: Login to GHCR if: github.event_name != 'pull_request' uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push + - name: Build and push uses: docker/build-push-action@v4 with: context: . platforms: linux/amd64,linux/arm64 push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} + tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-args: | BUILDTIME=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }} VERSION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }} - REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} \ No newline at end of file + REVISION=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.revision'] }} diff --git a/.prettierrc b/.prettierrc index 16f48f3f..3b74db8f 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,5 +1,6 @@ { - "singleQuote": true, - "printWidth": 100, - "trailingComma": "all" -} \ No newline at end of file + "singleQuote": true, + "printWidth": 100, + "trailingComma": "all", + "tabWidth": 2 +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 90b62f3c..fecc94ef 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,9 +8,7 @@ "type": "node", "request": "launch", "name": "Launch Program", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "program": "${workspaceFolder}/bin/www", "outputCapture": "std", "envFile": "${workspaceFolder}/.env" @@ -19,18 +17,13 @@ "type": "node", "request": "launch", "name": "Run Regression Tests", - "skipFiles": [ - "/**" - ], + "skipFiles": ["/**"], "runtimeExecutable": "npm", - "args": [ - "run", - "test" - ], + "args": ["run", "test"], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", "outputCapture": "std", "envFile": "${workspaceFolder}/.env" } ] -} \ No newline at end of file +} diff --git a/README.md b/README.md index e0efe502..85372deb 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,20 @@ # ATT&CK Workbench REST API -The ATT&CK Workbench is an application allowing users to **explore**, **create**, **annotate**, and **share** extensions of the MITRE ATT&CK® knowledge base. +The ATT&CK Workbench is an application allowing users to **explore**, **create**, **annotate**, and **share** extensions of the MITRE ATT&CK® knowledge base. -This repository contains the REST API service for storing, querying, and editing ATT&CK objects. It is a Node.js application that uses a MongoDB database for persisting data. +This repository contains the REST API service for storing, querying, and editing ATT&CK objects. It is a Node.js application that uses a MongoDB database for persisting data. The ATT&CK Workbench application requires additional components for full operation. The [ATT&CK Workbench Frontend](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend) repository contains the full documentation of the scope and function of the project. See the [install and run](#install-and-run) instructions for more details about setting up the entire project. ## REST API Documentation When running with the NODE_ENV environment variable set to `development`, the app hosts a description of the REST API using the Swagger UI module. -The REST API documentation can be viewed using a browser at the path `/api-docs`. +The REST API documentation can be viewed using a browser at the path `/api-docs`. For a basic installation on the local machine this documentation can be accessed at `http://localhost:3000/api-docs`. The [docs](/docs/README.md) folder contains additional documentation about using the REST API: + - [changelog](/docs/changelog.md): records of updates to the REST API. - [workbench data model](/docs/data-model.md): additional information about data model of objects stored via the REST API. - [standalone docker installation](/docs/docker.md): instructions for setting up the REST API via docker. Note that this is not the same as the full [ATT&CK Workbench Docker Installation](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/blob/master/docs/docker-compose.md). @@ -22,16 +23,19 @@ The [docs](/docs/README.md) folder contains additional documentation about using ## Install and run The ATT&CK Workbench application is made up of several repositories. For the full application to operate each needs to be running at the same time. The [docker install instructions](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/blob/master/docs/docker-compose.md) will install all components and is recommended for most deployments. -- [ATT&CK Workbench Frontend](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend) - + +- [ATT&CK Workbench Frontend](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend) + The front-end user interface for the ATT&CK Workbench tool, and the primary interface through which the knowledge base is accessed. + - [ATT&CK Workbench REST API](https://github.com/center-for-threat-informed-defense/attack-workbench-rest-api) (this repository) REST API service for storing, querying and editing ATT&CK objects. -The manual install instructions in each repository describe how each component to be deployed to a separate machine or with customized settings. +The manual install instructions in each repository describe how each component to be deployed to a separate machine or with customized settings. ### Installing using Docker + Please refer to our [Docker install instructions](https://github.com/center-for-threat-informed-defense/attack-workbench-frontend/blob/master/docs/docker-compose.md) for information on installing and deploying the app using Docker. The docker setup is the easiest way to deploy the application. ### Manual Installation @@ -40,7 +44,7 @@ Please refer to our [Docker install instructions](https://github.com/center-for- - [Node.js](https://nodejs.org) version `18.12.1` or greater - An instance of [MongoDB](https://www.mongodb.com/) version `4.4.x` or greater - + #### Installation ##### Step 1. Clone the git repository @@ -66,17 +70,17 @@ Note that any values set in a configuration file take precedence over values set ###### Using Environment Variables -| name | required | default | description | -|--------------------------------------|----------|---------------|-----------------------------------------------------------| -| **PORT** | no | `3000` | Port the HTTP server should listen on | -| **ENABLE_CORS_ANY_ORIGIN** | no | `true` | Allows requests from any domain to access the REST API endpoints | -| **NODE_ENV** | no | `development` | Environment that the app is running in | -| **DATABASE_URL** | yes | none | URL of the MongoDB server | -| **AUTHN_MECHANISM** | no | `anonymous` | Mechanism to use for authenticating users | -| **DEFAULT_INTERVAL** | no | `300` | How often collection indexes should check for updates (in seconds) | -| **JSON_CONFIG_PATH** | no | `` | Location of a JSON file containing configuration values | -| **LOG_LEVEL** | no | `info` | Level of messages to be written to the log (error, warn, http, info, verbose, debug) | -| **WB_REST_STATIC_MARKING_DEFS_PATH** | no | `./app/lib/default-static-marking-definitions/` | Path to a directory containing static marking definitions | +| name | required | default | description | +| ------------------------------------ | -------- | ----------------------------------------------- | ------------------------------------------------------------------------------------ | +| **PORT** | no | `3000` | Port the HTTP server should listen on | +| **ENABLE_CORS_ANY_ORIGIN** | no | `true` | Allows requests from any domain to access the REST API endpoints | +| **NODE_ENV** | no | `development` | Environment that the app is running in | +| **DATABASE_URL** | yes | none | URL of the MongoDB server | +| **AUTHN_MECHANISM** | no | `anonymous` | Mechanism to use for authenticating users | +| **DEFAULT_INTERVAL** | no | `300` | How often collection indexes should check for updates (in seconds) | +| **JSON_CONFIG_PATH** | no | `` | Location of a JSON file containing configuration values | +| **LOG_LEVEL** | no | `info` | Level of messages to be written to the log (error, warn, http, info, verbose, debug) | +| **WB_REST_STATIC_MARKING_DEFS_PATH** | no | `./app/lib/default-static-marking-definitions/` | Path to a directory containing static marking definitions | A typical value for DATABASE_URL when running on a development machine is `mongodb://localhost/attack-workspace`. This assumes that a MongoDB server is running on the same machine and is listening on the standard port of 27017. @@ -86,14 +90,14 @@ The MongoDB server can be running natively or in a Docker container. If the `JSON_CONFIG_PATH` environment variable is set, the app will also read configuration settings from a JSON file at that location. -| name | type | corresponding environment variable | -|-------------------------------------|----------|------------------------------------| -| **server.port** | int | PORT | -| **server.enableCorsAnyOrigin** | boolean | ENABLE_CORS_ANY_ORIGIN | -| **app.env** | string | NODE_ENV | -| **database.url** | string | DATABASE_URL | -| **collectionIndex.defaultInterval** | int | DEFAULT_INTERVAL | -| **logging.logLevel** | string | LOG_LEVEL | +| name | type | corresponding environment variable | +| ----------------------------------- | ------- | ---------------------------------- | +| **server.port** | int | PORT | +| **server.enableCorsAnyOrigin** | boolean | ENABLE_CORS_ANY_ORIGIN | +| **app.env** | string | NODE_ENV | +| **database.url** | string | DATABASE_URL | +| **collectionIndex.defaultInterval** | int | DEFAULT_INTERVAL | +| **logging.logLevel** | string | LOG_LEVEL | Sample configuration file setting the server port and database url: @@ -123,17 +127,17 @@ Workbench supports OIDC authentication for users, allowing you to integrate Work In order to use OIDC authentication, your Workbench instance must be registered with your organization's OIDC authentication server. The details depend on your authentication server, but the following values should cover most of what you need: -* Workbench uses the *Authorization Code Flow* for authenticating users -* Claims: +- Workbench uses the _Authorization Code Flow_ for authenticating users +- Claims: -| claim | required | description | -|------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------| -| **email** | yes | Identifies the user account associated with an authenticated user | +| claim | required | description | +| ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| **email** | yes | Identifies the user account associated with an authenticated user | | **preferred_username** | no | If present, the `preferred_username` claim is used to set the `name` property of the user account when the user initially registers with Workbench | -| **name** | no | If present, the `name` claim is used to set the `displayName` property of the user account when the user initially registers with Workbench | +| **name** | no | If present, the `name` claim is used to set the `displayName` property of the user account when the user initially registers with Workbench | -* Grant Types: *Client Credentials*, *Authorization Code* and *Refresh Token* -* Redirect URL: `/api/authn/oidc/callback` +- Grant Types: _Client Credentials_, _Authorization Code_ and _Refresh Token_ +- Redirect URL: `/api/authn/oidc/callback` After registering with the OIDC authentication system, you will need the `client_id` and `client_secret` assigned as part of that process. You will also need the Issuer URL for the OIDC Identity Server. @@ -143,7 +147,7 @@ You will also need the Issuer URL for the OIDC Identity Server. Configuring Workbench to use OIDC can be done using environment variables or the corresponding properties in a configuration file. | environment variable | required | description | configuration file property name | -|--------------------------------|----------|-------------------------------------------------------------------------------------------------------|----------------------------------| +| ------------------------------ | -------- | ----------------------------------------------------------------------------------------------------- | -------------------------------- | | **AUTHN_MECHANISM** | yes | Must be set to `oidc` | userAuthn.mechanism | | **AUTHN_OIDC_CLIENT_ID** | yes | Client ID assigned to the Workbench instance when registering with the OIDC authentication system | userAuthn.oidc.clientId | | **AUTHN_OIDC_CLIENT_SECRET** | yes | Client secret assigned to the Workbench instance when registering with the OIDC authentication system | userAuthn.oidc.clientSecret | @@ -192,15 +196,15 @@ This project is configured to run a Github workflow when one or more commits are The workflow is defined in `.github/workflows/ci-workflow.yml` -## Notice +## Notice Copyright 2020-2024 MITRE Engenuity. Approved for public release. Document number CT0020 -Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 +http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. This project makes use of ATT&CK® diff --git a/app/api/definitions/components/assets.yml b/app/api/definitions/components/assets.yml index bb66668d..e1b14d54 100644 --- a/app/api/definitions/components/assets.yml +++ b/app/api/definitions/components/assets.yml @@ -72,4 +72,4 @@ components: description: type: string required: - - name \ No newline at end of file + - name diff --git a/app/api/definitions/components/sessions.yml b/app/api/definitions/components/sessions.yml index 1c1b5837..b8539b08 100644 --- a/app/api/definitions/components/sessions.yml +++ b/app/api/definitions/components/sessions.yml @@ -17,4 +17,3 @@ components: $ref: 'identities.yml#/components/schemas/identity' registered: type: boolean - diff --git a/app/api/definitions/components/stix-common.yml b/app/api/definitions/components/stix-common.yml index 4260bb82..5f6a26e3 100644 --- a/app/api/definitions/components/stix-common.yml +++ b/app/api/definitions/components/stix-common.yml @@ -11,7 +11,24 @@ components: # STIX common properties type: type: string - enum: ['attack-pattern', 'x-mitre-tactic', 'intrusion-set', 'campaign', 'tool', 'malware', 'course-of-action', 'x-mitre-matrix', 'identity', 'x-mitre-data-source', 'x-mitre-data-component', 'x-mitre-asset', 'relationship', 'note', 'x-mitre-collection'] + enum: + [ + 'attack-pattern', + 'x-mitre-tactic', + 'intrusion-set', + 'campaign', + 'tool', + 'malware', + 'course-of-action', + 'x-mitre-matrix', + 'identity', + 'x-mitre-data-source', + 'x-mitre-data-component', + 'x-mitre-asset', + 'relationship', + 'note', + 'x-mitre-collection', + ] spec_version: type: string pattern: '^2.1$' @@ -89,4 +106,3 @@ components: type: string required: - source_name - diff --git a/app/api/definitions/components/tactics.yml b/app/api/definitions/components/tactics.yml index 8a6f3c7b..c28a5fd2 100644 --- a/app/api/definitions/components/tactics.yml +++ b/app/api/definitions/components/tactics.yml @@ -24,7 +24,7 @@ components: example: 'Collection' description: type: string - example: "This is a tactic." + example: 'This is a tactic.' # ATT&CK custom properties x_mitre_modified_by_ref: type: string diff --git a/app/api/definitions/components/user-accounts.yml b/app/api/definitions/components/user-accounts.yml index 591ec298..0a3c28fa 100644 --- a/app/api/definitions/components/user-accounts.yml +++ b/app/api/definitions/components/user-accounts.yml @@ -16,11 +16,11 @@ components: type: string status: type: string - enum: [ 'pending', 'active', 'inactive' ] + enum: ['pending', 'active', 'inactive'] example: 'active' role: type: string - enum: [ 'visitor', 'editor', 'admin' ] + enum: ['visitor', 'editor', 'admin'] example: 'editor' created: type: string diff --git a/app/api/definitions/openapi.yml b/app/api/definitions/openapi.yml index f122fe48..fc0bbdd8 100644 --- a/app/api/definitions/openapi.yml +++ b/app/api/definitions/openapi.yml @@ -297,7 +297,7 @@ paths: /api/user-accounts/{id}: $ref: 'paths/user-accounts-paths.yml#/paths/~1api~1user-accounts~1{id}' - + /api/user-accounts/{id}/teams: $ref: 'paths/user-accounts-paths.yml#/paths/~1api~1user-accounts~1{id}~1teams' diff --git a/app/api/definitions/paths/collections-paths.yml b/app/api/definitions/paths/collections-paths.yml index a6cc94c1..0c0d214e 100644 --- a/app/api/definitions/paths/collections-paths.yml +++ b/app/api/definitions/paths/collections-paths.yml @@ -192,71 +192,71 @@ paths: description: 'A collection with the requested STIX id was not found.' /api/collections/{stixId}/modified/{modified}: - get: - summary: 'Gets the version of a collection matching the STIX id and modified date' - operationId: 'collection-get-by-id-and-modified' - description: | - This endpoint gets a single version of a collection from the workspace, identified by its STIX id and modified date. - tags: - - 'Collections' - parameters: - - name: stixId - in: path - description: 'STIX id of the collection to retrieve' - required: true - schema: - type: string - - name: modified - in: path - description: 'modified date of the collection to retrieve' - required: true - schema: - type: string - - name: retrieveContents - in: query - description: | - Retrieve the objects that are referenced by the collection - schema: - type: boolean - default: false - responses: - '200': - description: 'The version of a collection matching the STIX id and modified date.' - content: - application/json: - schema: - $ref: '../components/collections.yml#/components/schemas/collection' - '404': - description: 'A collection with the requested STIX id and modified date was not found.' - delete: - summary: 'Delete a version of a collection' - operationId: 'collection-delete-by-id-and-modified' + get: + summary: 'Gets the version of a collection matching the STIX id and modified date' + operationId: 'collection-get-by-id-and-modified' + description: | + This endpoint gets a single version of a collection from the workspace, identified by its STIX id and modified date. + tags: + - 'Collections' + parameters: + - name: stixId + in: path + description: 'STIX id of the collection to retrieve' + required: true + schema: + type: string + - name: modified + in: path + description: 'modified date of the collection to retrieve' + required: true + schema: + type: string + - name: retrieveContents + in: query description: | - This endpoint deletes one versions of a collection from the workspace. - tags: - - 'Collections' - parameters: - - name: stixId - in: path - description: 'STIX id of the collections to delete' - required: true - schema: - type: string - - name: modified - in: path - description: 'modified date of the collection to delete' - required: true - schema: - type: string - - name: deleteAllContents - in: query - description: | - Delete all of the objects referenced in x_mitre_contents. Note: If an object is referenced in another collection, the object will not be + Retrieve the objects that are referenced by the collection + schema: + type: boolean + default: false + responses: + '200': + description: 'The version of a collection matching the STIX id and modified date.' + content: + application/json: schema: - type: boolean - default: false - responses: - '204': - description: 'The collection was successfully deleted.' - '404': - description: 'A collection with the requested STIX id was not found.' + $ref: '../components/collections.yml#/components/schemas/collection' + '404': + description: 'A collection with the requested STIX id and modified date was not found.' + delete: + summary: 'Delete a version of a collection' + operationId: 'collection-delete-by-id-and-modified' + description: | + This endpoint deletes one versions of a collection from the workspace. + tags: + - 'Collections' + parameters: + - name: stixId + in: path + description: 'STIX id of the collections to delete' + required: true + schema: + type: string + - name: modified + in: path + description: 'modified date of the collection to delete' + required: true + schema: + type: string + - name: deleteAllContents + in: query + description: | + Delete all of the objects referenced in x_mitre_contents. Note: If an object is referenced in another collection, the object will not be + schema: + type: boolean + default: false + responses: + '204': + description: 'The collection was successfully deleted.' + '404': + description: 'A collection with the requested STIX id was not found.' diff --git a/app/api/definitions/paths/data-components-paths.yml b/app/api/definitions/paths/data-components-paths.yml index 6fd24435..1a9f662d 100644 --- a/app/api/definitions/paths/data-components-paths.yml +++ b/app/api/definitions/paths/data-components-paths.yml @@ -174,7 +174,7 @@ paths: description: 'All the data component versions were successfully deleted.' '404': description: 'A data component with the requested STIX id was not found.' - + /api/data-components/{stixId}/modified/{modified}: get: summary: 'Gets the version of a data component matching the STIX id and modified date' diff --git a/app/api/definitions/paths/data-sources-paths.yml b/app/api/definitions/paths/data-sources-paths.yml index f45d14ce..1d9b5133 100644 --- a/app/api/definitions/paths/data-sources-paths.yml +++ b/app/api/definitions/paths/data-sources-paths.yml @@ -204,7 +204,7 @@ paths: responses: '204': description: 'All the data source versions were successfully deleted.' - + /api/data-sources/{stixId}/modified/{modified}: get: summary: 'Gets the version of a data source matching the STIX id and modified date' diff --git a/app/api/definitions/paths/groups-paths.yml b/app/api/definitions/paths/groups-paths.yml index c9e31c30..b42c007b 100644 --- a/app/api/definitions/paths/groups-paths.yml +++ b/app/api/definitions/paths/groups-paths.yml @@ -174,7 +174,7 @@ paths: description: 'All the group versions were successfully deleted.' '404': description: 'A group with the requested STIX id was not found.' - + /api/groups/{stixId}/modified/{modified}: get: summary: 'Gets the version of a group matching the STIX id and modified date' diff --git a/app/api/definitions/paths/identities-paths.yml b/app/api/definitions/paths/identities-paths.yml index 83c583b0..3e14729e 100644 --- a/app/api/definitions/paths/identities-paths.yml +++ b/app/api/definitions/paths/identities-paths.yml @@ -153,7 +153,7 @@ paths: responses: '204': description: 'All the identity versions were successfully deleted.' - + /api/identities/{stixId}/modified/{modified}: get: summary: 'Gets the version of a identity matching the STIX id and modified date' diff --git a/app/api/definitions/paths/matrices-paths.yml b/app/api/definitions/paths/matrices-paths.yml index d4d03625..237b3219 100644 --- a/app/api/definitions/paths/matrices-paths.yml +++ b/app/api/definitions/paths/matrices-paths.yml @@ -174,7 +174,7 @@ paths: description: 'All the matrix versions were successfully deleted.' '404': description: 'A matrix with the requested STIX id was not found.' - + /api/matrices/{stixId}/modified/{modified}: get: summary: 'Gets the version of a matrix matching the STIX id and modified date' @@ -278,18 +278,18 @@ paths: 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 + - 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.' diff --git a/app/api/definitions/paths/mitigations-paths.yml b/app/api/definitions/paths/mitigations-paths.yml index a773e6d1..de7e83c1 100644 --- a/app/api/definitions/paths/mitigations-paths.yml +++ b/app/api/definitions/paths/mitigations-paths.yml @@ -186,7 +186,7 @@ paths: description: 'All the mitigation versions were successfully deleted.' '404': description: 'A mitigation with the requested STIX id was not found.' - + /api/mitigations/{stixId}/modified/{modified}: get: summary: 'Gets the version of a mitigation matching the STIX id and modified date' diff --git a/app/api/definitions/paths/relationships-paths.yml b/app/api/definitions/paths/relationships-paths.yml index 62ecc75e..d28cc8bb 100644 --- a/app/api/definitions/paths/relationships-paths.yml +++ b/app/api/definitions/paths/relationships-paths.yml @@ -243,7 +243,7 @@ paths: description: 'All the relationship versions were successfully deleted.' '404': description: 'A relationship with the requested STIX id was not found.' - + /api/relationships/{stixId}/modified/{modified}: get: summary: 'Gets the version of a relationship matching the STIX id and modified date' diff --git a/app/api/definitions/paths/software-paths.yml b/app/api/definitions/paths/software-paths.yml index efc3ac7a..922b869b 100644 --- a/app/api/definitions/paths/software-paths.yml +++ b/app/api/definitions/paths/software-paths.yml @@ -198,7 +198,7 @@ paths: description: 'All the tactic versions were successfully deleted.' '404': description: 'A tactic with the requested STIX id was not found.' - + /api/software/{stixId}/modified/{modified}: get: summary: 'Gets the version of a software object matching the STIX id and modified date' diff --git a/app/api/definitions/paths/tactics-paths.yml b/app/api/definitions/paths/tactics-paths.yml index 551ca023..8ada2627 100644 --- a/app/api/definitions/paths/tactics-paths.yml +++ b/app/api/definitions/paths/tactics-paths.yml @@ -186,7 +186,7 @@ paths: description: 'All the tactic versions were successfully deleted.' '404': description: 'A tactic with the requested STIX id was not found.' - + /api/tactics/{stixId}/modified/{modified}: get: summary: 'Gets the version of a tactic matching the STIX id and modified date' diff --git a/app/api/definitions/paths/teams-paths.yml b/app/api/definitions/paths/teams-paths.yml index 641994ed..a33152ba 100644 --- a/app/api/definitions/paths/teams-paths.yml +++ b/app/api/definitions/paths/teams-paths.yml @@ -214,4 +214,4 @@ paths: schema: type: array items: - $ref: '../components/user-accounts.yml#/components/schemas/user-account' \ No newline at end of file + $ref: '../components/user-accounts.yml#/components/schemas/user-account' diff --git a/app/api/definitions/paths/user-accounts-paths.yml b/app/api/definitions/paths/user-accounts-paths.yml index 268240ab..f1f8799b 100644 --- a/app/api/definitions/paths/user-accounts-paths.yml +++ b/app/api/definitions/paths/user-accounts-paths.yml @@ -210,7 +210,7 @@ paths: $ref: '../components/user-accounts.yml#/components/schemas/user-account' '400': description: 'Missing or invalid parameters were provided. The user account was not created.' - + /api/user-accounts/{id}/teams: get: summary: 'Get a list of teams the user account is associated with' @@ -255,7 +255,7 @@ paths: description: 'A list of teams which the user is associated with.' content: application/json: - schema: + schema: type: array items: $ref: '../components/teams.yml#/components/schemas/team' diff --git a/app/config/allowed-values.json b/app/config/allowed-values.json index dfa30588..b5bf7608 100644 --- a/app/config/allowed-values.json +++ b/app/config/allowed-values.json @@ -22,10 +22,7 @@ }, { "domainName": "mobile-attack", - "allowedValues": [ - "Android", - "iOS" - ] + "allowedValues": ["Android", "iOS"] }, { "domainName": "ics-attack", @@ -62,10 +59,7 @@ "domains": [ { "domainName": "enterprise-attack", - "allowedValues": [ - "Availability", - "Integrity" - ] + "allowedValues": ["Availability", "Integrity"] } ] }, @@ -74,13 +68,7 @@ "domains": [ { "domainName": "enterprise-attack", - "allowedValues": [ - "Administrator", - "root", - "SYSTEM", - "User", - "Remote Desktop Users" - ] + "allowedValues": ["Administrator", "root", "SYSTEM", "User", "Remote Desktop Users"] } ] }, @@ -89,13 +77,7 @@ "domains": [ { "domainName": "enterprise-attack", - "allowedValues": [ - "Administrator", - "root", - "SYSTEM", - "User", - "Remote Desktop Users" - ] + "allowedValues": ["Administrator", "root", "SYSTEM", "User", "Remote Desktop Users"] } ] }, @@ -167,10 +149,7 @@ }, { "domainName": "mobile-attack", - "allowedValues": [ - "Android", - "iOS" - ] + "allowedValues": ["Android", "iOS"] }, { "domainName": "ics-attack", @@ -197,28 +176,15 @@ "domains": [ { "domainName": "enterprise-attack", - "allowedValues": [ - "Cloud Control Plane", - "Container", - "Host", - "Network", - "OSINT" - ] + "allowedValues": ["Cloud Control Plane", "Container", "Host", "Network", "OSINT"] }, { "domainName": "mobile-attack", - "allowedValues": [ - "Device", - "Report" - ] + "allowedValues": ["Device", "Report"] }, { "domainName": "ics-attack", - "allowedValues": [ - "Cloud Control Plane", - "Host", - "Network" - ] + "allowedValues": ["Cloud Control Plane", "Host", "Network"] } ] }, @@ -241,10 +207,7 @@ }, { "domainName": "mobile-attack", - "allowedValues": [ - "Android", - "iOS" - ] + "allowedValues": ["Android", "iOS"] }, { "domainName": "ics-attack", @@ -294,17 +257,10 @@ "domains": [ { "domainName": "ics-attack", - "allowedValues": [ - "Windows", - "Linux", - "Network", - "Embedded", - "Cloud" - ] + "allowedValues": ["Windows", "Linux", "Network", "Embedded", "Cloud"] } ] } ] } ] - diff --git a/app/config/config.js b/app/config/config.js index dd90c0d6..8306e3e6 100644 --- a/app/config/config.js +++ b/app/config/config.js @@ -11,12 +11,12 @@ const packageJson = require('../../package.json'); // - Sessions cannot be shared across server instances // Setting the SESSION_SECRET environment variable will override this generated value function generateSecret() { - const stringBase = 'base64'; - const byteLength = 48; - const buffer = crypto.randomBytes(byteLength); - const secret = buffer.toString(stringBase); + const stringBase = 'base64'; + const byteLength = 48; + const buffer = crypto.randomBytes(byteLength); + const secret = buffer.toString(stringBase); - return secret; + return secret; } const defaultSessionSecret = generateSecret(); @@ -30,311 +30,310 @@ convict.addFormat(enumFormat('service-role', serviceRoleValues, true)); // Creates a new convict format for a list of enumerated values function enumFormat(name, values, coerceLower) { - return { - name, - validate: function (val) { - if (!values.includes(val)) { - throw new Error(`Invalid ${ name } value`); - } - }, - coerce: function (val) { - if (coerceLower) { - return val.toLowerCase(); - } - else { - return val; - } - } - } + return { + name, + validate: function (val) { + if (!values.includes(val)) { + throw new Error(`Invalid ${name} value`); + } + }, + coerce: function (val) { + if (coerceLower) { + return val.toLowerCase(); + } else { + return val; + } + }, + }; } function arrayFormat(name) { - return { - name, - validate: function(entries, schema) { - if (!Array.isArray(entries)) { - throw new Error('Property must be of type Array'); - } + return { + name, + validate: function (entries, schema) { + if (!Array.isArray(entries)) { + throw new Error('Property must be of type Array'); + } - for (const entry of entries) { - convict(schema.children).load(entry).validate(); - } - } - } + for (const entry of entries) { + convict(schema.children).load(entry).validate(); + } + }, + }; } convict.addFormat(arrayFormat('oidc-client')); convict.addFormat(arrayFormat('service-account')); function loadConfig() { - const config = convict({ - server: { - port: { - doc: 'Port the HTTP server should listen on', - format: 'int', - default: 3000, - env: 'PORT' - }, - enableCorsAnyOrigin: { - doc: 'Access-Control-Allow-Origin will be set to the wildcard (*), allowing requests from any domain to access the REST API endpoints', - format: Boolean, - default: true, - env: 'ENABLE_CORS_ANY_ORIGIN' - } + const config = convict({ + server: { + port: { + doc: 'Port the HTTP server should listen on', + format: 'int', + default: 3000, + env: 'PORT', + }, + enableCorsAnyOrigin: { + doc: 'Access-Control-Allow-Origin will be set to the wildcard (*), allowing requests from any domain to access the REST API endpoints', + format: Boolean, + default: true, + env: 'ENABLE_CORS_ANY_ORIGIN', + }, + }, + app: { + name: { + default: 'attack-workbench-rest-api', + }, + env: { + default: 'development', + env: 'NODE_ENV', + }, + version: { + default: packageJson.version, + }, + attackSpecVersion: { + default: packageJson.attackSpecVersion, + }, + }, + database: { + url: { + doc: 'URL of the MongoDB server', + default: '', + env: 'DATABASE_URL', + }, + migration: { + enable: { + doc: 'Enable automatic database migration when starting the server', + format: Boolean, + default: true, + env: 'WB_REST_DATABASE_MIGRATION_ENABLE', }, - app: { - name: { - default: 'attack-workbench-rest-api' - }, - env: { - default: 'development', - env: 'NODE_ENV' - }, - version: { - default: packageJson.version - }, - attackSpecVersion: { - default: packageJson.attackSpecVersion - } + }, + }, + logging: { + logLevel: { + doc: 'Level of logging messages to write to console (error, warn, http, info, verbose, debug)', + default: 'info', + env: 'LOG_LEVEL', + }, + }, + openApi: { + specPath: { + default: './app/api/definitions/openapi.yml', + }, + }, + collectionIndex: { + defaultInterval: { + doc: 'How often collection indexes should check for updates (in seconds). Only applies to new indexes added to the REST API, does not affect existing collection indexes', + default: 300, + env: 'DEFAULT_INTERVAL', + }, + }, + configurationFiles: { + allowedValues: { + doc: 'Location of the allowed values configuration file', + default: './app/config/allowed-values.json', + env: 'ALLOWED_VALUES_PATH', + }, + jsonConfigFile: { + doc: 'Location of a JSON file containing configuration values', + default: '', + env: 'JSON_CONFIG_PATH', + }, + staticMarkingDefinitionsPath: { + doc: 'Location of a directory containing one or more JSON files with the static marking definitions to load into the system', + default: './app/lib/default-static-marking-definitions/', + env: 'WB_REST_STATIC_MARKING_DEFS_PATH', + }, + }, + scheduler: { + checkWorkbenchInterval: { + doc: 'Sets the interval in seconds for starting the scheduler.', + default: 10, + env: 'CHECK_WORKBENCH_INTERVAL', + }, + enableScheduler: { + format: Boolean, + default: true, + env: 'ENABLE_SCHEDULER', + }, + }, + session: { + secret: { + doc: 'Secret used to sign the session ID cookie', + default: defaultSessionSecret, + env: 'SESSION_SECRET', + }, + }, + userAuthn: { + mechanism: { + doc: 'Authentication mechanism to use for user log in', + format: 'user-authn-mechanism', + default: 'anonymous', + env: 'AUTHN_MECHANISM', + }, + oidc: { + issuerUrl: { + doc: 'OIDC Issuer URL', + format: String, + default: '', + env: 'AUTHN_OIDC_ISSUER_URL', }, - database: { - url: { - doc: 'URL of the MongoDB server', - default: '', - env: 'DATABASE_URL' - }, - migration: { - enable: { - doc: 'Enable automatic database migration when starting the server', - format: Boolean, - default: true, - env: 'WB_REST_DATABASE_MIGRATION_ENABLE' - }, - } + clientId: { + doc: 'OIDC Client ID', + format: String, + default: '', + env: 'AUTHN_OIDC_CLIENT_ID', }, - logging: { - logLevel: { - doc: 'Level of logging messages to write to console (error, warn, http, info, verbose, debug)', - default: 'info', - env: 'LOG_LEVEL' - } + clientSecret: { + doc: 'OIDC Client Secret', + format: String, + default: '', + env: 'AUTHN_OIDC_CLIENT_SECRET', }, - openApi: { - specPath: { - default: './app/api/definitions/openapi.yml' - } + redirectOrigin: { + doc: 'Origin (protocol and host) to use in building the OIDC redirect URI', + format: String, + default: 'http://localhost:3000', + env: 'AUTHN_OIDC_REDIRECT_ORIGIN', }, - collectionIndex: { - defaultInterval: { - doc: 'How often collection indexes should check for updates (in seconds). Only applies to new indexes added to the REST API, does not affect existing collection indexes', - default: 300, - env: 'DEFAULT_INTERVAL' - } + }, + }, + serviceAuthn: { + oidcClientCredentials: { + enable: { + doc: 'Enable OIDC Client Credentials Flow for service accounts', + format: Boolean, + default: false, + env: 'SERVICE_ACCOUNT_OIDC_ENABLE', }, - configurationFiles: { - allowedValues: { - doc: 'Location of the allowed values configuration file', - default: './app/config/allowed-values.json', - env: 'ALLOWED_VALUES_PATH' + jwksUri: { + doc: 'JWKS URI for obtaining the public key from the OIDC identity provider', + format: String, + default: '', + env: 'JWKS_URI', + }, + clients: { + doc: 'Services (OIDC clients) that may access the REST API', + format: 'oidc-client', + default: [], + children: { + clientId: { + doc: 'clientId for the service', + format: String, + default: null, }, - jsonConfigFile: { - doc: 'Location of a JSON file containing configuration values', - default: '', - env: 'JSON_CONFIG_PATH' + serviceRole: { + doc: 'The role determines which endpoints the service is permitted to access', + format: 'service-role', + default: 'read-only', }, - staticMarkingDefinitionsPath: { - doc: 'Location of a directory containing one or more JSON files with the static marking definitions to load into the system', - default: './app/lib/default-static-marking-definitions/', - env: 'WB_REST_STATIC_MARKING_DEFS_PATH' - } + }, }, - scheduler: { - checkWorkbenchInterval: { - doc: 'Sets the interval in seconds for starting the scheduler.', - default: 10, - env: 'CHECK_WORKBENCH_INTERVAL' - }, - enableScheduler: { - format: Boolean, - default: true, - env: "ENABLE_SCHEDULER" - } + }, + challengeApikey: { + enable: { + doc: 'Enable apikey authentication for service accounts (challenge)', + format: Boolean, + default: false, + env: 'WB_REST_SERVICE_ACCOUNT_CHALLENGE_APIKEY_ENABLE', }, - session: { - secret: { - doc: 'Secret used to sign the session ID cookie', - default: defaultSessionSecret, - env: 'SESSION_SECRET' - } + secret: { + doc: 'Secret used to sign the tokens issued to service accounts', + default: defaultTokenSigningSecret, + env: 'WB_REST_TOKEN_SIGNING_SECRET', }, - userAuthn: { - mechanism: { - doc: 'Authentication mechanism to use for user log in', - format: 'user-authn-mechanism', - default: 'anonymous', - env: 'AUTHN_MECHANISM' - }, - oidc: { - issuerUrl: { - doc: 'OIDC Issuer URL', - format: String, - default: '', - env: 'AUTHN_OIDC_ISSUER_URL' - }, - clientId: { - doc: 'OIDC Client ID', - format: String, - default: '', - env: 'AUTHN_OIDC_CLIENT_ID' - }, - clientSecret: { - doc: 'OIDC Client Secret', - format: String, - default: '', - env: 'AUTHN_OIDC_CLIENT_SECRET' - }, - redirectOrigin: { - doc: 'Origin (protocol and host) to use in building the OIDC redirect URI', - format: String, - default: 'http://localhost:3000', - env: 'AUTHN_OIDC_REDIRECT_ORIGIN' - } - } + tokenTimeout: { + doc: 'Access token timeout in seconds', + format: 'int', + default: 300, + env: 'WB_REST_TOKEN_TIMEOUT', }, - serviceAuthn: { - oidcClientCredentials: { - enable: { - doc: 'Enable OIDC Client Credentials Flow for service accounts', - format: Boolean, - default: false, - env: 'SERVICE_ACCOUNT_OIDC_ENABLE' - }, - jwksUri: { - doc: 'JWKS URI for obtaining the public key from the OIDC identity provider', - format: String, - default: '', - env: 'JWKS_URI' - }, - clients: { - doc: 'Services (OIDC clients) that may access the REST API', - format: 'oidc-client', - default: [], - children: { - clientId: { - doc: 'clientId for the service', - format: String, - default: null - }, - serviceRole: { - doc: 'The role determines which endpoints the service is permitted to access', - format: 'service-role', - default: 'read-only' - } - } - } + serviceAccounts: { + doc: 'Services accounts that may access the REST API (with challenge)', + format: 'service-account', + default: [], + children: { + name: { + doc: 'Name of the service account', + format: String, + default: null, }, - challengeApikey: { - enable: { - doc: 'Enable apikey authentication for service accounts (challenge)', - format: Boolean, - default: false, - env: 'WB_REST_SERVICE_ACCOUNT_CHALLENGE_APIKEY_ENABLE' - }, - secret: { - doc: 'Secret used to sign the tokens issued to service accounts', - default: defaultTokenSigningSecret, - env: 'WB_REST_TOKEN_SIGNING_SECRET' - }, - tokenTimeout: { - doc: 'Access token timeout in seconds', - format: 'int', - default: 300, - env: 'WB_REST_TOKEN_TIMEOUT' - }, - serviceAccounts: { - doc: 'Services accounts that may access the REST API (with challenge)', - format: 'service-account', - default: [], - children: { - name: { - doc: 'Name of the service account', - format: String, - default: null - }, - apikey: { - doc: 'apikey of the service account (shared secret)', - format: String, - default: null - }, - serviceRole: { - doc: 'The role determines which endpoints the service is permitted to access', - format: 'service-role', - default: 'read-only' - } - } - } + apikey: { + doc: 'apikey of the service account (shared secret)', + format: String, + default: null, }, - basicApikey: { - enable: { - doc: 'Enable apikey authentication for service accounts (no challenge)', - format: Boolean, - default: false, - env: 'WB_REST_SERVICE_ACCOUNT_BASIC_APIKEY_ENABLE' - }, - serviceAccounts: { - doc: 'Services accounts that may access the REST API using basic apikey', - format: 'service-account', - default: [], - children: { - name: { - doc: 'Name of the service account', - format: String, - default: null - }, - apikey: { - doc: 'apikey of the service account (shared secret)', - format: String, - default: null - }, - serviceRole: { - doc: 'The role determines which endpoints the service is permitted to access', - format: 'service-role', - default: 'read-only' - } - } - } - } + serviceRole: { + doc: 'The role determines which endpoints the service is permitted to access', + format: 'service-role', + default: 'read-only', + }, + }, + }, + }, + basicApikey: { + enable: { + doc: 'Enable apikey authentication for service accounts (no challenge)', + format: Boolean, + default: false, + env: 'WB_REST_SERVICE_ACCOUNT_BASIC_APIKEY_ENABLE', }, - attackSourceNames: { - doc: 'Valid source_name values used in MITRE ATT&CK external_references', - default: ['mitre-attack', 'mitre-mobile-attack', 'mobile-attack', 'mitre-ics-attack'] + serviceAccounts: { + doc: 'Services accounts that may access the REST API using basic apikey', + format: 'service-account', + default: [], + children: { + name: { + doc: 'Name of the service account', + format: String, + default: null, + }, + apikey: { + doc: 'apikey of the service account (shared secret)', + format: String, + default: null, + }, + serviceRole: { + doc: 'The role determines which endpoints the service is permitted to access', + format: 'service-role', + default: 'read-only', + }, + }, }, - domainToKillChainMap: { - doc: 'Map the built-in domain names to the corresponding kill-chain-phase names', - default: { - "enterprise-attack": "mitre-attack", - "mobile-attack": "mitre-mobile-attack", - "ics-attack": "mitre-ics-attack" - } - } - }); + }, + }, + attackSourceNames: { + doc: 'Valid source_name values used in MITRE ATT&CK external_references', + default: ['mitre-attack', 'mitre-mobile-attack', 'mobile-attack', 'mitre-ics-attack'], + }, + domainToKillChainMap: { + doc: 'Map the built-in domain names to the corresponding kill-chain-phase names', + default: { + 'enterprise-attack': 'mitre-attack', + 'mobile-attack': 'mitre-mobile-attack', + 'ics-attack': 'mitre-ics-attack', + }, + }, + }); - // Load configuration values from a JSON file if the JSON_CONFIG_PATH environment variable is set - if (config.get('configurationFiles.jsonConfigFile')) { - config.loadFile(config.get('configurationFiles.jsonConfigFile')); - } + // Load configuration values from a JSON file if the JSON_CONFIG_PATH environment variable is set + if (config.get('configurationFiles.jsonConfigFile')) { + config.loadFile(config.get('configurationFiles.jsonConfigFile')); + } - config.validate({ allowed: 'strict' }); + config.validate({ allowed: 'strict' }); - return config.getProperties(); + return config.getProperties(); } // Load the configuration and extract the configuration properties to simplify access const configurationObject = loadConfig(); // Add a function to reload the configuration properties -configurationObject.reloadConfig = function() { - const newConfigProperties = loadConfig(); - Object.assign(configurationObject, newConfigProperties); +configurationObject.reloadConfig = function () { + const newConfigProperties = loadConfig(); + Object.assign(configurationObject, newConfigProperties); }; module.exports = configurationObject; diff --git a/app/controllers/assets-controller.js b/app/controllers/assets-controller.js index c2fc14e7..f122c183 100644 --- a/app/controllers/assets-controller.js +++ b/app/controllers/assets-controller.js @@ -2,174 +2,166 @@ const assetsService = require('../services/assets-service'); const logger = require('../lib/logger'); -const { DuplicateIdError, BadlyFormattedParameterError, InvalidQueryStringParameterError } = require('../exceptions'); - -exports.retrieveAll = async function(req, res) { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0, - domain: req.query.domain, - platform: req.query.platform, - state: req.query.state, - includeRevoked: req.query.includeRevoked, - includeDeprecated: req.query.includeDeprecated, - search: req.query.search, - lastUpdatedBy: req.query.lastUpdatedBy, - includePagination: req.query.includePagination - } - - try { - const assets = await assetsService.retrieveAll(options); - if (options.includePagination) { - logger.debug(`Success: Retrieved ${ assets.data.length } of ${ assets.pagination.total } total asset(s)`); - } - else { - logger.debug(`Success: Retrieved ${ assets.length } asset(s)`); - } - return res.status(200).send(assets); - } - catch(err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get assets. Server error.'); - } +const { + DuplicateIdError, + BadlyFormattedParameterError, + InvalidQueryStringParameterError, +} = require('../exceptions'); + +exports.retrieveAll = async function (req, res) { + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + domain: req.query.domain, + platform: req.query.platform, + state: req.query.state, + includeRevoked: req.query.includeRevoked, + includeDeprecated: req.query.includeDeprecated, + search: req.query.search, + lastUpdatedBy: req.query.lastUpdatedBy, + includePagination: req.query.includePagination, + }; + + try { + const assets = await assetsService.retrieveAll(options); + if (options.includePagination) { + logger.debug( + `Success: Retrieved ${assets.data.length} of ${assets.pagination.total} total asset(s)`, + ); + } else { + logger.debug(`Success: Retrieved ${assets.length} asset(s)`); + } + return res.status(200).send(assets); + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get assets. Server error.'); + } }; -exports.retrieveById = async function(req, res) { - const options = { - versions: req.query.versions || 'latest', - retrieveDataComponents: req.query.retrieveDataComponents - } - - try { - const assets = await assetsService.retrieveById(req.params.stixId, options); - if (assets.length === 0) { - return res.status(404).send('Asset not found.'); - } - else { - logger.debug(`Success: Retrieved ${ assets.length } asset(s) with id ${ req.params.stixId }`); - return res.status(200).send(assets); - } - } - catch(err) { - if (err instanceof BadlyFormattedParameterError) { - logger.warn('Badly formatted stix id: ' + req.params.stixId); - return res.status(400).send('Stix id is badly formatted.'); - } else if (err instanceof InvalidQueryStringParameterError) { - logger.warn('Invalid query string: versions=' + req.query.versions); - return res.status(400).send('Query string parameter versions is invalid.'); - } else { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get assets. Server error.'); - } - - } +exports.retrieveById = async function (req, res) { + const options = { + versions: req.query.versions || 'latest', + retrieveDataComponents: req.query.retrieveDataComponents, + }; + + try { + const assets = await assetsService.retrieveById(req.params.stixId, options); + if (assets.length === 0) { + return res.status(404).send('Asset not found.'); + } else { + logger.debug(`Success: Retrieved ${assets.length} asset(s) with id ${req.params.stixId}`); + return res.status(200).send(assets); + } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + logger.warn('Badly formatted stix id: ' + req.params.stixId); + return res.status(400).send('Stix id is badly formatted.'); + } else if (err instanceof InvalidQueryStringParameterError) { + logger.warn('Invalid query string: versions=' + req.query.versions); + return res.status(400).send('Query string parameter versions is invalid.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get assets. Server error.'); + } + } }; -exports.retrieveVersionById = async function(req, res) { - - // TODO Remove after confirming these are not being used by assetsService.retrieveVersionById - // const options = { - // retrieveDataComponents: req.query.retrieveDataComponents - // } - - try { - // const asset = await assetsService.retrieveVersionById(req.params.stixId, req.params.modified, options); - const asset = await assetsService.retrieveVersionById(req.params.stixId, req.params.modified); - if (!asset) { - return res.status(404).send('Asset not found.'); - } - else { - logger.debug(`Success: Retrieved asset with id ${asset.id}`); - return res.status(200).send(asset); - } - } - catch(err) { - if (err instanceof BadlyFormattedParameterError) { - 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 asset. Server error.'); - } - - } +exports.retrieveVersionById = async function (req, res) { + // TODO Remove after confirming these are not being used by assetsService.retrieveVersionById + // const options = { + // retrieveDataComponents: req.query.retrieveDataComponents + // } + + try { + // const asset = await assetsService.retrieveVersionById(req.params.stixId, req.params.modified, options); + const asset = await assetsService.retrieveVersionById(req.params.stixId, req.params.modified); + if (!asset) { + return res.status(404).send('Asset not found.'); + } else { + logger.debug(`Success: Retrieved asset with id ${asset.id}`); + return res.status(200).send(asset); + } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + 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 asset. Server error.'); + } + } }; -exports.create = async function(req, res) { - // Get the data from the request - const assetData = req.body; +exports.create = async function (req, res) { + // Get the data from the request + const assetData = req.body; - try { - const options = { - import: false, - userAccountId: req.user?.userAccountId - }; - const asset = await assetsService.create(assetData, options); - logger.debug("Success: Created asset with id " + asset.stix.id); - return res.status(201).send(asset); - } - catch(err) { - if (err instanceof DuplicateIdError) { - logger.warn("Duplicate stix.id and stix.modified"); - return res.status(409).send('Unable to create asset. Duplicate stix.id and stix.modified properties.'); - } - else { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to create asset. Server error."); - } - } + try { + const options = { + import: false, + userAccountId: req.user?.userAccountId, + }; + const asset = await assetsService.create(assetData, options); + logger.debug('Success: Created asset with id ' + asset.stix.id); + return res.status(201).send(asset); + } catch (err) { + if (err instanceof DuplicateIdError) { + logger.warn('Duplicate stix.id and stix.modified'); + return res + .status(409) + .send('Unable to create asset. Duplicate stix.id and stix.modified properties.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to create asset. Server error.'); + } + } }; -exports.updateFull = async function(req, res) { - // Get the data from the request - const assetData = req.body; - - try { - const asset = await assetsService.updateFull(req.params.stixId, req.params.modified, assetData); - if (!asset) { - return res.status(404).send('Asset not found.'); - } else { - logger.debug("Success: Updated asset with id " + asset.stix.id); - return res.status(200).send(asset); - } - } - catch(err) { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to update asset. Server error."); - } +exports.updateFull = async function (req, res) { + // Get the data from the request + const assetData = req.body; + + try { + const asset = await assetsService.updateFull(req.params.stixId, req.params.modified, assetData); + if (!asset) { + return res.status(404).send('Asset not found.'); + } else { + logger.debug('Success: Updated asset with id ' + asset.stix.id); + return res.status(200).send(asset); + } + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to update asset. Server error.'); + } }; -exports.deleteById = async function(req, res) { - try { - const assets = await assetsService.deleteById(req.params.stixId); +exports.deleteById = async function (req, res) { + try { + const assets = await assetsService.deleteById(req.params.stixId); - if (assets.deletedCount === 0) { - return res.status(404).send('Assets not found.'); - } - else { - logger.debug(`Success: Deleted asset with id ${ req.params.stixId }`); - return res.status(204).end(); - } - } - catch(err) { - logger.error('Delete asset failed. ' + err); - return res.status(500).send('Unable to delete asset. Server error.'); + if (assets.deletedCount === 0) { + return res.status(404).send('Assets not found.'); + } else { + logger.debug(`Success: Deleted asset with id ${req.params.stixId}`); + return res.status(204).end(); } + } catch (err) { + logger.error('Delete asset failed. ' + err); + return res.status(500).send('Unable to delete asset. Server error.'); + } }; -exports.deleteVersionById = async function(req, res) { - try { - const asset = await assetsService.deleteVersionById(req.params.stixId, req.params.modified); - if (!asset) { - return res.status(404).send('Asset not found.'); - } else { - logger.debug("Success: Deleted asset with id " + asset.stix.id); - return res.status(204).end(); - } - } - catch(err) { - logger.error('Delete asset failed. ' + err); - return res.status(500).send('Unable to delete asset. Server error.'); - } +exports.deleteVersionById = async function (req, res) { + try { + const asset = await assetsService.deleteVersionById(req.params.stixId, req.params.modified); + if (!asset) { + return res.status(404).send('Asset not found.'); + } else { + logger.debug('Success: Deleted asset with id ' + asset.stix.id); + return res.status(204).end(); + } + } catch (err) { + logger.error('Delete asset failed. ' + err); + return res.status(500).send('Unable to delete asset. Server error.'); + } }; diff --git a/app/controllers/attack-objects-controller.js b/app/controllers/attack-objects-controller.js index 80220c45..58d99d50 100644 --- a/app/controllers/attack-objects-controller.js +++ b/app/controllers/attack-objects-controller.js @@ -3,34 +3,34 @@ const attackObjectsService = require('../services/attack-objects-service'); const logger = require('../lib/logger'); -exports.retrieveAll = async function(req, res) { - const options = { - attackId: req.query.attackId, - offset: req.query.offset || 0, - limit: req.query.limit || 0, - state: req.query.state, - includeRevoked: req.query.includeRevoked, - includeDeprecated: req.query.includeDeprecated, - search: req.query.search, - lastUpdatedBy: req.query.lastUpdatedBy, - includePagination: req.query.includePagination, - versions: req.query.versions - } - - try { - const results = await attackObjectsService.retrieveAll(options); +exports.retrieveAll = async function (req, res) { + const options = { + attackId: req.query.attackId, + offset: req.query.offset || 0, + limit: req.query.limit || 0, + state: req.query.state, + includeRevoked: req.query.includeRevoked, + includeDeprecated: req.query.includeDeprecated, + search: req.query.search, + lastUpdatedBy: req.query.lastUpdatedBy, + includePagination: req.query.includePagination, + versions: req.query.versions, + }; - if (options.includePagination) { - logger.debug(`Success: Retrieved ${results.data.length} of ${results.pagination.total} total ATT&CK object(s)`); - } - else { - logger.debug(`Success: Retrieved ${results.length} ATT&CK object(s)`); - } + try { + const results = await attackObjectsService.retrieveAll(options); - return res.status(200).send(results); - } - catch (err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get ATT&CK objects. Server error.'); + if (options.includePagination) { + logger.debug( + `Success: Retrieved ${results.data.length} of ${results.pagination.total} total ATT&CK object(s)`, + ); + } else { + logger.debug(`Success: Retrieved ${results.length} ATT&CK object(s)`); } + + return res.status(200).send(results); + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get ATT&CK objects. Server error.'); + } }; diff --git a/app/controllers/authn-anonymous-controller.js b/app/controllers/authn-anonymous-controller.js index abc6ccc1..4c77958b 100644 --- a/app/controllers/authn-anonymous-controller.js +++ b/app/controllers/authn-anonymous-controller.js @@ -2,33 +2,30 @@ const logger = require('../lib/logger'); -exports.login = function(req, res) { - if (req.user) { - logger.info(`Success: User logged in with uuid: ${ req.user.anonymousUuid }`); - return res.status(200).send('User logged in'); - } - else { - logger.warn('Unable to log user in, failed with error: req.user not found'); - return res.status(401).send('Not authorized'); - } +exports.login = function (req, res) { + if (req.user) { + logger.info(`Success: User logged in with uuid: ${req.user.anonymousUuid}`); + return res.status(200).send('User logged in'); + } else { + logger.warn('Unable to log user in, failed with error: req.user not found'); + return res.status(401).send('Not authorized'); + } }; -exports.logout = function(req, res) { - try { - const anonymousUuid = req.user.anonymousUuid; - req.logout(function(err) { - if (err) { - logger.error('Unable to log out anonymous user, failed with error: ' + err); - return res.status(500).send('Unable to log out anonymous user. Server error.'); - } - else { - logger.info(`Success: User logged out with uuid: ${ anonymousUuid }`); - return res.status(200).send('User logged out'); - } - }); - } - catch(err) { +exports.logout = function (req, res) { + try { + const anonymousUuid = req.user.anonymousUuid; + req.logout(function (err) { + if (err) { logger.error('Unable to log out anonymous user, failed with error: ' + err); return res.status(500).send('Unable to log out anonymous user. Server error.'); - } + } else { + logger.info(`Success: User logged out with uuid: ${anonymousUuid}`); + return res.status(200).send('User logged out'); + } + }); + } catch (err) { + logger.error('Unable to log out anonymous user, failed with error: ' + err); + return res.status(500).send('Unable to log out anonymous user. Server error.'); + } }; diff --git a/app/controllers/authn-oidc-controller.js b/app/controllers/authn-oidc-controller.js index 34be64b8..d7eb1750 100644 --- a/app/controllers/authn-oidc-controller.js +++ b/app/controllers/authn-oidc-controller.js @@ -2,33 +2,31 @@ const logger = require('../lib/logger'); -exports.login = function(req, res, next) { - // Save the destination and call next() to trigger the redirect to the identity provider - req.session.oidcDestination = req.query.destination; - return next(); +exports.login = function (req, res, next) { + // Save the destination and call next() to trigger the redirect to the identity provider + req.session.oidcDestination = req.query.destination; + return next(); }; -exports.identityProviderCallback = function(req, res) { - logger.debug('Success: OIDC user logged in.'); - return res.redirect(req.session.oidcDestination); +exports.identityProviderCallback = function (req, res) { + logger.debug('Success: OIDC user logged in.'); + return res.redirect(req.session.oidcDestination); }; -exports.logout = function(req, res) { - try { - const email = req.user?.email; - req.logout(function(err) { - if (err) { - logger.error('Unable to log out user, failed with error: ' + err); - return res.status(500).send('Unable to log out user. Server error.'); - } - else { - logger.info(`Success: User logged out with email: ${ email }`); - return res.status(200).send('User logged out'); - } - }); - } - catch(err) { +exports.logout = function (req, res) { + try { + const email = req.user?.email; + req.logout(function (err) { + if (err) { logger.error('Unable to log out user, failed with error: ' + err); return res.status(500).send('Unable to log out user. Server error.'); - } + } else { + logger.info(`Success: User logged out with email: ${email}`); + return res.status(200).send('User logged out'); + } + }); + } catch (err) { + logger.error('Unable to log out user, failed with error: ' + err); + return res.status(500).send('Unable to log out user. Server error.'); + } }; diff --git a/app/controllers/authn-service-controller.js b/app/controllers/authn-service-controller.js index 266251c1..2652d4c6 100644 --- a/app/controllers/authn-service-controller.js +++ b/app/controllers/authn-service-controller.js @@ -14,138 +14,133 @@ const config = require('../config/config'); const cache = new NodeCache(); function generateNonce() { - const stringBase = 'base64'; - const byteLength = 48; - const buffer = crypto.randomBytes(byteLength); - const nonce = buffer.toString(stringBase); + const stringBase = 'base64'; + const byteLength = 48; + const buffer = crypto.randomBytes(byteLength); + const nonce = buffer.toString(stringBase); - return nonce; + return nonce; } const errors = { - serviceNotFound: 'Service not found', - invalidChallengeHash: 'Invalid challenge hash', - challengeNotFound: 'Challenge not found' + serviceNotFound: 'Service not found', + invalidChallengeHash: 'Invalid challenge hash', + challengeNotFound: 'Challenge not found', }; exports.errors = errors; function createChallenge(serviceName) { - // Verify that the service is on the list of configured services and get the apikey - const services = config.serviceAuthn.challengeApikey.serviceAccounts; - const service = services.find(s => s.name === serviceName); - if (!service) { - throw new Error(errors.serviceNotFound); - } + // Verify that the service is on the list of configured services and get the apikey + const services = config.serviceAuthn.challengeApikey.serviceAccounts; + const service = services.find((s) => s.name === serviceName); + if (!service) { + throw new Error(errors.serviceNotFound); + } - // Generate the challenge string - const challenge = generateNonce(); + // Generate the challenge string + const challenge = generateNonce(); - // Save the challenge string and apikey in the cache - cache.set(serviceName, { challenge, apikey: service.apikey }, 60); + // Save the challenge string and apikey in the cache + cache.set(serviceName, { challenge, apikey: service.apikey }, 60); - // Return the challenge - return challenge; + // Return the challenge + return challenge; } function createToken(serviceName, challengeHash) { - // Get the cached challenge and apikey - const cachedValue = cache.take(serviceName); - if (!cachedValue) { - throw new Error(errors.challengeNotFound); - } - const { challenge, apikey } = cachedValue; - - // Generate the hash - const hmac = crypto.createHmac('sha256', apikey); - hmac.update(challenge); - const digest = hmac.digest('hex'); + // Get the cached challenge and apikey + const cachedValue = cache.take(serviceName); + if (!cachedValue) { + throw new Error(errors.challengeNotFound); + } + const { challenge, apikey } = cachedValue; + + // Generate the hash + const hmac = crypto.createHmac('sha256', apikey); + hmac.update(challenge); + const digest = hmac.digest('hex'); + + // Does the generated hash match the hash provided in the request? + if (digest !== challengeHash) { + throw new Error(errors.invalidChallengeHash); + } + + // Create the payload + const timeout = config.serviceAuthn.challengeApikey.tokenTimeout; + const payload = { + serviceName, + exp: Math.floor(Date.now() / 1000) + timeout, + }; + + // Generate the access token and return it + const token = jwt.sign(payload, config.serviceAuthn.challengeApikey.secret); + return { token, expiresIn: timeout }; +} - // Does the generated hash match the hash provided in the request? - if (digest !== challengeHash) { - throw new Error(errors.invalidChallengeHash); +exports.apikeyGetChallenge = function (req, res) { + try { + const serviceName = req.query.serviceName; + if (!serviceName) { + logger.warn('Unable to send service account challenge, missing service name'); + return res.status(400).send('Service name is required'); } - // Create the payload - const timeout = config.serviceAuthn.challengeApikey.tokenTimeout; - const payload = { - serviceName, - exp: Math.floor(Date.now() / 1000) + timeout - }; + const challenge = createChallenge(serviceName); + logger.debug('Success: Service account challenge created.'); + return res.status(200).send({ challenge }); + } catch (err) { + if (err.message === errors.serviceNotFound) { + logger.warn('Unable to create service account challenge, service not found'); + return res.status(404).send('Service not found'); + } else { + logger.error('Unable to create service account challenge, failed with error: ' + err); + return res.status(500).send('Unable to create service account challenge. Server error.'); + } + } +}; - // Generate the access token and return it - const token = jwt.sign(payload, config.serviceAuthn.challengeApikey.secret); - return { token, expiresIn: timeout }; -} +exports.apikeyGetToken = function (req, res) { + try { + const serviceName = req.query.serviceName; + if (!serviceName) { + logger.warn('Unable to send service account token, missing service name'); + return res.status(400).send('Service name is required'); + } -exports.apikeyGetChallenge = function(req, res) { - try { - const serviceName = req.query.serviceName; - if (!serviceName) { - logger.warn('Unable to send service account challenge, missing service name'); - return res.status(400).send('Service name is required'); - } - - const challenge = createChallenge(serviceName); - logger.debug('Success: Service account challenge created.'); - return res.status(200).send({ challenge }); + const authorizationHeader = req.get('Authorization'); + if (!authorizationHeader) { + logger.warn('Unable to send service account token, missing Authorization header'); + return res.status(400).send('Authorization header is required'); } - catch(err) { - if (err.message === errors.serviceNotFound) { - logger.warn('Unable to create service account challenge, service not found'); - return res.status(404).send('Service not found'); - } - else { - logger.error('Unable to create service account challenge, failed with error: ' + err); - return res.status(500).send('Unable to create service account challenge. Server error.'); - } + + const headerChunks = authorizationHeader.split(' '); + if (headerChunks.length !== 2) { + return res.status(400).send('Badly formatted request'); } -}; -exports.apikeyGetToken = function(req, res) { - try { - const serviceName = req.query.serviceName; - if (!serviceName) { - logger.warn('Unable to send service account token, missing service name'); - return res.status(400).send('Service name is required'); - } - - const authorizationHeader = req.get('Authorization'); - if (!authorizationHeader) { - logger.warn('Unable to send service account token, missing Authorization header'); - return res.status(400).send('Authorization header is required'); - } - - const headerChunks = authorizationHeader.split(' '); - if (headerChunks.length !== 2) { - return res.status(400).send('Badly formatted request'); - } - - if (headerChunks[0].toLowerCase() !== 'apikey') { - return res.status(400).send('Badly formatted request'); - } - - const challengeHash = headerChunks[1]; - - const token = createToken(serviceName, challengeHash); - logger.debug('Success: Service account token created.'); - const message = { - access_token: token.token, - expires_in: token.expiresIn - } - return res.status(200).send(message); + if (headerChunks[0].toLowerCase() !== 'apikey') { + return res.status(400).send('Badly formatted request'); } - catch(err) { - if (err.message === errors.invalidChallengeHash) { - logger.warn('Unable to create service account token, invalid challenge hash'); - return res.status(400).send('Invalid challenge hash'); - } - else if (err.message === errors.challengeNotFound) { - logger.warn('Unable to create service account token, challenge not found'); - return res.status(400).send('Challenge not found'); - } - else { - logger.error('Unable to create service account token, failed with error: ' + err); - return res.status(500).send('Unable to create service account token. Server error.'); - } + + const challengeHash = headerChunks[1]; + + const token = createToken(serviceName, challengeHash); + logger.debug('Success: Service account token created.'); + const message = { + access_token: token.token, + expires_in: token.expiresIn, + }; + return res.status(200).send(message); + } catch (err) { + if (err.message === errors.invalidChallengeHash) { + logger.warn('Unable to create service account token, invalid challenge hash'); + return res.status(400).send('Invalid challenge hash'); + } else if (err.message === errors.challengeNotFound) { + logger.warn('Unable to create service account token, challenge not found'); + return res.status(400).send('Challenge not found'); + } else { + logger.error('Unable to create service account token, failed with error: ' + err); + return res.status(500).send('Unable to create service account token. Server error.'); } + } }; diff --git a/app/controllers/campaigns-controller.js b/app/controllers/campaigns-controller.js index f424d0fe..e0381655 100644 --- a/app/controllers/campaigns-controller.js +++ b/app/controllers/campaigns-controller.js @@ -2,173 +2,173 @@ const campaignsService = require('../services/campaigns-service'); const logger = require('../lib/logger'); -const { DuplicateIdError, BadlyFormattedParameterError, InvalidQueryStringParameterError, InvalidTypeError } = require('../exceptions'); - -exports.retrieveAll = async function(req, res) { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0, - state: req.query.state, - includeRevoked: req.query.includeRevoked, - includeDeprecated: req.query.includeDeprecated, - search: req.query.search, - lastUpdatedBy: req.query.lastUpdatedBy, - includePagination: req.query.includePagination - } - - try { - const results = await campaignsService.retrieveAll(options); - if (options.includePagination) { - logger.debug(`Success: Retrieved ${ results.data.length } of ${ results.pagination.total } total campaign(s)`); - } - else { - logger.debug(`Success: Retrieved ${ results.length } campaign(s)`); - } - return res.status(200).send(results); - } catch (err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get campaigns. Server error.'); +const { + DuplicateIdError, + BadlyFormattedParameterError, + InvalidQueryStringParameterError, + InvalidTypeError, +} = require('../exceptions'); + +exports.retrieveAll = async function (req, res) { + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + state: req.query.state, + includeRevoked: req.query.includeRevoked, + includeDeprecated: req.query.includeDeprecated, + search: req.query.search, + lastUpdatedBy: req.query.lastUpdatedBy, + includePagination: req.query.includePagination, + }; + + try { + const results = await campaignsService.retrieveAll(options); + if (options.includePagination) { + logger.debug( + `Success: Retrieved ${results.data.length} of ${results.pagination.total} total campaign(s)`, + ); + } else { + logger.debug(`Success: Retrieved ${results.length} campaign(s)`); } - + return res.status(200).send(results); + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get campaigns. Server error.'); + } }; -exports.retrieveById = async function(req, res) { - const options = { - versions: req.query.versions || 'latest' +exports.retrieveById = async function (req, res) { + const options = { + versions: req.query.versions || 'latest', + }; + try { + const campaigns = await campaignsService.retrieveById(req.params.stixId, options); + if (campaigns.length === 0) { + return res.status(404).send('Campaign not found.'); + } else { + logger.debug( + `Success: Retrieved ${campaigns.length} campaign(s) with id ${req.params.stixId}`, + ); + return res.status(200).send(campaigns); } - try { - - const campaigns = await campaignsService.retrieveById(req.params.stixId, options); - if (campaigns.length === 0) { - return res.status(404).send('Campaign not found.'); - } - else { - logger.debug(`Success: Retrieved ${ campaigns.length } campaign(s) with id ${ req.params.stixId }`); - return res.status(200).send(campaigns); - } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - logger.warn('Badly formatted stix id: ' + req.params.stixId); - return res.status(400).send('Stix id is badly formatted.'); - } - else if (err instanceof InvalidQueryStringParameterError) { - logger.warn('Invalid query string: versions=' + req.query.versions); - return res.status(400).send('Query string parameter versions is invalid.'); - } - else { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get campaigns. Server error.'); - } - } - + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + logger.warn('Badly formatted stix id: ' + req.params.stixId); + return res.status(400).send('Stix id is badly formatted.'); + } else if (err instanceof InvalidQueryStringParameterError) { + logger.warn('Invalid query string: versions=' + req.query.versions); + return res.status(400).send('Query string parameter versions is invalid.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get campaigns. Server error.'); + } + } }; -exports.retrieveVersionById = async function(req, res) { - - try { - const campaign = await campaignsService.retrieveVersionById(req.params.stixId, req.params.modified); - if (!campaign) { - return res.status(404).send('Campaign not found.'); - } - else { - logger.debug(`Success: Retrieved campaign with id ${campaign.id}`); - return res.status(200).send(campaign); - } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - 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 campaign. Server error.'); - } - } +exports.retrieveVersionById = async function (req, res) { + try { + const campaign = await campaignsService.retrieveVersionById( + req.params.stixId, + req.params.modified, + ); + if (!campaign) { + return res.status(404).send('Campaign not found.'); + } else { + logger.debug(`Success: Retrieved campaign with id ${campaign.id}`); + return res.status(200).send(campaign); + } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + 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 campaign. Server error.'); + } + } }; -exports.create = async function(req, res) { - // Get the data from the request - const campaignData = req.body; +exports.create = async function (req, res) { + // Get the data from the request + const campaignData = req.body; - // Create the campaign - try { - const options = { - import: false, - userAccountId: req.user?.userAccountId - }; - - const campaign = await campaignsService.create(campaignData, options); - logger.debug('Success: Created campaign with id ' + campaign.stix.id); - return res.status(201).send(campaign); - } - catch(err) { - if (err instanceof DuplicateIdError) { - logger.warn('Duplicate stix.id and stix.modified'); - return res.status(409).send('Unable to create campaign. Duplicate stix.id and stix.modified properties.'); - } - else if (err instanceof InvalidTypeError) { - logger.warn('Invalid stix.type'); - return res.status(400).send('Unable to create campaign. stix.type must be campaign'); - } - else { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to create campaign. Server error.'); - } + // Create the campaign + try { + const options = { + import: false, + userAccountId: req.user?.userAccountId, + }; + + const campaign = await campaignsService.create(campaignData, options); + logger.debug('Success: Created campaign with id ' + campaign.stix.id); + return res.status(201).send(campaign); + } catch (err) { + if (err instanceof DuplicateIdError) { + logger.warn('Duplicate stix.id and stix.modified'); + return res + .status(409) + .send('Unable to create campaign. Duplicate stix.id and stix.modified properties.'); + } else if (err instanceof InvalidTypeError) { + logger.warn('Invalid stix.type'); + return res.status(400).send('Unable to create campaign. stix.type must be campaign'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to create campaign. Server error.'); } + } }; -exports.updateFull = async function(req, res) { - // Get the data from the request - const campaignData = req.body; - try { - const campaign = await campaignsService.updateFull(req.params.stixId, req.params.modified, campaignData); - if (!campaign) { - return res.status(404).send('Campaign not found.'); - } else { - logger.debug("Success: Updated campaign with id " + campaign.stix.id); - return res.status(200).send(campaign); - } +exports.updateFull = async function (req, res) { + // Get the data from the request + const campaignData = req.body; + try { + const campaign = await campaignsService.updateFull( + req.params.stixId, + req.params.modified, + campaignData, + ); + if (!campaign) { + return res.status(404).send('Campaign not found.'); + } else { + logger.debug('Success: Updated campaign with id ' + campaign.stix.id); + return res.status(200).send(campaign); } + } catch (err) { // Create the campaign - catch (err) { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to update campaign. Server error."); - } - + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to update campaign. Server error.'); + } }; -exports.deleteVersionById = async function(req, res) { - - try { - const campaign = await campaignsService.deleteVersionById(req.params.stixId, req.params.modified); - if (!campaign) { - return res.status(404).send('Campaign not found.'); - } else { - logger.debug("Success: Deleted campaign with id " + campaign.stix.id); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete campaign failed. ' + err); - return res.status(500).send('Unable to delete campaign. Server error.'); +exports.deleteVersionById = async function (req, res) { + try { + const campaign = await campaignsService.deleteVersionById( + req.params.stixId, + req.params.modified, + ); + if (!campaign) { + return res.status(404).send('Campaign not found.'); + } else { + logger.debug('Success: Deleted campaign with id ' + campaign.stix.id); + return res.status(204).end(); } - + } catch (err) { + logger.error('Delete campaign failed. ' + err); + return res.status(500).send('Unable to delete campaign. Server error.'); + } }; -exports.deleteById = async function(req, res) { - - - try { - const campaigns = await campaignsService.deleteById(req.params.stixId); - if (campaigns.deletedCount === 0) { - return res.status(404).send('Campaign not found.'); - } - else { - logger.debug(`Success: Deleted campaigns with id ${ req.params.stixId }`); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete campaign failed. ' + err); - return res.status(500).send('Unable to delete campaign. Server error.'); - } - +exports.deleteById = async function (req, res) { + try { + const campaigns = await campaignsService.deleteById(req.params.stixId); + if (campaigns.deletedCount === 0) { + return res.status(404).send('Campaign not found.'); + } else { + logger.debug(`Success: Deleted campaigns with id ${req.params.stixId}`); + return res.status(204).end(); + } + } catch (err) { + logger.error('Delete campaign failed. ' + err); + return res.status(500).send('Unable to delete campaign. Server error.'); + } }; diff --git a/app/controllers/collection-bundles-controller.js b/app/controllers/collection-bundles-controller.js index 3836cb65..8e8a46cc 100644 --- a/app/controllers/collection-bundles-controller.js +++ b/app/controllers/collection-bundles-controller.js @@ -4,164 +4,181 @@ const collectionBundlesService = require('../services/collection-bundles-service const logger = require('../lib/logger'); const availableForceImportParameters = [ - collectionBundlesService.forceImportParameters.attackSpecVersionViolations, - collectionBundlesService.forceImportParameters.duplicateCollection + collectionBundlesService.forceImportParameters.attackSpecVersionViolations, + collectionBundlesService.forceImportParameters.duplicateCollection, ]; function extractForceImportParameters(req) { - const params = []; - if (req.query.forceImport) { - if (Array.isArray(req.query.forceImport)) { - params.push(...req.query.forceImport); - } - else { - params.push(req.query.forceImport); - } + const params = []; + if (req.query.forceImport) { + if (Array.isArray(req.query.forceImport)) { + params.push(...req.query.forceImport); + } else { + params.push(req.query.forceImport); + } - if (params.find(param => param === 'all')) { - return availableForceImportParameters; - } + if (params.find((param) => param === 'all')) { + return availableForceImportParameters; } + } - return params; + return params; } -exports.importBundle = function(req, res) { - // Get the data from the request - const collectionBundleData = req.body; - - const forceImportParameters = extractForceImportParameters(req); - - const errorResult = { - bundleErrors: { - noCollection: false, - moreThanOneCollection: false, - duplicateCollection: false, - badlyFormattedCollection: false - }, - objectErrors: { - summary: { - duplicateObjectInBundleCount: 0, - invalidAttackSpecVersionCount: 0 - }, - errors: [] - } - }; - let errorFound = false; - - // Find the x-mitre-collection objects - const collections = collectionBundleData.objects.filter(object => object.type === 'x-mitre-collection'); - - // The bundle must have an x-mitre-collection object - if (collections.length === 0) { - logger.warn("Collection bundle is missing x-mitre-collection object."); - errorResult.bundleErrors.noCollection = true; - errorFound = true; +exports.importBundle = function (req, res) { + // Get the data from the request + const collectionBundleData = req.body; + + const forceImportParameters = extractForceImportParameters(req); + + const errorResult = { + bundleErrors: { + noCollection: false, + moreThanOneCollection: false, + duplicateCollection: false, + badlyFormattedCollection: false, + }, + objectErrors: { + summary: { + duplicateObjectInBundleCount: 0, + invalidAttackSpecVersionCount: 0, + }, + errors: [], + }, + }; + let errorFound = false; + + // Find the x-mitre-collection objects + const collections = collectionBundleData.objects.filter( + (object) => object.type === 'x-mitre-collection', + ); + + // The bundle must have an x-mitre-collection object + if (collections.length === 0) { + logger.warn('Collection bundle is missing x-mitre-collection object.'); + errorResult.bundleErrors.noCollection = true; + errorFound = true; + } else if (collections.length > 1) { + logger.warn('Collection bundle has more than one x-mitre-collection object.'); + errorResult.bundleErrors.moreThanOneCollection = true; + errorFound = true; + } + + // The collection must have an id. + if (collections.length > 0 && !collections[0].id) { + logger.warn('Badly formatted collection in bundle, x-mitre-collection missing id.'); + errorResult.bundleErrors.badlyFormattedCollection = true; + errorFound = true; + } + + const validationResult = collectionBundlesService.validateBundle(collectionBundleData); + if (validationResult.errors.length > 0) { + errorFound = true; + if (validationResult.duplicateObjectInBundleCount > 0) { + logger.warn( + `Collection bundle has ${validationResult.duplicateObjectInBundleCount} duplicate objects.`, + ); + errorResult.objectErrors.summary.duplicateObjectInBundleCount = + validationResult.duplicateObjectInBundleCount; } - else if (collections.length > 1) { - logger.warn("Collection bundle has more than one x-mitre-collection object."); - errorResult.bundleErrors.moreThanOneCollection = true; - errorFound = true; - } - - // The collection must have an id. - if (collections.length > 0 && !collections[0].id) { - logger.warn('Badly formatted collection in bundle, x-mitre-collection missing id.'); - errorResult.bundleErrors.badlyFormattedCollection = true; - errorFound = true; - } - - const validationResult = collectionBundlesService.validateBundle(collectionBundleData); - if (validationResult.errors.length > 0) { - errorFound = true; - if (validationResult.duplicateObjectInBundleCount > 0) { - logger.warn(`Collection bundle has ${ validationResult.duplicateObjectInBundleCount } duplicate objects.`); - errorResult.objectErrors.summary.duplicateObjectInBundleCount = validationResult.duplicateObjectInBundleCount; - } - - if (validationResult.invalidAttackSpecVersionCount > 0) { - logger.warn(`Collection bundle has ${ validationResult.invalidAttackSpecVersionCount } objects with invalid ATT&CK Spec Versions.`); - errorResult.objectErrors.summary.invalidAttackSpecVersionCount = validationResult.invalidAttackSpecVersionCount; - } - errorResult.objectErrors.errors.push(...validationResult.errors); + if (validationResult.invalidAttackSpecVersionCount > 0) { + logger.warn( + `Collection bundle has ${validationResult.invalidAttackSpecVersionCount} objects with invalid ATT&CK Spec Versions.`, + ); + errorResult.objectErrors.summary.invalidAttackSpecVersionCount = + validationResult.invalidAttackSpecVersionCount; } - if (errorFound) { - // Determine if any of the errors are overridden by the forceImport flag - - // These errors do not have forceImport flags yet - if (errorResult.bundleErrors.noCollection || - errorResult.bundleErrors.moreThanOneCollection || - errorResult.bundleErrors.badlyFormattedCollection || - errorResult.objectErrors.summary.duplicateObjectInBundleCount > 0) { - logger.error('Unable to import collection bundle due to an error in the bundle.'); - return res.status(400).send(errorResult); - } - - // Check the forceImport flag for overriding ATT&CK Spec version violations - if (errorResult.objectErrors.summary.invalidAttackSpecVersionCount > 0 && - !forceImportParameters.find(e => e === collectionBundlesService.forceImportParameters.attackSpecVersionViolations)) { - logger.error('Unable to import collection bundle due to an error in the bundle.'); - return res.status(400).send(errorResult); - } + errorResult.objectErrors.errors.push(...validationResult.errors); + } + + if (errorFound) { + // Determine if any of the errors are overridden by the forceImport flag + + // These errors do not have forceImport flags yet + if ( + errorResult.bundleErrors.noCollection || + errorResult.bundleErrors.moreThanOneCollection || + errorResult.bundleErrors.badlyFormattedCollection || + errorResult.objectErrors.summary.duplicateObjectInBundleCount > 0 + ) { + logger.error('Unable to import collection bundle due to an error in the bundle.'); + return res.status(400).send(errorResult); } - const options = { - previewOnly: req.query.previewOnly || req.query.checkOnly, - forceImportParameters + // Check the forceImport flag for overriding ATT&CK Spec version violations + if ( + errorResult.objectErrors.summary.invalidAttackSpecVersionCount > 0 && + !forceImportParameters.find( + (e) => e === collectionBundlesService.forceImportParameters.attackSpecVersionViolations, + ) + ) { + logger.error('Unable to import collection bundle due to an error in the bundle.'); + return res.status(400).send(errorResult); } - - // Import the collection bundle - collectionBundlesService.importBundle(collections[0], collectionBundleData, options, function(err, importedCollection) { - if (err) { - if (err.message === collectionBundlesService.errors.duplicateCollection) { - errorResult.bundleErrors.duplicateCollection = true; - logger.error('Unable to import collection, duplicate x-mitre-collection.'); - return res.status(400).send(errorResult); - } - else { - logger.error("Unable to import collection, create collection index failed with error: " + err); - return res.status(500).send("Unable to import collection, unable to create collection index. Server error."); - } + } + + const options = { + previewOnly: req.query.previewOnly || req.query.checkOnly, + forceImportParameters, + }; + + // Import the collection bundle + collectionBundlesService.importBundle( + collections[0], + collectionBundleData, + options, + function (err, importedCollection) { + if (err) { + if (err.message === collectionBundlesService.errors.duplicateCollection) { + errorResult.bundleErrors.duplicateCollection = true; + logger.error('Unable to import collection, duplicate x-mitre-collection.'); + return res.status(400).send(errorResult); + } else { + logger.error( + 'Unable to import collection, create collection index failed with error: ' + err, + ); + return res + .status(500) + .send('Unable to import collection, unable to create collection index. Server error.'); } - else { - if (req.query.checkOnly) { - logger.debug("Success: Previewed import of collection with id " + importedCollection.stix.id); - return res.status(201).send(importedCollection); - } - else { - logger.debug("Success: Imported collection with id " + importedCollection.stix.id); - return res.status(201).send(importedCollection); - } + } else { + if (req.query.checkOnly) { + logger.debug( + 'Success: Previewed import of collection with id ' + importedCollection.stix.id, + ); + return res.status(201).send(importedCollection); + } else { + logger.debug('Success: Imported collection with id ' + importedCollection.stix.id); + return res.status(201).send(importedCollection); } - }); + } + }, + ); }; -exports.exportBundle = async function(req, res) { - if (req.query.collectionModified && !req.query.collectionId) { - return res.status(400).send('collectionId is required when providing collectionModified'); - } - - const options = { - collectionId: req.query.collectionId, - collectionModified: req.query.collectionModified, - previewOnly: req.query.previewOnly, - includeNotes: req.query.includeNotes - }; - - try { - const collectionBundle = await collectionBundlesService.exportBundle(options); - return res.status(200).send(collectionBundle); - } - catch(err) { - if (err.message === collectionBundlesService.errors.notFound) { - return res.status(404).send('Collection not found'); - } - else { - logger.error('Unable to export collection: ' + err); - return res.status(500).send('Unable to export collection.'); - } +exports.exportBundle = async function (req, res) { + if (req.query.collectionModified && !req.query.collectionId) { + return res.status(400).send('collectionId is required when providing collectionModified'); + } + + const options = { + collectionId: req.query.collectionId, + collectionModified: req.query.collectionModified, + previewOnly: req.query.previewOnly, + includeNotes: req.query.includeNotes, + }; + + try { + const collectionBundle = await collectionBundlesService.exportBundle(options); + return res.status(200).send(collectionBundle); + } catch (err) { + if (err.message === collectionBundlesService.errors.notFound) { + return res.status(404).send('Collection not found'); + } else { + logger.error('Unable to export collection: ' + err); + return res.status(500).send('Unable to export collection.'); } -} - + } +}; diff --git a/app/controllers/collection-indexes-controller.js b/app/controllers/collection-indexes-controller.js index 2de3a803..3a386540 100644 --- a/app/controllers/collection-indexes-controller.js +++ b/app/controllers/collection-indexes-controller.js @@ -3,148 +3,141 @@ const collectionIndexService = require('../services/collection-indexes-service'); const logger = require('../lib/logger'); -exports.retrieveAll = function(req, res) { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0 +exports.retrieveAll = function (req, res) { + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + }; + + collectionIndexService.retrieveAll(options, function (err, collectionIndexes) { + if (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get collection indexes. Server error.'); + } else { + logger.debug(`Success: Retrieved ${collectionIndexes.length} collectionIndex(es)`); + return res.status(200).send(collectionIndexes); } - - collectionIndexService.retrieveAll(options, function(err, collectionIndexes) { - if (err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get collection indexes. Server error.'); - } - else { - logger.debug(`Success: Retrieved ${ collectionIndexes.length } collectionIndex(es)`); - return res.status(200).send(collectionIndexes); - } - }); + }); }; -exports.retrieveById = function(req, res) { - // Get the id from the request - const id = req.params.id; - - collectionIndexService.retrieveById(id, function (err, collectionIndex) { - if (err) { - if (err.message === collectionIndexService.errors.badlyFormattedParameter) { - logger.warn('Badly formatted id: ' + id); - return res.status(400).send('Id is badly formatted.'); - } - else { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get collection index. Server error.'); - } - } - else { - if (collectionIndex) { - return res.status(200).send(collectionIndex); - } - else { - return res.status(404).send('Collection Index not found.'); - } - } - }); +exports.retrieveById = function (req, res) { + // Get the id from the request + const id = req.params.id; + + collectionIndexService.retrieveById(id, function (err, collectionIndex) { + if (err) { + if (err.message === collectionIndexService.errors.badlyFormattedParameter) { + logger.warn('Badly formatted id: ' + id); + return res.status(400).send('Id is badly formatted.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get collection index. Server error.'); + } + } else { + if (collectionIndex) { + return res.status(200).send(collectionIndex); + } else { + return res.status(404).send('Collection Index not found.'); + } + } + }); }; -exports.create = function(req, res) { - // Get the data from the request - const collectionIndexData = req.body; - - // The collection index must have an id. - if (!collectionIndexData.collection_index.id) { - logger.warn('Create collection index failed: Missing id'); - return res.status(400).send('Unable to create collection index. Missing id.'); +exports.create = function (req, res) { + // Get the data from the request + const collectionIndexData = req.body; + + // The collection index must have an id. + if (!collectionIndexData.collection_index.id) { + logger.warn('Create collection index failed: Missing id'); + return res.status(400).send('Unable to create collection index. Missing id.'); + } + + // Create the collection index + collectionIndexService.create(collectionIndexData, function (err, collectionIndex) { + if (err) { + if (err.message === collectionIndexService.errors.duplicateId) { + logger.warn('Create collection index failed: Duplicate id'); + return res.status(409).send('Unable to create collection index. Duplicate id.'); + } else { + logger.error('Create collection index failed with error: ' + err); + return res.status(500).send('Unable to create collection index. Server error.'); + } + } else { + logger.debug( + 'Success: Created collection index with id ' + collectionIndex.collection_index.id, + ); + return res.status(201).send(collectionIndex); } - - // Create the collection index - collectionIndexService.create(collectionIndexData, function(err, collectionIndex) { - if (err) { - if (err.message === collectionIndexService.errors.duplicateId) { - logger.warn("Create collection index failed: Duplicate id"); - return res.status(409).send('Unable to create collection index. Duplicate id.'); - } - else { - logger.error("Create collection index failed with error: " + err); - return res.status(500).send("Unable to create collection index. Server error."); - } - } - else { - logger.debug("Success: Created collection index with id " + collectionIndex.collection_index.id); - return res.status(201).send(collectionIndex); - } - }); + }); }; -exports.updateFull = function(req, res) { - // Get the data and id from the request - const collectionIndexData = req.body; - const id = req.params.id; - - if (!id) { - logger.error('Delete collection index failed with error: Missing id'); - return res.status(400).send('Unable to delete collection index. Missing id.') +exports.updateFull = function (req, res) { + // Get the data and id from the request + const collectionIndexData = req.body; + const id = req.params.id; + + if (!id) { + logger.error('Delete collection index failed with error: Missing id'); + return res.status(400).send('Unable to delete collection index. Missing id.'); + } + + // Update the collection index + collectionIndexService.updateFull(id, collectionIndexData, function (err, collectionIndex) { + if (err) { + logger.error('Update collection index failed with error: ' + err); + return res.status(500).send('Unable to update collection index. Server error.'); + } else { + if (!collectionIndex) { + return res.status(404).send('Collection index not found.'); + } else { + logger.debug('Success: Updated collection index.'); + return res.status(200).send(collectionIndex); + } } - - // Update the collection index - collectionIndexService.updateFull(id, collectionIndexData, function(err, collectionIndex) { - if (err) { - logger.error("Update collection index failed with error: " + err); - return res.status(500).send("Unable to update collection index. Server error."); - } - else { - if (!collectionIndex) { - return res.status(404).send('Collection index not found.'); - } else { - logger.debug('Success: Updated collection index.'); - return res.status(200).send(collectionIndex); - } - } - }); + }); }; -exports.delete = function(req, res) { - // Get the id from the request - const id = req.params.id; - - if (!id) { - logger.error('Delete collection index failed with error: Missing id'); - return res.status(400).send('Unable to delete collection index. Missing id.') +exports.delete = function (req, res) { + // Get the id from the request + const id = req.params.id; + + if (!id) { + logger.error('Delete collection index failed with error: Missing id'); + return res.status(400).send('Unable to delete collection index. Missing id.'); + } + + // Delete the collection index + collectionIndexService.delete(id, function (err, collectionIndex) { + if (err) { + logger.error('Delete collection index failed with error: ' + err); + return res.status(500).send('Unable to delete collection index. Server error.'); + } else { + if (!collectionIndex) { + return res.status(404).send('Collection index not found.'); + } else { + logger.debug('Success: Deleted collection index.'); + return res.status(204).end(); + } } - - // Delete the collection index - collectionIndexService.delete(id, function (err, collectionIndex) { - if (err) { - logger.error('Delete collection index failed with error: ' + err); - return res.status(500).send('Unable to delete collection index. Server error.'); - } - else { - if (!collectionIndex) { - return res.status(404).send('Collection index not found.'); - } else { - logger.debug('Success: Deleted collection index.'); - return res.status(204).end(); - } - } - }); + }); }; -exports.refresh = function(req, res) { - const id = req.params.id; - - if (!id) { - logger.warn('Refresh collection index failed with error: Missing id'); - return res.status(400).send('Unable to refresh collection index. Missing id.') +exports.refresh = function (req, res) { + const id = req.params.id; + + if (!id) { + logger.warn('Refresh collection index failed with error: Missing id'); + return res.status(400).send('Unable to refresh collection index. Missing id.'); + } + + collectionIndexService.refresh(id, function (err, collectionIndex) { + if (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to refresh collection index. Server error.'); + } else { + logger.debug('Success: Refreshed collection index'); + return res.status(200).send(collectionIndex); } - - collectionIndexService.refresh(id, function(err, collectionIndex) { - if (err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to refresh collection index. Server error.'); - } - else { - logger.debug("Success: Refreshed collection index"); - return res.status(200).send(collectionIndex); - } - }); + }); }; diff --git a/app/controllers/collections-controller.js b/app/controllers/collections-controller.js index 47448594..9228ee3d 100644 --- a/app/controllers/collections-controller.js +++ b/app/controllers/collections-controller.js @@ -3,147 +3,157 @@ const collectionsService = require('../services/collections-service'); const logger = require('../lib/logger'); -exports.retrieveAll = function(req, res) { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0, - versions: req.query.versions || 'latest', - state: req.query.state, - includeRevoked: req.query.includeRevoked, - includeDeprecated: req.query.includeDeprecated, - search: req.query.search, - lastUpdatedBy: req.query.lastUpdatedBy, - } +exports.retrieveAll = function (req, res) { + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + versions: req.query.versions || 'latest', + state: req.query.state, + includeRevoked: req.query.includeRevoked, + includeDeprecated: req.query.includeDeprecated, + search: req.query.search, + lastUpdatedBy: req.query.lastUpdatedBy, + }; - collectionsService.retrieveAll(options, function(err, collections) { - if (err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get collections. Server error.'); - } - else { - logger.debug(`Success: Retrieved ${ collections.length } collection(s)`); - return res.status(200).send(collections); - } - }); + collectionsService.retrieveAll(options, function (err, collections) { + if (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get collections. Server error.'); + } else { + logger.debug(`Success: Retrieved ${collections.length} collection(s)`); + return res.status(200).send(collections); + } + }); }; -exports.retrieveById = function(req, res) { - const options = { - versions: req.query.versions || 'latest', - retrieveContents: req.query.retrieveContents +exports.retrieveById = function (req, res) { + const options = { + versions: req.query.versions || 'latest', + retrieveContents: req.query.retrieveContents, + }; + + collectionsService.retrieveById(req.params.stixId, options, function (err, collections) { + if (err) { + if (err.message === collectionsService.errors.badlyFormattedParameter) { + logger.warn('Badly formatted stix id: ' + req.params.stixId); + return res.status(400).send('Stix id is badly formatted.'); + } else if (err.message === collectionsService.errors.invalidQueryStringParameter) { + logger.warn('Invalid query string: versions=' + req.query.versions); + return res.status(400).send('Query string parameter versions is invalid.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get collections. Server error.'); + } + } else { + if (collections.length === 0) { + return res.status(404).send('Collection not found.'); + } else { + logger.debug( + `Success: Retrieved ${collections.length} collection(s) with id ${req.params.stixId}`, + ); + return res.status(200).send(collections); + } } + }); +}; - collectionsService.retrieveById(req.params.stixId, options, function (err, collections) { - if (err) { - if (err.message === collectionsService.errors.badlyFormattedParameter) { - logger.warn('Badly formatted stix id: ' + req.params.stixId); - return res.status(400).send('Stix id is badly formatted.'); - } - else if (err.message === collectionsService.errors.invalidQueryStringParameter) { - logger.warn('Invalid query string: versions=' + req.query.versions); - return res.status(400).send('Query string parameter versions is invalid.'); - } - else { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get collections. Server error.'); - } +exports.retrieveVersionById = function (req, res) { + const options = { + retrieveContents: req.query.retrieveContents, + }; + collectionsService.retrieveVersionById( + req.params.stixId, + req.params.modified, + options, + function (err, collection) { + if (err) { + if (err.message === collectionsService.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 collections. Server error.'); } - else { - if (collections.length === 0) { - return res.status(404).send('Collection not found.'); - } - else { - logger.debug(`Success: Retrieved ${ collections.length } collection(s) with id ${ req.params.stixId }`); - return res.status(200).send(collections); - } + } else { + if (!collection) { + return res.status(404).send('Collection not found.'); + } else { + logger.debug(`Success: Retrieved collection with id ${collection.id}`); + return res.status(200).send(collection); } - }); + } + }, + ); }; -exports.retrieveVersionById = function(req, res) { - const options = { - retrieveContents: req.query.retrieveContents - } - collectionsService.retrieveVersionById(req.params.stixId, req.params.modified, options, function(err, collection) { - if (err) { - if (err.message === collectionsService.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 collections. Server error.'); - } - } - else { - if (!collection) { - return res.status(404).send('Collection not found.'); - } - else { - logger.debug(`Success: Retrieved collection with id ${collection.id}`); - return res.status(200).send(collection); - } - } - }) -} - -exports.create = async function(req, res) { - // Get the data from the request - const collectionData = req.body; +exports.create = async function (req, res) { + // Get the data from the request + const collectionData = req.body; - // Create the collection - const options = { - addObjectsToCollection: true, - import: false, - userAccountId: req.user?.userAccountId - }; - try { - const { savedCollection, insertionErrors } = await collectionsService.create(collectionData, options); - logger.debug('Success: Created collection with id ' + savedCollection.stix.id); - if (insertionErrors.length > 0) { - logger.info(`There were ${ insertionErrors.length } errors while marking the objects in the collection.`); - } - return res.status(201).send(savedCollection); + // Create the collection + const options = { + addObjectsToCollection: true, + import: false, + userAccountId: req.user?.userAccountId, + }; + try { + const { savedCollection, insertionErrors } = await collectionsService.create( + collectionData, + options, + ); + logger.debug('Success: Created collection with id ' + savedCollection.stix.id); + if (insertionErrors.length > 0) { + logger.info( + `There were ${insertionErrors.length} errors while marking the objects in the collection.`, + ); } - catch(err) { - if (err.message === collectionsService.errors.duplicateId) { - logger.warn("Duplicate stix.id and stix.modified"); - return res.status(409).send('Unable to create collection. Duplicate stix.id and stix.modified properties.'); - } - else { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to create collection. Server error."); - } + return res.status(201).send(savedCollection); + } catch (err) { + if (err.message === collectionsService.errors.duplicateId) { + logger.warn('Duplicate stix.id and stix.modified'); + return res + .status(409) + .send('Unable to create collection. Duplicate stix.id and stix.modified properties.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to create collection. Server error.'); } + } }; -exports.delete = async function(req, res) { - try { - const removedCollections = await collectionsService.delete(req.params.stixId, req.query.deleteAllContents); - if (removedCollections.length === 0) { - return res.status(404).send('Collection not found.'); - } else { - logger.debug("Success: Deleted collection with id " + removedCollections[0].id); - return res.status(204).end(); - } - } catch (error) { - logger.error('Delete collections failed. ' + error); - return res.status(500).send('Unable to delete collections. Server error.'); +exports.delete = async function (req, res) { + try { + const removedCollections = await collectionsService.delete( + req.params.stixId, + req.query.deleteAllContents, + ); + if (removedCollections.length === 0) { + return res.status(404).send('Collection not found.'); + } else { + logger.debug('Success: Deleted collection with id ' + removedCollections[0].id); + return res.status(204).end(); } + } catch (error) { + logger.error('Delete collections failed. ' + error); + return res.status(500).send('Unable to delete collections. Server error.'); + } }; - -exports.deleteVersionById = async function(req, res) { - try { - const removedCollection = await collectionsService.deleteVersionById(req.params.stixId, req.params.modified, req.query.deleteAllContents); - if (!removedCollection) { - return res.status(404).send('Collection not found.'); - } else { - logger.debug("Success: Deleted collection with id " + removedCollection.id); - return res.status(204).end(); - } - } catch (error) { +exports.deleteVersionById = async function (req, res) { + try { + const removedCollection = await collectionsService.deleteVersionById( + req.params.stixId, + req.params.modified, + req.query.deleteAllContents, + ); + if (!removedCollection) { + return res.status(404).send('Collection not found.'); + } else { + logger.debug('Success: Deleted collection with id ' + removedCollection.id); + return res.status(204).end(); + } + } catch (error) { logger.error('Delete collection failed. ' + error); return res.status(500).send('Unable to delete collection. Server error.'); - } + } }; diff --git a/app/controllers/data-components-controller.js b/app/controllers/data-components-controller.js index 74c7acc4..74a1ec2d 100644 --- a/app/controllers/data-components-controller.js +++ b/app/controllers/data-components-controller.js @@ -2,166 +2,171 @@ const dataComponentsService = require('../services/data-components-service'); const logger = require('../lib/logger'); -const { BadlyFormattedParameterError, InvalidQueryStringParameterError, DuplicateIdError } = require('../exceptions'); - -exports.retrieveAll = async function(req, res) { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0, - state: req.query.state, - includeRevoked: req.query.includeRevoked, - includeDeprecated: req.query.includeDeprecated, - search: req.query.search, - lastUpdatedBy: req.query.lastUpdatedBy, - includePagination: req.query.includePagination - } - - try { - const results = await dataComponentsService.retrieveAll(options); - if (options.includePagination) { - logger.debug(`Success: Retrieved ${ results.data.length } of ${ results.pagination.total } total data component(s)`); - } - else { - logger.debug(`Success: Retrieved ${ results.length } data component(s)`); - } - return res.status(200).send(results); - } catch (err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get data components. Server error.'); +const { + BadlyFormattedParameterError, + InvalidQueryStringParameterError, + DuplicateIdError, +} = require('../exceptions'); + +exports.retrieveAll = async function (req, res) { + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + state: req.query.state, + includeRevoked: req.query.includeRevoked, + includeDeprecated: req.query.includeDeprecated, + search: req.query.search, + lastUpdatedBy: req.query.lastUpdatedBy, + includePagination: req.query.includePagination, + }; + + try { + const results = await dataComponentsService.retrieveAll(options); + if (options.includePagination) { + logger.debug( + `Success: Retrieved ${results.data.length} of ${results.pagination.total} total data component(s)`, + ); + } else { + logger.debug(`Success: Retrieved ${results.length} data component(s)`); } - + return res.status(200).send(results); + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get data components. Server error.'); + } }; -exports.retrieveById = async function(req, res) { - const options = { - versions: req.query.versions || 'latest' +exports.retrieveById = async function (req, res) { + const options = { + versions: req.query.versions || 'latest', + }; + + try { + const dataComponents = await dataComponentsService.retrieveById(req.params.stixId, options); + if (dataComponents.length === 0) { + return res.status(404).send('Data component not found.'); + } else { + logger.debug( + `Success: Retrieved ${dataComponents.length} data component(s) with id ${req.params.stixId}`, + ); + return res.status(200).send(dataComponents); } - - try { - const dataComponents = await dataComponentsService.retrieveById(req.params.stixId, options); - if (dataComponents.length === 0) { - return res.status(404).send('Data component not found.'); - } - else { - logger.debug(`Success: Retrieved ${ dataComponents.length } data component(s) with id ${ req.params.stixId }`); - return res.status(200).send(dataComponents); - } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - logger.warn('Badly formatted stix id: ' + req.params.stixId); - return res.status(400).send('Stix id is badly formatted.'); - } - else if (err instanceof InvalidQueryStringParameterError) { - logger.warn('Invalid query string: versions=' + req.query.versions); - return res.status(400).send('Query string parameter versions is invalid.'); - } - else { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get data component. Server error.'); - } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + logger.warn('Badly formatted stix id: ' + req.params.stixId); + return res.status(400).send('Stix id is badly formatted.'); + } else if (err instanceof InvalidQueryStringParameterError) { + logger.warn('Invalid query string: versions=' + req.query.versions); + return res.status(400).send('Query string parameter versions is invalid.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get data component. Server error.'); } - -}; - -exports.retrieveVersionById = async function(req, res) { - try { - const dataComponent = await dataComponentsService.retrieveVersionById(req.params.stixId, req.params.modified); - if (!dataComponent) { - return res.status(404).send('Data component not found.'); - } - else { - logger.debug(`Success: Retrieved data component with id ${dataComponent.id}`); - return res.status(200).send(dataComponent); - } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - 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 data component. Server error.'); - } - } + } }; -exports.create = async function(req, res) { - // Get the data from the request - const dataComponentData = req.body; - const options = { - import: false, - userAccountId: req.user?.userAccountId - }; - - // Create the data component - try { - const dataComponent = await dataComponentsService.create(dataComponentData, options); - logger.debug("Success: Created data component with id " + dataComponent.stix.id); - return res.status(201).send(dataComponent); +exports.retrieveVersionById = async function (req, res) { + try { + const dataComponent = await dataComponentsService.retrieveVersionById( + req.params.stixId, + req.params.modified, + ); + if (!dataComponent) { + return res.status(404).send('Data component not found.'); + } else { + logger.debug(`Success: Retrieved data component with id ${dataComponent.id}`); + return res.status(200).send(dataComponent); } - catch(err) { - if (err instanceof DuplicateIdError) { - logger.warn("Duplicate stix.id and stix.modified"); - return res.status(409).send('Unable to create data component. Duplicate stix.id and stix.modified properties.'); - } - else { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to create data component. Server error."); - } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + 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 data component. Server error.'); } + } }; -exports.updateFull = async function(req, res) { - // Get the data from the request - const dataComponentData = req.body; - - // Create the data component - - try { - const dataComponent = await dataComponentsService.updateFull(req.params.stixId, req.params.modified, dataComponentData); - if (!dataComponent) { - return res.status(404).send('Data component not found.'); - } else { - logger.debug("Success: Updated data component with id " + dataComponent.stix.id); - return res.status(200).send(dataComponent); - } - } catch (err) { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to update data component. Server error."); +exports.create = async function (req, res) { + // Get the data from the request + const dataComponentData = req.body; + const options = { + import: false, + userAccountId: req.user?.userAccountId, + }; + + // Create the data component + try { + const dataComponent = await dataComponentsService.create(dataComponentData, options); + logger.debug('Success: Created data component with id ' + dataComponent.stix.id); + return res.status(201).send(dataComponent); + } catch (err) { + if (err instanceof DuplicateIdError) { + logger.warn('Duplicate stix.id and stix.modified'); + return res + .status(409) + .send('Unable to create data component. Duplicate stix.id and stix.modified properties.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to create data component. Server error.'); } - + } }; -exports.deleteVersionById = async function(req, res) { - - try { - const dataComponent = await dataComponentsService.deleteVersionById(req.params.stixId, req.params.modified); - if (!dataComponent) { - return res.status(404).send('Data component not found.'); - } else { - logger.debug("Success: Deleted data component with id " + dataComponent.stix.id); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete data component failed. ' + err); - return res.status(500).send('Unable to delete data component. Server error.'); +exports.updateFull = async function (req, res) { + // Get the data from the request + const dataComponentData = req.body; + + // Create the data component + + try { + const dataComponent = await dataComponentsService.updateFull( + req.params.stixId, + req.params.modified, + dataComponentData, + ); + if (!dataComponent) { + return res.status(404).send('Data component not found.'); + } else { + logger.debug('Success: Updated data component with id ' + dataComponent.stix.id); + return res.status(200).send(dataComponent); } + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to update data component. Server error.'); + } +}; +exports.deleteVersionById = async function (req, res) { + try { + const dataComponent = await dataComponentsService.deleteVersionById( + req.params.stixId, + req.params.modified, + ); + if (!dataComponent) { + return res.status(404).send('Data component not found.'); + } else { + logger.debug('Success: Deleted data component with id ' + dataComponent.stix.id); + return res.status(204).end(); + } + } catch (err) { + logger.error('Delete data component failed. ' + err); + return res.status(500).send('Unable to delete data component. Server error.'); + } }; -exports.deleteById = async function(req, res) { - - try { - const dataComponents = await dataComponentsService.deleteById(req.params.stixId); - if (dataComponents.deletedCount === 0) { - return res.status(404).send('Data Component not found.'); - } - else { - logger.debug(`Success: Deleted data component with id ${ req.params.stixId }`); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete data component failed. ' + err); - return res.status(500).send('Unable to delete data component. Server error.'); +exports.deleteById = async function (req, res) { + try { + const dataComponents = await dataComponentsService.deleteById(req.params.stixId); + if (dataComponents.deletedCount === 0) { + return res.status(404).send('Data Component not found.'); + } else { + logger.debug(`Success: Deleted data component with id ${req.params.stixId}`); + return res.status(204).end(); } + } catch (err) { + logger.error('Delete data component failed. ' + err); + return res.status(500).send('Unable to delete data component. Server error.'); + } }; diff --git a/app/controllers/data-sources-controller.js b/app/controllers/data-sources-controller.js index a0ccce2d..4e2190f1 100644 --- a/app/controllers/data-sources-controller.js +++ b/app/controllers/data-sources-controller.js @@ -2,168 +2,176 @@ const dataSourcesService = require('../services/data-sources-service'); const logger = require('../lib/logger'); -const { DuplicateIdError, BadlyFormattedParameterError, InvalidQueryStringParameterError } = require('../exceptions'); +const { + DuplicateIdError, + BadlyFormattedParameterError, + InvalidQueryStringParameterError, +} = require('../exceptions'); -exports.retrieveAll = async function(req, res) { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0, - domain: req.query.domain, - platform: req.query.platform, - state: req.query.state, - includeRevoked: req.query.includeRevoked, - includeDeprecated: req.query.includeDeprecated, - search: req.query.search, - lastUpdatedBy: req.query.lastUpdatedBy, - includePagination: req.query.includePagination +exports.retrieveAll = async function (req, res) { + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + domain: req.query.domain, + platform: req.query.platform, + state: req.query.state, + includeRevoked: req.query.includeRevoked, + includeDeprecated: req.query.includeDeprecated, + search: req.query.search, + lastUpdatedBy: req.query.lastUpdatedBy, + includePagination: req.query.includePagination, + }; + try { + const results = await dataSourcesService.retrieveAll(options); + if (options.includePagination) { + logger.debug( + `Success: Retrieved ${results.data.length} of ${results.pagination.total} total data source(s)`, + ); + } else { + logger.debug(`Success: Retrieved ${results.length} data source(s)`); } - try { - const results = await dataSourcesService.retrieveAll(options); - if (options.includePagination) { - logger.debug(`Success: Retrieved ${ results.data.length } of ${ results.pagination.total } total data source(s)`); - } - else { - logger.debug(`Success: Retrieved ${ results.length } data source(s)`); - } - return res.status(200).send(results); - } catch (err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get data sources. Server error.'); - } - + return res.status(200).send(results); + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get data sources. Server error.'); + } }; -exports.retrieveById = async function(req, res) { - const options = { - versions: req.query.versions || 'latest', - retrieveDataComponents: req.query.retrieveDataComponents - } - try { - const dataSources = await dataSourcesService.retrieveById(req.params.stixId, options); - if (dataSources.length === 0) { - return res.status(404).send('Data source not found.'); - } - else { - logger.debug(`Success: Retrieved ${ dataSources.length } data source(s) with id ${ req.params.stixId }`); - return res.status(200).send(dataSources); - } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - logger.warn('Badly formatted stix id: ' + req.params.stixId); - return res.status(400).send('Stix id is badly formatted.'); - } - else if (err instanceof InvalidQueryStringParameterError) { - logger.warn('Invalid query string: versions=' + req.query.versions); - return res.status(400).send('Query string parameter versions is invalid.'); - } - else { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get data sources. Server error.'); - } +exports.retrieveById = async function (req, res) { + const options = { + versions: req.query.versions || 'latest', + retrieveDataComponents: req.query.retrieveDataComponents, + }; + try { + const dataSources = await dataSourcesService.retrieveById(req.params.stixId, options); + if (dataSources.length === 0) { + return res.status(404).send('Data source not found.'); + } else { + logger.debug( + `Success: Retrieved ${dataSources.length} data source(s) with id ${req.params.stixId}`, + ); + return res.status(200).send(dataSources); } -}; - -exports.retrieveVersionById = async function(req, res) { - const options = { - retrieveDataComponents: req.query.retrieveDataComponents + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + logger.warn('Badly formatted stix id: ' + req.params.stixId); + return res.status(400).send('Stix id is badly formatted.'); + } else if (err instanceof InvalidQueryStringParameterError) { + logger.warn('Invalid query string: versions=' + req.query.versions); + return res.status(400).send('Query string parameter versions is invalid.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get data sources. Server error.'); } - - try { - const dataSource = await dataSourcesService.retrieveVersionById(req.params.stixId, req.params.modified, options); - if (!dataSource) { - return res.status(404).send('Data source not found.'); - } - else { - logger.debug(`Success: Retrieved data source with id ${dataSource.id}`); - return res.status(200).send(dataSource); - } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - 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 data source. Server error.'); - } - } + } }; -exports.create = async function(req, res) { - // Get the data from the request - const dataSourceData = req.body; +exports.retrieveVersionById = async function (req, res) { + const options = { + retrieveDataComponents: req.query.retrieveDataComponents, + }; - // Create the data source - try { - const options = { - import: false, - userAccountId: req.user?.userAccountId - }; - const dataSource = await dataSourcesService.create(dataSourceData, options); - logger.debug("Success: Created data source with id " + dataSource.stix.id); - return res.status(201).send(dataSource); + try { + const dataSource = await dataSourcesService.retrieveVersionById( + req.params.stixId, + req.params.modified, + options, + ); + if (!dataSource) { + return res.status(404).send('Data source not found.'); + } else { + logger.debug(`Success: Retrieved data source with id ${dataSource.id}`); + return res.status(200).send(dataSource); } - catch(err) { - if (err instanceof DuplicateIdError) { - logger.warn("Duplicate stix.id and stix.modified"); - return res.status(409).send('Unable to create data source. Duplicate stix.id and stix.modified properties.'); - } - else { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to create data source. Server error."); - } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + 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 data source. Server error.'); } + } }; -exports.updateFull = async function(req, res) { - // Get the data from the request - const dataSourceData = req.body; - - // Create the data source - try { - const dataSource = await dataSourcesService.updateFull(req.params.stixId, req.params.modified, dataSourceData); - if (!dataSource) { - return res.status(404).send('Data source not found.'); - } else { - logger.debug("Success: Updated data source with id " + dataSource.stix.id); - return res.status(200).send(dataSource); - } - } catch (err) { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to update data source. Server error."); - } +exports.create = async function (req, res) { + // Get the data from the request + const dataSourceData = req.body; + // Create the data source + try { + const options = { + import: false, + userAccountId: req.user?.userAccountId, + }; + const dataSource = await dataSourcesService.create(dataSourceData, options); + logger.debug('Success: Created data source with id ' + dataSource.stix.id); + return res.status(201).send(dataSource); + } catch (err) { + if (err instanceof DuplicateIdError) { + logger.warn('Duplicate stix.id and stix.modified'); + return res + .status(409) + .send('Unable to create data source. Duplicate stix.id and stix.modified properties.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to create data source. Server error.'); + } + } }; -exports.deleteVersionById = async function(req, res) { +exports.updateFull = async function (req, res) { + // Get the data from the request + const dataSourceData = req.body; - try { - const dataSource = await dataSourcesService.deleteVersionById(req.params.stixId, req.params.modified); - if (!dataSource) { - return res.status(404).send('Data source not found.'); - } else { - logger.debug("Success: Deleted data source with id " + dataSource.stix.id); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete data source failed. ' + err); - return res.status(500).send('Unable to delete data source. Server error.'); - } + // Create the data source + try { + const dataSource = await dataSourcesService.updateFull( + req.params.stixId, + req.params.modified, + dataSourceData, + ); + if (!dataSource) { + return res.status(404).send('Data source not found.'); + } else { + logger.debug('Success: Updated data source with id ' + dataSource.stix.id); + return res.status(200).send(dataSource); + } + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to update data source. Server error.'); + } +}; +exports.deleteVersionById = async function (req, res) { + try { + const dataSource = await dataSourcesService.deleteVersionById( + req.params.stixId, + req.params.modified, + ); + if (!dataSource) { + return res.status(404).send('Data source not found.'); + } else { + logger.debug('Success: Deleted data source with id ' + dataSource.stix.id); + return res.status(204).end(); + } + } catch (err) { + logger.error('Delete data source failed. ' + err); + return res.status(500).send('Unable to delete data source. Server error.'); + } }; -exports.deleteById = async function(req, res) { - try { - const dataSources = await dataSourcesService.deleteById(req.params.stixId); - if (dataSources.deletedCount === 0) { - return res.status(404).send('Data Sources not found.'); - } - else { - logger.debug(`Success: Deleted data source with id ${ req.params.stixId }`); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete data source failed. ' + err); - return res.status(500).send('Unable to delete data source. Server error.'); - } +exports.deleteById = async function (req, res) { + try { + const dataSources = await dataSourcesService.deleteById(req.params.stixId); + if (dataSources.deletedCount === 0) { + return res.status(404).send('Data Sources not found.'); + } else { + logger.debug(`Success: Deleted data source with id ${req.params.stixId}`); + return res.status(204).end(); + } + } catch (err) { + logger.error('Delete data source failed. ' + err); + return res.status(500).send('Unable to delete data source. Server error.'); + } }; diff --git a/app/controllers/groups-controller.js b/app/controllers/groups-controller.js index 99251314..d0282f57 100644 --- a/app/controllers/groups-controller.js +++ b/app/controllers/groups-controller.js @@ -2,175 +2,162 @@ const groupsService = require('../services/groups-service'); const logger = require('../lib/logger'); -const { DuplicateIdError, BadlyFormattedParameterError, InvalidTypeError, InvalidQueryStringParameterError } = require('../exceptions'); - -exports.retrieveAll = async function(req, res) { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0, - state: req.query.state, - includeRevoked: req.query.includeRevoked, - includeDeprecated: req.query.includeDeprecated, - search: req.query.search, - lastUpdatedBy: req.query.lastUpdatedBy, - includePagination: req.query.includePagination +const { + DuplicateIdError, + BadlyFormattedParameterError, + InvalidTypeError, + InvalidQueryStringParameterError, +} = require('../exceptions'); + +exports.retrieveAll = async function (req, res) { + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + state: req.query.state, + includeRevoked: req.query.includeRevoked, + includeDeprecated: req.query.includeDeprecated, + search: req.query.search, + lastUpdatedBy: req.query.lastUpdatedBy, + includePagination: req.query.includePagination, + }; + try { + const results = await groupsService.retrieveAll(options); + + if (options.includePagination) { + logger.debug( + `Success: Retrieved ${results.data.length} of ${results.pagination.total} total group(s)`, + ); + } else { + logger.debug(`Success: Retrieved ${results.length} group(s)`); } - try { - const results = await groupsService.retrieveAll(options); - - if (options.includePagination) { - logger.debug(`Success: Retrieved ${ results.data.length } of ${ results.pagination.total } total group(s)`); - } - else { - logger.debug(`Success: Retrieved ${ results.length } group(s)`); - } - return res.status(200).send(results); - } catch (err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get groups. Server error.'); - } - + return res.status(200).send(results); + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get groups. Server error.'); + } }; -exports.retrieveById = async function(req, res) { - const options = { - versions: req.query.versions || 'latest' +exports.retrieveById = async function (req, res) { + const options = { + versions: req.query.versions || 'latest', + }; + try { + const groups = await groupsService.retrieveById(req.params.stixId, options); + + if (groups.length === 0) { + return res.status(404).send('Group not found.'); + } else { + logger.debug(`Success: Retrieved ${groups.length} group(s) with id ${req.params.stixId}`); + return res.status(200).send(groups); } - try { - const groups = await groupsService.retrieveById(req.params.stixId, options); - - - if (groups.length === 0) { - return res.status(404).send('Group not found.'); - } - else { - logger.debug(`Success: Retrieved ${ groups.length } group(s) with id ${ req.params.stixId }`); - return res.status(200).send(groups); - } - - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - logger.warn('Badly formatted stix id: ' + req.params.stixId); - return res.status(400).send('Stix id is badly formatted.'); - } - else if (err instanceof InvalidQueryStringParameterError) { - logger.warn('Invalid query string: versions=' + req.query.versions); - return res.status(400).send('Query string parameter versions is invalid.'); - } - else { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get groups. Server error.'); - } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + logger.warn('Badly formatted stix id: ' + req.params.stixId); + return res.status(400).send('Stix id is badly formatted.'); + } else if (err instanceof InvalidQueryStringParameterError) { + logger.warn('Invalid query string: versions=' + req.query.versions); + return res.status(400).send('Query string parameter versions is invalid.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get groups. Server error.'); } - + } }; -exports.retrieveVersionById = async function(req, res) { - - try { - const group = await groupsService.retrieveVersionById(req.params.stixId, req.params.modified); - if (!group) { - return res.status(404).send('Group not found.'); - } - else { - logger.debug(`Success: Retrieved group with id ${group.id}`); - return res.status(200).send(group); +exports.retrieveVersionById = async function (req, res) { + try { + const group = await groupsService.retrieveVersionById(req.params.stixId, req.params.modified); + if (!group) { + return res.status(404).send('Group not found.'); + } else { + logger.debug(`Success: Retrieved group with id ${group.id}`); + return res.status(200).send(group); } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - 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 group. Server error.'); - } - } - + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + 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 group. Server error.'); + } + } }; -exports.create = async function(req, res) { - // Get the data from the request - const groupData = req.body; - const options = { - import: false, - userAccountId: req.user?.userAccountId - }; - - // Create the group - try { - - const group = await groupsService.create(groupData, options); - logger.debug('Success: Created group with id ' + group.stix.id); - return res.status(201).send(group); - } - catch(err) { - if (err instanceof DuplicateIdError) { - logger.warn('Duplicate stix.id and stix.modified'); - return res.status(409).send('Unable to create group. Duplicate stix.id and stix.modified properties.'); - } - else if (err instanceof InvalidTypeError) { - logger.warn('Invalid stix.type'); - return res.status(400).send('Unable to create group. stix.type must be intrusion-set'); - } - else { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to create group. Server error.'); - } +exports.create = async function (req, res) { + // Get the data from the request + const groupData = req.body; + const options = { + import: false, + userAccountId: req.user?.userAccountId, + }; + + // Create the group + try { + const group = await groupsService.create(groupData, options); + logger.debug('Success: Created group with id ' + group.stix.id); + return res.status(201).send(group); + } catch (err) { + if (err instanceof DuplicateIdError) { + logger.warn('Duplicate stix.id and stix.modified'); + return res + .status(409) + .send('Unable to create group. Duplicate stix.id and stix.modified properties.'); + } else if (err instanceof InvalidTypeError) { + logger.warn('Invalid stix.type'); + return res.status(400).send('Unable to create group. stix.type must be intrusion-set'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to create group. Server error.'); } + } }; -exports.updateFull = async function(req, res) { - // Get the data from the request - const groupData = req.body; +exports.updateFull = async function (req, res) { + // Get the data from the request + const groupData = req.body; - try { - // Create the group - const group = await groupsService.updateFull(req.params.stixId, req.params.modified, groupData); - if (!group) { - return res.status(404).send('Group not found.'); - } else { - logger.debug("Success: Updated group with id " + group.stix.id); - return res.status(200).send(group); - } - } catch (err) { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to update group. Server error."); + try { + // Create the group + const group = await groupsService.updateFull(req.params.stixId, req.params.modified, groupData); + if (!group) { + return res.status(404).send('Group not found.'); + } else { + logger.debug('Success: Updated group with id ' + group.stix.id); + return res.status(200).send(group); } - + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to update group. Server error.'); + } }; -exports.deleteVersionById = async function(req, res) { - - try { - const group = await groupsService.deleteVersionById(req.params.stixId, req.params.modified); - if (!group) { - return res.status(404).send('Group not found.'); - } else { - logger.debug("Success: Deleted group with id " + group.stix.id); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete group failed. ' + err); - return res.status(500).send('Unable to delete group. Server error.'); +exports.deleteVersionById = async function (req, res) { + try { + const group = await groupsService.deleteVersionById(req.params.stixId, req.params.modified); + if (!group) { + return res.status(404).send('Group not found.'); + } else { + logger.debug('Success: Deleted group with id ' + group.stix.id); + return res.status(204).end(); } - + } catch (err) { + logger.error('Delete group failed. ' + err); + return res.status(500).send('Unable to delete group. Server error.'); + } }; -exports.deleteById = async function(req, res) { - - try { - const groups = await groupsService.deleteById(req.params.stixId); - if (groups.deletedCount === 0) { - return res.status(404).send('Group not found.'); - } - else { - logger.debug(`Success: Deleted group with id ${ req.params.stixId }`); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete group failed. ' + err); - return res.status(500).send('Unable to delete group. Server error.'); +exports.deleteById = async function (req, res) { + try { + const groups = await groupsService.deleteById(req.params.stixId); + if (groups.deletedCount === 0) { + return res.status(404).send('Group not found.'); + } else { + logger.debug(`Success: Deleted group with id ${req.params.stixId}`); + return res.status(204).end(); } - + } catch (err) { + logger.error('Delete group failed. ' + err); + return res.status(500).send('Unable to delete group. Server error.'); + } }; diff --git a/app/controllers/health-controller.js b/app/controllers/health-controller.js index 8b573cde..b5cbffc8 100644 --- a/app/controllers/health-controller.js +++ b/app/controllers/health-controller.js @@ -3,21 +3,20 @@ // const mongoose = require('mongoose'); const logger = require('../lib/logger'); -exports.getPing = function(req, res) { - return res.status(204).send(); +exports.getPing = function (req, res) { + return res.status(204).send(); }; -exports.getStatus = function(req, res) { - try { - const status = { - // TBD: check database connection without waiting 30 seconds for timeout when not connected - // dbState: mongoose.STATES[mongoose.connection.readyState], - uptime: process.uptime() - }; - return res.status(200).send(status); - } - catch(err) { - logger.error("Unable to retrieve system status, failed with error: " + err); - return res.status(500).send("Unable to retrieve system status. Server error."); - } +exports.getStatus = function (req, res) { + try { + const status = { + // TBD: check database connection without waiting 30 seconds for timeout when not connected + // dbState: mongoose.STATES[mongoose.connection.readyState], + uptime: process.uptime(), + }; + return res.status(200).send(status); + } catch (err) { + logger.error('Unable to retrieve system status, failed with error: ' + err); + return res.status(500).send('Unable to retrieve system status. Server error.'); + } }; diff --git a/app/controllers/identities-controller.js b/app/controllers/identities-controller.js index d87f6353..46d75a10 100644 --- a/app/controllers/identities-controller.js +++ b/app/controllers/identities-controller.js @@ -2,163 +2,168 @@ const identitiesService = require('../services/identities-service'); const logger = require('../lib/logger'); -const { DuplicateIdError, BadlyFormattedParameterError, InvalidQueryStringParameterError } = require('../exceptions'); - -exports.retrieveAll = async function(req, res) { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0, - state: req.query.state, - includeRevoked: req.query.includeRevoked, - includeDeprecated: req.query.includeDeprecated, - includePagination: req.query.includePagination +const { + DuplicateIdError, + BadlyFormattedParameterError, + InvalidQueryStringParameterError, +} = require('../exceptions'); + +exports.retrieveAll = async function (req, res) { + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + state: req.query.state, + includeRevoked: req.query.includeRevoked, + includeDeprecated: req.query.includeDeprecated, + includePagination: req.query.includePagination, + }; + + try { + const results = await identitiesService.retrieveAll(options); + if (options.includePagination) { + logger.debug( + `Success: Retrieved ${results.data.length} of ${results.pagination.total} total identities`, + ); + } else { + logger.debug(`Success: Retrieved ${results.length} identities`); } - - try { - const results = await identitiesService.retrieveAll(options); - if (options.includePagination) { - logger.debug(`Success: Retrieved ${ results.data.length } of ${ results.pagination.total } total identities`); - } - else { - logger.debug(`Success: Retrieved ${ results.length } identities`); - } - return res.status(200).send(results); - } catch (err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get identities. Server error.'); - } - + return res.status(200).send(results); + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get identities. Server error.'); + } }; -exports.retrieveById = async function(req, res) { - const options = { - versions: req.query.versions || 'latest' +exports.retrieveById = async function (req, res) { + const options = { + versions: req.query.versions || 'latest', + }; + + try { + const identities = await identitiesService.retrieveById(req.params.stixId, options); + if (identities.length === 0) { + return res.status(404).send('Identity not found.'); + } else { + logger.debug( + `Success: Retrieved ${identities.length} identities with id ${req.params.stixId}`, + ); + return res.status(200).send(identities); } - - try { - const identities = await identitiesService.retrieveById(req.params.stixId, options); - if (identities.length === 0) { - return res.status(404).send('Identity not found.'); - } - else { - logger.debug(`Success: Retrieved ${ identities.length } identities with id ${ req.params.stixId }`); - return res.status(200).send(identities); - } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - logger.warn('Badly formatted stix id: ' + req.params.stixId); - return res.status(400).send('Stix id is badly formatted.'); - } - else if (err instanceof InvalidQueryStringParameterError) { - logger.warn('Invalid query string: versions=' + req.query.versions); - return res.status(400).send('Query string parameter versions is invalid.'); - } - else { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get identities. Server error.'); - } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + logger.warn('Badly formatted stix id: ' + req.params.stixId); + return res.status(400).send('Stix id is badly formatted.'); + } else if (err instanceof InvalidQueryStringParameterError) { + logger.warn('Invalid query string: versions=' + req.query.versions); + return res.status(400).send('Query string parameter versions is invalid.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get identities. Server error.'); } - + } }; -exports.retrieveVersionById = async function(req, res) { - - try { - const identity = await identitiesService.retrieveVersionById(req.params.stixId, req.params.modified); - if (!identity) { - return res.status(404).send('Identity not found.'); - } - else { - logger.debug(`Success: Retrieved identity with id ${identity.id}`); - return res.status(200).send(identity); - } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - 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 identity. Server error.'); - } - } - -}; - -exports.create = async function(req, res) { - // Get the data from the request - const identityData = req.body; - - // Create the identity - try { - const options = { - import: false, - userAccountId: req.user?.userAccountId - }; - const identity = await identitiesService.create(identityData, options); - logger.debug("Success: Created identity with id " + identity.stix.id); - return res.status(201).send(identity); +exports.retrieveVersionById = async function (req, res) { + try { + const identity = await identitiesService.retrieveVersionById( + req.params.stixId, + req.params.modified, + ); + if (!identity) { + return res.status(404).send('Identity not found.'); + } else { + logger.debug(`Success: Retrieved identity with id ${identity.id}`); + return res.status(200).send(identity); } - catch(err) { - if (err instanceof DuplicateIdError) { - logger.warn("Duplicate stix.id and stix.modified"); - return res.status(409).send('Unable to create identity. Duplicate stix.id and stix.modified properties.'); - } - else { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to create identity. Server error."); - } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + 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 identity. Server error.'); } + } }; -exports.updateFull = async function(req, res) { - // Get the data from the request - const identityData = req.body; +exports.create = async function (req, res) { + // Get the data from the request + const identityData = req.body; - // Create the identity - try { - const identity = await identitiesService.updateFull(req.params.stixId, req.params.modified, identityData); - if (!identity) { - return res.status(404).send('Identity not found.'); - } else { - logger.debug("Success: Updated identity with id " + identity.stix.id); - return res.status(200).send(identity); - } - } catch (err) { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to update identity. Server error."); + // Create the identity + try { + const options = { + import: false, + userAccountId: req.user?.userAccountId, + }; + const identity = await identitiesService.create(identityData, options); + logger.debug('Success: Created identity with id ' + identity.stix.id); + return res.status(201).send(identity); + } catch (err) { + if (err instanceof DuplicateIdError) { + logger.warn('Duplicate stix.id and stix.modified'); + return res + .status(409) + .send('Unable to create identity. Duplicate stix.id and stix.modified properties.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to create identity. Server error.'); } + } }; -exports.deleteVersionById = async function(req, res) { - - try { - const identity = await identitiesService.deleteVersionById(req.params.stixId, req.params.modified); - if (!identity) { - return res.status(404).send('Identity not found.'); - } else { - logger.debug("Success: Deleted identity with id " + identity.stix.id); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete identity failed. ' + err); - return res.status(500).send('Unable to delete identity. Server error.'); +exports.updateFull = async function (req, res) { + // Get the data from the request + const identityData = req.body; + + // Create the identity + try { + const identity = await identitiesService.updateFull( + req.params.stixId, + req.params.modified, + identityData, + ); + if (!identity) { + return res.status(404).send('Identity not found.'); + } else { + logger.debug('Success: Updated identity with id ' + identity.stix.id); + return res.status(200).send(identity); } + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to update identity. Server error.'); + } }; -exports.deleteById = async function(req, res) { +exports.deleteVersionById = async function (req, res) { + try { + const identity = await identitiesService.deleteVersionById( + req.params.stixId, + req.params.modified, + ); + if (!identity) { + return res.status(404).send('Identity not found.'); + } else { + logger.debug('Success: Deleted identity with id ' + identity.stix.id); + return res.status(204).end(); + } + } catch (err) { + logger.error('Delete identity failed. ' + err); + return res.status(500).send('Unable to delete identity. Server error.'); + } +}; - try { - const identities = await identitiesService.deleteById(req.params.stixId); - if (identities.deletedCount === 0) { - return res.status(404).send('Identity not found.'); - } - else { - logger.debug(`Success: Deleted identity with id ${ req.params.stixId }`); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete identity failed. ' + err); - return res.status(500).send('Unable to identity identity. Server error.'); +exports.deleteById = async function (req, res) { + try { + const identities = await identitiesService.deleteById(req.params.stixId); + if (identities.deletedCount === 0) { + return res.status(404).send('Identity not found.'); + } else { + logger.debug(`Success: Deleted identity with id ${req.params.stixId}`); + return res.status(204).end(); } + } catch (err) { + logger.error('Delete identity failed. ' + err); + return res.status(500).send('Unable to identity identity. Server error.'); + } }; diff --git a/app/controllers/marking-definitions-controller.js b/app/controllers/marking-definitions-controller.js index fda60204..d45c4124 100644 --- a/app/controllers/marking-definitions-controller.js +++ b/app/controllers/marking-definitions-controller.js @@ -2,122 +2,132 @@ const markingDefinitionsService = require('../services/marking-definitions-service'); const logger = require('../lib/logger'); -const { BadlyFormattedParameterError, CannotUpdateStaticObjectError, InvalidQueryStringParameterError } = require('../exceptions'); +const { + BadlyFormattedParameterError, + CannotUpdateStaticObjectError, + InvalidQueryStringParameterError, +} = require('../exceptions'); // NOTE: A marking definition does not support the modified or revoked properties!! -exports.retrieveAll = async function(req, res) { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0, - state: req.query.state, - includeDeprecated: req.query.includeDeprecated, - includePagination: req.query.includePagination - } - try { - const markingDefinitions = await markingDefinitionsService.retrieveAll(options); - if (options.includePagination) { - logger.debug(`Success: Retrieved ${ markingDefinitions.data.length } of ${ markingDefinitions.pagination.total } total marking definition(s)`); - } - else { - logger.debug(`Success: Retrieved ${ markingDefinitions.length } marking definition(s)`); - } - return res.status(200).send(markingDefinitions); - } catch (err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get marking definitions. Server error.'); +exports.retrieveAll = async function (req, res) { + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + state: req.query.state, + includeDeprecated: req.query.includeDeprecated, + includePagination: req.query.includePagination, + }; + try { + const markingDefinitions = await markingDefinitionsService.retrieveAll(options); + if (options.includePagination) { + logger.debug( + `Success: Retrieved ${markingDefinitions.data.length} of ${markingDefinitions.pagination.total} total marking definition(s)`, + ); + } else { + logger.debug(`Success: Retrieved ${markingDefinitions.length} marking definition(s)`); } + return res.status(200).send(markingDefinitions); + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get marking definitions. Server error.'); + } }; -exports.retrieveById = async function(req, res) { - const options = { }; - try { - const markingDefinitions = await markingDefinitionsService.retrieveById(req.params.stixId, options); - if (markingDefinitions.length === 0) { - return res.status(404).send('Marking definitions not found.'); - } - else { - logger.debug(`Success: Retrieved ${ markingDefinitions.length } marking definition with id ${ req.params.stixId }`); - return res.status(200).send(markingDefinitions); - } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - logger.warn('Badly formatted stix id: ' + req.params.stixId); - return res.status(400).send('Stix id is badly formatted.'); - } - else if (err.message === InvalidQueryStringParameterError) { - logger.warn('Invalid query string: versions=' + req.query.versions); - return res.status(400).send('Query string parameter versions is invalid.'); - } - else { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get marking definitions. Server error.'); - } - } +exports.retrieveById = async function (req, res) { + const options = {}; + try { + const markingDefinitions = await markingDefinitionsService.retrieveById( + req.params.stixId, + options, + ); + if (markingDefinitions.length === 0) { + return res.status(404).send('Marking definitions not found.'); + } else { + logger.debug( + `Success: Retrieved ${markingDefinitions.length} marking definition with id ${req.params.stixId}`, + ); + return res.status(200).send(markingDefinitions); + } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + logger.warn('Badly formatted stix id: ' + req.params.stixId); + return res.status(400).send('Stix id is badly formatted.'); + } else if (err.message === InvalidQueryStringParameterError) { + logger.warn('Invalid query string: versions=' + req.query.versions); + return res.status(400).send('Query string parameter versions is invalid.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get marking definitions. Server error.'); + } + } }; -exports.create = async function(req, res) { - // Get the data from the request - const markingDefinitionData = req.body; +exports.create = async function (req, res) { + // Get the data from the request + const markingDefinitionData = req.body; - // Create the marking definition - try { - const options = { - import: false, - userAccountId: req.user?.userAccountId - }; - const markingDefinition = await markingDefinitionsService.create(markingDefinitionData, options); - logger.debug("Success: Created marking definition with id " + markingDefinition.stix.id); - return res.status(201).send(markingDefinition); - } - catch(err) { - if (err instanceof BadlyFormattedParameterError) { - logger.warn('Unable to create marking definition: Stix id not allowed'); - return res.status(400).send('Stix id not allowed.'); - } - else { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to create marking definition. Server error."); - } + // Create the marking definition + try { + const options = { + import: false, + userAccountId: req.user?.userAccountId, + }; + const markingDefinition = await markingDefinitionsService.create( + markingDefinitionData, + options, + ); + logger.debug('Success: Created marking definition with id ' + markingDefinition.stix.id); + return res.status(201).send(markingDefinition); + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + logger.warn('Unable to create marking definition: Stix id not allowed'); + return res.status(400).send('Stix id not allowed.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to create marking definition. Server error.'); } + } }; -exports.updateFull = async function(req, res) { - // Get the data from the request - const markingDefinitionData = req.body; +exports.updateFull = async function (req, res) { + // Get the data from the request + const markingDefinitionData = req.body; - // Create the marking definition - try { - const markingDefinition = await markingDefinitionsService.updateFull(req.params.stixId, markingDefinitionData); - if (!markingDefinition) { - return res.status(404).send('Marking definition not found.'); - } else { - logger.debug("Success: Updated marking definition with id " + markingDefinition.stix.id); - return res.status(200).send(markingDefinition); - } - } catch (err) { - if (err instanceof CannotUpdateStaticObjectError) { - logger.warn('Unable to update marking definition, cannot update static object'); - return res.status(400).send('Cannot update static object'); - } - else { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to update marking definition. Server error."); - } + // Create the marking definition + try { + const markingDefinition = await markingDefinitionsService.updateFull( + req.params.stixId, + markingDefinitionData, + ); + if (!markingDefinition) { + return res.status(404).send('Marking definition not found.'); + } else { + logger.debug('Success: Updated marking definition with id ' + markingDefinition.stix.id); + return res.status(200).send(markingDefinition); + } + } catch (err) { + if (err instanceof CannotUpdateStaticObjectError) { + logger.warn('Unable to update marking definition, cannot update static object'); + return res.status(400).send('Cannot update static object'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to update marking definition. Server error.'); } + } }; -exports.delete = async function(req, res) { - try { - const markingDefinition = await markingDefinitionsService.delete(req.params.stixId); - if (!markingDefinition) { - return res.status(404).send('Marking definition not found.'); - } else { - logger.debug("Success: Deleted marking definition with id " + markingDefinition.stix.id); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete marking definition failed. ' + err); - return res.status(500).send('Unable to delete marking definition. Server error.'); +exports.delete = async function (req, res) { + try { + const markingDefinition = await markingDefinitionsService.delete(req.params.stixId); + if (!markingDefinition) { + return res.status(404).send('Marking definition not found.'); + } else { + logger.debug('Success: Deleted marking definition with id ' + markingDefinition.stix.id); + return res.status(204).end(); } + } catch (err) { + logger.error('Delete marking definition failed. ' + err); + return res.status(500).send('Unable to delete marking definition. Server error.'); + } }; diff --git a/app/controllers/matrices-controller.js b/app/controllers/matrices-controller.js index 95a4a86b..1f50155c 100644 --- a/app/controllers/matrices-controller.js +++ b/app/controllers/matrices-controller.js @@ -2,175 +2,189 @@ const matricesService = require('../services/matrices-service'); const logger = require('../lib/logger'); -const { DuplicateIdError, BadlyFormattedParameterError, InvalidQueryStringParameterError } = require('../exceptions'); +const { + DuplicateIdError, + BadlyFormattedParameterError, + InvalidQueryStringParameterError, +} = require('../exceptions'); exports.retrieveAll = async function (req, res) { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0, - state: req.query.state, - includeRevoked: req.query.includeRevoked, - includeDeprecated: req.query.includeDeprecated, - search: req.query.search, - lastUpdatedBy: req.query.lastUpdatedBy, - includePagination: req.query.includePagination - } - try { - const results = await matricesService.retrieveAll(options); - - if (options.includePagination) { - logger.debug(`Success: Retrieved ${results.data.length} of ${results.pagination.total} total matrices`); - } - else { - logger.debug(`Success: Retrieved ${results.length} matrices`); - } - return res.status(200).send(results); - - } catch (err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get matrices. Server error.'); + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + state: req.query.state, + includeRevoked: req.query.includeRevoked, + includeDeprecated: req.query.includeDeprecated, + search: req.query.search, + lastUpdatedBy: req.query.lastUpdatedBy, + includePagination: req.query.includePagination, + }; + try { + const results = await matricesService.retrieveAll(options); + + if (options.includePagination) { + logger.debug( + `Success: Retrieved ${results.data.length} of ${results.pagination.total} total matrices`, + ); + } else { + logger.debug(`Success: Retrieved ${results.length} matrices`); } + return res.status(200).send(results); + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get matrices. Server error.'); + } }; exports.retrieveById = async function (req, res) { - const options = { - versions: req.query.versions || 'latest' - }; - - try { - const matrices = await matricesService.retrieveById(req.params.stixId, options); - - if (matrices.length === 0) { - return res.status(404).send('Matrix not found.'); - } else { - logger.debug(`Success: Retrieved ${matrices.length} matrices with id ${req.params.stixId}`); - return res.status(200).send(matrices); - } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - logger.warn('Badly formatted stix id: ' + req.params.stixId); - return res.status(400).send('Stix id is badly formatted.'); - } else if (err instanceof InvalidQueryStringParameterError) { - logger.warn('Invalid query string: versions=' + req.query.versions); - return res.status(400).send('Query string parameter versions is invalid.'); - } else { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get matrices. Server error.'); - } + const options = { + versions: req.query.versions || 'latest', + }; + + try { + const matrices = await matricesService.retrieveById(req.params.stixId, options); + + if (matrices.length === 0) { + return res.status(404).send('Matrix not found.'); + } else { + logger.debug(`Success: Retrieved ${matrices.length} matrices with id ${req.params.stixId}`); + return res.status(200).send(matrices); } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + logger.warn('Badly formatted stix id: ' + req.params.stixId); + return res.status(400).send('Stix id is badly formatted.'); + } else if (err instanceof InvalidQueryStringParameterError) { + logger.warn('Invalid query string: versions=' + req.query.versions); + return res.status(400).send('Query string parameter versions is invalid.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get matrices. Server error.'); + } + } }; exports.retrieveVersionById = async function (req, res) { - try { - const matrix = await matricesService.retrieveVersionById(req.params.stixId, req.params.modified); - - if (!matrix) { - return res.status(404).send('Matrix not found.'); - } else { - logger.debug(`Success: Retrieved matrix with id ${matrix.id}`); - return res.status(200).send(matrix); - } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - logger.warn('Badly formatted stix id: ' + req.params.stixId); - return res.status(400).send('Stix id is badly formatted.'); - } - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get matrix. Server error.'); + try { + const matrix = await matricesService.retrieveVersionById( + req.params.stixId, + req.params.modified, + ); + + if (!matrix) { + return res.status(404).send('Matrix not found.'); + } else { + logger.debug(`Success: Retrieved matrix with id ${matrix.id}`); + return res.status(200).send(matrix); + } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + logger.warn('Badly formatted stix id: ' + req.params.stixId); + return res.status(400).send('Stix id is badly formatted.'); } + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get matrix. Server error.'); + } }; -exports.create = async function(req, res) { - // Get the data from the request - const matrixData = req.body; +exports.create = async function (req, res) { + // Get the data from the request + const matrixData = req.body; - // Create the matrix - try { - const options = { - import: false, - userAccountId: req.user?.userAccountId - }; - const matrix = await matricesService.create(matrixData, options); - logger.debug("Success: Created matrix with id " + matrix.stix.id); - return res.status(201).send(matrix); - } - catch (err) { - if (err instanceof DuplicateIdError) { - logger.warn("Duplicate stix.id and stix.modified"); - return res.status(409).send('Unable to create matrix. Duplicate stix.id and stix.modified properties.'); - } - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to create matrix. Server error."); + // Create the matrix + try { + const options = { + import: false, + userAccountId: req.user?.userAccountId, + }; + const matrix = await matricesService.create(matrixData, options); + logger.debug('Success: Created matrix with id ' + matrix.stix.id); + return res.status(201).send(matrix); + } catch (err) { + if (err instanceof DuplicateIdError) { + logger.warn('Duplicate stix.id and stix.modified'); + return res + .status(409) + .send('Unable to create matrix. Duplicate stix.id and stix.modified properties.'); } + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to create matrix. Server error.'); + } }; exports.updateFull = async function (req, res) { - try { - // Get the data from the request - const matrixData = req.body; - - // Update the matrix - const matrix = await matricesService.updateFull(req.params.stixId, req.params.modified, matrixData); - - if (!matrix) { - return res.status(404).send('Matrix not found.'); - } - - logger.debug("Success: Updated matrix with id " + matrix.stix.id); - return res.status(200).send(matrix); - } catch (err) { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to update matrix. Server error."); + try { + // Get the data from the request + const matrixData = req.body; + + // Update the matrix + const matrix = await matricesService.updateFull( + req.params.stixId, + req.params.modified, + matrixData, + ); + + if (!matrix) { + return res.status(404).send('Matrix not found.'); } + + logger.debug('Success: Updated matrix with id ' + matrix.stix.id); + return res.status(200).send(matrix); + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to update matrix. Server error.'); + } }; exports.deleteVersionById = async function (req, res) { - try { - const matrix = await matricesService.deleteVersionById(req.params.stixId, req.params.modified); - if (!matrix) { - return res.status(404).send('Matrix not found.'); - } else { - logger.debug("Success: Deleted matrix with id " + matrix.stix.id); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete matrix failed. ' + err); - return res.status(500).send('Unable to delete matrix. Server error.'); + try { + const matrix = await matricesService.deleteVersionById(req.params.stixId, req.params.modified); + if (!matrix) { + return res.status(404).send('Matrix not found.'); + } else { + logger.debug('Success: Deleted matrix with id ' + matrix.stix.id); + return res.status(204).end(); } + } catch (err) { + logger.error('Delete matrix failed. ' + err); + return res.status(500).send('Unable to delete matrix. Server error.'); + } }; exports.deleteById = async function (req, res) { - try { - const matrices = await matricesService.deleteById(req.params.stixId); - - if (matrices.deletedCount === 0) { - return res.status(404).send('Matrix not found.'); - } - else { - logger.debug(`Success: Deleted matrix with id ${req.params.stixId}`); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete matrix failed. ' + err); - return res.status(500).send('Unable to delete matrix. Server error.'); + try { + const matrices = await matricesService.deleteById(req.params.stixId); + + if (matrices.deletedCount === 0) { + return res.status(404).send('Matrix not found.'); + } else { + logger.debug(`Success: Deleted matrix with id ${req.params.stixId}`); + return res.status(204).end(); } + } catch (err) { + logger.error('Delete matrix failed. ' + err); + return res.status(500).send('Unable to delete matrix. Server error.'); + } }; exports.retrieveTechniquesForMatrix = async function (req, res) { - try { - const techniquesByTactic = await matricesService.retrieveTechniquesForMatrix(req.params.stixId, req.params.modified); - 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); - } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - logger.warn('Badly formatted stix id: ' + req.params.stixId); - return res.status(400).send('Stix id is badly formatted.'); - } - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get techniques for matrix. Server error.'); + try { + const techniquesByTactic = await matricesService.retrieveTechniquesForMatrix( + req.params.stixId, + req.params.modified, + ); + 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); + } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + logger.warn('Badly formatted stix id: ' + req.params.stixId); + return res.status(400).send('Stix id is badly formatted.'); } + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get techniques for matrix. Server error.'); + } }; diff --git a/app/controllers/mitigations-controller.js b/app/controllers/mitigations-controller.js index 59b0d527..9c0855e7 100644 --- a/app/controllers/mitigations-controller.js +++ b/app/controllers/mitigations-controller.js @@ -2,166 +2,172 @@ const mitigationsService = require('../services/mitigations-service'); const logger = require('../lib/logger'); -const { DuplicateIdError, BadlyFormattedParameterError, InvalidQueryStringParameterError } = require('../exceptions'); - -exports.retrieveAll = async function(req, res) { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0, - domain: req.query.domain, - state: req.query.state, - includeRevoked: req.query.includeRevoked, - includeDeprecated: req.query.includeDeprecated, - search: req.query.search, - lastUpdatedBy: req.query.lastUpdatedBy, - includePagination: req.query.includePagination - } - - try { - const results = await mitigationsService.retrieveAll(options); - if (options.includePagination) { - logger.debug(`Success: Retrieved ${ results.data.length } of ${ results.pagination.total } total mitigation(s)`); - } - else { - logger.debug(`Success: Retrieved ${ results.length } mitigation(s)`); - } - return res.status(200).send(results); - } catch (err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get mitigations. Server error.'); +const { + DuplicateIdError, + BadlyFormattedParameterError, + InvalidQueryStringParameterError, +} = require('../exceptions'); + +exports.retrieveAll = async function (req, res) { + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + domain: req.query.domain, + state: req.query.state, + includeRevoked: req.query.includeRevoked, + includeDeprecated: req.query.includeDeprecated, + search: req.query.search, + lastUpdatedBy: req.query.lastUpdatedBy, + includePagination: req.query.includePagination, + }; + + try { + const results = await mitigationsService.retrieveAll(options); + if (options.includePagination) { + logger.debug( + `Success: Retrieved ${results.data.length} of ${results.pagination.total} total mitigation(s)`, + ); + } else { + logger.debug(`Success: Retrieved ${results.length} mitigation(s)`); } + return res.status(200).send(results); + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get mitigations. Server error.'); + } }; -exports.retrieveById = async function(req, res) { - const options = { - versions: req.query.versions || 'latest' +exports.retrieveById = async function (req, res) { + const options = { + versions: req.query.versions || 'latest', + }; + + try { + const mitigations = await mitigationsService.retrieveById(req.params.stixId, options); + if (mitigations.length === 0) { + return res.status(404).send('Mitigation not found.'); + } else { + logger.debug( + `Success: Retrieved ${mitigations.length} mitigation(s) with id ${req.params.stixId}`, + ); + return res.status(200).send(mitigations); } - - try { - const mitigations = await mitigationsService.retrieveById(req.params.stixId, options); - if (mitigations.length === 0) { - return res.status(404).send('Mitigation not found.'); - } - else { - logger.debug(`Success: Retrieved ${ mitigations.length } mitigation(s) with id ${ req.params.stixId }`); - return res.status(200).send(mitigations); - } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - logger.warn('Badly formatted stix id: ' + req.params.stixId); - return res.status(400).send('Stix id is badly formatted.'); - } - else if (err instanceof InvalidQueryStringParameterError) { - logger.warn('Invalid query string: versions=' + req.query.versions); - return res.status(400).send('Query string parameter versions is invalid.'); - } - else { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get mitigations. Server error.'); - } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + logger.warn('Badly formatted stix id: ' + req.params.stixId); + return res.status(400).send('Stix id is badly formatted.'); + } else if (err instanceof InvalidQueryStringParameterError) { + logger.warn('Invalid query string: versions=' + req.query.versions); + return res.status(400).send('Query string parameter versions is invalid.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get mitigations. Server error.'); } - -}; - -exports.retrieveVersionById = async function(req, res) { - try { - const mitigation = await mitigationsService.retrieveVersionById(req.params.stixId, req.params.modified); - if (!mitigation) { - return res.status(404).send('Mitigation not found.'); - } - else { - logger.debug(`Success: Retrieved mitigation with id ${mitigation.id}`); - return res.status(200).send(mitigation); - } - } catch (err) { - - if (err instanceof BadlyFormattedParameterError) { - 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 mitigation. Server error.'); - } - } - + } }; -exports.create = async function(req, res) { - // Get the data from the request - const mitigationData = req.body; - const options = { - import: false, - userAccountId: req.user?.userAccountId - }; - - // Create the mitigation - try { - const mitigation = await mitigationsService.create(mitigationData, options); - logger.debug("Success: Created mitigation with id " + mitigation.stix.id); - return res.status(201).send(mitigation); +exports.retrieveVersionById = async function (req, res) { + try { + const mitigation = await mitigationsService.retrieveVersionById( + req.params.stixId, + req.params.modified, + ); + if (!mitigation) { + return res.status(404).send('Mitigation not found.'); + } else { + logger.debug(`Success: Retrieved mitigation with id ${mitigation.id}`); + return res.status(200).send(mitigation); } - catch(err) { - if (err instanceof DuplicateIdError) { - logger.warn("Duplicate stix.id and stix.modified"); - return res.status(409).send('Unable to create mitigation. Duplicate stix.id and stix.modified properties.'); - } - else { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to create mitigation. Server error."); - } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + 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 mitigation. Server error.'); } + } }; -exports.updateFull = async function(req, res) { - // Get the data from the request - const mitigationData = req.body; - - // Create the mitigation - - try { - const mitigation = await mitigationsService.updateFull(req.params.stixId, req.params.modified, mitigationData); - if (!mitigation) { - return res.status(404).send('Mitigation not found.'); - } else { - logger.debug("Success: Updated mitigation with id " + mitigation.stix.id); - return res.status(200).send(mitigation); - } - } catch (err) { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to update mitigation. Server error."); +exports.create = async function (req, res) { + // Get the data from the request + const mitigationData = req.body; + const options = { + import: false, + userAccountId: req.user?.userAccountId, + }; + + // Create the mitigation + try { + const mitigation = await mitigationsService.create(mitigationData, options); + logger.debug('Success: Created mitigation with id ' + mitigation.stix.id); + return res.status(201).send(mitigation); + } catch (err) { + if (err instanceof DuplicateIdError) { + logger.warn('Duplicate stix.id and stix.modified'); + return res + .status(409) + .send('Unable to create mitigation. Duplicate stix.id and stix.modified properties.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to create mitigation. Server error.'); } - + } }; -exports.deleteVersionById = async function(req, res) { - try { - const mitigation = await mitigationsService.deleteVersionById(req.params.stixId, req.params.modified); - if (!mitigation) { - return res.status(404).send('Mitigation not found.'); - } else { - logger.debug("Success: Deleted mitigation with id " + mitigation.stix.id); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete mitigation failed. ' + err); - return res.status(500).send('Unable to delete mitigation. Server error.'); +exports.updateFull = async function (req, res) { + // Get the data from the request + const mitigationData = req.body; + + // Create the mitigation + + try { + const mitigation = await mitigationsService.updateFull( + req.params.stixId, + req.params.modified, + mitigationData, + ); + if (!mitigation) { + return res.status(404).send('Mitigation not found.'); + } else { + logger.debug('Success: Updated mitigation with id ' + mitigation.stix.id); + return res.status(200).send(mitigation); } + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to update mitigation. Server error.'); + } +}; +exports.deleteVersionById = async function (req, res) { + try { + const mitigation = await mitigationsService.deleteVersionById( + req.params.stixId, + req.params.modified, + ); + if (!mitigation) { + return res.status(404).send('Mitigation not found.'); + } else { + logger.debug('Success: Deleted mitigation with id ' + mitigation.stix.id); + return res.status(204).end(); + } + } catch (err) { + logger.error('Delete mitigation failed. ' + err); + return res.status(500).send('Unable to delete mitigation. Server error.'); + } }; -exports.deleteById = async function(req, res) { - try { - const mitigations = await mitigationsService.deleteById(req.params.stixId); - if (mitigations.deletedCount === 0) { - return res.status(404).send('Mitigation not found.'); - } - else { - logger.debug(`Success: Deleted mitigation with id ${ req.params.stixId }`); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete mitigation failed. ' + err); - return res.status(500).send('Unable to delete mitigation. Server error.'); +exports.deleteById = async function (req, res) { + try { + const mitigations = await mitigationsService.deleteById(req.params.stixId); + if (mitigations.deletedCount === 0) { + return res.status(404).send('Mitigation not found.'); + } else { + logger.debug(`Success: Deleted mitigation with id ${req.params.stixId}`); + return res.status(204).end(); } + } catch (err) { + logger.error('Delete mitigation failed. ' + err); + return res.status(500).send('Unable to delete mitigation. Server error.'); + } }; diff --git a/app/controllers/notes-controller.js b/app/controllers/notes-controller.js index a9f63380..4c5b3cee 100644 --- a/app/controllers/notes-controller.js +++ b/app/controllers/notes-controller.js @@ -2,161 +2,160 @@ const notesService = require('../services/notes-service'); const logger = require('../lib/logger'); -const { DuplicateIdError, InvalidQueryStringParameterError, BadlyFormattedParameterError } = require('../exceptions'); +const { + DuplicateIdError, + InvalidQueryStringParameterError, + BadlyFormattedParameterError, +} = require('../exceptions'); -exports.retrieveAll = async function(req, res) { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0, - state: req.query.state, - includeRevoked: req.query.includeRevoked, - includeDeprecated: req.query.includeDeprecated, - search: req.query.search, - includePagination: req.query.includePagination, - lastUpdatedBy: req.query.lastUpdatedBy, - } +exports.retrieveAll = async function (req, res) { + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + state: req.query.state, + includeRevoked: req.query.includeRevoked, + includeDeprecated: req.query.includeDeprecated, + search: req.query.search, + includePagination: req.query.includePagination, + lastUpdatedBy: req.query.lastUpdatedBy, + }; - try { - const results = await notesService.retrieveAll(options); - if (options.includePagination) { - logger.debug(`Success: Retrieved ${ results.data.length } of ${ results.pagination.total } total note(s)`); - } - else { - logger.debug(`Success: Retrieved ${ results.length } note(s)`); - } - return res.status(200).send(results); - } catch (err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get notes. Server error.'); + try { + const results = await notesService.retrieveAll(options); + if (options.includePagination) { + logger.debug( + `Success: Retrieved ${results.data.length} of ${results.pagination.total} total note(s)`, + ); + } else { + logger.debug(`Success: Retrieved ${results.length} note(s)`); } + return res.status(200).send(results); + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get notes. Server error.'); + } }; -exports.retrieveById = async function(req, res) { - const options = { - versions: req.query.versions || 'latest' - } +exports.retrieveById = async function (req, res) { + const options = { + versions: req.query.versions || 'latest', + }; - try { - const notes = await notesService.retrieveById(req.params.stixId, options); - if (notes.length === 0) { - return res.status(404).send('Note not found.'); - } - else { - logger.debug(`Success: Retrieved ${ notes.length } note(s) with id ${ req.params.stixId }`); - return res.status(200).send(notes); - } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - logger.warn('Badly formatted stix id: ' + req.params.stixId); - return res.status(400).send('Stix id is badly formatted.'); - } - else if (err instanceof InvalidQueryStringParameterError) { - logger.warn('Invalid query string: versions=' + req.query.versions); - return res.status(400).send('Query string parameter versions is invalid.'); - } - else { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get notes. Server error.'); - } + try { + const notes = await notesService.retrieveById(req.params.stixId, options); + if (notes.length === 0) { + return res.status(404).send('Note not found.'); + } else { + logger.debug(`Success: Retrieved ${notes.length} note(s) with id ${req.params.stixId}`); + return res.status(200).send(notes); + } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + logger.warn('Badly formatted stix id: ' + req.params.stixId); + return res.status(400).send('Stix id is badly formatted.'); + } else if (err instanceof InvalidQueryStringParameterError) { + logger.warn('Invalid query string: versions=' + req.query.versions); + return res.status(400).send('Query string parameter versions is invalid.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get notes. Server error.'); } + } }; -exports.retrieveVersionById = async function(req, res) { - try { - const note = await notesService.retrieveVersionById(req.params.stixId, req.params.modified); - if (!note) { - return res.status(404).send('Note not found.'); - } - else { - logger.debug(`Success: Retrieved note with id ${ note.id }`); - return res.status(200).send(note); - } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - 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 note. Server error.'); - } +exports.retrieveVersionById = async function (req, res) { + try { + const note = await notesService.retrieveVersionById(req.params.stixId, req.params.modified); + if (!note) { + return res.status(404).send('Note not found.'); + } else { + logger.debug(`Success: Retrieved note with id ${note.id}`); + return res.status(200).send(note); } - + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + 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 note. Server error.'); + } + } }; -exports.create = async function(req, res) { - // Get the data from the request - const noteData = req.body; - const options = { - import: false, - userAccountId: req.user?.userAccountId - }; +exports.create = async function (req, res) { + // Get the data from the request + const noteData = req.body; + const options = { + import: false, + userAccountId: req.user?.userAccountId, + }; - // Create the note - try { - const note = await notesService.create(noteData, options); - logger.debug("Success: Created note with id " + note.stix.id); - return res.status(201).send(note); - } - catch(err) { - if (err instanceof DuplicateIdError) { - logger.warn("Duplicate stix.id and stix.modified"); - return res.status(409).send('Unable to create note. Duplicate stix.id and stix.modified properties.'); - } - else { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to create note. Server error."); - } + // Create the note + try { + const note = await notesService.create(noteData, options); + logger.debug('Success: Created note with id ' + note.stix.id); + return res.status(201).send(note); + } catch (err) { + if (err instanceof DuplicateIdError) { + logger.warn('Duplicate stix.id and stix.modified'); + return res + .status(409) + .send('Unable to create note. Duplicate stix.id and stix.modified properties.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to create note. Server error.'); } + } }; -exports.updateVersion = async function(req, res) { - // Get the data from the request - const noteData = req.body; +exports.updateVersion = async function (req, res) { + // Get the data from the request + const noteData = req.body; - // Create the note - try { - const note = await notesService.updateVersion(req.params.stixId, req.params.modified, noteData); - if (!note) { - return res.status(404).send('Note not found.'); - } - else { - logger.debug("Success: Updated note with id " + note.stix.id); - return res.status(200).send(note); - } - } catch (err) { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to update note. Server error."); + // Create the note + try { + const note = await notesService.updateVersion(req.params.stixId, req.params.modified, noteData); + if (!note) { + return res.status(404).send('Note not found.'); + } else { + logger.debug('Success: Updated note with id ' + note.stix.id); + return res.status(200).send(note); } + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to update note. Server error.'); + } }; -exports.deleteById = async function(req, res) { - try { - const results = await notesService.deleteById(req.params.stixId); - if (results.deletedCount === 0) { - return res.status(404).send('Note not found.'); - } - else { - logger.debug(`Success: Deleted note with id ${ req.params.stixId }`); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete note failed. ' + err); - return res.status(500).send('Unable to delete note. Server error.'); +exports.deleteById = async function (req, res) { + try { + const results = await notesService.deleteById(req.params.stixId); + if (results.deletedCount === 0) { + return res.status(404).send('Note not found.'); + } else { + logger.debug(`Success: Deleted note with id ${req.params.stixId}`); + return res.status(204).end(); } + } catch (err) { + logger.error('Delete note failed. ' + err); + return res.status(500).send('Unable to delete note. Server error.'); + } }; -exports.deleteVersionById = async function(req, res) { - try { - const note = await notesService.deleteVersionById(req.params.stixId, req.params.modified); - if (!note) { - return res.status(404).send('Note not found.'); - } else { - logger.debug(`Success: Deleted note with id ${ note.stix.id} and modified ${ note.stix.modified }`); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete note version failed. ' + err); - return res.status(500).send('Unable to delete note. Server error.'); +exports.deleteVersionById = async function (req, res) { + try { + const note = await notesService.deleteVersionById(req.params.stixId, req.params.modified); + if (!note) { + return res.status(404).send('Note not found.'); + } else { + logger.debug( + `Success: Deleted note with id ${note.stix.id} and modified ${note.stix.modified}`, + ); + return res.status(204).end(); } + } catch (err) { + logger.error('Delete note version failed. ' + err); + return res.status(500).send('Unable to delete note. Server error.'); + } }; diff --git a/app/controllers/recent-activity-controller.js b/app/controllers/recent-activity-controller.js index b0c5f26f..6d57e233 100644 --- a/app/controllers/recent-activity-controller.js +++ b/app/controllers/recent-activity-controller.js @@ -3,30 +3,30 @@ const recentActivityService = require('../services/recent-activity-service'); const logger = require('../lib/logger'); -exports.retrieveAll = async function(req, res) { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0, - includeRevoked: req.query.includeRevoked, - includeDeprecated: req.query.includeDeprecated, - lastUpdatedBy: req.query.lastUpdatedBy, - includePagination: req.query.includePagination, - } - - try { - const results = await recentActivityService.retrieveAll(options); +exports.retrieveAll = async function (req, res) { + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + includeRevoked: req.query.includeRevoked, + includeDeprecated: req.query.includeDeprecated, + lastUpdatedBy: req.query.lastUpdatedBy, + includePagination: req.query.includePagination, + }; - if (options.includePagination) { - logger.debug(`Success: Retrieved ${results.data.length} of ${results.pagination.total} total ATT&CK object(s)`); - } - else { - logger.debug(`Success: Retrieved ${results.length} ATT&CK object(s)`); - } + try { + const results = await recentActivityService.retrieveAll(options); - return res.status(200).send(results); - } - catch (err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get ATT&CK objects. Server error.'); + if (options.includePagination) { + logger.debug( + `Success: Retrieved ${results.data.length} of ${results.pagination.total} total ATT&CK object(s)`, + ); + } else { + logger.debug(`Success: Retrieved ${results.length} ATT&CK object(s)`); } + + return res.status(200).send(results); + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get ATT&CK objects. Server error.'); + } }; diff --git a/app/controllers/references-controller.js b/app/controllers/references-controller.js index 7ba2e259..97fe574d 100644 --- a/app/controllers/references-controller.js +++ b/app/controllers/references-controller.js @@ -4,92 +4,86 @@ const referencesService = require('../services/references-service'); const logger = require('../lib/logger'); const { DuplicateIdError } = require('../exceptions'); -exports.retrieveAll = async function(req, res) { - const options = { - sourceName: req.query.sourceName, - offset: req.query.offset || 0, - limit: req.query.limit || 0, - search: req.query.search, - includePagination: req.query.includePagination - } +exports.retrieveAll = async function (req, res) { + const options = { + sourceName: req.query.sourceName, + offset: req.query.offset || 0, + limit: req.query.limit || 0, + search: req.query.search, + includePagination: req.query.includePagination, + }; - try { - const results = await referencesService.retrieveAll(options); - if (options.includePagination) { - logger.debug(`Success: Retrieved ${ results.data.length } of ${ results.pagination.total } total reference(s)`); - } - else { - logger.debug(`Success: Retrieved ${ results.length } reference(s)`); - } - return res.status(200).send(results); - } - catch(err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get references. Server error.'); + try { + const results = await referencesService.retrieveAll(options); + if (options.includePagination) { + logger.debug( + `Success: Retrieved ${results.data.length} of ${results.pagination.total} total reference(s)`, + ); + } else { + logger.debug(`Success: Retrieved ${results.length} reference(s)`); } + return res.status(200).send(results); + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get references. Server error.'); + } }; -exports.create = async function(req, res) { - // Get the data from the request - const referenceData = req.body; +exports.create = async function (req, res) { + // Get the data from the request + const referenceData = req.body; - // The open api validator should catch missing properties, but it isn't working for this one endpoint - if (!referenceData.source_name || !referenceData.description) { - return res.status(400).send('Unable to create reference. Missing required properties.'); - } + // The open api validator should catch missing properties, but it isn't working for this one endpoint + if (!referenceData.source_name || !referenceData.description) { + return res.status(400).send('Unable to create reference. Missing required properties.'); + } - // Create the reference - try { - const reference = await referencesService.create(referenceData); - logger.debug("Success: Created reference with source_name " + reference.source_name); - return res.status(201).send(reference); - } - catch(err) { - if (err instanceof DuplicateIdError) { - logger.warn("Duplicate source_name"); - return res.status(409).send('Unable to create reference. Duplicate source_name.'); - } - else { - logger.error("***** Failed with error: " + err); - return res.status(500).send("Unable to create reference. Server error."); - } + // Create the reference + try { + const reference = await referencesService.create(referenceData); + logger.debug('Success: Created reference with source_name ' + reference.source_name); + return res.status(201).send(reference); + } catch (err) { + if (err instanceof DuplicateIdError) { + logger.warn('Duplicate source_name'); + return res.status(409).send('Unable to create reference. Duplicate source_name.'); + } else { + logger.error('***** Failed with error: ' + err); + return res.status(500).send('Unable to create reference. Server error.'); } + } }; -exports.update = async function(req, res) { - // Get the data from the request - const referenceData = req.body; +exports.update = async function (req, res) { + // Get the data from the request + const referenceData = req.body; - // Create the reference - try { - const reference = await referencesService.update(referenceData); - if (!reference) { - return res.status(404).send('Reference not found.'); - } - else { - logger.debug('Success: Updated reference with source_name ' + reference.source_name); - return res.status(200).send(reference); - } - } - catch(err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to update reference. Server error.'); + // Create the reference + try { + const reference = await referencesService.update(referenceData); + if (!reference) { + return res.status(404).send('Reference not found.'); + } else { + logger.debug('Success: Updated reference with source_name ' + reference.source_name); + return res.status(200).send(reference); } + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to update reference. Server error.'); + } }; -exports.deleteBySourceName = async function(req, res) { - try { - const reference = await referencesService.deleteBySourceName(req.query.sourceName); - if (!reference) { - return res.status(404).send('Reference not found.'); - } - else { - logger.debug(`Success: Deleted reference with source_name ${ reference.source_name}`); - return res.status(204).end(); - } - } - catch(err) { - logger.error('Delete reference failed. ' + err); - return res.status(500).send('Unable to delete reference. Server error.'); +exports.deleteBySourceName = async function (req, res) { + try { + const reference = await referencesService.deleteBySourceName(req.query.sourceName); + if (!reference) { + return res.status(404).send('Reference not found.'); + } else { + logger.debug(`Success: Deleted reference with source_name ${reference.source_name}`); + return res.status(204).end(); } + } catch (err) { + logger.error('Delete reference failed. ' + err); + return res.status(500).send('Unable to delete reference. Server error.'); + } }; diff --git a/app/controllers/relationships-controller.js b/app/controllers/relationships-controller.js index 37f8b4b9..ac4fb53d 100644 --- a/app/controllers/relationships-controller.js +++ b/app/controllers/relationships-controller.js @@ -2,171 +2,178 @@ const relationshipsService = require('../services/relationships-service'); const logger = require('../lib/logger'); -const { BadlyFormattedParameterError, InvalidQueryStringParameterError, DuplicateIdError } = require('../exceptions'); +const { + BadlyFormattedParameterError, + InvalidQueryStringParameterError, + DuplicateIdError, +} = require('../exceptions'); -exports.retrieveAll = async function(req, res) { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0, - state: req.query.state, - includeRevoked: req.query.includeRevoked, - includeDeprecated: req.query.includeDeprecated, - sourceRef: req.query.sourceRef, - targetRef: req.query.targetRef, - sourceOrTargetRef: req.query.sourceOrTargetRef, - relationshipType: req.query.relationshipType, - sourceType: req.query.sourceType, - targetType: req.query.targetType, - versions: req.query.versions || 'latest', - includePagination: req.query.includePagination, - lookupRefs: req.query.lookupRefs, - includeIdentities: req.query.includeIdentities, - lastUpdatedBy: req.query.lastUpdatedBy - } +exports.retrieveAll = async function (req, res) { + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + state: req.query.state, + includeRevoked: req.query.includeRevoked, + includeDeprecated: req.query.includeDeprecated, + sourceRef: req.query.sourceRef, + targetRef: req.query.targetRef, + sourceOrTargetRef: req.query.sourceOrTargetRef, + relationshipType: req.query.relationshipType, + sourceType: req.query.sourceType, + targetType: req.query.targetType, + versions: req.query.versions || 'latest', + includePagination: req.query.includePagination, + lookupRefs: req.query.lookupRefs, + includeIdentities: req.query.includeIdentities, + lastUpdatedBy: req.query.lastUpdatedBy, + }; - try { - const results = await relationshipsService.retrieveAll(options); - if (options.includePagination) { - logger.debug(`Success: Retrieved ${results.data.length} of ${results.pagination.total} total relationship(s)`); - } - else { - logger.debug(`Success: Retrieved ${results.length} relationship(s)`); - } - return res.status(200).send(results); - } - catch(err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get relationships. Server error.'); + try { + const results = await relationshipsService.retrieveAll(options); + if (options.includePagination) { + logger.debug( + `Success: Retrieved ${results.data.length} of ${results.pagination.total} total relationship(s)`, + ); + } else { + logger.debug(`Success: Retrieved ${results.length} relationship(s)`); } + return res.status(200).send(results); + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get relationships. Server error.'); + } }; -exports.retrieveById = async function(req, res) { - const options = { - versions: req.query.versions || 'latest' - } - try { - const relationships = await relationshipsService.retrieveById(req.params.stixId, options); +exports.retrieveById = async function (req, res) { + const options = { + versions: req.query.versions || 'latest', + }; + try { + const relationships = await relationshipsService.retrieveById(req.params.stixId, options); - if (relationships.length === 0) { - return res.status(404).send('Relationship not found.'); - } - else { - logger.debug(`Success: Retrieved ${ relationships.length } relationship(s) with id ${ req.params.stixId }`); - return res.status(200).send(relationships); - } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - logger.warn('Badly formatted stix id: ' + req.params.stixId); - return res.status(400).send('Stix id is badly formatted.'); - } - else if (err instanceof InvalidQueryStringParameterError) { - logger.warn('Invalid query string: versions=' + req.query.versions); - return res.status(400).send('Query string parameter versions is invalid.'); - } - else { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get relationships. Server error.'); - } - } + if (relationships.length === 0) { + return res.status(404).send('Relationship not found.'); + } else { + logger.debug( + `Success: Retrieved ${relationships.length} relationship(s) with id ${req.params.stixId}`, + ); + return res.status(200).send(relationships); + } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + logger.warn('Badly formatted stix id: ' + req.params.stixId); + return res.status(400).send('Stix id is badly formatted.'); + } else if (err instanceof InvalidQueryStringParameterError) { + logger.warn('Invalid query string: versions=' + req.query.versions); + return res.status(400).send('Query string parameter versions is invalid.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get relationships. Server error.'); + } + } }; -exports.retrieveVersionById = async function(req, res) { - - try { - const relationship = await relationshipsService.retrieveVersionById(req.params.stixId, req.params.modified); - if (!relationship) { - return res.status(404).send('Relationship not found.'); - } - else { - logger.debug(`Success: Retrieved relationship with id ${relationship.id}`); - return res.status(200).send(relationship); - } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - 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 relationship. Server error.'); - } - } +exports.retrieveVersionById = async function (req, res) { + try { + const relationship = await relationshipsService.retrieveVersionById( + req.params.stixId, + req.params.modified, + ); + if (!relationship) { + return res.status(404).send('Relationship not found.'); + } else { + logger.debug(`Success: Retrieved relationship with id ${relationship.id}`); + return res.status(200).send(relationship); + } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + 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 relationship. Server error.'); + } + } }; -exports.create = async function(req, res) { - // Get the data from the request - const relationshipData = req.body; +exports.create = async function (req, res) { + // Get the data from the request + const relationshipData = req.body; - // Create the relationship - try { - const options = { - import: false, - userAccountId: req.user?.userAccountId - }; - const relationship = await relationshipsService.create(relationshipData, options); - logger.debug("Success: Created relationship with id " + relationship.stix.id); - return res.status(201).send(relationship); - } - catch(err) { - if (err instanceof DuplicateIdError) { - logger.warn("Duplicate stix.id and stix.modified"); - return res.status(409).send('Unable to create relationship. Duplicate stix.id and stix.modified properties.'); - } - else { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to create relationship. Server error."); - } + // Create the relationship + try { + const options = { + import: false, + userAccountId: req.user?.userAccountId, + }; + const relationship = await relationshipsService.create(relationshipData, options); + logger.debug('Success: Created relationship with id ' + relationship.stix.id); + return res.status(201).send(relationship); + } catch (err) { + if (err instanceof DuplicateIdError) { + logger.warn('Duplicate stix.id and stix.modified'); + return res + .status(409) + .send('Unable to create relationship. Duplicate stix.id and stix.modified properties.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to create relationship. Server error.'); } + } }; -exports.updateFull = async function(req, res) { - // Get the data from the request - const relationshipData = req.body; - - // Create the relationship - try { - const relationship = await relationshipsService.updateFull(req.params.stixId, req.params.modified, relationshipData); - if (!relationship) { - return res.status(404).send('Relationship not found.'); - } else { - logger.debug("Success: Updated relationship with id " + relationship.stix.id); - return res.status(200).send(relationship); - } - } catch (err) { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to update relationship. Server error."); - } +exports.updateFull = async function (req, res) { + // Get the data from the request + const relationshipData = req.body; + // Create the relationship + try { + const relationship = await relationshipsService.updateFull( + req.params.stixId, + req.params.modified, + relationshipData, + ); + if (!relationship) { + return res.status(404).send('Relationship not found.'); + } else { + logger.debug('Success: Updated relationship with id ' + relationship.stix.id); + return res.status(200).send(relationship); + } + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to update relationship. Server error.'); + } }; -exports.deleteVersionById = async function(req, res) { - try { - const relationship = await relationshipsService.deleteVersionById(req.params.stixId, req.params.modified); - if (!relationship) { - return res.status(404).send('Relationship not found.'); - } else { - logger.debug("Success: Deleted relationship with id " + relationship.stix.id); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete relationship failed. ' + err); - return res.status(500).send('Unable to delete relationship. Server error.'); - } - +exports.deleteVersionById = async function (req, res) { + try { + const relationship = await relationshipsService.deleteVersionById( + req.params.stixId, + req.params.modified, + ); + if (!relationship) { + return res.status(404).send('Relationship not found.'); + } else { + logger.debug('Success: Deleted relationship with id ' + relationship.stix.id); + return res.status(204).end(); + } + } catch (err) { + logger.error('Delete relationship failed. ' + err); + return res.status(500).send('Unable to delete relationship. Server error.'); + } }; -exports.deleteById = async function(req, res) { - try { - const relationships = await relationshipsService.deleteById(req.params.stixId); - if (relationships.deletedCount === 0) { - return res.status(404).send('Relationship not found.'); - } - else { - logger.debug(`Success: Deleted relationship with id ${ req.params.stixId }`); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete relationship failed. ' + err); - return res.status(500).send('Unable to delete relationship. Server error.'); - } +exports.deleteById = async function (req, res) { + try { + const relationships = await relationshipsService.deleteById(req.params.stixId); + if (relationships.deletedCount === 0) { + return res.status(404).send('Relationship not found.'); + } else { + logger.debug(`Success: Deleted relationship with id ${req.params.stixId}`); + return res.status(204).end(); + } + } catch (err) { + logger.error('Delete relationship failed. ' + err); + return res.status(500).send('Unable to delete relationship. Server error.'); + } }; diff --git a/app/controllers/session-controller.js b/app/controllers/session-controller.js index d5397d01..92421c35 100644 --- a/app/controllers/session-controller.js +++ b/app/controllers/session-controller.js @@ -3,13 +3,12 @@ //const sessionService = require('../services/session-service'); const logger = require('../lib/logger'); -exports.retrieveCurrentSession = function(req, res) { - if (req.user) { - logger.debug('Success: Retrieved current user session.'); - return res.status(200).send(req.user); - } - else { - logger.warn('Unable to retrieve current user session, failed with error: req.user not found'); - return res.status(401).send('Not authorized'); - } +exports.retrieveCurrentSession = function (req, res) { + if (req.user) { + logger.debug('Success: Retrieved current user session.'); + return res.status(200).send(req.user); + } else { + logger.warn('Unable to retrieve current user session, failed with error: req.user not found'); + return res.status(401).send('Not authorized'); + } }; diff --git a/app/controllers/software-controller.js b/app/controllers/software-controller.js index da22f886..72a6d027 100644 --- a/app/controllers/software-controller.js +++ b/app/controllers/software-controller.js @@ -2,187 +2,193 @@ const softwareService = require('../services/software-service'); const logger = require('../lib/logger'); -const { DuplicateIdError, BadlyFormattedParameterError, InvalidQueryStringParameterError, PropertyNotAllowedError } = require('../exceptions'); - -exports.retrieveAll = async function(req, res) { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0, - domain: req.query.domain, - platform: req.query.platform, - state: req.query.state, - includeRevoked: req.query.includeRevoked, - includeDeprecated: req.query.includeDeprecated, - search: req.query.search, - lastUpdatedBy: req.query.lastUpdatedBy, - includePagination: req.query.includePagination - } - try { - const results = await softwareService.retrieveAll(options); - if (options.includePagination) { - logger.debug(`Success: Retrieved ${ results.data.length } of ${ results.pagination.total } total software`); - } - else { - logger.debug(`Success: Retrieved ${ results.length } software`); - } - return res.status(200).send(results); - } catch (err) { - console.log("retrieve all error"); - console.log(err); - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get software. Server error.'); +const { + DuplicateIdError, + BadlyFormattedParameterError, + InvalidQueryStringParameterError, + PropertyNotAllowedError, +} = require('../exceptions'); + +exports.retrieveAll = async function (req, res) { + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + domain: req.query.domain, + platform: req.query.platform, + state: req.query.state, + includeRevoked: req.query.includeRevoked, + includeDeprecated: req.query.includeDeprecated, + search: req.query.search, + lastUpdatedBy: req.query.lastUpdatedBy, + includePagination: req.query.includePagination, + }; + try { + const results = await softwareService.retrieveAll(options); + if (options.includePagination) { + logger.debug( + `Success: Retrieved ${results.data.length} of ${results.pagination.total} total software`, + ); + } else { + logger.debug(`Success: Retrieved ${results.length} software`); } + return res.status(200).send(results); + } catch (err) { + console.log('retrieve all error'); + console.log(err); + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get software. Server error.'); + } }; -exports.retrieveById = async function(req, res) { - const options = { - versions: req.query.versions || 'latest' +exports.retrieveById = async function (req, res) { + const options = { + versions: req.query.versions || 'latest', + }; + try { + const software = await softwareService.retrieveById(req.params.stixId, options); + if (software.length === 0) { + return res.status(404).send('Software not found.'); + } else { + logger.debug(`Success: Retrieved ${software.length} software with id ${req.params.stixId}`); + return res.status(200).send(software); } - try { - const software = await softwareService.retrieveById(req.params.stixId, options); - if (software.length === 0) { - return res.status(404).send('Software not found.'); - } - else { - logger.debug(`Success: Retrieved ${ software.length } software with id ${ req.params.stixId }`); - return res.status(200).send(software); - } - - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - logger.warn('Badly formatted stix id: ' + req.params.stixId); - return res.status(400).send('Stix id is badly formatted.'); - } - else if (err instanceof InvalidQueryStringParameterError) { - logger.warn('Invalid query string: versions=' + req.query.versions); - return res.status(400).send('Query string parameter versions is invalid.'); - } - else { - console.log("retrieve by id error"); - console.log(err); - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get software. Server error.'); - } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + logger.warn('Badly formatted stix id: ' + req.params.stixId); + return res.status(400).send('Stix id is badly formatted.'); + } else if (err instanceof InvalidQueryStringParameterError) { + logger.warn('Invalid query string: versions=' + req.query.versions); + return res.status(400).send('Query string parameter versions is invalid.'); + } else { + console.log('retrieve by id error'); + console.log(err); + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get software. Server error.'); } - + } }; -exports.retrieveVersionById = async function(req, res) { - try { - const software = await softwareService.retrieveVersionById(req.params.stixId, req.params.modified); - - if (!software) { - return res.status(404).send('Software not found.'); - } - else { - logger.debug(`Success: Retrieved software with id ${software.id}`); - return res.status(200).send(software); - } - } catch (err) { - - if (err instanceof BadlyFormattedParameterError) { - logger.warn('Badly formatted stix id: ' + req.params.stixId); - return res.status(400).send('Stix id is badly formatted.'); - } - else { - console.log("retrieve version by id error"); - console.log(err); - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get software. Server error.'); - } +exports.retrieveVersionById = async function (req, res) { + try { + const software = await softwareService.retrieveVersionById( + req.params.stixId, + req.params.modified, + ); + + if (!software) { + return res.status(404).send('Software not found.'); + } else { + logger.debug(`Success: Retrieved software with id ${software.id}`); + return res.status(200).send(software); } - + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + logger.warn('Badly formatted stix id: ' + req.params.stixId); + return res.status(400).send('Stix id is badly formatted.'); + } else { + console.log('retrieve version by id error'); + console.log(err); + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get software. Server error.'); + } + } }; -exports.create = async function(req, res) { - // Get the data from the request - const softwareData = req.body; - - const options = { - import: false, - userAccountId: req.user?.userAccountId - }; - - // Create the software - try { - const software = await await softwareService.create(softwareData, options); - logger.debug("Success: Created software with id " + software.stix.id); - return res.status(201).send(software); - } catch(err) { - if (err instanceof DuplicateIdError) { - logger.warn("Duplicate stix.id and stix.modified"); - return res.status(409).send('Unable to create software. Duplicate stix.id and stix.modified properties.'); - } - else if (err instanceof PropertyNotAllowedError) { - logger.warn(`Unable to create software, property ${ err.propertyName } is not allowed`); - return res.status(400).send(`Unable to create software, property ${ err.propertyName } is not allowed`); - } - else { - console.log("create error"); - console.log(err); - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to create software. Server error."); - } +exports.create = async function (req, res) { + // Get the data from the request + const softwareData = req.body; + + const options = { + import: false, + userAccountId: req.user?.userAccountId, + }; + + // Create the software + try { + const software = await await softwareService.create(softwareData, options); + logger.debug('Success: Created software with id ' + software.stix.id); + return res.status(201).send(software); + } catch (err) { + if (err instanceof DuplicateIdError) { + logger.warn('Duplicate stix.id and stix.modified'); + return res + .status(409) + .send('Unable to create software. Duplicate stix.id and stix.modified properties.'); + } else if (err instanceof PropertyNotAllowedError) { + logger.warn(`Unable to create software, property ${err.propertyName} is not allowed`); + return res + .status(400) + .send(`Unable to create software, property ${err.propertyName} is not allowed`); + } else { + console.log('create error'); + console.log(err); + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to create software. Server error.'); } + } }; -exports.updateFull = async function(req, res) { - // Get the data from the request - const softwareData = req.body; - - try { - // Create the software - const software = await softwareService.updateFull(req.params.stixId, req.params.modified, softwareData); +exports.updateFull = async function (req, res) { + // Get the data from the request + const softwareData = req.body; - if (!software) { - return res.status(404).send('Software not found.'); - } else { - logger.debug("Success: Updated software with id " + software.stix.id); - return res.status(200).send(software); - } - } catch (err) { - console.log("update full error"); - console.log(err); - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to update software. Server error."); + try { + // Create the software + const software = await softwareService.updateFull( + req.params.stixId, + req.params.modified, + softwareData, + ); + + if (!software) { + return res.status(404).send('Software not found.'); + } else { + logger.debug('Success: Updated software with id ' + software.stix.id); + return res.status(200).send(software); } + } catch (err) { + console.log('update full error'); + console.log(err); + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to update software. Server error.'); + } }; -exports.deleteVersionById = async function(req, res) { - try { - const software = await softwareService.deleteVersionById(req.params.stixId, req.params.modified); - - if (!software) { - return res.status(404).send('Software not found.'); - } else { - logger.debug("Success: Deleted software with id " + software.stix.id); - return res.status(204).end(); - } - - } catch (err) { - console.log("delete version by id error"); - console.log(err); - logger.error('Delete software failed. ' + err); - return res.status(500).send('Unable to delete software. Server error.'); +exports.deleteVersionById = async function (req, res) { + try { + const software = await softwareService.deleteVersionById( + req.params.stixId, + req.params.modified, + ); + + if (!software) { + return res.status(404).send('Software not found.'); + } else { + logger.debug('Success: Deleted software with id ' + software.stix.id); + return res.status(204).end(); } + } catch (err) { + console.log('delete version by id error'); + console.log(err); + logger.error('Delete software failed. ' + err); + return res.status(500).send('Unable to delete software. Server error.'); + } }; -exports.deleteById = async function(req, res) { - try { - const softwares = await softwareService.deleteById(req.params.stixId); - - if (softwares.deletedCount === 0) { - return res.status(404).send('Software not found.'); - } - else { - logger.debug(`Success: Deleted software with id ${ req.params.stixId }`); - return res.status(204).end(); - } +exports.deleteById = async function (req, res) { + try { + const softwares = await softwareService.deleteById(req.params.stixId); - } catch (err) { - console.log("delete by id error"); - console.log(err); - logger.error('Delete software failed. ' + err); - return res.status(500).send('Unable to delete software. Server error.'); + if (softwares.deletedCount === 0) { + return res.status(404).send('Software not found.'); + } else { + logger.debug(`Success: Deleted software with id ${req.params.stixId}`); + return res.status(204).end(); } + } catch (err) { + console.log('delete by id error'); + console.log(err); + logger.error('Delete software failed. ' + err); + return res.status(500).send('Unable to delete software. Server error.'); + } }; diff --git a/app/controllers/stix-bundles-controller.js b/app/controllers/stix-bundles-controller.js index 79faa90a..223461fc 100644 --- a/app/controllers/stix-bundles-controller.js +++ b/app/controllers/stix-bundles-controller.js @@ -3,35 +3,33 @@ const stixBundlesService = require('../services/stix-bundles-service'); const logger = require('../lib/logger'); -const validStixVersions = [ '2.0', '2.1' ]; - -exports.exportBundle = async function(req, res) { - if (!req.query.domain) { - return res.status(400).send('domain is required'); - } - - if (!validStixVersions.includes(req.query.stixVersion)) { - return res.status(400).send('invalid STIX version'); - } - - const options = { - domain: req.query.domain, - state: req.query.state, - includeRevoked: req.query.includeRevoked, - includeDeprecated: req.query.includeDeprecated, - stixVersion: req.query.stixVersion, - includeMissingAttackId: req.query.includeMissingAttackId, - includeNotes: req.query.includeNotes - }; - - try { - const stixBundle = await stixBundlesService.exportBundle(options); - - return res.status(200).send(stixBundle); - } - catch(err) { - logger.error('Unable to export STIX bundle: ' + err); - return res.status(500).send('Unable to export STIX bundle.'); - } -} - +const validStixVersions = ['2.0', '2.1']; + +exports.exportBundle = async function (req, res) { + if (!req.query.domain) { + return res.status(400).send('domain is required'); + } + + if (!validStixVersions.includes(req.query.stixVersion)) { + return res.status(400).send('invalid STIX version'); + } + + const options = { + domain: req.query.domain, + state: req.query.state, + includeRevoked: req.query.includeRevoked, + includeDeprecated: req.query.includeDeprecated, + stixVersion: req.query.stixVersion, + includeMissingAttackId: req.query.includeMissingAttackId, + includeNotes: req.query.includeNotes, + }; + + try { + const stixBundle = await stixBundlesService.exportBundle(options); + + return res.status(200).send(stixBundle); + } catch (err) { + logger.error('Unable to export STIX bundle: ' + err); + return res.status(500).send('Unable to export STIX bundle.'); + } +}; diff --git a/app/controllers/system-configuration-controller.js b/app/controllers/system-configuration-controller.js index 750bca9e..a2ec5c83 100644 --- a/app/controllers/system-configuration-controller.js +++ b/app/controllers/system-configuration-controller.js @@ -3,129 +3,124 @@ const systemConfigurationService = require('../services/system-configuration-service'); const logger = require('../lib/logger'); -exports.retrieveSystemVersion = function(req, res) { - try { - const systemVersionInfo = systemConfigurationService.retrieveSystemVersion(); - logger.debug(`Success: Retrieved system version, version: ${ systemVersionInfo.version }, attackSpecVersion: ${ systemVersionInfo.attackSpecVersion }`); - return res.status(200).send(systemVersionInfo); - } - catch(err) { - logger.error("Unable to retrieve system version, failed with error: " + err); - return res.status(500).send("Unable to retrieve system version. Server error."); - } +exports.retrieveSystemVersion = function (req, res) { + try { + const systemVersionInfo = systemConfigurationService.retrieveSystemVersion(); + logger.debug( + `Success: Retrieved system version, version: ${systemVersionInfo.version}, attackSpecVersion: ${systemVersionInfo.attackSpecVersion}`, + ); + return res.status(200).send(systemVersionInfo); + } catch (err) { + logger.error('Unable to retrieve system version, failed with error: ' + err); + return res.status(500).send('Unable to retrieve system version. Server error.'); + } }; -exports.retrieveAllowedValues = async function(req, res) { - try { - const allowedValues = await systemConfigurationService.retrieveAllowedValues(); - logger.debug("Success: Retrieved allowed values."); - return res.status(200).send(allowedValues); - } - catch(err) { - logger.error("Unable to retrieve allowed values, failed with error: " + err); - return res.status(500).send("Unable to retrieve allowed values. Server error."); - } +exports.retrieveAllowedValues = async function (req, res) { + try { + const allowedValues = await systemConfigurationService.retrieveAllowedValues(); + logger.debug('Success: Retrieved allowed values.'); + return res.status(200).send(allowedValues); + } catch (err) { + logger.error('Unable to retrieve allowed values, failed with error: ' + err); + return res.status(500).send('Unable to retrieve allowed values. Server error.'); + } }; -exports.retrieveOrganizationIdentity = async function(req, res) { - try { - const identity = await systemConfigurationService.retrieveOrganizationIdentity(); - logger.debug('Success: Retrieved organization identity.'); - return res.status(200).send(identity); - } - catch(err) { - logger.error('Unable to retrieve organization identity, failed with error: ' + err); - return res.status(500).send("Unable to retrieve organization identity. Server error."); - } +exports.retrieveOrganizationIdentity = async function (req, res) { + try { + const identity = await systemConfigurationService.retrieveOrganizationIdentity(); + logger.debug('Success: Retrieved organization identity.'); + return res.status(200).send(identity); + } catch (err) { + logger.error('Unable to retrieve organization identity, failed with error: ' + err); + return res.status(500).send('Unable to retrieve organization identity. Server error.'); + } }; -exports.setOrganizationIdentity = async function(req, res) { - const organizationIdentity = req.body; - if (!organizationIdentity.id) { - logger.warn('Missing organization identity id'); - return res.status(400).send('Organization identity id is required'); - } +exports.setOrganizationIdentity = async function (req, res) { + const organizationIdentity = req.body; + if (!organizationIdentity.id) { + logger.warn('Missing organization identity id'); + return res.status(400).send('Organization identity id is required'); + } - try { - await systemConfigurationService.setOrganizationIdentity(organizationIdentity.id); - logger.debug(`Success: Set organization identity to: ${ organizationIdentity.id }`); - return res.status(204).send(); - } - catch(err) { - logger.error('Unable to set organization identity, failed with error: ' + err); - return res.status(500).send('Unable to set organization identity. Server error.'); - } + try { + await systemConfigurationService.setOrganizationIdentity(organizationIdentity.id); + logger.debug(`Success: Set organization identity to: ${organizationIdentity.id}`); + return res.status(204).send(); + } catch (err) { + logger.error('Unable to set organization identity, failed with error: ' + err); + return res.status(500).send('Unable to set organization identity. Server error.'); + } }; -exports.retrieveAuthenticationConfig = function(req, res) { - try { - const authenticationConfig = systemConfigurationService.retrieveAuthenticationConfig(); - logger.debug('Success: Retrieved authentication configuration.'); - return res.status(200).send(authenticationConfig); - } - catch(err) { - logger.error('Unable to retrieve authentication configuration, failed with error: ' + err); - return res.status(500).send('Unable to retrieve authentication configuration. Server error.'); - } +exports.retrieveAuthenticationConfig = function (req, res) { + try { + const authenticationConfig = systemConfigurationService.retrieveAuthenticationConfig(); + logger.debug('Success: Retrieved authentication configuration.'); + return res.status(200).send(authenticationConfig); + } catch (err) { + logger.error('Unable to retrieve authentication configuration, failed with error: ' + err); + return res.status(500).send('Unable to retrieve authentication configuration. Server error.'); + } }; -exports.retrieveDefaultMarkingDefinitions = async function(req, res) { - try { - const options = { refOnly: req.query.refOnly }; - const defaultMarkingDefinitions = await systemConfigurationService.retrieveDefaultMarkingDefinitions(options); - logger.debug('Success: Retrieved default marking definitions.'); - return res.status(200).send(defaultMarkingDefinitions); - } - catch(err) { - logger.error(`Unable to retrieve default marking definitions, failed with error: ${ err } (${ err.markingDefinitionRef })`); - return res.status(500).send("Unable to retrieve default marking definitions. Server error."); - } +exports.retrieveDefaultMarkingDefinitions = async function (req, res) { + try { + const options = { refOnly: req.query.refOnly }; + const defaultMarkingDefinitions = + await systemConfigurationService.retrieveDefaultMarkingDefinitions(options); + logger.debug('Success: Retrieved default marking definitions.'); + return res.status(200).send(defaultMarkingDefinitions); + } catch (err) { + logger.error( + `Unable to retrieve default marking definitions, failed with error: ${err} (${err.markingDefinitionRef})`, + ); + return res.status(500).send('Unable to retrieve default marking definitions. Server error.'); + } }; -exports.setDefaultMarkingDefinitions = async function(req, res) { - const defaultMarkingDefinitionIds = req.body; - if (!defaultMarkingDefinitionIds) { - logger.warn('Missing default marking definition ids'); - return res.status(400).send('Missing default marking definition ids'); - } - else if (!Array.isArray(defaultMarkingDefinitionIds)) { - logger.warn('Default marking definition ids not an array'); - return res.status(400).send('Request must contain an array of marking definition ids'); - } +exports.setDefaultMarkingDefinitions = async function (req, res) { + const defaultMarkingDefinitionIds = req.body; + if (!defaultMarkingDefinitionIds) { + logger.warn('Missing default marking definition ids'); + return res.status(400).send('Missing default marking definition ids'); + } else if (!Array.isArray(defaultMarkingDefinitionIds)) { + logger.warn('Default marking definition ids not an array'); + return res.status(400).send('Request must contain an array of marking definition ids'); + } - try { - await systemConfigurationService.setDefaultMarkingDefinitions(defaultMarkingDefinitionIds); - logger.debug(`Success: Set default marking definitions`); - return res.status(204).send(); - } - catch(err) { - logger.error('Unable to set default marking definitions, failed with error: ' + err); - return res.status(500).send('Unable to default marking definitions. Server error.'); - } + try { + await systemConfigurationService.setDefaultMarkingDefinitions(defaultMarkingDefinitionIds); + logger.debug(`Success: Set default marking definitions`); + return res.status(204).send(); + } catch (err) { + logger.error('Unable to set default marking definitions, failed with error: ' + err); + return res.status(500).send('Unable to default marking definitions. Server error.'); + } }; -exports.retrieveOrganizationNamespace = async function(req, res) { - try { - const namespace = await systemConfigurationService.retrieveOrganizationNamespace(); - logger.debug('Success: Retrieved organization namespace.'); - return res.status(200).send(namespace); - } - catch(err) { - logger.error('Unable to retrieve organization namespace, failed with error: ' + err); - return res.status(500).send("Unable to retrieve organization namespace. Server error."); - } +exports.retrieveOrganizationNamespace = async function (req, res) { + try { + const namespace = await systemConfigurationService.retrieveOrganizationNamespace(); + logger.debug('Success: Retrieved organization namespace.'); + return res.status(200).send(namespace); + } catch (err) { + logger.error('Unable to retrieve organization namespace, failed with error: ' + err); + return res.status(500).send('Unable to retrieve organization namespace. Server error.'); + } }; -exports.setOrganizationNamespace = async function(req, res) { - const organizationNamespace = req.body; +exports.setOrganizationNamespace = async function (req, res) { + const organizationNamespace = req.body; - try { - await systemConfigurationService.setOrganizationNamespace(organizationNamespace); - logger.debug(`Success: Set organization namespace to: ${ organizationNamespace.prefix }`); - return res.status(204).send(); - } - catch(err) { - logger.error('Unable to set organization namespace, failed with error: ' + err); - return res.status(500).send('Unable to set organization namespace. Server error.'); - } + try { + await systemConfigurationService.setOrganizationNamespace(organizationNamespace); + logger.debug(`Success: Set organization namespace to: ${organizationNamespace.prefix}`); + return res.status(204).send(); + } catch (err) { + logger.error('Unable to set organization namespace, failed with error: ' + err); + return res.status(500).send('Unable to set organization namespace. Server error.'); + } }; diff --git a/app/controllers/tactics-controller.js b/app/controllers/tactics-controller.js index fbf694f1..64c476a4 100644 --- a/app/controllers/tactics-controller.js +++ b/app/controllers/tactics-controller.js @@ -2,205 +2,199 @@ const tacticsService = require('../services/tactics-service'); const logger = require('../lib/logger'); -const { DuplicateIdError, BadlyFormattedParameterError, InvalidQueryStringParameterError } = require('../exceptions'); - -exports.retrieveAll = async function(req, res) { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0, - domain: req.query.domain, - state: req.query.state, - includeRevoked: req.query.includeRevoked, - includeDeprecated: req.query.includeDeprecated, - search: req.query.search, - lastUpdatedBy: req.query.lastUpdatedBy, - includePagination: req.query.includePagination +const { + DuplicateIdError, + BadlyFormattedParameterError, + InvalidQueryStringParameterError, +} = require('../exceptions'); + +exports.retrieveAll = async function (req, res) { + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + domain: req.query.domain, + state: req.query.state, + includeRevoked: req.query.includeRevoked, + includeDeprecated: req.query.includeDeprecated, + search: req.query.search, + lastUpdatedBy: req.query.lastUpdatedBy, + includePagination: req.query.includePagination, + }; + try { + const results = await tacticsService.retrieveAll(options); + + if (options.includePagination) { + logger.debug( + `Success: Retrieved ${results.data.length} of ${results.pagination.total} total tactic(s)`, + ); + } else { + logger.debug(`Success: Retrieved ${results.length} tactic(s)`); } - try { - const results = await tacticsService.retrieveAll(options) - - if (options.includePagination) { - logger.debug(`Success: Retrieved ${ results.data.length } of ${ results.pagination.total } total tactic(s)`); - } - else { - logger.debug(`Success: Retrieved ${ results.length } tactic(s)`); - } - return res.status(200).send(results); - - } catch (err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get tactics. Server error.'); - } + return res.status(200).send(results); + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get tactics. Server error.'); + } }; -exports.retrieveById = async function(req, res) { - const options = { - versions: req.query.versions || 'latest' - } +exports.retrieveById = async function (req, res) { + const options = { + versions: req.query.versions || 'latest', + }; - try { - const tactics = await tacticsService.retrieveById(req.params.stixId, options); - - if (tactics.length === 0) { - return res.status(404).send('Tactic not found.'); - } - else { - logger.debug(`Success: Retrieved ${ tactics.length } tactic(s) with id ${ req.params.stixId }`); - return res.status(200).send(tactics); - } - - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - logger.warn('Badly formatted stix id: ' + req.params.stixId); - return res.status(400).send('Stix id is badly formatted.'); - } - else if (err instanceof InvalidQueryStringParameterError) { - logger.warn('Invalid query string: versions=' + req.query.versions); - return res.status(400).send('Query string parameter versions is invalid.'); - } - else { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get tactics. Server error.'); - } - } -}; + try { + const tactics = await tacticsService.retrieveById(req.params.stixId, options); -exports.retrieveVersionById = async function(req, res) { - - try { - - const tactic = await tacticsService.retrieveVersionById(req.params.stixId, req.params.modified); - - if (!tactic) { - return res.status(404).send('tactic not found.'); - } - else { - logger.debug(`Success: Retrieved tactic with id ${tactic.id}`); - return res.status(200).send(tactic); - } - - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - 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 tactic. Server error.'); - } + if (tactics.length === 0) { + return res.status(404).send('Tactic not found.'); + } else { + logger.debug(`Success: Retrieved ${tactics.length} tactic(s) with id ${req.params.stixId}`); + return res.status(200).send(tactics); + } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + logger.warn('Badly formatted stix id: ' + req.params.stixId); + return res.status(400).send('Stix id is badly formatted.'); + } else if (err instanceof InvalidQueryStringParameterError) { + logger.warn('Invalid query string: versions=' + req.query.versions); + return res.status(400).send('Query string parameter versions is invalid.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get tactics. Server error.'); } + } }; -exports.create = async function(req, res) { - // Get the data from the request - const tacticData = req.body; - - const options = { - import: false, - userAccountId: req.user?.userAccountId - }; +exports.retrieveVersionById = async function (req, res) { + try { + const tactic = await tacticsService.retrieveVersionById(req.params.stixId, req.params.modified); - // Create the tactic - try { - const tactic = await tacticsService.create(tacticData, options); - - logger.debug("Success: Created tactic with id " + tactic.stix.id); - return res.status(201).send(tactic); + if (!tactic) { + return res.status(404).send('tactic not found.'); + } else { + logger.debug(`Success: Retrieved tactic with id ${tactic.id}`); + return res.status(200).send(tactic); } - catch(err) { - if (err instanceof DuplicateIdError) { - logger.warn("Duplicate stix.id and stix.modified"); - return res.status(409).send('Unable to create tactic. Duplicate stix.id and stix.modified properties.'); - } - else { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to create tactic. Server error."); - } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + 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 tactic. Server error.'); } + } }; -exports.updateFull = async function(req, res) { - // Get the data from the request - const tacticData = req.body; - - try { - const tactic = await tacticsService.updateFull(req.params.stixId, req.params.modified, tacticData); - - if (!tactic) { - return res.status(404).send('tactic not found.'); - } else { - logger.debug("Success: Updated tactic with id " + tactic.stix.id); - return res.status(200).send(tactic); - } - } catch (err) { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to update tactic. Server error."); +exports.create = async function (req, res) { + // Get the data from the request + const tacticData = req.body; + + const options = { + import: false, + userAccountId: req.user?.userAccountId, + }; + + // Create the tactic + try { + const tactic = await tacticsService.create(tacticData, options); + + logger.debug('Success: Created tactic with id ' + tactic.stix.id); + return res.status(201).send(tactic); + } catch (err) { + if (err instanceof DuplicateIdError) { + logger.warn('Duplicate stix.id and stix.modified'); + return res + .status(409) + .send('Unable to create tactic. Duplicate stix.id and stix.modified properties.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to create tactic. Server error.'); } + } }; -exports.deleteVersionById = async function(req, res) { +exports.updateFull = async function (req, res) { + // Get the data from the request + const tacticData = req.body; + + try { + const tactic = await tacticsService.updateFull( + req.params.stixId, + req.params.modified, + tacticData, + ); + + if (!tactic) { + return res.status(404).send('tactic not found.'); + } else { + logger.debug('Success: Updated tactic with id ' + tactic.stix.id); + return res.status(200).send(tactic); + } + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to update tactic. Server error.'); + } +}; - try { +exports.deleteVersionById = async function (req, res) { + try { + const tactic = await tacticsService.deleteVersionById(req.params.stixId, req.params.modified); - const tactic = await tacticsService.deleteVersionById(req.params.stixId, req.params.modified); + if (!tactic) { + return res.status(404).send('tactic not found.'); + } else { + logger.debug('Success: Deleted tactic with id ' + tactic.stix.id); + return res.status(204).end(); + } + } catch (err) { + logger.error('Delete tactic failed. ' + err); + return res.status(500).send('Unable to delete tactic. Server error.'); + } +}; - if (!tactic) { - return res.status(404).send('tactic not found.'); - } else { - logger.debug("Success: Deleted tactic with id " + tactic.stix.id); - return res.status(204).end(); - } +exports.deleteById = async function (req, res) { + try { + const tactics = await tacticsService.deleteById(req.params.stixId); - } catch (err) { - logger.error('Delete tactic failed. ' + err); - return res.status(500).send('Unable to delete tactic. Server error.'); + if (tactics.deletedCount === 0) { + return res.status(404).send('Tactic not found.'); + } else { + logger.debug(`Success: Deleted tactic with id ${req.params.stixId}`); + return res.status(204).end(); } - + } catch (err) { + logger.error('Delete tactic failed. ' + err); + return res.status(500).send('Unable to delete tactic. Server error.'); + } }; -exports.deleteById = async function(req, res) { - - try { - - const tactics = await tacticsService.deleteById(req.params.stixId); - - if (tactics.deletedCount === 0) { - return res.status(404).send('Tactic not found.'); - } else { - logger.debug(`Success: Deleted tactic with id ${req.params.stixId}`); - return res.status(204).end(); - } - - } catch (err) { - logger.error('Delete tactic failed. ' + err); - return res.status(500).send('Unable to delete tactic. Server error.'); - } -}; +exports.retrieveTechniquesForTactic = async function (req, res) { + try { + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + includePagination: req.query.includePagination, + }; -exports.retrieveTechniquesForTactic = async function(req, res) { - try { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0, - includePagination: req.query.includePagination - }; - - const techniques = await tacticsService.retrieveTechniquesForTactic(req.params.stixId, req.params.modified, options); - if (!techniques) { - return res.status(404).send('tactic not found.'); - } - else { - logger.debug(`Success: Retrieved techniques for tactic with id ${ req.params.stixId }`); - return res.status(200).send(techniques); - } + const techniques = await tacticsService.retrieveTechniquesForTactic( + req.params.stixId, + req.params.modified, + options, + ); + if (!techniques) { + return res.status(404).send('tactic not found.'); + } else { + logger.debug(`Success: Retrieved techniques for tactic with id ${req.params.stixId}`); + return res.status(200).send(techniques); } - catch(err) { - if (err instanceof BadlyFormattedParameterError) { - 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 tactic. Server error.'); - } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + 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 tactic. Server error.'); } + } }; diff --git a/app/controllers/teams-controller.js b/app/controllers/teams-controller.js index c4b94912..8dd61578 100644 --- a/app/controllers/teams-controller.js +++ b/app/controllers/teams-controller.js @@ -4,110 +4,105 @@ const teamsService = require('../services/teams-service'); const logger = require('../lib/logger'); const { NotFoundError, BadlyFormattedParameterError, DuplicateIdError } = require('../exceptions'); -exports.retrieveAll = async function(req, res) { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0, - search: req.query.search, - includePagination: req.query.includePagination, - }; +exports.retrieveAll = async function (req, res) { + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + search: req.query.search, + includePagination: req.query.includePagination, + }; - try { - const results = await teamsService.retrieveAll(options); - if (options.includePagination) { - logger.debug(`Success: Retrieved ${ results.data.length } of ${ results.pagination.total } total team(s)`); - } - else { - logger.debug(`Success: Retrieved ${ results.length } team(s)`); - } - return res.status(200).send(results); - } catch (err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get teams. Server error.'); + try { + const results = await teamsService.retrieveAll(options); + if (options.includePagination) { + logger.debug( + `Success: Retrieved ${results.data.length} of ${results.pagination.total} total team(s)`, + ); + } else { + logger.debug(`Success: Retrieved ${results.length} team(s)`); } + return res.status(200).send(results); + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get teams. Server error.'); + } }; -exports.retrieveById = async function(req, res) { - - try { - const team = await teamsService.retrieveById(req.params.id); - if (!team) { - return res.status(404).send('TEam not found.'); - } - else { - logger.debug(`Success: Retrieved team with id ${ req.params.id }`); - return res.status(200).send(team); - } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - logger.warn('Badly formatted teams id: ' + req.params.id); - return res.status(400).send('Team id is badly formatted.'); - } - else { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get team. Server error.'); - } +exports.retrieveById = async function (req, res) { + try { + const team = await teamsService.retrieveById(req.params.id); + if (!team) { + return res.status(404).send('TEam not found.'); + } else { + logger.debug(`Success: Retrieved team with id ${req.params.id}`); + return res.status(200).send(team); + } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + logger.warn('Badly formatted teams id: ' + req.params.id); + return res.status(400).send('Team id is badly formatted.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get team. Server error.'); } + } }; -exports.create = async function(req, res) { - // Get the data from the request - const teamData = req.body; +exports.create = async function (req, res) { + // Get the data from the request + const teamData = req.body; - // Create the user account - try { - const team = await teamsService.create(teamData); - logger.debug(`Success: Created team with id ${ team.id }`); - return res.status(201).send(team); - } - catch(err) { - if (err instanceof DuplicateIdError) { - logger.error("Duplicated team name"); - return res.status(409).send('Team name must be unique.'); - } - else { - logger.error("Failed with error: " + err); - return res.status(500).send('Unable to create team. Server error.'); - } + // Create the user account + try { + const team = await teamsService.create(teamData); + logger.debug(`Success: Created team with id ${team.id}`); + return res.status(201).send(team); + } catch (err) { + if (err instanceof DuplicateIdError) { + logger.error('Duplicated team name'); + return res.status(409).send('Team name must be unique.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to create team. Server error.'); } + } }; -exports.updateFull = async function(req, res) { - // Get the data from the request - const teamData = req.body; +exports.updateFull = async function (req, res) { + // Get the data from the request + const teamData = req.body; - try { - // Create the technique - const team = await teamsService.updateFull(req.params.id, teamData); - if (!team) { - return res.status(404).send('Team not found.'); - } else { - logger.debug("Success: Updated team with id " + team.id); - return res.status(200).send(team); - } - } catch (err) { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to update team. Server error."); + try { + // Create the technique + const team = await teamsService.updateFull(req.params.id, teamData); + if (!team) { + return res.status(404).send('Team not found.'); + } else { + logger.debug('Success: Updated team with id ' + team.id); + return res.status(200).send(team); } + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to update team. Server error.'); + } }; -exports.delete = async function(req, res) { - - try { - const team = await teamsService.delete(req.params.id); - if (!team) { - return res.status(404).send('Team not found.'); - } else { - logger.debug("Success: Deleted team with id " + team.id); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete team failed. ' + err); - return res.status(500).send('Unable to delete team. Server error.'); +exports.delete = async function (req, res) { + try { + const team = await teamsService.delete(req.params.id); + if (!team) { + return res.status(404).send('Team not found.'); + } else { + logger.debug('Success: Deleted team with id ' + team.id); + return res.status(204).end(); } + } catch (err) { + logger.error('Delete team failed. ' + err); + return res.status(500).send('Unable to delete team. Server error.'); + } }; -exports.retrieveAllUsers = async function(req, res) { +exports.retrieveAllUsers = async function (req, res) { const options = { offset: req.query.offset || 0, limit: req.query.limit || 0, @@ -115,28 +110,28 @@ exports.retrieveAllUsers = async function(req, res) { role: req.query.role, search: req.query.search, includePagination: req.query.includePagination, - includeStixIdentity: req.query.includeStixIdentity + includeStixIdentity: req.query.includeStixIdentity, }; const teamId = req.params.id; - try { - const results = await teamsService.retrieveAllUsers(teamId, options); - if (options.includePagination) { - logger.debug(`Success: Retrieved ${ results.data.length } of ${ results.pagination.total } total user account(s)`); - } - else { - logger.debug(`Success: Retrieved ${ results.length } user account(s)`); - } - return res.status(200).send(results); - } catch(err) { - if (err instanceof NotFoundError) { - logger.error(`Could not find team with with id ${teamId}. `); - return res.status(404).send('Team not found'); - } else { - logger.error('Retrieve users from teamId failed. ' + err); - return res.status(500).send('Unable to retrieve users. Server error.'); - } + try { + const results = await teamsService.retrieveAllUsers(teamId, options); + if (options.includePagination) { + logger.debug( + `Success: Retrieved ${results.data.length} of ${results.pagination.total} total user account(s)`, + ); + } else { + logger.debug(`Success: Retrieved ${results.length} user account(s)`); } - + return res.status(200).send(results); + } catch (err) { + if (err instanceof NotFoundError) { + logger.error(`Could not find team with with id ${teamId}. `); + return res.status(404).send('Team not found'); + } else { + logger.error('Retrieve users from teamId failed. ' + err); + return res.status(500).send('Unable to retrieve users. Server error.'); + } + } }; diff --git a/app/controllers/techniques-controller.js b/app/controllers/techniques-controller.js index 53859b08..c0133537 100644 --- a/app/controllers/techniques-controller.js +++ b/app/controllers/techniques-controller.js @@ -2,193 +2,202 @@ const techniquesService = require('../services/techniques-service'); const logger = require('../lib/logger'); -const { BadlyFormattedParameterError, InvalidQueryStringParameterError, DuplicateIdError } = require('../exceptions'); - -exports.retrieveAll = async function(req, res) { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0, - domain: req.query.domain, - platform: req.query.platform, - state: req.query.state, - includeRevoked: req.query.includeRevoked, - includeDeprecated: req.query.includeDeprecated, - search: req.query.search, - lastUpdatedBy: req.query.lastUpdatedBy, - includePagination: req.query.includePagination - }; - - try { - const results = await techniquesService.retrieveAll(options); - if (options.includePagination) { - logger.debug(`Success: Retrieved ${ results.data.length } of ${ results.pagination.total } total technique(s)`); - } - else { - logger.debug(`Success: Retrieved ${ results.length } technique(s)`); - } - return res.status(200).send(results); - } catch (err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get techniques. Server error.'); +const { + BadlyFormattedParameterError, + InvalidQueryStringParameterError, + DuplicateIdError, +} = require('../exceptions'); + +exports.retrieveAll = async function (req, res) { + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + domain: req.query.domain, + platform: req.query.platform, + state: req.query.state, + includeRevoked: req.query.includeRevoked, + includeDeprecated: req.query.includeDeprecated, + search: req.query.search, + lastUpdatedBy: req.query.lastUpdatedBy, + includePagination: req.query.includePagination, + }; + + try { + const results = await techniquesService.retrieveAll(options); + if (options.includePagination) { + logger.debug( + `Success: Retrieved ${results.data.length} of ${results.pagination.total} total technique(s)`, + ); + } else { + logger.debug(`Success: Retrieved ${results.length} technique(s)`); } + return res.status(200).send(results); + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get techniques. Server error.'); + } }; -exports.retrieveById = async function(req, res) { - const options = { - versions: req.query.versions || 'latest' +exports.retrieveById = async function (req, res) { + const options = { + versions: req.query.versions || 'latest', + }; + try { + const techniques = await techniquesService.retrieveById(req.params.stixId, options); + if (techniques.length === 0) { + return res.status(404).send('Technique not found.'); + } else { + logger.debug( + `Success: Retrieved ${techniques.length} technique(s) with id ${req.params.stixId}`, + ); + return res.status(200).send(techniques); } - try { - const techniques = await techniquesService.retrieveById(req.params.stixId, options); - if (techniques.length === 0) { - return res.status(404).send('Technique not found.'); - } - else { - logger.debug(`Success: Retrieved ${ techniques.length } technique(s) with id ${ req.params.stixId }`); - return res.status(200).send(techniques); - } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - logger.warn('Badly formatted stix id: ' + req.params.stixId); - return res.status(400).send('Stix id is badly formatted.'); - } - else if (err instanceof InvalidQueryStringParameterError) { - logger.warn('Invalid query string: versions=' + req.query.versions); - return res.status(400).send('Query string parameter versions is invalid.'); - } - else { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get techniques. Server error.'); - } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + logger.warn('Badly formatted stix id: ' + req.params.stixId); + return res.status(400).send('Stix id is badly formatted.'); + } else if (err instanceof InvalidQueryStringParameterError) { + logger.warn('Invalid query string: versions=' + req.query.versions); + return res.status(400).send('Query string parameter versions is invalid.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get techniques. Server error.'); } + } }; -exports.retrieveVersionById = async function(req, res) { - - try { - const technique = await techniquesService.retrieveVersionById(req.params.stixId, req.params.modified); - if (!technique) { - return res.status(404).send('Technique not found.'); - } - else { - logger.debug(`Success: Retrieved technique with id ${technique.id}`); - return res.status(200).send(technique); - } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - 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 technique. Server error.'); - } - } -}; - -exports.create = async function(req, res) { - // Get the data from the request - const techniqueData = req.body; - const options = { - import: false, - userAccountId: req.user?.userAccountId - }; - - // Create the technique - try { - const technique = await techniquesService.create(techniqueData, options); - - logger.debug("Success: Created technique with id " + technique.stix.id); - return res.status(201).send(technique); +exports.retrieveVersionById = async function (req, res) { + try { + const technique = await techniquesService.retrieveVersionById( + req.params.stixId, + req.params.modified, + ); + if (!technique) { + return res.status(404).send('Technique not found.'); + } else { + logger.debug(`Success: Retrieved technique with id ${technique.id}`); + return res.status(200).send(technique); } - catch(err) { - if (err instanceof DuplicateIdError) { - logger.warn("Duplicate stix.id and stix.modified"); - return res.status(409).send('Unable to create technique. Duplicate stix.id and stix.modified properties.'); - } - else { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to create technique. Server error."); - } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + 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 technique. Server error.'); } + } }; -exports.updateFull = async function(req, res) { - // Get the data from the request - const techniqueData = req.body; - - try { - // Create the technique - const technique = await techniquesService.updateFull(req.params.stixId, req.params.modified, techniqueData); - if (!technique) { - return res.status(404).send('Technique not found.'); - } else { - logger.debug("Success: Updated technique with id " + technique.stix.id); - return res.status(200).send(technique); - } - } catch (err) { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to update technique. Server error."); +exports.create = async function (req, res) { + // Get the data from the request + const techniqueData = req.body; + const options = { + import: false, + userAccountId: req.user?.userAccountId, + }; + + // Create the technique + try { + const technique = await techniquesService.create(techniqueData, options); + + logger.debug('Success: Created technique with id ' + technique.stix.id); + return res.status(201).send(technique); + } catch (err) { + if (err instanceof DuplicateIdError) { + logger.warn('Duplicate stix.id and stix.modified'); + return res + .status(409) + .send('Unable to create technique. Duplicate stix.id and stix.modified properties.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to create technique. Server error.'); } + } }; -exports.deleteVersionById = async function(req, res) { +exports.updateFull = async function (req, res) { + // Get the data from the request + const techniqueData = req.body; - try { - const technique = await techniquesService.deleteVersionById(req.params.stixId, req.params.modified); - if (!technique) { - return res.status(404).send('Technique not found.'); - } else { - logger.debug("Success: Deleted technique with id " + technique.stix.id); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete technique failed. ' + err); - return res.status(500).send('Unable to delete technique. Server error.'); + try { + // Create the technique + const technique = await techniquesService.updateFull( + req.params.stixId, + req.params.modified, + techniqueData, + ); + if (!technique) { + return res.status(404).send('Technique not found.'); + } else { + logger.debug('Success: Updated technique with id ' + technique.stix.id); + return res.status(200).send(technique); } + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to update technique. Server error.'); + } }; -exports.deleteById = async function(req, res) { - - try { - const techniques = await techniquesService.deleteById(req.params.stixId); - if (techniques.deletedCount === 0) { - return res.status(404).send('Technique not found.'); - } else { - logger.debug(`Success: Deleted technique with id ${req.params.stixId}`); - return res.status(204).end(); - } - } catch (err) { - logger.error('Delete technique failed. ' + err); - return res.status(500).send('Unable to delete technique. Server error.'); - } +exports.deleteVersionById = async function (req, res) { + try { + const technique = await techniquesService.deleteVersionById( + req.params.stixId, + req.params.modified, + ); + if (!technique) { + return res.status(404).send('Technique not found.'); + } else { + logger.debug('Success: Deleted technique with id ' + technique.stix.id); + return res.status(204).end(); + } + } catch (err) { + logger.error('Delete technique failed. ' + err); + return res.status(500).send('Unable to delete technique. Server error.'); + } +}; +exports.deleteById = async function (req, res) { + try { + const techniques = await techniquesService.deleteById(req.params.stixId); + if (techniques.deletedCount === 0) { + return res.status(404).send('Technique not found.'); + } else { + logger.debug(`Success: Deleted technique with id ${req.params.stixId}`); + return res.status(204).end(); + } + } catch (err) { + logger.error('Delete technique failed. ' + err); + return res.status(500).send('Unable to delete technique. Server error.'); + } }; -exports.retrieveTacticsForTechnique = async function(req, res) { - try { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0, - includePagination: req.query.includePagination - }; +exports.retrieveTacticsForTechnique = async function (req, res) { + try { + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + includePagination: req.query.includePagination, + }; - const tactics = await techniquesService.retrieveTacticsForTechnique(req.params.stixId, req.params.modified, options); - if (!tactics) { - return res.status(404).send('Technique not found.'); - } - else { - logger.debug(`Success: Retrieved tactics for technique with id ${ req.params.stixId }`); - return res.status(200).send(tactics); - } + const tactics = await techniquesService.retrieveTacticsForTechnique( + req.params.stixId, + req.params.modified, + options, + ); + if (!tactics) { + return res.status(404).send('Technique not found.'); + } else { + logger.debug(`Success: Retrieved tactics for technique with id ${req.params.stixId}`); + return res.status(200).send(tactics); } - catch(err) { - if (err instanceof BadlyFormattedParameterError) { - 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 tactics for technique. Server error.'); - } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + 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 tactics for technique. Server error.'); } + } }; diff --git a/app/controllers/user-accounts-controller.js b/app/controllers/user-accounts-controller.js index f9fb8964..5662324d 100644 --- a/app/controllers/user-accounts-controller.js +++ b/app/controllers/user-accounts-controller.js @@ -6,230 +6,223 @@ const config = require('../config/config'); const { BadlyFormattedParameterError, DuplicateEmailError } = require('../exceptions'); exports.retrieveAll = async function (req, res) { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0, - status: req.query.status, - role: req.query.role, - search: req.query.search, - includePagination: req.query.includePagination, - includeStixIdentity: req.query.includeStixIdentity - }; - - try { - - const results = await userAccountsService.retrieveAll(options); - - if (options.includePagination) { - logger.debug(`Success: Retrieved ${results.data.length} of ${results.pagination.total} total user account(s)`); - } - else { - logger.debug(`Success: Retrieved ${results.length} user account(s)`); - } - return res.status(200).send(results); - } catch (err) { - console.log("retrieveall"); - console.log(err); - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get user accounts. Server error.'); - } + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + status: req.query.status, + role: req.query.role, + search: req.query.search, + includePagination: req.query.includePagination, + includeStixIdentity: req.query.includeStixIdentity, + }; + + try { + const results = await userAccountsService.retrieveAll(options); + + if (options.includePagination) { + logger.debug( + `Success: Retrieved ${results.data.length} of ${results.pagination.total} total user account(s)`, + ); + } else { + logger.debug(`Success: Retrieved ${results.length} user account(s)`); + } + return res.status(200).send(results); + } catch (err) { + console.log('retrieveall'); + console.log(err); + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get user accounts. Server error.'); + } }; exports.retrieveById = async function (req, res) { - const options = { - includeStixIdentity: req.query.includeStixIdentity - }; - - try { - const userAccount = await userAccountsService.retrieveById(req.params.id, options); - if (!userAccount) { - return res.status(404).send('User account not found.'); - } - else { - logger.debug(`Success: Retrieved user account with id ${req.params.id}`); - return res.status(200).send(userAccount); - } - } catch (err) { - if (err instanceof BadlyFormattedParameterError) { - logger.warn('Badly formatted user account id: ' + req.params.id); - return res.status(400).send('User account id is badly formatted.'); - } - else { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get user account. Server error.'); - } - } + const options = { + includeStixIdentity: req.query.includeStixIdentity, + }; + + try { + const userAccount = await userAccountsService.retrieveById(req.params.id, options); + if (!userAccount) { + return res.status(404).send('User account not found.'); + } else { + logger.debug(`Success: Retrieved user account with id ${req.params.id}`); + return res.status(200).send(userAccount); + } + } catch (err) { + if (err instanceof BadlyFormattedParameterError) { + logger.warn('Badly formatted user account id: ' + req.params.id); + return res.status(400).send('User account id is badly formatted.'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get user account. Server error.'); + } + } }; -exports.create = async function(req, res) { - // Get the data from the request - const userAccountData = req.body; - - if (userAccountData.status !== 'active' && userAccountData.role ) { - logger.warn('Unable to create user account, role not allowed when status is not active'); - return res.status(400).send('role not allowed when status is not active'); - } - - // Create the user account - try { - const userAccount = await userAccountsService.create(userAccountData); - - logger.debug(`Success: Created user account with id ${ userAccount.id }`); - return res.status(201).send(userAccount); - } - catch(err) { - if (err instanceof DuplicateEmailError) { - logger.warn(`Unable to create user account, duplicate email: ${ userAccountData.email }`); - return res.status(400).send('Duplicate email'); - } - else { - console.log("create"); - console.log(err); - logger.error("Failed with error: " + err); - return res.status(500).send('Unable to create user account. Server error.'); - } - } +exports.create = async function (req, res) { + // Get the data from the request + const userAccountData = req.body; + + if (userAccountData.status !== 'active' && userAccountData.role) { + logger.warn('Unable to create user account, role not allowed when status is not active'); + return res.status(400).send('role not allowed when status is not active'); + } + + // Create the user account + try { + const userAccount = await userAccountsService.create(userAccountData); + + logger.debug(`Success: Created user account with id ${userAccount.id}`); + return res.status(201).send(userAccount); + } catch (err) { + if (err instanceof DuplicateEmailError) { + logger.warn(`Unable to create user account, duplicate email: ${userAccountData.email}`); + return res.status(400).send('Duplicate email'); + } else { + console.log('create'); + console.log(err); + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to create user account. Server error.'); + } + } }; exports.updateFullAsync = async function (req, res) { - try { - // Create the technique - const userAccount = await userAccountsService.updateFull(req.params.id, req.body); - if (!userAccount) { - return res.status(404).send('User account not found.'); - } else { - logger.debug("Success: Updated user account with id " + userAccount.id); - return res.status(200).send(userAccount); - } - } catch (err) { - console.log("updatefullasync"); - console.log(err); - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to update user account. Server error."); - } + try { + // Create the technique + const userAccount = await userAccountsService.updateFull(req.params.id, req.body); + if (!userAccount) { + return res.status(404).send('User account not found.'); + } else { + logger.debug('Success: Updated user account with id ' + userAccount.id); + return res.status(200).send(userAccount); + } + } catch (err) { + console.log('updatefullasync'); + console.log(err); + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to update user account. Server error.'); + } }; exports.updateFull = async function (req, res) { - // Create the technique - try { - const userAccount = await userAccountsService.updateFull(req.params.id, req.body); - if (!userAccount) { - return res.status(404).send('User account not found.'); - } else { - logger.debug("Success: Updated user account with id " + userAccount.id); - return res.status(200).send(userAccount); - } - } catch (err) { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to update user account. Server error."); - } + // Create the technique + try { + const userAccount = await userAccountsService.updateFull(req.params.id, req.body); + if (!userAccount) { + return res.status(404).send('User account not found.'); + } else { + logger.debug('Success: Updated user account with id ' + userAccount.id); + return res.status(200).send(userAccount); + } + } catch (err) { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to update user account. Server error.'); + } }; exports.deleteAsync = async function (req, res) { - try { - const userAccount = await userAccountsService.delete(req.params.id); - if (!userAccount) { - return res.status(404).send('User account not found.'); - } else { - logger.debug("Success: Deleted user account with id " + userAccount.id); - return res.status(204).end(); - } - } catch (err) { - console.log("deleteasync"); - console.log(err); - logger.error('Delete user account failed. ' + err); - return res.status(500).send('Unable to delete user account. Server error.'); - } + try { + const userAccount = await userAccountsService.delete(req.params.id); + if (!userAccount) { + return res.status(404).send('User account not found.'); + } else { + logger.debug('Success: Deleted user account with id ' + userAccount.id); + return res.status(204).end(); + } + } catch (err) { + console.log('deleteasync'); + console.log(err); + logger.error('Delete user account failed. ' + err); + return res.status(500).send('Unable to delete user account. Server error.'); + } }; exports.delete = async function (req, res) { - - try { - const userAccount = await userAccountsService.delete(req.params.id); - if (!userAccount) { - return res.status(404).send('User account not found.'); - } else { - logger.debug("Success: Deleted user account with id " + userAccount.id); - return res.status(204).end(); - } - } catch (err) { - console.log("delete"); - console.log(err); - logger.error('Delete user account failed. ' + err); - return res.status(500).send('Unable to delete user account. Server error.'); - } + try { + const userAccount = await userAccountsService.delete(req.params.id); + if (!userAccount) { + return res.status(404).send('User account not found.'); + } else { + logger.debug('Success: Deleted user account with id ' + userAccount.id); + return res.status(204).end(); + } + } catch (err) { + console.log('delete'); + console.log(err); + logger.error('Delete user account failed. ' + err); + return res.status(500).send('Unable to delete user account. Server error.'); + } }; -exports.register = async function(req, res) { - // The function supports self-registration of a logged in user - - if (config.userAuthn.mechanism === 'anonymous') { - logger.warn('Unable to register user account, app configured to use anonymous authentication'); - return res.status(400).send('Cannot register user accounts when anonymous authentication is enabled'); - } - else if (!req.user) { - logger.warn('Unable to register user account, not logged in'); - return res.status(400).send('Must login before registering'); - } - else if (req.user.registered) { - logger.warn('Unable to register user account, already registered'); - return res.status(400).send('Already registered'); - } - else if (req.user.service) { - logger.warn('Unable to register service account'); - return res.status(400).send('Cannot register service account'); - } - - const userAccountData = { - email: req.user.email, - username: req.user.name, - displayName: req.user.displayName, - status: 'pending' - } - - // Register (create) the user account - try { - const userAccount = await userAccountsService.create(userAccountData); - - logger.debug(`Success: Registed user account with id ${ userAccount.id }`); - return res.status(201).send(userAccount); - } - catch(err) { - console.log("register"); - console.log(err); - if (err.message === userAccountsService.errors.duplicateEmail) { - logger.warn(`Unable to register user account, duplicate email: ${ userAccountData.email }`); - return res.status(400).send('Duplicate email'); - } - else { - logger.error("Failed with error: " + err); - return res.status(500).send('Unable to register user account. Server error.'); - } - } +exports.register = async function (req, res) { + // The function supports self-registration of a logged in user + + if (config.userAuthn.mechanism === 'anonymous') { + logger.warn('Unable to register user account, app configured to use anonymous authentication'); + return res + .status(400) + .send('Cannot register user accounts when anonymous authentication is enabled'); + } else if (!req.user) { + logger.warn('Unable to register user account, not logged in'); + return res.status(400).send('Must login before registering'); + } else if (req.user.registered) { + logger.warn('Unable to register user account, already registered'); + return res.status(400).send('Already registered'); + } else if (req.user.service) { + logger.warn('Unable to register service account'); + return res.status(400).send('Cannot register service account'); + } + + const userAccountData = { + email: req.user.email, + username: req.user.name, + displayName: req.user.displayName, + status: 'pending', + }; + + // Register (create) the user account + try { + const userAccount = await userAccountsService.create(userAccountData); + + logger.debug(`Success: Registed user account with id ${userAccount.id}`); + return res.status(201).send(userAccount); + } catch (err) { + console.log('register'); + console.log(err); + if (err.message === userAccountsService.errors.duplicateEmail) { + logger.warn(`Unable to register user account, duplicate email: ${userAccountData.email}`); + return res.status(400).send('Duplicate email'); + } else { + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to register user account. Server error.'); + } + } }; exports.retrieveTeamsByUserId = async function (req, res) { - const options = { - offset: req.query.offset || 0, - limit: req.query.limit || 0, - status: req.query.status, - includePagination: req.query.includePagination, - }; - - const userId = req.params.id; - try { - const results = await userAccountsService.constructor.retrieveTeamsByUserId(userId, options); - if (options.includePagination) { - logger.debug(`Success: Retrieved ${results.data.length} of ${results.pagination.total} total team(s)`); - } - else { - logger.debug(`Success: Retrieved ${results.length} team(s)`); - } - return res.status(200).send(results); - } catch (err) { - console.log("retrieveTeamsByUserId"); - console.log(err); - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get teams. Server error.'); - } -}; \ No newline at end of file + const options = { + offset: req.query.offset || 0, + limit: req.query.limit || 0, + status: req.query.status, + includePagination: req.query.includePagination, + }; + + const userId = req.params.id; + try { + const results = await userAccountsService.constructor.retrieveTeamsByUserId(userId, options); + if (options.includePagination) { + logger.debug( + `Success: Retrieved ${results.data.length} of ${results.pagination.total} total team(s)`, + ); + } else { + logger.debug(`Success: Retrieved ${results.length} team(s)`); + } + return res.status(200).send(results); + } catch (err) { + console.log('retrieveTeamsByUserId'); + console.log(err); + logger.error('Failed with error: ' + err); + return res.status(500).send('Unable to get teams. Server error.'); + } +}; diff --git a/app/exceptions/index.js b/app/exceptions/index.js index dc144f33..3a8a2e59 100644 --- a/app/exceptions/index.js +++ b/app/exceptions/index.js @@ -2,181 +2,179 @@ 'use strict'; class CustomError extends Error { - constructor(message, options = {}) { - super(message); - - // Apply options (if defined) to the error object - for (const key in options) { - if (Object.prototype.hasOwnProperty.call(options, key)) { - this[key] = options[key]; - } - } + constructor(message, options = {}) { + super(message); + + // Apply options (if defined) to the error object + for (const key in options) { + if (Object.prototype.hasOwnProperty.call(options, key)) { + this[key] = options[key]; + } } + } } class MissingParameterError extends CustomError { - constructor(options) { - super('Missing required parameter', options); - } + constructor(options) { + super('Missing required parameter', options); + } } class BadlyFormattedParameterError extends CustomError { - constructor(options) { - super('Badly formatted parameter', options); - } + constructor(options) { + super('Badly formatted parameter', options); + } } class DuplicateIdError extends CustomError { - constructor(options) { - super('Duplicate id', options); - } + constructor(options) { + super('Duplicate id', options); + } } class DuplicateEmailError extends CustomError { - constructor(options) { - super('Duplicate email', options); - } + constructor(options) { + super('Duplicate email', options); + } } class DuplicateNameError extends CustomError { - constructor(options) { - super('Duplicate name', options); - } + constructor(options) { + super('Duplicate name', options); + } } class NotFoundError extends CustomError { - constructor(options) { - super('Document not found', options); - } + constructor(options) { + super('Document not found', options); + } } class InvalidQueryStringParameterError extends CustomError { - constructor(options) { - super('Invalid query string parameter', options); - } + constructor(options) { + super('Invalid query string parameter', options); + } } class CannotUpdateStaticObjectError extends CustomError { - constructor(options) { - super('Cannot update static object', options); - } + constructor(options) { + super('Cannot update static object', options); + } } class IdentityServiceError extends CustomError { - constructor(options) { - super('An error occurred in the identities service.', options); - } + constructor(options) { + super('An error occurred in the identities service.', options); + } } class TechniquesServiceError extends CustomError { - constructor(options) { - super('An error occurred in the techniques service.', options); - } + constructor(options) { + super('An error occurred in the techniques service.', options); + } } class TacticsServiceError extends CustomError { - constructor(options) { - super('An error occurred in the tactics service.', options); - } + constructor(options) { + super('An error occurred in the tactics service.', options); + } } class GenericServiceError extends CustomError { - constructor(options) { - super('An error occurred in a service.', options); - } + constructor(options) { + super('An error occurred in a service.', options); + } } class DatabaseError extends CustomError { - constructor(options) { - super('The database operation failed.', options); - } + constructor(options) { + super('The database operation failed.', options); + } } class NotImplementedError extends CustomError { - constructor(moduleName, functionName, options) { - super(`The function ${functionName} in module ${moduleName} is not implemented!`, options); - - } + constructor(moduleName, functionName, options) { + super(`The function ${functionName} in module ${moduleName} is not implemented!`, options); + } } class PropertyNotAllowedError extends CustomError { - constructor(propertyName, options) { - super(`Unable to create software, property ${propertyName} is not allowed`, options); - } + constructor(propertyName, options) { + super(`Unable to create software, property ${propertyName} is not allowed`, options); + } } class SystemConfigurationNotFound extends CustomError { - constructor(options) { - super(`System configuration not found`, options); - } + constructor(options) { + super(`System configuration not found`, options); + } } class OrganizationIdentityNotSetError extends CustomError { - constructor(options) { - super(`Organization identity not set`, options); - } + constructor(options) { + super(`Organization identity not set`, options); + } } class DefaultMarkingDefinitionsNotFoundError extends CustomError { - constructor(options) { - super(`Default marking definitions not found`, options); - } + constructor(options) { + super(`Default marking definitions not found`, options); + } } class OrganizationIdentityNotFoundError extends CustomError { - constructor(identityRef, options) { - super(`Identity with id ${ identityRef } not found`, options); - } + constructor(identityRef, options) { + super(`Identity with id ${identityRef} not found`, options); + } } class AnonymousUserAccountNotSetError extends CustomError { - constructor(options) { - super(`Anonymous user account not set`, options); - } + constructor(options) { + super(`Anonymous user account not set`, options); + } } class AnonymousUserAccountNotFoundError extends CustomError { - constructor(userAccountid, options) { - super(`Anonymous user account ${ userAccountid } not found`, options); - } + constructor(userAccountid, options) { + super(`Anonymous user account ${userAccountid} not found`, options); + } } class InvalidTypeError extends CustomError { - constructor(options) { - super('Invalid stix.type', options); - } + constructor(options) { + super('Invalid stix.type', options); + } } module.exports = { - - //** General errors */ - NotImplementedError, - - //** User-related errors */ - MissingParameterError, - BadlyFormattedParameterError, - InvalidQueryStringParameterError, - CannotUpdateStaticObjectError, - - //** Database-related errors */ - DuplicateIdError, - DuplicateEmailError, - DuplicateNameError, - NotFoundError, - DatabaseError, - - /** Service-specific errors */ - GenericServiceError, - IdentityServiceError, - TechniquesServiceError, - TacticsServiceError, - PropertyNotAllowedError, - SystemConfigurationNotFound, - DefaultMarkingDefinitionsNotFoundError, - OrganizationIdentityNotSetError, - OrganizationIdentityNotFoundError, - AnonymousUserAccountNotSetError, - AnonymousUserAccountNotFoundError, - - InvalidTypeError, + //** General errors */ + NotImplementedError, + + //** User-related errors */ + MissingParameterError, + BadlyFormattedParameterError, + InvalidQueryStringParameterError, + CannotUpdateStaticObjectError, + + //** Database-related errors */ + DuplicateIdError, + DuplicateEmailError, + DuplicateNameError, + NotFoundError, + DatabaseError, + + /** Service-specific errors */ + GenericServiceError, + IdentityServiceError, + TechniquesServiceError, + TacticsServiceError, + PropertyNotAllowedError, + SystemConfigurationNotFound, + DefaultMarkingDefinitionsNotFoundError, + OrganizationIdentityNotSetError, + OrganizationIdentityNotFoundError, + AnonymousUserAccountNotSetError, + AnonymousUserAccountNotFoundError, + + InvalidTypeError, }; diff --git a/app/index.js b/app/index.js index 556c9ffa..ace57866 100644 --- a/app/index.js +++ b/app/index.js @@ -6,123 +6,126 @@ * @param {object} helmet - helmet module */ function disableUpgradeInsecureRequests(app, helmet) { - const defaultDirectives = helmet.contentSecurityPolicy.getDefaultDirectives(); - delete defaultDirectives['upgrade-insecure-requests']; - - app.use(helmet.contentSecurityPolicy({ - directives: { - ...defaultDirectives, - }, - })); + const defaultDirectives = helmet.contentSecurityPolicy.getDefaultDirectives(); + delete defaultDirectives['upgrade-insecure-requests']; + + app.use( + helmet.contentSecurityPolicy({ + directives: { + ...defaultDirectives, + }, + }), + ); } /** * Creates a new instance of the express app. * @return The new express app */ -exports.initializeApp = async function() { - const logger = require('./lib/logger'); - logger.info('ATT&CK Workbench REST API app starting'); - - // Configure the app - logger.info('Configuring the app'); - const config = require('./config/config'); - - // Create the express application - logger.info('Starting express'); - const express = require('express'); - const app = express(); - - // Add a unique id to each request - const requestId = require('./lib/requestId'); - app.use(requestId); - - // Allow CORS - if (config.server.enableCorsAnyOrigin) { - logger.info('CORS is enabled'); - const cors = require('cors'); - const corsOptions = { - credentials: true, - origin: true - }; - app.use(cors(corsOptions)); - } - else { - logger.info('CORS is not enabled'); - } - - // Compress response bodies - const compression = require('compression'); - app.use(compression()); - - // Set HTTP response headers - const helmet = require("helmet"); - app.use(helmet()); - disableUpgradeInsecureRequests(app, helmet); - - // Development Environment - if (config.app.env === 'development') { - // Enable request logging - logger.info('Enabling HTTP request logging'); - const morgan = require('morgan'); - app.use(morgan('dev', {stream: logger.stream})); -// morgan.token('requestId', req => req.id); -// app.use(morgan('[:requestId] :method :url :status :response-time ms - :res[content-length]', { stream: logger.stream })); - - // Enable Swagger UI - logger.info('Enabling Swagger UI'); - const swaggerUi = require('swagger-ui-express'); - const refParser = require("@apidevtools/json-schema-ref-parser"); - const openApiDoc = await refParser.dereference(config.openApi.specPath); - app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openApiDoc)); - } - - // Configure server-side sessions - // TBD: Replace default MemoryStore with production quality session storage - const session = require('express-session'); - const sessionOptions = { - secret: config.session.secret, - resave: false, - saveUninitialized: false - } - app.use(session(sessionOptions)); - - // Set up the static routes - logger.info('Configuring static routes'); - app.use(express.static('public')); - - // Configure passport with the user authentication mechanism - const authnConfiguration = require('./lib/authn-configuration'); - if (config.userAuthn.mechanism === 'oidc') { - await authnConfiguration.configurePassport('oidc'); - } - else if (config.userAuthn.mechanism === 'anonymous') { - await authnConfiguration.configurePassport('anonymous'); - } - - // Configure passport for service authentication if enabled - if (config.serviceAuthn.oidcClientCredentials.enable || config.serviceAuthn.challengeApikey.enable) { - await authnConfiguration.configurePassport('bearer'); - } - if (config.serviceAuthn.oidcClientCredentials.enable) { - logger.info('Enabled service authentication: client credentials'); - } - if (config.serviceAuthn.challengeApikey.enable) { - logger.info('Enabled service authentication: challenge apikey'); - } - if (config.serviceAuthn.basicApikey.enable) { - await authnConfiguration.configurePassport('basic'); - logger.info('Enabled service authentication: basic apikey'); - } - - // Set up the api routes - logger.info('Configuring REST API routes'); - const routes = require('./routes'); - app.use(routes); - - // Make the config and logger objects accessible from the app object - app.set('config', config); - app.set('logger', logger); - - return app; -} +exports.initializeApp = async function () { + const logger = require('./lib/logger'); + logger.info('ATT&CK Workbench REST API app starting'); + + // Configure the app + logger.info('Configuring the app'); + const config = require('./config/config'); + + // Create the express application + logger.info('Starting express'); + const express = require('express'); + const app = express(); + + // Add a unique id to each request + const requestId = require('./lib/requestId'); + app.use(requestId); + + // Allow CORS + if (config.server.enableCorsAnyOrigin) { + logger.info('CORS is enabled'); + const cors = require('cors'); + const corsOptions = { + credentials: true, + origin: true, + }; + app.use(cors(corsOptions)); + } else { + logger.info('CORS is not enabled'); + } + + // Compress response bodies + const compression = require('compression'); + app.use(compression()); + + // Set HTTP response headers + const helmet = require('helmet'); + app.use(helmet()); + disableUpgradeInsecureRequests(app, helmet); + + // Development Environment + if (config.app.env === 'development') { + // Enable request logging + logger.info('Enabling HTTP request logging'); + const morgan = require('morgan'); + app.use(morgan('dev', { stream: logger.stream })); + // morgan.token('requestId', req => req.id); + // app.use(morgan('[:requestId] :method :url :status :response-time ms - :res[content-length]', { stream: logger.stream })); + + // Enable Swagger UI + logger.info('Enabling Swagger UI'); + const swaggerUi = require('swagger-ui-express'); + const refParser = require('@apidevtools/json-schema-ref-parser'); + const openApiDoc = await refParser.dereference(config.openApi.specPath); + app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openApiDoc)); + } + + // Configure server-side sessions + // TBD: Replace default MemoryStore with production quality session storage + const session = require('express-session'); + const sessionOptions = { + secret: config.session.secret, + resave: false, + saveUninitialized: false, + }; + app.use(session(sessionOptions)); + + // Set up the static routes + logger.info('Configuring static routes'); + app.use(express.static('public')); + + // Configure passport with the user authentication mechanism + const authnConfiguration = require('./lib/authn-configuration'); + if (config.userAuthn.mechanism === 'oidc') { + await authnConfiguration.configurePassport('oidc'); + } else if (config.userAuthn.mechanism === 'anonymous') { + await authnConfiguration.configurePassport('anonymous'); + } + + // Configure passport for service authentication if enabled + if ( + config.serviceAuthn.oidcClientCredentials.enable || + config.serviceAuthn.challengeApikey.enable + ) { + await authnConfiguration.configurePassport('bearer'); + } + if (config.serviceAuthn.oidcClientCredentials.enable) { + logger.info('Enabled service authentication: client credentials'); + } + if (config.serviceAuthn.challengeApikey.enable) { + logger.info('Enabled service authentication: challenge apikey'); + } + if (config.serviceAuthn.basicApikey.enable) { + await authnConfiguration.configurePassport('basic'); + logger.info('Enabled service authentication: basic apikey'); + } + + // Set up the api routes + logger.info('Configuring REST API routes'); + const routes = require('./routes'); + app.use(routes); + + // Make the config and logger objects accessible from the app object + app.set('config', config); + app.set('logger', logger); + + return app; +}; diff --git a/app/lib/authenticated-request.js b/app/lib/authenticated-request.js index 13d4078a..fb7e9696 100644 --- a/app/lib/authenticated-request.js +++ b/app/lib/authenticated-request.js @@ -6,64 +6,50 @@ const authenticationService = require('../services/authentication-service'); /** * Send an HTTP GET request to the provided URL, including the appropriate Authorization header */ -exports.get = async function(url) { - try { - const tokenString = await authenticationService.getAccessToken(); - const authorizationHeader = `Bearer ${ tokenString }`; - return await superagent - .get(url) - .set('Authorization', authorizationHeader); +exports.get = async function (url) { + try { + const tokenString = await authenticationService.getAccessToken(); + const authorizationHeader = `Bearer ${tokenString}`; + return await superagent.get(url).set('Authorization', authorizationHeader); + } catch (err) { + if (Object.values(authenticationService.errors).includes(err.message)) { + throw new Error(`Authentication Error, ${err.message}`); + } else { + throw err; } - catch(err) { - if (Object.values(authenticationService.errors).includes(err.message)) { - throw new Error(`Authentication Error, ${ err.message }`); - } - else { - throw err; - } - } -} + } +}; /** * Send an HTTP PUT request to the provided URL, including the appropriate Authorization header */ -exports.put = async function(url, data) { - try { - const tokenString = await authenticationService.getAccessToken(); - const authorizationHeader = `Bearer ${ tokenString }`; - return await superagent - .put(url) - .set('Authorization', authorizationHeader) - .send(data); - } - catch(err) { - if (Object.values(authenticationService.errors).includes(err.message)) { - throw new Error(`Authentication Error, ${ err.message }`); - } - else { - throw err; - } +exports.put = async function (url, data) { + try { + const tokenString = await authenticationService.getAccessToken(); + const authorizationHeader = `Bearer ${tokenString}`; + return await superagent.put(url).set('Authorization', authorizationHeader).send(data); + } catch (err) { + if (Object.values(authenticationService.errors).includes(err.message)) { + throw new Error(`Authentication Error, ${err.message}`); + } else { + throw err; } -} + } +}; /** * Send an HTTP POST request to the provided URL, including the appropriate Authorization header */ -exports.post = async function(url, data) { - try { - const tokenString = await authenticationService.getAccessToken(); - const authorizationHeader = `Bearer ${ tokenString }`; - return await superagent - .post(url) - .set('Authorization', authorizationHeader) - .send(data); - } - catch(err) { - if (Object.values(authenticationService.errors).includes(err.message)) { - throw new Error(`Authentication Error, ${ err.message }`); - } - else { - throw err; - } +exports.post = async function (url, data) { + try { + const tokenString = await authenticationService.getAccessToken(); + const authorizationHeader = `Bearer ${tokenString}`; + return await superagent.post(url).set('Authorization', authorizationHeader).send(data); + } catch (err) { + if (Object.values(authenticationService.errors).includes(err.message)) { + throw new Error(`Authentication Error, ${err.message}`); + } else { + throw err; } -} + } +}; diff --git a/app/lib/authn-anonymous.js b/app/lib/authn-anonymous.js index a24b7b72..5a1561b7 100644 --- a/app/lib/authn-anonymous.js +++ b/app/lib/authn-anonymous.js @@ -5,74 +5,74 @@ const AnonymousUuidStrategy = require('passport-anonym-uuid'); const systemConfigurationService = require('../services/system-configuration-service'); let strategyName; -exports.strategyName = function() { - return strategyName; -} +exports.strategyName = function () { + return strategyName; +}; /** * This function takes the user session object and returns the value (the userSessionKey) that will be * stored in the express session for this user */ -exports.serializeUser = function(userSession, done) { - if (userSession.strategy === 'anonymId') { - const userSessionKey = { - strategy: 'anonymId', - sessionId: userSession.anonymousUuid - } +exports.serializeUser = function (userSession, done) { + if (userSession.strategy === 'anonymId') { + const userSessionKey = { + strategy: 'anonymId', + sessionId: userSession.anonymousUuid, + }; - done(null, userSessionKey); - } - else { - // Try the next serializer - done('pass'); - } + done(null, userSessionKey); + } else { + // Try the next serializer + done('pass'); + } }; /** * This function takes the userSessionKey (the value stored in the express session for this user) and * returns the user session object */ -exports.deserializeUser = function(userSessionKey, done) { - if (userSessionKey.strategy === 'anonymId') { - makeUserSession(userSessionKey.sessionId) - .then(userSession => done(null, userSession)) - .catch(err => done(err)); - } - else { - // Try the next deserializer - done('pass'); - } +exports.deserializeUser = function (userSessionKey, done) { + if (userSessionKey.strategy === 'anonymId') { + makeUserSession(userSessionKey.sessionId) + .then((userSession) => done(null, userSession)) + .catch((err) => done(err)); + } else { + // Try the next deserializer + done('pass'); + } }; -exports.getStrategy = function() { - const strategy = new AnonymousUuidStrategy(verifyCallback); - strategyName = strategy.name; +exports.getStrategy = function () { + const strategy = new AnonymousUuidStrategy(verifyCallback); + strategyName = strategy.name; - return strategy; -} + return strategy; +}; /** * This function is called by the strategy after the user has authenticated using the anonymous strategy * It creates and returns the user session for this user */ function verifyCallback(req, uuid, done) { - // The anonymous strategy creates a new uuid for each login - makeUserSession(uuid) - .then(userSession => done(null, userSession)) - .catch(err => done(err)); + // The anonymous strategy creates a new uuid for each login + makeUserSession(uuid) + .then((userSession) => done(null, userSession)) + .catch((err) => done(err)); } async function makeUserSession(uuid) { - const anonymousUserAccount = await systemConfigurationService.retrieveAnonymousUserAccount(); + const anonymousUserAccount = await systemConfigurationService.retrieveAnonymousUserAccount(); - const userAccountData = (({ email, name, status, role }) => ({ email, name, status, role }))(anonymousUserAccount); - const userSession = { - strategy: 'anonymId', - userAccountId: anonymousUserAccount.id, - ...userAccountData, - registered: true, - anonymousUuid: uuid - }; + const userAccountData = (({ email, name, status, role }) => ({ email, name, status, role }))( + anonymousUserAccount, + ); + const userSession = { + strategy: 'anonymId', + userAccountId: anonymousUserAccount.id, + ...userAccountData, + registered: true, + anonymousUuid: uuid, + }; - return userSession; + return userSession; } diff --git a/app/lib/authn-basic.js b/app/lib/authn-basic.js index c79a6d41..6efb40d2 100644 --- a/app/lib/authn-basic.js +++ b/app/lib/authn-basic.js @@ -6,9 +6,9 @@ const { BasicStrategy } = require('passport-http'); const config = require('../config/config'); let strategyName; -exports.strategyName = function() { - return strategyName; -} +exports.strategyName = function () { + return strategyName; +}; /** * This strategy is intended to support service authentication using the Basic strategy. The service must @@ -21,17 +21,16 @@ exports.strategyName = function() { * This function takes the user session object and returns the value (the userSessionKey) that will be * stored in the express session for this user */ -exports.serializeUser = function(userSession, done) { - if (userSession.strategy === 'basic') { - // This indicates that the client has been authenticated using the Basic strategy. This will be used when - // deserializing. - const userSessionKey = { strategy: 'basic' }; - done(null, userSessionKey); - } - else { - // Try the next serializer - done('pass'); - } +exports.serializeUser = function (userSession, done) { + if (userSession.strategy === 'basic') { + // This indicates that the client has been authenticated using the Basic strategy. This will be used when + // deserializing. + const userSessionKey = { strategy: 'basic' }; + done(null, userSessionKey); + } else { + // Try the next serializer + done('pass'); + } }; /** @@ -45,46 +44,44 @@ exports.serializeUser = function(userSession, done) { * Note that req.user will be set to the correct value after the strategy calls verifyCallback() and the Basic token * is verified. */ -exports.deserializeUser = function(userSessionKey, done) { - if (userSessionKey.strategy === 'basic') { - done(null, null); - } - else { - // Try the next deserializer - done('pass'); - } +exports.deserializeUser = function (userSessionKey, done) { + if (userSessionKey.strategy === 'basic') { + done(null, null); + } else { + // Try the next deserializer + done('pass'); + } }; let authenticateWithBasicToken; -exports.getStrategy = function() { - const strategy = new BasicStrategy(verifyCallback); - strategyName = strategy.name; +exports.getStrategy = function () { + const strategy = new BasicStrategy(verifyCallback); + strategyName = strategy.name; - // Get a passport authenticate middleware function for this strategy - authenticateWithBasicToken = passport.authenticate(strategy.name); + // Get a passport authenticate middleware function for this strategy + authenticateWithBasicToken = passport.authenticate(strategy.name); - return strategy; -} + return strategy; +}; function verifyApikey(serviceName, apikey, done) { - // Do not attempt to verify the apikey unless basic apikey authentication is enabled - if (!config.serviceAuthn.basicApikey.enable) { - return done(null, false, { message: 'Authentication mechanism not found' }); - } - - // Verify that the service is on the list of configured services and the apikey is correct - const services = config.serviceAuthn.basicApikey.serviceAccounts; - const service = services.find(s => s.name === serviceName); - if (!service) { - return done(null, false, { message: 'Service not found' }); - } - else if (service.apikey !== apikey) { - return done(null, false, { message: 'Invalid apikey' }); - } - - const userSession = makeUserSession(null, serviceName); - - return done(null, userSession); + // Do not attempt to verify the apikey unless basic apikey authentication is enabled + if (!config.serviceAuthn.basicApikey.enable) { + return done(null, false, { message: 'Authentication mechanism not found' }); + } + + // Verify that the service is on the list of configured services and the apikey is correct + const services = config.serviceAuthn.basicApikey.serviceAccounts; + const service = services.find((s) => s.name === serviceName); + if (!service) { + return done(null, false, { message: 'Service not found' }); + } else if (service.apikey !== apikey) { + return done(null, false, { message: 'Invalid apikey' }); + } + + const userSession = makeUserSession(null, serviceName); + + return done(null, userSession); } /** @@ -92,25 +89,25 @@ function verifyApikey(serviceName, apikey, done) { * It verifies that the userid (service name) and password (apikey) is valid, then creates and returns the user session for this user. */ function verifyCallback(serviceName, apikey, done) { - if (!serviceName) { - return done(null, false, { message: 'Missing service name' }); - } - if (!apikey) { - return done(null, false, { message: 'Missing apikey' }); - } - - return verifyApikey(serviceName, apikey, done); + if (!serviceName) { + return done(null, false, { message: 'Missing service name' }); + } + if (!apikey) { + return done(null, false, { message: 'Missing apikey' }); + } + + return verifyApikey(serviceName, apikey, done); } function makeUserSession(clientId, serviceName) { - const userSession = { - strategy: 'basic', - clientId, - serviceName, - service: true - }; - - return userSession; + const userSession = { + strategy: 'basic', + clientId, + serviceName, + service: true, + }; + + return userSession; } /** @@ -118,12 +115,10 @@ function makeUserSession(clientId, serviceName) { * calls the authenticate() function for the Basic strategy (which cause the apikey to be validated). * */ -exports.authenticate = function(req, res, next) { - if (authenticateWithBasicToken) { - authenticateWithBasicToken(req, res, next); - } - else { - throw new Error('Basic strategy not configured'); - } -} - +exports.authenticate = function (req, res, next) { + if (authenticateWithBasicToken) { + authenticateWithBasicToken(req, res, next); + } else { + throw new Error('Basic strategy not configured'); + } +}; diff --git a/app/lib/authn-bearer.js b/app/lib/authn-bearer.js index d5d5b473..d16b5318 100644 --- a/app/lib/authn-bearer.js +++ b/app/lib/authn-bearer.js @@ -11,25 +11,24 @@ const config = require('../config/config'); let jwksClient; let strategyName; -exports.strategyName = function() { - return strategyName; -} +exports.strategyName = function () { + return strategyName; +}; /** * This function takes the user session object and returns the value (the userSessionKey) that will be * stored in the express session for this user */ -exports.serializeUser = function(userSession, done) { - if (userSession.strategy === 'bearer') { - // This indicates that the client has been authenticated using the Bearer strategy. This will be used when - // deserializing. - const userSessionKey = { strategy: 'bearer' }; - done(null, userSessionKey); - } - else { - // Try the next serializer - done('pass'); - } +exports.serializeUser = function (userSession, done) { + if (userSession.strategy === 'bearer') { + // This indicates that the client has been authenticated using the Bearer strategy. This will be used when + // deserializing. + const userSessionKey = { strategy: 'bearer' }; + done(null, userSessionKey); + } else { + // Try the next serializer + done('pass'); + } }; /** @@ -43,100 +42,101 @@ exports.serializeUser = function(userSession, done) { * Note that req.user will be set to the correct value after the strategy calls verifyCallback() and the Bearer token * is verified. */ -exports.deserializeUser = function(userSessionKey, done) { - if (userSessionKey.strategy === 'bearer') { - done(null, null); - } - else { - // Try the next deserializer - done('pass'); - } +exports.deserializeUser = function (userSessionKey, done) { + if (userSessionKey.strategy === 'bearer') { + done(null, null); + } else { + // Try the next deserializer + done('pass'); + } }; let authenticateWithBearerToken; -exports.getStrategy = function() { - // Create the JWKS client - jwksClient = jwks({ - jwksUri: config.serviceAuthn.oidcClientCredentials.jwksUri - }); +exports.getStrategy = function () { + // Create the JWKS client + jwksClient = jwks({ + jwksUri: config.serviceAuthn.oidcClientCredentials.jwksUri, + }); - const strategy = new BearerStrategy(verifyCallback); - strategyName = strategy.name; + const strategy = new BearerStrategy(verifyCallback); + strategyName = strategy.name; - // Get a passport authenticate middleware function for this strategy - authenticateWithBearerToken = passport.authenticate(strategy.name); + // Get a passport authenticate middleware function for this strategy + authenticateWithBearerToken = passport.authenticate(strategy.name); - return strategy; -} + return strategy; +}; function verifyApikeyToken(token, done) { - // Do not attempt to verify the token unless apikey authentication is enabled - if (!config.serviceAuthn.challengeApikey.enable) { - return done(null, false, { message: 'Authentication mechanism not found' }); - } - - let payload; - try { - // Verify that the token is valid and extract the payload - payload = jwt.verify(token, config.serviceAuthn.challengeApikey.secret, { algorithms: ['HS256'] }); - } catch (err) { - if (err.name === 'TokenExpiredError') { - return done(null, false, { message: 'Token expired' }); - } else if (err.name === 'JsonWebTokenError' && err.message === 'invalid signature') { - return done(null, false, { message: 'Invalid signature' }); - } else { - return done(err); - } + // Do not attempt to verify the token unless apikey authentication is enabled + if (!config.serviceAuthn.challengeApikey.enable) { + return done(null, false, { message: 'Authentication mechanism not found' }); + } + + let payload; + try { + // Verify that the token is valid and extract the payload + payload = jwt.verify(token, config.serviceAuthn.challengeApikey.secret, { + algorithms: ['HS256'], + }); + } catch (err) { + if (err.name === 'TokenExpiredError') { + return done(null, false, { message: 'Token expired' }); + } else if (err.name === 'JsonWebTokenError' && err.message === 'invalid signature') { + return done(null, false, { message: 'Invalid signature' }); + } else { + return done(err); } + } - const userSession = makeUserSession(null, payload.serviceName); + const userSession = makeUserSession(null, payload.serviceName); - return done(null, userSession); + return done(null, userSession); } function verifyClientCredentialsToken(token, decodedHeader, done) { - // Do not attempt to verify the token unless client credentials authentication is enabled - if (!config.serviceAuthn.oidcClientCredentials.enable) { - return done(null, false, { message: 'Authentication mechanism not found' }); - } - - jwksClient.getSigningKey(decodedHeader.kid) - .then(function (signingKey) { - let payload; - let clientId; - try { - payload = jwt.verify(token, signingKey.getPublicKey(), { algorithms: ['RS256'] }); - - // Make sure the client is allowed to access the REST API - // Okta returns the client id in payload.cid - // Keycloak returns the client id in payload.clientId - // Newer versions of keycloak appear to return the client id in payload.client_id - clientId = payload.cid || payload.clientId || payload.client_id; - const clients = config.serviceAuthn.oidcClientCredentials.clients; - const client = clients.find(c => c.clientId === clientId); - if (!client) { - return done(null, false, { message: 'Client not found' }); - } - } catch (err) { - if (err.name === 'TokenExpiredError') { - return done(null, false, { message: 'Token expired' }); - } else { - return done(err); - } - } - - const userSession = makeUserSession(clientId); - - return done(null, userSession); - }) - .catch(err => { - if (err.name === 'SigningKeyNotFoundError') { - return done(null, false, { message: 'Signing key not found'}); - } - else { - return done(err); - } - }); + // Do not attempt to verify the token unless client credentials authentication is enabled + if (!config.serviceAuthn.oidcClientCredentials.enable) { + return done(null, false, { message: 'Authentication mechanism not found' }); + } + + jwksClient + .getSigningKey(decodedHeader.kid) + .then(function (signingKey) { + let payload; + let clientId; + try { + payload = jwt.verify(token, signingKey.getPublicKey(), { algorithms: ['RS256'] }); + + // Make sure the client is allowed to access the REST API + // Okta returns the client id in payload.cid + // Keycloak returns the client id in payload.clientId + // Newer versions of keycloak appear to return the client id in payload.client_id + clientId = payload.cid || payload.clientId || payload.client_id; + const clients = config.serviceAuthn.oidcClientCredentials.clients; + const client = clients.find((c) => c.clientId === clientId); + if (!client) { + return done(null, false, { message: 'Client not found' }); + } + } catch (err) { + if (err.name === 'TokenExpiredError') { + return done(null, false, { message: 'Token expired' }); + } else { + return done(err); + } + } + + const userSession = makeUserSession(clientId); + + return done(null, userSession); + }) + .catch((err) => { + if (err.name === 'SigningKeyNotFoundError') { + return done(null, false, { message: 'Signing key not found' }); + } else { + return done(err); + } + }); } /** @@ -146,38 +146,35 @@ function verifyClientCredentialsToken(token, decodedHeader, done) { * the OIDC client credentials flow, and if the alg property is 'HS256' that the token was generated from an apikey. */ function verifyCallback(token, done) { - if (!token) { - return done(null, false, { message: 'Missing token' }); - } - - let decodedHeader; - try { - decodedHeader = jwtDecoder(token, { header: true }); - } - catch(err) { - return done(null, false, err); - } - - if (decodedHeader.alg === 'RS256') { - return verifyClientCredentialsToken(token, decodedHeader, done); - } - else if (decodedHeader.alg === 'HS256') { - return verifyApikeyToken(token, done); - } - else { - return done(null, false, { message: 'Unknown token' }); - } + if (!token) { + return done(null, false, { message: 'Missing token' }); + } + + let decodedHeader; + try { + decodedHeader = jwtDecoder(token, { header: true }); + } catch (err) { + return done(null, false, err); + } + + if (decodedHeader.alg === 'RS256') { + return verifyClientCredentialsToken(token, decodedHeader, done); + } else if (decodedHeader.alg === 'HS256') { + return verifyApikeyToken(token, done); + } else { + return done(null, false, { message: 'Unknown token' }); + } } function makeUserSession(clientId, serviceName) { - const userSession = { - strategy: 'bearer', - clientId, - serviceName, - service: true - }; - - return userSession; + const userSession = { + strategy: 'bearer', + clientId, + serviceName, + service: true, + }; + + return userSession; } /** @@ -185,12 +182,10 @@ function makeUserSession(clientId, serviceName) { * calls the authenticate() function for the Bearer strategy (which cause the token to be validated). * */ -exports.authenticate = function(req, res, next) { - if (authenticateWithBearerToken) { - authenticateWithBearerToken(req, res, next); - } - else { - throw new Error('Bearer strategy not configured'); - } -} - +exports.authenticate = function (req, res, next) { + if (authenticateWithBearerToken) { + authenticateWithBearerToken(req, res, next); + } else { + throw new Error('Bearer strategy not configured'); + } +}; diff --git a/app/lib/authn-configuration.js b/app/lib/authn-configuration.js index b5f5a79a..ffe7b275 100644 --- a/app/lib/authn-configuration.js +++ b/app/lib/authn-configuration.js @@ -10,69 +10,78 @@ const oidcConfig = require('./authn-oidc'); const bearerConfig = require('./authn-bearer'); const basicConfig = require('./authn-basic'); -const availableMechanisms = new Map([['oidc', oidcConfig], ['anonymous', anonymousConfig], ['bearer', bearerConfig], ['basic', basicConfig]]); +const availableMechanisms = new Map([ + ['oidc', oidcConfig], + ['anonymous', anonymousConfig], + ['bearer', bearerConfig], + ['basic', basicConfig], +]); -exports.passportMiddleware = function() { - const router = express.Router(); +exports.passportMiddleware = function () { + const router = express.Router(); - // Configure passport middleware - router.use(passport.initialize()); - router.use(passport.session()); + // Configure passport middleware + router.use(passport.initialize()); + router.use(passport.session()); - return router; -} + return router; +}; -exports.configurePassport = async function(mechanismName) { - // Configure passport with the selected authentication mechanism - const mechanism = availableMechanisms.get(mechanismName); - if (mechanism) { - try { - passport.serializeUser(mechanism.serializeUser); - passport.deserializeUser(mechanism.deserializeUser); +exports.configurePassport = async function (mechanismName) { + // Configure passport with the selected authentication mechanism + const mechanism = availableMechanisms.get(mechanismName); + if (mechanism) { + try { + passport.serializeUser(mechanism.serializeUser); + passport.deserializeUser(mechanism.deserializeUser); - const strategy = await mechanism.getStrategy(); - passport.use(strategy); + const strategy = await mechanism.getStrategy(); + passport.use(strategy); - logger.info(`Configured authentication mechanism: ${ mechanismName }`); - } - catch(err) { - logger.error(`Unable to configure system with authentication mechanism ${ mechanismName }`, err); - } + logger.info(`Configured authentication mechanism: ${mechanismName}`); + } catch (err) { + logger.error( + `Unable to configure system with authentication mechanism ${mechanismName}`, + err, + ); } - else { - logger.error(`Unable to configure system with unknown authentication mechanism: ${ mechanismName }`); - throw new Error(`Unable to configure system with unknown authentication mechanism: ${ mechanismName }`); - } -} + } else { + logger.error( + `Unable to configure system with unknown authentication mechanism: ${mechanismName}`, + ); + throw new Error( + `Unable to configure system with unknown authentication mechanism: ${mechanismName}`, + ); + } +}; // Middleware that will return a 404 if the routeMechanism doesn't match the configured authentication mechanism // This can be used to prevent access to routes that don't match the current configuration -exports.isUserAuthenticationMechanismEnabled = function(routeMechanism) { - return function(req, res, next) { - if (config.userAuthn.mechanism === routeMechanism) { - return next(); - } - else { - return res.status(404).send('Authentication mechanism not found'); - } +exports.isUserAuthenticationMechanismEnabled = function (routeMechanism) { + return function (req, res, next) { + if (config.userAuthn.mechanism === routeMechanism) { + return next(); + } else { + return res.status(404).send('Authentication mechanism not found'); } -} + }; +}; // Middleware that will return a 404 if the routeMechanism doesn't match the configured authentication mechanism // This can be used to prevent access to routes that don't match the current configuration -exports.isServiceAuthenticationMechanismEnabled = function(routeMechanism) { - return function(req, res, next) { - if (routeMechanism === 'challenge-apikey' && config.serviceAuthn.challengeApikey.enable) { - return next(); - } - else if (routeMechanism === 'basic-apikey' && config.serviceAuthn.basicApikey.enable) { - return next(); - } - else if (routeMechanism === 'client-credentials' && config.serviceAuthn.oidcClientCredentials.enable) { - return next(); - } - else { - return res.status(404).send('Authentication mechanism not found'); - } +exports.isServiceAuthenticationMechanismEnabled = function (routeMechanism) { + return function (req, res, next) { + if (routeMechanism === 'challenge-apikey' && config.serviceAuthn.challengeApikey.enable) { + return next(); + } else if (routeMechanism === 'basic-apikey' && config.serviceAuthn.basicApikey.enable) { + return next(); + } else if ( + routeMechanism === 'client-credentials' && + config.serviceAuthn.oidcClientCredentials.enable + ) { + return next(); + } else { + return res.status(404).send('Authentication mechanism not found'); } -} + }; +}; diff --git a/app/lib/authn-middleware.js b/app/lib/authn-middleware.js index d10aa188..ea1dbe7a 100644 --- a/app/lib/authn-middleware.js +++ b/app/lib/authn-middleware.js @@ -17,33 +17,34 @@ const authnBasic = require('../lib/authn-basic'); */ const bearerScheme = 'bearer'; const basicScheme = 'basic'; -exports.authenticate = function(req, res, next) { - const authzHeader = req.get('Authorization'); - const authzScheme = getScheme(authzHeader); - if ((config.serviceAuthn.oidcClientCredentials.enable || config.serviceAuthn.challengeApikey.enable) && (authzHeader && authzScheme === bearerScheme)) { - // Authorization header found - // Authenticate the service using the Bearer token - authnBearer.authenticate(req, res, next); - } - else if (config.serviceAuthn.basicApikey.enable && (authzHeader && authzScheme === basicScheme)) { - // Authorization header found - // Authenticate the service using Basic Authentication with apikey - authnBasic.authenticate(req, res, next); - } - else if (req.isAuthenticated()) { - // User has been authenticated using a non-Bearer strategy - next(); - } - else { - return res.status(401).send('Not authorized'); - } -} +exports.authenticate = function (req, res, next) { + const authzHeader = req.get('Authorization'); + const authzScheme = getScheme(authzHeader); + if ( + (config.serviceAuthn.oidcClientCredentials.enable || + config.serviceAuthn.challengeApikey.enable) && + authzHeader && + authzScheme === bearerScheme + ) { + // Authorization header found + // Authenticate the service using the Bearer token + authnBearer.authenticate(req, res, next); + } else if (config.serviceAuthn.basicApikey.enable && authzHeader && authzScheme === basicScheme) { + // Authorization header found + // Authenticate the service using Basic Authentication with apikey + authnBasic.authenticate(req, res, next); + } else if (req.isAuthenticated()) { + // User has been authenticated using a non-Bearer strategy + next(); + } else { + return res.status(401).send('Not authorized'); + } +}; function getScheme(authorizationHeader) { - if (authorizationHeader) { - return authorizationHeader.split(' ')[0].toLowerCase(); - } - else { - return null; - } + if (authorizationHeader) { + return authorizationHeader.split(' ')[0].toLowerCase(); + } else { + return null; + } } diff --git a/app/lib/authn-oidc.js b/app/lib/authn-oidc.js index 8fe39a81..8d075f6d 100644 --- a/app/lib/authn-oidc.js +++ b/app/lib/authn-oidc.js @@ -4,71 +4,73 @@ const openIdClient = require('openid-client'); const retry = require('async-await-retry'); const config = require('../config/config'); -const userAccountsService = require("../services/user-accounts-service"); +const userAccountsService = require('../services/user-accounts-service'); let strategyName; -exports.strategyName = function() { - return strategyName; -} +exports.strategyName = function () { + return strategyName; +}; /** * This function takes the user session object and returns the value (the userSessionKey) that will be * stored in the express session for this user */ -exports.serializeUser = function(userSession, done) { - if (userSession.strategy === 'oidc') { - const userSessionKey = { - strategy: 'oidc', - sessionId: userSession.email, - username: userSession.name, - displayName: userSession.displayName - } - done(null, userSessionKey); - } - else { - // Try the next serializer - done('pass'); - } +exports.serializeUser = function (userSession, done) { + if (userSession.strategy === 'oidc') { + const userSessionKey = { + strategy: 'oidc', + sessionId: userSession.email, + username: userSession.name, + displayName: userSession.displayName, + }; + done(null, userSessionKey); + } else { + // Try the next serializer + done('pass'); + } }; /** * This function takes the userSessionKey (the value stored in the express session for this user) and * returns the user session object */ -exports.deserializeUser = function(userSessionKey, done) { - if (userSessionKey.strategy === 'oidc') { - makeUserSession(userSessionKey.sessionId, userSessionKey.username, userSessionKey.displayName) - .then(userSession => done(null, userSession)) - .catch(err => done(err)); - } - else { - // Try the next deserializer - done('pass'); - } +exports.deserializeUser = function (userSessionKey, done) { + if (userSessionKey.strategy === 'oidc') { + makeUserSession(userSessionKey.sessionId, userSessionKey.username, userSessionKey.displayName) + .then((userSession) => done(null, userSession)) + .catch((err) => done(err)); + } else { + // Try the next deserializer + done('pass'); + } }; -exports.getStrategy = async function() { - // Retry to give the identity provider time to start (when using docker-compose) - const retryOptions = { interval: 1000 }; - const issuer = await retry(openIdClient.Issuer.discover, [config.userAuthn.oidc.issuerUrl], retryOptions); - - const clientOptions = { - client_id: config.userAuthn.oidc.clientId, - client_secret: config.userAuthn.oidc.clientSecret, - redirect_uris: [`${ config.userAuthn.oidc.redirectOrigin }/api/authn/oidc/callback`], - response_types: ['code'] - }; - const client = new issuer.Client(clientOptions); - - // oidc strategy for passport - const strategyOptions = { - client, - params: { scope: 'openid email profile' } - }; - const strategy = new openIdClient.Strategy(strategyOptions, verifyCallback); - strategyName = strategy.name; - - return strategy; +exports.getStrategy = async function () { + // Retry to give the identity provider time to start (when using docker-compose) + const retryOptions = { interval: 1000 }; + const issuer = await retry( + openIdClient.Issuer.discover, + [config.userAuthn.oidc.issuerUrl], + retryOptions, + ); + + const clientOptions = { + client_id: config.userAuthn.oidc.clientId, + client_secret: config.userAuthn.oidc.clientSecret, + redirect_uris: [`${config.userAuthn.oidc.redirectOrigin}/api/authn/oidc/callback`], + response_types: ['code'], + }; + const client = new issuer.Client(clientOptions); + + // oidc strategy for passport + const strategyOptions = { + client, + params: { scope: 'openid email profile' }, + }; + const strategy = new openIdClient.Strategy(strategyOptions, verifyCallback); + strategyName = strategy.name; + + return strategy; }; /** @@ -76,53 +78,57 @@ exports.getStrategy = async function() { * It creates and returns the user session for this user */ function verifyCallback(tokenSet, userInfo, done) { - const claims = tokenSet.claims(); + const claims = tokenSet.claims(); - makeUserSession(claims.email, claims.preferred_username, claims.name) - .then(userSession => done(null, userSession)) - .catch(err => done(err)); + makeUserSession(claims.email, claims.preferred_username, claims.name) + .then((userSession) => done(null, userSession)) + .catch((err) => done(err)); } async function makeUserSession(email, username, displayName) { - // Create the user session from the user account in the database - let userSession = await makeRegisteredUserSession(email); - if (!userSession) { - // Not in the database, unregistered user - userSession = makeUnregisteredUserSession(email, username, displayName); - } - - return userSession; + // Create the user session from the user account in the database + let userSession = await makeRegisteredUserSession(email); + if (!userSession) { + // Not in the database, unregistered user + userSession = makeUnregisteredUserSession(email, username, displayName); + } + + return userSession; } async function makeRegisteredUserSession(email) { - const userAccount = await userAccountsService.retrieveByEmail(email); - - if (userAccount) { - const userAccountData = (({ email, status, role, displayName }) => ({ email, status, role, displayName }))(userAccount); - const userSession = { - strategy: 'oidc', - userAccountId: userAccount.id, - ...userAccountData, - name: userAccount.username, - registered: true - }; - - return userSession; - } - else { - return null; - } -} - -function makeUnregisteredUserSession(email, username, displayName) { + const userAccount = await userAccountsService.retrieveByEmail(email); + + if (userAccount) { + const userAccountData = (({ email, status, role, displayName }) => ({ + email, + status, + role, + displayName, + }))(userAccount); const userSession = { - strategy: 'oidc', - email: email, - role: 'none', - name: username, - displayName: displayName, - registered: false + strategy: 'oidc', + userAccountId: userAccount.id, + ...userAccountData, + name: userAccount.username, + registered: true, }; return userSession; + } else { + return null; + } +} + +function makeUnregisteredUserSession(email, username, displayName) { + const userSession = { + strategy: 'oidc', + email: email, + role: 'none', + name: username, + displayName: displayName, + registered: false, + }; + + return userSession; } diff --git a/app/lib/authz-middleware.js b/app/lib/authz-middleware.js index 2a4374f3..67b2bcbe 100644 --- a/app/lib/authz-middleware.js +++ b/app/lib/authz-middleware.js @@ -4,73 +4,80 @@ const config = require('../config/config'); const logger = require('../lib/logger'); const userRoles = { - admin: 'admin', - editor: 'editor', - visitor: 'visitor' -} + admin: 'admin', + editor: 'editor', + visitor: 'visitor', +}; exports.userRoles = userRoles; -exports.admin = [ userRoles.admin ]; -exports.editorOrHigher = [ userRoles.admin, userRoles.editor ]; -exports.visitorOrHigher = [ userRoles.admin, userRoles.editor, userRoles.visitor ]; +exports.admin = [userRoles.admin]; +exports.editorOrHigher = [userRoles.admin, userRoles.editor]; +exports.visitorOrHigher = [userRoles.admin, userRoles.editor, userRoles.visitor]; const serviceRoles = { - readOnly: 'read-only', - collectionManager: 'collection-manager', - stixExport: 'stix-export' + readOnly: 'read-only', + collectionManager: 'collection-manager', + stixExport: 'stix-export', }; exports.serviceRoles = serviceRoles; -exports.readOnlyService = [ serviceRoles.readOnly ]; +exports.readOnlyService = [serviceRoles.readOnly]; /** * This middleware function verifies that a request is authorized. */ -exports.requireRole = function(allowedUserRoles, allowedServiceRoles) { - return function(req, res, next) { - if (!req.user) { - return res.status(401).send('Not authorized'); - } - - if (req.user.service) { - if (req.user.serviceName) { - // apikey service - let serviceConfig; - if (req.user.strategy === 'bearer') { - serviceConfig = config.serviceAuthn.challengeApikey.serviceAccounts.find(s => s.name === req.user.serviceName); - } - else if (req.user.strategy === 'basic') { - serviceConfig = config.serviceAuthn.basicApikey.serviceAccounts.find(s => s.name === req.user.serviceName); - } +exports.requireRole = function (allowedUserRoles, allowedServiceRoles) { + return function (req, res, next) { + if (!req.user) { + return res.status(401).send('Not authorized'); + } - if (serviceConfig && allowedServiceRoles && allowedServiceRoles.includes(serviceConfig.serviceRole)) { - return next(); - } - else { - logger.verbose(`Service not authorized. Service name is ${ req.user.serviceName }`); - return res.status(401).send('Not authorized'); - } - } - else if (req.user.clientId) { - // client credentials service - const serviceConfig = config.serviceAuthn.oidcClientCredentials.clients.find(c => c.clientId === req.user.clientId); - if (serviceConfig && allowedServiceRoles && allowedServiceRoles.includes(serviceConfig.serviceRole)) { - return next(); - } - else { - logger.verbose(`Service not authorized. Client Id is ${ req.user.clientId }`); - return res.status(401).send('Not authorized'); - } - } - else { - logger.verbose('Service not authorized. Missing serviceName and clientId'); - return res.status(401).send('Not authorized'); - } + if (req.user.service) { + if (req.user.serviceName) { + // apikey service + let serviceConfig; + if (req.user.strategy === 'bearer') { + serviceConfig = config.serviceAuthn.challengeApikey.serviceAccounts.find( + (s) => s.name === req.user.serviceName, + ); + } else if (req.user.strategy === 'basic') { + serviceConfig = config.serviceAuthn.basicApikey.serviceAccounts.find( + (s) => s.name === req.user.serviceName, + ); } - else if (allowedUserRoles && allowedUserRoles.includes(req.user.role)) { - return next(); + + if ( + serviceConfig && + allowedServiceRoles && + allowedServiceRoles.includes(serviceConfig.serviceRole) + ) { + return next(); + } else { + logger.verbose(`Service not authorized. Service name is ${req.user.serviceName}`); + return res.status(401).send('Not authorized'); } - else { - logger.verbose(`User not authorized. User role is ${ req.user.role }`); - return res.status(401).send('Not authorized'); + } else if (req.user.clientId) { + // client credentials service + const serviceConfig = config.serviceAuthn.oidcClientCredentials.clients.find( + (c) => c.clientId === req.user.clientId, + ); + if ( + serviceConfig && + allowedServiceRoles && + allowedServiceRoles.includes(serviceConfig.serviceRole) + ) { + return next(); + } else { + logger.verbose(`Service not authorized. Client Id is ${req.user.clientId}`); + return res.status(401).send('Not authorized'); } + } else { + logger.verbose('Service not authorized. Missing serviceName and clientId'); + return res.status(401).send('Not authorized'); + } + } else if (allowedUserRoles && allowedUserRoles.includes(req.user.role)) { + return next(); + } else { + logger.verbose(`User not authorized. User role is ${req.user.role}`); + return res.status(401).send('Not authorized'); } -} + }; +}; diff --git a/app/lib/database-configuration.js b/app/lib/database-configuration.js index f38c6746..e15ba021 100644 --- a/app/lib/database-configuration.js +++ b/app/lib/database-configuration.js @@ -11,246 +11,259 @@ const logger = require('../lib/logger'); const AttackObject = require('../models/attack-object-model'); const CollectionIndex = require('../models/collection-index-model'); const MarkingDefinition = require('../models/marking-definition-model'); -const { OrganizationIdentityNotFoundError, OrganizationIdentityNotSetError, AnonymousUserAccountNotFoundError, AnonymousUserAccountNotSetError } = require('../exceptions'); +const { + OrganizationIdentityNotFoundError, + OrganizationIdentityNotSetError, + AnonymousUserAccountNotFoundError, + AnonymousUserAccountNotSetError, +} = require('../exceptions'); async function createPlaceholderOrganizationIdentity() { - // Create placeholder identity object - const timestamp = new Date().toISOString(); - const placeholderIdentity = { - workspace: { - workflow: { - state: 'awaiting-review' - } - }, - stix: { - created: timestamp, - modified: timestamp, - name: 'Placeholder Organization Identity', - identity_class: 'organization', - spec_version: '2.1', - type: 'identity', - description: 'This is a placeholder organization identity. Please edit it or replace it with another identity.' - } - }; + // Create placeholder identity object + const timestamp = new Date().toISOString(); + const placeholderIdentity = { + workspace: { + workflow: { + state: 'awaiting-review', + }, + }, + stix: { + created: timestamp, + modified: timestamp, + name: 'Placeholder Organization Identity', + identity_class: 'organization', + spec_version: '2.1', + type: 'identity', + description: + 'This is a placeholder organization identity. Please edit it or replace it with another identity.', + }, + }; - try { - const newIdentity = await identitiesService.create(placeholderIdentity, { import: false }); + try { + const newIdentity = await identitiesService.create(placeholderIdentity, { import: false }); - // Set the organization identity to the placeholder identity - await systemConfigurationService.setOrganizationIdentity(newIdentity.stix.id); - logger.info(`Organization identity set to placeholder identity with id: ${ newIdentity.stix.id }`); - } - catch(err) { - logger.error('Unable to create or set placeholder organization identity: ' + err); - throw err; - } + // Set the organization identity to the placeholder identity + await systemConfigurationService.setOrganizationIdentity(newIdentity.stix.id); + logger.info( + `Organization identity set to placeholder identity with id: ${newIdentity.stix.id}`, + ); + } catch (err) { + logger.error('Unable to create or set placeholder organization identity: ' + err); + throw err; + } } async function createAnonymousUserAccount() { - // Create the anonymous user account - const anonymousUserAccount = { - email: null, - username: 'anonymous', - displayName: 'Anonymous User', - status: 'active', - role: 'admin' - }; + // Create the anonymous user account + const anonymousUserAccount = { + email: null, + username: 'anonymous', + displayName: 'Anonymous User', + status: 'active', + role: 'admin', + }; - try { - const newUserAccount = await userAccountsService.create(anonymousUserAccount); + try { + const newUserAccount = await userAccountsService.create(anonymousUserAccount); - // Set the anonymous user account id to the new user account id - await systemConfigurationService.setAnonymousUserAccountId(newUserAccount.id); - logger.info(`Anonymous user account set to user account with id: ${ newUserAccount.id }`); - } - catch(err) { - logger.error('Unable to create or set anonymous user account: ' + err); - throw err; - } + // Set the anonymous user account id to the new user account id + await systemConfigurationService.setAnonymousUserAccountId(newUserAccount.id); + logger.info(`Anonymous user account set to user account with id: ${newUserAccount.id}`); + } catch (err) { + logger.error('Unable to create or set anonymous user account: ' + err); + throw err; + } } async function checkForOrganizationIdentity() { - try { - const identity = await systemConfigurationService.retrieveOrganizationIdentity(); - logger.info(`Success: Organization identity is set to ${ identity.stix.name }`); - } - catch(err) { - if (err instanceof OrganizationIdentityNotFoundError) { - logger.warn(`Organization identity with id ${ err } not found, setting to placeholder identity`); - await createPlaceholderOrganizationIdentity(); - } - else if (err instanceof OrganizationIdentityNotSetError) { - logger.warn(`Organization identity not set, setting to placeholder identity`); - await createPlaceholderOrganizationIdentity(); - } - else { - logger.error("Unable to retrieve organization identity, failed with error: " + err); - logger.warn(`Attempting to set organization identity to placeholder identity`); - await createPlaceholderOrganizationIdentity(); - } + try { + const identity = await systemConfigurationService.retrieveOrganizationIdentity(); + logger.info(`Success: Organization identity is set to ${identity.stix.name}`); + } catch (err) { + if (err instanceof OrganizationIdentityNotFoundError) { + logger.warn( + `Organization identity with id ${err} not found, setting to placeholder identity`, + ); + await createPlaceholderOrganizationIdentity(); + } else if (err instanceof OrganizationIdentityNotSetError) { + logger.warn(`Organization identity not set, setting to placeholder identity`); + await createPlaceholderOrganizationIdentity(); + } else { + logger.error('Unable to retrieve organization identity, failed with error: ' + err); + logger.warn(`Attempting to set organization identity to placeholder identity`); + await createPlaceholderOrganizationIdentity(); } + } } async function checkForAnonymousUserAccount() { - const config = require('../config/config'); + const config = require('../config/config'); - // Only check for an anonymous user account if the system has been configured to use the anonymous authn mechanism - if (config.userAuthn.mechanism === 'anonymous') { - try { - const anonymousUserAccount = await systemConfigurationService.retrieveAnonymousUserAccount(); - logger.info(`Success: Anonymous user account is set to ${ anonymousUserAccount.id }`); - } - catch (err) { - if (err instanceof AnonymousUserAccountNotFoundError) { - logger.warn(`Anonymous user account with id ${ err.anonymousUserAccountId } not found, creating new anonymous user account`); - await createAnonymousUserAccount(); - } - else if (err instanceof AnonymousUserAccountNotSetError) { - logger.warn(`Anonymous user account not set, creating new anonymous user account`); - await createAnonymousUserAccount(); - } - else { - logger.error("Unable to retrieve anonymous user account, failed with error: " + err); - logger.warn(`Attempting to create new anonymous user account`); - await createAnonymousUserAccount(); - } - } + // Only check for an anonymous user account if the system has been configured to use the anonymous authn mechanism + if (config.userAuthn.mechanism === 'anonymous') { + try { + const anonymousUserAccount = await systemConfigurationService.retrieveAnonymousUserAccount(); + logger.info(`Success: Anonymous user account is set to ${anonymousUserAccount.id}`); + } catch (err) { + if (err instanceof AnonymousUserAccountNotFoundError) { + logger.warn( + `Anonymous user account with id ${err.anonymousUserAccountId} not found, creating new anonymous user account`, + ); + await createAnonymousUserAccount(); + } else if (err instanceof AnonymousUserAccountNotSetError) { + logger.warn(`Anonymous user account not set, creating new anonymous user account`); + await createAnonymousUserAccount(); + } else { + logger.error('Unable to retrieve anonymous user account, failed with error: ' + err); + logger.warn(`Attempting to create new anonymous user account`); + await createAnonymousUserAccount(); + } } + } } async function checkForInvalidEnterpriseCollectionId() { - // The v1.0 release of ATT&CK Workbench used x-mitre-collection--23320f4-22ad-8467-3b73-ed0c869a12838 for the id of - // the Enterprise collection object. This value isn't a legal STIX id (the UUID portion is incorrect). - // The v1.1 release of ATT&CK Workbench replaced this value with another invalid id. - // This function checks for the presence of either invalid id and replaces it with a valid id wherever found. - const invalidIds = [ - 'x-mitre-collection--23320f4-22ad-8467-3b73-ed0c869a12838', - 'x-mitre-collection--402e24b4-436e-4936-b19b-2038648f489' - ]; - const validId = 'x-mitre-collection-1f5f1533-f617-4ca8-9ab4-6a02367fa019'; + // The v1.0 release of ATT&CK Workbench used x-mitre-collection--23320f4-22ad-8467-3b73-ed0c869a12838 for the id of + // the Enterprise collection object. This value isn't a legal STIX id (the UUID portion is incorrect). + // The v1.1 release of ATT&CK Workbench replaced this value with another invalid id. + // This function checks for the presence of either invalid id and replaces it with a valid id wherever found. + const invalidIds = [ + 'x-mitre-collection--23320f4-22ad-8467-3b73-ed0c869a12838', + 'x-mitre-collection--402e24b4-436e-4936-b19b-2038648f489', + ]; + const validId = 'x-mitre-collection-1f5f1533-f617-4ca8-9ab4-6a02367fa019'; - logger.info(`Starting check for invalid enterprise collection id. This may take a few minutes the first time it runs...`); - - let collectionUpdates = 0; - let objectUpdates = 0; - const attackObjects = await AttackObject.find(); - for (const attackObject of attackObjects) { - // Check for x-mitre-collection objects - if (attackObject.stix.type === 'x-mitre-collection') { - if (invalidIds.includes(attackObject.stix.id)) { - attackObject.stix.id = validId; - // eslint-disable-next-line no-await-in-loop - await attackObject.save(); - collectionUpdates += 1; - } - } - // Check for an object that references an invalid x-mitre-collection id - else { - let attackObjectUpdated = false; - for (const collectionRef of attackObject.workspace.collections) { - if (invalidIds.includes(collectionRef.collection_ref)) { - collectionRef.collection_ref = validId; - attackObjectUpdated = true; - } - } - if (attackObjectUpdated) { - // eslint-disable-next-line no-await-in-loop - await attackObject.save(); - objectUpdates += 1; - } - } + logger.info( + `Starting check for invalid enterprise collection id. This may take a few minutes the first time it runs...`, + ); + let collectionUpdates = 0; + let objectUpdates = 0; + const attackObjects = await AttackObject.find(); + for (const attackObject of attackObjects) { + // Check for x-mitre-collection objects + if (attackObject.stix.type === 'x-mitre-collection') { + if (invalidIds.includes(attackObject.stix.id)) { + attackObject.stix.id = validId; + // eslint-disable-next-line no-await-in-loop + await attackObject.save(); + collectionUpdates += 1; + } } - - let collectionIndexUpdates = 0; - const collectionIndexes = await CollectionIndex.find(); - for (const collectionIndex of collectionIndexes) { - // Check the list of collections - let collectionIndexUpdated = false; - for (const collection of collectionIndex.collection_index.collections) { - if (invalidIds.includes(collection.id)) { - collection.id = validId; - collectionIndexUpdated = true; - } + // Check for an object that references an invalid x-mitre-collection id + else { + let attackObjectUpdated = false; + for (const collectionRef of attackObject.workspace.collections) { + if (invalidIds.includes(collectionRef.collection_ref)) { + collectionRef.collection_ref = validId; + attackObjectUpdated = true; } + } + if (attackObjectUpdated) { + // eslint-disable-next-line no-await-in-loop + await attackObject.save(); + objectUpdates += 1; + } + } + } - // Check the list of subscriptions - if (collectionIndex.workspace?.update_policy?.subscriptions) { - for (let i = 0; i < collectionIndex.workspace.update_policy.subscriptions.length; i++) { - if (invalidIds.includes(collectionIndex.workspace.update_policy.subscriptions[i])) { - collectionIndex.workspace.update_policy.subscriptions[i] = validId; - collectionIndexUpdated = true; - } - } - } + let collectionIndexUpdates = 0; + const collectionIndexes = await CollectionIndex.find(); + for (const collectionIndex of collectionIndexes) { + // Check the list of collections + let collectionIndexUpdated = false; + for (const collection of collectionIndex.collection_index.collections) { + if (invalidIds.includes(collection.id)) { + collection.id = validId; + collectionIndexUpdated = true; + } + } - if (collectionIndexUpdated) { - // eslint-disable-next-line no-await-in-loop - await collectionIndex.save(); - collectionIndexUpdates += 1; + // Check the list of subscriptions + if (collectionIndex.workspace?.update_policy?.subscriptions) { + for (let i = 0; i < collectionIndex.workspace.update_policy.subscriptions.length; i++) { + if (invalidIds.includes(collectionIndex.workspace.update_policy.subscriptions[i])) { + collectionIndex.workspace.update_policy.subscriptions[i] = validId; + collectionIndexUpdated = true; } + } } - if (collectionUpdates > 0) { - logger.warn(`Fixed ${ collectionUpdates } instances of x-mitre-collection that had an invalid STIX id.`); + if (collectionIndexUpdated) { + // eslint-disable-next-line no-await-in-loop + await collectionIndex.save(); + collectionIndexUpdates += 1; } + } - if (objectUpdates > 0) { - logger.warn(`Fixed ${ objectUpdates } ATT&CK objects that referenced an invalid x-mitre-collection STIX id.`); - } + if (collectionUpdates > 0) { + logger.warn( + `Fixed ${collectionUpdates} instances of x-mitre-collection that had an invalid STIX id.`, + ); + } - if (collectionIndexUpdates > 0) { - logger.warn(`Fixed ${ collectionIndexUpdates } Collection Indexes that referenced an invalid x-mitre-collection STIX id.`); - } + if (objectUpdates > 0) { + logger.warn( + `Fixed ${objectUpdates} ATT&CK objects that referenced an invalid x-mitre-collection STIX id.`, + ); + } + + if (collectionIndexUpdates > 0) { + logger.warn( + `Fixed ${collectionIndexUpdates} Collection Indexes that referenced an invalid x-mitre-collection STIX id.`, + ); + } } async function checkForStaticMarkingDefinitions() { - // Get the list static marking definitions configured for the system - const directoryPath = config.configurationFiles.staticMarkingDefinitionsPath; - if (!directoryPath) { - logger.info('No path provided for static marking definitions.'); - return; - } + // Get the list static marking definitions configured for the system + const directoryPath = config.configurationFiles.staticMarkingDefinitionsPath; + if (!directoryPath) { + logger.info('No path provided for static marking definitions.'); + return; + } - const files = await fs.readdir(directoryPath); - try { - for (const file of files.filter(file => path.extname(file) === '.json')) { - const filePath = path.join(directoryPath, file); - const fileData = await fs.readFile(filePath); - const staticMarkingDefinitionList = JSON.parse(fileData.toString()); + const files = await fs.readdir(directoryPath); + try { + for (const file of files.filter((file) => path.extname(file) === '.json')) { + const filePath = path.join(directoryPath, file); + const fileData = await fs.readFile(filePath); + const staticMarkingDefinitionList = JSON.parse(fileData.toString()); - for (const staticMarkingDefinition of staticMarkingDefinitionList) { - const markingDefinition = await MarkingDefinition.findOne({ 'stix.id': staticMarkingDefinition.id }).lean(); - if (!markingDefinition) { - const newMarkingDefinitionData = { - workspace: { - workflow: { - state: 'static' - } - }, - stix: staticMarkingDefinition - }; - try { - const newMarkingDefinition = new MarkingDefinition(newMarkingDefinitionData); - await newMarkingDefinition.save(); - logger.info(`Created static marking definition ${ newMarkingDefinition.stix.name }`); - } - catch(err) { - logger.error(`Unable to create static marking definition ${ staticMarkingDefinition.name }`); - } - } - } + for (const staticMarkingDefinition of staticMarkingDefinitionList) { + const markingDefinition = await MarkingDefinition.findOne({ + 'stix.id': staticMarkingDefinition.id, + }).lean(); + if (!markingDefinition) { + const newMarkingDefinitionData = { + workspace: { + workflow: { + state: 'static', + }, + }, + stix: staticMarkingDefinition, + }; + try { + const newMarkingDefinition = new MarkingDefinition(newMarkingDefinitionData); + await newMarkingDefinition.save(); + logger.info(`Created static marking definition ${newMarkingDefinition.stix.name}`); + } catch (err) { + logger.error( + `Unable to create static marking definition ${staticMarkingDefinition.name}`, + ); + } } + } } - catch(err) { - logger.error('Unable to parse static marking definitions'); - } + } catch (err) { + logger.error('Unable to parse static marking definitions'); + } } -exports.checkSystemConfiguration = async function() { - logger.info(`Performing system configuration check...`); - await checkForOrganizationIdentity(); - await checkForAnonymousUserAccount(); - await checkForInvalidEnterpriseCollectionId(); - await checkForStaticMarkingDefinitions(); -} +exports.checkSystemConfiguration = async function () { + logger.info(`Performing system configuration check...`); + await checkForOrganizationIdentity(); + await checkForAnonymousUserAccount(); + await checkForInvalidEnterpriseCollectionId(); + await checkForStaticMarkingDefinitions(); +}; diff --git a/app/lib/database-connection.js b/app/lib/database-connection.js index 84cf2ab3..a8e965d2 100644 --- a/app/lib/database-connection.js +++ b/app/lib/database-connection.js @@ -1,23 +1,25 @@ 'use strict'; -exports.initializeConnection = async function(options) { - const logger = require('./logger'); - const config = require('../config/config'); +exports.initializeConnection = async function (options) { + const logger = require('./logger'); + const config = require('../config/config'); - const databaseUrl = options?.databaseUrl || config.database.url; + const databaseUrl = options?.databaseUrl || config.database.url; - if (!databaseUrl) { - throw new Error('The URL for the MongoDB database was not set in the DATABASE_URL environment variable.'); - } + if (!databaseUrl) { + throw new Error( + 'The URL for the MongoDB database was not set in the DATABASE_URL environment variable.', + ); + } - const mongoose = require('mongoose'); + const mongoose = require('mongoose'); - // Configure mongoose to use ES6 promises - mongoose.Promise = global.Promise; + // Configure mongoose to use ES6 promises + mongoose.Promise = global.Promise; - // Bootstrap db connection - logger.info('Mongoose attempting to connect to ' + databaseUrl); - await mongoose.connect(databaseUrl); + // Bootstrap db connection + logger.info('Mongoose attempting to connect to ' + databaseUrl); + await mongoose.connect(databaseUrl); - logger.info('Mongoose connected to ' + databaseUrl); -} + logger.info('Mongoose connected to ' + databaseUrl); +}; diff --git a/app/lib/database-in-memory.js b/app/lib/database-in-memory.js index 6d8eb153..e9639277 100644 --- a/app/lib/database-in-memory.js +++ b/app/lib/database-in-memory.js @@ -4,50 +4,50 @@ const logger = require('./logger'); let mongod; -exports.initializeConnection = async function() { - if (!mongod) { - mongod = await MongoMemoryServer.create(); - } - - const uri = mongod.getUri(); - - // Configure mongoose to use ES6 promises - mongoose.Promise = global.Promise; - - // Bootstrap db connection - logger.info('Mongoose attempting to connect to in memory database at ' + uri); - try { - await mongoose.connect(uri); - } catch (error) { - handleError(error); - } - logger.info('Mongoose connected to ' + uri); -} - -exports.closeConnection = async function() { - if (mongod) { - await mongoose.connection.dropDatabase(); - await mongoose.connection.close(); - await mongod.stop(); - - mongod = null; - } -} - -exports.clearDatabase = async function() { - const collections = mongoose.connection.collections; - - for (const key in collections) { - const collection = collections[key]; - // eslint-disable-next-line no-await-in-loop - await collection.deleteMany(); - } -} +exports.initializeConnection = async function () { + if (!mongod) { + mongod = await MongoMemoryServer.create(); + } + + const uri = mongod.getUri(); + + // Configure mongoose to use ES6 promises + mongoose.Promise = global.Promise; + + // Bootstrap db connection + logger.info('Mongoose attempting to connect to in memory database at ' + uri); + try { + await mongoose.connect(uri); + } catch (error) { + handleError(error); + } + logger.info('Mongoose connected to ' + uri); +}; + +exports.closeConnection = async function () { + if (mongod) { + await mongoose.connection.dropDatabase(); + await mongoose.connection.close(); + await mongod.stop(); + + mongod = null; + } +}; + +exports.clearDatabase = async function () { + const collections = mongoose.connection.collections; + + for (const key in collections) { + const collection = collections[key]; + // eslint-disable-next-line no-await-in-loop + await collection.deleteMany(); + } +}; function handleError(error) { - logger.warn('Mongoose connection error: ' + error); - logger.warn('Database (mongoose) connection is required. Terminating app.'); + logger.warn('Mongoose connection error: ' + error); + logger.warn('Database (mongoose) connection is required. Terminating app.'); - // Terminate the app - process.exit(1); + // Terminate the app + process.exit(1); } diff --git a/app/lib/error-handler.js b/app/lib/error-handler.js index 40309d2e..ba3b138f 100644 --- a/app/lib/error-handler.js +++ b/app/lib/error-handler.js @@ -2,28 +2,26 @@ const logger = require('./logger'); -exports.bodyParser = function(err, req, res, next) { - if (err.name === 'SyntaxError') { - logger.warn('Unable to parse body, syntax error: ' + err.type); - res.status(400).send('Syntax error.'); - } - else { - next(err); - } +exports.bodyParser = function (err, req, res, next) { + if (err.name === 'SyntaxError') { + logger.warn('Unable to parse body, syntax error: ' + err.type); + res.status(400).send('Syntax error.'); + } else { + next(err); + } }; -exports.requestValidation = function(err, req, res, next) { - if (err.status && err.message) { - logger.warn('Request failed validation'); - logger.info((JSON.stringify(err))); - res.status(err.status).send(err.message); - } - else { - next(err); - } +exports.requestValidation = function (err, req, res, next) { + if (err.status && err.message) { + logger.warn('Request failed validation'); + logger.info(JSON.stringify(err)); + res.status(err.status).send(err.message); + } else { + next(err); + } }; -exports.catchAll = function(err, req, res, next) { - logger.error('catch all: ' + err); - res.status(500).send('Server error.'); +exports.catchAll = function (err, req, res, next) { + logger.error('catch all: ' + err); + res.status(500).send('Server error.'); }; diff --git a/app/lib/linkById.js b/app/lib/linkById.js index 05fd67e2..c37707ec 100644 --- a/app/lib/linkById.js +++ b/app/lib/linkById.js @@ -5,100 +5,103 @@ const config = require('../config/config'); // Default implmentation. Retrieves the attack object from the database. async function getAttackObjectFromDatabase(attackId) { - const attackObject = await AttackObject - .findOne({ 'workspace.attack_id': attackId }) - .sort('-stix.modified') - .lean() - .exec(); + const attackObject = await AttackObject.findOne({ 'workspace.attack_id': attackId }) + .sort('-stix.modified') + .lean() + .exec(); - return attackObject; + return attackObject; } exports.getAttackObjectFromDatabase = getAttackObjectFromDatabase; function attackReference(externalReferences) { - if (Array.isArray(externalReferences) && externalReferences.length > 0) { - return externalReferences.find(ref => config.attackSourceNames.includes(ref.source_name)); - } - else { - return null; - } + if (Array.isArray(externalReferences) && externalReferences.length > 0) { + return externalReferences.find((ref) => config.attackSourceNames.includes(ref.source_name)); + } else { + return null; + } } const linkByIdRegex = /\(LinkById: ([A-Z]+[0-9]+(\.[0-9]+)?)\)/g; async function convertLinkById(text, getAttackObject) { - if (text) { - let convertedText = ''; - let lastIndex = 0; - - const matches = text.matchAll(linkByIdRegex); - for (const match of matches) { - const prefix = text.slice(lastIndex, match.index); - const attackId = match[1]; - const citation = { - name: 'linked object not found', - url: '' - } - - if (attackId) { - const attackObject = await getAttackObject(attackId); - if (attackObject) { - citation.name = attackObject.stix.name; - const reference = attackReference(attackObject?.stix.external_references); - if (reference) { - citation.url = reference.url; - } - } - } - - convertedText = convertedText.concat(prefix, `[${ citation.name }](${ citation.url })`); - lastIndex = match.index + match[0].length; + if (text) { + let convertedText = ''; + let lastIndex = 0; + + const matches = text.matchAll(linkByIdRegex); + for (const match of matches) { + const prefix = text.slice(lastIndex, match.index); + const attackId = match[1]; + const citation = { + name: 'linked object not found', + url: '', + }; + + if (attackId) { + const attackObject = await getAttackObject(attackId); + if (attackObject) { + citation.name = attackObject.stix.name; + const reference = attackReference(attackObject?.stix.external_references); + if (reference) { + citation.url = reference.url; + } } + } - const postText = text.slice(lastIndex); - convertedText = convertedText.concat(postText); - - return convertedText; - } - else { - return text; + convertedText = convertedText.concat(prefix, `[${citation.name}](${citation.url})`); + lastIndex = match.index + match[0].length; } + + const postText = text.slice(lastIndex); + convertedText = convertedText.concat(postText); + + return convertedText; + } else { + return text; + } } async function convertExternalReferencesWithLinkById(externalReferences, getAttackObject) { - if (Array.isArray(externalReferences)) { - for (const externalReference of externalReferences) { - externalReference.description = await convertLinkById(externalReference.description, getAttackObject); - } + if (Array.isArray(externalReferences)) { + for (const externalReference of externalReferences) { + externalReference.description = await convertLinkById( + externalReference.description, + getAttackObject, + ); } + } } - // If provided, getAttackObject() must be an async function with the signature: // getAttackObject(attackId) returning an attack object -async function convertLinkByIdTags (stixObject, getAttackObject) { - if (!(getAttackObject instanceof Function)) { - getAttackObject = getAttackObjectFromDatabase; +async function convertLinkByIdTags(stixObject, getAttackObject) { + if (!(getAttackObject instanceof Function)) { + getAttackObject = getAttackObjectFromDatabase; + } + + if (stixObject) { + stixObject.description = await convertLinkById(stixObject.description, getAttackObject); + + if (stixObject.type === 'attack-pattern') { + stixObject.x_mitre_detection = await convertLinkById( + stixObject.x_mitre_detection, + getAttackObject, + ); } - if (stixObject) { - stixObject.description = await convertLinkById(stixObject.description, getAttackObject); - - if (stixObject.type === 'attack-pattern') { - stixObject.x_mitre_detection = await convertLinkById(stixObject.x_mitre_detection, getAttackObject); - } - - await convertExternalReferencesWithLinkById(stixObject.external_references, getAttackObject); - } + await convertExternalReferencesWithLinkById(stixObject.external_references, getAttackObject); + } } exports.convertLinkByIdTags = convertLinkByIdTags; function getAttackId(stixObject) { - if (Array.isArray(stixObject?.external_references)) { - const mitreAttackReference = stixObject.external_references.find(externalReference => config.attackSourceNames.includes(externalReference.source_name)); - return mitreAttackReference?.external_id; - } - else { - return null; - } + if (Array.isArray(stixObject?.external_references)) { + const mitreAttackReference = stixObject.external_references.find((externalReference) => + config.attackSourceNames.includes(externalReference.source_name), + ); + return mitreAttackReference?.external_id; + } else { + return null; + } } exports.getAttackId = getAttackId; diff --git a/app/lib/logger.js b/app/lib/logger.js index 6df03ab7..6bd86092 100644 --- a/app/lib/logger.js +++ b/app/lib/logger.js @@ -16,33 +16,33 @@ const config = require('../config/config'); // } const consoleFormat = winston.format.combine( - winston.format.timestamp(), - winston.format.printf(info => `${ info.timestamp } [${ info.level.toUpperCase() }] ${ info.message }`) - // winston.format.printf(info => `${ info.timestamp } [${ info.level.toUpperCase() }] ${ formatId(info) }${ info.message }`) + winston.format.timestamp(), + winston.format.printf( + (info) => `${info.timestamp} [${info.level.toUpperCase()}] ${info.message}`, + ), + // winston.format.printf(info => `${ info.timestamp } [${ info.level.toUpperCase() }] ${ formatId(info) }${ info.message }`) ); const logLevels = { - error: 0, - warn: 1, - http: 2, - info: 3, - verbose: 4, - debug: 5 + error: 0, + warn: 1, + http: 2, + info: 3, + verbose: 4, + debug: 5, }; const logger = winston.createLogger({ - format: consoleFormat, - transports: [ - new winston.transports.Console({ level: config.logging.logLevel }) - ], - levels: logLevels + format: consoleFormat, + transports: [new winston.transports.Console({ level: config.logging.logLevel })], + levels: logLevels, }); logger.stream = { - write: function(message, encoding) { - // Write to the log. Remove the last character to avoid double 'new line' characters. - logger.http(message.slice(0, -1)); - } + write: function (message, encoding) { + // Write to the log. Remove the last character to avoid double 'new line' characters. + logger.http(message.slice(0, -1)); + }, }; module.exports = logger; diff --git a/app/lib/migration/migrate-database.js b/app/lib/migration/migrate-database.js index 3d47c2c9..7f4b1c85 100644 --- a/app/lib/migration/migrate-database.js +++ b/app/lib/migration/migrate-database.js @@ -4,31 +4,31 @@ const migrateMongo = require('migrate-mongo'); const config = require('../../config/config'); const logger = require('../logger'); -exports.migrateDatabase = async function() { - global.options = { - file: './app/lib/migration/migration-config.js' - }; +exports.migrateDatabase = async function () { + global.options = { + file: './app/lib/migration/migration-config.js', + }; - const { db, client } = await migrateMongo.database.connect(); - const migrationStatus = await migrateMongo.status(db); + const { db, client } = await migrateMongo.database.connect(); + const migrationStatus = await migrateMongo.status(db); - const actionsPending = Boolean(migrationStatus.find(elem => elem.appliedAt === 'PENDING')); - if (actionsPending) { - if (config.database.migration.enable) { - logger.info('Starting database migration...'); - const appliedActions = await migrateMongo.up(db, client); - for (const action of appliedActions) { - logger.info(`Applied migration action: ${ action }`); - } - } - else { - await client.close(); - throw new Error('One or more database migration actions are pending, but database migrations are disabled'); - } - } - else { - logger.info('No pending database migration actions found'); + const actionsPending = Boolean(migrationStatus.find((elem) => elem.appliedAt === 'PENDING')); + if (actionsPending) { + if (config.database.migration.enable) { + logger.info('Starting database migration...'); + const appliedActions = await migrateMongo.up(db, client); + for (const action of appliedActions) { + logger.info(`Applied migration action: ${action}`); + } + } else { + await client.close(); + throw new Error( + 'One or more database migration actions are pending, but database migrations are disabled', + ); } + } else { + logger.info('No pending database migration actions found'); + } - await client.close(); + await client.close(); }; diff --git a/app/lib/migration/migration-config.js b/app/lib/migration/migration-config.js index 12da6b3c..bbc9b8d7 100644 --- a/app/lib/migration/migration-config.js +++ b/app/lib/migration/migration-config.js @@ -3,24 +3,24 @@ const config = require('../../config/config'); module.exports = { - mongodb: { - url: config.database.url, + mongodb: { + url: config.database.url, - options: { - useNewUrlParser: true - } + options: { + useNewUrlParser: true, }, + }, - // The migrations dir, can be an relative or absolute path. Only edit this when really necessary. - migrationsDir: "migrations", + // The migrations dir, can be an relative or absolute path. Only edit this when really necessary. + migrationsDir: 'migrations', - // The mongodb collection where the applied changes are stored. Only edit this when really necessary. - changelogCollectionName: "changelog", + // The mongodb collection where the applied changes are stored. Only edit this when really necessary. + changelogCollectionName: 'changelog', - // The file extension to create migrations and search for in migration dir - migrationFileExtension: ".js", + // The file extension to create migrations and search for in migration dir + migrationFileExtension: '.js', - // Enable the algorithm to create a checksum of the file contents and use that in the comparison to determine - // if the file should be run. Requires that scripts are coded to be run multiple times. - useFileHash: false + // Enable the algorithm to create a checksum of the file contents and use that in the comparison to determine + // if the file should be run. Requires that scripts are coded to be run multiple times. + useFileHash: false, }; diff --git a/app/lib/model-names.js b/app/lib/model-names.js index adf65d7f..5d7ae23c 100644 --- a/app/lib/model-names.js +++ b/app/lib/model-names.js @@ -6,18 +6,18 @@ * @enum {string} */ exports.ModelName = { - Asset: "Asset", - Campaign: "Campaign", - Collection: "Collection", - DataComponent: "Data-Component", - DataSource: "Data-Source", - Group: "Intrusion-Set", - Identity: "IdentityModel", - MarkingDefinition: "MarkingDefinitionModel", - Matrix: "MatrixModel", - Note: "NoteModel", - Relationship: "Relationship", - Software: "Software", - Tactic: "Tactic", - Technique: "Technique" -}; \ No newline at end of file + Asset: 'Asset', + Campaign: 'Campaign', + Collection: 'Collection', + DataComponent: 'Data-Component', + DataSource: 'Data-Source', + Group: 'Intrusion-Set', + Identity: 'IdentityModel', + MarkingDefinition: 'MarkingDefinitionModel', + Matrix: 'MatrixModel', + Note: 'NoteModel', + Relationship: 'Relationship', + Software: 'Software', + Tactic: 'Tactic', + Technique: 'Technique', +}; diff --git a/app/lib/regex.js b/app/lib/regex.js index 2253f9ee..8ffaa6da 100644 --- a/app/lib/regex.js +++ b/app/lib/regex.js @@ -1,16 +1,15 @@ 'use strict'; -exports.sanitizeRegex = function(expression) { - // Compile the expression. If it's valid, return the expression. Otherwise, return an empty string. - try { - // Escapes all regex characters so they are treated like literal characters rather than special regex characters - // eslint-disable-next-line no-useless-escape - expression = expression.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - // eslint-disable-next-line no-new - new RegExp(expression); - return expression; - } - catch(err) { - return ''; - } -} +exports.sanitizeRegex = function (expression) { + // Compile the expression. If it's valid, return the expression. Otherwise, return an empty string. + try { + // Escapes all regex characters so they are treated like literal characters rather than special regex characters + // eslint-disable-next-line no-useless-escape + expression = expression.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + // eslint-disable-next-line no-new + new RegExp(expression); + return expression; + } catch (err) { + return ''; + } +}; diff --git a/app/lib/request-parameter-helper.js b/app/lib/request-parameter-helper.js index 99bd9905..718001e5 100644 --- a/app/lib/request-parameter-helper.js +++ b/app/lib/request-parameter-helper.js @@ -1,11 +1,11 @@ const lastUpdatedByQueryHelper = function (lastUpdatedByOption) { - if (Array.isArray(lastUpdatedByOption)) { - return { $in: lastUpdatedByOption }; - } else { - return lastUpdatedByOption; - } + if (Array.isArray(lastUpdatedByOption)) { + return { $in: lastUpdatedByOption }; + } else { + return lastUpdatedByOption; + } }; module.exports = { - lastUpdatedByQueryHelper, + lastUpdatedByQueryHelper, }; diff --git a/app/lib/requestId.js b/app/lib/requestId.js index 1ea1fa98..a15f7aeb 100644 --- a/app/lib/requestId.js +++ b/app/lib/requestId.js @@ -5,6 +5,6 @@ const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz const idGenerator = customAlphabet(alphabet, 12); module.exports = function (req, res, next) { - req.id = idGenerator(); - next(); -} + req.id = idGenerator(); + next(); +}; diff --git a/app/models/asset-model.js b/app/models/asset-model.js index d2e24124..1704314d 100644 --- a/app/models/asset-model.js +++ b/app/models/asset-model.js @@ -5,35 +5,35 @@ const AttackObject = require('./attack-object-model'); const { ModelName } = require('../lib/model-names'); const relatedAsset = { - name: { type: String, required: true }, - related_asset_sectors : [ String ], - description: String, + name: { type: String, required: true }, + related_asset_sectors: [String], + description: String, }; const relatedAssetSchema = new mongoose.Schema(relatedAsset, { _id: false }); const stixAsset = { - // STIX asset specific properties - modified: { type: Date, required: true }, - name: { type: String, required: true }, - description: String, - - // ATT&CK custom stix properties - x_mitre_sectors: [ String ], - x_mitre_related_assets: [ relatedAssetSchema ], - x_mitre_modified_by_ref: String, - x_mitre_platforms: [ String ], - x_mitre_deprecated: Boolean, - x_mitre_domains: [ String ], - x_mitre_version: String, - x_mitre_attack_spec_version: String, - x_mitre_contributors: [ String ], + // STIX asset specific properties + modified: { type: Date, required: true }, + name: { type: String, required: true }, + description: String, + + // ATT&CK custom stix properties + x_mitre_sectors: [String], + x_mitre_related_assets: [relatedAssetSchema], + x_mitre_modified_by_ref: String, + x_mitre_platforms: [String], + x_mitre_deprecated: Boolean, + x_mitre_domains: [String], + x_mitre_version: String, + x_mitre_attack_spec_version: String, + x_mitre_contributors: [String], }; // Create the definition const assetDefinition = { - stix: { - ...stixAsset - } + stix: { + ...stixAsset, + }, }; // Create the schema diff --git a/app/models/attack-object-model.js b/app/models/attack-object-model.js index a68c8db2..a338c6df 100644 --- a/app/models/attack-object-model.js +++ b/app/models/attack-object-model.js @@ -8,30 +8,32 @@ const config = require('../config/config'); // Create the definition const attackObjectDefinition = { - workspace: { - ...workspaceDefinitions.common - }, - stix: { - ...stixCoreDefinitions.commonRequiredSDO, - ...stixCoreDefinitions.commonOptionalSDO - } + workspace: { + ...workspaceDefinitions.common, + }, + stix: { + ...stixCoreDefinitions.commonRequiredSDO, + ...stixCoreDefinitions.commonOptionalSDO, + }, }; // Create the schema const options = { - collection: 'attackObjects' + collection: 'attackObjects', }; const attackObjectSchema = new mongoose.Schema(attackObjectDefinition, options); //Save the ATT&CK ID in a more easily queried location -attackObjectSchema.pre('save', function(next) { - if (this.stix.external_references) { - const mitreAttackReference = this.stix.external_references.find(externalReference => config.attackSourceNames.includes(externalReference.source_name)); - if (mitreAttackReference && mitreAttackReference.external_id) { - this.workspace.attack_id = mitreAttackReference.external_id; - } +attackObjectSchema.pre('save', function (next) { + if (this.stix.external_references) { + const mitreAttackReference = this.stix.external_references.find((externalReference) => + config.attackSourceNames.includes(externalReference.source_name), + ); + if (mitreAttackReference && mitreAttackReference.external_id) { + this.workspace.attack_id = mitreAttackReference.external_id; } - return next(); + } + return next(); }); // Add an index on stix.id and stix.modified diff --git a/app/models/campaign-model.js b/app/models/campaign-model.js index 680735af..32e42383 100644 --- a/app/models/campaign-model.js +++ b/app/models/campaign-model.js @@ -5,29 +5,29 @@ const AttackObject = require('./attack-object-model'); const { ModelName } = require('../lib/model-names'); const stixCampaign = { - // STIX campaign specific properties - modified: { type: Date, required: true }, - name: { type: String, required: true }, - description: String, - aliases: [ String ], - first_seen: { type: Date, required: true }, - last_seen: { type: Date, required: true }, + // STIX campaign specific properties + modified: { type: Date, required: true }, + name: { type: String, required: true }, + description: String, + aliases: [String], + first_seen: { type: Date, required: true }, + last_seen: { type: Date, required: true }, - // ATT&CK custom stix properties - x_mitre_first_seen_citation: { type: String, required: true }, - x_mitre_last_seen_citation: { type: String, required: true }, - x_mitre_modified_by_ref: String, - x_mitre_deprecated: Boolean, - x_mitre_version: String, - x_mitre_attack_spec_version: String, - x_mitre_contributors: [ String ] + // ATT&CK custom stix properties + x_mitre_first_seen_citation: { type: String, required: true }, + x_mitre_last_seen_citation: { type: String, required: true }, + x_mitre_modified_by_ref: String, + x_mitre_deprecated: Boolean, + x_mitre_version: String, + x_mitre_attack_spec_version: String, + x_mitre_contributors: [String], }; // Create the definition const campaignDefinition = { - stix: { - ...stixCampaign - } + stix: { + ...stixCampaign, + }, }; // Create the schema diff --git a/app/models/collection-index-model.js b/app/models/collection-index-model.js index be7183fb..d5c62c66 100644 --- a/app/models/collection-index-model.js +++ b/app/models/collection-index-model.js @@ -4,52 +4,54 @@ const mongoose = require('mongoose'); // Create the definition const collectionVersionDefinition = { - version: { type: String, required: true }, - modified: { type: Date, required: true }, - url: { type: String }, - taxii_url: { type: String }, - release_notes: { type: String } + version: { type: String, required: true }, + modified: { type: Date, required: true }, + url: { type: String }, + taxii_url: { type: String }, + release_notes: { type: String }, }; const collectionVersionSchema = new mongoose.Schema(collectionVersionDefinition, { _id: false }); const collectionReferenceDefinition = { - id: { type: String, required: true }, - name : { type: String, required: true }, - description : { type: String }, - created : { type: Date, required: true }, - versions : [ collectionVersionSchema ] + id: { type: String, required: true }, + name: { type: String, required: true }, + description: { type: String }, + created: { type: Date, required: true }, + versions: [collectionVersionSchema], }; -const collectionReferenceSchema = new mongoose.Schema(collectionReferenceDefinition, { _id: false }); +const collectionReferenceSchema = new mongoose.Schema(collectionReferenceDefinition, { + _id: false, +}); // This is the collection index that was retrieved const collectionIndexObjectDefinition = { - id: { type: String, required: true }, - name: { type: String, required: true }, - description: { type: String }, - created: { type: Date, required: true }, - modified: { type: Date, required: true }, - collections: [ collectionReferenceSchema ] + id: { type: String, required: true }, + name: { type: String, required: true }, + description: { type: String }, + created: { type: Date, required: true }, + modified: { type: Date, required: true }, + collections: [collectionReferenceSchema], }; // This is the collection index with its workspace data const collectionIndexWrapperDefinition = { - collection_index: { - ...collectionIndexObjectDefinition + collection_index: { + ...collectionIndexObjectDefinition, + }, + workspace: { + remote_url: { type: String }, + update_policy: { + automatic: { type: Boolean }, + interval: { type: Number }, + last_retrieval: { type: Date }, + subscriptions: [String], }, - workspace: { - remote_url: { type: String }, - update_policy: { - automatic: { type: Boolean }, - interval: { type: Number }, - last_retrieval: { type: Date }, - subscriptions: [ String ] - } - } + }, }; // Create the schema const options = { - collection: 'collectionIndexes' + collection: 'collectionIndexes', }; const collectionIndexSchema = new mongoose.Schema(collectionIndexWrapperDefinition, options); diff --git a/app/models/collection-model.js b/app/models/collection-model.js index 7ee3fa17..380d6916 100644 --- a/app/models/collection-model.js +++ b/app/models/collection-model.js @@ -6,32 +6,32 @@ const workspaceDefinitions = require('./subschemas/workspace'); const { ModelName } = require('../lib/model-names'); const xMitreContent = { - object_ref: { type: String, required: true }, - object_modified : { type: Date, required: true } + object_ref: { type: String, required: true }, + object_modified: { type: Date, required: true }, }; const xMitreContentSchema = new mongoose.Schema(xMitreContent, { _id: false }); const xMitreCollection = { - modified: { type: Date, required: true }, - name: { type: String, required: true }, - description: String, - - x_mitre_modified_by_ref: String, - x_mitre_contents: [ xMitreContentSchema ], - x_mitre_deprecated: Boolean, - x_mitre_domains: [ String ], - x_mitre_version: String, - x_mitre_attack_spec_version: String + modified: { type: Date, required: true }, + name: { type: String, required: true }, + description: String, + + x_mitre_modified_by_ref: String, + x_mitre_contents: [xMitreContentSchema], + x_mitre_deprecated: Boolean, + x_mitre_domains: [String], + x_mitre_version: String, + x_mitre_attack_spec_version: String, }; // Create the definition const collectionDefinition = { - workspace: { - ...workspaceDefinitions.collection - }, - stix: { - ...xMitreCollection - } + workspace: { + ...workspaceDefinitions.collection, + }, + stix: { + ...xMitreCollection, + }, }; // Create the schema diff --git a/app/models/data-component-model.js b/app/models/data-component-model.js index 51e84cbc..fd2c67f7 100644 --- a/app/models/data-component-model.js +++ b/app/models/data-component-model.js @@ -5,25 +5,25 @@ const AttackObject = require('./attack-object-model'); const { ModelName } = require('../lib/model-names'); const stixDataComponent = { - // STIX x-mitre-data-component specific properties - modified: { type: Date, required: true }, - name: { type: String, required: true }, - description: String, + // STIX x-mitre-data-component specific properties + modified: { type: Date, required: true }, + name: { type: String, required: true }, + description: String, - // ATT&CK custom stix properties - x_mitre_data_source_ref: String, - x_mitre_modified_by_ref: String, - x_mitre_deprecated: Boolean, - x_mitre_domains: [ String ], - x_mitre_version: String, - x_mitre_attack_spec_version: String + // ATT&CK custom stix properties + x_mitre_data_source_ref: String, + x_mitre_modified_by_ref: String, + x_mitre_deprecated: Boolean, + x_mitre_domains: [String], + x_mitre_version: String, + x_mitre_attack_spec_version: String, }; // Create the definition const dataComponentDefinition = { - stix: { - ...stixDataComponent - } + stix: { + ...stixDataComponent, + }, }; // Create the schema diff --git a/app/models/data-source-model.js b/app/models/data-source-model.js index 3f6fa713..1ffcc36d 100644 --- a/app/models/data-source-model.js +++ b/app/models/data-source-model.js @@ -5,27 +5,27 @@ const AttackObject = require('./attack-object-model'); const { ModelName } = require('../lib/model-names'); const stixDataSource = { - // STIX x-mitre-data-source specific properties - modified: { type: Date, required: true }, - name: { type: String, required: true }, - description: String, + // STIX x-mitre-data-source specific properties + modified: { type: Date, required: true }, + name: { type: String, required: true }, + description: String, - // ATT&CK custom stix properties - x_mitre_modified_by_ref: String, - x_mitre_platforms: [ String ], - x_mitre_deprecated: Boolean, - x_mitre_domains: [ String ], - x_mitre_version: String, - x_mitre_attack_spec_version: String, - x_mitre_contributors: [ String ], - x_mitre_collection_layers: [ String ] + // ATT&CK custom stix properties + x_mitre_modified_by_ref: String, + x_mitre_platforms: [String], + x_mitre_deprecated: Boolean, + x_mitre_domains: [String], + x_mitre_version: String, + x_mitre_attack_spec_version: String, + x_mitre_contributors: [String], + x_mitre_collection_layers: [String], }; // Create the definition const dataSourceDefinition = { - stix: { - ...stixDataSource - } + stix: { + ...stixDataSource, + }, }; // Create the schema diff --git a/app/models/group-model.js b/app/models/group-model.js index 692d05b1..7d6a24b5 100644 --- a/app/models/group-model.js +++ b/app/models/group-model.js @@ -5,26 +5,26 @@ const AttackObject = require('./attack-object-model'); const { ModelName } = require('../lib/model-names'); const stixIntrusionSet = { - // STIX intrusion-set specific properties - modified: { type: Date, required: true }, - name: { type: String, required: true }, - description: String, + // STIX intrusion-set specific properties + modified: { type: Date, required: true }, + name: { type: String, required: true }, + description: String, - // ATT&CK custom stix properties - aliases: [ String ], - x_mitre_modified_by_ref: String, - x_mitre_deprecated: Boolean, - x_mitre_domains: [ String ], // TBD drop this property - x_mitre_version: String, - x_mitre_attack_spec_version: String, - x_mitre_contributors: [ String ] + // ATT&CK custom stix properties + aliases: [String], + x_mitre_modified_by_ref: String, + x_mitre_deprecated: Boolean, + x_mitre_domains: [String], // TBD drop this property + x_mitre_version: String, + x_mitre_attack_spec_version: String, + x_mitre_contributors: [String], }; // Create the definition const groupDefinition = { - stix: { - ...stixIntrusionSet - } + stix: { + ...stixIntrusionSet, + }, }; // Create the schema diff --git a/app/models/identity-model.js b/app/models/identity-model.js index 743efb31..8fd0ef7b 100644 --- a/app/models/identity-model.js +++ b/app/models/identity-model.js @@ -5,27 +5,27 @@ const AttackObject = require('./attack-object-model'); const { ModelName } = require('../lib/model-names'); const identityProperties = { - // identity specific properties - modified: { type: Date, required: true }, - name: { type: String, required: true }, - description: String, - roles: [ String ], - identity_class: String, - sectors: [ String ], - contact_information: String, + // identity specific properties + modified: { type: Date, required: true }, + name: { type: String, required: true }, + description: String, + roles: [String], + identity_class: String, + sectors: [String], + contact_information: String, - // ATT&CK custom stix properties - x_mitre_modified_by_ref: String, - x_mitre_deprecated: Boolean, - x_mitre_version: String, - x_mitre_attack_spec_version: String + // ATT&CK custom stix properties + x_mitre_modified_by_ref: String, + x_mitre_deprecated: Boolean, + x_mitre_version: String, + x_mitre_attack_spec_version: String, }; // Create the definition const identityDefinition = { - stix: { - ...identityProperties - } + stix: { + ...identityProperties, + }, }; // Create the schema diff --git a/app/models/marking-definition-model.js b/app/models/marking-definition-model.js index 4699b18c..5e24dc20 100644 --- a/app/models/marking-definition-model.js +++ b/app/models/marking-definition-model.js @@ -5,34 +5,37 @@ const AttackObject = require('./attack-object-model'); const { ModelName } = require('../lib/model-names'); const markingObject = { - statement: String, - tlp: String + statement: String, + tlp: String, }; // TBD: Marking Definition should not have modified or revoked properties. const markingDefinitionProperties = { - // marking definition specific properties - name: String, - definition_type: String, - definition: markingObject, - - // ATT&CK custom stix properties - x_mitre_deprecated: Boolean, - x_mitre_attack_spec_version: String + // marking definition specific properties + name: String, + definition_type: String, + definition: markingObject, + + // ATT&CK custom stix properties + x_mitre_deprecated: Boolean, + x_mitre_attack_spec_version: String, }; // Create the definition const markingDefinitionDefinition = { - stix: { - ...markingDefinitionProperties - } + stix: { + ...markingDefinitionProperties, + }, }; // Create the schema const markingDefinitionSchema = new mongoose.Schema(markingDefinitionDefinition); // Create the model -const MarkingDefinitionModel = AttackObject.discriminator(ModelName.MarkingDefinition, markingDefinitionSchema); +const MarkingDefinitionModel = AttackObject.discriminator( + ModelName.MarkingDefinition, + markingDefinitionSchema, +); module.exports = MarkingDefinitionModel; diff --git a/app/models/matrix-model.js b/app/models/matrix-model.js index c9556583..6cd166b2 100644 --- a/app/models/matrix-model.js +++ b/app/models/matrix-model.js @@ -5,25 +5,25 @@ const AttackObject = require('./attack-object-model'); const { ModelName } = require('../lib/model-names'); const matrixProperties = { - // x-mitre-matrix specific properties - modified: { type: Date, required: true }, - name: { type: String, required: true }, - description: String, + // x-mitre-matrix specific properties + modified: { type: Date, required: true }, + name: { type: String, required: true }, + description: String, - // ATT&CK custom stix properties - tactic_refs: [ String ], - x_mitre_modified_by_ref: String, - x_mitre_deprecated: Boolean, - x_mitre_domains: [ String ], // TBD drop this property - x_mitre_version: String, - x_mitre_attack_spec_version: String + // ATT&CK custom stix properties + tactic_refs: [String], + x_mitre_modified_by_ref: String, + x_mitre_deprecated: Boolean, + x_mitre_domains: [String], // TBD drop this property + x_mitre_version: String, + x_mitre_attack_spec_version: String, }; // Create the definition const matrixDefinition = { - stix: { - ...matrixProperties - } + stix: { + ...matrixProperties, + }, }; // Create the schema diff --git a/app/models/mitigation-model.js b/app/models/mitigation-model.js index 0acf636f..81dc366f 100644 --- a/app/models/mitigation-model.js +++ b/app/models/mitigation-model.js @@ -4,25 +4,25 @@ const mongoose = require('mongoose'); const AttackObject = require('./attack-object-model'); const stixCourseOfAction = { - // STIX course-of-action specific properties - modified: { type: Date, required: true }, - name: { type: String, required: true }, - description: String, - labels: [ String ], + // STIX course-of-action specific properties + modified: { type: Date, required: true }, + name: { type: String, required: true }, + description: String, + labels: [String], - // ATT&CK custom stix properties - x_mitre_modified_by_ref: String, - x_mitre_deprecated: Boolean, - x_mitre_domains: [ String ], - x_mitre_version: String, - x_mitre_attack_spec_version: String + // ATT&CK custom stix properties + x_mitre_modified_by_ref: String, + x_mitre_deprecated: Boolean, + x_mitre_domains: [String], + x_mitre_version: String, + x_mitre_attack_spec_version: String, }; // Create the definition const mitigationDefinition = { - stix: { - ...stixCourseOfAction - } + stix: { + ...stixCourseOfAction, + }, }; // Create the schema diff --git a/app/models/note-model.js b/app/models/note-model.js index bfbde1e9..5b6bc798 100644 --- a/app/models/note-model.js +++ b/app/models/note-model.js @@ -5,24 +5,24 @@ const AttackObject = require('./attack-object-model'); const { ModelName } = require('../lib/model-names'); const noteProperties = { - // note specific properties - modified: { type: Date, required: true }, - abstract: String, - content: { type: String, required: true }, - authors: [ String ], - object_refs: { type: [ String ], required: true }, + // note specific properties + modified: { type: Date, required: true }, + abstract: String, + content: { type: String, required: true }, + authors: [String], + object_refs: { type: [String], required: true }, - // ATT&CK custom stix properties - x_mitre_modified_by_ref: String, - x_mitre_deprecated: Boolean, - x_mitre_attack_spec_version: String + // ATT&CK custom stix properties + x_mitre_modified_by_ref: String, + x_mitre_deprecated: Boolean, + x_mitre_attack_spec_version: String, }; // Create the definition const noteDefinition = { - stix: { - ...noteProperties - } + stix: { + ...noteProperties, + }, }; // Create the schema diff --git a/app/models/reference-model.js b/app/models/reference-model.js index b52404ea..42e1296e 100644 --- a/app/models/reference-model.js +++ b/app/models/reference-model.js @@ -4,16 +4,16 @@ const mongoose = require('mongoose'); // Create the definition const referenceDefinition = { - source_name: { type: String, required: true }, - description: { type: String, required: true }, - url: String + source_name: { type: String, required: true }, + description: { type: String, required: true }, + url: String, }; // Create the schema const referenceSchema = new mongoose.Schema(referenceDefinition, { bufferCommands: false }); // The source_name must be unique -referenceSchema.index({ 'source_name': 1}, { unique: true }); +referenceSchema.index({ source_name: 1 }, { unique: true }); // Create a text index to allow for text-based queries referenceSchema.index({ source_name: 'text', description: 'text', url: 'text' }); diff --git a/app/models/relationship-model.js b/app/models/relationship-model.js index d21abef9..8a348fe3 100644 --- a/app/models/relationship-model.js +++ b/app/models/relationship-model.js @@ -6,33 +6,33 @@ const stixCoreDefinitions = require('./subschemas/stix-core'); const { ModelName } = require('../lib/model-names'); const relationshipProperties = { - // relationship specific properties - modified: { type: Date, required: true }, - name: String, - description: String, - relationship_type: { type: String, required: true }, - source_ref: { type: String, required: true }, - target_ref: { type: String, required: true }, - start_time: Date, - stop_time: Date, - - // ATT&CK custom stix properties - x_mitre_modified_by_ref: String, - x_mitre_deprecated: Boolean, - x_mitre_version: String, - x_mitre_attack_spec_version: String + // relationship specific properties + modified: { type: Date, required: true }, + name: String, + description: String, + relationship_type: { type: String, required: true }, + source_ref: { type: String, required: true }, + target_ref: { type: String, required: true }, + start_time: Date, + stop_time: Date, + + // ATT&CK custom stix properties + x_mitre_modified_by_ref: String, + x_mitre_deprecated: Boolean, + x_mitre_version: String, + x_mitre_attack_spec_version: String, }; // Create the definition const relationshipDefinition = { - workspace: { - ...workspaceDefinitions.common - }, - stix: { - ...stixCoreDefinitions.commonRequiredSDO, - ...stixCoreDefinitions.commonOptionalSDO, - ...relationshipProperties - } + workspace: { + ...workspaceDefinitions.common, + }, + stix: { + ...stixCoreDefinitions.commonRequiredSDO, + ...stixCoreDefinitions.commonOptionalSDO, + ...relationshipProperties, + }, }; // Create the schema diff --git a/app/models/software-model.js b/app/models/software-model.js index 1c1ce411..4174e3ad 100644 --- a/app/models/software-model.js +++ b/app/models/software-model.js @@ -5,29 +5,29 @@ const AttackObject = require('./attack-object-model'); const { ModelName } = require('../lib/model-names'); const stixMalware = { - // STIX malware and tool specific properties - modified: { type: Date, required: true }, - name: { type: String, required: true }, - description: String, - is_family: Boolean, - labels: [ String ], + // STIX malware and tool specific properties + modified: { type: Date, required: true }, + name: { type: String, required: true }, + description: String, + is_family: Boolean, + labels: [String], - // ATT&CK custom stix properties - x_mitre_modified_by_ref: String, - x_mitre_platforms: [ String ], - x_mitre_deprecated: Boolean, - x_mitre_domains: [ String ], - x_mitre_version: String, - x_mitre_attack_spec_version: String, - x_mitre_contributors: [ String ], - x_mitre_aliases: [ String ], + // ATT&CK custom stix properties + x_mitre_modified_by_ref: String, + x_mitre_platforms: [String], + x_mitre_deprecated: Boolean, + x_mitre_domains: [String], + x_mitre_version: String, + x_mitre_attack_spec_version: String, + x_mitre_contributors: [String], + x_mitre_aliases: [String], }; // Create the definition const softwareDefinition = { - stix: { - ...stixMalware - } + stix: { + ...stixMalware, + }, }; // Create the schema diff --git a/app/models/subschemas/attack-pattern.js b/app/models/subschemas/attack-pattern.js index b3fbcfab..4dc20785 100644 --- a/app/models/subschemas/attack-pattern.js +++ b/app/models/subschemas/attack-pattern.js @@ -3,40 +3,40 @@ const stixCore = require('./stix-core'); module.exports.attackPattern = { - // STIX attack-pattern specific properties - modified: { type: Date, required: true }, - name: { type: String, required: true }, - description: String, - kill_chain_phases: [ stixCore.killChainPhaseSchema ], + // STIX attack-pattern specific properties + modified: { type: Date, required: true }, + name: { type: String, required: true }, + description: String, + kill_chain_phases: [stixCore.killChainPhaseSchema], - // ATT&CK custom STIX properties - x_mitre_attack_spec_version: String, - x_mitre_contributors: [ String ], - x_mitre_deprecated: Boolean, - x_mitre_detection: String, - x_mitre_domains: [ String ], - x_mitre_is_subtechnique: Boolean, - x_mitre_modified_by_ref: String, - x_mitre_platforms: [ String ], - x_mitre_version: String, + // ATT&CK custom STIX properties + x_mitre_attack_spec_version: String, + x_mitre_contributors: [String], + x_mitre_deprecated: Boolean, + x_mitre_detection: String, + x_mitre_domains: [String], + x_mitre_is_subtechnique: Boolean, + x_mitre_modified_by_ref: String, + x_mitre_platforms: [String], + x_mitre_version: String, }; // Domain specific properties module.exports.attackPatternEnterpriseDomain = { - x_mitre_data_sources: { type: [ String ], default: undefined }, - x_mitre_defense_bypassed: { type: [ String ], default: undefined }, - x_mitre_effective_permissions: { type: [ String ], default: undefined }, - x_mitre_impact_type: { type: [ String ], default: undefined }, - x_mitre_network_requirements: Boolean, - x_mitre_permissions_required: { type: [ String ], default: undefined }, - x_mitre_remote_support: Boolean, - x_mitre_system_requirements: { type: [ String ], default: undefined }, + x_mitre_data_sources: { type: [String], default: undefined }, + x_mitre_defense_bypassed: { type: [String], default: undefined }, + x_mitre_effective_permissions: { type: [String], default: undefined }, + x_mitre_impact_type: { type: [String], default: undefined }, + x_mitre_network_requirements: Boolean, + x_mitre_permissions_required: { type: [String], default: undefined }, + x_mitre_remote_support: Boolean, + x_mitre_system_requirements: { type: [String], default: undefined }, }; module.exports.attackPatternMobileDomain = { - x_mitre_tactic_type: { type: [ String ], default: undefined } + x_mitre_tactic_type: { type: [String], default: undefined }, }; module.exports.attackPatternICSDomain = { - x_mitre_data_sources: { type: [ String ], default: undefined } + x_mitre_data_sources: { type: [String], default: undefined }, }; diff --git a/app/models/subschemas/stix-core.js b/app/models/subschemas/stix-core.js index 76f1846d..969b3287 100644 --- a/app/models/subschemas/stix-core.js +++ b/app/models/subschemas/stix-core.js @@ -3,49 +3,49 @@ const mongoose = require('mongoose'); const externalReference = { - source_name: { type: String, required: true }, - description: { type: String }, - url: { type: String }, - external_id: { type: String } + source_name: { type: String, required: true }, + description: { type: String }, + url: { type: String }, + external_id: { type: String }, }; const externalReferenceSchema = new mongoose.Schema(externalReference, { _id: false }); const killChainPhase = { - kill_chain_name: { type: String, required: true }, - phase_name : { type: String, required: true } + kill_chain_name: { type: String, required: true }, + phase_name: { type: String, required: true }, }; module.exports.killChainPhaseSchema = new mongoose.Schema(killChainPhase, { _id: false }); module.exports.commonRequiredSDO = { - type: { - type: String, - enum: [ - 'attack-pattern', - 'campaign', - 'course-of-action', - 'identity', - 'intrusion-set', - 'malware', - 'marking-definition', - 'note', - 'relationship', - 'tool', - 'x-mitre-asset', - 'x-mitre-collection', - 'x-mitre-data-source', - 'x-mitre-data-component', - 'x-mitre-matrix', - 'x-mitre-tactic', - ] - }, - spec_version: { type: String, required: true }, - id: { type: String, required: true }, - created: { type: Date, required: true } + type: { + type: String, + enum: [ + 'attack-pattern', + 'campaign', + 'course-of-action', + 'identity', + 'intrusion-set', + 'malware', + 'marking-definition', + 'note', + 'relationship', + 'tool', + 'x-mitre-asset', + 'x-mitre-collection', + 'x-mitre-data-source', + 'x-mitre-data-component', + 'x-mitre-matrix', + 'x-mitre-tactic', + ], + }, + spec_version: { type: String, required: true }, + id: { type: String, required: true }, + created: { type: Date, required: true }, }; module.exports.commonOptionalSDO = { - created_by_ref: { type: String }, - revoked: { type: Boolean }, - external_references: [ externalReferenceSchema ], - object_marking_refs: [ String ] + created_by_ref: { type: String }, + revoked: { type: Boolean }, + external_references: [externalReferenceSchema], + object_marking_refs: [String], }; diff --git a/app/models/subschemas/workspace.js b/app/models/subschemas/workspace.js index cc64f502..7bb99572 100644 --- a/app/models/subschemas/workspace.js +++ b/app/models/subschemas/workspace.js @@ -3,44 +3,39 @@ const mongoose = require('mongoose'); const collectionVersion = { - collection_ref: { type: String, required: true }, - collection_modified : { type: Date, required: true } -} + collection_ref: { type: String, required: true }, + collection_modified: { type: Date, required: true }, +}; const collectionVersionSchema = new mongoose.Schema(collectionVersion, { _id: false }); /** * Workspace property definition for most object types */ module.exports.common = { - workflow: { - state: { - type: String, - enum: [ - 'work-in-progress', - 'awaiting-review', - 'reviewed', - 'static' - ] - }, - created_by_user_account: String + workflow: { + state: { + type: String, + enum: ['work-in-progress', 'awaiting-review', 'reviewed', 'static'], }, - attack_id: String, - collections: [ collectionVersionSchema ] + created_by_user_account: String, + }, + attack_id: String, + collections: [collectionVersionSchema], }; // x-mitre-collection workspace structure const exportData = { - export_timestamp: Date, - bundle_id: String + export_timestamp: Date, + bundle_id: String, }; const exportDataSchema = new mongoose.Schema(exportData, { _id: false }); const importError = { - object_ref: { type: String, required: true }, - object_modified : { type: Date }, - error_type: { type: String, required: true }, - error_message: { type: String } + object_ref: { type: String, required: true }, + object_modified: { type: Date }, + error_type: { type: String, required: true }, + error_message: { type: String }, }; const importErrorSchema = new mongoose.Schema(importError, { _id: false }); @@ -48,45 +43,41 @@ const importErrorSchema = new mongoose.Schema(importError, { _id: false }); * Workspace property definition for collection objects */ const importCategories = { - additions: [ String ], - changes: [ String ], - minor_changes: [ String ], - revocations: [ String ], - deprecations: [ String ], - supersedes_user_edits: [ String ], - supersedes_collection_changes: [ String ], - duplicates: [ String ], - out_of_date: [ String ], - errors: [ importErrorSchema ] + additions: [String], + changes: [String], + minor_changes: [String], + revocations: [String], + deprecations: [String], + supersedes_user_edits: [String], + supersedes_collection_changes: [String], + duplicates: [String], + out_of_date: [String], + errors: [importErrorSchema], }; const importReferences = { - additions: [ String ], - changes: [ String ] + additions: [String], + changes: [String], }; const reimportData = { - imported: Date, - import_categories: importCategories, - import_references: importReferences + imported: Date, + import_categories: importCategories, + import_references: importReferences, }; module.exports.collection = { - imported: Date, - exported: [ exportDataSchema ], - import_categories: importCategories, - import_references: importReferences, - reimports: [ reimportData ], - workflow: { - state: { - type: String, - enum: [ - 'work-in-progress', - 'awaiting-review', - 'reviewed' - ] - }, - created_by_user_account: String, - release: Boolean - } + imported: Date, + exported: [exportDataSchema], + import_categories: importCategories, + import_references: importReferences, + reimports: [reimportData], + workflow: { + state: { + type: String, + enum: ['work-in-progress', 'awaiting-review', 'reviewed'], + }, + created_by_user_account: String, + release: Boolean, + }, }; diff --git a/app/models/system-configuration-model.js b/app/models/system-configuration-model.js index 12cbb1c5..7092518a 100644 --- a/app/models/system-configuration-model.js +++ b/app/models/system-configuration-model.js @@ -4,17 +4,19 @@ const mongoose = require('mongoose'); // Create the definition const systemConfigurationDefinition = { - organization_identity_ref: { type: String, required: true }, - anonymous_user_account_id: String, - default_marking_definitions: [ String ], - organization_namespace: { - range_start: { type: Number, default: null }, - prefix: { type: String, default: null } - } + organization_identity_ref: { type: String, required: true }, + anonymous_user_account_id: String, + default_marking_definitions: [String], + organization_namespace: { + range_start: { type: Number, default: null }, + prefix: { type: String, default: null }, + }, }; // Create the schema -const systemConfigurationSchema = new mongoose.Schema(systemConfigurationDefinition, { bufferCommands: false }); +const systemConfigurationSchema = new mongoose.Schema(systemConfigurationDefinition, { + bufferCommands: false, +}); // Create the model const SystemConfigurationModel = mongoose.model('SystemConfiguration', systemConfigurationSchema); diff --git a/app/models/tactic-model.js b/app/models/tactic-model.js index 2a7071ba..f0548208 100644 --- a/app/models/tactic-model.js +++ b/app/models/tactic-model.js @@ -5,26 +5,26 @@ const AttackObject = require('./attack-object-model'); const { ModelName } = require('../lib/model-names'); const stixTactic = { - // STIX x-mitre-tactic specific properties - modified: { type: Date, required: true }, - name: { type: String, required: true }, - description: String, + // STIX x-mitre-tactic specific properties + modified: { type: Date, required: true }, + name: { type: String, required: true }, + description: String, - // ATT&CK custom stix properties - x_mitre_modified_by_ref: String, - x_mitre_deprecated: Boolean, - x_mitre_domains: [ String ], - x_mitre_version: String, - x_mitre_attack_spec_version: String, - x_mitre_contributors: [ String ], - x_mitre_shortname: String + // ATT&CK custom stix properties + x_mitre_modified_by_ref: String, + x_mitre_deprecated: Boolean, + x_mitre_domains: [String], + x_mitre_version: String, + x_mitre_attack_spec_version: String, + x_mitre_contributors: [String], + x_mitre_shortname: String, }; // Create the definition const tacticDefinition = { - stix: { - ...stixTactic - } + stix: { + ...stixTactic, + }, }; // Create the schema diff --git a/app/models/team-model.js b/app/models/team-model.js index e3eeafc1..ac8ca2c7 100644 --- a/app/models/team-model.js +++ b/app/models/team-model.js @@ -4,17 +4,17 @@ const mongoose = require('mongoose'); // Create the definition const teamDefinition = { - id: { - type: String, - index: { - unique: true - } + id: { + type: String, + index: { + unique: true, }, - name: { type: String, required: true, unique: true }, - description: { type: String }, - userIDs: [ String ], - created: { type: Date, required: true }, - modified: { type: Date, required: true } + }, + name: { type: String, required: true, unique: true }, + description: { type: String }, + userIDs: [String], + created: { type: Date, required: true }, + modified: { type: Date, required: true }, }; // Create the schema diff --git a/app/models/technique-model.js b/app/models/technique-model.js index d35bdc28..709eec69 100644 --- a/app/models/technique-model.js +++ b/app/models/technique-model.js @@ -7,9 +7,9 @@ const { ModelName } = require('../lib/model-names'); // Create the definition const techniqueDefinition = { - stix: { - ...attackPatternDefinitions.attackPattern - } + stix: { + ...attackPatternDefinitions.attackPattern, + }, }; // Use Object.assign() to add properties in case there are duplicates Object.assign(techniqueDefinition.stix, attackPatternDefinitions.attackPatternEnterpriseDomain); diff --git a/app/models/user-account-model.js b/app/models/user-account-model.js index 016887b9..bfc8c1b1 100644 --- a/app/models/user-account-model.js +++ b/app/models/user-account-model.js @@ -4,19 +4,20 @@ const mongoose = require('mongoose'); // Create the definition const userAccountDefinition = { - id: { type: String, required: true }, - email: { - type: String, index: { - unique: true, - partialFilterExpression: { email: { $type: 'string' }} - } + id: { type: String, required: true }, + email: { + type: String, + index: { + unique: true, + partialFilterExpression: { email: { $type: 'string' } }, }, - username: { type: String, required: true }, - displayName: { type: String }, - status: { type: String, required: true }, - role: { type: String }, - created: { type: Date, required: true }, - modified: { type: Date, required: true } + }, + username: { type: String, required: true }, + displayName: { type: String }, + status: { type: String, required: true }, + role: { type: String }, + created: { type: Date, required: true }, + modified: { type: Date, required: true }, }; // Create the schema diff --git a/app/repository/_abstract.repository.js b/app/repository/_abstract.repository.js index 27688a3c..9ef841e9 100644 --- a/app/repository/_abstract.repository.js +++ b/app/repository/_abstract.repository.js @@ -4,88 +4,88 @@ const { NotImplementedError } = require('../exceptions'); class AbstractRepository { - /** - * @description Retrieves documents based on the provided options. - * @param {*} options Options object containing various filters and parameters for the search. - * @returns {Array} Array of aggregated documents. - */ - async retrieveAll(options) { - throw new NotImplementedError(this.constructor.name, 'retrieveAll'); - } + /** + * @description Retrieves documents based on the provided options. + * @param {*} options Options object containing various filters and parameters for the search. + * @returns {Array} Array of aggregated documents. + */ + async retrieveAll(options) { + throw new NotImplementedError(this.constructor.name, 'retrieveAll'); + } - /** - * @description Retrieves a document by its stixId. - * @param {*} stixId The unique identifier for the document. - * @returns {Object} The retrieved document. - */ - async retrieveOneByStixId(stixId) { - throw new NotImplementedError(this.constructor.name, 'retrieveOneById'); - } + /** + * @description Retrieves a document by its stixId. + * @param {*} stixId The unique identifier for the document. + * @returns {Object} The retrieved document. + */ + async retrieveOneByStixId(stixId) { + throw new NotImplementedError(this.constructor.name, 'retrieveOneById'); + } - /** - * @description Retrieves all documents by a specific stixId. - * @param {*} stixId The unique identifier for the documents. - * @returns {Array} Array of aggregated documents. - */ - async retrieveAllByStixId(stixId) { - throw new NotImplementedError(this.constructor.name, 'retrieveAllById'); - } + /** + * @description Retrieves all documents by a specific stixId. + * @param {*} stixId The unique identifier for the documents. + * @returns {Array} Array of aggregated documents. + */ + async retrieveAllByStixId(stixId) { + throw new NotImplementedError(this.constructor.name, 'retrieveAllById'); + } - /** - * @description Retrieves the latest document by its stixId. - * @param {*} stixId The unique identifier for the document. - * @returns {Object} The retrieved document. - */ - async retrieveLatestByStixId(stixId) { - throw new NotImplementedError(this.constructor.name, 'retrieveLatestByStixId'); - } + /** + * @description Retrieves the latest document by its stixId. + * @param {*} stixId The unique identifier for the document. + * @returns {Object} The retrieved document. + */ + async retrieveLatestByStixId(stixId) { + throw new NotImplementedError(this.constructor.name, 'retrieveLatestByStixId'); + } - /** - * @description Retrieves a document by its stixId and modification date. - * @param {*} stixId The unique identifier for the document. - * @param {*} modified The modification date for the document. - * @returns {Object} The retrieved document. - */ - async retrieveOneByVersion(stixId, modified) { - throw new NotImplementedError(this.constructor.name, 'retrieveOneByVersion'); - } + /** + * @description Retrieves a document by its stixId and modification date. + * @param {*} stixId The unique identifier for the document. + * @param {*} modified The modification date for the document. + * @returns {Object} The retrieved document. + */ + async retrieveOneByVersion(stixId, modified) { + throw new NotImplementedError(this.constructor.name, 'retrieveOneByVersion'); + } - /** - * @description Saves the provided data. - * @param {*} data The data to be saved. - * @returns {Object} The saved document. - */ - async save(data) { - throw new NotImplementedError(this.constructor.name, 'save'); - } + /** + * @description Saves the provided data. + * @param {*} data The data to be saved. + * @returns {Object} The saved document. + */ + async save(data) { + throw new NotImplementedError(this.constructor.name, 'save'); + } - /** - * @description Updates and saves the provided document. - * @param {*} document The document to be updated. - * @param {*} data The data for updating the document. - * @returns {Object} The updated and saved document. - */ - static async updateAndSave(document, data) { - throw new NotImplementedError(this.constructor.name, 'updateAndSave'); - } + /** + * @description Updates and saves the provided document. + * @param {*} document The document to be updated. + * @param {*} data The data for updating the document. + * @returns {Object} The updated and saved document. + */ + static async updateAndSave(document, data) { + throw new NotImplementedError(this.constructor.name, 'updateAndSave'); + } - /** - * @description Retrieves and removes a document by its stixId and modification date. - * @param {*} stixId The unique identifier for the document. - * @param {*} modified The modification date for the document. - * @returns {Object} The removed document. - */ - async findOneAndRemove(stixId, modified) { - throw new NotImplementedError(this.constructor.name, 'findOneAndRemove'); - } + /** + * @description Retrieves and removes a document by its stixId and modification date. + * @param {*} stixId The unique identifier for the document. + * @param {*} modified The modification date for the document. + * @returns {Object} The removed document. + */ + async findOneAndRemove(stixId, modified) { + throw new NotImplementedError(this.constructor.name, 'findOneAndRemove'); + } - /** - * @description Deletes many documents by a specific stixId. - * @param {*} stixId The unique identifier for the documents. - */ - async deleteMany(stixId) { - throw new NotImplementedError(this.constructor.name, 'deleteMany'); - } + /** + * @description Deletes many documents by a specific stixId. + * @param {*} stixId The unique identifier for the documents. + */ + async deleteMany(stixId) { + throw new NotImplementedError(this.constructor.name, 'deleteMany'); + } } module.exports = AbstractRepository; diff --git a/app/repository/_base.repository.js b/app/repository/_base.repository.js index 88bb23e5..8005ddcc 100644 --- a/app/repository/_base.repository.js +++ b/app/repository/_base.repository.js @@ -6,206 +6,197 @@ const { lastUpdatedByQueryHelper } = require('../lib/request-parameter-helper'); const { DatabaseError, DuplicateIdError, BadlyFormattedParameterError } = require('../exceptions'); class BaseRepository extends AbstractRepository { - constructor(model) { - super(); - this.model = model; - } - - async retrieveAll(options) { - try { - // Build the query - const query = {}; - - // Build the query - if (!options.includeRevoked) { - query['stix.revoked'] = { $in: [null, false] }; - } - if (!options.includeDeprecated) { - query['stix.x_mitre_deprecated'] = { $in: [null, false] }; - } - if (typeof options.state !== 'undefined') { - if (Array.isArray(options.state)) { - query['workspace.workflow.state'] = { $in: options.state }; - } - else { - query['workspace.workflow.state'] = options.state; - } - } - if (typeof options.domain !== 'undefined') { - if (Array.isArray(options.domain)) { - query['stix.x_mitre_domains'] = { $in: options.domain }; - } - else { - query['stix.x_mitre_domains'] = options.domain; - } - } - if (typeof options.platform !== 'undefined') { - if (Array.isArray(options.platform)) { - query['stix.x_mitre_platforms'] = { $in: options.platform }; - } - else { - query['stix.x_mitre_platforms'] = options.platform; - } - } - if (typeof options.lastUpdatedBy !== 'undefined') { - query['workspace.workflow.created_by_user_account'] = lastUpdatedByQueryHelper(options.lastUpdatedBy); - } - - // Build the aggregation - // - Group the documents by stix.id, sorted by stix.modified - // - Use the first document in each group (according to the value of stix.modified) - // - Then apply query, skip and limit options - const aggregation = [ - { $sort: { 'stix.id': 1, 'stix.modified': -1 } }, - { $group: { _id: '$stix.id', document: { $first: '$$ROOT' } } }, - { $replaceRoot: { newRoot: '$document' } }, - { $sort: { 'stix.id': 1 } }, - { $match: query } - ]; - - if (typeof options.search !== 'undefined') { - options.search = regexValidator.sanitizeRegex(options.search); - const match = { - $match: { - $or: [ - { 'stix.name': { '$regex': options.search, '$options': 'i' } }, - { 'stix.description': { '$regex': options.search, '$options': 'i' } }, - { 'workspace.attack_id': { '$regex': options.search, '$options': 'i' } } - ] - } - }; - aggregation.push(match); - } - - const facet = { - $facet: { - totalCount: [{ $count: 'totalCount' }], - documents: [] - } - }; - if (options.offset) { - facet.$facet.documents.push({ $skip: options.offset }); - } - else { - facet.$facet.documents.push({ $skip: 0 }); - } - if (options.limit) { - facet.$facet.documents.push({ $limit: options.limit }); - } - aggregation.push(facet); - - // Retrieve the documents - return await this.model.aggregate(aggregation).exec(); - } catch (err) { - throw new DatabaseError(err); + constructor(model) { + super(); + this.model = model; + } + + async retrieveAll(options) { + try { + // Build the query + const query = {}; + + // Build the query + if (!options.includeRevoked) { + query['stix.revoked'] = { $in: [null, false] }; + } + if (!options.includeDeprecated) { + query['stix.x_mitre_deprecated'] = { $in: [null, false] }; + } + if (typeof options.state !== 'undefined') { + if (Array.isArray(options.state)) { + query['workspace.workflow.state'] = { $in: options.state }; + } else { + query['workspace.workflow.state'] = options.state; } - } - - async retrieveOneById(stixId) { - try { - return await this.model.findOne({ 'stix.id': stixId }).exec(); - } catch (err) { - throw new DatabaseError(err); + } + if (typeof options.domain !== 'undefined') { + if (Array.isArray(options.domain)) { + query['stix.x_mitre_domains'] = { $in: options.domain }; + } else { + query['stix.x_mitre_domains'] = options.domain; } - } - - async retrieveAllById(stixId) { - try { - return await this.model.find({ 'stix.id': stixId }) - .sort('-stix.modified') - .lean() - .exec(); - } catch (err) { - throw new DatabaseError(err); + } + if (typeof options.platform !== 'undefined') { + if (Array.isArray(options.platform)) { + query['stix.x_mitre_platforms'] = { $in: options.platform }; + } else { + query['stix.x_mitre_platforms'] = options.platform; } + } + if (typeof options.lastUpdatedBy !== 'undefined') { + query['workspace.workflow.created_by_user_account'] = lastUpdatedByQueryHelper( + options.lastUpdatedBy, + ); + } + + // Build the aggregation + // - Group the documents by stix.id, sorted by stix.modified + // - Use the first document in each group (according to the value of stix.modified) + // - Then apply query, skip and limit options + const aggregation = [ + { $sort: { 'stix.id': 1, 'stix.modified': -1 } }, + { $group: { _id: '$stix.id', document: { $first: '$$ROOT' } } }, + { $replaceRoot: { newRoot: '$document' } }, + { $sort: { 'stix.id': 1 } }, + { $match: query }, + ]; + + if (typeof options.search !== 'undefined') { + options.search = regexValidator.sanitizeRegex(options.search); + const match = { + $match: { + $or: [ + { 'stix.name': { $regex: options.search, $options: 'i' } }, + { 'stix.description': { $regex: options.search, $options: 'i' } }, + { 'workspace.attack_id': { $regex: options.search, $options: 'i' } }, + ], + }, + }; + aggregation.push(match); + } + + const facet = { + $facet: { + totalCount: [{ $count: 'totalCount' }], + documents: [], + }, + }; + if (options.offset) { + facet.$facet.documents.push({ $skip: options.offset }); + } else { + facet.$facet.documents.push({ $skip: 0 }); + } + if (options.limit) { + facet.$facet.documents.push({ $limit: options.limit }); + } + aggregation.push(facet); + + // Retrieve the documents + return await this.model.aggregate(aggregation).exec(); + } catch (err) { + throw new DatabaseError(err); } + } - async retrieveLatestByStixId(stixId) { - try { - return await this.model.findOne({ 'stix.id': stixId }) - .sort('-stix.modified') - .lean() - .exec(); - } catch (err) { - throw new DatabaseError(err); - } + async retrieveOneById(stixId) { + try { + return await this.model.findOne({ 'stix.id': stixId }).exec(); + } catch (err) { + throw new DatabaseError(err); } + } - async retrieveOneByVersion(stixId, modified) { - try { - return await this.model.findOne({ 'stix.id': stixId, 'stix.modified': modified }) - .exec(); - } catch (err) { - if (err.name === 'CastError') { - throw new BadlyFormattedParameterError({ parameterName: 'stixId' }); - } else if (err.name === 'MongoServerError' && err.code === 11000) { - throw new DuplicateIdError(); - } - throw new DatabaseError(err); - } + async retrieveAllById(stixId) { + try { + return await this.model.find({ 'stix.id': stixId }).sort('-stix.modified').lean().exec(); + } catch (err) { + throw new DatabaseError(err); } + } - createNewDocument(data) { - return new this.model(data); + async retrieveLatestByStixId(stixId) { + try { + return await this.model.findOne({ 'stix.id': stixId }).sort('-stix.modified').lean().exec(); + } catch (err) { + throw new DatabaseError(err); } - - // eslint-disable-next-line class-methods-use-this - async saveDocument(document) { - try { - return await document.save(); - } - catch(err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - throw new DuplicateIdError({ - details: `Document with id '${ document.stix.id }' already exists.` - }); - } - throw new DatabaseError(err); - } + } + + async retrieveOneByVersion(stixId, modified) { + try { + return await this.model.findOne({ 'stix.id': stixId, 'stix.modified': modified }).exec(); + } catch (err) { + if (err.name === 'CastError') { + throw new BadlyFormattedParameterError({ parameterName: 'stixId' }); + } else if (err.name === 'MongoServerError' && err.code === 11000) { + throw new DuplicateIdError(); + } + throw new DatabaseError(err); } - - // eslint-disable-next-line class-methods-use-this - async save(data) { - try { - const document = new this.model(data); - return await document.save(); - } catch (err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - throw new DuplicateIdError({ - details: `Document with id '${data.stix.id}' already exists.` - }); - } - throw new DatabaseError(err); - } + } + + createNewDocument(data) { + return new this.model(data); + } + + // eslint-disable-next-line class-methods-use-this + async saveDocument(document) { + try { + return await document.save(); + } catch (err) { + if (err.name === 'MongoServerError' && err.code === 11000) { + throw new DuplicateIdError({ + details: `Document with id '${document.stix.id}' already exists.`, + }); + } + throw new DatabaseError(err); } - - // eslint-disable-next-line class-methods-use-this - async updateAndSave(document, data) { - try { - // TODO validate that document is valid mongoose object first - Object.assign(document, data); - return await document.save(); - } catch (err) { - throw new DatabaseError(err); - } + } + + // eslint-disable-next-line class-methods-use-this + async save(data) { + try { + const document = new this.model(data); + return await document.save(); + } catch (err) { + if (err.name === 'MongoServerError' && err.code === 11000) { + throw new DuplicateIdError({ + details: `Document with id '${data.stix.id}' already exists.`, + }); + } + throw new DatabaseError(err); } - - async findOneAndRemove(stixId, modified) { - try { - return await this.model.findOneAndRemove({ 'stix.id': stixId, 'stix.modified': modified }).exec(); - } catch (err) { - throw new DatabaseError(err); - } + } + + // eslint-disable-next-line class-methods-use-this + async updateAndSave(document, data) { + try { + // TODO validate that document is valid mongoose object first + Object.assign(document, data); + return await document.save(); + } catch (err) { + throw new DatabaseError(err); } + } + + async findOneAndRemove(stixId, modified) { + try { + return await this.model + .findOneAndRemove({ 'stix.id': stixId, 'stix.modified': modified }) + .exec(); + } catch (err) { + throw new DatabaseError(err); + } + } - async deleteMany(stixId) { - try { - return await this.model.deleteMany({ 'stix.id': stixId }).exec(); - } catch (err) { - throw new DatabaseError(err); - } + async deleteMany(stixId) { + try { + return await this.model.deleteMany({ 'stix.id': stixId }).exec(); + } catch (err) { + throw new DatabaseError(err); } - + } } module.exports = BaseRepository; diff --git a/app/repository/assets-repository.js b/app/repository/assets-repository.js index af99e3e3..dd11b790 100644 --- a/app/repository/assets-repository.js +++ b/app/repository/assets-repository.js @@ -6,91 +6,92 @@ const regexValidator = require('../lib/regex'); const { lastUpdatedByQueryHelper } = require('../lib/request-parameter-helper'); const { DatabaseError } = require('../exceptions'); -class AssetsRepository extends BaseRepository { - - async retrieveAll(options) { - try { - // Build the query - const query = {}; - if (!options.includeRevoked) { - query['stix.revoked'] = { $in: [null, false] }; - } - if (!options.includeDeprecated) { - query['stix.x_mitre_deprecated'] = { $in: [null, false] }; - } - if (typeof options.state !== 'undefined') { - if (Array.isArray(options.state)) { - query['workspace.workflow.state'] = { $in: options.state }; - } - else { - query['workspace.workflow.state'] = options.state; - } - } - if (typeof options.domain !== 'undefined') { - if (Array.isArray(options.domain)) { - query['stix.x_mitre_domains'] = { $in: options.domain }; - } - else { - query['stix.x_mitre_domains'] = options.domain; - } - } - if (typeof options.platform !== 'undefined') { - if (Array.isArray(options.platform)) { - query['stix.x_mitre_platforms'] = { $in: options.platform }; - } - else { - query['stix.x_mitre_platforms'] = options.platform; - } - } - if (typeof options.lastUpdatedBy !== 'undefined') { - query['workspace.workflow.created_by_user_account'] = lastUpdatedByQueryHelper(options.lastUpdatedBy); - } +class AssetsRepository extends BaseRepository { + async retrieveAll(options) { + try { + // Build the query + const query = {}; + if (!options.includeRevoked) { + query['stix.revoked'] = { $in: [null, false] }; + } + if (!options.includeDeprecated) { + query['stix.x_mitre_deprecated'] = { $in: [null, false] }; + } + if (typeof options.state !== 'undefined') { + if (Array.isArray(options.state)) { + query['workspace.workflow.state'] = { $in: options.state }; + } else { + query['workspace.workflow.state'] = options.state; + } + } + if (typeof options.domain !== 'undefined') { + if (Array.isArray(options.domain)) { + query['stix.x_mitre_domains'] = { $in: options.domain }; + } else { + query['stix.x_mitre_domains'] = options.domain; + } + } + if (typeof options.platform !== 'undefined') { + if (Array.isArray(options.platform)) { + query['stix.x_mitre_platforms'] = { $in: options.platform }; + } else { + query['stix.x_mitre_platforms'] = options.platform; + } + } + if (typeof options.lastUpdatedBy !== 'undefined') { + query['workspace.workflow.created_by_user_account'] = lastUpdatedByQueryHelper( + options.lastUpdatedBy, + ); + } - // Build the aggregation - // - Group the documents by stix.id, sorted by stix.modified - // - Use the last document in each group (according to the value of stix.modified) - // - Then apply query, skip and limit options - const aggregation = [ - { $sort: { 'stix.id': 1, 'stix.modified': 1 } }, - { $group: { _id: '$stix.id', document: { $last: '$$ROOT' }}}, - { $replaceRoot: { newRoot: '$document' }}, - { $sort: { 'stix.id': 1 }}, - { $match: query } - ]; + // Build the aggregation + // - Group the documents by stix.id, sorted by stix.modified + // - Use the last document in each group (according to the value of stix.modified) + // - Then apply query, skip and limit options + const aggregation = [ + { $sort: { 'stix.id': 1, 'stix.modified': 1 } }, + { $group: { _id: '$stix.id', document: { $last: '$$ROOT' } } }, + { $replaceRoot: { newRoot: '$document' } }, + { $sort: { 'stix.id': 1 } }, + { $match: query }, + ]; - if (typeof options.search !== 'undefined') { - options.search = regexValidator.sanitizeRegex(options.search); - const match = { $match: { $or: [ - { 'stix.name': { '$regex': options.search, '$options': 'i' }}, - { 'stix.description': { '$regex': options.search, '$options': 'i' }}, - { 'workspace.attack_id': { '$regex': options.search, '$options': 'i' }} - ]}}; - aggregation.push(match); - } + if (typeof options.search !== 'undefined') { + options.search = regexValidator.sanitizeRegex(options.search); + const match = { + $match: { + $or: [ + { 'stix.name': { $regex: options.search, $options: 'i' } }, + { 'stix.description': { $regex: options.search, $options: 'i' } }, + { 'workspace.attack_id': { $regex: options.search, $options: 'i' } }, + ], + }, + }; + aggregation.push(match); + } - const facet = { - $facet: { - totalCount: [ { $count: 'totalCount' }], - documents: [ ] - } - }; - if (options.offset) { - facet.$facet.documents.push({ $skip: options.offset }); - } - else { - facet.$facet.documents.push({ $skip: 0 }); - } - if (options.limit) { - facet.$facet.documents.push({ $limit: options.limit }); - } - aggregation.push(facet); + const facet = { + $facet: { + totalCount: [{ $count: 'totalCount' }], + documents: [], + }, + }; + if (options.offset) { + facet.$facet.documents.push({ $skip: options.offset }); + } else { + facet.$facet.documents.push({ $skip: 0 }); + } + if (options.limit) { + facet.$facet.documents.push({ $limit: options.limit }); + } + aggregation.push(facet); - // Retrieve the documents - return await this.model.aggregate(aggregation).exec(); - } catch (err) { - throw new DatabaseError(err); - } + // Retrieve the documents + return await this.model.aggregate(aggregation).exec(); + } catch (err) { + throw new DatabaseError(err); } + } } -module.exports = new AssetsRepository(Asset); \ No newline at end of file +module.exports = new AssetsRepository(Asset); diff --git a/app/repository/attack-objects-repository.js b/app/repository/attack-objects-repository.js index 86000199..4db0226f 100644 --- a/app/repository/attack-objects-repository.js +++ b/app/repository/attack-objects-repository.js @@ -7,96 +7,94 @@ const { lastUpdatedByQueryHelper } = require('../lib/request-parameter-helper'); const regexValidator = require('../lib/regex'); class AttackObjectsRepository extends BaseRepository { + errors = { + missingParameter: 'Missing required parameter', + badlyFormattedParameter: 'Badly formatted parameter', + duplicateId: 'Duplicate id', + notFound: 'Document not found', + invalidQueryStringParameter: 'Invalid query string parameter', + duplicateCollection: 'Duplicate collection', + }; - errors = { - missingParameter: 'Missing required parameter', - badlyFormattedParameter: 'Badly formatted parameter', - duplicateId: 'Duplicate id', - notFound: 'Document not found', - invalidQueryStringParameter: 'Invalid query string parameter', - duplicateCollection: 'Duplicate collection' - }; - - identitiesService; + identitiesService; - async retrieveAll(options) { - // Build the query - const query = {}; - if (typeof options.attackId !== 'undefined') { - if (Array.isArray(options.attackId)) { - query['workspace.attack_id'] = { $in: options.attackId }; - } - else { - query['workspace.attack_id'] = options.attackId; - } - } - if (!options.includeRevoked) { - query['stix.revoked'] = { $in: [null, false] }; - } - if (!options.includeDeprecated) { - query['stix.x_mitre_deprecated'] = { $in: [null, false] }; - } - if (typeof options.state !== 'undefined') { - if (Array.isArray(options.state)) { - query['workspace.workflow.state'] = { $in: options.state }; - } - else { - query['workspace.workflow.state'] = options.state; - } - } - - if (typeof options.lastUpdatedBy !== 'undefined') { - query['workspace.workflow.created_by_user_account'] = lastUpdatedByQueryHelper(options.lastUpdatedBy); - } + async retrieveAll(options) { + // Build the query + const query = {}; + if (typeof options.attackId !== 'undefined') { + if (Array.isArray(options.attackId)) { + query['workspace.attack_id'] = { $in: options.attackId }; + } else { + query['workspace.attack_id'] = options.attackId; + } + } + if (!options.includeRevoked) { + query['stix.revoked'] = { $in: [null, false] }; + } + if (!options.includeDeprecated) { + query['stix.x_mitre_deprecated'] = { $in: [null, false] }; + } + if (typeof options.state !== 'undefined') { + if (Array.isArray(options.state)) { + query['workspace.workflow.state'] = { $in: options.state }; + } else { + query['workspace.workflow.state'] = options.state; + } + } - // Build the aggregation - const aggregation = []; - if (options.versions === 'latest') { - // - Group the documents by stix.id, sorted by stix.modified - // - Use the first document in each group (according to the value of stix.modified) - aggregation.push({ $sort: { 'stix.id': 1, 'stix.modified': -1 } }); - aggregation.push({ $group: { _id: '$stix.id', document: { $first: '$$ROOT' } } }); - aggregation.push({ $replaceRoot: { newRoot: '$document' } }); - } + if (typeof options.lastUpdatedBy !== 'undefined') { + query['workspace.workflow.created_by_user_account'] = lastUpdatedByQueryHelper( + options.lastUpdatedBy, + ); + } - // - Then apply query, skip and limit options - aggregation.push({ $sort: { 'stix.id': 1 } }); - aggregation.push({ $match: query }); + // Build the aggregation + const aggregation = []; + if (options.versions === 'latest') { + // - Group the documents by stix.id, sorted by stix.modified + // - Use the first document in each group (according to the value of stix.modified) + aggregation.push({ $sort: { 'stix.id': 1, 'stix.modified': -1 } }); + aggregation.push({ $group: { _id: '$stix.id', document: { $first: '$$ROOT' } } }); + aggregation.push({ $replaceRoot: { newRoot: '$document' } }); + } - if (typeof options.search !== 'undefined') { - options.search = regexValidator.sanitizeRegex(options.search); - const match = { - $match: { - $or: [ - { 'workspace.attack_id': { '$regex': options.search, '$options': 'i' }}, - { 'stix.name': { '$regex': options.search, '$options': 'i' } }, - { 'stix.description': { '$regex': options.search, '$options': 'i' } } - ] - } - }; - aggregation.push(match); - } + // - Then apply query, skip and limit options + aggregation.push({ $sort: { 'stix.id': 1 } }); + aggregation.push({ $match: query }); - const facet = { - $facet: { - totalCount: [{ $count: 'totalCount' }], - documents: [] - } - }; - if (options.offset) { - facet.$facet.documents.push({ $skip: options.offset }); - } - else { - facet.$facet.documents.push({ $skip: 0 }); - } - if (options.limit) { - facet.$facet.documents.push({ $limit: options.limit }); - } - aggregation.push(facet); + if (typeof options.search !== 'undefined') { + options.search = regexValidator.sanitizeRegex(options.search); + const match = { + $match: { + $or: [ + { 'workspace.attack_id': { $regex: options.search, $options: 'i' } }, + { 'stix.name': { $regex: options.search, $options: 'i' } }, + { 'stix.description': { $regex: options.search, $options: 'i' } }, + ], + }, + }; + aggregation.push(match); + } - // Retrieve the documents - return await this.model.aggregate(aggregation).exec(); + const facet = { + $facet: { + totalCount: [{ $count: 'totalCount' }], + documents: [], + }, + }; + if (options.offset) { + facet.$facet.documents.push({ $skip: options.offset }); + } else { + facet.$facet.documents.push({ $skip: 0 }); + } + if (options.limit) { + facet.$facet.documents.push({ $limit: options.limit }); } + aggregation.push(facet); + + // Retrieve the documents + return await this.model.aggregate(aggregation).exec(); + } } -module.exports = new AttackObjectsRepository(AttackObject); \ No newline at end of file +module.exports = new AttackObjectsRepository(AttackObject); diff --git a/app/repository/campaigns-repository.js b/app/repository/campaigns-repository.js index f98f6214..aa1cd129 100644 --- a/app/repository/campaigns-repository.js +++ b/app/repository/campaigns-repository.js @@ -3,6 +3,6 @@ const BaseRepository = require('./_base.repository'); const Campaign = require('../models/campaign-model'); -class CampaignRepository extends BaseRepository { } +class CampaignRepository extends BaseRepository {} -module.exports = new CampaignRepository(Campaign); \ No newline at end of file +module.exports = new CampaignRepository(Campaign); diff --git a/app/repository/data-components-repository.js b/app/repository/data-components-repository.js index 5729c336..0490d15c 100644 --- a/app/repository/data-components-repository.js +++ b/app/repository/data-components-repository.js @@ -5,4 +5,4 @@ const DataComponent = require('../models/data-component-model'); class DataComponentsRepository extends BaseRepository {} -module.exports = new DataComponentsRepository(DataComponent); \ No newline at end of file +module.exports = new DataComponentsRepository(DataComponent); diff --git a/app/repository/data-sources-repository.js b/app/repository/data-sources-repository.js index f44865bf..989683c5 100644 --- a/app/repository/data-sources-repository.js +++ b/app/repository/data-sources-repository.js @@ -3,6 +3,6 @@ const BaseRepository = require('./_base.repository'); const DataSource = require('../models/data-source-model'); -class DataSourcesRepository extends BaseRepository { } +class DataSourcesRepository extends BaseRepository {} module.exports = new DataSourcesRepository(DataSource); diff --git a/app/repository/groups-repository.js b/app/repository/groups-repository.js index 12a67479..00d3d041 100644 --- a/app/repository/groups-repository.js +++ b/app/repository/groups-repository.js @@ -3,6 +3,6 @@ const BaseRepository = require('./_base.repository'); const Group = require('../models/group-model'); -class GroupsRepository extends BaseRepository { } +class GroupsRepository extends BaseRepository {} module.exports = new GroupsRepository(Group); diff --git a/app/repository/identities-repository.js b/app/repository/identities-repository.js index 5690e513..f230a0dc 100644 --- a/app/repository/identities-repository.js +++ b/app/repository/identities-repository.js @@ -3,6 +3,6 @@ const BaseRepository = require('./_base.repository'); const Identity = require('../models/identity-model'); -class IdentitiesRepository extends BaseRepository { } +class IdentitiesRepository extends BaseRepository {} -module.exports = new IdentitiesRepository(Identity); \ No newline at end of file +module.exports = new IdentitiesRepository(Identity); diff --git a/app/repository/marking-definitions-repository.js b/app/repository/marking-definitions-repository.js index ac9c30fb..c37075fc 100644 --- a/app/repository/marking-definitions-repository.js +++ b/app/repository/marking-definitions-repository.js @@ -5,15 +5,13 @@ const MarkingDefinition = require('../models/marking-definition-model'); const { DatabaseError } = require('../exceptions'); class MarkingDefinitionsRepository extends BaseRepository { - - async deleteOneById(stixId) { - try { - return await this.model.findOneAndRemove({ 'stix.id': stixId }).exec(); - } - catch (err) { - throw new DatabaseError(err); - } + async deleteOneById(stixId) { + try { + return await this.model.findOneAndRemove({ 'stix.id': stixId }).exec(); + } catch (err) { + throw new DatabaseError(err); } + } } -module.exports = new MarkingDefinitionsRepository(MarkingDefinition); \ No newline at end of file +module.exports = new MarkingDefinitionsRepository(MarkingDefinition); diff --git a/app/repository/matrix-repository.js b/app/repository/matrix-repository.js index db14f2fb..088a0a46 100644 --- a/app/repository/matrix-repository.js +++ b/app/repository/matrix-repository.js @@ -3,6 +3,6 @@ const BaseRepository = require('./_base.repository'); const Matrix = require('../models/matrix-model'); -class MatrixRepository extends BaseRepository { } +class MatrixRepository extends BaseRepository {} -module.exports = new MatrixRepository(Matrix); \ No newline at end of file +module.exports = new MatrixRepository(Matrix); diff --git a/app/repository/mitigations-repository.js b/app/repository/mitigations-repository.js index 1b78a966..2aa5b646 100644 --- a/app/repository/mitigations-repository.js +++ b/app/repository/mitigations-repository.js @@ -3,6 +3,6 @@ const BaseRepository = require('./_base.repository'); const Mitigation = require('../models/mitigation-model'); -class MitigationsRepository extends BaseRepository { } +class MitigationsRepository extends BaseRepository {} -module.exports = new MitigationsRepository(Mitigation); \ No newline at end of file +module.exports = new MitigationsRepository(Mitigation); diff --git a/app/repository/notes-repository.js b/app/repository/notes-repository.js index 77b0fcbd..41a523e6 100644 --- a/app/repository/notes-repository.js +++ b/app/repository/notes-repository.js @@ -7,74 +7,78 @@ const { lastUpdatedByQueryHelper } = require('../lib/request-parameter-helper'); const { DatabaseError } = require('../exceptions'); class NotesRepository extends BaseRepository { - async retrieveAll(options) { - try { - // Build the query - const query = {}; + async retrieveAll(options) { + try { + // Build the query + const query = {}; - // Build the query - if (!options.includeRevoked) { - query['stix.revoked'] = { $in: [null, false] }; - } - if (!options.includeDeprecated) { - query['stix.x_mitre_deprecated'] = { $in: [null, false] }; - } - if (typeof options.state !== 'undefined') { - if (Array.isArray(options.state)) { - query['workspace.workflow.state'] = { $in: options.state }; - } - else { - query['workspace.workflow.state'] = options.state; - } - } - if (typeof options.lastUpdatedBy !== 'undefined') { - query['workspace.workflow.created_by_user_account'] = lastUpdatedByQueryHelper(options.lastUpdatedBy); - } + // Build the query + if (!options.includeRevoked) { + query['stix.revoked'] = { $in: [null, false] }; + } + if (!options.includeDeprecated) { + query['stix.x_mitre_deprecated'] = { $in: [null, false] }; + } + if (typeof options.state !== 'undefined') { + if (Array.isArray(options.state)) { + query['workspace.workflow.state'] = { $in: options.state }; + } else { + query['workspace.workflow.state'] = options.state; + } + } + if (typeof options.lastUpdatedBy !== 'undefined') { + query['workspace.workflow.created_by_user_account'] = lastUpdatedByQueryHelper( + options.lastUpdatedBy, + ); + } - // Build the aggregation - // - Group the documents by stix.id, sorted by stix.modified - // - Use the first document in each group (according to the value of stix.modified) - // - Then apply query, skip and limit options - const aggregation = [ - { $sort: { 'stix.id': 1, 'stix.modified': -1 } }, - { $group: { _id: '$stix.id', document: { $first: '$$ROOT' } } }, - { $replaceRoot: { newRoot: '$document' } }, - { $sort: { 'stix.id': 1 } }, - { $match: query } - ]; + // Build the aggregation + // - Group the documents by stix.id, sorted by stix.modified + // - Use the first document in each group (according to the value of stix.modified) + // - Then apply query, skip and limit options + const aggregation = [ + { $sort: { 'stix.id': 1, 'stix.modified': -1 } }, + { $group: { _id: '$stix.id', document: { $first: '$$ROOT' } } }, + { $replaceRoot: { newRoot: '$document' } }, + { $sort: { 'stix.id': 1 } }, + { $match: query }, + ]; - if (typeof options.search !== 'undefined') { - options.search = regexValidator.sanitizeRegex(options.search); - const match = { $match: { $or: [ - { 'stix.abstract': { '$regex': options.search, '$options': 'i' }}, - { 'stix.content': { '$regex': options.search, '$options': 'i' }} - ]}}; - aggregation.push(match); - } + if (typeof options.search !== 'undefined') { + options.search = regexValidator.sanitizeRegex(options.search); + const match = { + $match: { + $or: [ + { 'stix.abstract': { $regex: options.search, $options: 'i' } }, + { 'stix.content': { $regex: options.search, $options: 'i' } }, + ], + }, + }; + aggregation.push(match); + } - const facet = { - $facet: { - totalCount: [{ $count: 'totalCount' }], - documents: [] - } - }; - if (options.offset) { - facet.$facet.documents.push({ $skip: options.offset }); - } - else { - facet.$facet.documents.push({ $skip: 0 }); - } - if (options.limit) { - facet.$facet.documents.push({ $limit: options.limit }); - } - aggregation.push(facet); + const facet = { + $facet: { + totalCount: [{ $count: 'totalCount' }], + documents: [], + }, + }; + if (options.offset) { + facet.$facet.documents.push({ $skip: options.offset }); + } else { + facet.$facet.documents.push({ $skip: 0 }); + } + if (options.limit) { + facet.$facet.documents.push({ $limit: options.limit }); + } + aggregation.push(facet); - // Retrieve the documents - return await this.model.aggregate(aggregation).exec(); - } catch (err) { - throw new DatabaseError(err); - } + // Retrieve the documents + return await this.model.aggregate(aggregation).exec(); + } catch (err) { + throw new DatabaseError(err); } + } } -module.exports = new NotesRepository(Note); \ No newline at end of file +module.exports = new NotesRepository(Note); diff --git a/app/repository/references-repository.js b/app/repository/references-repository.js index 202cc473..fa333a31 100644 --- a/app/repository/references-repository.js +++ b/app/repository/references-repository.js @@ -1,111 +1,103 @@ 'use strict'; - const Reference = require('../models/reference-model'); - const { BadlyFormattedParameterError, DuplicateIdError, DatabaseError } = require('../exceptions'); +const Reference = require('../models/reference-model'); +const { BadlyFormattedParameterError, DuplicateIdError, DatabaseError } = require('../exceptions'); class ReferencesRepository { + constructor(model) { + this.model = model; + } - constructor(model) { - this.model = model; + async retrieveAll(options) { + // Build the text search + let textSearch; + if (typeof options.search !== 'undefined') { + textSearch = { $text: { $search: options.search } }; } - async retrieveAll(options) { - // Build the text search - let textSearch; - if (typeof options.search !== 'undefined') { - textSearch = { $text: { $search: options.search }}; - } - - // Build the query - const query = {}; - if (typeof options.sourceName !== 'undefined') { - query['source_name'] = options.sourceName; - } - - // Build the aggregation - const aggregation = []; - if (textSearch) { - aggregation.push({ $match: textSearch }); - } - - aggregation.push({ $sort: { 'source_name': 1 }}); - aggregation.push({ $match: query }); - - const facet = { - $facet: { - totalCount: [ { $count: 'totalCount' }], - documents: [ ] - } - }; - if (options.offset) { - facet.$facet.documents.push({ $skip: options.offset }); - } - else { - facet.$facet.documents.push({ $skip: 0 }); - } - if (options.limit) { - facet.$facet.documents.push({ $limit: options.limit }); - } - aggregation.push(facet); - - // Retrieve the documents - return await this.model.aggregate(aggregation).exec(); + // Build the query + const query = {}; + if (typeof options.sourceName !== 'undefined') { + query['source_name'] = options.sourceName; } - - async save(data) { - // Create the document - const reference = new this.model(data); - - // Save the document in the database - try { - const savedReference = await reference.save(); - return savedReference; - } - catch(err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - // 11000 = Duplicate index - throw new DuplicateIdError({ - details: `Reference with source_name '${ data.source_name }' already exists.` - }); - } - else { - throw new DatabaseError(err); - } - } + + // Build the aggregation + const aggregation = []; + if (textSearch) { + aggregation.push({ $match: textSearch }); + } + + aggregation.push({ $sort: { source_name: 1 } }); + aggregation.push({ $match: query }); + + const facet = { + $facet: { + totalCount: [{ $count: 'totalCount' }], + documents: [], + }, + }; + if (options.offset) { + facet.$facet.documents.push({ $skip: options.offset }); + } else { + facet.$facet.documents.push({ $skip: 0 }); + } + if (options.limit) { + facet.$facet.documents.push({ $limit: options.limit }); } - - async updateAndSave(data) { - try { - const document = await this.model.findOne({ 'source_name': data.source_name }).exec(); - if (!document) { - // document not found - return null; - } - else { - // Copy data to found document and save - Object.assign(document, data); - const savedDocument = await document.save(); - return savedDocument; - } - } - catch(err) { - if (err.name === 'CastError') { - throw new BadlyFormattedParameterError({ parameterName: 'source_name' }); - } - else { - throw new DatabaseError(err); - } - } + aggregation.push(facet); + + // Retrieve the documents + return await this.model.aggregate(aggregation).exec(); + } + + async save(data) { + // Create the document + const reference = new this.model(data); + + // Save the document in the database + try { + const savedReference = await reference.save(); + return savedReference; + } catch (err) { + if (err.name === 'MongoServerError' && err.code === 11000) { + // 11000 = Duplicate index + throw new DuplicateIdError({ + details: `Reference with source_name '${data.source_name}' already exists.`, + }); + } else { + throw new DatabaseError(err); + } + } + } + + async updateAndSave(data) { + try { + const document = await this.model.findOne({ source_name: data.source_name }).exec(); + if (!document) { + // document not found + return null; + } else { + // Copy data to found document and save + Object.assign(document, data); + const savedDocument = await document.save(); + return savedDocument; + } + } catch (err) { + if (err.name === 'CastError') { + throw new BadlyFormattedParameterError({ parameterName: 'source_name' }); + } else { + throw new DatabaseError(err); + } } + } - async findOneAndRemove(sourceName) { - try { - return await this.model.findOneAndRemove({ 'source_name': sourceName }).exec(); - } - catch(err) { - throw new DatabaseError(err); - } + async findOneAndRemove(sourceName) { + try { + return await this.model.findOneAndRemove({ source_name: sourceName }).exec(); + } catch (err) { + throw new DatabaseError(err); } + } } -module.exports = new ReferencesRepository(Reference); \ No newline at end of file +module.exports = new ReferencesRepository(Reference); diff --git a/app/repository/relationships-repository.js b/app/repository/relationships-repository.js index ca2e99de..f1943868 100644 --- a/app/repository/relationships-repository.js +++ b/app/repository/relationships-repository.js @@ -5,110 +5,105 @@ const Relationship = require('../models/relationship-model'); const { DatabaseError } = require('../exceptions'); class RelationshipsRepository extends BaseRepository { - /** - * Extends BaseRepository.retrieveAll() to include relationship-specific query parameters - * and data lookup functionality. - * - * Additional options supported beyond base implementation: - * @param {Object} options - * @param {string} options.sourceRef - Filter by source reference - * @param {string} options.targetRef - Filter by target reference - * @param {string} options.sourceOrTargetRef - Filter by either source or target reference - * @param {string} options.relationshipType - Filter by relationship type - * @param {boolean} options.lookupRefs - Include source/target object data via $lookup - * - * @returns {Promise} Array of relationship documents with optional source/target data - */ - async retrieveAll(options) { - try { - const query = this._buildBaseQuery(options); - const aggregation = RelationshipsRepository._buildAggregation(options, query); - return await this.model.aggregate(aggregation).exec(); - } catch (err) { - throw new DatabaseError(err); - } + /** + * Extends BaseRepository.retrieveAll() to include relationship-specific query parameters + * and data lookup functionality. + * + * Additional options supported beyond base implementation: + * @param {Object} options + * @param {string} options.sourceRef - Filter by source reference + * @param {string} options.targetRef - Filter by target reference + * @param {string} options.sourceOrTargetRef - Filter by either source or target reference + * @param {string} options.relationshipType - Filter by relationship type + * @param {boolean} options.lookupRefs - Include source/target object data via $lookup + * + * @returns {Promise} Array of relationship documents with optional source/target data + */ + async retrieveAll(options) { + try { + const query = this._buildBaseQuery(options); + const aggregation = RelationshipsRepository._buildAggregation(options, query); + return await this.model.aggregate(aggregation).exec(); + } catch (err) { + throw new DatabaseError(err); } + } - _buildBaseQuery(options) { - const query = super._buildBaseQuery(options); + _buildBaseQuery(options) { + const query = super._buildBaseQuery(options); - if (options.sourceRef) { - query['stix.source_ref'] = options.sourceRef; - } - if (options.targetRef) { - query['stix.target_ref'] = options.targetRef; - } - if (options.sourceOrTargetRef) { - query.$or = [ - { 'stix.source_ref': options.sourceOrTargetRef }, - { 'stix.target_ref': options.sourceOrTargetRef } - ]; - } - if (options.relationshipType) { - query['stix.relationship_type'] = options.relationshipType; - } - return query; + if (options.sourceRef) { + query['stix.source_ref'] = options.sourceRef; } + if (options.targetRef) { + query['stix.target_ref'] = options.targetRef; + } + if (options.sourceOrTargetRef) { + query.$or = [ + { 'stix.source_ref': options.sourceOrTargetRef }, + { 'stix.target_ref': options.sourceOrTargetRef }, + ]; + } + if (options.relationshipType) { + query['stix.relationship_type'] = options.relationshipType; + } + return query; + } - static _buildAggregation(options, query) { - const aggregation = [ - { $sort: { 'stix.id': 1, 'stix.modified': -1 } } - ]; - - if (options.versions === 'latest') { - aggregation.push( - { $group: { _id: '$stix.id', document: { $first: '$$ROOT' } } }, - { $replaceRoot: { newRoot: '$document' } } - ); - } + static _buildAggregation(options, query) { + const aggregation = [{ $sort: { 'stix.id': 1, 'stix.modified': -1 } }]; - aggregation.push( - { $sort: { 'stix.id': 1 } }, - { $match: query } - ); + if (options.versions === 'latest') { + aggregation.push( + { $group: { _id: '$stix.id', document: { $first: '$$ROOT' } } }, + { $replaceRoot: { newRoot: '$document' } }, + ); + } - if (options.lookupRefs) { - aggregation.push( - { - $lookup: { - from: 'attackObjects', - localField: 'stix.source_ref', - foreignField: 'stix.id', - as: 'source_objects' - } - }, - { - $lookup: { - from: 'attackObjects', - localField: 'stix.target_ref', - foreignField: 'stix.id', - as: 'target_objects' - } - } - ); - } + aggregation.push({ $sort: { 'stix.id': 1 } }, { $match: query }); - return RelationshipsRepository._addPaginationStages(aggregation, options); + if (options.lookupRefs) { + aggregation.push( + { + $lookup: { + from: 'attackObjects', + localField: 'stix.source_ref', + foreignField: 'stix.id', + as: 'source_objects', + }, + }, + { + $lookup: { + from: 'attackObjects', + localField: 'stix.target_ref', + foreignField: 'stix.id', + as: 'target_objects', + }, + }, + ); } - static _addPaginationStages(aggregation, options) { - const facet = { - $facet: { - totalCount: [{ $count: 'totalCount' }], - documents: [] - } - }; + return RelationshipsRepository._addPaginationStages(aggregation, options); + } - if (options.offset) { - facet.$facet.documents.push({ $skip: options.offset }); - } - if (options.limit) { - facet.$facet.documents.push({ $limit: options.limit }); - } + static _addPaginationStages(aggregation, options) { + const facet = { + $facet: { + totalCount: [{ $count: 'totalCount' }], + documents: [], + }, + }; - aggregation.push(facet); - return aggregation; + if (options.offset) { + facet.$facet.documents.push({ $skip: options.offset }); + } + if (options.limit) { + facet.$facet.documents.push({ $limit: options.limit }); } + + aggregation.push(facet); + return aggregation; + } } -module.exports = new RelationshipsRepository(Relationship); \ No newline at end of file +module.exports = new RelationshipsRepository(Relationship); diff --git a/app/repository/software-repository.js b/app/repository/software-repository.js index a1aeb200..aefb81ae 100644 --- a/app/repository/software-repository.js +++ b/app/repository/software-repository.js @@ -3,6 +3,6 @@ const BaseRepository = require('./_base.repository'); const Software = require('../models/software-model'); -class SoftwareRepository extends BaseRepository { } +class SoftwareRepository extends BaseRepository {} -module.exports = new SoftwareRepository(Software); \ No newline at end of file +module.exports = new SoftwareRepository(Software); diff --git a/app/repository/system-configurations-repository.js b/app/repository/system-configurations-repository.js index 3ee017b7..ede51662 100644 --- a/app/repository/system-configurations-repository.js +++ b/app/repository/system-configurations-repository.js @@ -1,34 +1,31 @@ - const SystemConfiguration = require('../models/system-configuration-model'); const { DatabaseError } = require('../exceptions'); class SystemConfigurationsRepository { - constructor(model) { - this.model = model; - } + constructor(model) { + this.model = model; + } - createNewDocument(data) { - return new this.model(data); - } + createNewDocument(data) { + return new this.model(data); + } - static async saveDocument(document) { - try { - return await document.save(); - } - catch(err) { - throw new DatabaseError(err); - } + static async saveDocument(document) { + try { + return await document.save(); + } catch (err) { + throw new DatabaseError(err); } + } - async retrieveOne(options) { - options = options ?? {}; - if (options.lean) { - return await this.model.findOne().lean(); - } - else { - return await this.model.findOne(); - } + async retrieveOne(options) { + options = options ?? {}; + if (options.lean) { + return await this.model.findOne().lean(); + } else { + return await this.model.findOne(); } + } } -module.exports = new SystemConfigurationsRepository(SystemConfiguration); \ No newline at end of file +module.exports = new SystemConfigurationsRepository(SystemConfiguration); diff --git a/app/repository/tactics-repository.js b/app/repository/tactics-repository.js index fca89d8e..0e7b9eda 100644 --- a/app/repository/tactics-repository.js +++ b/app/repository/tactics-repository.js @@ -3,6 +3,6 @@ const BaseRepository = require('./_base.repository'); const Tactic = require('../models/tactic-model'); -class TacticsRepository extends BaseRepository { } +class TacticsRepository extends BaseRepository {} module.exports = new TacticsRepository(Tactic); diff --git a/app/repository/teams-repository.js b/app/repository/teams-repository.js index d0a3fd57..a8d9c739 100644 --- a/app/repository/teams-repository.js +++ b/app/repository/teams-repository.js @@ -2,147 +2,148 @@ const Team = require('../models/team-model'); const { - BadlyFormattedParameterError, - MissingParameterError, - DuplicateIdError, - DatabaseError, DuplicateNameError + BadlyFormattedParameterError, + MissingParameterError, + DuplicateIdError, + DatabaseError, + DuplicateNameError, } = require('../exceptions'); const regexValidator = require('../lib/regex'); class TeamsRepository { - constructor(model) { - this.model = model; - } + constructor(model) { + this.model = model; + } - retrieveByUserId(userAccountId, options) { - const aggregation = [ - { $sort: { 'name': 1 } }, - { $match: { userIDs: { $in: [userAccountId] } } }, - { - $facet: { - totalCount: [{ $count: 'totalCount' }], - documents: [ - { $skip: options.offset || 0 }, - ...options.limit ? [{ $limit: options.limit }] : [] - ] - } - } - ]; - - return this.model.aggregate(aggregation).exec(); - } + retrieveByUserId(userAccountId, options) { + const aggregation = [ + { $sort: { name: 1 } }, + { $match: { userIDs: { $in: [userAccountId] } } }, + { + $facet: { + totalCount: [{ $count: 'totalCount' }], + documents: [ + { $skip: options.offset || 0 }, + ...(options.limit ? [{ $limit: options.limit }] : []), + ], + }, + }, + ]; + + return this.model.aggregate(aggregation).exec(); + } + + async retrieveAll(options) { + // Build the aggregation + const aggregation = [{ $sort: { name: 1 } }]; - async retrieveAll(options) { - // Build the aggregation - const aggregation = [ - { $sort: { 'name': 1 } }, - ]; - - if (typeof options.search !== 'undefined') { - options.search = regexValidator.sanitizeRegex(options.search); - const match = { $match: { $or: [ - { 'name': { '$regex': options.search, '$options': 'i' }}, - { 'description': { '$regex': options.search, '$options': 'i' }}, - ]}}; - aggregation.push(match); - } - - const facet = { - $facet: { - totalCount: [ { $count: 'totalCount' }], - documents: [ ] - } - }; - if (options.offset) { - facet.$facet.documents.push({ $skip: options.offset }); - } - else { - facet.$facet.documents.push({ $skip: 0 }); - } - if (options.limit) { - facet.$facet.documents.push({ $limit: options.limit }); - } - aggregation.push(facet); - - // Retrieve the documents - return await this.model.aggregate(aggregation); + if (typeof options.search !== 'undefined') { + options.search = regexValidator.sanitizeRegex(options.search); + const match = { + $match: { + $or: [ + { name: { $regex: options.search, $options: 'i' } }, + { description: { $regex: options.search, $options: 'i' } }, + ], + }, + }; + aggregation.push(match); } - async retrieveById(teamId) { - try { - if (!teamId) { - throw new MissingParameterError; - } - - const team = await this.model.findOne({ 'id': teamId }).lean().exec(); - - return team; - } - catch (err) { - if (err.name === 'CastError') { - throw new BadlyFormattedParameterError; - } else { - throw err; - } - } + const facet = { + $facet: { + totalCount: [{ $count: 'totalCount' }], + documents: [], + }, + }; + if (options.offset) { + facet.$facet.documents.push({ $skip: options.offset }); + } else { + facet.$facet.documents.push({ $skip: 0 }); } + if (options.limit) { + facet.$facet.documents.push({ $limit: options.limit }); + } + aggregation.push(facet); + + // Retrieve the documents + return await this.model.aggregate(aggregation); + } + + async retrieveById(teamId) { + try { + if (!teamId) { + throw new MissingParameterError(); + } + + const team = await this.model.findOne({ id: teamId }).lean().exec(); - createNewDocument(data) { - return new this.model(data); + return team; + } catch (err) { + if (err.name === 'CastError') { + throw new BadlyFormattedParameterError(); + } else { + throw err; + } } + } - static async saveDocument(document) { - try { - return await document.save(); - } - catch(err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - throw new DuplicateIdError({ - details: `Document with id '${ document.id }' already exists.` - }); - } - throw new DatabaseError(err); - } + createNewDocument(data) { + return new this.model(data); + } + + static async saveDocument(document) { + try { + return await document.save(); + } catch (err) { + if (err.name === 'MongoServerError' && err.code === 11000) { + throw new DuplicateIdError({ + details: `Document with id '${document.id}' already exists.`, + }); + } + throw new DatabaseError(err); } + } + + async updateFull(teamId, data) { + try { + if (!teamId) { + throw new MissingParameterError(); + } + + const document = await this.model.findOne({ id: teamId }); + + if (!document) { + // Document not found + return null; + } - async updateFull(teamId, data) { - try { - if (!teamId) { - throw new MissingParameterError; - } - - const document = await this.model.findOne({ 'id': teamId }); - - if (!document) { - // Document not found - return null; - } - - // Copy data to found document - document.name = data.name; - document.description = data.description; - document.userIDs = data.userIDs; - - // Set the modified timestamp - document.modified = new Date().toISOString(); - - // And save - const savedDocument = await document.save(); - - return savedDocument; - } catch (err) { - if (err.name === 'CastError') { - throw new BadlyFormattedParameterError; - } else if (err.name === 'MongoServerError' && err.code === 11000) { - // 11000 = Duplicate index - const error = err.message.includes('name_') ? new Error(DuplicateNameError) : new DuplicateIdError; - throw error; - } else { - throw err; - } - } + // Copy data to found document + document.name = data.name; + document.description = data.description; + document.userIDs = data.userIDs; + + // Set the modified timestamp + document.modified = new Date().toISOString(); + + // And save + const savedDocument = await document.save(); + + return savedDocument; + } catch (err) { + if (err.name === 'CastError') { + throw new BadlyFormattedParameterError(); + } else if (err.name === 'MongoServerError' && err.code === 11000) { + // 11000 = Duplicate index + const error = err.message.includes('name_') + ? new Error(DuplicateNameError) + : new DuplicateIdError(); + throw error; + } else { + throw err; + } } + } } module.exports = new TeamsRepository(Team); - diff --git a/app/repository/techniques-repository.js b/app/repository/techniques-repository.js index 18eb3fe3..86ec081d 100644 --- a/app/repository/techniques-repository.js +++ b/app/repository/techniques-repository.js @@ -3,6 +3,6 @@ const BaseRepository = require('./_base.repository'); const Technique = require('../models/technique-model'); -class TechniqueRepository extends BaseRepository { } +class TechniqueRepository extends BaseRepository {} -module.exports = new TechniqueRepository(Technique); \ No newline at end of file +module.exports = new TechniqueRepository(Technique); diff --git a/app/repository/user-accounts-repository.js b/app/repository/user-accounts-repository.js index 67a74c4c..100cf4de 100644 --- a/app/repository/user-accounts-repository.js +++ b/app/repository/user-accounts-repository.js @@ -6,188 +6,177 @@ const regexValidator = require('../lib/regex'); const { BadlyFormattedParameterError } = require('../exceptions'); class UserAccountsRepository { - constructor(model) { - this.model = model; - } - - async retrieveAll(options) { - try { - // Build the query - const query = {}; - if (typeof options.status !== 'undefined') { - if (Array.isArray(options.status)) { - query['status'] = { $in: options.status }; - } - else { - query['status'] = options.status; - } - } - - if (typeof options.role !== 'undefined') { - if (Array.isArray(options.role)) { - query['role'] = { $in: options.role }; - } - else { - query['role'] = options.role; - } - } - - // Build the aggregation - // - Then apply query, skip, and limit options - const aggregation = [ - { $sort: { 'username': 1 } }, - { $match: query } - ]; - - if (typeof options.search !== 'undefined') { - options.search = regexValidator.sanitizeRegex(options.search); - const match = { - $match: { - $or: [ - { 'username': { '$regex': options.search, '$options': 'i' } }, - { 'email': { '$regex': options.search, '$options': 'i' } }, - { 'displayName': { '$regex': options.search, '$options': 'i' } } - ] - } - }; - aggregation.push(match); - } - - const facet = { - $facet: { - totalCount: [{ $count: 'totalCount' }], - documents: [] - } - }; - - if (options.offset) { - facet.$facet.documents.push({ $skip: options.offset }); - } - else { - facet.$facet.documents.push({ $skip: 0 }); - } - - if (options.limit) { - facet.$facet.documents.push({ $limit: options.limit }); - } - - aggregation.push(facet); - - // Retrieve the documents - return await this.model.aggregate(aggregation).exec(); + constructor(model) { + this.model = model; + } + + async retrieveAll(options) { + try { + // Build the query + const query = {}; + if (typeof options.status !== 'undefined') { + if (Array.isArray(options.status)) { + query['status'] = { $in: options.status }; + } else { + query['status'] = options.status; } - catch (err) { - throw new DatabaseError(err); - } - } + } - async retrieveOneById(stixId) { - try { - return await this.model.findOne({ 'id': stixId }).lean().exec(); - } catch (err) { - throw new DatabaseError(err); + if (typeof options.role !== 'undefined') { + if (Array.isArray(options.role)) { + query['role'] = { $in: options.role }; + } else { + query['role'] = options.role; } + } + + // Build the aggregation + // - Then apply query, skip, and limit options + const aggregation = [{ $sort: { username: 1 } }, { $match: query }]; + + if (typeof options.search !== 'undefined') { + options.search = regexValidator.sanitizeRegex(options.search); + const match = { + $match: { + $or: [ + { username: { $regex: options.search, $options: 'i' } }, + { email: { $regex: options.search, $options: 'i' } }, + { displayName: { $regex: options.search, $options: 'i' } }, + ], + }, + }; + aggregation.push(match); + } + + const facet = { + $facet: { + totalCount: [{ $count: 'totalCount' }], + documents: [], + }, + }; + + if (options.offset) { + facet.$facet.documents.push({ $skip: options.offset }); + } else { + facet.$facet.documents.push({ $skip: 0 }); + } + + if (options.limit) { + facet.$facet.documents.push({ $limit: options.limit }); + } + + aggregation.push(facet); + + // Retrieve the documents + return await this.model.aggregate(aggregation).exec(); + } catch (err) { + throw new DatabaseError(err); } + } - createNewDocument(data) { - return new this.model(data); + async retrieveOneById(stixId) { + try { + return await this.model.findOne({ id: stixId }).lean().exec(); + } catch (err) { + throw new DatabaseError(err); } - - // eslint-disable-next-line class-methods-use-this - async saveDocument(document) { - try { - return await document.save(); - } - catch(err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - throw new DuplicateIdError({ - details: `Document with id '${ document.id }' already exists.` - }); - } - throw new DatabaseError(err); - } + } + + createNewDocument(data) { + return new this.model(data); + } + + // eslint-disable-next-line class-methods-use-this + async saveDocument(document) { + try { + return await document.save(); + } catch (err) { + if (err.name === 'MongoServerError' && err.code === 11000) { + throw new DuplicateIdError({ + details: `Document with id '${document.id}' already exists.`, + }); + } + throw new DatabaseError(err); } - - // eslint-disable-next-line class-methods-use-this - async save(data) { - try { - const document = new this.model(data); - return await document.save(); - } catch (err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - throw new DuplicateIdError({ - details: `Document with id '${data.id}' already exists.` - }); - } - throw new DatabaseError(err); - } + } + + // eslint-disable-next-line class-methods-use-this + async save(data) { + try { + const document = new this.model(data); + return await document.save(); + } catch (err) { + if (err.name === 'MongoServerError' && err.code === 11000) { + throw new DuplicateIdError({ + details: `Document with id '${data.id}' already exists.`, + }); + } + throw new DatabaseError(err); } + } - async findOneAndRemove(stixId) { - try { - return await this.model.findOneAndRemove({ 'id': stixId}).exec(); - } catch (err) { - throw new DatabaseError(err); - } + async findOneAndRemove(stixId) { + try { + return await this.model.findOneAndRemove({ id: stixId }).exec(); + } catch (err) { + throw new DatabaseError(err); } + } - async retrieveOneByEmail(email) { - try { - return await this.model.findOne({ 'email': email }).lean().exec(); - } catch (err) { - throw new DatabaseError(err); - } + async retrieveOneByEmail(email) { + try { + return await this.model.findOne({ email: email }).lean().exec(); + } catch (err) { + throw new DatabaseError(err); } + } - async retrieveOneByUserAccountId(userAccountId) { - try { - return await this.model.findOne({ 'id': userAccountId }).exec(); - } catch (err) { - throw new DatabaseError(err); - } + async retrieveOneByUserAccountId(userAccountId) { + try { + return await this.model.findOne({ id: userAccountId }).exec(); + } catch (err) { + throw new DatabaseError(err); } - - async updateById(userAccountId, data) { - try { - const document = await this.retrieveOneByUserAccountId(userAccountId); - - if (!document) { - // document not found - return null; - } - - // Copy data to found document - document.email = data.email; - document.username = data.username; - document.displayName = data.displayName; - document.status = data.status; - document.role = data.role; - - // Set the modified timestamp - document.modified = new Date().toISOString(); - - // Save and return the document - try { - return await document.save(); - } - catch (err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - throw new DuplicateIdError({ - details: `Document with id '${ data.stix.id }' already exists.` - }); - } - throw new DatabaseError(err); - } - } - catch (err) { - if (err.name === 'CastError') { - throw new BadlyFormattedParameterError('userId'); - } - else { - throw err; - } + } + + async updateById(userAccountId, data) { + try { + const document = await this.retrieveOneByUserAccountId(userAccountId); + + if (!document) { + // document not found + return null; + } + + // Copy data to found document + document.email = data.email; + document.username = data.username; + document.displayName = data.displayName; + document.status = data.status; + document.role = data.role; + + // Set the modified timestamp + document.modified = new Date().toISOString(); + + // Save and return the document + try { + return await document.save(); + } catch (err) { + if (err.name === 'MongoServerError' && err.code === 11000) { + throw new DuplicateIdError({ + details: `Document with id '${data.stix.id}' already exists.`, + }); } + throw new DatabaseError(err); + } + } catch (err) { + if (err.name === 'CastError') { + throw new BadlyFormattedParameterError('userId'); + } else { + throw err; + } } + } } -module.exports = new UserAccountsRepository(UserAccount); \ No newline at end of file +module.exports = new UserAccountsRepository(UserAccount); diff --git a/app/routes/assets-routes.js b/app/routes/assets-routes.js index 1a7cb86a..bc434fe7 100644 --- a/app/routes/assets-routes.js +++ b/app/routes/assets-routes.js @@ -8,45 +8,32 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/assets') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - assetsController.retrieveAll - ) - .post( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - assetsController.create - ); +router + .route('/assets') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + assetsController.retrieveAll, + ) + .post(authn.authenticate, authz.requireRole(authz.editorOrHigher), assetsController.create); -router.route('/assets/:stixId') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - assetsController.retrieveById - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - assetsController.deleteById - ); +router + .route('/assets/:stixId') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + assetsController.retrieveById, + ) + .delete(authn.authenticate, authz.requireRole(authz.admin), assetsController.deleteById); -router.route('/assets/:stixId/modified/:modified') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - assetsController.retrieveVersionById - ) - .put( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - assetsController.updateFull - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - assetsController.deleteVersionById - ); +router + .route('/assets/:stixId/modified/:modified') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + assetsController.retrieveVersionById, + ) + .put(authn.authenticate, authz.requireRole(authz.editorOrHigher), assetsController.updateFull) + .delete(authn.authenticate, authz.requireRole(authz.admin), assetsController.deleteVersionById); module.exports = router; diff --git a/app/routes/attack-objects-routes.js b/app/routes/attack-objects-routes.js index 6982a4a5..f9117488 100644 --- a/app/routes/attack-objects-routes.js +++ b/app/routes/attack-objects-routes.js @@ -8,11 +8,12 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/attack-objects') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - attackObjectsController.retrieveAll - ); +router + .route('/attack-objects') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + attackObjectsController.retrieveAll, + ); module.exports = router; diff --git a/app/routes/authn-anonymous-routes.js b/app/routes/authn-anonymous-routes.js index c5326eb5..3116d10f 100644 --- a/app/routes/authn-anonymous-routes.js +++ b/app/routes/authn-anonymous-routes.js @@ -1,7 +1,7 @@ 'use strict'; const express = require('express'); -const passport = require("passport"); +const passport = require('passport'); const authnConfig = require('../lib/authn-configuration'); const authnAnonymous = require('../lib/authn-anonymous'); @@ -9,16 +9,19 @@ const authnAnonymousController = require('../controllers/authn-anonymous-control const router = express.Router(); -router.route('/authn/anonymous/login') - .get( - authnConfig.isUserAuthenticationMechanismEnabled('anonymous'), - passport.authenticate(authnAnonymous.strategyName()), - authnAnonymousController.login); +router + .route('/authn/anonymous/login') + .get( + authnConfig.isUserAuthenticationMechanismEnabled('anonymous'), + passport.authenticate(authnAnonymous.strategyName()), + authnAnonymousController.login, + ); -router.route('/authn/anonymous/logout') - .get( - authnConfig.isUserAuthenticationMechanismEnabled('anonymous'), - authnAnonymousController.logout - ); +router + .route('/authn/anonymous/logout') + .get( + authnConfig.isUserAuthenticationMechanismEnabled('anonymous'), + authnAnonymousController.logout, + ); module.exports = router; diff --git a/app/routes/authn-oidc-routes.js b/app/routes/authn-oidc-routes.js index 479d4bab..3633fb3a 100644 --- a/app/routes/authn-oidc-routes.js +++ b/app/routes/authn-oidc-routes.js @@ -8,22 +8,24 @@ const authnOidc = require('../lib/authn-oidc'); const authnOidcController = require('../controllers/authn-oidc-controller'); const router = express.Router(); -router.route('/authn/oidc/login') - .get( - authnConfig.isUserAuthenticationMechanismEnabled('oidc'), - authnOidcController.login, - passport.authenticate(authnOidc.strategyName())); +router + .route('/authn/oidc/login') + .get( + authnConfig.isUserAuthenticationMechanismEnabled('oidc'), + authnOidcController.login, + passport.authenticate(authnOidc.strategyName()), + ); -router.route('/authn/oidc/callback') - .get( - authnConfig.isUserAuthenticationMechanismEnabled('oidc'), - passport.authenticate(authnOidc.strategyName(), { keepSessionInfo: true }), - authnOidcController.identityProviderCallback); +router + .route('/authn/oidc/callback') + .get( + authnConfig.isUserAuthenticationMechanismEnabled('oidc'), + passport.authenticate(authnOidc.strategyName(), { keepSessionInfo: true }), + authnOidcController.identityProviderCallback, + ); -router.route('/authn/oidc/logout') - .get( - authnConfig.isUserAuthenticationMechanismEnabled('oidc'), - authnOidcController.logout - ); +router + .route('/authn/oidc/logout') + .get(authnConfig.isUserAuthenticationMechanismEnabled('oidc'), authnOidcController.logout); module.exports = router; diff --git a/app/routes/authn-service-routes.js b/app/routes/authn-service-routes.js index 74c79fd0..6890ef27 100644 --- a/app/routes/authn-service-routes.js +++ b/app/routes/authn-service-routes.js @@ -7,16 +7,18 @@ const authnConfig = require('../lib/authn-configuration'); const router = express.Router(); -router.route('/authn/service/apikey-challenge') - .get( - authnConfig.isServiceAuthenticationMechanismEnabled('challenge-apikey'), - authnServiceController.apikeyGetChallenge - ); +router + .route('/authn/service/apikey-challenge') + .get( + authnConfig.isServiceAuthenticationMechanismEnabled('challenge-apikey'), + authnServiceController.apikeyGetChallenge, + ); -router.route('/authn/service/apikey-token') - .get( - authnConfig.isServiceAuthenticationMechanismEnabled('challenge-apikey'), - authnServiceController.apikeyGetToken - ); +router + .route('/authn/service/apikey-token') + .get( + authnConfig.isServiceAuthenticationMechanismEnabled('challenge-apikey'), + authnServiceController.apikeyGetToken, + ); module.exports = router; diff --git a/app/routes/campaigns-routes.js b/app/routes/campaigns-routes.js index dfe73c31..efdb09fb 100644 --- a/app/routes/campaigns-routes.js +++ b/app/routes/campaigns-routes.js @@ -8,45 +8,36 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/campaigns') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - campaignsController.retrieveAll - ) - .post( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - campaignsController.create - ); +router + .route('/campaigns') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + campaignsController.retrieveAll, + ) + .post(authn.authenticate, authz.requireRole(authz.editorOrHigher), campaignsController.create); -router.route('/campaigns/:stixId') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - campaignsController.retrieveById - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - campaignsController.deleteById - ); +router + .route('/campaigns/:stixId') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + campaignsController.retrieveById, + ) + .delete(authn.authenticate, authz.requireRole(authz.admin), campaignsController.deleteById); -router.route('/campaigns/:stixId/modified/:modified') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - campaignsController.retrieveVersionById - ) - .put( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - campaignsController.updateFull - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - campaignsController.deleteVersionById - ); +router + .route('/campaigns/:stixId/modified/:modified') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + campaignsController.retrieveVersionById, + ) + .put(authn.authenticate, authz.requireRole(authz.editorOrHigher), campaignsController.updateFull) + .delete( + authn.authenticate, + authz.requireRole(authz.admin), + campaignsController.deleteVersionById, + ); module.exports = router; diff --git a/app/routes/collection-bundles-routes.js b/app/routes/collection-bundles-routes.js index b82840b1..a95c34aa 100644 --- a/app/routes/collection-bundles-routes.js +++ b/app/routes/collection-bundles-routes.js @@ -8,16 +8,17 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/collection-bundles') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - collectionBundlesController.exportBundle - ) - .post( - authn.authenticate, - authz.requireRole(authz.editorOrHigher, [ authz.serviceRoles.collectionManager ]), - collectionBundlesController.importBundle - ); +router + .route('/collection-bundles') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + collectionBundlesController.exportBundle, + ) + .post( + authn.authenticate, + authz.requireRole(authz.editorOrHigher, [authz.serviceRoles.collectionManager]), + collectionBundlesController.importBundle, + ); module.exports = router; diff --git a/app/routes/collection-indexes-routes.js b/app/routes/collection-indexes-routes.js index dad5bc42..ace4d945 100644 --- a/app/routes/collection-indexes-routes.js +++ b/app/routes/collection-indexes-routes.js @@ -8,40 +8,42 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/collection-indexes') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, [ authz.serviceRoles.readOnly, authz.serviceRoles.collectionManager ]), - collectionIndexesController.retrieveAll - ) - .post( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - collectionIndexesController.create - ); +router + .route('/collection-indexes') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, [ + authz.serviceRoles.readOnly, + authz.serviceRoles.collectionManager, + ]), + collectionIndexesController.retrieveAll, + ) + .post( + authn.authenticate, + authz.requireRole(authz.editorOrHigher), + collectionIndexesController.create, + ); -router.route('/collection-indexes/:id') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - collectionIndexesController.retrieveById - ) - .put( - authn.authenticate, - authz.requireRole(authz.editorOrHigher, [ authz.serviceRoles.collectionManager ]), - collectionIndexesController.updateFull - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - collectionIndexesController.delete - ); +router + .route('/collection-indexes/:id') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + collectionIndexesController.retrieveById, + ) + .put( + authn.authenticate, + authz.requireRole(authz.editorOrHigher, [authz.serviceRoles.collectionManager]), + collectionIndexesController.updateFull, + ) + .delete(authn.authenticate, authz.requireRole(authz.admin), collectionIndexesController.delete); -router.route('/collection-indexes/:id/refresh') - .post( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - collectionIndexesController.refresh - ); +router + .route('/collection-indexes/:id/refresh') + .post( + authn.authenticate, + authz.requireRole(authz.editorOrHigher), + collectionIndexesController.refresh, + ); module.exports = router; diff --git a/app/routes/collections-routes.js b/app/routes/collections-routes.js index f7d9a550..725dddde 100644 --- a/app/routes/collections-routes.js +++ b/app/routes/collections-routes.js @@ -8,40 +8,38 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/collections') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - collectionsController.retrieveAll - ) - .post( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - collectionsController.create - ); +router + .route('/collections') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + collectionsController.retrieveAll, + ) + .post(authn.authenticate, authz.requireRole(authz.editorOrHigher), collectionsController.create); -router.route('/collections/:stixId') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, [ authz.serviceRoles.readOnly, authz.serviceRoles.collectionManager ]), - collectionsController.retrieveById - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - collectionsController.delete - ); +router + .route('/collections/:stixId') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, [ + authz.serviceRoles.readOnly, + authz.serviceRoles.collectionManager, + ]), + collectionsController.retrieveById, + ) + .delete(authn.authenticate, authz.requireRole(authz.admin), collectionsController.delete); -router.route('/collections/:stixId/modified/:modified') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - collectionsController.retrieveVersionById - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - collectionsController.deleteVersionById - ); +router + .route('/collections/:stixId/modified/:modified') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + collectionsController.retrieveVersionById, + ) + .delete( + authn.authenticate, + authz.requireRole(authz.admin), + collectionsController.deleteVersionById, + ); module.exports = router; diff --git a/app/routes/data-components-routes.js b/app/routes/data-components-routes.js index 39f93c49..ac2a4237 100644 --- a/app/routes/data-components-routes.js +++ b/app/routes/data-components-routes.js @@ -8,45 +8,44 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/data-components') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - dataComponentsController.retrieveAll - ) - .post( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - dataComponentsController.create - ); +router + .route('/data-components') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + dataComponentsController.retrieveAll, + ) + .post( + authn.authenticate, + authz.requireRole(authz.editorOrHigher), + dataComponentsController.create, + ); -router.route('/data-components/:stixId') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - dataComponentsController.retrieveById - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - dataComponentsController.deleteById - ); +router + .route('/data-components/:stixId') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + dataComponentsController.retrieveById, + ) + .delete(authn.authenticate, authz.requireRole(authz.admin), dataComponentsController.deleteById); -router.route('/data-components/:stixId/modified/:modified') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - dataComponentsController.retrieveVersionById - ) - .put( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - dataComponentsController.updateFull - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - dataComponentsController.deleteVersionById - ); +router + .route('/data-components/:stixId/modified/:modified') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + dataComponentsController.retrieveVersionById, + ) + .put( + authn.authenticate, + authz.requireRole(authz.editorOrHigher), + dataComponentsController.updateFull, + ) + .delete( + authn.authenticate, + authz.requireRole(authz.admin), + dataComponentsController.deleteVersionById, + ); module.exports = router; diff --git a/app/routes/data-sources-routes.js b/app/routes/data-sources-routes.js index 5d1564f3..f220850f 100644 --- a/app/routes/data-sources-routes.js +++ b/app/routes/data-sources-routes.js @@ -8,45 +8,40 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/data-sources') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - dataSourcesController.retrieveAll - ) - .post( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - dataSourcesController.create - ); +router + .route('/data-sources') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + dataSourcesController.retrieveAll, + ) + .post(authn.authenticate, authz.requireRole(authz.editorOrHigher), dataSourcesController.create); -router.route('/data-sources/:stixId') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - dataSourcesController.retrieveById - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - dataSourcesController.deleteById - ); +router + .route('/data-sources/:stixId') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + dataSourcesController.retrieveById, + ) + .delete(authn.authenticate, authz.requireRole(authz.admin), dataSourcesController.deleteById); -router.route('/data-sources/:stixId/modified/:modified') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - dataSourcesController.retrieveVersionById - ) - .put( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - dataSourcesController.updateFull - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - dataSourcesController.deleteVersionById - ); +router + .route('/data-sources/:stixId/modified/:modified') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + dataSourcesController.retrieveVersionById, + ) + .put( + authn.authenticate, + authz.requireRole(authz.editorOrHigher), + dataSourcesController.updateFull, + ) + .delete( + authn.authenticate, + authz.requireRole(authz.admin), + dataSourcesController.deleteVersionById, + ); module.exports = router; diff --git a/app/routes/groups-routes.js b/app/routes/groups-routes.js index 3795bf6e..41b0ad9d 100644 --- a/app/routes/groups-routes.js +++ b/app/routes/groups-routes.js @@ -8,45 +8,32 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/groups') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - groupsController.retrieveAll - ) - .post( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - groupsController.create - ); +router + .route('/groups') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + groupsController.retrieveAll, + ) + .post(authn.authenticate, authz.requireRole(authz.editorOrHigher), groupsController.create); -router.route('/groups/:stixId') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - groupsController.retrieveById - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - groupsController.deleteById - ); +router + .route('/groups/:stixId') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + groupsController.retrieveById, + ) + .delete(authn.authenticate, authz.requireRole(authz.admin), groupsController.deleteById); -router.route('/groups/:stixId/modified/:modified') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - groupsController.retrieveVersionById - ) - .put( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - groupsController.updateFull - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - groupsController.deleteVersionById - ); +router + .route('/groups/:stixId/modified/:modified') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + groupsController.retrieveVersionById, + ) + .put(authn.authenticate, authz.requireRole(authz.editorOrHigher), groupsController.updateFull) + .delete(authn.authenticate, authz.requireRole(authz.admin), groupsController.deleteVersionById); module.exports = router; diff --git a/app/routes/health-routes.js b/app/routes/health-routes.js index e32e4a14..362b904c 100644 --- a/app/routes/health-routes.js +++ b/app/routes/health-routes.js @@ -8,17 +8,17 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/health/ping') - .get( - // No authentication or authorization required - healthController.getPing - ); +router.route('/health/ping').get( + // No authentication or authorization required + healthController.getPing, +); -router.route('/health/status') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - healthController.getStatus - ); +router + .route('/health/status') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + healthController.getStatus, + ); module.exports = router; diff --git a/app/routes/identities-routes.js b/app/routes/identities-routes.js index 194145c8..6f80d5dd 100644 --- a/app/routes/identities-routes.js +++ b/app/routes/identities-routes.js @@ -8,45 +8,36 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/identities') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - identitiesController.retrieveAll - ) - .post( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - identitiesController.create - ); +router + .route('/identities') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + identitiesController.retrieveAll, + ) + .post(authn.authenticate, authz.requireRole(authz.editorOrHigher), identitiesController.create); -router.route('/identities/:stixId') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - identitiesController.retrieveById - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - identitiesController.deleteById - ); +router + .route('/identities/:stixId') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + identitiesController.retrieveById, + ) + .delete(authn.authenticate, authz.requireRole(authz.admin), identitiesController.deleteById); -router.route('/identities/:stixId/modified/:modified') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - identitiesController.retrieveVersionById - ) - .put( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - identitiesController.updateFull - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - identitiesController.deleteVersionById - ); +router + .route('/identities/:stixId/modified/:modified') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + identitiesController.retrieveVersionById, + ) + .put(authn.authenticate, authz.requireRole(authz.editorOrHigher), identitiesController.updateFull) + .delete( + authn.authenticate, + authz.requireRole(authz.admin), + identitiesController.deleteVersionById, + ); module.exports = router; diff --git a/app/routes/index.js b/app/routes/index.js index b300bce8..c6180c81 100644 --- a/app/routes/index.js +++ b/app/routes/index.js @@ -13,27 +13,29 @@ const authnConfiguration = require('../lib/authn-configuration'); const router = express.Router(); // Parse the request body -router.use('/api', bodyParser.json({limit: '50mb'})); +router.use('/api', bodyParser.json({ limit: '50mb' })); router.use('/api', bodyParser.urlencoded({ limit: '1mb', extended: true })); // Setup request validation -router.use(OpenApiValidator.middleware({ +router.use( + OpenApiValidator.middleware({ apiSpec: config.openApi.specPath, validateRequests: true, - validateResponses: false -})); + validateResponses: false, + }), +); // Setup passport middleware router.use('/api', authnConfiguration.passportMiddleware()); // Set up the endpoint routes // All files in this directory that end in '-routes.js' will be added as endpoint routes -fs.readdirSync(path.join(__dirname, '.')).forEach(function(filename) { - if (filename.endsWith('-routes.js')) { - const moduleName = path.basename(filename, '.js'); - const module = require('./' + moduleName); - router.use('/api', module); - } +fs.readdirSync(path.join(__dirname, '.')).forEach(function (filename) { + if (filename.endsWith('-routes.js')) { + const moduleName = path.basename(filename, '.js'); + const module = require('./' + moduleName); + router.use('/api', module); + } }); // Handle errors that haven't otherwise been caught diff --git a/app/routes/marking-definitions-routes.js b/app/routes/marking-definitions-routes.js index 5923d15d..097f383d 100644 --- a/app/routes/marking-definitions-routes.js +++ b/app/routes/marking-definitions-routes.js @@ -8,33 +8,31 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/marking-definitions') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - markingDefinitionsController.retrieveAll - ) - .post( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - markingDefinitionsController.create - ); +router + .route('/marking-definitions') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + markingDefinitionsController.retrieveAll, + ) + .post( + authn.authenticate, + authz.requireRole(authz.editorOrHigher), + markingDefinitionsController.create, + ); -router.route('/marking-definitions/:stixId') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - markingDefinitionsController.retrieveById - ) - .put( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - markingDefinitionsController.updateFull - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - markingDefinitionsController.delete - ); +router + .route('/marking-definitions/:stixId') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + markingDefinitionsController.retrieveById, + ) + .put( + authn.authenticate, + authz.requireRole(authz.editorOrHigher), + markingDefinitionsController.updateFull, + ) + .delete(authn.authenticate, authz.requireRole(authz.admin), markingDefinitionsController.delete); module.exports = router; diff --git a/app/routes/matrices-routes.js b/app/routes/matrices-routes.js index c666f09b..20a27c32 100644 --- a/app/routes/matrices-routes.js +++ b/app/routes/matrices-routes.js @@ -8,52 +8,40 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/matrices') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - matricesController.retrieveAll - ) - .post( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - matricesController.create - ); - -router.route('/matrices/:stixId') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - matricesController.retrieveById - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - matricesController.deleteById - ); - -router.route('/matrices/:stixId/modified/:modified') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - matricesController.retrieveVersionById - ) - .put( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - matricesController.updateFull - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - matricesController.deleteVersionById - ); - -router.route('/matrices/:stixId/modified/:modified/techniques') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - matricesController.retrieveTechniquesForMatrix - ); +router + .route('/matrices') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + matricesController.retrieveAll, + ) + .post(authn.authenticate, authz.requireRole(authz.editorOrHigher), matricesController.create); + +router + .route('/matrices/:stixId') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + matricesController.retrieveById, + ) + .delete(authn.authenticate, authz.requireRole(authz.admin), matricesController.deleteById); + +router + .route('/matrices/:stixId/modified/:modified') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + matricesController.retrieveVersionById, + ) + .put(authn.authenticate, authz.requireRole(authz.editorOrHigher), matricesController.updateFull) + .delete(authn.authenticate, authz.requireRole(authz.admin), 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/routes/mitigations-routes.js b/app/routes/mitigations-routes.js index 6a493cf1..04b7e234 100644 --- a/app/routes/mitigations-routes.js +++ b/app/routes/mitigations-routes.js @@ -8,45 +8,40 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/mitigations') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - mitigationsController.retrieveAll - ) - .post( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - mitigationsController.create - ); +router + .route('/mitigations') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + mitigationsController.retrieveAll, + ) + .post(authn.authenticate, authz.requireRole(authz.editorOrHigher), mitigationsController.create); -router.route('/mitigations/:stixId') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - mitigationsController.retrieveById - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - mitigationsController.deleteById - ); +router + .route('/mitigations/:stixId') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + mitigationsController.retrieveById, + ) + .delete(authn.authenticate, authz.requireRole(authz.admin), mitigationsController.deleteById); -router.route('/mitigations/:stixId/modified/:modified') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - mitigationsController.retrieveVersionById - ) - .put( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - mitigationsController.updateFull - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - mitigationsController.deleteVersionById - ); +router + .route('/mitigations/:stixId/modified/:modified') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + mitigationsController.retrieveVersionById, + ) + .put( + authn.authenticate, + authz.requireRole(authz.editorOrHigher), + mitigationsController.updateFull, + ) + .delete( + authn.authenticate, + authz.requireRole(authz.admin), + mitigationsController.deleteVersionById, + ); module.exports = router; diff --git a/app/routes/notes-routes.js b/app/routes/notes-routes.js index 87ba1f99..bb2866ef 100644 --- a/app/routes/notes-routes.js +++ b/app/routes/notes-routes.js @@ -8,45 +8,36 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/notes') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - notesController.retrieveAll - ) - .post( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - notesController.create - ); +router + .route('/notes') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + notesController.retrieveAll, + ) + .post(authn.authenticate, authz.requireRole(authz.editorOrHigher), notesController.create); -router.route('/notes/:stixId') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - notesController.retrieveById - ) - .delete( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - notesController.deleteById - ); +router + .route('/notes/:stixId') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + notesController.retrieveById, + ) + .delete(authn.authenticate, authz.requireRole(authz.editorOrHigher), notesController.deleteById); -router.route('/notes/:stixId/modified/:modified') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - notesController.retrieveVersionById - ) - .put( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - notesController.updateVersion - ) - .delete( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - notesController.deleteVersionById - ); +router + .route('/notes/:stixId/modified/:modified') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + notesController.retrieveVersionById, + ) + .put(authn.authenticate, authz.requireRole(authz.editorOrHigher), notesController.updateVersion) + .delete( + authn.authenticate, + authz.requireRole(authz.editorOrHigher), + notesController.deleteVersionById, + ); module.exports = router; diff --git a/app/routes/recent-activity-routes.js b/app/routes/recent-activity-routes.js index bc450cb9..51926325 100644 --- a/app/routes/recent-activity-routes.js +++ b/app/routes/recent-activity-routes.js @@ -8,11 +8,12 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/recent-activity') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - recentActivityController.retrieveAll - ); +router + .route('/recent-activity') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + recentActivityController.retrieveAll, + ); module.exports = router; diff --git a/app/routes/references-routes.js b/app/routes/references-routes.js index 7eba5e21..4e7ef7be 100644 --- a/app/routes/references-routes.js +++ b/app/routes/references-routes.js @@ -8,26 +8,19 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/references') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - referencesController.retrieveAll - ) - .post( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - referencesController.create - ) - .put( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - referencesController.update - ) - .delete( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - referencesController.deleteBySourceName - ); +router + .route('/references') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + referencesController.retrieveAll, + ) + .post(authn.authenticate, authz.requireRole(authz.editorOrHigher), referencesController.create) + .put(authn.authenticate, authz.requireRole(authz.editorOrHigher), referencesController.update) + .delete( + authn.authenticate, + authz.requireRole(authz.editorOrHigher), + referencesController.deleteBySourceName, + ); module.exports = router; diff --git a/app/routes/relationships-routes.js b/app/routes/relationships-routes.js index dc54871d..341c0644 100644 --- a/app/routes/relationships-routes.js +++ b/app/routes/relationships-routes.js @@ -8,45 +8,44 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/relationships') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - relationshipsController.retrieveAll - ) - .post( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - relationshipsController.create - ); +router + .route('/relationships') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + relationshipsController.retrieveAll, + ) + .post( + authn.authenticate, + authz.requireRole(authz.editorOrHigher), + relationshipsController.create, + ); -router.route('/relationships/:stixId') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - relationshipsController.retrieveById - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - relationshipsController.deleteById - ); +router + .route('/relationships/:stixId') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + relationshipsController.retrieveById, + ) + .delete(authn.authenticate, authz.requireRole(authz.admin), relationshipsController.deleteById); -router.route('/relationships/:stixId/modified/:modified') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - relationshipsController.retrieveVersionById - ) - .put( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - relationshipsController.updateFull - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - relationshipsController.deleteVersionById - ); +router + .route('/relationships/:stixId/modified/:modified') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + relationshipsController.retrieveVersionById, + ) + .put( + authn.authenticate, + authz.requireRole(authz.editorOrHigher), + relationshipsController.updateFull, + ) + .delete( + authn.authenticate, + authz.requireRole(authz.admin), + relationshipsController.deleteVersionById, + ); module.exports = router; diff --git a/app/routes/session-routes.js b/app/routes/session-routes.js index 74b05f1a..96a2d431 100644 --- a/app/routes/session-routes.js +++ b/app/routes/session-routes.js @@ -7,10 +7,6 @@ const authn = require('../lib/authn-middleware'); const router = express.Router(); -router.route('/session') - .get( - authn.authenticate, - sessionController.retrieveCurrentSession - ); +router.route('/session').get(authn.authenticate, sessionController.retrieveCurrentSession); module.exports = router; diff --git a/app/routes/software-routes.js b/app/routes/software-routes.js index db557128..c1707464 100644 --- a/app/routes/software-routes.js +++ b/app/routes/software-routes.js @@ -8,45 +8,32 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/software') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - softwareController.retrieveAll - ) - .post( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - softwareController.create - ); +router + .route('/software') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + softwareController.retrieveAll, + ) + .post(authn.authenticate, authz.requireRole(authz.editorOrHigher), softwareController.create); -router.route('/software/:stixId') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - softwareController.retrieveById - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - softwareController.deleteById - ); +router + .route('/software/:stixId') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + softwareController.retrieveById, + ) + .delete(authn.authenticate, authz.requireRole(authz.admin), softwareController.deleteById); -router.route('/software/:stixId/modified/:modified') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - softwareController.retrieveVersionById - ) - .put( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - softwareController.updateFull - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - softwareController.deleteVersionById - ); +router + .route('/software/:stixId/modified/:modified') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + softwareController.retrieveVersionById, + ) + .put(authn.authenticate, authz.requireRole(authz.editorOrHigher), softwareController.updateFull) + .delete(authn.authenticate, authz.requireRole(authz.admin), softwareController.deleteVersionById); module.exports = router; diff --git a/app/routes/stix-bundles-routes.js b/app/routes/stix-bundles-routes.js index 1dabb592..88e67a15 100644 --- a/app/routes/stix-bundles-routes.js +++ b/app/routes/stix-bundles-routes.js @@ -8,11 +8,15 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/stix-bundles') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, [ authz.serviceRoles.readOnly, authz.serviceRoles.stixExport ]), - stixBundlesController.exportBundle - ); +router + .route('/stix-bundles') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, [ + authz.serviceRoles.readOnly, + authz.serviceRoles.stixExport, + ]), + stixBundlesController.exportBundle, + ); module.exports = router; diff --git a/app/routes/system-configuration-routes.js b/app/routes/system-configuration-routes.js index 62be1ee2..d75613a1 100644 --- a/app/routes/system-configuration-routes.js +++ b/app/routes/system-configuration-routes.js @@ -8,60 +8,64 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/config/system-version') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - systemConfigurationController.retrieveSystemVersion - ); +router + .route('/config/system-version') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + systemConfigurationController.retrieveSystemVersion, + ); -router.route('/config/allowed-values') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - systemConfigurationController.retrieveAllowedValues - ); +router + .route('/config/allowed-values') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + systemConfigurationController.retrieveAllowedValues, + ); -router.route('/config/organization-identity') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - systemConfigurationController.retrieveOrganizationIdentity - ) - .post( - authn.authenticate, - authz.requireRole(authz.admin), - systemConfigurationController.setOrganizationIdentity - ); +router + .route('/config/organization-identity') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + systemConfigurationController.retrieveOrganizationIdentity, + ) + .post( + authn.authenticate, + authz.requireRole(authz.admin), + systemConfigurationController.setOrganizationIdentity, + ); -router.route('/config/authn') - .get( - // No authentication or authorization required - systemConfigurationController.retrieveAuthenticationConfig - ); +router.route('/config/authn').get( + // No authentication or authorization required + systemConfigurationController.retrieveAuthenticationConfig, +); -router.route('/config/default-marking-definitions') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - systemConfigurationController.retrieveDefaultMarkingDefinitions - ) - .post( - authn.authenticate, - authz.requireRole(authz.admin), - systemConfigurationController.setDefaultMarkingDefinitions - ); +router + .route('/config/default-marking-definitions') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + systemConfigurationController.retrieveDefaultMarkingDefinitions, + ) + .post( + authn.authenticate, + authz.requireRole(authz.admin), + systemConfigurationController.setDefaultMarkingDefinitions, + ); -router.route('/config/organization-namespace') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - systemConfigurationController.retrieveOrganizationNamespace - ) - .post( - authn.authenticate, - authz.requireRole(authz.admin), - systemConfigurationController.setOrganizationNamespace - ); +router + .route('/config/organization-namespace') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + systemConfigurationController.retrieveOrganizationNamespace, + ) + .post( + authn.authenticate, + authz.requireRole(authz.admin), + systemConfigurationController.setOrganizationNamespace, + ); module.exports = router; diff --git a/app/routes/tactics-routes.js b/app/routes/tactics-routes.js index 5294a60b..0e7b6080 100644 --- a/app/routes/tactics-routes.js +++ b/app/routes/tactics-routes.js @@ -8,52 +8,40 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/tactics') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - tacticsController.retrieveAll - ) - .post( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - tacticsController.create - ); - -router.route('/tactics/:stixId') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - tacticsController.retrieveById - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - tacticsController.deleteById - ); - -router.route('/tactics/:stixId/modified/:modified') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - tacticsController.retrieveVersionById - ) - .put( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - tacticsController.updateFull - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - tacticsController.deleteVersionById - ); - -router.route('/tactics/:stixId/modified/:modified/techniques') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - tacticsController.retrieveTechniquesForTactic - ); +router + .route('/tactics') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + tacticsController.retrieveAll, + ) + .post(authn.authenticate, authz.requireRole(authz.editorOrHigher), tacticsController.create); + +router + .route('/tactics/:stixId') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + tacticsController.retrieveById, + ) + .delete(authn.authenticate, authz.requireRole(authz.admin), tacticsController.deleteById); + +router + .route('/tactics/:stixId/modified/:modified') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + tacticsController.retrieveVersionById, + ) + .put(authn.authenticate, authz.requireRole(authz.editorOrHigher), tacticsController.updateFull) + .delete(authn.authenticate, authz.requireRole(authz.admin), tacticsController.deleteVersionById); + +router + .route('/tactics/:stixId/modified/:modified/techniques') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + tacticsController.retrieveTechniquesForTactic, + ); module.exports = router; diff --git a/app/routes/teams-routes.js b/app/routes/teams-routes.js index 2ab7a843..c4e30642 100644 --- a/app/routes/teams-routes.js +++ b/app/routes/teams-routes.js @@ -8,40 +8,19 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/teams') - .get( - authn.authenticate, - authz.requireRole(authz.admin), - teamsController.retrieveAll - ) - .post( - authn.authenticate, - authz.requireRole(authz.admin), - teamsController.create - ); +router + .route('/teams') + .get(authn.authenticate, authz.requireRole(authz.admin), teamsController.retrieveAll) + .post(authn.authenticate, authz.requireRole(authz.admin), teamsController.create); -router.route('/teams/:id') - .get( - authn.authenticate, - authz.requireRole(authz.admin), - teamsController.retrieveById - ) - .put( - authn.authenticate, - authz.requireRole(authz.admin), - teamsController.updateFull - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - teamsController.delete - ); +router + .route('/teams/:id') + .get(authn.authenticate, authz.requireRole(authz.admin), teamsController.retrieveById) + .put(authn.authenticate, authz.requireRole(authz.admin), teamsController.updateFull) + .delete(authn.authenticate, authz.requireRole(authz.admin), teamsController.delete); -router.route('/teams/:id/users') - .get( - authn.authenticate, - authz.requireRole(authz.admin), - teamsController.retrieveAllUsers - ) +router + .route('/teams/:id/users') + .get(authn.authenticate, authz.requireRole(authz.admin), teamsController.retrieveAllUsers); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/app/routes/techniques-routes.js b/app/routes/techniques-routes.js index acd9a390..c9908aac 100644 --- a/app/routes/techniques-routes.js +++ b/app/routes/techniques-routes.js @@ -8,52 +8,44 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/techniques') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - techniquesController.retrieveAll - ) - .post( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - techniquesController.create - ); - -router.route('/techniques/:stixId') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - techniquesController.retrieveById - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - techniquesController.deleteById - ); - -router.route('/techniques/:stixId/modified/:modified') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - techniquesController.retrieveVersionById - ) - .put( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - techniquesController.updateFull - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - techniquesController.deleteVersionById - ); - -router.route('/techniques/:stixId/modified/:modified/tactics') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - techniquesController.retrieveTacticsForTechnique - ) +router + .route('/techniques') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + techniquesController.retrieveAll, + ) + .post(authn.authenticate, authz.requireRole(authz.editorOrHigher), techniquesController.create); + +router + .route('/techniques/:stixId') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + techniquesController.retrieveById, + ) + .delete(authn.authenticate, authz.requireRole(authz.admin), techniquesController.deleteById); + +router + .route('/techniques/:stixId/modified/:modified') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + techniquesController.retrieveVersionById, + ) + .put(authn.authenticate, authz.requireRole(authz.editorOrHigher), techniquesController.updateFull) + .delete( + authn.authenticate, + authz.requireRole(authz.admin), + techniquesController.deleteVersionById, + ); + +router + .route('/techniques/:stixId/modified/:modified/tactics') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + techniquesController.retrieveTacticsForTechnique, + ); module.exports = router; diff --git a/app/routes/user-accounts-routes.js b/app/routes/user-accounts-routes.js index c26185d5..89614ab5 100644 --- a/app/routes/user-accounts-routes.js +++ b/app/routes/user-accounts-routes.js @@ -8,46 +8,32 @@ const authz = require('../lib/authz-middleware'); const router = express.Router(); -router.route('/user-accounts') - .get( - authn.authenticate, - authz.requireRole(authz.admin), - userAccountsController.retrieveAll - ) - .post( - authn.authenticate, - authz.requireRole(authz.admin), - userAccountsController.create - ); - -router.route('/user-accounts/:id') - .get( - authn.authenticate, - authz.requireRole(authz.editorOrHigher), - userAccountsController.retrieveById - ) - .put( - authn.authenticate, - authz.requireRole(authz.admin), - userAccountsController.updateFull - ) - .delete( - authn.authenticate, - authz.requireRole(authz.admin), - userAccountsController.delete - ); - -router.route('/user-accounts/:id/teams') - .get( - authn.authenticate, - authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), - userAccountsController.retrieveTeamsByUserId - ); - -router.route('/user-accounts/register') - .post( - // authn and authz handled in controller - userAccountsController.register - ); +router + .route('/user-accounts') + .get(authn.authenticate, authz.requireRole(authz.admin), userAccountsController.retrieveAll) + .post(authn.authenticate, authz.requireRole(authz.admin), userAccountsController.create); + +router + .route('/user-accounts/:id') + .get( + authn.authenticate, + authz.requireRole(authz.editorOrHigher), + userAccountsController.retrieveById, + ) + .put(authn.authenticate, authz.requireRole(authz.admin), userAccountsController.updateFull) + .delete(authn.authenticate, authz.requireRole(authz.admin), userAccountsController.delete); + +router + .route('/user-accounts/:id/teams') + .get( + authn.authenticate, + authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), + userAccountsController.retrieveTeamsByUserId, + ); + +router.route('/user-accounts/register').post( + // authn and authz handled in controller + userAccountsController.register, +); module.exports = router; diff --git a/app/scheduler/scheduler.js b/app/scheduler/scheduler.js index b097d086..992400e8 100644 --- a/app/scheduler/scheduler.js +++ b/app/scheduler/scheduler.js @@ -9,195 +9,261 @@ const config = require('../config/config'); const async = require('async'); let timer; -exports.initializeScheduler = function() { - logger.info('Starting the scheduler'); +exports.initializeScheduler = function () { + logger.info('Starting the scheduler'); - const intervalMilliseconds = config.scheduler.checkWorkbenchInterval * 1000; - timer = setInterval(runCheckCollectionIndexes, intervalMilliseconds); -} + const intervalMilliseconds = config.scheduler.checkWorkbenchInterval * 1000; + timer = setInterval(runCheckCollectionIndexes, intervalMilliseconds); +}; -exports.stopScheduler = function() { - if (timer) { - clearInterval(timer); - } -} +exports.stopScheduler = function () { + if (timer) { + clearInterval(timer); + } +}; const scheduledSubscriptions = new Map(); function runCheckCollectionIndexes() { - logger.info('Scheduler running...'); - const options = { - offset: 0, - limit: 0 - } - collectionIndexesService.retrieveAll(options, function(err, collectionIndexes) { - if (err) { - logger.error('Unable to get existing collection indexes: ' + err); - } - else { - for (const collectionIndex of collectionIndexes) { - if (collectionIndex.collection_index && collectionIndex.workspace.update_policy.automatic) { - // Is it time to retrieve the collection index from the remote URL? - let lastRetrieval; - const now = Date.now(); - if (collectionIndex.workspace.update_policy.last_retrieval) { - lastRetrieval = new Date(collectionIndex.workspace.update_policy.last_retrieval); - } - if (!lastRetrieval || (now - lastRetrieval) > (1000 * collectionIndex.workspace.update_policy.interval)) { - logger.info(`Checking collection index: ${ collectionIndex.collection_index.name } (${ collectionIndex.collection_index.id })`); - logger.verbose('Retrieving collection index from remote url ' + collectionIndex.workspace.remote_url); - collectionIndexesService.retrieveByUrl(collectionIndex.workspace.remote_url, function(err, remoteCollectionIndex) { - if (err) { - logger.error('Unable to retrieve collection index from remote url. ' + err); - } - else { - const remoteTimestamp = new Date(remoteCollectionIndex.modified); - const existingTimestamp = new Date(collectionIndex.collection_index.modified); - if (remoteTimestamp > existingTimestamp) { - logger.info('The retrieved collection index is newer. Updating collection index in workbench.'); - collectionIndex.collection_index = remoteCollectionIndex; - collectionIndex.workspace.update_policy.last_retrieval = new Date(now).toISOString(); - - collectionIndexesService.updateFull(collectionIndex.collection_index.id, collectionIndex, function(err, savedCollectionIndex) { - if (err) { - logger.error('Unable to update collection index in workbench. ' + err); - return; - } - else { - // Check subscribed collections - if (scheduledSubscriptions.has(savedCollectionIndex.collection_index.id)) { - logger.info(`Subscriptions for collection index ${ savedCollectionIndex.collection_index.id } are already being checked`); - } - else { - logger.verbose(`Checking Subscriptions for collection index ${ savedCollectionIndex.collection_index.id }`); - scheduledSubscriptions.set(savedCollectionIndex.collection_index.id, true); - subscriptionHandler(savedCollectionIndex, function (err) { - scheduledSubscriptions.delete(savedCollectionIndex.collection_index.id); - if (err) { - logger.error('Error checking subscriptions in collection index. ' + err); - return; - } - }); - } - } - }); - } - else { - logger.verbose('The retrieved collection index is not newer.') - collectionIndex.workspace.update_policy.last_retrieval = new Date(now).toISOString(); - collectionIndexesService.updateFull(collectionIndex.collection_index.id, collectionIndex, function(err) { - if (err) { - logger.error('Unable to update collection index in workbench. ' + err); - return; - } - else { - // Check subscribed collections - if (scheduledSubscriptions.has(collectionIndex.collection_index.id)) { - logger.info(`Subscriptions for collection index ${ collectionIndex.collection_index.id } are already being checked`); - } - else { - logger.info(`Checking Subscriptions for collection index ${ collectionIndex.collection_index.id }`); - scheduledSubscriptions.set(collectionIndex.collection_index.id, true); - subscriptionHandler(collectionIndex, function (err) { - scheduledSubscriptions.delete(collectionIndex.collection_index.id); - if (err) { - logger.error('Error checking subscriptions in collection index. ' + err); - return; - } - }); - } - } - }); - } - } - }); - } + logger.info('Scheduler running...'); + const options = { + offset: 0, + limit: 0, + }; + collectionIndexesService.retrieveAll(options, function (err, collectionIndexes) { + if (err) { + logger.error('Unable to get existing collection indexes: ' + err); + } else { + for (const collectionIndex of collectionIndexes) { + if (collectionIndex.collection_index && collectionIndex.workspace.update_policy.automatic) { + // Is it time to retrieve the collection index from the remote URL? + let lastRetrieval; + const now = Date.now(); + if (collectionIndex.workspace.update_policy.last_retrieval) { + lastRetrieval = new Date(collectionIndex.workspace.update_policy.last_retrieval); + } + if ( + !lastRetrieval || + now - lastRetrieval > 1000 * collectionIndex.workspace.update_policy.interval + ) { + logger.info( + `Checking collection index: ${collectionIndex.collection_index.name} (${collectionIndex.collection_index.id})`, + ); + logger.verbose( + 'Retrieving collection index from remote url ' + collectionIndex.workspace.remote_url, + ); + collectionIndexesService.retrieveByUrl( + collectionIndex.workspace.remote_url, + function (err, remoteCollectionIndex) { + if (err) { + logger.error('Unable to retrieve collection index from remote url. ' + err); + } else { + const remoteTimestamp = new Date(remoteCollectionIndex.modified); + const existingTimestamp = new Date(collectionIndex.collection_index.modified); + if (remoteTimestamp > existingTimestamp) { + logger.info( + 'The retrieved collection index is newer. Updating collection index in workbench.', + ); + collectionIndex.collection_index = remoteCollectionIndex; + collectionIndex.workspace.update_policy.last_retrieval = new Date( + now, + ).toISOString(); + + collectionIndexesService.updateFull( + collectionIndex.collection_index.id, + collectionIndex, + function (err, savedCollectionIndex) { + if (err) { + logger.error('Unable to update collection index in workbench. ' + err); + return; + } else { + // Check subscribed collections + if ( + scheduledSubscriptions.has(savedCollectionIndex.collection_index.id) + ) { + logger.info( + `Subscriptions for collection index ${savedCollectionIndex.collection_index.id} are already being checked`, + ); + } else { + logger.verbose( + `Checking Subscriptions for collection index ${savedCollectionIndex.collection_index.id}`, + ); + scheduledSubscriptions.set( + savedCollectionIndex.collection_index.id, + true, + ); + subscriptionHandler(savedCollectionIndex, function (err) { + scheduledSubscriptions.delete( + savedCollectionIndex.collection_index.id, + ); + if (err) { + logger.error( + 'Error checking subscriptions in collection index. ' + err, + ); + return; + } + }); + } + } + }, + ); + } else { + logger.verbose('The retrieved collection index is not newer.'); + collectionIndex.workspace.update_policy.last_retrieval = new Date( + now, + ).toISOString(); + collectionIndexesService.updateFull( + collectionIndex.collection_index.id, + collectionIndex, + function (err) { + if (err) { + logger.error('Unable to update collection index in workbench. ' + err); + return; + } else { + // Check subscribed collections + if (scheduledSubscriptions.has(collectionIndex.collection_index.id)) { + logger.info( + `Subscriptions for collection index ${collectionIndex.collection_index.id} are already being checked`, + ); + } else { + logger.info( + `Checking Subscriptions for collection index ${collectionIndex.collection_index.id}`, + ); + scheduledSubscriptions.set(collectionIndex.collection_index.id, true); + subscriptionHandler(collectionIndex, function (err) { + scheduledSubscriptions.delete(collectionIndex.collection_index.id); + if (err) { + logger.error( + 'Error checking subscriptions in collection index. ' + err, + ); + return; + } + }); + } + } + }, + ); + } } - } + }, + ); + } } - }); + } + } + }); } function subscriptionHandler(collectionIndex, callback) { - // Check each subscription in the collection index - async.eachSeries(collectionIndex.workspace.update_policy.subscriptions, function(collectionId, callback2) { - // collections is a list of the versions of the collection that are in the Workbench data store - collectionsService.retrieveById(collectionId, { versions: 'latest' }, function(err, collections) { - if (err) { - return callback2(err); - } + // Check each subscription in the collection index + async.eachSeries( + collectionIndex.workspace.update_policy.subscriptions, + function (collectionId, callback2) { + // collections is a list of the versions of the collection that are in the Workbench data store + collectionsService.retrieveById( + collectionId, + { versions: 'latest' }, + function (err, collections) { + if (err) { + return callback2(err); + } - // Get the corresponding collection info from the collection index - // collectionInfo.versions is a list of versions that are available to be imported - const collectionInfo = collectionIndex.collection_index.collections.find(item => item.id === collectionId); - if (!collectionInfo || collectionInfo.versions.length === 0) { - // No versions available to import - return callback2(); - } + // Get the corresponding collection info from the collection index + // collectionInfo.versions is a list of versions that are available to be imported + const collectionInfo = collectionIndex.collection_index.collections.find( + (item) => item.id === collectionId, + ); + if (!collectionInfo || collectionInfo.versions.length === 0) { + // No versions available to import + return callback2(); + } - // Order both lists of collection versions, latest version first - collections.sort((a, b) => b.stix.modified.getTime() - a.stix.modified.getTime()); - collectionInfo.versions.sort((a, b) => b.modified.getTime() - a.modified.getTime()); + // Order both lists of collection versions, latest version first + collections.sort((a, b) => b.stix.modified.getTime() - a.stix.modified.getTime()); + collectionInfo.versions.sort((a, b) => b.modified.getTime() - a.modified.getTime()); - if (collections.length === 0 || collections[0].stix.modified < collectionInfo.versions[0].modified) { - // Latest version in collection index is later than latest version in the Workbench data store, - // so we should import it - logger.info(`Retrieving collection bundle from remote url ${ collectionInfo.versions[0].url }`); - collectionsService.retrieveByUrl(collectionInfo.versions[0].url, function(err, collectionBundle) { - if (err) { - const error = new Error('Unable to retrieve updated collection bundle. ' + err); - return callback2(error); - } - - logger.info(`Downloaded updated collection bundle with id ${ collectionBundle.id }`); - - // Find the x-mitre-collection objects - const collections = collectionBundle.objects.filter(object => object.type === 'x-mitre-collection'); + if ( + collections.length === 0 || + collections[0].stix.modified < collectionInfo.versions[0].modified + ) { + // Latest version in collection index is later than latest version in the Workbench data store, + // so we should import it + logger.info( + `Retrieving collection bundle from remote url ${collectionInfo.versions[0].url}`, + ); + collectionsService.retrieveByUrl( + collectionInfo.versions[0].url, + function (err, collectionBundle) { + if (err) { + const error = new Error('Unable to retrieve updated collection bundle. ' + err); + return callback2(error); + } - // The bundle must have an x-mitre-collection object - if (collections.length === 0) { - const error = new Error("Unable to import collection bundle. Collection bundle is missing x-mitre-collection object."); - return callback2(error); - } - else if (collections.length > 1) { - const error = new Error("Unable to import collection bundle. Collection bundle has more than one x-mitre-collection object."); - return callback2(error); - } + logger.info(`Downloaded updated collection bundle with id ${collectionBundle.id}`); - // The collection must have an id. - if (collections.length > 0 && !collections[0].id) { - const error = new Error('Unable to import collection bundle. Badly formatted collection in bundle, x-mitre-collection missing id.'); - return callback2(error); - } + // Find the x-mitre-collection objects + const collections = collectionBundle.objects.filter( + (object) => object.type === 'x-mitre-collection', + ); - const importOptions = { - previewOnly: false, - forceImportParameters: [] - }; - collectionBundlesService.importBundle(collections[0], collectionBundle, importOptions, function (err, importedCollection) { - if (err) { - const error = new Error('Unable to import collection bundle into ATT&CK Workbench database. ' + err); - return callback2(error); - } - else { - logger.info(`Imported collection bundle with x-mitre-collection id ${ importedCollection.stix.id }`); - return callback2(); - } - }) - }) + // The bundle must have an x-mitre-collection object + if (collections.length === 0) { + const error = new Error( + 'Unable to import collection bundle. Collection bundle is missing x-mitre-collection object.', + ); + return callback2(error); + } else if (collections.length > 1) { + const error = new Error( + 'Unable to import collection bundle. Collection bundle has more than one x-mitre-collection object.', + ); + return callback2(error); } - else { - // Workbench data store is up-to-date, don't import new version - return callback2(); + + // The collection must have an id. + if (collections.length > 0 && !collections[0].id) { + const error = new Error( + 'Unable to import collection bundle. Badly formatted collection in bundle, x-mitre-collection missing id.', + ); + return callback2(error); } - }) + + const importOptions = { + previewOnly: false, + forceImportParameters: [], + }; + collectionBundlesService.importBundle( + collections[0], + collectionBundle, + importOptions, + function (err, importedCollection) { + if (err) { + const error = new Error( + 'Unable to import collection bundle into ATT&CK Workbench database. ' + err, + ); + return callback2(error); + } else { + logger.info( + `Imported collection bundle with x-mitre-collection id ${importedCollection.stix.id}`, + ); + return callback2(); + } + }, + ); + }, + ); + } else { + // Workbench data store is up-to-date, don't import new version + return callback2(); + } }, - function(err) { - if (err) { - return callback(err); - } - else { - return callback(); - } - }); + ); + }, + function (err) { + if (err) { + return callback(err); + } else { + return callback(); + } + }, + ); } diff --git a/app/services/_abstract.service.js b/app/services/_abstract.service.js index 26993de9..ed0bf92d 100644 --- a/app/services/_abstract.service.js +++ b/app/services/_abstract.service.js @@ -3,25 +3,23 @@ const { NotImplementedError } = require('../exceptions'); class AbstractService { + retrieveAll(options) { + throw new NotImplementedError(this.constructor.name, 'retrieveAll'); + } - retrieveAll(options) { - throw new NotImplementedError(this.constructor.name, 'retrieveAll'); - } + retrieveById(stixId, options) { + throw new NotImplementedError(this.constructor.name, 'retrieveById'); + } - retrieveById(stixId, options) { - throw new NotImplementedError(this.constructor.name, 'retrieveById'); - } + retrieveVersionById(stixId, modified) { + throw new NotImplementedError(this.constructor.name, 'retrieveVersionById'); + } - retrieveVersionById(stixId, modified) { - throw new NotImplementedError(this.constructor.name, 'retrieveVersionById'); - } - - create(data, options) { - throw new NotImplementedError(this.constructor.name, 'create'); - } - - // ... other abstract methods ... + create(data, options) { + throw new NotImplementedError(this.constructor.name, 'create'); + } + // ... other abstract methods ... } -module.exports = AbstractService; \ No newline at end of file +module.exports = AbstractService; diff --git a/app/services/_base.service.js b/app/services/_base.service.js index 0d80d98e..9465f2ed 100644 --- a/app/services/_base.service.js +++ b/app/services/_base.service.js @@ -3,438 +3,439 @@ const uuid = require('uuid'); const systemConfigurationService = require('./system-configuration-service'); const config = require('../config/config'); -const { DatabaseError, - IdentityServiceError, - MissingParameterError, - InvalidQueryStringParameterError, - InvalidTypeError } = require('../exceptions'); +const { + DatabaseError, + IdentityServiceError, + MissingParameterError, + InvalidQueryStringParameterError, + InvalidTypeError, +} = require('../exceptions'); const AbstractService = require('./_abstract.service'); class BaseService extends AbstractService { + constructor(type, repository) { + super(); + this.type = type; + this.repository = repository; + } - constructor(type, repository) { - super(); - this.type = type; - this.repository = repository; + static attackObjectsService; + + static identitiesService; + + static systemConfigurationService; + + static requireServices() { + // Late binding to avoid circular dependencies + if (!BaseService.identitiesService) { + BaseService.identitiesService = require('./identities-service'); + } + if (!BaseService.systemConfigurationService) { + BaseService.systemConfigurationService = require('./system-configuration-service'); } + } + + // Helper function to determine if the last argument is a callback + static isCallback(arg) { + return typeof arg === 'function'; + } + + static paginate(options, results) { + if (options.includePagination) { + let derivedTotalCount = 0; + if (results[0].totalCount && results[0].totalCount.length > 0) { + derivedTotalCount = results[0].totalCount[0].totalCount; + } + return { + pagination: { + total: derivedTotalCount, + offset: options.offset, + limit: options.limit, + }, + data: results[0].documents, + }; + } else { + return results[0].documents; + } + } - static attackObjectsService; + async retrieveAll(options, callback) { + BaseService.requireServices(); + if (BaseService.isCallback(arguments[arguments.length - 1])) { + callback = arguments[arguments.length - 1]; + } - static identitiesService; + let results; + try { + results = await this.repository.retrieveAll(options); + } catch (err) { + const databaseError = new DatabaseError(err); // Let the DatabaseError bubble up + if (callback) { + return callback(databaseError); + } + throw databaseError; + } - static systemConfigurationService; + try { + await BaseService.identitiesService.addCreatedByAndModifiedByIdentitiesToAll( + results[0].documents, + ); + } catch (err) { + const identityError = new IdentityServiceError({ + details: err.message, + cause: err, + }); + if (callback) { + return callback(identityError); + } + throw identityError; + } - static requireServices() { - // Late binding to avoid circular dependencies - if (!BaseService.identitiesService) { - BaseService.identitiesService = require('./identities-service'); - } - if (!BaseService.systemConfigurationService) { - BaseService.systemConfigurationService = require('./system-configuration-service'); - } + const paginatedResults = BaseService.paginate(options, results); + if (callback) { + return callback(null, paginatedResults); } + return paginatedResults; + } - // Helper function to determine if the last argument is a callback - static isCallback(arg) { - return typeof arg === 'function'; + async retrieveById(stixId, options, callback) { + BaseService.requireServices(); + + if (BaseService.isCallback(arguments[arguments.length - 1])) { + callback = arguments[arguments.length - 1]; } - static paginate(options, results) { - if (options.includePagination) { - let derivedTotalCount = 0; - if (results[0].totalCount && results[0].totalCount.length > 0) { - derivedTotalCount = results[0].totalCount[0].totalCount; - } - return { - pagination: { - total: derivedTotalCount, - offset: options.offset, - limit: options.limit - }, - data: results[0].documents - }; - } else { - return results[0].documents; - } + if (!stixId) { + const err = new MissingParameterError({ parameterName: 'stixId' }); + if (callback) { + return callback(err); + } + throw err; } - async retrieveAll(options, callback) { - BaseService.requireServices(); - if (BaseService.isCallback(arguments[arguments.length - 1])) { - callback = arguments[arguments.length - 1]; - } + try { + if (options.versions === 'all') { + const documents = await this.repository.retrieveAllById(stixId); - let results; try { - results = await this.repository.retrieveAll(options); + await BaseService.identitiesService.addCreatedByAndModifiedByIdentitiesToAll(documents); } catch (err) { - const databaseError = new DatabaseError(err); // Let the DatabaseError bubble up - if (callback) { - return callback(databaseError); - } - throw databaseError; + const identityError = new IdentityServiceError({ + details: err.message, + cause: err, + }); + if (callback) { + return callback(identityError); + } + throw identityError; } - - try { - await BaseService.identitiesService.addCreatedByAndModifiedByIdentitiesToAll(results[0].documents); - } catch (err) { + if (callback) { + return callback(null, documents); + } + return documents; + } else if (options.versions === 'latest') { + const document = await this.repository.retrieveLatestByStixId(stixId); + + if (document) { + try { + await BaseService.identitiesService.addCreatedByAndModifiedByIdentities(document); + } catch (err) { const identityError = new IdentityServiceError({ - details: err.message, - cause: err + details: err.message, + cause: err, }); if (callback) { - return callback(identityError); + return callback(identityError); } throw identityError; + } + if (callback) { + return callback(null, [document]); + } + return [document]; + } else { + if (callback) { + return callback(null, []); + } + return []; } - - const paginatedResults = BaseService.paginate(options, results); + } else { + const err = new InvalidQueryStringParameterError({ parameterName: 'versions' }); if (callback) { - return callback(null, paginatedResults); + return callback(err); } - return paginatedResults; + throw err; + } + } catch (err) { + if (callback) { + return callback(err); + } + throw err; // Let the DatabaseError bubble up } + } - async retrieveById(stixId, options, callback) { - BaseService.requireServices(); + async retrieveVersionById(stixId, modified, callback) { + BaseService.requireServices(); - if (BaseService.isCallback(arguments[arguments.length - 1])) { - callback = arguments[arguments.length - 1]; - } + if (BaseService.isCallback(arguments[arguments.length - 1])) { + callback = arguments[arguments.length - 1]; + } + if (!stixId) { + const err = new MissingParameterError({ parameterName: 'stixId' }); + if (callback) { + return callback(err); + } + throw err; + } - if (!stixId) { - const err = new MissingParameterError({ parameterName: 'stixId' }); - if (callback) { - return callback(err); - } - throw err; - } + if (!modified) { + const err = new MissingParameterError({ parameterName: 'modified' }); + if (callback) { + return callback(err); + } + throw err; + } + // eslint-disable-next-line no-useless-catch + try { + const document = await this.repository.retrieveOneByVersion(stixId, modified); + + if (!document) { + if (callback) { + return callback(null, null); + } + return null; + } else { try { - if (options.versions === 'all') { - const documents = await this.repository.retrieveAllById(stixId); - - try { - await BaseService.identitiesService.addCreatedByAndModifiedByIdentitiesToAll(documents); - } catch (err) { - const identityError = new IdentityServiceError({ - details: err.message, - cause: err - }); - if (callback) { - return callback(identityError); - } - throw identityError; - } - if (callback) { - return callback(null, documents); - } - return documents; - - } else if (options.versions === 'latest') { - const document = await this.repository.retrieveLatestByStixId(stixId); - - if (document) { - try { - await BaseService.identitiesService.addCreatedByAndModifiedByIdentities(document); - } catch (err) { - const identityError = new IdentityServiceError({ - details: err.message, - cause: err - }); - if (callback) { - return callback(identityError); - } - throw identityError; - } - if (callback) { - return callback(null, [document]); - } - return [document]; - } else { - if (callback) { - return callback(null, []); - } - return []; - } - - } else { - const err = new InvalidQueryStringParameterError({ parameterName: 'versions' }); - if (callback) { - return callback(err); - } - throw err; - } + await BaseService.identitiesService.addCreatedByAndModifiedByIdentities(document); } catch (err) { - if (callback) { - return callback(err); - } - throw err; // Let the DatabaseError bubble up + const identityError = new IdentityServiceError({ + details: err.message, + cause: err, + }); + if (callback) { + return callback(identityError); + } + throw identityError; + } + if (callback) { + return callback(null, document); } + return document; + } + } catch (err) { + if (callback) { + return callback(err); + } + throw err; // Let the DatabaseError bubble up } + } - async retrieveVersionById(stixId, modified, callback) { - BaseService.requireServices(); + async create(data, options, callback) { + BaseService.requireServices(); - if (BaseService.isCallback(arguments[arguments.length - 1])) { - callback = arguments[arguments.length - 1]; - } - if (!stixId) { - const err = new MissingParameterError({ parameterName: 'stixId' }); - if (callback) { - return callback(err); - } - throw err; - } + if (BaseService.isCallback(arguments[arguments.length - 1])) { + callback = arguments[arguments.length - 1]; + } - if (!modified) { - const err = new MissingParameterError({ parameterName: 'modified' }); - if (callback) { - return callback(err); - } - throw err; - } + if (data?.stix?.type !== this.type) { + throw new InvalidTypeError(); + } - // eslint-disable-next-line no-useless-catch - try { - const document = await this.repository.retrieveOneByVersion(stixId, modified); - - if (!document) { - if (callback) { - return callback(null, null); - } - return null; - } else { - try { - await BaseService.identitiesService.addCreatedByAndModifiedByIdentities(document); - } catch (err) { - const identityError = new IdentityServiceError({ - details: err.message, - cause: err - }); - if (callback) { - return callback(identityError); - } - throw identityError; - } - if (callback) { - return callback(null, document); - } - return document; - } - } catch (err) { - if (callback) { - return callback(err); - } - throw err; // Let the DatabaseError bubble up + // eslint-disable-next-line no-useless-catch + try { + // This function handles two use cases: + // 1. This is a completely new object. Create a new object and generate the stix.id if not already + // provided. Set both stix.created_by_ref and stix.x_mitre_modified_by_ref to the organization identity. + // 2. This is a new version of an existing object. Create a new object with the specified id. + // Set stix.x_mitre_modified_by_ref to the organization identity. + + options = options || {}; + if (!options.import) { + // Set the ATT&CK Spec Version + data.stix.x_mitre_attack_spec_version = + data.stix.x_mitre_attack_spec_version ?? config.app.attackSpecVersion; + + // Record the user account that created the object + if (options.userAccountId) { + data.workspace.workflow.created_by_user_account = options.userAccountId; } - } - async create(data, options, callback) { - BaseService.requireServices(); + // Set the default marking definitions + await BaseService.systemConfigurationService.setDefaultMarkingDefinitionsForObject(data); - if (BaseService.isCallback(arguments[arguments.length - 1])) { - callback = arguments[arguments.length - 1]; - } + // Get the organization identity + const organizationIdentityRef = + await systemConfigurationService.retrieveOrganizationIdentityRef(); - if (data?.stix?.type !== this.type) { - throw new InvalidTypeError(); + // Check for an existing object + let existingObject; + if (data.stix.id) { + existingObject = await this.repository.retrieveOneById(data.stix.id); } - // eslint-disable-next-line no-useless-catch - try { - // This function handles two use cases: - // 1. This is a completely new object. Create a new object and generate the stix.id if not already - // provided. Set both stix.created_by_ref and stix.x_mitre_modified_by_ref to the organization identity. - // 2. This is a new version of an existing object. Create a new object with the specified id. - // Set stix.x_mitre_modified_by_ref to the organization identity. - - options = options || {}; - if (!options.import) { - // Set the ATT&CK Spec Version - data.stix.x_mitre_attack_spec_version = data.stix.x_mitre_attack_spec_version ?? config.app.attackSpecVersion; - - // Record the user account that created the object - if (options.userAccountId) { - data.workspace.workflow.created_by_user_account = options.userAccountId; - } - - // Set the default marking definitions - await BaseService.systemConfigurationService.setDefaultMarkingDefinitionsForObject(data); - - // Get the organization identity - const organizationIdentityRef = await systemConfigurationService.retrieveOrganizationIdentityRef(); - - // Check for an existing object - let existingObject; - if (data.stix.id) { - existingObject = await this.repository.retrieveOneById(data.stix.id); - } - - if (existingObject) { - // New version of an existing object - // Only set the x_mitre_modified_by_ref property - data.stix.x_mitre_modified_by_ref = organizationIdentityRef; - } - else { - // New object - // Assign a new STIX id if not already provided - if (!data.stix.id) { - // const stixIdPrefix = getStixIdPrefixFromModel(this.model.modelName, data.stix.type); - data.stix.id = `${data.stix.type}--${uuid.v4()}`; - } - - // Set the created_by_ref and x_mitre_modified_by_ref properties - data.stix.created_by_ref = organizationIdentityRef; - data.stix.x_mitre_modified_by_ref = organizationIdentityRef; - } - } - const res = await this.repository.save(data); - if (callback) { - return callback(null, res); - } - return res; - } catch (err) { - if (callback) { - return callback(err); - } - throw err; + if (existingObject) { + // New version of an existing object + // Only set the x_mitre_modified_by_ref property + data.stix.x_mitre_modified_by_ref = organizationIdentityRef; + } else { + // New object + // Assign a new STIX id if not already provided + if (!data.stix.id) { + // const stixIdPrefix = getStixIdPrefixFromModel(this.model.modelName, data.stix.type); + data.stix.id = `${data.stix.type}--${uuid.v4()}`; + } + + // Set the created_by_ref and x_mitre_modified_by_ref properties + data.stix.created_by_ref = organizationIdentityRef; + data.stix.x_mitre_modified_by_ref = organizationIdentityRef; } + } + const res = await this.repository.save(data); + if (callback) { + return callback(null, res); + } + return res; + } catch (err) { + if (callback) { + return callback(err); + } + throw err; } + } - async updateFull(stixId, stixModified, data, callback) { - if (BaseService.isCallback(arguments[arguments.length - 1])) { - callback = arguments[arguments.length - 1]; - } - if (!stixId) { - const err = new MissingParameterError({ parameterName: 'stixId' }); - if (callback) { - return callback(err); - } - throw err; - } - - if (!stixModified) { - const err = new MissingParameterError({ parameterName: 'modified' }); - if (callback) { - return callback(err); - } - throw err; - } + async updateFull(stixId, stixModified, data, callback) { + if (BaseService.isCallback(arguments[arguments.length - 1])) { + callback = arguments[arguments.length - 1]; + } + if (!stixId) { + const err = new MissingParameterError({ parameterName: 'stixId' }); + if (callback) { + return callback(err); + } + throw err; + } - let document; - // eslint-disable-next-line no-useless-catch - try { - document = await this.repository.retrieveOneByVersion(stixId, stixModified); - } catch (err) { - if (callback) { - return callback(err); - } - throw err; - } + if (!stixModified) { + const err = new MissingParameterError({ parameterName: 'modified' }); + if (callback) { + return callback(err); + } + throw err; + } - if (!document) { - if (callback) { - return callback(null, null); - } - return null; - } + let document; + // eslint-disable-next-line no-useless-catch + try { + document = await this.repository.retrieveOneByVersion(stixId, stixModified); + } catch (err) { + if (callback) { + return callback(err); + } + throw err; + } - try { - const newDocument = await this.repository.updateAndSave(document, data); - - if (newDocument === document) { - // Document successfully saved - if (callback) { - return callback(null, newDocument); - } - return newDocument; - } else { - const err = new DatabaseError({ - details: 'Document could not be saved', - document // Pass along the document that could not be saved - }); - if (callback) { - return callback(err); - } - throw err; - } - } catch (err) { - if (callback) { - return callback(err); - } - throw err; - } + if (!document) { + if (callback) { + return callback(null, null); + } + return null; } - async deleteVersionById(stixId, stixModified, callback) { - if (BaseService.isCallback(arguments[arguments.length - 1])) { - callback = arguments[arguments.length - 1]; + try { + const newDocument = await this.repository.updateAndSave(document, data); + + if (newDocument === document) { + // Document successfully saved + if (callback) { + return callback(null, newDocument); } - if (!stixId) { - const err = new MissingParameterError({ parameterName: 'stixId' }); - if (callback) { - return callback(err); - } - throw err; + return newDocument; + } else { + const err = new DatabaseError({ + details: 'Document could not be saved', + document, // Pass along the document that could not be saved + }); + if (callback) { + return callback(err); } + throw err; + } + } catch (err) { + if (callback) { + return callback(err); + } + throw err; + } + } - if (!stixModified) { - const err = new MissingParameterError({ parameterName: 'modified' }); - if (callback) { - return callback(err); - } - throw err; - } - // eslint-disable-next-line no-useless-catch - try { - const document = await this.repository.findOneAndRemove(stixId, stixModified); - - if (!document) { - //Note: document is null if not found - if (callback) { - return callback(null, null); - } - return null; - } - if (callback) { - return callback(null, document); - } - return document; + async deleteVersionById(stixId, stixModified, callback) { + if (BaseService.isCallback(arguments[arguments.length - 1])) { + callback = arguments[arguments.length - 1]; + } + if (!stixId) { + const err = new MissingParameterError({ parameterName: 'stixId' }); + if (callback) { + return callback(err); + } + throw err; + } - } catch (err) { - if (callback) { - return callback(err); - } - throw err; - } + if (!stixModified) { + const err = new MissingParameterError({ parameterName: 'modified' }); + if (callback) { + return callback(err); + } + throw err; } + // eslint-disable-next-line no-useless-catch + try { + const document = await this.repository.findOneAndRemove(stixId, stixModified); - async deleteById(stixId, callback) { - if (BaseService.isCallback(arguments[arguments.length - 1])) { - callback = arguments[arguments.length - 1]; - } - if (!stixId) { - const err = new MissingParameterError({ parameterName: 'stixId' }); - if (callback) { - return callback(err); - } - throw err; - } - // eslint-disable-next-line no-useless-catch - try { - const res = await this.repository.deleteMany(stixId); - if (callback) { - return callback(null, res); - } - return res; - } catch (err) { - if (callback) { - return callback(err); - } - throw err; + if (!document) { + //Note: document is null if not found + if (callback) { + return callback(null, null); } + return null; + } + if (callback) { + return callback(null, document); + } + return document; + } catch (err) { + if (callback) { + return callback(err); + } + throw err; + } + } + + async deleteById(stixId, callback) { + if (BaseService.isCallback(arguments[arguments.length - 1])) { + callback = arguments[arguments.length - 1]; + } + if (!stixId) { + const err = new MissingParameterError({ parameterName: 'stixId' }); + if (callback) { + return callback(err); + } + throw err; + } + // eslint-disable-next-line no-useless-catch + try { + const res = await this.repository.deleteMany(stixId); + if (callback) { + return callback(null, res); + } + return res; + } catch (err) { + if (callback) { + return callback(err); + } + throw err; } + } } -module.exports = BaseService; \ No newline at end of file +module.exports = BaseService; diff --git a/app/services/assets-service.js b/app/services/assets-service.js index 888ab728..5f22e5ea 100644 --- a/app/services/assets-service.js +++ b/app/services/assets-service.js @@ -4,6 +4,6 @@ const assetsRepository = require('../repository/assets-repository'); const BaseService = require('./_base.service'); -class AssetsService extends BaseService { } +class AssetsService extends BaseService {} -module.exports = new AssetsService('x-mitre-asset', assetsRepository); \ No newline at end of file +module.exports = new AssetsService('x-mitre-asset', assetsRepository); diff --git a/app/services/attack-objects-service.js b/app/services/attack-objects-service.js index 475e5605..1dacef36 100644 --- a/app/services/attack-objects-service.js +++ b/app/services/attack-objects-service.js @@ -10,119 +10,122 @@ const relationshipsService = require('./relationships-service'); const { DatabaseError, IdentityServiceError, NotImplementedError } = require('../exceptions'); class AttackObjectsService extends BaseService { - - async retrieveAll(options, callback) { - if (AttackObjectsService.isCallback(arguments[arguments.length - 1])) { - callback = arguments[arguments.length - 1]; - } - - let results; - try { - results = await this.repository.retrieveAll(options); - } catch (err) { - const databaseError = new DatabaseError(err); // Let the DatabaseError bubble up - if (callback) { - return callback(databaseError); - } - throw databaseError; - } - - try { - await identitiesService.addCreatedByAndModifiedByIdentitiesToAll(results[0].documents); - } catch (err) { - const identityError = new IdentityServiceError({ - details: err.message, - cause: err - }); - if (callback) { - return callback(identityError); - } - throw identityError; - } - // Add relationships from separate collection - if (!options.attackId && !options.search) { - const relationshipsOptions = { - includeRevoked: options.includeRevoked, - includeDeprecated: options.includeDeprecated, - state: options.state, - versions: options.versions, - lookupRefs: false, - includeIdentities: false, - lastUpdatedBy: options.lastUpdatedBy - }; - const relationships = await relationshipsService.retrieveAll(relationshipsOptions); - if (relationships.length > 0) { - results[0].documents = results[0].documents.concat(relationships); - results[0].totalCount[0].totalCount += 1; - } - } - - const paginatedResults = AttackObjectsService.paginate(options, results); - if (callback) { - return callback(null, paginatedResults); - } - return paginatedResults; + async retrieveAll(options, callback) { + if (AttackObjectsService.isCallback(arguments[arguments.length - 1])) { + callback = arguments[arguments.length - 1]; } - retrieveById(stixId, options, callback) { - throw new NotImplementedError(this.constructor.name, 'retrieveById'); + let results; + try { + results = await this.repository.retrieveAll(options); + } catch (err) { + const databaseError = new DatabaseError(err); // Let the DatabaseError bubble up + if (callback) { + return callback(databaseError); + } + throw databaseError; } - create(data, options, callback) { - throw new NotImplementedError(this.constructor.name, 'create'); + try { + await identitiesService.addCreatedByAndModifiedByIdentitiesToAll(results[0].documents); + } catch (err) { + const identityError = new IdentityServiceError({ + details: err.message, + cause: err, + }); + if (callback) { + return callback(identityError); + } + throw identityError; } - - updateFull(stixId, stixModified, data, callback) { - throw new NotImplementedError(this.constructor.name, 'updateFull'); + // Add relationships from separate collection + if (!options.attackId && !options.search) { + const relationshipsOptions = { + includeRevoked: options.includeRevoked, + includeDeprecated: options.includeDeprecated, + state: options.state, + versions: options.versions, + lookupRefs: false, + includeIdentities: false, + lastUpdatedBy: options.lastUpdatedBy, + }; + const relationships = await relationshipsService.retrieveAll(relationshipsOptions); + if (relationships.length > 0) { + results[0].documents = results[0].documents.concat(relationships); + results[0].totalCount[0].totalCount += 1; + } } - deleteVersionById(stixId, stixModified, callback) { - throw new NotImplementedError(this.constructor.name, 'deleteVersionById'); + const paginatedResults = AttackObjectsService.paginate(options, results); + if (callback) { + return callback(null, paginatedResults); } - - deleteById(stixId, callback) { - throw new NotImplementedError(this.constructor.name, 'deleteById'); + return paginatedResults; + } + + retrieveById(stixId, options, callback) { + throw new NotImplementedError(this.constructor.name, 'retrieveById'); + } + + create(data, options, callback) { + throw new NotImplementedError(this.constructor.name, 'create'); + } + + updateFull(stixId, stixModified, data, callback) { + throw new NotImplementedError(this.constructor.name, 'updateFull'); + } + + deleteVersionById(stixId, stixModified, callback) { + throw new NotImplementedError(this.constructor.name, 'deleteVersionById'); + } + + deleteById(stixId, callback) { + throw new NotImplementedError(this.constructor.name, 'deleteById'); + } + + // Record that this object is part of a collection + async insertCollection(stixId, modified, collectionId, collectionModified) { + let attackObject; + if (stixId.startsWith('relationship')) { + // TBD: Use relationships service when that is converted to async + attackObject = await Relationship.findOne({ + 'stix.id': stixId, + 'stix.modified': modified, + }); + } else { + attackObject = await this.repository.retrieveOneByVersion(stixId, modified); } - // Record that this object is part of a collection - async insertCollection(stixId, modified, collectionId, collectionModified) { - let attackObject; - if (stixId.startsWith('relationship')) { - // TBD: Use relationships service when that is converted to async - attackObject = await Relationship.findOne({ 'stix.id': stixId, 'stix.modified': modified }); - } - else { - attackObject = await this.repository.retrieveOneByVersion(stixId, modified); - } - - if (attackObject) { - // Create the collection reference - const collection = { - collection_ref: collectionId, - collection_modified: collectionModified - }; - - // Make sure the exports array exists and add the collection reference - if (!attackObject.workspace.collections) { - attackObject.workspace.collections = []; - } - - // Check to see if the collection is already added - // (collection with same id and version should only be created--and therefore objects added--one time) - const duplicateCollection = attackObject.workspace.collections.find( - item => item.collection_ref === collection.collection_ref && item.collection_modified === collection.collection_modified); - if (duplicateCollection) { - throw new Error(this.errors.duplicateCollection); - } - - attackObject.workspace.collections.push(collection); - - await attackObject.save(); - } - else { - throw new Error(this.errors.notFound); - } + if (attackObject) { + // Create the collection reference + const collection = { + collection_ref: collectionId, + collection_modified: collectionModified, + }; + + // Make sure the exports array exists and add the collection reference + if (!attackObject.workspace.collections) { + attackObject.workspace.collections = []; + } + + // Check to see if the collection is already added + // (collection with same id and version should only be created--and therefore objects added--one time) + const duplicateCollection = attackObject.workspace.collections.find( + (item) => + item.collection_ref === collection.collection_ref && + item.collection_modified === collection.collection_modified, + ); + if (duplicateCollection) { + throw new Error(this.errors.duplicateCollection); + } + + attackObject.workspace.collections.push(collection); + + await attackObject.save(); + } else { + throw new Error(this.errors.notFound); } + } } -module.exports = new AttackObjectsService('not-a-valid-type', attackObjectsRepository); \ No newline at end of file +module.exports = new AttackObjectsService('not-a-valid-type', attackObjectsRepository); diff --git a/app/services/authentication-service.js b/app/services/authentication-service.js index d9204dfe..3675fc96 100644 --- a/app/services/authentication-service.js +++ b/app/services/authentication-service.js @@ -7,12 +7,12 @@ const jwtDecoder = require('jwt-decode'); const config = require('../config/config'); const errors = { - serviceNotFound: 'Service not found', - invalidChallengeHash: 'Invalid challenge hash', - invalidToken: 'Invalid token', - noMechanismConfigured: 'No authentication mechanism configured', - invalidOidcCredentials: 'Invalid OIDC credentials', - connectionRefused: 'OIDC Provider Connection refused' + serviceNotFound: 'Service not found', + invalidChallengeHash: 'Invalid challenge hash', + invalidToken: 'Invalid token', + noMechanismConfigured: 'No authentication mechanism configured', + invalidOidcCredentials: 'Invalid OIDC credentials', + connectionRefused: 'OIDC Provider Connection refused', }; exports.errors = errors; @@ -23,171 +23,168 @@ let tokenData; * Is the token current? The token must exist and not be expired or about to expire. */ function tokenIsCurrent(tokenType) { - if (!tokenData || tokenData.type !== tokenType) { - return false; - } - else { - // Add cushion: reauthenticate if less than 1 second until expiration - const cushion = 1000; // milliseconds - const now = Date.now(); - return (tokenData.exp * 1000) >= (now + cushion); - } + if (!tokenData || tokenData.type !== tokenType) { + return false; + } else { + // Add cushion: reauthenticate if less than 1 second until expiration + const cushion = 1000; // milliseconds + const now = Date.now(); + return tokenData.exp * 1000 >= now + cushion; + } } /** * Get the challenge string (nonce) from the REST API service */ async function getApikeyChallengeFromServer() { - try { - const challengeResponse = await request - .get(`${ config.workbench.restApiBaseUrl }/api/authn/service/apikey-challenge?serviceName=${ config.workbench.authn.apikey.serviceName }`); - return challengeResponse.body.challenge; - } - catch (err) { - if (err.status === 404 && err.response.text === 'Service not found') { - throw new Error(errors.serviceNotFound); - } else { - throw err; - } + try { + const challengeResponse = await request.get( + `${config.workbench.restApiBaseUrl}/api/authn/service/apikey-challenge?serviceName=${config.workbench.authn.apikey.serviceName}`, + ); + return challengeResponse.body.challenge; + } catch (err) { + if (err.status === 404 && err.response.text === 'Service not found') { + throw new Error(errors.serviceNotFound); + } else { + throw err; } + } } /** * Compute the HMAC-SHA256 hash of the challenge string using the configured API Key. */ function makeChallengeHash(nonce) { - const hmac = crypto.createHmac('sha256', config.workbench.authn.apikey.apikey); - hmac.update(nonce); - return hmac.digest('hex'); + const hmac = crypto.createHmac('sha256', config.workbench.authn.apikey.apikey); + hmac.update(nonce); + return hmac.digest('hex'); } /** * Send the computed challenge hash to the REST API service and receive the access token. */ async function getApikeyAccessTokenFromServer(challengeHash) { + try { + const tokenResponse = await request + .get( + `${config.workbench.restApiBaseUrl}/api/authn/service/apikey-token?serviceName=${config.workbench.authn.apikey.serviceName}`, + ) + .set('Authorization', `Apikey ${challengeHash}`); + + const accessToken = tokenResponse.body.access_token; + let decodedToken; try { - const tokenResponse = await request - .get(`${ config.workbench.restApiBaseUrl }/api/authn/service/apikey-token?serviceName=${ config.workbench.authn.apikey.serviceName }`) - .set('Authorization', `Apikey ${ challengeHash }`); - - const accessToken = tokenResponse.body.access_token; - let decodedToken; - try { - decodedToken = jwtDecoder(accessToken); - } - catch(err) { - throw new Error(errors.invalidToken); - } - - return { - accessToken, - service: decodedToken.serviceName, - exp: decodedToken.exp, - type: 'apikey' - }; + decodedToken = jwtDecoder(accessToken); + } catch (err) { + throw new Error(errors.invalidToken); } - catch (err) { - if (err.status === 404 && err.response.text === 'Service not found') { - throw new Error(errors.serviceNotFound); - } else if (err.status === 400 && err.response.text === 'Invalid challenge hash') { - throw new Error(errors.invalidChallengeHash); - } else { - throw err; - } + + return { + accessToken, + service: decodedToken.serviceName, + exp: decodedToken.exp, + type: 'apikey', + }; + } catch (err) { + if (err.status === 404 && err.response.text === 'Service not found') { + throw new Error(errors.serviceNotFound); + } else if (err.status === 400 && err.response.text === 'Invalid challenge hash') { + throw new Error(errors.invalidChallengeHash); + } else { + throw err; } + } } /** * Get the access token from the OIDC Identity Provider */ async function getClientCredentialsAccessTokenFromServer() { - let accessToken; - try { - const body = { - client_id: config.workbench.authn.oidcClientCredentials.clientId, - client_secret: config.workbench.authn.oidcClientCredentials.clientSecret, - grant_type: 'client_credentials' - }; - - // Some OIDC Identity Providers require scope - if (config.workbench.authn.oidcClientCredentials.scope) { - body.scope = config.workbench.authn.oidcClientCredentials.scope; - } - - const res = await request - .post(config.workbench.authn.oidcClientCredentials.tokenUrl) - .type('form') - .send(body); - accessToken = res.body.access_token; - } - catch(err) { - if (err.status === 401) { - throw new Error(errors.invalidOidcCredentials); - } - else if (err.status === 400 && err?.response?.body.error_description === 'Invalid client credentials') { - throw new Error(errors.invalidOidcCredentials); - } else if (err.code === 'ECONNREFUSED') { - throw new Error(errors.connectionRefused, { cause: err }); - } else { - throw err; - } - } - let decodedToken; - try { - decodedToken = jwtDecoder(accessToken); - } - catch(err) { - throw new Error(errors.invalidToken); + let accessToken; + try { + const body = { + client_id: config.workbench.authn.oidcClientCredentials.clientId, + client_secret: config.workbench.authn.oidcClientCredentials.clientSecret, + grant_type: 'client_credentials', + }; + + // Some OIDC Identity Providers require scope + if (config.workbench.authn.oidcClientCredentials.scope) { + body.scope = config.workbench.authn.oidcClientCredentials.scope; } - return { - accessToken, - service: decodedToken.serviceName, - exp: decodedToken.exp, - type: 'client-credentials' - }; + const res = await request + .post(config.workbench.authn.oidcClientCredentials.tokenUrl) + .type('form') + .send(body); + accessToken = res.body.access_token; + } catch (err) { + if (err.status === 401) { + throw new Error(errors.invalidOidcCredentials); + } else if ( + err.status === 400 && + err?.response?.body.error_description === 'Invalid client credentials' + ) { + throw new Error(errors.invalidOidcCredentials); + } else if (err.code === 'ECONNREFUSED') { + throw new Error(errors.connectionRefused, { cause: err }); + } else { + throw err; + } + } + let decodedToken; + try { + decodedToken = jwtDecoder(accessToken); + } catch (err) { + throw new Error(errors.invalidToken); + } + + return { + accessToken, + service: decodedToken.serviceName, + exp: decodedToken.exp, + type: 'client-credentials', + }; } /** * Get the API Key access token, using the cached value if it is current. */ async function getApikeyAccessToken() { - if (!tokenIsCurrent('apikey')) { - const nonce = await getApikeyChallengeFromServer(); - const challengeHash = makeChallengeHash(nonce); - tokenData = await getApikeyAccessTokenFromServer(challengeHash); - } + if (!tokenIsCurrent('apikey')) { + const nonce = await getApikeyChallengeFromServer(); + const challengeHash = makeChallengeHash(nonce); + tokenData = await getApikeyAccessTokenFromServer(challengeHash); + } - return tokenData.accessToken; + return tokenData.accessToken; } /** * Get the client credentials access token, using the cached value if it is current. */ async function getClientCredentialsAccessToken() { - if (!tokenIsCurrent('client-credentials')) { - tokenData = await getClientCredentialsAccessTokenFromServer(); - } + if (!tokenIsCurrent('client-credentials')) { + tokenData = await getClientCredentialsAccessTokenFromServer(); + } - return tokenData.accessToken; + return tokenData.accessToken; } /** * Get the access token that can be used to access the REST API service */ -exports.getAccessToken = function() { - if (config.workbench.authn.mechanism === 'apikey') { - return getApikeyAccessToken(); - } - else if (config.workbench.authn.mechanism === 'client-credentials') { - return getClientCredentialsAccessToken(); - } - else { - throw new Error(errors.noMechanismConfigured); - } -} +exports.getAccessToken = function () { + if (config.workbench.authn.mechanism === 'apikey') { + return getApikeyAccessToken(); + } else if (config.workbench.authn.mechanism === 'client-credentials') { + return getClientCredentialsAccessToken(); + } else { + throw new Error(errors.noMechanismConfigured); + } +}; // Invalidate the cached token and force a new authentication with the service -exports.invalidateToken = function() { - tokenData = undefined; -} +exports.invalidateToken = function () { + tokenData = undefined; +}; diff --git a/app/services/campaigns-service.js b/app/services/campaigns-service.js index 4b0c2b19..ab384132 100644 --- a/app/services/campaigns-service.js +++ b/app/services/campaigns-service.js @@ -4,6 +4,6 @@ const campaignsRepository = require('../repository/campaigns-repository'); const BaseService = require('./_base.service'); -class CampaignService extends BaseService { } +class CampaignService extends BaseService {} -module.exports = new CampaignService('campaign', campaignsRepository); \ No newline at end of file +module.exports = new CampaignService('campaign', campaignsRepository); diff --git a/app/services/collection-bundles-service.js b/app/services/collection-bundles-service.js index 3ec6cd3b..5f8d7f11 100644 --- a/app/services/collection-bundles-service.js +++ b/app/services/collection-bundles-service.js @@ -27,850 +27,925 @@ const logger = require('../lib/logger'); const config = require('../config/config'); const async = require('async'); -const systemConfigurationService = require("./system-configuration-service"); +const systemConfigurationService = require('./system-configuration-service'); const linkById = require('../lib/linkById'); -const Note = require("../models/note-model"); -const { DuplicateIdError } = require("../exceptions"); +const Note = require('../models/note-model'); +const { DuplicateIdError } = require('../exceptions'); const forceImportParameters = { - attackSpecVersionViolations: 'attack-spec-version-violations', - duplicateCollection: 'duplicate-collection' -} + attackSpecVersionViolations: 'attack-spec-version-violations', + duplicateCollection: 'duplicate-collection', +}; exports.forceImportParameters = forceImportParameters; const errors = { - duplicateCollection: 'Duplicate collection', - notFound: 'Collection not found', - attackSpecVersionViolation: 'ATT&CK Spec version violation' + duplicateCollection: 'Duplicate collection', + notFound: 'Collection not found', + attackSpecVersionViolation: 'ATT&CK Spec version violation', }; exports.errors = errors; const validationErrors = { - duplicateObjectInBundle: 'Duplicate object in bundle', - invalidAttackSpecVersion: 'Invalid ATT&CK Spec version' + duplicateObjectInBundle: 'Duplicate object in bundle', + invalidAttackSpecVersion: 'Invalid ATT&CK Spec version', }; exports.validationErrors = validationErrors; const importErrors = { - duplicateCollection: 'Duplicate collection object', - retrievalError: 'Retrieval error', - unknownObjectType: 'Unknown object type', - notInContents: 'Not in contents', // object in bundle but not in x_mitre_contents - missingObject: 'Missing object', // object in x_mitre_contents but not in bundle - saveError: 'Save error', - attackSpecVersionViolation: 'ATT&CK Spec version violation' -} + duplicateCollection: 'Duplicate collection object', + retrievalError: 'Retrieval error', + unknownObjectType: 'Unknown object type', + notInContents: 'Not in contents', // object in bundle but not in x_mitre_contents + missingObject: 'Missing object', // object in x_mitre_contents but not in bundle + saveError: 'Save error', + attackSpecVersionViolation: 'ATT&CK Spec version violation', +}; // Default ATT&CK Spec version to use for objects that do not have x_mitre_attack_spec_version set // This isn't saved, but is used for comparisons const defaultAttackSpecVersion = '2.0.0'; function makeKey(stixId, modified) { - return stixId + '/' + modified; + return stixId + '/' + modified; } function makeKeyFromObject(stixObject) { - if (stixObject.type === 'marking-definition') { - return makeKey(stixObject.id, stixObject.created); - } - else { - return makeKey(stixObject.id, stixObject.modified); - } + if (stixObject.type === 'marking-definition') { + return makeKey(stixObject.id, stixObject.created); + } else { + return makeKey(stixObject.id, stixObject.modified); + } } // Convert the date to seconds past the epoch // Works for both Date and string types function toEpoch(date) { - if (date instanceof Date) { - return date.getTime(); - } - else { - return Date.parse(date); - } + if (date instanceof Date) { + return date.getTime(); + } else { + return Date.parse(date); + } } -exports.validateBundle = function(bundle) { - const validationResult = { - errors: [], - duplicateObjectInBundleCount: 0, - invalidAttackSpecVersionCount: 0 - }; - - // Validate the objects in the bundle - const objectMap = new Map(); - for (const stixObject of bundle.objects) { - // Check for a duplicate object - const key = makeKey(stixObject.id, stixObject.modified); - if (objectMap.has(key)) { - // Object already in map: duplicate STIX id and modified date - const error = { - type: validationErrors.duplicateObjectInBundle, - id: stixObject.id, - modified: stixObject.modified - }; - validationResult.errors.push(error); - validationResult.duplicateObjectInBundleCount += 1; - } else { - objectMap.set(makeKey(stixObject.id, stixObject.modified), stixObject); - } - - // Check the ATT&CK Spec version - const objectAttackSpecVersion = stixObject.x_mitre_attack_spec_version ?? defaultAttackSpecVersion; - if (!semver.valid(objectAttackSpecVersion)) { - // Object's ATT&CK Spec version isn't a correctly formatted semantic version - const error = { - type: validationErrors.invalidAttackSpecVersion, - id: stixObject.id, - modified: stixObject.modified - }; - validationResult.errors.push(error); - validationResult.invalidAttackSpecVersionCount += 1; - } - else if (semver.gt(objectAttackSpecVersion, config.app.attackSpecVersion)) { - // Object's ATT&CK Spec version is newer than system can process - const error = { - type: validationErrors.invalidAttackSpecVersion, - id: stixObject.id, - modified: stixObject.modified - }; - validationResult.errors.push(error); - validationResult.invalidAttackSpecVersionCount += 1; - } +exports.validateBundle = function (bundle) { + const validationResult = { + errors: [], + duplicateObjectInBundleCount: 0, + invalidAttackSpecVersionCount: 0, + }; + + // Validate the objects in the bundle + const objectMap = new Map(); + for (const stixObject of bundle.objects) { + // Check for a duplicate object + const key = makeKey(stixObject.id, stixObject.modified); + if (objectMap.has(key)) { + // Object already in map: duplicate STIX id and modified date + const error = { + type: validationErrors.duplicateObjectInBundle, + id: stixObject.id, + modified: stixObject.modified, + }; + validationResult.errors.push(error); + validationResult.duplicateObjectInBundleCount += 1; + } else { + objectMap.set(makeKey(stixObject.id, stixObject.modified), stixObject); } - return validationResult; -} - -exports.importBundle = function(collection, data, options, callback) { - const referenceImportResults = { - uniqueReferences: 0, - duplicateReferences: 0, - aliasReferences: 0 - }; + // Check the ATT&CK Spec version + const objectAttackSpecVersion = + stixObject.x_mitre_attack_spec_version ?? defaultAttackSpecVersion; + if (!semver.valid(objectAttackSpecVersion)) { + // Object's ATT&CK Spec version isn't a correctly formatted semantic version + const error = { + type: validationErrors.invalidAttackSpecVersion, + id: stixObject.id, + modified: stixObject.modified, + }; + validationResult.errors.push(error); + validationResult.invalidAttackSpecVersionCount += 1; + } else if (semver.gt(objectAttackSpecVersion, config.app.attackSpecVersion)) { + // Object's ATT&CK Spec version is newer than system can process + const error = { + type: validationErrors.invalidAttackSpecVersion, + id: stixObject.id, + modified: stixObject.modified, + }; + validationResult.errors.push(error); + validationResult.invalidAttackSpecVersionCount += 1; + } + } - // Create the collection reference - const collectionReference = { - collection_ref: collection.id, - collection_modified: collection.modified - }; + return validationResult; +}; - // Create the x-mitre-collection object - const importedCollection = { - workspace: { - imported: new Date().toISOString(), - exported: [], - import_categories: { - additions: [], - changes: [], - minor_changes: [], - revocations: [], - deprecations: [], - supersedes_user_edits: [], - supersedes_collection_changes: [], - duplicates: [], - out_of_date: [], - errors: [] - }, - import_references: { - additions: [], - changes: [], - duplicates: [] +exports.importBundle = function (collection, data, options, callback) { + const referenceImportResults = { + uniqueReferences: 0, + duplicateReferences: 0, + aliasReferences: 0, + }; + + // Create the collection reference + const collectionReference = { + collection_ref: collection.id, + collection_modified: collection.modified, + }; + + // Create the x-mitre-collection object + const importedCollection = { + workspace: { + imported: new Date().toISOString(), + exported: [], + import_categories: { + additions: [], + changes: [], + minor_changes: [], + revocations: [], + deprecations: [], + supersedes_user_edits: [], + supersedes_collection_changes: [], + duplicates: [], + out_of_date: [], + errors: [], + }, + import_references: { + additions: [], + changes: [], + duplicates: [], + }, + }, + stix: collection, + }; + + // Build a map of the objects in x_mitre_contents + const contentsMap = new Map(); + for (const entry of collection.x_mitre_contents) { + contentsMap.set(makeKey(entry.object_ref, entry.object_modified), entry); + } + + const importReferences = new Map(); + let duplicateCollection; + + async.series( + [ + // Check for a duplicate x-mitre-collection object + function (callback1) { + collectionsService.retrieveById( + importedCollection.stix.id, + { versions: 'all' }, + function (err, collections) { + if (err) { + return callback1(err); + } else { + duplicateCollection = collections.find( + (collection) => + toEpoch(collection.stix.modified) === toEpoch(importedCollection.stix.modified), + ); + if (duplicateCollection) { + if ( + options.forceImportParameters?.find( + (param) => param === forceImportParameters.duplicateCollection, + ) + ) { + // Duplicate x-mitre-collection object + // Client wants to reimport collection + // Record the warning but continue processing the bundle + const importError = { + object_ref: importedCollection.stix.id, + object_modified: importedCollection.stix.modified, + error_type: importErrors.duplicateCollection, + error_message: `Warning: Duplicate x-mitre-collection object.`, + }; + logger.verbose( + `Import Bundle Warning: Duplicate x-mitre-collection object. Continuing import due to forceImport parameter.`, + ); + importedCollection.workspace.import_categories.errors.push(importError); + + return callback1(); + } else { + const error = new Error(errors.duplicateCollection); + return callback1(error); + } + } else { + return callback1(); + } + } + }, + ); + }, + // Iterate over the objects + function (callback2) { + async.eachLimit( + data.objects, + 8, + function (importObject, callback2a) { + // Check to see if the object is in x_mitre_contents + if ( + !contentsMap.delete(makeKeyFromObject(importObject)) && + importObject.type !== 'x-mitre-collection' + ) { + // Not found in x_mitre_contents + // Record the error but continue processing the object + const importError = { + object_ref: importObject.id, + object_modified: importObject.modified, + error_type: importErrors.notInContents, + error_message: `Warning: Object in bundle but not in x_mitre_contents. Object will be saved in database.`, + }; + logger.verbose( + `Import Bundle Warning: Object not in x_mitre_contents. id = ${importObject.id}, modified = ${importObject.modified}`, + ); + importedCollection.workspace.import_categories.errors.push(importError); } - }, - stix: collection - }; - // Build a map of the objects in x_mitre_contents - const contentsMap = new Map(); - for (const entry of collection.x_mitre_contents) { - contentsMap.set(makeKey(entry.object_ref, entry.object_modified), entry); - } + // Check to see if the object has a later ATT&CK Spec Version than this system can process + const objectAttackSpecVersion = + importObject.x_mitre_attack_spec_version ?? defaultAttackSpecVersion; + if (semver.gt(objectAttackSpecVersion, config.app.attackSpecVersion)) { + // Do not save the object + const importError = { + object_ref: importObject.id, + object_modified: importObject.modified, + error_type: importErrors.attackSpecVersionViolation, + error_message: `Error: Object x_mitre_attack_spec_version later than system.`, + }; + logger.verbose( + `Import Bundle Error: Object's x_mitre_attack_spec_version later than system. id = ${importObject.id}, modified = ${importObject.modified}`, + ); + importedCollection.workspace.import_categories.errors.push(importError); + + if ( + options.forceImportParameters.find( + (param) => param === forceImportParameters.attackSpecVersionViolations, + ) + ) { + // Skip this object but continue the import + return callback2a(); + } else { + // Stop the import + const error = new Error(errors.attackSpecVersionViolation); + return callback2a(error); + } + } - const importReferences = new Map(); - let duplicateCollection; + let service; + if (importObject.type === 'attack-pattern') { + service = techniquesService; + } else if (importObject.type === 'x-mitre-tactic') { + service = tacticsService; + } else if (importObject.type === 'intrusion-set') { + service = groupsService; + } else if (importObject.type === 'campaign') { + service = campaignsService; + } else if (importObject.type === 'course-of-action') { + service = mitigationsService; + } else if (importObject.type === 'malware' || importObject.type === 'tool') { + service = softwareService; + } else if (importObject.type === 'x-mitre-matrix') { + service = matricesService; + } else if (importObject.type === 'relationship') { + service = relationshipService; + } else if (importObject.type === 'marking-definition') { + service = markingDefinitionsService; + } else if (importObject.type === 'identity') { + service = identitiesService; + } else if (importObject.type === 'note') { + service = notesService; + } else if (importObject.type === 'x-mitre-data-source') { + service = dataSourcesService; + } else if (importObject.type === 'x-mitre-data-component') { + service = dataComponentsService; + } else if (importObject.type === 'x-mitre-asset') { + service = assetsService; + } - async.series( - [ - // Check for a duplicate x-mitre-collection object - function(callback1) { - collectionsService.retrieveById(importedCollection.stix.id, { versions: 'all' }, function(err, collections) { - if (err) { - return callback1(err); + if (service) { + // Retrieve all the objects with the same stix ID + service.retrieveById(importObject.id, { versions: 'all' }, function (err, objects) { + if (err) { + // Record the error, but don't cancel the import + const importError = { + object_ref: importObject.id, + object_modified: importObject.modified, + error_type: importErrors.retrievalError, + }; + logger.verbose( + `Import Bundle Error: Unable to retrieve objects with matching STIX id. id = ${importObject.id}, modified = ${importObject.modified}`, + ); + importedCollection.workspace.import_categories.errors.push(importError); + return callback2a(); + } else { + // Is this a duplicate? (same stixId and modified) + if (importObject.type === 'marking-definition') { + const duplicateObject = objects.find( + (object) => toEpoch(object.stix.created) === toEpoch(importObject.created), + ); + if (duplicateObject) { + // Record the duplicate, but don't save it and don't cancel the import + importedCollection.workspace.import_categories.duplicates.push( + importObject.id, + ); + return callback2a(); } - else { - duplicateCollection = collections.find(collection => toEpoch(collection.stix.modified) === toEpoch(importedCollection.stix.modified)); - if (duplicateCollection) { - if (options.forceImportParameters?.find(param => param === forceImportParameters.duplicateCollection)) { - // Duplicate x-mitre-collection object - // Client wants to reimport collection - // Record the warning but continue processing the bundle - const importError = { - object_ref: importedCollection.stix.id, - object_modified: importedCollection.stix.modified, - error_type: importErrors.duplicateCollection, - error_message: `Warning: Duplicate x-mitre-collection object.` - } - logger.verbose(`Import Bundle Warning: Duplicate x-mitre-collection object. Continuing import due to forceImport parameter.`); - importedCollection.workspace.import_categories.errors.push(importError); - - return callback1(); - } - else { - const error = new Error(errors.duplicateCollection); - return callback1(error); - } - } - else { - return callback1(); - } + } else { + const duplicateObject = objects.find( + (object) => toEpoch(object.stix.modified) === toEpoch(importObject.modified), + ); + if (duplicateObject) { + // Record the duplicate, but don't save it and don't cancel the import + importedCollection.workspace.import_categories.duplicates.push( + importObject.id, + ); + return callback2a(); } - }); - }, - // Iterate over the objects - function(callback2) { - async.eachLimit(data.objects, 8, function(importObject, callback2a) { - // Check to see if the object is in x_mitre_contents - if (!contentsMap.delete(makeKeyFromObject(importObject)) && importObject.type !== 'x-mitre-collection') { - // Not found in x_mitre_contents - // Record the error but continue processing the object - const importError = { - object_ref: importObject.id, - object_modified: importObject.modified, - error_type: importErrors.notInContents, - error_message: `Warning: Object in bundle but not in x_mitre_contents. Object will be saved in database.` - } - logger.verbose(`Import Bundle Warning: Object not in x_mitre_contents. id = ${ importObject.id }, modified = ${ importObject.modified }`); - importedCollection.workspace.import_categories.errors.push(importError); - } - - // Check to see if the object has a later ATT&CK Spec Version than this system can process - const objectAttackSpecVersion = importObject.x_mitre_attack_spec_version ?? defaultAttackSpecVersion; - if (semver.gt(objectAttackSpecVersion, config.app.attackSpecVersion)) { - // Do not save the object - const importError = { - object_ref: importObject.id, - object_modified: importObject.modified, - error_type: importErrors.attackSpecVersionViolation, - error_message: `Error: Object x_mitre_attack_spec_version later than system.` - } - logger.verbose(`Import Bundle Error: Object's x_mitre_attack_spec_version later than system. id = ${ importObject.id }, modified = ${ importObject.modified }`); - importedCollection.workspace.import_categories.errors.push(importError); - - if (options.forceImportParameters.find(param => param === forceImportParameters.attackSpecVersionViolations)) { - // Skip this object but continue the import - return callback2a(); - } - else { - // Stop the import - const error = new Error(errors.attackSpecVersionViolation); - return callback2a(error); - } - } - - let service; - if (importObject.type === 'attack-pattern') { - service = techniquesService; - } - else if (importObject.type === 'x-mitre-tactic') { - service = tacticsService; - } - else if (importObject.type === 'intrusion-set') { - service = groupsService; - } - else if (importObject.type === 'campaign') { - service = campaignsService; - } - else if (importObject.type === 'course-of-action') { - service = mitigationsService; - } - else if (importObject.type === 'malware' || importObject.type === 'tool') { - service = softwareService; - } - else if (importObject.type === 'x-mitre-matrix') { - service = matricesService; - } - else if (importObject.type === 'relationship') { - service = relationshipService; - } - else if (importObject.type === 'marking-definition') { - service = markingDefinitionsService; - } - else if (importObject.type === 'identity') { - service = identitiesService; - } - else if (importObject.type === 'note') { - service = notesService; - } - else if (importObject.type === 'x-mitre-data-source') { - service = dataSourcesService; - } - else if (importObject.type === 'x-mitre-data-component') { - service = dataComponentsService; - } - else if (importObject.type === 'x-mitre-asset') { - service = assetsService; + } + + // Is this an addition? (new stixId) + if (objects.length === 0) { + importedCollection.workspace.import_categories.additions.push(importObject.id); + } else { + const latestExistingObject = objects[0]; + + if (importObject.revoked && !latestExistingObject.revoked) { + // This a newly revoked object + importedCollection.workspace.import_categories.revocations.push( + importObject.id, + ); + } else if ( + importObject.x_mitre_deprecated && + !latestExistingObject.x_mitre_deprecated + ) { + // This a newly deprecated object + importedCollection.workspace.import_categories.deprecations.push( + importObject.id, + ); + } else if ( + toEpoch(latestExistingObject.stix.modified) < toEpoch(importObject.modified) + ) { + // TBD: change x_mitre_version comparison from lexical to numerical + if ( + latestExistingObject.stix.x_mitre_version < importObject.x_mitre_version + ) { + // This a change (same stixId, higher x-mitre-version, later modified) + importedCollection.workspace.import_categories.changes.push( + importObject.id, + ); + } else if ( + latestExistingObject.stix.x_mitre_version > importObject.x_mitre_version + ) { + // TBD: How to handle if modified is later, but x_mitre_version is lower + } else { + // This a minor change (same stixId, same x-mitre-version, later modified) + importedCollection.workspace.import_categories.minor_changes.push( + importObject.id, + ); + } + } else { + // Imported object is older than latest existing object + importedCollection.workspace.import_categories.out_of_date.push( + importObject.id, + ); + } + } + + // Extract the references from the object + if ( + importObject.external_references && + Array.isArray(importObject.external_references) + ) { + for (const externalReference of importObject.external_references) { + if ( + externalReference.source_name && + externalReference.description && + !externalReference.external_id + ) { + // Is this reference just an alias? + let isAlias = false; + if (importObject.type === 'intrusion-set') { + if ( + importObject.aliases && + importObject.aliases.includes(externalReference.source_name) + ) { + isAlias = true; + } + } else if ( + importObject.type === 'malware' || + importObject.type === 'tool' + ) { + if ( + importObject.x_mitre_aliases && + importObject.x_mitre_aliases.includes(externalReference.source_name) + ) { + isAlias = true; + } } - if (service) { - // Retrieve all the objects with the same stix ID - service.retrieveById(importObject.id, { versions: 'all' }, function(err, objects) { - if (err) { - // Record the error, but don't cancel the import - const importError = { - object_ref: importObject.id, - object_modified: importObject.modified, - error_type: importErrors.retrievalError - } - logger.verbose(`Import Bundle Error: Unable to retrieve objects with matching STIX id. id = ${ importObject.id }, modified = ${ importObject.modified }`); - importedCollection.workspace.import_categories.errors.push(importError); - return callback2a(); - } - else { - // Is this a duplicate? (same stixId and modified) - if (importObject.type === 'marking-definition') { - const duplicateObject = objects.find(object => toEpoch(object.stix.created) === toEpoch(importObject.created)); - if (duplicateObject) { - // Record the duplicate, but don't save it and don't cancel the import - importedCollection.workspace.import_categories.duplicates.push(importObject.id); - return callback2a(); - } - } - else { - const duplicateObject = objects.find(object => toEpoch(object.stix.modified) === toEpoch(importObject.modified)); - if (duplicateObject) { - // Record the duplicate, but don't save it and don't cancel the import - importedCollection.workspace.import_categories.duplicates.push(importObject.id); - return callback2a(); - } - } - - // Is this an addition? (new stixId) - if (objects.length === 0) { - importedCollection.workspace.import_categories.additions.push(importObject.id); - } - else { - const latestExistingObject = objects[0]; - - if (importObject.revoked && !latestExistingObject.revoked) { - // This a newly revoked object - importedCollection.workspace.import_categories.revocations.push(importObject.id); - } - else if (importObject.x_mitre_deprecated && !latestExistingObject.x_mitre_deprecated) { - // This a newly deprecated object - importedCollection.workspace.import_categories.deprecations.push(importObject.id); - } - else if (toEpoch(latestExistingObject.stix.modified) < toEpoch(importObject.modified)) { - // TBD: change x_mitre_version comparison from lexical to numerical - if (latestExistingObject.stix.x_mitre_version < importObject.x_mitre_version) { - // This a change (same stixId, higher x-mitre-version, later modified) - importedCollection.workspace.import_categories.changes.push(importObject.id); - } - else if (latestExistingObject.stix.x_mitre_version > importObject.x_mitre_version) { - // TBD: How to handle if modified is later, but x_mitre_version is lower - } - else { - // This a minor change (same stixId, same x-mitre-version, later modified) - importedCollection.workspace.import_categories.minor_changes.push(importObject.id); - } - } - else { - // Imported object is older than latest existing object - importedCollection.workspace.import_categories.out_of_date.push(importObject.id); - } - } - - // Extract the references from the object - if (importObject.external_references && Array.isArray(importObject.external_references)) { - for (const externalReference of importObject.external_references) { - if (externalReference.source_name && externalReference.description && !externalReference.external_id) { - - // Is this reference just an alias? - let isAlias = false; - if (importObject.type === 'intrusion-set') { - if (importObject.aliases && importObject.aliases.includes(externalReference.source_name)) { - isAlias = true; - } - } - else if (importObject.type === 'malware' || importObject.type === 'tool') { - if (importObject.x_mitre_aliases && importObject.x_mitre_aliases.includes(externalReference.source_name)) { - isAlias = true; - } - } - - if (isAlias) { - referenceImportResults.aliasReferences++; - } - else { - if (importReferences.has(externalReference.source_name)) { - referenceImportResults.duplicateReferences++; - // if (externalReference.description === importReferences.get(externalReference.source_name)) { - // // Duplicate in collection bundle -- skip - // } else { - // // Duplicate source name in collection bundle, but description is different - // // Skip for now - // } - } else { - referenceImportResults.uniqueReferences++; - importReferences.set(externalReference.source_name, externalReference); - } - } - } - } - } - - // Save the object - if (options.previewOnly) { - // Do nothing - return callback2a(); - } - else { - const newObject = { - workspace: { - collections: [ collectionReference ] - }, - stix: importObject - }; - if (importObject.type === 'marking-definition') { - service.create(newObject, { import: true }) - .then(function (savedObject) { - return callback2a(); - }) - .catch(function(err) { - if (err.message === service.errors.duplicateId) { - return callback2a(err); - } else { - return callback2a(err); - } - }); - } - else { - service.create(newObject, { import: true }) - .then(function(savedObject) { - return callback2a(); - }) - .catch(function(err) { - if (err.message === service.errors?.duplicateId || err instanceof DuplicateIdError) { - // We've checked for this already, so this shouldn't occur - return callback2a(err); - } else { - // Record the error, but don't cancel the import - const importError = { - object_ref: importObject.id, - object_modified: importObject.modified, - error_type: importErrors.saveError, - error_message: err.message - } - logger.verbose(`Import Bundle Error: Unable to save object. id = ${ importObject.id }, modified = ${ importObject.modified }, ${ err.message }`); - importedCollection.workspace.import_categories.errors.push(importError); - return callback2a(); - } - }); - } - } - } - }); - } - else { - if (importObject.type === 'x-mitre-collection') { - // Skip x-mitre-collection objects - return callback2a(); - } - else { - // Unknown object type - // Record the error, but don't cancel the import - const importError = { - object_ref: importObject.id, - object_modified: importObject.modified, - error_type: importErrors.unknownObjectType, - error_message: `Unknown object type: ${ importObject.type }` - } - logger.verbose(`Import Bundle Error: Unknown object type. id = ${ importObject.id }, modified = ${ importObject.modified }, type = ${ importObject.type }`); - importedCollection.workspace.import_categories.errors.push(importError); - return callback2a(); - } + if (isAlias) { + referenceImportResults.aliasReferences++; + } else { + if (importReferences.has(externalReference.source_name)) { + referenceImportResults.duplicateReferences++; + // if (externalReference.description === importReferences.get(externalReference.source_name)) { + // // Duplicate in collection bundle -- skip + // } else { + // // Duplicate source name in collection bundle, but description is different + // // Skip for now + // } + } else { + referenceImportResults.uniqueReferences++; + importReferences.set(externalReference.source_name, externalReference); + } } - }, - function(err) { - // All the entries in the entry map should be removed now - for (const entry of contentsMap.values()) { - // Object was in x_mitre_contents but not in the bundle - const importError = { - object_ref: entry.object_ref, - object_modified: entry.object_modified, - error_type: importErrors.missingObject, - error_message: 'Object listed in x_mitre_contents, but not in bundle' - } - logger.verbose(`Import Bundle Error: Object in x_mitre_contents but not in bundle. id = ${ entry.object_ref }, modified = ${ entry.object_modified }`); - importedCollection.workspace.import_categories.errors.push(importError); - } - - return callback2(err); - }) - }, - // Import the new references - function(callback3) { - const retrievalOptions = {}; - referencesService.retrieveAll(retrievalOptions) - .then(function(references) { - const existingReferences = new Map(references.map(item => [ item.source_name, item ])); - - // Iterate over the import references - async.eachLimit([...importReferences.values()], 8, function(importReference, callback3a) { - if (existingReferences.has(importReference.source_name)) { - // Duplicate of existing reference -- overwrite - if (options.previewOnly) { - // Do nothing - importedCollection.workspace.import_references.changes.push(importReference.source_name); - return callback3a(); - } else { - // Save the reference - referencesService.update(importReference) - .then(function (reference) { - importedCollection.workspace.import_references.changes.push(importReference.source_name); - return callback3a(); - }) - .catch(function (err) { - return callback3a(err); - }); - } - } else { - if (options.previewOnly) { - // Do nothing - importedCollection.workspace.import_references.additions.push(importReference.source_name); - return callback3a(); - } else { - // Save the reference - referencesService.create(importReference) - .then(function (reference) { - importedCollection.workspace.import_references.additions.push(importReference.source_name); - return callback3a(); - }) - .catch(function (err) { - return callback3a(err); - }); - } - } - }, - function(err) { - return callback3(err); - }); - }) - .catch(err => callback3(err)); - }, - // Save the x-mitre-collection object - function(callback4) { - if (duplicateCollection) { - // The x-mitre-collection object already exists, this is a reimport - // Add the results of the import to the workspace.reimports property - const reimport = { - imported: new Date().toISOString(), - import_categories: importedCollection.workspace.import_categories, - import_references: importedCollection.workspace.import_references - }; - if (!duplicateCollection.workspace.reimports) { - duplicateCollection.workspace.reimports = []; - } - duplicateCollection.workspace.reimports.push(reimport); - - if (options.previewOnly) { - // Do nothing - process.nextTick(() => callback4(null, importedCollection)); + } } - else { - const options = { new: true, lean: true }; - Collection.findByIdAndUpdate(duplicateCollection._id, duplicateCollection, options, function (err, savedDocument) { - if (err) { - return callback4(err); - } - else { - return callback4(null, savedDocument); - } + } + + // Save the object + if (options.previewOnly) { + // Do nothing + return callback2a(); + } else { + const newObject = { + workspace: { + collections: [collectionReference], + }, + stix: importObject, + }; + if (importObject.type === 'marking-definition') { + service + .create(newObject, { import: true }) + .then(function (savedObject) { + return callback2a(); + }) + .catch(function (err) { + if (err.message === service.errors.duplicateId) { + return callback2a(err); + } else { + return callback2a(err); + } }); - } - } - else { - // New x-mitre-collection object - if (options.previewOnly) { - // Do nothing - process.nextTick(() => callback4(null, importedCollection)); } else { - const options = { addObjectsToCollection: false, import: true }; - collectionsService.create(importedCollection, options) - .then(function (result) { - return callback4(null, result.savedCollection); - }) - .catch(function (err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - // 11000 = Duplicate index - const error = new Error(errors.duplicateCollection); - return callback4(error); - } else { - return callback4(err); - } - }); + service + .create(newObject, { import: true }) + .then(function (savedObject) { + return callback2a(); + }) + .catch(function (err) { + if ( + err.message === service.errors?.duplicateId || + err instanceof DuplicateIdError + ) { + // We've checked for this already, so this shouldn't occur + return callback2a(err); + } else { + // Record the error, but don't cancel the import + const importError = { + object_ref: importObject.id, + object_modified: importObject.modified, + error_type: importErrors.saveError, + error_message: err.message, + }; + logger.verbose( + `Import Bundle Error: Unable to save object. id = ${importObject.id}, modified = ${importObject.modified}, ${err.message}`, + ); + importedCollection.workspace.import_categories.errors.push(importError); + return callback2a(); + } + }); } + } } + }); + } else { + if (importObject.type === 'x-mitre-collection') { + // Skip x-mitre-collection objects + return callback2a(); + } else { + // Unknown object type + // Record the error, but don't cancel the import + const importError = { + object_ref: importObject.id, + object_modified: importObject.modified, + error_type: importErrors.unknownObjectType, + error_message: `Unknown object type: ${importObject.type}`, + }; + logger.verbose( + `Import Bundle Error: Unknown object type. id = ${importObject.id}, modified = ${importObject.modified}, type = ${importObject.type}`, + ); + importedCollection.workspace.import_categories.errors.push(importError); + return callback2a(); + } } - ], - function(err, results) { - if (err) { - return callback(err); - } - else { - return callback(null, results[3]); + }, + function (err) { + // All the entries in the entry map should be removed now + for (const entry of contentsMap.values()) { + // Object was in x_mitre_contents but not in the bundle + const importError = { + object_ref: entry.object_ref, + object_modified: entry.object_modified, + error_type: importErrors.missingObject, + error_message: 'Object listed in x_mitre_contents, but not in bundle', + }; + logger.verbose( + `Import Bundle Error: Object in x_mitre_contents but not in bundle. id = ${entry.object_ref}, modified = ${entry.object_modified}`, + ); + importedCollection.workspace.import_categories.errors.push(importError); } + + return callback2(err); + }, + ); + }, + // Import the new references + function (callback3) { + const retrievalOptions = {}; + referencesService + .retrieveAll(retrievalOptions) + .then(function (references) { + const existingReferences = new Map(references.map((item) => [item.source_name, item])); + + // Iterate over the import references + async.eachLimit( + [...importReferences.values()], + 8, + function (importReference, callback3a) { + if (existingReferences.has(importReference.source_name)) { + // Duplicate of existing reference -- overwrite + if (options.previewOnly) { + // Do nothing + importedCollection.workspace.import_references.changes.push( + importReference.source_name, + ); + return callback3a(); + } else { + // Save the reference + referencesService + .update(importReference) + .then(function (reference) { + importedCollection.workspace.import_references.changes.push( + importReference.source_name, + ); + return callback3a(); + }) + .catch(function (err) { + return callback3a(err); + }); + } + } else { + if (options.previewOnly) { + // Do nothing + importedCollection.workspace.import_references.additions.push( + importReference.source_name, + ); + return callback3a(); + } else { + // Save the reference + referencesService + .create(importReference) + .then(function (reference) { + importedCollection.workspace.import_references.additions.push( + importReference.source_name, + ); + return callback3a(); + }) + .catch(function (err) { + return callback3a(err); + }); + } + } + }, + function (err) { + return callback3(err); + }, + ); + }) + .catch((err) => callback3(err)); + }, + // Save the x-mitre-collection object + function (callback4) { + if (duplicateCollection) { + // The x-mitre-collection object already exists, this is a reimport + // Add the results of the import to the workspace.reimports property + const reimport = { + imported: new Date().toISOString(), + import_categories: importedCollection.workspace.import_categories, + import_references: importedCollection.workspace.import_references, + }; + if (!duplicateCollection.workspace.reimports) { + duplicateCollection.workspace.reimports = []; + } + duplicateCollection.workspace.reimports.push(reimport); + + if (options.previewOnly) { + // Do nothing + process.nextTick(() => callback4(null, importedCollection)); + } else { + const options = { new: true, lean: true }; + Collection.findByIdAndUpdate( + duplicateCollection._id, + duplicateCollection, + options, + function (err, savedDocument) { + if (err) { + return callback4(err); + } else { + return callback4(null, savedDocument); + } + }, + ); + } + } else { + // New x-mitre-collection object + if (options.previewOnly) { + // Do nothing + process.nextTick(() => callback4(null, importedCollection)); + } else { + const options = { addObjectsToCollection: false, import: true }; + collectionsService + .create(importedCollection, options) + .then(function (result) { + return callback4(null, result.savedCollection); + }) + .catch(function (err) { + if (err.name === 'MongoServerError' && err.code === 11000) { + // 11000 = Duplicate index + const error = new Error(errors.duplicateCollection); + return callback4(error); + } else { + return callback4(err); + } + }); + } } - ); + }, + ], + function (err, results) { + if (err) { + return callback(err); + } else { + return callback(null, results[3]); + } + }, + ); }; async function createBundle(collection, options) { - // Create the bundle to hold the exported objects - const bundle = { - type: 'bundle', - id: `bundle--${uuid.v4()}`, - objects: [] - }; - - // Put the collection object in the bundle - bundle.objects.push(collection.stix); - - // The attackObjectMap maps attack IDs to attack objects and is used to make the LinkById conversion - // more efficient. - const attackObjectMap = new Map(); - - // Put the contents in the bundle - for (const attackObject of collection.contents) { - // Add the object to the attack map - const attackId = linkById.getAttackId(attackObject.stix); - if (attackId) { - attackObjectMap.set(attackId, attackObject); - } - - // And put it in the bundle - bundle.objects.push(attackObject.stix); + // Create the bundle to hold the exported objects + const bundle = { + type: 'bundle', + id: `bundle--${uuid.v4()}`, + objects: [], + }; + + // Put the collection object in the bundle + bundle.objects.push(collection.stix); + + // The attackObjectMap maps attack IDs to attack objects and is used to make the LinkById conversion + // more efficient. + const attackObjectMap = new Map(); + + // Put the contents in the bundle + for (const attackObject of collection.contents) { + // Add the object to the attack map + const attackId = linkById.getAttackId(attackObject.stix); + if (attackId) { + attackObjectMap.set(attackId, attackObject); } - await addDerivedDataSources(bundle.objects); - if (options.includeNotes) { - await addNotes(bundle.objects); - } - await convertLinkedById(bundle.objects, attackObjectMap); + // And put it in the bundle + bundle.objects.push(attackObject.stix); + } - if (!options.previewOnly) { - const exportData = { - export_timestamp: new Date(), - bundle_id: bundle.id - } - // Mark all of the objects as belonging to the bundle - await collectionsService.insertExport(collection.stix.id, collection.stix.modified, exportData); - } + await addDerivedDataSources(bundle.objects); + if (options.includeNotes) { + await addNotes(bundle.objects); + } + await convertLinkedById(bundle.objects, attackObjectMap); + + if (!options.previewOnly) { + const exportData = { + export_timestamp: new Date(), + bundle_id: bundle.id, + }; + // Mark all of the objects as belonging to the bundle + await collectionsService.insertExport(collection.stix.id, collection.stix.modified, exportData); + } - return bundle; + return bundle; } -exports.exportBundle = async function(options) { - if (options.collectionModified) { - // Retrieve the collection with the provided id and modified date - const retrievalOptions = { retrieveContents: true }; - const retrieveCollection = util.promisify(collectionsService.retrieveVersionById); - const collection = await retrieveCollection(options.collectionId, options.collectionModified, retrievalOptions); - if (collection) { - const bundle = await createBundle(collection, options); - return bundle; - } - else { - const error = new Error(errors.notFound); - throw error; - } +exports.exportBundle = async function (options) { + if (options.collectionModified) { + // Retrieve the collection with the provided id and modified date + const retrievalOptions = { retrieveContents: true }; + const retrieveCollection = util.promisify(collectionsService.retrieveVersionById); + const collection = await retrieveCollection( + options.collectionId, + options.collectionModified, + retrievalOptions, + ); + if (collection) { + const bundle = await createBundle(collection, options); + return bundle; + } else { + const error = new Error(errors.notFound); + throw error; } - else { - // Retrieve the latest collection with the provided id - const retrievalOptions = { - versions: 'latest', - retrieveContents: true - }; - const retrieveCollection = util.promisify(collectionsService.retrieveById); - const collections = await retrieveCollection(options.collectionId, retrievalOptions); - if (collections.length === 1) { - const exportedCollection = collections[0]; - const bundle = await createBundle(exportedCollection, options); - return bundle; - } else if (collections.length === 0) { - const error = new Error(errors.notFound); - throw error; - } else { - const error = new Error('Unknown error occurred'); - throw error; - } + } else { + // Retrieve the latest collection with the provided id + const retrievalOptions = { + versions: 'latest', + retrieveContents: true, + }; + const retrieveCollection = util.promisify(collectionsService.retrieveById); + const collections = await retrieveCollection(options.collectionId, retrievalOptions); + if (collections.length === 1) { + const exportedCollection = collections[0]; + const bundle = await createBundle(exportedCollection, options); + return bundle; + } else if (collections.length === 0) { + const error = new Error(errors.notFound); + throw error; + } else { + const error = new Error('Unknown error occurred'); + throw error; } -} + } +}; async function addDerivedDataSources(bundleObjects) { - // Get the data components, data sources, and techniques detected by data components - const dataComponents = new Map(); - const dataSources = new Map(); - const techniqueDetectedBy = new Map(); - for (const bundleObject of bundleObjects) { - if (bundleObject.type === 'x-mitre-data-component') { - dataComponents.set(bundleObject.id, bundleObject); - } - else if (bundleObject.type === 'x-mitre-data-source') { - dataSources.set(bundleObject.id, bundleObject); - } - else if (bundleObject.type === 'relationship' && bundleObject.relationship_type === 'detects') { - // technique (target_ref) detected by array of data-component (source_ref) - const techniqueDataComponents = techniqueDetectedBy.get(bundleObject.target_ref); - if (techniqueDataComponents) { - // Add to the existing array - techniqueDataComponents.push(bundleObject.source_ref); - } - else { - // Create a new array and add to map - techniqueDetectedBy.set(bundleObject.target_ref, [bundleObject.source_ref]); - } - } + // Get the data components, data sources, and techniques detected by data components + const dataComponents = new Map(); + const dataSources = new Map(); + const techniqueDetectedBy = new Map(); + for (const bundleObject of bundleObjects) { + if (bundleObject.type === 'x-mitre-data-component') { + dataComponents.set(bundleObject.id, bundleObject); + } else if (bundleObject.type === 'x-mitre-data-source') { + dataSources.set(bundleObject.id, bundleObject); + } else if ( + bundleObject.type === 'relationship' && + bundleObject.relationship_type === 'detects' + ) { + // technique (target_ref) detected by array of data-component (source_ref) + const techniqueDataComponents = techniqueDetectedBy.get(bundleObject.target_ref); + if (techniqueDataComponents) { + // Add to the existing array + techniqueDataComponents.push(bundleObject.source_ref); + } else { + // Create a new array and add to map + techniqueDetectedBy.set(bundleObject.target_ref, [bundleObject.source_ref]); + } } + } - const icsDataSourceValues = await systemConfigurationService.retrieveAllowedValuesForTypePropertyDomain('technique', 'x_mitre_data_sources', 'ics-attack'); - for (const bundleObject of bundleObjects) { - if (bundleObject.type === 'attack-pattern') { - const enterpriseDomain = bundleObject.x_mitre_domains.includes('enterprise-attack'); - const icsDomain = bundleObject.x_mitre_domains.includes('ics-attack'); - if (enterpriseDomain && !icsDomain) { - // Remove any existing data sources - bundleObject.x_mitre_data_sources = []; - - // Add in any enterprise data sources from detects relationships - const dataComponentIds = techniqueDetectedBy.get(bundleObject.id); - if (dataComponentIds) { - for (const dataComponentId of dataComponentIds) { - const dataComponent = dataComponents.get(dataComponentId); - if (dataComponent) { - const dataSource = dataSources.get(dataComponent.x_mitre_data_source_ref); - if (dataSource) { - const derivedDataSource = `${ dataSource.name }: ${ dataComponent.name }`; - bundleObject.x_mitre_data_sources.push(derivedDataSource); - } - else { - console.log(`Referenced data source not found: ${ dataComponent.x_mitre_data_source_ref }`); - } - } - else { - console.log(`Referenced data component not found: ${ dataComponentId }`); - } - } - } - } - else if (icsDomain && !enterpriseDomain) { - // Remove any data sources that are not in the list of valid ICS data sources - if (Array.isArray(bundleObject.x_mitre_data_sources)) { - bundleObject.x_mitre_data_sources = bundleObject.x_mitre_data_sources.filter(source => icsDataSourceValues.allowedValues.includes(source)); - } + const icsDataSourceValues = + await systemConfigurationService.retrieveAllowedValuesForTypePropertyDomain( + 'technique', + 'x_mitre_data_sources', + 'ics-attack', + ); + for (const bundleObject of bundleObjects) { + if (bundleObject.type === 'attack-pattern') { + const enterpriseDomain = bundleObject.x_mitre_domains.includes('enterprise-attack'); + const icsDomain = bundleObject.x_mitre_domains.includes('ics-attack'); + if (enterpriseDomain && !icsDomain) { + // Remove any existing data sources + bundleObject.x_mitre_data_sources = []; + + // Add in any enterprise data sources from detects relationships + const dataComponentIds = techniqueDetectedBy.get(bundleObject.id); + if (dataComponentIds) { + for (const dataComponentId of dataComponentIds) { + const dataComponent = dataComponents.get(dataComponentId); + if (dataComponent) { + const dataSource = dataSources.get(dataComponent.x_mitre_data_source_ref); + if (dataSource) { + const derivedDataSource = `${dataSource.name}: ${dataComponent.name}`; + bundleObject.x_mitre_data_sources.push(derivedDataSource); + } else { + console.log( + `Referenced data source not found: ${dataComponent.x_mitre_data_source_ref}`, + ); + } + } else { + console.log(`Referenced data component not found: ${dataComponentId}`); } - else if (enterpriseDomain && icsDomain) { - // Remove any data sources that are not in the list of valid ICS data sources - if (Array.isArray(bundleObject.x_mitre_data_sources)) { - bundleObject.x_mitre_data_sources = bundleObject.x_mitre_data_sources.filter(source => icsDataSourceValues.allowedValues.includes(source)); - } - else { - bundleObject.x_mitre_data_sources = []; - } + } + } + } else if (icsDomain && !enterpriseDomain) { + // Remove any data sources that are not in the list of valid ICS data sources + if (Array.isArray(bundleObject.x_mitre_data_sources)) { + bundleObject.x_mitre_data_sources = bundleObject.x_mitre_data_sources.filter((source) => + icsDataSourceValues.allowedValues.includes(source), + ); + } + } else if (enterpriseDomain && icsDomain) { + // Remove any data sources that are not in the list of valid ICS data sources + if (Array.isArray(bundleObject.x_mitre_data_sources)) { + bundleObject.x_mitre_data_sources = bundleObject.x_mitre_data_sources.filter((source) => + icsDataSourceValues.allowedValues.includes(source), + ); + } else { + bundleObject.x_mitre_data_sources = []; + } - // Add in any enterprise data sources from detects relationships - const dataComponentIds = techniqueDetectedBy.get(bundleObject.id); - if (dataComponentIds) { - for (const dataComponentId of dataComponentIds) { - const dataComponent = dataComponents.get(dataComponentId); - if (dataComponent) { - const dataSource = dataSources.get(dataComponent.x_mitre_data_source_ref); - if (dataSource) { - const derivedDataSource = `${ dataSource.name }: ${ dataComponent.name }`; - bundleObject.x_mitre_data_sources.push(derivedDataSource); - } - else { - console.log(`Referenced data source not found: ${ dataComponent.x_mitre_data_source_ref }`); - } - } - else { - console.log(`Referenced data component not found: ${ dataComponentId }`); - } - } - } - } - else { - // Remove any existing data sources - bundleObject.x_mitre_data_sources = []; + // Add in any enterprise data sources from detects relationships + const dataComponentIds = techniqueDetectedBy.get(bundleObject.id); + if (dataComponentIds) { + for (const dataComponentId of dataComponentIds) { + const dataComponent = dataComponents.get(dataComponentId); + if (dataComponent) { + const dataSource = dataSources.get(dataComponent.x_mitre_data_source_ref); + if (dataSource) { + const derivedDataSource = `${dataSource.name}: ${dataComponent.name}`; + bundleObject.x_mitre_data_sources.push(derivedDataSource); + } else { + console.log( + `Referenced data source not found: ${dataComponent.x_mitre_data_source_ref}`, + ); + } + } else { + console.log(`Referenced data component not found: ${dataComponentId}`); } + } } + } else { + // Remove any existing data sources + bundleObject.x_mitre_data_sources = []; + } } + } } async function addNotes(bundleObjects) { - // Add notes that reference an object in the bundle - - // Note that this function adds the note objects to the bundle but doesn't add them to the - // x_mitre_contents of the collection object. This will be flagged as an error if the collection - // bundle is subsequently imported into Workbench, but it won't stop the import. - - // Start by getting the latest version of all notes (excluding deprecated and revoked notes) - const noteQuery = { }; - noteQuery['stix.revoked'] = { $in: [null, false] }; - noteQuery['stix.x_mitre_deprecated'] = { $in: [null, false] }; - const noteAggregation = [ - { $sort: { 'stix.id': 1, 'stix.modified': -1 } }, - { $group: { _id: '$stix.id', document: { $first: '$$ROOT' }}}, - { $replaceRoot: { newRoot: '$document' }}, - { $match: noteQuery } - ]; - const allNotes = await Note.aggregate(noteAggregation); - - // Build a map of the bundle objects - const bundleObjectMap = new Map(); - for (const bundleObject of bundleObjects) { - bundleObjectMap.set(bundleObject.id, bundleObject); - } - - // Iterate over the notes, keeping any that have an object_ref that points at an object in the bundle - const notes = []; - for (const note of allNotes) { - if (Array.isArray(note?.stix?.object_refs)) { - let includeNote = false; - for (const objectRef of note.stix.object_refs) { - if (bundleObjectMap.has(objectRef)) { - includeNote = true; - break; - } - } - if (includeNote) { - // Make sure we don't add a note that's already in the bundle - if (!bundleObjectMap.has(note.stix.id)) { - notes.push(note); - } - } + // Add notes that reference an object in the bundle + + // Note that this function adds the note objects to the bundle but doesn't add them to the + // x_mitre_contents of the collection object. This will be flagged as an error if the collection + // bundle is subsequently imported into Workbench, but it won't stop the import. + + // Start by getting the latest version of all notes (excluding deprecated and revoked notes) + const noteQuery = {}; + noteQuery['stix.revoked'] = { $in: [null, false] }; + noteQuery['stix.x_mitre_deprecated'] = { $in: [null, false] }; + const noteAggregation = [ + { $sort: { 'stix.id': 1, 'stix.modified': -1 } }, + { $group: { _id: '$stix.id', document: { $first: '$$ROOT' } } }, + { $replaceRoot: { newRoot: '$document' } }, + { $match: noteQuery }, + ]; + const allNotes = await Note.aggregate(noteAggregation); + + // Build a map of the bundle objects + const bundleObjectMap = new Map(); + for (const bundleObject of bundleObjects) { + bundleObjectMap.set(bundleObject.id, bundleObject); + } + + // Iterate over the notes, keeping any that have an object_ref that points at an object in the bundle + const notes = []; + for (const note of allNotes) { + if (Array.isArray(note?.stix?.object_refs)) { + let includeNote = false; + for (const objectRef of note.stix.object_refs) { + if (bundleObjectMap.has(objectRef)) { + includeNote = true; + break; } + } + if (includeNote) { + // Make sure we don't add a note that's already in the bundle + if (!bundleObjectMap.has(note.stix.id)) { + notes.push(note); + } + } } + } - // Put the notes in the bundle - for (const note of notes) { - bundleObjects.push(note.stix); - } + // Put the notes in the bundle + for (const note of notes) { + bundleObjects.push(note.stix); + } } async function convertLinkedById(bundleObjects, attackObjectMap) { - // Create the function to be used by the LinkById conversion process - // Note that using this map instead of database retrieval results in a - // dramatic performance improvement. - const getAttackObjectFromMap = async function (attackId) { - let attackObject = attackObjectMap.get(attackId); - if (!attackObject) { - attackObject = await linkById.getAttackObjectFromDatabase(attackId); - } - return attackObject; + // Create the function to be used by the LinkById conversion process + // Note that using this map instead of database retrieval results in a + // dramatic performance improvement. + const getAttackObjectFromMap = async function (attackId) { + let attackObject = attackObjectMap.get(attackId); + if (!attackObject) { + attackObject = await linkById.getAttackObjectFromDatabase(attackId); } + return attackObject; + }; - // Convert LinkById tags into markdown citations - for (const bundleObject of bundleObjects) { - await linkById.convertLinkByIdTags(bundleObject, getAttackObjectFromMap); - } + // Convert LinkById tags into markdown citations + for (const bundleObject of bundleObjects) { + await linkById.convertLinkByIdTags(bundleObject, getAttackObjectFromMap); + } } diff --git a/app/services/collection-indexes-service.js b/app/services/collection-indexes-service.js index 6312e947..d90b7918 100644 --- a/app/services/collection-indexes-service.js +++ b/app/services/collection-indexes-service.js @@ -6,184 +6,180 @@ const CollectionIndex = require('../models/collection-index-model'); const config = require('../config/config'); const errors = { - badRequest: 'Bad request', - missingParameter: 'Missing required parameter', - badlyFormattedParameter: 'Badly formatted parameter', - duplicateId: 'Duplicate id', - notFound: 'Document not found', - hostNotFound: 'Host not found', - connectionRefused: 'Connection refused', + badRequest: 'Bad request', + missingParameter: 'Missing required parameter', + badlyFormattedParameter: 'Badly formatted parameter', + duplicateId: 'Duplicate id', + notFound: 'Document not found', + hostNotFound: 'Host not found', + connectionRefused: 'Connection refused', }; exports.errors = errors; exports.retrieveAll = function (options, callback) { - CollectionIndex.find() - .skip(options.offset) - .limit(options.limit) - .lean() - .exec(function (err, collectionIndexes) { - if (err) { - return callback(err); - } - else { - return callback(null, collectionIndexes); - } - }); + CollectionIndex.find() + .skip(options.offset) + .limit(options.limit) + .lean() + .exec(function (err, collectionIndexes) { + if (err) { + return callback(err); + } else { + return callback(null, collectionIndexes); + } + }); }; -exports.retrieveById = function(id, callback) { - if (!id) { - const error = new Error(errors.missingParameter); +exports.retrieveById = function (id, callback) { + if (!id) { + const error = new Error(errors.missingParameter); + error.parameterName = 'id'; + return callback(error); + } + + CollectionIndex.findOne({ 'collection_index.id': id }, function (err, collectionIndex) { + if (err) { + if (err.name === 'CastError') { + const error = new Error(errors.badlyFormattedParameter); error.parameterName = 'id'; return callback(error); + } else { + return callback(err); + } + } else { + // Note: document is null if not found + return callback(null, collectionIndex); } - - CollectionIndex.findOne({ "collection_index.id": id }, function(err, collectionIndex) { - if (err) { - if (err.name === 'CastError') { - const error = new Error(errors.badlyFormattedParameter); - error.parameterName = 'id'; - return callback(error); - } else { - return callback(err); - } - } else { - // Note: document is null if not found - return callback(null, collectionIndex); - } - }); + }); }; -exports.create = function(data, callback) { - // Create the document - const collectionIndex = new CollectionIndex(data); +exports.create = function (data, callback) { + // Create the document + const collectionIndex = new CollectionIndex(data); - if (collectionIndex.workspace.update_policy) { - if (collectionIndex.workspace.update_policy.automatic && !collectionIndex.workspace.update_policy.interval) { - collectionIndex.workspace.update_policy.interval = config.collectionIndex.defaultInterval; - } + if (collectionIndex.workspace.update_policy) { + if ( + collectionIndex.workspace.update_policy.automatic && + !collectionIndex.workspace.update_policy.interval + ) { + collectionIndex.workspace.update_policy.interval = config.collectionIndex.defaultInterval; } + } - // Save the document in the database - collectionIndex.save(function(err, collectionIndex) { - if (err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - // 11000 = Duplicate index - const error = new Error(errors.duplicateId); - return callback(error); - } - else { - return callback(err); - } - } - else { - return callback(null, collectionIndex); - } - }); -}; - -exports.updateFull = function(id, data, callback) { - if (!id) { - const error = new Error(errors.missingParameter); - error.parameterName = 'id'; + // Save the document in the database + collectionIndex.save(function (err, collectionIndex) { + if (err) { + if (err.name === 'MongoServerError' && err.code === 11000) { + // 11000 = Duplicate index + const error = new Error(errors.duplicateId); return callback(error); + } else { + return callback(err); + } + } else { + return callback(null, collectionIndex); } - - CollectionIndex.findOne({ "collection_index.id": id }, function(err, collectionIndex) { - if (err) { - if (err.name === 'CastError') { - var error = new Error(errors.badlyFormattedParameter); - error.parameterName = 'id'; - return callback(error); - } - else { - return callback(err); - } - } - else if (!collectionIndex) { - // Collection index not found - return callback(null); - } - else { - // Copy data to found document and save - Object.assign(collectionIndex, data); - collectionIndex.save(function(err, savedCollectionIndex) { - if (err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - // 11000 = Duplicate index - var error = new Error(errors.duplicateId); - return callback(error); - } - else { - return callback(err); - } - } - else { - return callback(null, savedCollectionIndex); - } - }); - } - }); + }); }; -exports.delete = function (id, callback) { - if (!id) { - const error = new Error(errors.missingParameter); +exports.updateFull = function (id, data, callback) { + if (!id) { + const error = new Error(errors.missingParameter); + error.parameterName = 'id'; + return callback(error); + } + + CollectionIndex.findOne({ 'collection_index.id': id }, function (err, collectionIndex) { + if (err) { + if (err.name === 'CastError') { + var error = new Error(errors.badlyFormattedParameter); error.parameterName = 'id'; return callback(error); - } - - CollectionIndex.findOneAndRemove({ "collection_index.id": id }, function (err, collectionIndex) { + } else { + return callback(err); + } + } else if (!collectionIndex) { + // Collection index not found + return callback(null); + } else { + // Copy data to found document and save + Object.assign(collectionIndex, data); + collectionIndex.save(function (err, savedCollectionIndex) { if (err) { + if (err.name === 'MongoServerError' && err.code === 11000) { + // 11000 = Duplicate index + var error = new Error(errors.duplicateId); + return callback(error); + } else { return callback(err); + } } else { - //Note: collectionIndex is null if not found - return callback(null, collectionIndex); + return callback(null, savedCollectionIndex); } - }); + }); + } + }); +}; + +exports.delete = function (id, callback) { + if (!id) { + const error = new Error(errors.missingParameter); + error.parameterName = 'id'; + return callback(error); + } + + CollectionIndex.findOneAndRemove({ 'collection_index.id': id }, function (err, collectionIndex) { + if (err) { + return callback(err); + } else { + //Note: collectionIndex is null if not found + return callback(null, collectionIndex); + } + }); }; /** * Retrieves a collection index from the provided URL. * This is expected to be a remote URL that does not require authentication. */ -exports.retrieveByUrl = function(url, callback) { - if (!url) { - const error = new Error(errors.missingParameter); - return callback(error); - } - superagent.get(url).set('Accept', 'application/json').end((err, res) => { - if (err) { - if (err.response && err.response.notFound) { - const error = new Error(errors.notFound); - return callback(error); - } else if (err.response && err.response.badRequest) { - const error = new Error(errors.badRequest); - return callback(error); - } else if (err.code === 'ENOTFOUND') { - const error = new Error(errors.hostNotFound); - return callback(error); - } else if (err.code === 'ECONNREFUSED') { - const error = new Error(errors.connectionRefused); - return callback(error); - } else { - return callback(err) - } +exports.retrieveByUrl = function (url, callback) { + if (!url) { + const error = new Error(errors.missingParameter); + return callback(error); + } + superagent + .get(url) + .set('Accept', 'application/json') + .end((err, res) => { + if (err) { + if (err.response && err.response.notFound) { + const error = new Error(errors.notFound); + return callback(error); + } else if (err.response && err.response.badRequest) { + const error = new Error(errors.badRequest); + return callback(error); + } else if (err.code === 'ENOTFOUND') { + const error = new Error(errors.hostNotFound); + return callback(error); + } else if (err.code === 'ECONNREFUSED') { + const error = new Error(errors.connectionRefused); + return callback(error); + } else { + return callback(err); } - else { - try { - // Parsing res.text handles both the content-type text/plain and application/json use cases - const collectionIndex = JSON.parse(res.text); - return callback(null, collectionIndex); - } - catch (err) { - return callback(err); - } + } else { + try { + // Parsing res.text handles both the content-type text/plain and application/json use cases + const collectionIndex = JSON.parse(res.text); + return callback(null, collectionIndex); + } catch (err) { + return callback(err); } + } }); -} +}; -exports.refresh = function(id, callback) { - // Do nothing for now - process.nextTick(() => callback(null, {})); +exports.refresh = function (id, callback) { + // Do nothing for now + process.nextTick(() => callback(null, {})); }; diff --git a/app/services/collections-service.js b/app/services/collections-service.js index 055620b4..9f076260 100644 --- a/app/services/collections-service.js +++ b/app/services/collections-service.js @@ -11,470 +11,490 @@ const attackObjectsService = require('./attack-objects-service'); const identitiesService = require('./identities-service'); const config = require('../config/config'); const regexValidator = require('../lib/regex'); -const {lastUpdatedByQueryHelper} = require('../lib/request-parameter-helper'); +const { lastUpdatedByQueryHelper } = require('../lib/request-parameter-helper'); const errors = { - missingParameter: 'Missing required parameter', - badRequest: 'Bad request', - invalidFormat: 'Invalid format', - badlyFormattedParameter: 'Badly formatted parameter', - duplicateId: 'Duplicate id', - notFound: 'Document not found', - hostNotFound: 'Host not found', - connectionRefused: 'Connection refused', - unauthorized: 'Unauthorized', - invalidQueryStringParameter: 'Invalid query string parameter' + missingParameter: 'Missing required parameter', + badRequest: 'Bad request', + invalidFormat: 'Invalid format', + badlyFormattedParameter: 'Badly formatted parameter', + duplicateId: 'Duplicate id', + notFound: 'Document not found', + hostNotFound: 'Host not found', + connectionRefused: 'Connection refused', + unauthorized: 'Unauthorized', + invalidQueryStringParameter: 'Invalid query string parameter', }; exports.errors = errors; -exports.retrieveAll = function(options, callback) { - // Build the query - const query = {}; - if (!options.includeRevoked) { - query['stix.revoked'] = { $in: [null, false] }; +exports.retrieveAll = function (options, callback) { + // Build the query + const query = {}; + if (!options.includeRevoked) { + query['stix.revoked'] = { $in: [null, false] }; + } + if (!options.includeDeprecated) { + query['stix.x_mitre_deprecated'] = { $in: [null, false] }; + } + if (typeof options.state !== 'undefined') { + if (Array.isArray(options.state)) { + query['workspace.workflow.state'] = { $in: options.state }; + } else { + query['workspace.workflow.state'] = options.state; } - if (!options.includeDeprecated) { - query['stix.x_mitre_deprecated'] = { $in: [null, false] }; + } + if (typeof options.lastUpdatedBy !== 'undefined') { + query['workspace.workflow.created_by_user_account'] = lastUpdatedByQueryHelper( + options.lastUpdatedBy, + ); + } + + // Build the aggregation + const aggregation = [{ $sort: { 'stix.id': 1, 'stix.modified': -1 } }]; + if (options.versions === 'latest') { + // Group the documents by stix.id, sorted by stix.modified + // Use the first document in each group (according to the value of stix.modified) + // Then sort again since the $group does not retain the sort order + aggregation.push({ $group: { _id: '$stix.id', document: { $first: '$$ROOT' } } }); + aggregation.push({ $replaceRoot: { newRoot: '$document' } }); + aggregation.push({ $sort: { 'stix.id': 1 } }); + } + + // Apply query, skip and limit options + aggregation.push({ $match: query }); + + if (typeof options.search !== 'undefined') { + options.search = regexValidator.sanitizeRegex(options.search); + const match = { + $match: { + $or: [ + { 'stix.name': { $regex: options.search, $options: 'i' } }, + { 'stix.description': { $regex: options.search, $options: 'i' } }, + ], + }, + }; + aggregation.push(match); + } + + if (options.skip) { + aggregation.push({ $skip: options.skip }); + } + if (options.limit) { + aggregation.push({ $limit: options.limit }); + } + + // Retrieve the documents + Collection.aggregate(aggregation, function (err, collections) { + if (err) { + return callback(err); + } else { + identitiesService.addCreatedByAndModifiedByIdentitiesToAll(collections).then(function () { + return callback(null, collections); + }); } - if (typeof options.state !== 'undefined') { - if (Array.isArray(options.state)) { - query['workspace.workflow.state'] = { $in: options.state }; - } - else { - query['workspace.workflow.state'] = options.state; - } - } - if (typeof options.lastUpdatedBy !== 'undefined') { - query['workspace.workflow.created_by_user_account'] = lastUpdatedByQueryHelper(options.lastUpdatedBy); - } - - // Build the aggregation - const aggregation = [ { $sort: { 'stix.id': 1, 'stix.modified': -1 }} ]; - if (options.versions === 'latest') { - // Group the documents by stix.id, sorted by stix.modified - // Use the first document in each group (according to the value of stix.modified) - // Then sort again since the $group does not retain the sort order - aggregation.push({ $group: { _id: '$stix.id', document: { $first: '$$ROOT' }}}); - aggregation.push({ $replaceRoot: { newRoot: '$document' }}); - aggregation.push({ $sort: { 'stix.id': 1 }}); - } - - // Apply query, skip and limit options - aggregation.push({ $match: query }); - - if (typeof options.search !== 'undefined') { - options.search = regexValidator.sanitizeRegex(options.search); - const match = { $match: { $or: [ - { 'stix.name': { '$regex': options.search, '$options': 'i' }}, - { 'stix.description': { '$regex': options.search, '$options': 'i' }} - ]}}; - aggregation.push(match); - } - - if (options.skip) { - aggregation.push({ $skip: options.skip }); - } - if (options.limit) { - aggregation.push({ $limit: options.limit }); - } - - // Retrieve the documents - Collection.aggregate(aggregation, function(err, collections) { - if (err) { - return callback(err); - } - else { - identitiesService.addCreatedByAndModifiedByIdentitiesToAll(collections) - .then(function() { - return callback(null, collections); - }); - } - }); + }); }; function getContents(objectList, callback) { - asyncLib.mapLimit( - objectList, - 5, - async function(objectRef) { - const attackObject = await attackObjectsService.retrieveVersionById(objectRef.object_ref, objectRef.object_modified); - return attackObject; - }, - function(err, results) { - if (err) { - return callback(err); - } - else { - const filteredResults = results.filter(item => item); - return callback(null, filteredResults); - } - }); + asyncLib.mapLimit( + objectList, + 5, + async function (objectRef) { + const attackObject = await attackObjectsService.retrieveVersionById( + objectRef.object_ref, + objectRef.object_modified, + ); + return attackObject; + }, + function (err, results) { + if (err) { + return callback(err); + } else { + const filteredResults = results.filter((item) => item); + return callback(null, filteredResults); + } + }, + ); } -exports.retrieveById = function(stixId, options, callback) { - // versions=all Retrieve all collections with the stixId - // versions=latest Retrieve the collection with the latest modified date for this stixId - if (!stixId) { - const error = new Error(errors.missingParameter); - error.parameterName = 'stixId'; - return callback(error); - } - - if (options.versions === 'all') { - Collection.find({'stix.id': stixId}) - .lean() - .exec(function (err, collections) { +exports.retrieveById = function (stixId, options, callback) { + // versions=all Retrieve all collections with the stixId + // versions=latest Retrieve the collection with the latest modified date for this stixId + if (!stixId) { + const error = new Error(errors.missingParameter); + error.parameterName = 'stixId'; + return callback(error); + } + + if (options.versions === 'all') { + Collection.find({ 'stix.id': stixId }) + .lean() + .exec(function (err, collections) { + if (err) { + if (err.name === 'CastError') { + const error = new Error(errors.badlyFormattedParameter); + error.parameterName = 'stixId'; + return callback(error); + } else { + return callback(err); + } + } else { + if (options.retrieveContents) { + asyncLib.eachSeries( + collections, + function (collection, callback2) { + getContents(collection.stix.x_mitre_contents, function (err, contents) { + if (err) { + return callback2(err); + } else { + collection.contents = contents; + return callback2(null); + } + }); + }, + function (err, results) { if (err) { - if (err.name === 'CastError') { - const error = new Error(errors.badlyFormattedParameter); - error.parameterName = 'stixId'; - return callback(error); - } - else { - return callback(err); - } - } - else { - if (options.retrieveContents) { - asyncLib.eachSeries( - collections, - function(collection, callback2) { - getContents(collection.stix.x_mitre_contents, function (err, contents) { - if (err) { - return callback2(err); - } else { - collection.contents = contents; - return callback2(null); - } - }) - }, - function(err, results) { - if (err) { - return callback(err); - } - else { - return callback(null, collections); - } - }); - } - else { - return callback(null, collections); - } + return callback(err); + } else { + return callback(null, collections); } - }); - } - else if (options.versions === 'latest') { - Collection.findOne({ 'stix.id': stixId }) - .sort('-stix.modified') - .lean() - .exec(function(err, collection) { + }, + ); + } else { + return callback(null, collections); + } + } + }); + } else if (options.versions === 'latest') { + Collection.findOne({ 'stix.id': stixId }) + .sort('-stix.modified') + .lean() + .exec(function (err, collection) { + if (err) { + if (err.name === 'CastError') { + const error = new Error(errors.badlyFormattedParameter); + error.parameterName = 'stixId'; + return callback(error); + } else { + return callback(err); + } + } else { + // Note: document is null if not found + if (collection) { + if (options.retrieveContents) { + getContents(collection.stix.x_mitre_contents, function (err, contents) { if (err) { - if (err.name === 'CastError') { - const error = new Error(errors.badlyFormattedParameter); - error.parameterName = 'stixId'; - return callback(error); - } - else { - return callback(err); - } - } - else { - // Note: document is null if not found - if (collection) { - if (options.retrieveContents) { - getContents(collection.stix.x_mitre_contents, function (err, contents) { - if (err) { - return callback(err); - } else { - collection.contents = contents; - return callback(null, [ collection ]); - } - }) - } - else { - return callback(null, [ collection ]); - } - } - else { - return callback(null, []); - } - } - }); - } - else { - const error = new Error(errors.invalidQueryStringParameter); - error.parameterName = 'versions'; - return callback(error); - } -}; - -exports.retrieveVersionById = function(stixId, modified, options, callback) { - // Retrieve the versions of the collection with the matching stixID and modified date - - 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); - } - - Collection.findOne({ 'stix.id': stixId, 'stix.modified': modified }) - .lean() - .exec(function(err, collection) { - if (err) { - if (err.name === 'CastError') { - const error = new Error(errors.badlyFormattedParameter); - error.parameterName = 'stixId'; - return callback(error); - } - else { - return callback(err); - } - } - else { - if (collection) { - if (options.retrieveContents) { - getContents(collection.stix.x_mitre_contents, function (err, contents) { - if (err) { - return callback(err); - } else { - collection.contents = contents; - identitiesService.addCreatedByAndModifiedByIdentities(collection) - .then(() => callback(null, collection)); - } - }) - } - else { - identitiesService.addCreatedByAndModifiedByIdentities(collection) - .then(() => callback(null, collection)); - } - } - else { - return callback(); + return callback(err); + } else { + collection.contents = contents; + return callback(null, [collection]); } + }); + } else { + return callback(null, [collection]); } - }) -} + } else { + return callback(null, []); + } + } + }); + } else { + const error = new Error(errors.invalidQueryStringParameter); + error.parameterName = 'versions'; + return callback(error); + } +}; -async function addObjectsToCollection(objectList, collectionID, collectionModified) { - // Modify the objects in the collection to show that they are part of the collection - const insertionErrors = []; - for (const attackObject of objectList) { - try { - // eslint-disable-next-line no-await-in-loop - await attackObjectsService.insertCollection(attackObject.object_ref, attackObject.object_modified, collectionID, collectionModified); +exports.retrieveVersionById = function (stixId, modified, options, callback) { + // Retrieve the versions of the collection with the matching stixID and modified date + + 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); + } + + Collection.findOne({ 'stix.id': stixId, 'stix.modified': modified }) + .lean() + .exec(function (err, collection) { + if (err) { + if (err.name === 'CastError') { + const error = new Error(errors.badlyFormattedParameter); + error.parameterName = 'stixId'; + return callback(error); + } else { + return callback(err); } - catch(err) { - insertionErrors.push(err); + } else { + if (collection) { + if (options.retrieveContents) { + getContents(collection.stix.x_mitre_contents, function (err, contents) { + if (err) { + return callback(err); + } else { + collection.contents = contents; + identitiesService + .addCreatedByAndModifiedByIdentities(collection) + .then(() => callback(null, collection)); + } + }); + } else { + identitiesService + .addCreatedByAndModifiedByIdentities(collection) + .then(() => callback(null, collection)); + } + } else { + return callback(); } + } + }); +}; + +async function addObjectsToCollection(objectList, collectionID, collectionModified) { + // Modify the objects in the collection to show that they are part of the collection + const insertionErrors = []; + for (const attackObject of objectList) { + try { + // eslint-disable-next-line no-await-in-loop + await attackObjectsService.insertCollection( + attackObject.object_ref, + attackObject.object_modified, + collectionID, + collectionModified, + ); + } catch (err) { + insertionErrors.push(err); } + } - return insertionErrors; + return insertionErrors; } exports.createIsAsync = true; -exports.create = async function(data, options) { - // Create the document - const collection = new Collection(data); - - options = options || {}; - if (!options.import) { - // Set the ATT&CK Spec Version - collection.stix.x_mitre_attack_spec_version = collection.stix.x_mitre_attack_spec_version ?? config.app.attackSpecVersion; - - // Record the user account that created the object - if (options.userAccountId) { - collection.workspace.workflow.created_by_user_account = options.userAccountId; - } - - // Set the default marking definitions - await systemConfigurationService.setDefaultMarkingDefinitionsForObject(collection); - - // Get the organization identity - const organizationIdentityRef = await systemConfigurationService.retrieveOrganizationIdentityRef(); - - // Check for an existing object - let existingObject; - if (collection.stix.id) { - existingObject = await Collection.findOne({ 'stix.id': collection.stix.id }); - } - - if (existingObject) { - // New version of an existing object - // Only set the x_mitre_modified_by_ref property - collection.stix.x_mitre_modified_by_ref = organizationIdentityRef; - } - else { - // New object - // Assign a new STIX id if not already provided - collection.stix.id = collection.stix.id || `x-mitre-collection--${uuid.v4()}`; - - // Set the created_by_ref and x_mitre_modified_by_ref properties - collection.stix.created_by_ref = organizationIdentityRef; - collection.stix.x_mitre_modified_by_ref = organizationIdentityRef; - } +exports.create = async function (data, options) { + // Create the document + const collection = new Collection(data); + + options = options || {}; + if (!options.import) { + // Set the ATT&CK Spec Version + collection.stix.x_mitre_attack_spec_version = + collection.stix.x_mitre_attack_spec_version ?? config.app.attackSpecVersion; + + // Record the user account that created the object + if (options.userAccountId) { + collection.workspace.workflow.created_by_user_account = options.userAccountId; } - // Save the document in the database - let insertionErrors = []; - try { - const savedCollection = await collection.save(); + // Set the default marking definitions + await systemConfigurationService.setDefaultMarkingDefinitionsForObject(collection); - if (options.addObjectsToCollection) { - insertionErrors = await addObjectsToCollection(savedCollection.stix.x_mitre_contents, savedCollection.stix.id, savedCollection.stix.modified); - } + // Get the organization identity + const organizationIdentityRef = + await systemConfigurationService.retrieveOrganizationIdentityRef(); - return { savedCollection, insertionErrors }; - } - catch (err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - // 11000 = Duplicate index - const error = new Error(errors.duplicateId); - throw error; - } - else { - throw err; - } + // Check for an existing object + let existingObject; + if (collection.stix.id) { + existingObject = await Collection.findOne({ 'stix.id': collection.stix.id }); } -}; -exports.delete = async function (stixId, deleteAllContents) { - if (!stixId) { - const error = new Error(errors.missingParameter); - error.parameterName = 'stixId'; - throw error; + if (existingObject) { + // New version of an existing object + // Only set the x_mitre_modified_by_ref property + collection.stix.x_mitre_modified_by_ref = organizationIdentityRef; + } else { + // New object + // Assign a new STIX id if not already provided + collection.stix.id = collection.stix.id || `x-mitre-collection--${uuid.v4()}`; + + // Set the created_by_ref and x_mitre_modified_by_ref properties + collection.stix.created_by_ref = organizationIdentityRef; + collection.stix.x_mitre_modified_by_ref = organizationIdentityRef; + } + } + + // Save the document in the database + let insertionErrors = []; + try { + const savedCollection = await collection.save(); + + if (options.addObjectsToCollection) { + insertionErrors = await addObjectsToCollection( + savedCollection.stix.x_mitre_contents, + savedCollection.stix.id, + savedCollection.stix.modified, + ); } - const collections = await Collection.find({'stix.id': stixId}).lean(); - if (!collections) { - const error = new Error(errors.badlyFormattedParameter); - error.parameterName = 'stixId'; - throw error; + return { savedCollection, insertionErrors }; + } catch (err) { + if (err.name === 'MongoServerError' && err.code === 11000) { + // 11000 = Duplicate index + const error = new Error(errors.duplicateId); + throw error; + } else { + throw err; } + } +}; - if (deleteAllContents) { - for (const collection of collections) { - await deleteAllContentsOfCollection(collection, stixId); - } +exports.delete = async function (stixId, deleteAllContents) { + if (!stixId) { + const error = new Error(errors.missingParameter); + error.parameterName = 'stixId'; + throw error; + } + + const collections = await Collection.find({ 'stix.id': stixId }).lean(); + if (!collections) { + const error = new Error(errors.badlyFormattedParameter); + error.parameterName = 'stixId'; + throw error; + } + + if (deleteAllContents) { + for (const collection of collections) { + await deleteAllContentsOfCollection(collection, stixId); } + } - const allCollections = await Collection.find({'stix.id': stixId}).lean(); - const removedCollections = []; - for (const collection of allCollections) { - try { - await Collection.findByIdAndDelete(collection._id).lean(); - } catch (err) { - continue; - } - removedCollections.push(collection); + const allCollections = await Collection.find({ 'stix.id': stixId }).lean(); + const removedCollections = []; + for (const collection of allCollections) { + try { + await Collection.findByIdAndDelete(collection._id).lean(); + } catch (err) { + continue; } - return removedCollections; + removedCollections.push(collection); + } + return removedCollections; }; exports.deleteVersionById = async function (stixId, modified, deleteAllContents) { - if (!stixId) { - const error = new Error(errors.missingParameter); - error.parameterName = 'stixId'; - throw error; - } + if (!stixId) { + const error = new Error(errors.missingParameter); + error.parameterName = 'stixId'; + throw error; + } + + const collection = await Collection.findOne({ + 'stix.id': stixId, + 'stix.modified': modified, + }).lean(); + if (!collection) { + const error = new Error(errors.badlyFormattedParameter); + error.parameterName = 'stixId'; + throw error; + } + + if (deleteAllContents) { + await deleteAllContentsOfCollection(collection, stixId, modified); + } + + try { + await Collection.findByIdAndDelete(collection._id).lean(); + } catch (err) { + return; + } + return collection; +}; - const collection = await Collection.findOne({'stix.id': stixId, 'stix.modified': modified}).lean(); - if (!collection) { - const error = new Error(errors.badlyFormattedParameter); - error.parameterName = 'stixId'; - throw error; +const deleteAllContentsOfCollection = async function (collection, stixId, modified) { + for (const reference of collection.stix.x_mitre_contents) { + const referenceObj = await AttackObject.findOne({ + 'stix.id': reference.object_ref, + 'stix.modified': reference.object_modified, + }).lean(); + if (!referenceObj) { + continue; } - - if (deleteAllContents) { - await deleteAllContentsOfCollection(collection, stixId, modified); + const matchQuery = { + 'stix.id': { $ne: stixId }, + 'stix.x_mitre_contents': { + $elemMatch: { + object_ref: reference.object_ref, + object_modified: reference.object_modified, + }, + }, + }; + if (modified) { + delete matchQuery['stix.id']; + matchQuery['$or'] = [{ 'stix.id': { $ne: stixId } }, { 'stix.modified': { $ne: modified } }]; } - - try { - await Collection.findByIdAndDelete(collection._id).lean(); - } catch (err) { - return; + const matches = await Collection.find(matchQuery).lean(); + if (matches.length === 0) { + // if this attack object is NOT in another collection, we can just delete it + await AttackObject.findByIdAndDelete(referenceObj._id); + } else { + // if this object IS in another collection, we need to update the workspace.collections array + if (referenceObj.workspace && referenceObj.workspace.collections) { + const newCollectionsArr = referenceObj.workspace.collections.filter( + (collectionElem) => collectionElem.collection_ref !== stixId, + ); + await AttackObject.findByIdAndUpdate(referenceObj.id, { + 'workspace.collections': newCollectionsArr, + }); + } } - return collection; + } }; -const deleteAllContentsOfCollection = async function(collection, stixId, modified) { - for (const reference of collection.stix.x_mitre_contents) { - const referenceObj = await AttackObject.findOne({ 'stix.id': reference.object_ref, 'stix.modified': reference.object_modified }).lean(); - if (!referenceObj) { continue;} - const matchQuery = {'stix.id': {'$ne': stixId}, 'stix.x_mitre_contents' : {'$elemMatch' : {'object_ref' : reference.object_ref, 'object_modified': reference.object_modified}}}; - if (modified) { - delete matchQuery['stix.id']; - matchQuery['$or'] = [{'stix.id': {'$ne': stixId}},{'stix.modified': {'$ne': modified}}]; - } - const matches = await Collection.find(matchQuery).lean(); - if (matches.length === 0) { - // if this attack object is NOT in another collection, we can just delete it - await AttackObject.findByIdAndDelete(referenceObj._id); - } else { - // if this object IS in another collection, we need to update the workspace.collections array - if (referenceObj.workspace && referenceObj.workspace.collections) { - const newCollectionsArr = referenceObj.workspace.collections.filter(collectionElem => collectionElem.collection_ref !== stixId); - await AttackObject.findByIdAndUpdate(referenceObj.id, {'workspace.collections' : newCollectionsArr}); - } - } - } -} - -exports.insertExport = async function(stixId, modified, exportData) { - const collection = await Collection.findOne({ 'stix.id': stixId, 'stix.modified': modified }); - - if (collection) { - // Make sure the exports array exists - if (!collection.workspace.exported) { - collection.workspace.exported = []; - } - collection.workspace.exported.push(exportData); +exports.insertExport = async function (stixId, modified, exportData) { + const collection = await Collection.findOne({ 'stix.id': stixId, 'stix.modified': modified }); - await collection.save(); - } - else { - throw new Error(errors.notFound); + if (collection) { + // Make sure the exports array exists + if (!collection.workspace.exported) { + collection.workspace.exported = []; } + collection.workspace.exported.push(exportData); + + await collection.save(); + } else { + throw new Error(errors.notFound); + } }; -exports.retrieveByUrl = function(url, callback) { - if (!url) { - const error = new Error(errors.missingParameter); +exports.retrieveByUrl = function (url, callback) { + if (!url) { + const error = new Error(errors.missingParameter); + return callback(error); + } + + superagent + .get(url) + .then((res) => { + try { + const body = JSON.parse(res.text); + return callback(null, body); + } catch (err) { + const error = new Error(errors.invalidFormat); return callback(error); - } - - superagent.get(url).then(res => { - try { - const body = JSON.parse(res.text); - return callback(null, body); - } catch (err) { - const error = new Error(errors.invalidFormat); - return callback(error); - } - }).catch(err => { - if (err.response && err.response.notFound) { - const error = new Error(errors.notFound); - return callback(error); - } - else if (err.response && err.response.badRequest) { - const error = new Error(errors.badRequest); - return callback(error); - } - else if (err.code === 'ENOTFOUND') { - const error = new Error(errors.hostNotFound); - return callback(error); - } - else if (err.code === 'ECONNREFUSED') { - const error = new Error(errors.connectionRefused); - return callback(error); - } - else { - return callback(err) - } + } + }) + .catch((err) => { + if (err.response && err.response.notFound) { + const error = new Error(errors.notFound); + return callback(error); + } else if (err.response && err.response.badRequest) { + const error = new Error(errors.badRequest); + return callback(error); + } else if (err.code === 'ENOTFOUND') { + const error = new Error(errors.hostNotFound); + return callback(error); + } else if (err.code === 'ECONNREFUSED') { + const error = new Error(errors.connectionRefused); + return callback(error); + } else { + return callback(err); + } }); }; diff --git a/app/services/data-components-service.js b/app/services/data-components-service.js index 83875bc4..32e07617 100644 --- a/app/services/data-components-service.js +++ b/app/services/data-components-service.js @@ -4,6 +4,6 @@ const dataComponentsRepository = require('../repository/data-components-reposito const BaseService = require('./_base.service'); -class DataComponentsService extends BaseService { } +class DataComponentsService extends BaseService {} module.exports = new DataComponentsService('x-mitre-data-component', dataComponentsRepository); diff --git a/app/services/data-sources-service.js b/app/services/data-sources-service.js index 0373a7d7..3deb642b 100644 --- a/app/services/data-sources-service.js +++ b/app/services/data-sources-service.js @@ -4,147 +4,161 @@ const dataSourcesRepository = require('../repository/data-sources-repository'); const identitiesService = require('./identities-service'); const dataComponentsService = require('./data-components-service'); const BaseService = require('./_base.service'); -const { MissingParameterError, BadlyFormattedParameterError, InvalidQueryStringParameterError } = require('../exceptions'); +const { + MissingParameterError, + BadlyFormattedParameterError, + InvalidQueryStringParameterError, +} = require('../exceptions'); class DataSourcesService extends BaseService { + errors = { + missingParameter: 'Missing required parameter', + badlyFormattedParameter: 'Badly formatted parameter', + duplicateId: 'Duplicate id', + notFound: 'Document not found', + invalidQueryStringParameter: 'Invalid query string parameter', + }; - errors = { - missingParameter: 'Missing required parameter', - badlyFormattedParameter: 'Badly formatted parameter', - duplicateId: 'Duplicate id', - notFound: 'Document not found', - invalidQueryStringParameter: 'Invalid query string parameter' + static async addExtraData(dataSource, retrieveDataComponents) { + await identitiesService.addCreatedByAndModifiedByIdentities(dataSource); + if (retrieveDataComponents) { + await DataSourcesService.addDataComponents(dataSource); } + } - static async addExtraData(dataSource, retrieveDataComponents) { - await identitiesService.addCreatedByAndModifiedByIdentities(dataSource); - if (retrieveDataComponents) { - await DataSourcesService.addDataComponents(dataSource); - } + static async addExtraDataToAll(dataSources, retrieveDataComponents) { + for (const dataSource of dataSources) { + // eslint-disable-next-line no-await-in-loop + await DataSourcesService.addExtraData(dataSource, retrieveDataComponents); } + } - static async addExtraDataToAll(dataSources, retrieveDataComponents) { - for (const dataSource of dataSources) { - // eslint-disable-next-line no-await-in-loop - await DataSourcesService.addExtraData(dataSource, retrieveDataComponents); - } - } + static async addDataComponents(dataSource) { + // We have to work with the latest version of all data components to avoid mishandling a situation + // where an earlier version of a data component may reference a data source, but the latest + // version doesn't. - static async addDataComponents(dataSource) { - // We have to work with the latest version of all data components to avoid mishandling a situation - // where an earlier version of a data component may reference a data source, but the latest - // version doesn't. + // Retrieve the latest version of all data components + const allDataComponents = await dataComponentsService.retrieveAll({ + includeDeprecated: true, + includeRevoked: true, + }); - // Retrieve the latest version of all data components - const allDataComponents = await dataComponentsService.retrieveAll({ includeDeprecated: true, includeRevoked: true }); + // Add the data components that reference the data source + dataSource.dataComponents = allDataComponents.filter( + (dataComponent) => dataComponent.stix.x_mitre_data_source_ref === dataSource.stix.id, + ); + } - // Add the data components that reference the data source - dataSource.dataComponents = allDataComponents.filter(dataComponent => dataComponent.stix.x_mitre_data_source_ref === dataSource.stix.id); - } + async retrieveById(stixId, options, callback) { + try { + // versions=all Retrieve all versions of the data source with the stixId + // versions=latest Retrieve the data source with the latest modified date for this stixId + + if (!stixId) { + if (callback) { + return callback(new Error(this.errors.missingParameter)); + } + throw new MissingParameterError(); + } + + if (options.versions === 'all') { + const dataSources = await this.repository.model.find({ 'stix.id': stixId }).lean().exec(); + await DataSourcesService.addExtraDataToAll(dataSources, options.retrieveDataComponents); + if (callback) { + return callback(null, dataSources); + } + return dataSources; + } else if (options.versions === 'latest') { + const dataSource = await this.repository.model + .findOne({ 'stix.id': stixId }) + .sort('-stix.modified') + .lean() + .exec(); - async retrieveById(stixId, options, callback) { - try { - // versions=all Retrieve all versions of the data source with the stixId - // versions=latest Retrieve the data source with the latest modified date for this stixId - - if (!stixId) { - if (callback) { - return callback(new Error(this.errors.missingParameter)); - } - throw new MissingParameterError; - } - - if (options.versions === 'all') { - const dataSources = await this.repository.model.find({ 'stix.id': stixId }).lean().exec(); - await DataSourcesService.addExtraDataToAll(dataSources, options.retrieveDataComponents); - if (callback) { - return callback(null, dataSources); - } - return dataSources; - } else if (options.versions === 'latest') { - const dataSource = await this.repository.model.findOne({ 'stix.id': stixId }).sort('-stix.modified').lean().exec(); - - // Note: document is null if not found - if (dataSource) { - await DataSourcesService.addExtraData(dataSource, options.retrieveDataComponents); - if (callback) { - return callback(null, [dataSource]); - } - return [dataSource]; - } else { - if (callback) { - return callback(null, []); - } - return []; - } - } else { - if (callback) { - return callback(new Error(this.errors.invalidQueryStringParameter)); - } - throw new InvalidQueryStringParameterError; - } - } catch (err) { - if (err.name === 'CastError') { - if (callback) { - return callback(new Error(this.errors.badlyFormattedParameter)); - } - throw new BadlyFormattedParameterError; - } else { - if (callback) { - return callback(err); - } - throw err; - } + // Note: document is null if not found + if (dataSource) { + await DataSourcesService.addExtraData(dataSource, options.retrieveDataComponents); + if (callback) { + return callback(null, [dataSource]); + } + return [dataSource]; + } else { + if (callback) { + return callback(null, []); + } + return []; + } + } else { + if (callback) { + return callback(new Error(this.errors.invalidQueryStringParameter)); + } + throw new InvalidQueryStringParameterError(); + } + } catch (err) { + if (err.name === 'CastError') { + if (callback) { + return callback(new Error(this.errors.badlyFormattedParameter)); } + throw new BadlyFormattedParameterError(); + } else { + if (callback) { + return callback(err); + } + throw err; + } } + } + + async retrieveVersionById(stixId, modified, options, callback) { + try { + // Retrieve the version of the data source with the matching stixId and modified date + + if (!stixId) { + if (callback) { + return callback(new Error(this.errors.missingParameter)); + } + throw new MissingParameterError(); + } + + if (!modified) { + if (callback) { + return callback(new Error(this.errors.missingParameter)); + } + throw new MissingParameterError(); + } + + const dataSource = await this.repository.model + .findOne({ 'stix.id': stixId, 'stix.modified': modified }) + .exec(); - async retrieveVersionById(stixId, modified, options, callback) { - try { - // Retrieve the version of the data source with the matching stixId and modified date - - if (!stixId) { - if (callback) { - return callback(new Error(this.errors.missingParameter)); - } - throw new MissingParameterError; - } - - if (!modified) { - if (callback) { - return callback(new Error(this.errors.missingParameter)); - } - throw new MissingParameterError; - } - - const dataSource = await this.repository.model.findOne({ 'stix.id': stixId, 'stix.modified': modified }).exec(); - - // Note: document is null if not found - if (dataSource) { - await DataSourcesService.addExtraData(dataSource, options.retrieveDataComponents); - if (callback) { - return callback(null, dataSource); - } - return dataSource; - } else { - if (callback) { - return callback(null, null); - } - return null; - } - } catch (err) { - if (err.name === 'CastError') { - if (callback) { - return callback(new Error(this.errors.badlyFormattedParameter)); - } - throw new BadlyFormattedParameterError; - } else { - if (callback) { - return callback(err); - } - throw err; - } + // Note: document is null if not found + if (dataSource) { + await DataSourcesService.addExtraData(dataSource, options.retrieveDataComponents); + if (callback) { + return callback(null, dataSource); + } + return dataSource; + } else { + if (callback) { + return callback(null, null); + } + return null; + } + } catch (err) { + if (err.name === 'CastError') { + if (callback) { + return callback(new Error(this.errors.badlyFormattedParameter)); + } + throw new BadlyFormattedParameterError(); + } else { + if (callback) { + return callback(err); } + throw err; + } } + } } -module.exports = new DataSourcesService('x-mitre-data-source', dataSourcesRepository); \ No newline at end of file +module.exports = new DataSourcesService('x-mitre-data-source', dataSourcesRepository); diff --git a/app/services/groups-service.js b/app/services/groups-service.js index 0a8b098e..b833852f 100644 --- a/app/services/groups-service.js +++ b/app/services/groups-service.js @@ -3,6 +3,6 @@ const BaseService = require('./_base.service'); const groupsRepository = require('../repository/groups-repository'); -class GroupsService extends BaseService { } +class GroupsService extends BaseService {} -module.exports = new GroupsService('intrusion-set', groupsRepository); \ No newline at end of file +module.exports = new GroupsService('intrusion-set', groupsRepository); diff --git a/app/services/identities-service.js b/app/services/identities-service.js index 0a7b92fa..ddc45b5b 100644 --- a/app/services/identities-service.js +++ b/app/services/identities-service.js @@ -9,145 +9,147 @@ const BaseService = require('./_base.service'); const { InvalidTypeError } = require('../exceptions'); const errors = { - missingParameter: 'Missing required parameter', - badlyFormattedParameter: 'Badly formatted parameter', - duplicateId: 'Duplicate id', - notFound: 'Document not found', - invalidQueryStringParameter: 'Invalid query string parameter' + missingParameter: 'Missing required parameter', + badlyFormattedParameter: 'Badly formatted parameter', + duplicateId: 'Duplicate id', + notFound: 'Document not found', + invalidQueryStringParameter: 'Invalid query string parameter', }; exports.errors = errors; const identityType = 'identity'; class IdentitiesService extends BaseService { + async addCreatedByAndModifiedByIdentitiesToAll(attackObjects) { + const identityCache = new Map(); + const userAccountCache = new Map(); + for (const attackObject of attackObjects) { + // eslint-disable-next-line no-await-in-loop + await this.addCreatedByAndModifiedByIdentities(attackObject, identityCache, userAccountCache); + } + } - async addCreatedByAndModifiedByIdentitiesToAll(attackObjects) { - const identityCache = new Map(); - const userAccountCache = new Map(); - for (const attackObject of attackObjects) { - // eslint-disable-next-line no-await-in-loop - await this.addCreatedByAndModifiedByIdentities(attackObject, identityCache, userAccountCache); - } + async create(data, options) { + // This function overrides the base class create() because + // 1. It does not set the created_by_ref or x_mitre_modified_by_ref properties + // 2. It does not check for an existing identity object + + if (data?.stix?.type !== identityType) { + throw new InvalidTypeError(); } - - async create(data, options) { - // This function overrides the base class create() because - // 1. It does not set the created_by_ref or x_mitre_modified_by_ref properties - // 2. It does not check for an existing identity object - - if (data?.stix?.type !== identityType) { - throw new InvalidTypeError(); - } - options = options || {}; - if (!options.import) { - // Set the ATT&CK Spec Version - data.stix.x_mitre_attack_spec_version = data.stix.x_mitre_attack_spec_version ?? config.app.attackSpecVersion; + options = options || {}; + if (!options.import) { + // Set the ATT&CK Spec Version + data.stix.x_mitre_attack_spec_version = + data.stix.x_mitre_attack_spec_version ?? config.app.attackSpecVersion; - // Record the user account that created the object - if (options.userAccountId) { - data.workspace.workflow.created_by_user_account = options.userAccountId; - } + // Record the user account that created the object + if (options.userAccountId) { + data.workspace.workflow.created_by_user_account = options.userAccountId; + } - // Set the default marking definitions - await systemConfigurationService.setDefaultMarkingDefinitionsForObject(data); + // Set the default marking definitions + await systemConfigurationService.setDefaultMarkingDefinitionsForObject(data); - // Assign a new STIX id if not already provided - data.stix.id = data.stix.id || `identity--${uuid.v4()}`; - } + // Assign a new STIX id if not already provided + data.stix.id = data.stix.id || `identity--${uuid.v4()}`; + } - // Save the document in the database - const savedIdentity = await this.repository.save(data); - return savedIdentity; + // Save the document in the database + const savedIdentity = await this.repository.save(data); + return savedIdentity; + } + + async addCreatedByIdentity(attackObject, cache) { + // Use the cache if the caller provides one + if (cache) { + const identityObject = cache.get(attackObject.stix.created_by_ref); + if (identityObject) { + attackObject.created_by_identity = identityObject; + } } - async addCreatedByIdentity(attackObject, cache) { - // Use the cache if the caller provides one - if (cache) { - const identityObject = cache.get(attackObject.stix.created_by_ref); - if (identityObject) { - attackObject.created_by_identity = identityObject; - } - } + if (!attackObject.created_by_identity) { + // No cache or not found in cache + try { + // eslint-disable-next-line require-atomic-updates + const identityObject = await this.repository.retrieveLatestByStixId( + attackObject.stix.created_by_ref, + ); + attackObject.created_by_identity = identityObject; - if (!attackObject.created_by_identity) { - // No cache or not found in cache - try { - // eslint-disable-next-line require-atomic-updates - const identityObject = await this.repository.retrieveLatestByStixId(attackObject.stix.created_by_ref); - attackObject.created_by_identity = identityObject; - - if (cache) { - cache.set(attackObject.stix.created_by_ref, identityObject); - } - } - catch (err) { - // Ignore lookup errors - } + if (cache) { + cache.set(attackObject.stix.created_by_ref, identityObject); } + } catch (err) { + // Ignore lookup errors + } + } + } + + async addModifiedByIdentity(attackObject, cache) { + // Use the cache if the caller provides one + if (cache) { + const identityObject = cache.get(attackObject.stix.x_mitre_modified_by_ref); + if (identityObject) { + attackObject.modified_by_identity = identityObject; + } } - async addModifiedByIdentity(attackObject, cache) { - // Use the cache if the caller provides one - if (cache) { - const identityObject = cache.get(attackObject.stix.x_mitre_modified_by_ref); - if (identityObject) { - attackObject.modified_by_identity = identityObject; - } - } + if (!attackObject.modified_by_identity) { + // No cache or not found in cache + try { + // eslint-disable-next-line require-atomic-updates + const identityObject = await this.repository.retrieveLatestByStixId( + attackObject.stix.x_mitre_modified_by_ref, + ); + attackObject.modified_by_identity = identityObject; - if (!attackObject.modified_by_identity) { - // No cache or not found in cache - try { - // eslint-disable-next-line require-atomic-updates - const identityObject = await this.repository.retrieveLatestByStixId(attackObject.stix.x_mitre_modified_by_ref); - attackObject.modified_by_identity = identityObject; - - if (cache) { - cache.set(attackObject.stix.x_mitre_modified_by_ref, identityObject); - } - } - catch (err) { - // Ignore lookup errors - } + if (cache) { + cache.set(attackObject.stix.x_mitre_modified_by_ref, identityObject); } + } catch (err) { + // Ignore lookup errors + } } + } + + static async addCreatedByUserAccountWithCache(attackObject, cache) { + const userAccountRef = attackObject?.workspace?.workflow?.created_by_user_account; + if (userAccountRef) { + // Use the cache if the caller provides one + if (cache) { + const userAccountObject = cache.get(userAccountRef); + if (userAccountObject) { + attackObject.created_by_user_account = userAccountObject; + } + } - static async addCreatedByUserAccountWithCache(attackObject, cache) { - const userAccountRef = attackObject?.workspace?.workflow?.created_by_user_account; - if (userAccountRef) { - // Use the cache if the caller provides one - if (cache) { - const userAccountObject = cache.get(userAccountRef); - if (userAccountObject) { - attackObject.created_by_user_account = userAccountObject; - } - } - - if (!attackObject.created_by_user_account) { - // No cache or not found in cache - await userAccountsService.addCreatedByUserAccount(attackObject); - if (cache) { - cache.set(userAccountRef, attackObject.created_by_user_account); - } - } + if (!attackObject.created_by_user_account) { + // No cache or not found in cache + await userAccountsService.addCreatedByUserAccount(attackObject); + if (cache) { + cache.set(userAccountRef, attackObject.created_by_user_account); } + } } + } - async addCreatedByAndModifiedByIdentities(attackObject, identityCache, userAccountCache) { - if (attackObject && attackObject.stix && attackObject.stix.created_by_ref) { - await this.addCreatedByIdentity(attackObject, identityCache); - } + async addCreatedByAndModifiedByIdentities(attackObject, identityCache, userAccountCache) { + if (attackObject && attackObject.stix && attackObject.stix.created_by_ref) { + await this.addCreatedByIdentity(attackObject, identityCache); + } - if (attackObject && attackObject.stix && attackObject.stix.x_mitre_modified_by_ref) { - await this.addModifiedByIdentity(attackObject, identityCache); - } + if (attackObject && attackObject.stix && attackObject.stix.x_mitre_modified_by_ref) { + await this.addModifiedByIdentity(attackObject, identityCache); + } - // Add user account data - if (attackObject?.workspace?.workflow?.created_by_user_account) { - await IdentitiesService.addCreatedByUserAccountWithCache(attackObject, userAccountCache); - } + // Add user account data + if (attackObject?.workspace?.workflow?.created_by_user_account) { + await IdentitiesService.addCreatedByUserAccountWithCache(attackObject, userAccountCache); } + } } -module.exports = new IdentitiesService(identityType, identitiesRepository); \ No newline at end of file +module.exports = new IdentitiesService(identityType, identitiesRepository); diff --git a/app/services/marking-definitions-service.js b/app/services/marking-definitions-service.js index 304b117a..6d15a81d 100644 --- a/app/services/marking-definitions-service.js +++ b/app/services/marking-definitions-service.js @@ -6,123 +6,127 @@ const identitiesService = require('./identities-service'); const config = require('../config/config'); const BaseService = require('./_base.service'); const markingDefinitionsRepository = require('../repository/marking-definitions-repository'); -const { MissingParameterError, BadlyFormattedParameterError, CannotUpdateStaticObjectError, DuplicateIdError } = require('../exceptions'); +const { + MissingParameterError, + BadlyFormattedParameterError, + CannotUpdateStaticObjectError, + DuplicateIdError, +} = require('../exceptions'); // NOTE: A marking definition does not support the modified or revoked properties!! class MarkingDefinitionsService extends BaseService { - async create(data, options) { - // This function handles two use cases: - // 1. This is a completely new object. Create a new object and generate the stix.id if not already - // provided. Set stix.created_by_ref to the organization identity. - // 2. stix.id is defined and options.import is set. Create a new object - // using the specified stix.id and stix.created_by_ref. - // TBD: Overwrite existing object when importing?? - - const markingDefinition = this.repository.createNewDocument(data); - - options = options || {}; - if (!options.import) { - // Set the ATT&CK Spec Version - markingDefinition.stix.x_mitre_attack_spec_version = markingDefinition.stix.x_mitre_attack_spec_version ?? config.app.attackSpecVersion; - - // Record the user account that created the object - if (options.userAccountId) { - markingDefinition.workspace.workflow.created_by_user_account = options.userAccountId; - } - - // Get the organization identity - const organizationIdentityRef = await systemConfigurationService.retrieveOrganizationIdentityRef(); - - // Check for an existing object - let existingObject; - if (markingDefinition.stix.id) { - existingObject = await this.repository.retrieveOneById(markingDefinition.stix.id); - } - - if (existingObject) { - // Cannot create a new version of an existing object - throw new BadlyFormattedParameterError; - } - else { - // New object - // Assign a new STIX id if not already provided - markingDefinition.stix.id = markingDefinition.stix.id || `marking-definition--${uuid.v4()}`; - - // Set the created_by_ref property - markingDefinition.stix.created_by_ref = organizationIdentityRef; - } - } + async create(data, options) { + // This function handles two use cases: + // 1. This is a completely new object. Create a new object and generate the stix.id if not already + // provided. Set stix.created_by_ref to the organization identity. + // 2. stix.id is defined and options.import is set. Create a new object + // using the specified stix.id and stix.created_by_ref. + // TBD: Overwrite existing object when importing?? + + const markingDefinition = this.repository.createNewDocument(data); + + options = options || {}; + if (!options.import) { + // Set the ATT&CK Spec Version + markingDefinition.stix.x_mitre_attack_spec_version = + markingDefinition.stix.x_mitre_attack_spec_version ?? config.app.attackSpecVersion; + + // Record the user account that created the object + if (options.userAccountId) { + markingDefinition.workspace.workflow.created_by_user_account = options.userAccountId; + } + + // Get the organization identity + const organizationIdentityRef = + await systemConfigurationService.retrieveOrganizationIdentityRef(); + + // Check for an existing object + let existingObject; + if (markingDefinition.stix.id) { + existingObject = await this.repository.retrieveOneById(markingDefinition.stix.id); + } + + if (existingObject) { + // Cannot create a new version of an existing object + throw new BadlyFormattedParameterError(); + } else { + // New object + // Assign a new STIX id if not already provided + markingDefinition.stix.id = markingDefinition.stix.id || `marking-definition--${uuid.v4()}`; + + // Set the created_by_ref property + markingDefinition.stix.created_by_ref = organizationIdentityRef; + } + } - // Save the document in the database - try { - return await this.repository.saveDocument(markingDefinition); - } - catch(err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - throw new DuplicateIdError; - } - else { - throw err; - } - } + // Save the document in the database + try { + return await this.repository.saveDocument(markingDefinition); + } catch (err) { + if (err.name === 'MongoServerError' && err.code === 11000) { + throw new DuplicateIdError(); + } else { + throw err; + } } + } - async updateFull(stixId, data, callback) { - if (data?.workspace?.workflow?.state === 'static') { - const err = new CannotUpdateStaticObjectError; - if (callback) { - return callback(err); - } + async updateFull(stixId, data, callback) { + if (data?.workspace?.workflow?.state === 'static') { + const err = new CannotUpdateStaticObjectError(); + if (callback) { + return callback(err); + } - throw new CannotUpdateStaticObjectError; - } + throw new CannotUpdateStaticObjectError(); + } - const newDoc = await super.updateFull(stixId, data, callback); - return newDoc; -} + const newDoc = await super.updateFull(stixId, data, callback); + return newDoc; + } - async retrieveById(stixId, options, callback) { - try { - if (!stixId) { - throw new MissingParameterError; - } - - const markingDefinition = await this.repository.retrieveOneById(stixId ); - - // Note: document is null if not found - if (markingDefinition) { - await identitiesService.addCreatedByAndModifiedByIdentities(markingDefinition); - if (callback) { - return callback(null, [markingDefinition]); - } - return [markingDefinition]; - } else { - if (callback) { - return callback(null, []); - } - return []; - } - } catch (err) { - if (callback) { - return callback(err); - } - if (err.name === 'CastError') { - throw new BadlyFormattedParameterError; - } else { - throw err; - } - } - } + async retrieveById(stixId, options, callback) { + try { + if (!stixId) { + throw new MissingParameterError(); + } + + const markingDefinition = await this.repository.retrieveOneById(stixId); - async delete(stixId) { - if (!stixId) { - throw new MissingParameterError; + // Note: document is null if not found + if (markingDefinition) { + await identitiesService.addCreatedByAndModifiedByIdentities(markingDefinition); + if (callback) { + return callback(null, [markingDefinition]); } + return [markingDefinition]; + } else { + if (callback) { + return callback(null, []); + } + return []; + } + } catch (err) { + if (callback) { + return callback(err); + } + if (err.name === 'CastError') { + throw new BadlyFormattedParameterError(); + } else { + throw err; + } + } + } - //Note: markingDefinition is null if not found - return await this.repository.deleteOneById(stixId); + async delete(stixId) { + if (!stixId) { + throw new MissingParameterError(); } + + //Note: markingDefinition is null if not found + return await this.repository.deleteOneById(stixId); + } } module.exports = new MarkingDefinitionsService('marking-definition', markingDefinitionsRepository); diff --git a/app/services/matrices-service.js b/app/services/matrices-service.js index 858a2e18..9a2c95c0 100644 --- a/app/services/matrices-service.js +++ b/app/services/matrices-service.js @@ -8,111 +8,115 @@ const matrixRepository = require('../repository/matrix-repository'); const BaseService = require('./_base.service'); class MatrixService extends BaseService { + constructor(type, repository) { + super(type, repository); - constructor(type, repository) { - super(type, repository); + this.retrieveTacticById = null; + this.retrieveTechniquesForTactic = null; + } - this.retrieveTacticById = null; - this.retrieveTechniquesForTactic = null; + // Custom methods specific to MatrixService should be specified below + + async retrieveTechniquesForMatrix(stixId, modified, callback) { + if (BaseService.isCallback(arguments[arguments.length - 1])) { + callback = arguments[arguments.length - 1]; } - // Custom methods specific to MatrixService should be specified below + // Lazy loading of services + if (!this.retrieveTacticById || !this.retrieveTechniquesForTactic) { + const tacticsService = require('./tactics-service'); + this.retrieveTacticById = util.promisify(tacticsService.retrieveById); + this.retrieveTechniquesForTactic = tacticsService.retrieveTechniquesForTactic; + } - async retrieveTechniquesForMatrix(stixId, modified, callback) { + if (!stixId) { + const err = new MissingParameterError({ parameterName: 'stixId' }); + if (callback) { + return callback(err); + } + throw err; + } - if (BaseService.isCallback(arguments[arguments.length - 1])) { - callback = arguments[arguments.length - 1]; - } + if (!modified) { + const err = new MissingParameterError({ parameterName: 'modified' }); + if (callback) { + return callback(err); + } + throw err; + } - // Lazy loading of services - if (!this.retrieveTacticById || !this.retrieveTechniquesForTactic) { - const tacticsService = require('./tactics-service'); - this.retrieveTacticById = util.promisify(tacticsService.retrieveById); - this.retrieveTechniquesForTactic = tacticsService.retrieveTechniquesForTactic; - } + let matrix; + // eslint-disable-next-line no-useless-catch + try { + matrix = await matrixRepository.retrieveOneByVersion(stixId, modified); + } catch (err) { + if (callback) { + return callback(err); + } + throw err; // Let the DatabaseError bubble up + } - if (!stixId) { - const err = new MissingParameterError({ parameterName: 'stixId' }); - if (callback) { - return callback(err); - } - throw err; - } + if (!matrix) { + if (callback) { + return callback(null, null); + } + return null; + } - if (!modified) { - const err = new MissingParameterError({ parameterName: 'modified' }); - if (callback) { - return callback(err); - } - throw err; + const options = { versions: 'latest', offset: 0, limit: 0 }; + const tacticsTechniques = {}; + + for (const tacticId of matrix.stix.tactic_refs) { + let tactics, techniques; + try { + tactics = await this.retrieveTacticById(tacticId, options); + if (tactics && tactics.length) { + techniques = await this.retrieveTechniquesForTactic( + tacticId, + tactics[0].stix.modified, + options, + ); } - - let matrix; - // eslint-disable-next-line no-useless-catch - try { - matrix = await matrixRepository.retrieveOneByVersion(stixId, modified); - } catch (err) { - if (callback) { - return callback(err); - } - throw err; // Let the DatabaseError bubble up + } catch (err) { + const genericServiceError = new GenericServiceError(err); // TODO it's probably better to throw TechniquesServiceError or TacticsServiceError + if (callback) { + return callback(genericServiceError); } - - if (!matrix) { - if (callback) { - return callback(null, null); - } - return null; + throw genericServiceError; + } + + if (tactics && tactics.length) { + const tactic = tactics[0]; + const parentTechniques = []; + const subtechniques = []; + + for (const technique of techniques) { + if (!technique.stix.x_mitre_is_subtechnique) { + parentTechniques.push(technique); + } else { + subtechniques.push(technique); + } } - const options = { versions: 'latest', offset: 0, limit: 0 }; - const tacticsTechniques = {}; - - for (const tacticId of matrix.stix.tactic_refs) { - let tactics, techniques; - try { - tactics = await this.retrieveTacticById(tacticId, options); - if (tactics && tactics.length) { - techniques = await this.retrieveTechniquesForTactic(tacticId, tactics[0].stix.modified, options); - } - } catch (err) { - const genericServiceError = new GenericServiceError(err); // TODO it's probably better to throw TechniquesServiceError or TacticsServiceError - if (callback) { - return callback(genericServiceError); - } - throw genericServiceError; - } - - if (tactics && tactics.length) { - const tactic = tactics[0]; - 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); - } - } - } - tactic.techniques = parentTechniques; - tacticsTechniques[tactic.stix.name] = tactic; + 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); } + } } - if (callback) { - return callback(null, tacticsTechniques); - } - return tacticsTechniques; + tactic.techniques = parentTechniques; + tacticsTechniques[tactic.stix.name] = tactic; + } + } + if (callback) { + return callback(null, tacticsTechniques); } + return tacticsTechniques; + } } -module.exports = new MatrixService('x-mitre-matrix', matrixRepository); \ No newline at end of file +module.exports = new MatrixService('x-mitre-matrix', matrixRepository); diff --git a/app/services/mitigations-service.js b/app/services/mitigations-service.js index a2b81ded..3684647b 100644 --- a/app/services/mitigations-service.js +++ b/app/services/mitigations-service.js @@ -4,6 +4,6 @@ const mitigationsRepository = require('../repository/mitigations-repository'); const BaseService = require('./_base.service'); -class MitigationsService extends BaseService { } +class MitigationsService extends BaseService {} -module.exports = new MitigationsService('course-of-action', mitigationsRepository); \ No newline at end of file +module.exports = new MitigationsService('course-of-action', mitigationsRepository); diff --git a/app/services/notes-service.js b/app/services/notes-service.js index 26e033c1..8074bb2d 100644 --- a/app/services/notes-service.js +++ b/app/services/notes-service.js @@ -3,51 +3,50 @@ const notesRepository = require('../repository/notes-repository'); const BaseService = require('./_base.service'); -const { BadlyFormattedParameterError, DuplicateIdError, MissingParameterError } = require('../exceptions'); +const { + BadlyFormattedParameterError, + DuplicateIdError, + MissingParameterError, +} = require('../exceptions'); class NotesService extends BaseService { + async updateVersion(stixId, stixModified, data) { + if (!stixId) { + throw new MissingParameterError(); + } - async updateVersion(stixId, stixModified, data) { - if (!stixId) { - throw new MissingParameterError; - } + if (!stixModified) { + throw new MissingParameterError(); + } - if (!stixModified) { - throw new MissingParameterError; - } + try { + const document = await this.repository.retrieveOneByVersion(stixId, stixModified); + if (!document) { + // document not found + return null; + } else { + // Copy data to found document and save try { - const document = await this.repository.retrieveOneByVersion(stixId, stixModified); - - if (!document) { - // document not found - return null; - } - else { - // Copy data to found document and save - try { - Object.assign(document, data); - const savedDocument = await document.save(); - return savedDocument; - } catch (err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - throw new DuplicateIdError; - } - else { - throw err; - } - } - } - } catch (err) { - if (err.name === 'CastError') { - throw new BadlyFormattedParameterError; - } - else { - throw err; - } + Object.assign(document, data); + const savedDocument = await document.save(); + return savedDocument; + } catch (err) { + if (err.name === 'MongoServerError' && err.code === 11000) { + throw new DuplicateIdError(); + } else { + throw err; + } } + } + } catch (err) { + if (err.name === 'CastError') { + throw new BadlyFormattedParameterError(); + } else { + throw err; + } } - + } } -module.exports = new NotesService('note', notesRepository); \ No newline at end of file +module.exports = new NotesService('note', notesRepository); diff --git a/app/services/recent-activity-service.js b/app/services/recent-activity-service.js index 0800127b..9709d61f 100644 --- a/app/services/recent-activity-service.js +++ b/app/services/recent-activity-service.js @@ -7,121 +7,119 @@ const identitiesService = require('./identities-service'); const { lastUpdatedByQueryHelper } = require('../lib/request-parameter-helper'); const errors = { - missingParameter: 'Missing required parameter', - badlyFormattedParameter: 'Badly formatted parameter', - duplicateId: 'Duplicate id', - notFound: 'Document not found', - invalidQueryStringParameter: 'Invalid query string parameter', - duplicateCollection: 'Duplicate collection' + missingParameter: 'Missing required parameter', + badlyFormattedParameter: 'Badly formatted parameter', + duplicateId: 'Duplicate id', + notFound: 'Document not found', + invalidQueryStringParameter: 'Invalid query string parameter', + duplicateCollection: 'Duplicate collection', }; exports.errors = errors; -exports.retrieveAll = async function(options) { - // Build the query - const query = {}; - if (!options.includeRevoked) { - query['stix.revoked'] = { $in: [null, false] }; - } - if (!options.includeDeprecated) { - query['stix.x_mitre_deprecated'] = { $in: [null, false] }; - } - if (typeof options.lastUpdatedBy !== 'undefined') { - query['workspace.workflow.created_by_user_account'] = lastUpdatedByQueryHelper(options.lastUpdatedBy); - } - - // Filter out objects without modified dates (incl. Marking Definitions & Identities) - query['stix.modified'] = { $exists: true }; - - // Build the aggregation - const aggregation = []; - - // Sort objects by last modified - aggregation.push({ $sort: { 'stix.modified': -1 } }); - - // Limit documents to prevent memory issues - const limit = options.limit ?? 0; - if (limit) { - aggregation.push({ $limit: limit }); +exports.retrieveAll = async function (options) { + // Build the query + const query = {}; + if (!options.includeRevoked) { + query['stix.revoked'] = { $in: [null, false] }; + } + if (!options.includeDeprecated) { + query['stix.x_mitre_deprecated'] = { $in: [null, false] }; + } + if (typeof options.lastUpdatedBy !== 'undefined') { + query['workspace.workflow.created_by_user_account'] = lastUpdatedByQueryHelper( + options.lastUpdatedBy, + ); + } + + // Filter out objects without modified dates (incl. Marking Definitions & Identities) + query['stix.modified'] = { $exists: true }; + + // Build the aggregation + const aggregation = []; + + // Sort objects by last modified + aggregation.push({ $sort: { 'stix.modified': -1 } }); + + // Limit documents to prevent memory issues + const limit = options.limit ?? 0; + if (limit) { + aggregation.push({ $limit: limit }); + } + + // Then apply query, skip and limit options + aggregation.push({ $match: query }); + + // Retrieve the documents + const objectDocuments = await AttackObject.aggregate(aggregation); + + // Lookup source/target refs for relationships + aggregation.push({ + $lookup: { + from: 'attackObjects', + localField: 'stix.source_ref', + foreignField: 'stix.id', + as: 'source_objects', + }, + }); + aggregation.push({ + $lookup: { + from: 'attackObjects', + localField: 'stix.target_ref', + foreignField: 'stix.id', + as: 'target_objects', + }, + }); + const relationshipDocuments = await Relationship.aggregate(aggregation); + const documents = objectDocuments.concat(relationshipDocuments); + + // Sort by most recent + documents.sort((a, b) => b.stix.modified - a.stix.modified); + + // Move latest source and target objects to a non-array property, then remove array of source and target objects + for (const document of documents) { + if (Array.isArray(document.source_objects)) { + if (document.source_objects.length === 0) { + document.source_objects = undefined; + } else { + document.source_object = document.source_objects[0]; + document.source_objects = undefined; + } } - // Then apply query, skip and limit options - aggregation.push({ $match: query }); - - // Retrieve the documents - const objectDocuments = await AttackObject.aggregate(aggregation); - - // Lookup source/target refs for relationships - aggregation.push({ - $lookup: { - from: 'attackObjects', - localField: 'stix.source_ref', - foreignField: 'stix.id', - as: 'source_objects' - } - }); - aggregation.push({ - $lookup: { - from: 'attackObjects', - localField: 'stix.target_ref', - foreignField: 'stix.id', - as: 'target_objects' - } - }); - const relationshipDocuments = await Relationship.aggregate(aggregation); - const documents = objectDocuments.concat(relationshipDocuments); - - // Sort by most recent - documents.sort((a, b) => b.stix.modified - a.stix.modified); - - // Move latest source and target objects to a non-array property, then remove array of source and target objects - for (const document of documents) { - if (Array.isArray(document.source_objects)) { - if (document.source_objects.length === 0) { - document.source_objects = undefined; - } - else { - document.source_object = document.source_objects[0]; - document.source_objects = undefined; - } - } - - if (Array.isArray(document.target_objects)) { - if (document.target_objects.length === 0) { - document.target_objects = undefined; - } - else { - document.target_object = document.target_objects[0]; - document.target_objects = undefined; - } - } - } - - // Apply pagination - const offset = options.offset ?? 0; - let paginatedDocuments; - if (limit > 0) { - paginatedDocuments = documents.slice(offset, offset + limit); - } - else { - paginatedDocuments = documents.slice(offset); - } - - // Add identities - await identitiesService.addCreatedByAndModifiedByIdentitiesToAll(paginatedDocuments); - - // Prepare the return value - if (options.includePagination) { - const returnValue = { - pagination: { - total: documents.length, - offset: options.offset, - limit: options.limit - }, - data: paginatedDocuments - }; - return returnValue; - } - else { - return paginatedDocuments; + if (Array.isArray(document.target_objects)) { + if (document.target_objects.length === 0) { + document.target_objects = undefined; + } else { + document.target_object = document.target_objects[0]; + document.target_objects = undefined; + } } + } + + // Apply pagination + const offset = options.offset ?? 0; + let paginatedDocuments; + if (limit > 0) { + paginatedDocuments = documents.slice(offset, offset + limit); + } else { + paginatedDocuments = documents.slice(offset); + } + + // Add identities + await identitiesService.addCreatedByAndModifiedByIdentitiesToAll(paginatedDocuments); + + // Prepare the return value + if (options.includePagination) { + const returnValue = { + pagination: { + total: documents.length, + offset: options.offset, + limit: options.limit, + }, + data: paginatedDocuments, + }; + return returnValue; + } else { + return paginatedDocuments; + } }; diff --git a/app/services/references-service.js b/app/services/references-service.js index 5ef607be..a8963bc2 100644 --- a/app/services/references-service.js +++ b/app/services/references-service.js @@ -5,38 +5,37 @@ const referencesRepository = require('../repository/references-repository'); const { MissingParameterError } = require('../exceptions'); class ReferencesService { + constructor(repository) { + this.repository = repository; + } - constructor(repository) { - this.repository = repository; - } + async retrieveAll(options) { + const results = await this.repository.retrieveAll(options); + const paginatedResults = baseService.paginate(options, results); - async retrieveAll(options) { - const results = await this.repository.retrieveAll(options); - const paginatedResults = baseService.paginate(options, results); + return paginatedResults; + } - return paginatedResults; - } - - async create(data) { - return await this.repository.save(data); - } - - async update(data) { - // Note: source_name is used as the key and cannot be updated - if (!data.source_name) { - throw new MissingParameterError({ parameterName: 'source_name' }); - } - - return await this.repository.updateAndSave(data); + async create(data) { + return await this.repository.save(data); + } + + async update(data) { + // Note: source_name is used as the key and cannot be updated + if (!data.source_name) { + throw new MissingParameterError({ parameterName: 'source_name' }); } - async deleteBySourceName(sourceName) { - if (!sourceName) { - throw new MissingParameterError({ parameterName: 'source_name' }); - } + return await this.repository.updateAndSave(data); + } - return await this.repository.findOneAndRemove(sourceName); + async deleteBySourceName(sourceName) { + if (!sourceName) { + throw new MissingParameterError({ parameterName: 'source_name' }); } + + return await this.repository.findOneAndRemove(sourceName); + } } -module.exports = new ReferencesService(referencesRepository); \ No newline at end of file +module.exports = new ReferencesService(referencesRepository); diff --git a/app/services/relationships-service.js b/app/services/relationships-service.js index 9a355eb6..d37fc8a6 100644 --- a/app/services/relationships-service.js +++ b/app/services/relationships-service.js @@ -14,135 +14,144 @@ const uuid = require('uuid'); const { InvalidTypeError } = require('../exceptions'); class RelationshipsService extends BaseService { + static objectTypeMap = new Map([ + ['malware', 'software'], + ['tool', 'software'], + ['attack-pattern', 'technique'], + ['intrusion-set', 'group'], + ['campaign', 'campaign'], + ['x-mitre-asset', 'asset'], + ['course-of-action', 'mitigation'], + ['x-mitre-tactic', 'tactic'], + ['x-mitre-matrix', 'matrix'], + ['x-mitre-data-component', 'data-component'], + ]); + + async retrieveAll(options) { + // First get results from repository + const results = await this.repository.retrieveAll(options); + + // Apply source/target type filtering if needed + if (options.sourceType || options.targetType) { + const [{ documents, totalCount }] = results; + let filteredDocs = documents; + + // Filter by source type if specified + if (options.sourceType) { + filteredDocs = filteredDocs.filter((document) => { + if (!document.source_objects?.length) { + return false; + } + // Sort by modified date to get the latest version + document.source_objects.sort((a, b) => b.stix.modified - a.stix.modified); + return ( + RelationshipsService.objectTypeMap.get(document.source_objects[0].stix.type) === + options.sourceType + ); + }); + } + + // Filter by target type if specified + if (options.targetType) { + filteredDocs = filteredDocs.filter((document) => { + if (!document.target_objects?.length) { + return false; + } + // Sort by modified date to get the latest version + document.target_objects.sort((a, b) => b.stix.modified - a.stix.modified); + return ( + RelationshipsService.objectTypeMap.get(document.target_objects[0].stix.type) === + options.targetType + ); + }); + } + + // Update results with filtered documents and recalculate total count + results[0].documents = filteredDocs; + if (totalCount?.length > 0) { + results[0].totalCount[0].totalCount = filteredDocs.length; + } + } - static objectTypeMap = new Map([ - ['malware', 'software'], - ['tool', 'software'], - ['attack-pattern', 'technique'], - ['intrusion-set', 'group'], - ['campaign', 'campaign'], - ['x-mitre-asset', 'asset'], - ['course-of-action', 'mitigation'], - ['x-mitre-tactic', 'tactic'], - ['x-mitre-matrix', 'matrix'], - ['x-mitre-data-component', 'data-component'] - ]); - - async retrieveAll(options) { - // First get results from repository - const results = await this.repository.retrieveAll(options); - - // Apply source/target type filtering if needed - if (options.sourceType || options.targetType) { - const [{ documents, totalCount }] = results; - let filteredDocs = documents; - - // Filter by source type if specified - if (options.sourceType) { - filteredDocs = filteredDocs.filter(document => { - if (!document.source_objects?.length) { - return false; - } - // Sort by modified date to get the latest version - document.source_objects.sort((a, b) => b.stix.modified - a.stix.modified); - return RelationshipsService.objectTypeMap.get(document.source_objects[0].stix.type) === options.sourceType; - }); - } - - // Filter by target type if specified - if (options.targetType) { - filteredDocs = filteredDocs.filter(document => { - if (!document.target_objects?.length) { - return false; - } - // Sort by modified date to get the latest version - document.target_objects.sort((a, b) => b.stix.modified - a.stix.modified); - return RelationshipsService.objectTypeMap.get(document.target_objects[0].stix.type) === options.targetType; - }); - } - - // Update results with filtered documents and recalculate total count - results[0].documents = filteredDocs; - if (totalCount?.length > 0) { - results[0].totalCount[0].totalCount = filteredDocs.length; - } - } + // Add identity information if requested + if (options.includeIdentities) { + await identitiesService.addCreatedByAndModifiedByIdentitiesToAll(results[0].documents); + } - // Add identity information if requested - if (options.includeIdentities) { - await identitiesService.addCreatedByAndModifiedByIdentitiesToAll(results[0].documents); + // Format and return results + return RelationshipsService._formatResults(results, options); + } + + static _formatResults(results, options) { + const [{ documents, totalCount }] = results; + + // Move source/target objects to single properties + for (const document of documents) { + if (Array.isArray(document.source_objects)) { + document.source_object = document.source_objects[0]; + delete document.source_objects; + } + if (Array.isArray(document.target_objects)) { + document.target_object = document.target_objects[0]; + delete document.target_objects; + } + } + + // Return with or without pagination wrapper + return options.includePagination + ? { + pagination: { + total: totalCount[0]?.totalCount || 0, + offset: options.offset, + limit: options.limit, + }, + data: documents, } + : documents; + } - // Format and return results - return RelationshipsService._formatResults(results, options); + async create(data, options = {}) { + if (data?.stix?.type !== 'relationship') { + throw new InvalidTypeError(); } - static _formatResults(results, options) { - const [{ documents, totalCount }] = results; - - // Move source/target objects to single properties - for (const document of documents) { - if (Array.isArray(document.source_objects)) { - document.source_object = document.source_objects[0]; - delete document.source_objects; - } - if (Array.isArray(document.target_objects)) { - document.target_object = document.target_objects[0]; - delete document.target_objects; - } - } + if (!options.import) { + await this._enrichMetadata(data, options); + } - // Return with or without pagination wrapper - return options.includePagination ? { - pagination: { - total: totalCount[0]?.totalCount || 0, - offset: options.offset, - limit: options.limit - }, - data: documents - } : documents; + return this.repository.save(data); + } + + async _enrichMetadata(data, options) { + data.stix.x_mitre_attack_spec_version = + data.stix.x_mitre_attack_spec_version ?? config.app.attackSpecVersion; + + if (options.userAccountId) { + data.workspace.workflow.created_by_user_account = options.userAccountId; } - async create(data, options = {}) { - if (data?.stix?.type !== 'relationship') { - throw new InvalidTypeError(); - } + // Set the default marking definitions + await systemConfigService.setDefaultMarkingDefinitionsForObject(data); - if (!options.import) { - await this._enrichMetadata(data, options); - } + const organizationIdentityRef = + await this.systemConfigService.retrieveOrganizationIdentityRef(); - return this.repository.save(data); + // Check for existing object + let existingObject; + if (data.stix.id) { + existingObject = await this.repository.retrieveOneById(data.stix.id); } - async _enrichMetadata(data, options) { - data.stix.x_mitre_attack_spec_version = data.stix.x_mitre_attack_spec_version ?? config.app.attackSpecVersion; - - if (options.userAccountId) { - data.workspace.workflow.created_by_user_account = options.userAccountId; - } - - // Set the default marking definitions - await systemConfigService.setDefaultMarkingDefinitionsForObject(data); - - const organizationIdentityRef = await this.systemConfigService.retrieveOrganizationIdentityRef(); - - // Check for existing object - let existingObject; - if (data.stix.id) { - existingObject = await this.repository.retrieveOneById(data.stix.id); - } - - if (existingObject) { - // New version - only set modified_by_ref - data.stix.x_mitre_modified_by_ref = organizationIdentityRef; - } else { - // New object - set both refs and generate id if needed - data.stix.id = data.stix.id || `relationship--${uuid.v4()}`; - data.stix.created_by_ref = organizationIdentityRef; - data.stix.x_mitre_modified_by_ref = organizationIdentityRef; - } + if (existingObject) { + // New version - only set modified_by_ref + data.stix.x_mitre_modified_by_ref = organizationIdentityRef; + } else { + // New object - set both refs and generate id if needed + data.stix.id = data.stix.id || `relationship--${uuid.v4()}`; + data.stix.created_by_ref = organizationIdentityRef; + data.stix.x_mitre_modified_by_ref = organizationIdentityRef; } + } } module.exports = new RelationshipsService('relationship', relationshipsRepository); diff --git a/app/services/software-service.js b/app/services/software-service.js index f54516d9..c6be1b27 100644 --- a/app/services/software-service.js +++ b/app/services/software-service.js @@ -10,78 +10,78 @@ const BaseService = require('./_base.service'); const softwareRepository = require('../repository/software-repository'); class SoftwareService extends BaseService { - async create(data, options, callback) { - // This function handles two use cases: - // 1. This is a completely new object. Create a new object and generate the stix.id if not already - // provided. Set both stix.created_by_ref and stix.x_mitre_modified_by_ref to the organization identity. - // 2. This is a new version of an existing object. Create a new object with the specified id. - // Set stix.x_mitre_modified_by_ref to the organization identity. + async create(data, options, callback) { + // This function handles two use cases: + // 1. This is a completely new object. Create a new object and generate the stix.id if not already + // provided. Set both stix.created_by_ref and stix.x_mitre_modified_by_ref to the organization identity. + // 2. This is a new version of an existing object. Create a new object with the specified id. + // Set stix.x_mitre_modified_by_ref to the organization identity. - // is_family defaults to true for malware, not allowed for tools - try { - if (data?.stix?.type !== 'malware' && data?.stix?.type !== 'tool') { - throw new InvalidTypeError(); - } + // is_family defaults to true for malware, not allowed for tools + try { + if (data?.stix?.type !== 'malware' && data?.stix?.type !== 'tool') { + throw new InvalidTypeError(); + } - if (data.stix && data.stix.type === 'malware' && typeof data.stix.is_family !== 'boolean') { - data.stix.is_family = true; - } - else if (data.stix && data.stix.type === 'tool' && data.stix.is_family !== undefined) { - throw new PropertyNotAllowedError(); - } + if (data.stix && data.stix.type === 'malware' && typeof data.stix.is_family !== 'boolean') { + data.stix.is_family = true; + } else if (data.stix && data.stix.type === 'tool' && data.stix.is_family !== undefined) { + throw new PropertyNotAllowedError(); + } - options = options || {}; - if (!options.import) { - // Set the ATT&CK Spec Version - data.stix.x_mitre_attack_spec_version = data.stix.x_mitre_attack_spec_version ?? config.app.attackSpecVersion; + options = options || {}; + if (!options.import) { + // Set the ATT&CK Spec Version + data.stix.x_mitre_attack_spec_version = + data.stix.x_mitre_attack_spec_version ?? config.app.attackSpecVersion; - // Record the user account that created the object - if (options.userAccountId) { - data.workspace.workflow.created_by_user_account = options.userAccountId; - } + // Record the user account that created the object + if (options.userAccountId) { + data.workspace.workflow.created_by_user_account = options.userAccountId; + } - // Set the default marking definitions - await systemConfigurationService.setDefaultMarkingDefinitionsForObject(data); + // Set the default marking definitions + await systemConfigurationService.setDefaultMarkingDefinitionsForObject(data); - // Get the organization identity - const organizationIdentityRef = await systemConfigurationService.retrieveOrganizationIdentityRef(); + // Get the organization identity + const organizationIdentityRef = + await systemConfigurationService.retrieveOrganizationIdentityRef(); - // Check for an existing object - let existingObject; - if (data.stix.id) { - existingObject = await this.repository.retrieveOneById(data.stix.id); - } + // Check for an existing object + let existingObject; + if (data.stix.id) { + existingObject = await this.repository.retrieveOneById(data.stix.id); + } - if (existingObject) { - // New version of an existing object - // Only set the x_mitre_modified_by_ref property - data.stix.x_mitre_modified_by_ref = organizationIdentityRef; - } - else { - // New object - // Assign a new STIX id if not already provided - if (!data.stix.id) { - // const stixIdPrefix = getStixIdPrefixFromModel(this.model.modelName, data.stix.type); - data.stix.id = `${data.stix.type}--${uuid.v4()}`; - } + if (existingObject) { + // New version of an existing object + // Only set the x_mitre_modified_by_ref property + data.stix.x_mitre_modified_by_ref = organizationIdentityRef; + } else { + // New object + // Assign a new STIX id if not already provided + if (!data.stix.id) { + // const stixIdPrefix = getStixIdPrefixFromModel(this.model.modelName, data.stix.type); + data.stix.id = `${data.stix.type}--${uuid.v4()}`; + } - // Set the created_by_ref and x_mitre_modified_by_ref properties - data.stix.created_by_ref = organizationIdentityRef; - data.stix.x_mitre_modified_by_ref = organizationIdentityRef; - } - } - const res = await this.repository.save(data); - if (callback) { - return callback(null, res); - } - return res; - } catch (err) { - if (callback) { - return callback(err); - } - throw err; + // Set the created_by_ref and x_mitre_modified_by_ref properties + data.stix.created_by_ref = organizationIdentityRef; + data.stix.x_mitre_modified_by_ref = organizationIdentityRef; } + } + const res = await this.repository.save(data); + if (callback) { + return callback(null, res); + } + return res; + } catch (err) { + if (callback) { + return callback(err); + } + throw err; } + } } -module.exports = new SoftwareService(null, softwareRepository); \ No newline at end of file +module.exports = new SoftwareService(null, softwareRepository); diff --git a/app/services/stix-bundles-service.js b/app/services/stix-bundles-service.js index 01ddbea1..bb33d610 100644 --- a/app/services/stix-bundles-service.js +++ b/app/services/stix-bundles-service.js @@ -14,581 +14,637 @@ const Tactic = require('../models/tactic-model'); const Technique = require('../models/technique-model'); const linkById = require('../lib/linkById'); -const config = require("../config/config"); +const config = require('../config/config'); const errors = { - notFound: 'Domain not found', + notFound: 'Domain not found', }; exports.errors = errors; // Retrieve the attack object from the database using its STIX ID async function getAttackObject(stixId) { - const attackObject = await AttackObject - .findOne({ 'stix.id': stixId }) - .sort('-stix.modified') - .lean() - .exec(); + const attackObject = await AttackObject.findOne({ 'stix.id': stixId }) + .sort('-stix.modified') + .lean() + .exec(); - return attackObject; + return attackObject; } -const attackIdObjectTypes = ['intrusion-set', 'campaign', 'malware', 'tool', 'attack-pattern', 'course-of-action', 'x-mitre-data_source']; +const attackIdObjectTypes = [ + 'intrusion-set', + 'campaign', + 'malware', + 'tool', + 'attack-pattern', + 'course-of-action', + 'x-mitre-data_source', +]; function requiresAttackId(attackObject) { - return attackIdObjectTypes.includes(attackObject?.stix.type); + return attackIdObjectTypes.includes(attackObject?.stix.type); } function hasAttackId(attackObject) { - if (attackObject) { - const externalReferences = attackObject?.stix?.external_references; - if (Array.isArray(externalReferences) && externalReferences.length > 0) { - const mitreAttackReference = externalReferences.find(ref => config.attackSourceNames.includes(ref.source_name)); - if (mitreAttackReference?.external_id) { - return true; - } - } - } - - return false; + if (attackObject) { + const externalReferences = attackObject?.stix?.external_references; + if (Array.isArray(externalReferences) && externalReferences.length > 0) { + const mitreAttackReference = externalReferences.find((ref) => + config.attackSourceNames.includes(ref.source_name), + ); + if (mitreAttackReference?.external_id) { + return true; + } + } + } + + return false; } function removeEmptyArrays(stixObject, propertyNames) { - for (const propertyName of propertyNames) { - if (Array.isArray(stixObject[propertyName]) && stixObject[propertyName].length === 0) { - delete stixObject[propertyName]; - } + for (const propertyName of propertyNames) { + if (Array.isArray(stixObject[propertyName]) && stixObject[propertyName].length === 0) { + delete stixObject[propertyName]; } + } } const stixOptionalArrayProperties = [ - 'x_mitre_aliases', - 'x_mitre_contributors', - 'x_mitre_data_sources', - 'x_mitre_defense_bypassed', - 'x_mitre_domains', - 'x_mitre_effective_permissions', - 'x_mitre_impact_type', - 'x_mitre_related_assets', - 'x_mitre_sectors', - 'x_mitre_system_requirements', - 'x_mitre_permissions_required', - 'x_mitre_platforms', - 'x_mitre_remote_support', - 'x_mitre_tactic_type', - 'external_references', - 'kill_chain_phases', - 'aliases', - 'labels', - 'object_marking_refs', - 'roles', - 'sectors' + 'x_mitre_aliases', + 'x_mitre_contributors', + 'x_mitre_data_sources', + 'x_mitre_defense_bypassed', + 'x_mitre_domains', + 'x_mitre_effective_permissions', + 'x_mitre_impact_type', + 'x_mitre_related_assets', + 'x_mitre_sectors', + 'x_mitre_system_requirements', + 'x_mitre_permissions_required', + 'x_mitre_platforms', + 'x_mitre_remote_support', + 'x_mitre_tactic_type', + 'external_references', + 'kill_chain_phases', + 'aliases', + 'labels', + 'object_marking_refs', + 'roles', + 'sectors', ]; function conformToStixVersion(stixObject, stixVersion) { - if (stixVersion === '2.0') { - // eslint-disable-next-line no-prototype-builtins - if (stixObject.hasOwnProperty('spec_version')) { - stixObject.spec_version = undefined; - } - - if (stixObject.type === 'malware') { - // STIX 2.0 malware may not have the property is_family - // eslint-disable-next-line no-prototype-builtins - if (stixObject.hasOwnProperty('is_family')) { - stixObject.is_family = undefined; - } - - // STIX 2.0 malware must have the property labels - stixObject.labels = [ 'malware' ]; - } - - if (stixObject.type === 'tool') { - // STIX 2.0 tools may not have the property is_family - // eslint-disable-next-line no-prototype-builtins - if (stixObject.hasOwnProperty('is_family')) { - stixObject.is_family = undefined; - } - - // STIX 2.0 tools must have the property labels - stixObject.labels = [ 'tool' ]; - } - } - else if (stixVersion === '2.1') { - stixObject.spec_version = '2.1'; - - // STIX 2.1 malware must have the property is_family - if (stixObject.type === 'malware') { - stixObject.is_family = stixObject.is_family ?? true; - } - } - - removeEmptyArrays(stixObject, stixOptionalArrayProperties); -} - -exports.exportBundle = async function(options) { - // The attackObjectMap maps attack IDs to attack objects and is used to make the LinkById conversion - // more efficient. - const attackObjectMap = new Map(); - function addAttackObjectToMap(attackObject) { - if (attackObject?.workspace.attack_id) { - attackObjectMap.set(attackObject.workspace.attack_id, attackObject); - } - } - - // Create the bundle to hold the exported objects - const bundle = { - type: 'bundle', - id: `bundle--${uuid.v4()}`, - objects: [] - }; - - // STIX 2.0: The bundle must have the spec_version property - // STIX 2.1: The bundle may not have the spec_version property - if (options.stixVersion === '2.0') { - bundle.spec_version = '2.0'; - } - - // Get the primary objects (objects that match the domain) - - // Build the query - const primaryObjectsQuery = { 'stix.x_mitre_domains': options.domain }; - const matrixQuery = { }; - if (!options.includeRevoked) { - primaryObjectsQuery['stix.revoked'] = { $in: [null, false] }; - matrixQuery['stix.revoked'] = { $in: [null, false] }; - } - if (!options.includeDeprecated) { - primaryObjectsQuery['stix.x_mitre_deprecated'] = { $in: [null, false] }; - matrixQuery['stix.x_mitre_deprecated'] = { $in: [null, false] }; - } - if (typeof options.state !== 'undefined') { - if (Array.isArray(options.state)) { - primaryObjectsQuery['workspace.workflow.state'] = { $in: options.state }; - matrixQuery['workspace.workflow.state'] = { $in: options.state }; - } - else { - primaryObjectsQuery['workspace.workflow.state'] = options.state; - matrixQuery['workspace.workflow.state'] = options.state; - } - } - - // Build the aggregation - // - Group the documents by stix.id, sorted by stix.modified - // - Use the first document in each group (according to the value of stix.modified) - // - Then apply query, skip and limit options - const aggregation = [ - { $sort: { 'stix.id': 1, 'stix.modified': -1 } }, - { $group: { _id: '$stix.id', document: { $first: '$$ROOT' }}}, - { $replaceRoot: { newRoot: '$document' }}, - { $sort: { 'stix.id': 1 }}, - { $match: primaryObjectsQuery } - ]; - - // Retrieve the primary objects - const domainMitigations = await Mitigation.aggregate(aggregation); - const domainSoftware = await Software.aggregate(aggregation); - const domainTactics = await Tactic.aggregate(aggregation); - const domainTechniques = await Technique.aggregate(aggregation); - - // Retrieve the matrices - const matrixAggregation = aggregation.filter(val => !val.$match); - matrixAggregation.push({ $match: matrixQuery }); - const allMatrices = await Matrix.aggregate(matrixAggregation); - const domainMatrices = allMatrices.filter(matrix => matrix?.stix?.external_references.length && matrix.stix.external_references[0].external_id === options.domain); - - let primaryObjects = [...domainMatrices, ...domainMitigations, ...domainSoftware, ...domainTactics, ...domainTechniques]; - - // No primary objects means that the domain doesn't exist - // Return an empty bundle - if (primaryObjects.length === 0) { - return bundle; - } - - // Remove any primary objects that don't have an ATT&CK ID - if (!options.includeMissingAttackId) { - primaryObjects = primaryObjects.filter(o => hasAttackId(o)); - } - - // Put the primary objects in the bundle - // Also create a map of the objects added to the bundle (use the id as the key, since relationships only reference the id) - const objectsMap = new Map(); - for (const primaryObject of primaryObjects) { - bundle.objects.push(primaryObject.stix); - objectsMap.set(primaryObject.stix.id, true); - addAttackObjectToMap(primaryObject); - } - - // Get the relationships that point at primary objects (removing duplicates) - - // Get all of the relationships - // Use the aggregation to only get the last version of each relationship and - // filter out revoked and deprecated relationships - const relationshipQuery = { }; - if (!options.includeRevoked) { - relationshipQuery['stix.revoked'] = { $in: [null, false] }; - } - if (!options.includeDeprecated) { - relationshipQuery['stix.x_mitre_deprecated'] = { $in: [null, false] }; - } - if (typeof options.state !== 'undefined') { - if (Array.isArray(options.state)) { - relationshipQuery['workspace.workflow.state'] = { $in: options.state }; - } - else { - relationshipQuery['workspace.workflow.state'] = options.state; - } - } - const relationshipAggregation = [ - { $sort: { 'stix.id': 1, 'stix.modified': -1 } }, - { $group: { _id: '$stix.id', document: { $first: '$$ROOT' }}}, - { $replaceRoot: { newRoot: '$document' }}, - { $match: relationshipQuery } - ]; - const allRelationships = await Relationship.aggregate(relationshipAggregation); - - // Iterate over the relationships, keeping any that have a source_ref or target_ref that points at a primary object - const primaryObjectRelationships = []; - for (const relationship of allRelationships) { - if (objectsMap.has(relationship.stix.source_ref) || objectsMap.has(relationship.stix.target_ref)) { - primaryObjectRelationships.push(relationship); - } - } - - // function to test if an object belongs to the domain - // Only applies to certain object types - const domainCheckTypes = ['attack-pattern', 'course-of-action', 'malware', 'tool', 'x-mitre-tactic']; - function isCorrectDomain(attackObject) { - return !domainCheckTypes.includes(attackObject?.stix?.type) || (attackObject?.stix?.x_mitre_domains && attackObject.stix.x_mitre_domains.includes(options.domain)); - } - - // function to test if a secondary object is valid and should be included in the bundle - function secondaryObjectIsValid(secondaryObject) { - return ( - secondaryObject && - (options.includeMissingAttackId || !requiresAttackId(secondaryObject) || hasAttackId(secondaryObject)) && - (options.includeDeprecated || !secondaryObject.stix.x_mitre_deprecated) && - (options.includeRevoked || !secondaryObject.stix.revoked) && - (options.state === undefined || secondaryObject.workspace.workflow.state === options.state) && - isCorrectDomain(secondaryObject) - ); - } - - function relationshipIsActive(relationship) { - // Include the source and target of inactive relationships, but don't include the relationship itself - return !relationship.stix.x_mitre_deprecated && !relationship.stix.revoked; - } - - // Get the secondary objects (additional objects pointed to by a relationship) - const secondaryObjects = []; - const dataComponents = new Map(); - for (const relationship of primaryObjectRelationships) { - if (objectsMap.has(relationship.stix.source_ref) && objectsMap.has(relationship.stix.target_ref)) { - // source_ref (primary) => target_ref (primary) - if (relationshipIsActive(relationship)) { - bundle.objects.push(relationship.stix); - } - } - else if (!objectsMap.has(relationship.stix.source_ref) && objectsMap.has(relationship.stix.target_ref)) { - // source_ref (secondary) => target_ref (primary) - const secondaryObject = await getAttackObject(relationship.stix.source_ref); - if (secondaryObjectIsValid(secondaryObject)) { - secondaryObjects.push(secondaryObject); - objectsMap.set(secondaryObject.stix.id, true); - if (relationshipIsActive(relationship)) { - bundle.objects.push(relationship.stix); - } - } - - // Save data components for later - if (relationship.stix.relationship_type === 'detects') { - dataComponents.set(secondaryObject.stix.id, secondaryObject.stix); - } - } - else if (objectsMap.has(relationship.stix.source_ref) && !objectsMap.has(relationship.stix.target_ref)) { - // source_ref (primary) => target_ref (secondary) - const secondaryObject = await getAttackObject(relationship.stix.target_ref); - if (secondaryObjectIsValid(secondaryObject)) { - secondaryObjects.push(secondaryObject); - objectsMap.set(secondaryObject.stix.id, true); - if (relationshipIsActive(relationship)) { - bundle.objects.push(relationship.stix); - } - } - } - } - - // Secondary object domains are the union of the related primary object domains - // Do not include primary objects when the relationship is deprecated - async function domainsForSecondaryObject(attackObject) { - const objectRelationships = allRelationships.filter(r => r.stix.source_ref === attackObject.stix.id); - const domainMap = new Map(); - for (const relationship of objectRelationships) { - const targetObject = await getAttackObject(relationship.stix.target_ref); - if (targetObject?.stix.x_mitre_domains) { - for (const domain of targetObject.stix.x_mitre_domains) { - domainMap.set(domain, true); - } - } - } - - return [...domainMap.keys()]; + if (stixVersion === '2.0') { + // eslint-disable-next-line no-prototype-builtins + if (stixObject.hasOwnProperty('spec_version')) { + stixObject.spec_version = undefined; } - // Put the secondary objects in the bundle - for (const secondaryObject of secondaryObjects) { - // Groups and campaigns need to have the domain added to x_mitre_domains - if (secondaryObject.stix.type === 'intrusion-set' || secondaryObject.stix.type === 'campaign') { - secondaryObject.stix.x_mitre_domains = await domainsForSecondaryObject(secondaryObject); - } - bundle.objects.push(secondaryObject.stix); - addAttackObjectToMap(secondaryObject); - } + if (stixObject.type === 'malware') { + // STIX 2.0 malware may not have the property is_family + // eslint-disable-next-line no-prototype-builtins + if (stixObject.hasOwnProperty('is_family')) { + stixObject.is_family = undefined; + } - // Add groups to the bundle that are referenced by a campaign but are not referenced by a primary object - for (const relationship of allRelationships) { - if (relationship.stix.relationship_type === 'attributed-to') { - if (objectsMap.has(relationship.stix.source_ref) && !objectsMap.has(relationship.stix.target_ref)) { - // Add the group to the bundle - const groupObject = await getAttackObject(relationship.stix.target_ref); - if (groupObject.stix.type === 'intrusion-set' && secondaryObjectIsValid(groupObject)) { - groupObject.stix.x_mitre_domains = [ options.domain ]; - bundle.objects.push(groupObject.stix); - objectsMap.set(groupObject.stix.id, true); - // relationships will be added to the bundle later - } - } - } + // STIX 2.0 malware must have the property labels + stixObject.labels = ['malware']; } - // Data components have already been added to the bundle because they're referenced in a relationship - // Get the data sources referenced by data components, using a map to eliminate duplicates - const dataSourceIds = new Map(); - for (const bundleObject of bundle.objects) { - if (bundleObject.type === 'x-mitre-data-component') { - dataSourceIds.set(bundleObject.x_mitre_data_source_ref, true); - } - } + if (stixObject.type === 'tool') { + // STIX 2.0 tools may not have the property is_family + // eslint-disable-next-line no-prototype-builtins + if (stixObject.hasOwnProperty('is_family')) { + stixObject.is_family = undefined; + } - // Retrieve the data sources, add to bundle, and save for deriving x_mitre_data_sources - const dataSources = new Map(); - for (const dataSourceId of dataSourceIds.keys()) { - const dataSource = await getAttackObject(dataSourceId); - if (secondaryObjectIsValid(dataSource)) { - bundle.objects.push(dataSource.stix); - dataSources.set(dataSourceId, dataSource.stix); - addAttackObjectToMap(dataSource); - } + // STIX 2.0 tools must have the property labels + stixObject.labels = ['tool']; } + } else if (stixVersion === '2.1') { + stixObject.spec_version = '2.1'; - // Add secondary objects that were revoked-by other secondary objects - // It's possible that a revoked object is not referenced in a relationship to a primary object - // Include it if the object that revoked it is already included - for (const relationship of allRelationships) { - if (relationship.stix.relationship_type === 'revoked-by') { - if (!objectsMap.has(relationship.stix.source_ref) && objectsMap.has(relationship.stix.target_ref)) { - const revokedObject = await getAttackObject(relationship.stix.source_ref); - if (secondaryObjectIsValid(revokedObject)) { - if (revokedObject.stix.type === 'intrusion-set' || revokedObject.stix.type === 'campaign') { - revokedObject.stix.x_mitre_domains = [ options.domain ]; - } - bundle.objects.push(revokedObject.stix); - addAttackObjectToMap(revokedObject); - objectsMap.set(revokedObject.stix.id, true); - } - } - } + // STIX 2.1 malware must have the property is_family + if (stixObject.type === 'malware') { + stixObject.is_family = stixObject.is_family ?? true; } + } - // Create a map of techniques detected by data components - // key = technique id, value = array of data component refs - const techniqueDetectedBy = new Map(); - for (const relationship of primaryObjectRelationships) { - if (relationship.stix.relationship_type === 'detects' && relationshipIsActive(relationship)) { - // technique (target_ref) detected by array of data-component (source_ref) - const techniqueDataComponents = techniqueDetectedBy.get(relationship.stix.target_ref); - if (techniqueDataComponents) { - // Add to the existing array - techniqueDataComponents.push(relationship.stix.source_ref); - } - else { - // Create a new array and add to map - techniqueDetectedBy.set(relationship.stix.target_ref, [relationship.stix.source_ref]); - } - } - } + removeEmptyArrays(stixObject, stixOptionalArrayProperties); +} - // Supplement techniques with x_mitre_data_sources for backwards compatibility - // Also make sure that all techniques have x_mitre_is_subtechnique set - for (const bundleObject of bundle.objects) { - if (bundleObject.type === 'attack-pattern') { - // Make sure that x_mitre_is_subtechnique is set - bundleObject.x_mitre_is_subtechnique = bundleObject.x_mitre_is_subtechnique ?? false; - - const enterpriseDomain = bundleObject.x_mitre_domains.includes('enterprise-attack'); - const icsDomain = bundleObject.x_mitre_domains.includes('ics-attack'); - if (enterpriseDomain || icsDomain) { - // Remove any existing data source string entries - bundleObject.x_mitre_data_sources = []; - - // Add data source string entries based on the data sources associated with the technique - // data component detects technique AND data component refers to data source - const dataComponentIds = techniqueDetectedBy.get(bundleObject.id); - if (dataComponentIds) { - for (const dataComponentId of dataComponentIds) { - const dataComponent = dataComponents.get(dataComponentId); - if (dataComponent) { - const dataSourceForTechnique = dataSources.get(dataComponent.x_mitre_data_source_ref); - if (dataSourceForTechnique) { - const derivedDataSource = `${ dataSourceForTechnique.name }: ${ dataComponent.name }`; - bundleObject.x_mitre_data_sources.push(derivedDataSource); - } - else { - console.log(`Referenced data source not found: ${ dataComponent.x_mitre_data_source_ref }`); - } - } - else { - console.log(`Referenced data component not found: ${ dataComponentId }`); - } - } - } - } - else { - // Remove any existing data sources - bundleObject.x_mitre_data_sources = []; +exports.exportBundle = async function (options) { + // The attackObjectMap maps attack IDs to attack objects and is used to make the LinkById conversion + // more efficient. + const attackObjectMap = new Map(); + function addAttackObjectToMap(attackObject) { + if (attackObject?.workspace.attack_id) { + attackObjectMap.set(attackObject.workspace.attack_id, attackObject); + } + } + + // Create the bundle to hold the exported objects + const bundle = { + type: 'bundle', + id: `bundle--${uuid.v4()}`, + objects: [], + }; + + // STIX 2.0: The bundle must have the spec_version property + // STIX 2.1: The bundle may not have the spec_version property + if (options.stixVersion === '2.0') { + bundle.spec_version = '2.0'; + } + + // Get the primary objects (objects that match the domain) + + // Build the query + const primaryObjectsQuery = { 'stix.x_mitre_domains': options.domain }; + const matrixQuery = {}; + if (!options.includeRevoked) { + primaryObjectsQuery['stix.revoked'] = { $in: [null, false] }; + matrixQuery['stix.revoked'] = { $in: [null, false] }; + } + if (!options.includeDeprecated) { + primaryObjectsQuery['stix.x_mitre_deprecated'] = { $in: [null, false] }; + matrixQuery['stix.x_mitre_deprecated'] = { $in: [null, false] }; + } + if (typeof options.state !== 'undefined') { + if (Array.isArray(options.state)) { + primaryObjectsQuery['workspace.workflow.state'] = { $in: options.state }; + matrixQuery['workspace.workflow.state'] = { $in: options.state }; + } else { + primaryObjectsQuery['workspace.workflow.state'] = options.state; + matrixQuery['workspace.workflow.state'] = options.state; + } + } + + // Build the aggregation + // - Group the documents by stix.id, sorted by stix.modified + // - Use the first document in each group (according to the value of stix.modified) + // - Then apply query, skip and limit options + const aggregation = [ + { $sort: { 'stix.id': 1, 'stix.modified': -1 } }, + { $group: { _id: '$stix.id', document: { $first: '$$ROOT' } } }, + { $replaceRoot: { newRoot: '$document' } }, + { $sort: { 'stix.id': 1 } }, + { $match: primaryObjectsQuery }, + ]; + + // Retrieve the primary objects + const domainMitigations = await Mitigation.aggregate(aggregation); + const domainSoftware = await Software.aggregate(aggregation); + const domainTactics = await Tactic.aggregate(aggregation); + const domainTechniques = await Technique.aggregate(aggregation); + + // Retrieve the matrices + const matrixAggregation = aggregation.filter((val) => !val.$match); + matrixAggregation.push({ $match: matrixQuery }); + const allMatrices = await Matrix.aggregate(matrixAggregation); + const domainMatrices = allMatrices.filter( + (matrix) => + matrix?.stix?.external_references.length && + matrix.stix.external_references[0].external_id === options.domain, + ); + + let primaryObjects = [ + ...domainMatrices, + ...domainMitigations, + ...domainSoftware, + ...domainTactics, + ...domainTechniques, + ]; + + // No primary objects means that the domain doesn't exist + // Return an empty bundle + if (primaryObjects.length === 0) { + return bundle; + } + + // Remove any primary objects that don't have an ATT&CK ID + if (!options.includeMissingAttackId) { + primaryObjects = primaryObjects.filter((o) => hasAttackId(o)); + } + + // Put the primary objects in the bundle + // Also create a map of the objects added to the bundle (use the id as the key, since relationships only reference the id) + const objectsMap = new Map(); + for (const primaryObject of primaryObjects) { + bundle.objects.push(primaryObject.stix); + objectsMap.set(primaryObject.stix.id, true); + addAttackObjectToMap(primaryObject); + } + + // Get the relationships that point at primary objects (removing duplicates) + + // Get all of the relationships + // Use the aggregation to only get the last version of each relationship and + // filter out revoked and deprecated relationships + const relationshipQuery = {}; + if (!options.includeRevoked) { + relationshipQuery['stix.revoked'] = { $in: [null, false] }; + } + if (!options.includeDeprecated) { + relationshipQuery['stix.x_mitre_deprecated'] = { $in: [null, false] }; + } + if (typeof options.state !== 'undefined') { + if (Array.isArray(options.state)) { + relationshipQuery['workspace.workflow.state'] = { $in: options.state }; + } else { + relationshipQuery['workspace.workflow.state'] = options.state; + } + } + const relationshipAggregation = [ + { $sort: { 'stix.id': 1, 'stix.modified': -1 } }, + { $group: { _id: '$stix.id', document: { $first: '$$ROOT' } } }, + { $replaceRoot: { newRoot: '$document' } }, + { $match: relationshipQuery }, + ]; + const allRelationships = await Relationship.aggregate(relationshipAggregation); + + // Iterate over the relationships, keeping any that have a source_ref or target_ref that points at a primary object + const primaryObjectRelationships = []; + for (const relationship of allRelationships) { + if ( + objectsMap.has(relationship.stix.source_ref) || + objectsMap.has(relationship.stix.target_ref) + ) { + primaryObjectRelationships.push(relationship); + } + } + + // function to test if an object belongs to the domain + // Only applies to certain object types + const domainCheckTypes = [ + 'attack-pattern', + 'course-of-action', + 'malware', + 'tool', + 'x-mitre-tactic', + ]; + function isCorrectDomain(attackObject) { + return ( + !domainCheckTypes.includes(attackObject?.stix?.type) || + (attackObject?.stix?.x_mitre_domains && + attackObject.stix.x_mitre_domains.includes(options.domain)) + ); + } + + // function to test if a secondary object is valid and should be included in the bundle + function secondaryObjectIsValid(secondaryObject) { + return ( + secondaryObject && + (options.includeMissingAttackId || + !requiresAttackId(secondaryObject) || + hasAttackId(secondaryObject)) && + (options.includeDeprecated || !secondaryObject.stix.x_mitre_deprecated) && + (options.includeRevoked || !secondaryObject.stix.revoked) && + (options.state === undefined || secondaryObject.workspace.workflow.state === options.state) && + isCorrectDomain(secondaryObject) + ); + } + + function relationshipIsActive(relationship) { + // Include the source and target of inactive relationships, but don't include the relationship itself + return !relationship.stix.x_mitre_deprecated && !relationship.stix.revoked; + } + + // Get the secondary objects (additional objects pointed to by a relationship) + const secondaryObjects = []; + const dataComponents = new Map(); + for (const relationship of primaryObjectRelationships) { + if ( + objectsMap.has(relationship.stix.source_ref) && + objectsMap.has(relationship.stix.target_ref) + ) { + // source_ref (primary) => target_ref (primary) + if (relationshipIsActive(relationship)) { + bundle.objects.push(relationship.stix); + } + } else if ( + !objectsMap.has(relationship.stix.source_ref) && + objectsMap.has(relationship.stix.target_ref) + ) { + // source_ref (secondary) => target_ref (primary) + const secondaryObject = await getAttackObject(relationship.stix.source_ref); + if (secondaryObjectIsValid(secondaryObject)) { + secondaryObjects.push(secondaryObject); + objectsMap.set(secondaryObject.stix.id, true); + if (relationshipIsActive(relationship)) { + bundle.objects.push(relationship.stix); + } + } + + // Save data components for later + if (relationship.stix.relationship_type === 'detects') { + dataComponents.set(secondaryObject.stix.id, secondaryObject.stix); + } + } else if ( + objectsMap.has(relationship.stix.source_ref) && + !objectsMap.has(relationship.stix.target_ref) + ) { + // source_ref (primary) => target_ref (secondary) + const secondaryObject = await getAttackObject(relationship.stix.target_ref); + if (secondaryObjectIsValid(secondaryObject)) { + secondaryObjects.push(secondaryObject); + objectsMap.set(secondaryObject.stix.id, true); + if (relationshipIsActive(relationship)) { + bundle.objects.push(relationship.stix); + } + } + } + } + + // Secondary object domains are the union of the related primary object domains + // Do not include primary objects when the relationship is deprecated + async function domainsForSecondaryObject(attackObject) { + const objectRelationships = allRelationships.filter( + (r) => r.stix.source_ref === attackObject.stix.id, + ); + const domainMap = new Map(); + for (const relationship of objectRelationships) { + const targetObject = await getAttackObject(relationship.stix.target_ref); + if (targetObject?.stix.x_mitre_domains) { + for (const domain of targetObject.stix.x_mitre_domains) { + domainMap.set(domain, true); + } + } + } + + return [...domainMap.keys()]; + } + + // Put the secondary objects in the bundle + for (const secondaryObject of secondaryObjects) { + // Groups and campaigns need to have the domain added to x_mitre_domains + if (secondaryObject.stix.type === 'intrusion-set' || secondaryObject.stix.type === 'campaign') { + secondaryObject.stix.x_mitre_domains = await domainsForSecondaryObject(secondaryObject); + } + bundle.objects.push(secondaryObject.stix); + addAttackObjectToMap(secondaryObject); + } + + // Add groups to the bundle that are referenced by a campaign but are not referenced by a primary object + for (const relationship of allRelationships) { + if (relationship.stix.relationship_type === 'attributed-to') { + if ( + objectsMap.has(relationship.stix.source_ref) && + !objectsMap.has(relationship.stix.target_ref) + ) { + // Add the group to the bundle + const groupObject = await getAttackObject(relationship.stix.target_ref); + if (groupObject.stix.type === 'intrusion-set' && secondaryObjectIsValid(groupObject)) { + groupObject.stix.x_mitre_domains = [options.domain]; + bundle.objects.push(groupObject.stix); + objectsMap.set(groupObject.stix.id, true); + // relationships will be added to the bundle later + } + } + } + } + + // Data components have already been added to the bundle because they're referenced in a relationship + // Get the data sources referenced by data components, using a map to eliminate duplicates + const dataSourceIds = new Map(); + for (const bundleObject of bundle.objects) { + if (bundleObject.type === 'x-mitre-data-component') { + dataSourceIds.set(bundleObject.x_mitre_data_source_ref, true); + } + } + + // Retrieve the data sources, add to bundle, and save for deriving x_mitre_data_sources + const dataSources = new Map(); + for (const dataSourceId of dataSourceIds.keys()) { + const dataSource = await getAttackObject(dataSourceId); + if (secondaryObjectIsValid(dataSource)) { + bundle.objects.push(dataSource.stix); + dataSources.set(dataSourceId, dataSource.stix); + addAttackObjectToMap(dataSource); + } + } + + // Add secondary objects that were revoked-by other secondary objects + // It's possible that a revoked object is not referenced in a relationship to a primary object + // Include it if the object that revoked it is already included + for (const relationship of allRelationships) { + if (relationship.stix.relationship_type === 'revoked-by') { + if ( + !objectsMap.has(relationship.stix.source_ref) && + objectsMap.has(relationship.stix.target_ref) + ) { + const revokedObject = await getAttackObject(relationship.stix.source_ref); + if (secondaryObjectIsValid(revokedObject)) { + if ( + revokedObject.stix.type === 'intrusion-set' || + revokedObject.stix.type === 'campaign' + ) { + revokedObject.stix.x_mitre_domains = [options.domain]; + } + bundle.objects.push(revokedObject.stix); + addAttackObjectToMap(revokedObject); + objectsMap.set(revokedObject.stix.id, true); + } + } + } + } + + // Create a map of techniques detected by data components + // key = technique id, value = array of data component refs + const techniqueDetectedBy = new Map(); + for (const relationship of primaryObjectRelationships) { + if (relationship.stix.relationship_type === 'detects' && relationshipIsActive(relationship)) { + // technique (target_ref) detected by array of data-component (source_ref) + const techniqueDataComponents = techniqueDetectedBy.get(relationship.stix.target_ref); + if (techniqueDataComponents) { + // Add to the existing array + techniqueDataComponents.push(relationship.stix.source_ref); + } else { + // Create a new array and add to map + techniqueDetectedBy.set(relationship.stix.target_ref, [relationship.stix.source_ref]); + } + } + } + + // Supplement techniques with x_mitre_data_sources for backwards compatibility + // Also make sure that all techniques have x_mitre_is_subtechnique set + for (const bundleObject of bundle.objects) { + if (bundleObject.type === 'attack-pattern') { + // Make sure that x_mitre_is_subtechnique is set + bundleObject.x_mitre_is_subtechnique = bundleObject.x_mitre_is_subtechnique ?? false; + + const enterpriseDomain = bundleObject.x_mitre_domains.includes('enterprise-attack'); + const icsDomain = bundleObject.x_mitre_domains.includes('ics-attack'); + if (enterpriseDomain || icsDomain) { + // Remove any existing data source string entries + bundleObject.x_mitre_data_sources = []; + + // Add data source string entries based on the data sources associated with the technique + // data component detects technique AND data component refers to data source + const dataComponentIds = techniqueDetectedBy.get(bundleObject.id); + if (dataComponentIds) { + for (const dataComponentId of dataComponentIds) { + const dataComponent = dataComponents.get(dataComponentId); + if (dataComponent) { + const dataSourceForTechnique = dataSources.get(dataComponent.x_mitre_data_source_ref); + if (dataSourceForTechnique) { + const derivedDataSource = `${dataSourceForTechnique.name}: ${dataComponent.name}`; + bundleObject.x_mitre_data_sources.push(derivedDataSource); + } else { + console.log( + `Referenced data source not found: ${dataComponent.x_mitre_data_source_ref}`, + ); + } + } else { + console.log(`Referenced data component not found: ${dataComponentId}`); } - } - } - - // Create a map of relationship ids - const relationshipsMap = new Map(); - for (const relationship of primaryObjectRelationships) { + } + } + } else { + // Remove any existing data sources + bundleObject.x_mitre_data_sources = []; + } + } + } + + // Create a map of relationship ids + const relationshipsMap = new Map(); + for (const relationship of primaryObjectRelationships) { + relationshipsMap.set(relationship.stix.id, true); + } + + // Add secondary object-to-secondary object revoked-by relationships + // (any revoked-by relationship for a primary object should already be included) + for (const relationship of allRelationships) { + if ( + relationship.stix.relationship_type === 'revoked-by' && + !relationshipsMap.has(relationship.stix.id) && + objectsMap.has(relationship.stix.source_ref) && + objectsMap.has(relationship.stix.target_ref) + ) { + if (relationshipIsActive(relationship)) { + bundle.objects.push(relationship.stix); relationshipsMap.set(relationship.stix.id, true); + } + } + } + + // Add campaign-to-group attributed-to relationships + // (campaigns and groups are secondary objects, so an attributed-to relationship will not be added yet) + for (const relationship of allRelationships) { + if ( + relationship.stix.relationship_type === 'attributed-to' && + !relationshipsMap.has(relationship.stix.id) && + objectsMap.has(relationship.stix.source_ref) && + objectsMap.has(relationship.stix.target_ref) + ) { + if (relationshipIsActive(relationship)) { + bundle.objects.push(relationship.stix); + relationshipsMap.set(relationship.stix.id, true); + } } + } - // Add secondary object-to-secondary object revoked-by relationships - // (any revoked-by relationship for a primary object should already be included) - for (const relationship of allRelationships) { - if (relationship.stix.relationship_type === 'revoked-by' && !relationshipsMap.has(relationship.stix.id) && - objectsMap.has(relationship.stix.source_ref) && objectsMap.has(relationship.stix.target_ref)) - { - if (relationshipIsActive(relationship)) { - bundle.objects.push(relationship.stix); - relationshipsMap.set(relationship.stix.id, true); - } - } - } - - // Add campaign-to-group attributed-to relationships - // (campaigns and groups are secondary objects, so an attributed-to relationship will not be added yet) - for (const relationship of allRelationships) { - if (relationship.stix.relationship_type === 'attributed-to' && !relationshipsMap.has(relationship.stix.id) && - objectsMap.has(relationship.stix.source_ref) && objectsMap.has(relationship.stix.target_ref)) - { - if (relationshipIsActive(relationship)) { - bundle.objects.push(relationship.stix); - relationshipsMap.set(relationship.stix.id, true); - } - } - } - - if (options.includeNotes) { - // Get any note that references an object in the bundle - // Start by getting all notes - const allNotes = await Note.aggregate(relationshipAggregation); - - // Iterate over the notes, keeping any that have an object_ref that points at an object in the bundle - const notes = []; - for (const note of allNotes) { - if (Array.isArray(note?.stix?.object_refs)) { - let includeNote = false; - for (const objectRef of note.stix.object_refs) { - if (objectsMap.has(objectRef) || relationshipsMap.has(objectRef)) { - includeNote = true; - break; - } - } - if (includeNote) { - notes.push(note); - } - } - } - - // Put the notes in the bundle - for (const note of notes) { - bundle.objects.push(note.stix); - } - } - - // Create the function to be used by the LinkById conversion process - // Note that using this map instead of database retrieval results in a - // dramatic performance improvement. - const getAttackObjectFromMap = async function (attackId) { - let attackObject = attackObjectMap.get(attackId); - if (!attackObject) { - attackObject = await linkById.getAttackObjectFromDatabase(attackId); - } - return attackObject; - } - - // Convert LinkById tags into markdown citations - for (const attackObject of bundle.objects) { - await linkById.convertLinkByIdTags(attackObject, getAttackObjectFromMap); - } - - // Make a list of identities referenced - const identitiesMap = new Map(); - for (const bundleObject of bundle.objects) { - if (bundleObject.created_by_ref) { - identitiesMap.set(bundleObject.created_by_ref, true); - } - } - - // Make a list of marking definitions referenced - const markingDefinitionsMap = new Map(); - for (const bundleObject of bundle.objects) { - if (bundleObject.object_marking_refs) { - for (const markingRef of bundleObject.object_marking_refs) { - markingDefinitionsMap.set(markingRef, true); - } - } - } + if (options.includeNotes) { + // Get any note that references an object in the bundle + // Start by getting all notes + const allNotes = await Note.aggregate(relationshipAggregation); - for (const stixId of identitiesMap.keys()) { - const identity = await getAttackObject(stixId); - if (secondaryObjectIsValid(identity)) { - bundle.objects.push(identity.stix); + // Iterate over the notes, keeping any that have an object_ref that points at an object in the bundle + const notes = []; + for (const note of allNotes) { + if (Array.isArray(note?.stix?.object_refs)) { + let includeNote = false; + for (const objectRef of note.stix.object_refs) { + if (objectsMap.has(objectRef) || relationshipsMap.has(objectRef)) { + includeNote = true; + break; + } } - else { - console.log(`Referenced identity not found: ${ stixId }`); + if (includeNote) { + notes.push(note); } + } } - for (const stixId of markingDefinitionsMap.keys()) { - const markingDefinition = await getAttackObject(stixId); - if (secondaryObjectIsValid(markingDefinition)) { - bundle.objects.push(markingDefinition.stix); - } + // Put the notes in the bundle + for (const note of notes) { + bundle.objects.push(note.stix); } + } - // Modify the bundle objects to conform to the selected STIX version - for (const stixObject of bundle.objects) { - conformToStixVersion(stixObject, options.stixVersion); + // Create the function to be used by the LinkById conversion process + // Note that using this map instead of database retrieval results in a + // dramatic performance improvement. + const getAttackObjectFromMap = async function (attackId) { + let attackObject = attackObjectMap.get(attackId); + if (!attackObject) { + attackObject = await linkById.getAttackObjectFromDatabase(attackId); } - - // Verify that there are no relationships with orphaned references - for (const stixObject of bundle.objects) { - if (stixObject.type === 'relationship') { - if (!objectsMap.has(stixObject.source_ref)) { - console.warn(`source_ref not found ${stixObject.source_ref} ${stixObject.relationship_type} ${stixObject.target_ref}`); - } - if (!objectsMap.has(stixObject.target_ref)) { - console.warn(`target_ref not found ${stixObject.source_ref} ${stixObject.relationship_type} ${stixObject.target_ref}`); - } - } + return attackObject; + }; + + // Convert LinkById tags into markdown citations + for (const attackObject of bundle.objects) { + await linkById.convertLinkByIdTags(attackObject, getAttackObjectFromMap); + } + + // Make a list of identities referenced + const identitiesMap = new Map(); + for (const bundleObject of bundle.objects) { + if (bundleObject.created_by_ref) { + identitiesMap.set(bundleObject.created_by_ref, true); + } + } + + // Make a list of marking definitions referenced + const markingDefinitionsMap = new Map(); + for (const bundleObject of bundle.objects) { + if (bundleObject.object_marking_refs) { + for (const markingRef of bundleObject.object_marking_refs) { + markingDefinitionsMap.set(markingRef, true); + } + } + } + + for (const stixId of identitiesMap.keys()) { + const identity = await getAttackObject(stixId); + if (secondaryObjectIsValid(identity)) { + bundle.objects.push(identity.stix); + } else { + console.log(`Referenced identity not found: ${stixId}`); + } + } + + for (const stixId of markingDefinitionsMap.keys()) { + const markingDefinition = await getAttackObject(stixId); + if (secondaryObjectIsValid(markingDefinition)) { + bundle.objects.push(markingDefinition.stix); + } + } + + // Modify the bundle objects to conform to the selected STIX version + for (const stixObject of bundle.objects) { + conformToStixVersion(stixObject, options.stixVersion); + } + + // Verify that there are no relationships with orphaned references + for (const stixObject of bundle.objects) { + if (stixObject.type === 'relationship') { + if (!objectsMap.has(stixObject.source_ref)) { + console.warn( + `source_ref not found ${stixObject.source_ref} ${stixObject.relationship_type} ${stixObject.target_ref}`, + ); + } + if (!objectsMap.has(stixObject.target_ref)) { + console.warn( + `target_ref not found ${stixObject.source_ref} ${stixObject.relationship_type} ${stixObject.target_ref}`, + ); + } } + } - // TBD: A marking definition can contain a created_by_ref - // and an identity can contain a marking definition. - // An unlikely edge case is for one of those to reference - // an object that hasn't been loaded by any other object. + // TBD: A marking definition can contain a created_by_ref + // and an identity can contain a marking definition. + // An unlikely edge case is for one of those to reference + // an object that hasn't been loaded by any other object. - return bundle; -} + return bundle; +}; diff --git a/app/services/system-configuration-service.js b/app/services/system-configuration-service.js index 5bb2768c..1d6485f3 100644 --- a/app/services/system-configuration-service.js +++ b/app/services/system-configuration-service.js @@ -5,12 +5,13 @@ const config = require('../config/config'); const systemConfigurationRepository = require('../repository/system-configurations-repository'); const userAccountsService = require('./user-accounts-service'); const { - SystemConfigurationNotFound, - OrganizationIdentityNotSetError, - OrganizationIdentityNotFoundError, - DefaultMarkingDefinitionsNotFoundError, - AnonymousUserAccountNotSetError, - AnonymousUserAccountNotFoundError } = require('../exceptions'); + SystemConfigurationNotFound, + OrganizationIdentityNotSetError, + OrganizationIdentityNotFoundError, + DefaultMarkingDefinitionsNotFoundError, + AnonymousUserAccountNotSetError, + AnonymousUserAccountNotFoundError, +} = require('../exceptions'); let allowedValues; let markingDefinitionsService; @@ -21,236 +22,226 @@ let identitiesService; // Other parts of the system configuration are read from the config module (which is prepared at start-up // based on environment variables and an optional configuration file) -exports.retrieveSystemVersion = function() { - const systemVersionInfo = { - version: config.app.version, - attackSpecVersion: config.app.attackSpecVersion - }; +exports.retrieveSystemVersion = function () { + const systemVersionInfo = { + version: config.app.version, + attackSpecVersion: config.app.attackSpecVersion, + }; - return systemVersionInfo; -} + return systemVersionInfo; +}; async function retrieveAllowedValues() { - if (allowedValues) { - return allowedValues; - } - else { - const data = await fs.promises.readFile(config.configurationFiles.allowedValues); - allowedValues = JSON.parse(data); - return allowedValues; - } + if (allowedValues) { + return allowedValues; + } else { + const data = await fs.promises.readFile(config.configurationFiles.allowedValues); + allowedValues = JSON.parse(data); + return allowedValues; + } } exports.retrieveAllowedValues = retrieveAllowedValues; async function retrieveAllowedValuesForType(objectType) { - const values = await retrieveAllowedValues(); + const values = await retrieveAllowedValues(); - return values.find(element => element.objectType === objectType); + return values.find((element) => element.objectType === objectType); } exports.retrieveAllowedValuesForType = retrieveAllowedValuesForType; async function retrieveAllowedValuesForTypeAndProperty(type, propertyName) { - const values = await retrieveAllowedValuesForType(type); + const values = await retrieveAllowedValuesForType(type); - return values?.properties.find(element => element.propertyName === propertyName); + return values?.properties.find((element) => element.propertyName === propertyName); } exports.retrieveAllowedValuesForTypeAndProperty = retrieveAllowedValuesForTypeAndProperty; async function retrieveAllowedValuesForTypePropertyDomain(objectType, propertyName, domainName) { - const values = await retrieveAllowedValuesForTypeAndProperty(objectType, propertyName); + const values = await retrieveAllowedValuesForTypeAndProperty(objectType, propertyName); - return values?.domains.find(element => element.domainName === domainName); + return values?.domains.find((element) => element.domainName === domainName); } exports.retrieveAllowedValuesForTypePropertyDomain = retrieveAllowedValuesForTypePropertyDomain; -exports.retrieveOrganizationIdentityRef = async function() { - // There should be exactly one system configuration document - const systemConfig = await systemConfigurationRepository.retrieveOne(); - - if (systemConfig && systemConfig.organization_identity_ref) { - return systemConfig.organization_identity_ref; - } - else { - throw new OrganizationIdentityNotSetError; - } -} - -exports.retrieveOrganizationIdentity = async function() { - if (!identitiesService) { - identitiesService = require('./identities-service'); - } - // There should be exactly one system configuration document - const systemConfig = await systemConfigurationRepository.retrieveOne({ lean: true }); - - if (systemConfig && systemConfig.organization_identity_ref) { - const identities = await identitiesService.retrieveById(systemConfig.organization_identity_ref, { versions: 'latest' }); - if (identities.length === 1) { - return identities[0]; - } - else { - throw new OrganizationIdentityNotFoundError(systemConfig.organization_identity_ref); - } - } - else { - throw new OrganizationIdentityNotSetError; - } -} - -exports.setOrganizationIdentity = async function(stixId) { - // There should be exactly one system configuration document - const systemConfig = await systemConfigurationRepository.retrieveOne(); - - if (systemConfig) { - // The document exists already. Set the identity reference. - systemConfig.organization_identity_ref = stixId; - await systemConfigurationRepository.constructor.saveDocument(systemConfig); - } - else { - // The document doesn't exist yet. Create a new one. - const systemConfigData = { - organization_identity_ref: stixId - }; - const systemConfig = systemConfigurationRepository.createNewDocument(systemConfigData); - await systemConfigurationRepository.constructor.saveDocument(systemConfig); - } -} - -const retrieveDefaultMarkingDefinitions = async function(options) { - if (!markingDefinitionsService) { - markingDefinitionsService = require('./marking-definitions-service'); - } - options = options ?? {}; - - // There should be exactly one system configuration document - const systemConfig = await systemConfigurationRepository.retrieveOne({ lean: true }); - - if (systemConfig) { - if (systemConfig.default_marking_definitions) { - if (options.refOnly) { - return systemConfig.default_marking_definitions; - } - else { - const defaultMarkingDefinitions = []; - for (const stixId of systemConfig.default_marking_definitions) { - // eslint-disable-next-line no-await-in-loop - const markingDefinition = await markingDefinitionsService.retrieveById(stixId); - if (markingDefinition.length === 1) { - defaultMarkingDefinitions.push(markingDefinition[0]); - } - else { - throw new DefaultMarkingDefinitionsNotFoundError; - } - } - return defaultMarkingDefinitions; - } - } - else { - // default_marking_definitions not set - return []; +exports.retrieveOrganizationIdentityRef = async function () { + // There should be exactly one system configuration document + const systemConfig = await systemConfigurationRepository.retrieveOne(); + + if (systemConfig && systemConfig.organization_identity_ref) { + return systemConfig.organization_identity_ref; + } else { + throw new OrganizationIdentityNotSetError(); + } +}; + +exports.retrieveOrganizationIdentity = async function () { + if (!identitiesService) { + identitiesService = require('./identities-service'); + } + // There should be exactly one system configuration document + const systemConfig = await systemConfigurationRepository.retrieveOne({ lean: true }); + + if (systemConfig && systemConfig.organization_identity_ref) { + const identities = await identitiesService.retrieveById( + systemConfig.organization_identity_ref, + { versions: 'latest' }, + ); + if (identities.length === 1) { + return identities[0]; + } else { + throw new OrganizationIdentityNotFoundError(systemConfig.organization_identity_ref); + } + } else { + throw new OrganizationIdentityNotSetError(); + } +}; + +exports.setOrganizationIdentity = async function (stixId) { + // There should be exactly one system configuration document + const systemConfig = await systemConfigurationRepository.retrieveOne(); + + if (systemConfig) { + // The document exists already. Set the identity reference. + systemConfig.organization_identity_ref = stixId; + await systemConfigurationRepository.constructor.saveDocument(systemConfig); + } else { + // The document doesn't exist yet. Create a new one. + const systemConfigData = { + organization_identity_ref: stixId, + }; + const systemConfig = systemConfigurationRepository.createNewDocument(systemConfigData); + await systemConfigurationRepository.constructor.saveDocument(systemConfig); + } +}; + +const retrieveDefaultMarkingDefinitions = async function (options) { + if (!markingDefinitionsService) { + markingDefinitionsService = require('./marking-definitions-service'); + } + options = options ?? {}; + + // There should be exactly one system configuration document + const systemConfig = await systemConfigurationRepository.retrieveOne({ lean: true }); + + if (systemConfig) { + if (systemConfig.default_marking_definitions) { + if (options.refOnly) { + return systemConfig.default_marking_definitions; + } else { + const defaultMarkingDefinitions = []; + for (const stixId of systemConfig.default_marking_definitions) { + // eslint-disable-next-line no-await-in-loop + const markingDefinition = await markingDefinitionsService.retrieveById(stixId); + if (markingDefinition.length === 1) { + defaultMarkingDefinitions.push(markingDefinition[0]); + } else { + throw new DefaultMarkingDefinitionsNotFoundError(); + } } - } - else { - // No system config - return []; - } -} + return defaultMarkingDefinitions; + } + } else { + // default_marking_definitions not set + return []; + } + } else { + // No system config + return []; + } +}; exports.retrieveDefaultMarkingDefinitions = retrieveDefaultMarkingDefinitions; // ** migrated from attack-objects-service ** -exports.setDefaultMarkingDefinitionsForObject = async function(attackObject) { - // Add any default marking definitions that are not in the current list for this object - const defaultMarkingDefinitions = await retrieveDefaultMarkingDefinitions({ refOnly: true }); - if (attackObject.stix.object_marking_refs) { - attackObject.stix.object_marking_refs = attackObject.stix.object_marking_refs.concat(defaultMarkingDefinitions.filter(e => !attackObject.stix.object_marking_refs.includes(e))); - } - else { - attackObject.stix.object_marking_refs = defaultMarkingDefinitions; - } -} - -exports.setDefaultMarkingDefinitions = async function(stixIds) { - // There should be exactly one system configuration document - const systemConfig = await systemConfigurationRepository.retrieveOne(); - - if (systemConfig) { - // The document exists already. Set the default marking definitions. - systemConfig.default_marking_definitions = stixIds; - await systemConfigurationRepository.constructor.saveDocument(systemConfig); - } - else { - // The document doesn't exist yet. Create a new one. - const systemConfigData = { - default_marking_definitions: stixIds - }; - const systemConfig = systemConfigurationRepository.createNewDocument(systemConfigData); - await systemConfigurationRepository.constructor.saveDocument(systemConfig); - } -} - -exports.retrieveAnonymousUserAccount = async function() { - // There should be exactly one system configuration document - const systemConfig = await systemConfigurationRepository.retrieveOne({ lean: true }); - - if (systemConfig && systemConfig.anonymous_user_account_id) { - const userAccount = await userAccountsService.retrieveById(systemConfig.anonymous_user_account_id, {}); - if (userAccount) { - return userAccount; - } - else { - throw new AnonymousUserAccountNotFoundError(systemConfig.anonymous_user_account_id); - } - } - else { - throw new AnonymousUserAccountNotSetError; - } -} - -exports.setAnonymousUserAccountId = async function(userAccountId) { - // There should be exactly one system configuration document - const systemConfig = await systemConfigurationRepository.retrieveOne(); - - if (systemConfig) { - // The document exists already. Set the anonymous user account id. - systemConfig.anonymous_user_account_id = userAccountId; - await systemConfigurationRepository.constructor.saveDocument(systemConfig); - } - else { - throw new SystemConfigurationNotFound; - } -} - -exports.retrieveAuthenticationConfig = function() { - // We only support a one mechanism at a time, but may support multiples in the future, - // so return an array of mechanisms - const authenticationConfig = { - mechanisms: [ - { authnType: config.userAuthn.mechanism } - ] +exports.setDefaultMarkingDefinitionsForObject = async function (attackObject) { + // Add any default marking definitions that are not in the current list for this object + const defaultMarkingDefinitions = await retrieveDefaultMarkingDefinitions({ refOnly: true }); + if (attackObject.stix.object_marking_refs) { + attackObject.stix.object_marking_refs = attackObject.stix.object_marking_refs.concat( + defaultMarkingDefinitions.filter((e) => !attackObject.stix.object_marking_refs.includes(e)), + ); + } else { + attackObject.stix.object_marking_refs = defaultMarkingDefinitions; + } +}; + +exports.setDefaultMarkingDefinitions = async function (stixIds) { + // There should be exactly one system configuration document + const systemConfig = await systemConfigurationRepository.retrieveOne(); + + if (systemConfig) { + // The document exists already. Set the default marking definitions. + systemConfig.default_marking_definitions = stixIds; + await systemConfigurationRepository.constructor.saveDocument(systemConfig); + } else { + // The document doesn't exist yet. Create a new one. + const systemConfigData = { + default_marking_definitions: stixIds, }; - return authenticationConfig; -} - -exports.retrieveOrganizationNamespace = async function() { - // There should be exactly one system configuration document - const systemConfig = await systemConfigurationRepository.retrieveOne({ lean: true }); - - if (systemConfig) { - return systemConfig.organization_namespace; - } - else { - throw new SystemConfigurationNotFound; - } -} - -exports.setOrganizationNamespace = async function(namespace) { - // There should be exactly one system configuration document - const systemConfig = await systemConfigurationRepository.retrieveOne(); - - if (systemConfig) { - systemConfig.organization_namespace = namespace; - await systemConfigurationRepository.constructor.saveDocument(systemConfig); - } - else { - throw new SystemConfigurationNotFound; - } -} + const systemConfig = systemConfigurationRepository.createNewDocument(systemConfigData); + await systemConfigurationRepository.constructor.saveDocument(systemConfig); + } +}; + +exports.retrieveAnonymousUserAccount = async function () { + // There should be exactly one system configuration document + const systemConfig = await systemConfigurationRepository.retrieveOne({ lean: true }); + + if (systemConfig && systemConfig.anonymous_user_account_id) { + const userAccount = await userAccountsService.retrieveById( + systemConfig.anonymous_user_account_id, + {}, + ); + if (userAccount) { + return userAccount; + } else { + throw new AnonymousUserAccountNotFoundError(systemConfig.anonymous_user_account_id); + } + } else { + throw new AnonymousUserAccountNotSetError(); + } +}; + +exports.setAnonymousUserAccountId = async function (userAccountId) { + // There should be exactly one system configuration document + const systemConfig = await systemConfigurationRepository.retrieveOne(); + + if (systemConfig) { + // The document exists already. Set the anonymous user account id. + systemConfig.anonymous_user_account_id = userAccountId; + await systemConfigurationRepository.constructor.saveDocument(systemConfig); + } else { + throw new SystemConfigurationNotFound(); + } +}; + +exports.retrieveAuthenticationConfig = function () { + // We only support a one mechanism at a time, but may support multiples in the future, + // so return an array of mechanisms + const authenticationConfig = { + mechanisms: [{ authnType: config.userAuthn.mechanism }], + }; + return authenticationConfig; +}; + +exports.retrieveOrganizationNamespace = async function () { + // There should be exactly one system configuration document + const systemConfig = await systemConfigurationRepository.retrieveOne({ lean: true }); + + if (systemConfig) { + return systemConfig.organization_namespace; + } else { + throw new SystemConfigurationNotFound(); + } +}; + +exports.setOrganizationNamespace = async function (namespace) { + // There should be exactly one system configuration document + const systemConfig = await systemConfigurationRepository.retrieveOne(); + + if (systemConfig) { + systemConfig.organization_namespace = namespace; + await systemConfigurationRepository.constructor.saveDocument(systemConfig); + } else { + throw new SystemConfigurationNotFound(); + } +}; diff --git a/app/services/tactics-service.js b/app/services/tactics-service.js index 166028d0..def7e34a 100644 --- a/app/services/tactics-service.js +++ b/app/services/tactics-service.js @@ -8,79 +8,83 @@ const BaseService = require('./_base.service'); const tacticsRepository = require('../repository/tactics-repository'); class TacticsService extends BaseService { - static techniquesService = null; + static techniquesService = null; - static techniqueMatchesTactic(tactic) { - return function(technique) { - // A tactic matches if the technique has a kill chain phase such that: - // 1. The phase's kill_chain_name matches one of the tactic's kill chain names (which are derived from the tactic's x_mitre_domains) - // 2. The phase's phase_name matches the tactic's x_mitre_shortname - // Convert the tactic's domain names to kill chain names - const tacticKillChainNames = tactic.stix.x_mitre_domains.map(domain => config.domainToKillChainMap[domain]); - return technique.stix.kill_chain_phases.some(phase => phase.phase_name === tactic.stix.x_mitre_shortname && tacticKillChainNames.includes(phase.kill_chain_name)); - } - } + static techniqueMatchesTactic(tactic) { + return function (technique) { + // A tactic matches if the technique has a kill chain phase such that: + // 1. The phase's kill_chain_name matches one of the tactic's kill chain names (which are derived from the tactic's x_mitre_domains) + // 2. The phase's phase_name matches the tactic's x_mitre_shortname + // Convert the tactic's domain names to kill chain names + const tacticKillChainNames = tactic.stix.x_mitre_domains.map( + (domain) => config.domainToKillChainMap[domain], + ); + return technique.stix.kill_chain_phases.some( + (phase) => + phase.phase_name === tactic.stix.x_mitre_shortname && + tacticKillChainNames.includes(phase.kill_chain_name), + ); + }; + } - static getPageOfData(data, options) { - const startPos = options.offset; - const endPos = (options.limit === 0) ? data.length : Math.min(options.offset + options.limit, data.length); + static getPageOfData(data, options) { + const startPos = options.offset; + const endPos = + options.limit === 0 ? data.length : Math.min(options.offset + options.limit, data.length); - return data.slice(startPos, endPos); - } + return data.slice(startPos, endPos); + } - async retrieveTechniquesForTactic (stixId, modified, options) { - // Late binding to avoid circular dependency between modules - if (!TacticsService.techniquesService) { - TacticsService.techniquesService = require('./techniques-service'); - } + async retrieveTechniquesForTactic(stixId, modified, options) { + // Late binding to avoid circular dependency between modules + if (!TacticsService.techniquesService) { + TacticsService.techniquesService = require('./techniques-service'); + } - // Retrieve the techniques associated with the tactic (the tactic identified by stixId and modified date) - if (!stixId) { - throw new MissingParameterError({ parameterName: 'stixId' }); - } + // Retrieve the techniques associated with the tactic (the tactic identified by stixId and modified date) + if (!stixId) { + throw new MissingParameterError({ parameterName: 'stixId' }); + } - if (!modified) { - throw new MissingParameterError({ parameterName: 'modified' }); - } + if (!modified) { + throw new MissingParameterError({ parameterName: 'modified' }); + } - try { - const tactic = await this.repository.retrieveOneByVersion(stixId, modified); + try { + const tactic = await this.repository.retrieveOneByVersion(stixId, modified); - // Note: document is null if not found - if (!tactic) { - return null; - } - else { - const allTechniques = await TacticsService.techniquesService.retrieveAll({}); - const filteredTechniques = allTechniques.filter(TacticsService.techniqueMatchesTactic(tactic)); - const pagedResults = TacticsService.getPageOfData(filteredTechniques, options); + // Note: document is null if not found + if (!tactic) { + return null; + } else { + const allTechniques = await TacticsService.techniquesService.retrieveAll({}); + const filteredTechniques = allTechniques.filter( + TacticsService.techniqueMatchesTactic(tactic), + ); + const pagedResults = TacticsService.getPageOfData(filteredTechniques, options); - if (options.includePagination) { - const returnValue = { - pagination: { - total: pagedResults.length, - offset: options.offset, - limit: options.limit - }, - data: pagedResults - }; - return returnValue; - } - else { - return pagedResults; - } - } - } - catch(err) { - if (err.name === 'CastError') { - throw new BadlyFormattedParameterError({ parameterName: 'stixId' }); - } - else { - throw err; - } + if (options.includePagination) { + const returnValue = { + pagination: { + total: pagedResults.length, + offset: options.offset, + limit: options.limit, + }, + data: pagedResults, + }; + return returnValue; + } else { + return pagedResults; } + } + } catch (err) { + if (err.name === 'CastError') { + throw new BadlyFormattedParameterError({ parameterName: 'stixId' }); + } else { + throw err; + } } - + } } module.exports = new TacticsService('x-mitre-tactic', tacticsRepository); diff --git a/app/services/teams-service.js b/app/services/teams-service.js index 027e2ea7..1d10cd85 100644 --- a/app/services/teams-service.js +++ b/app/services/teams-service.js @@ -3,181 +3,179 @@ const regexValidator = require('../lib/regex'); const UserAccount = require('../models/user-account-model'); const userAccountsService = require('./user-accounts-service'); -const { MissingParameterError, BadlyFormattedParameterError, NotFoundError, DuplicateIdError} = require('../exceptions'); +const { + MissingParameterError, + BadlyFormattedParameterError, + NotFoundError, + DuplicateIdError, +} = require('../exceptions'); const teamsRepository = require('../repository/teams-repository'); -const uuid = require("uuid"); +const uuid = require('uuid'); class TeamsService { - constructor(type, repository) { - this.type = type; - this.repository = repository; + constructor(type, repository) { + this.type = type; + this.repository = repository; + } + + async retrieveAll(options) { + const results = await this.repository.retrieveAll(options); + + const teams = results[0].documents; + + if (options.includePagination) { + let derivedTotalCount = 0; + if (results[0].totalCount.length > 0) { + derivedTotalCount = results[0].totalCount[0].totalCount; + } + const paginatedResults = { + pagination: { + total: derivedTotalCount, + offset: options.offset, + limit: options.limit, + }, + data: teams, + }; + return paginatedResults; + } else { + return teams; } + } - async retrieveAll(options) { - const results = await this.repository.retrieveAll(options); + async retrieveById(teamId) { + return await this.repository.retrieveById(teamId); + } - const teams = results[0].documents; + async create(data) { + // Create the document + const team = await this.repository.createNewDocument(data); - if (options.includePagination) { - let derivedTotalCount = 0; - if (results[0].totalCount.length > 0) { - derivedTotalCount = results[0].totalCount[0].totalCount; - } - const paginatedResults = { - pagination: { - total: derivedTotalCount, - offset: options.offset, - limit: options.limit - }, - data: teams - }; - return paginatedResults; - } - else { - return teams; - } + // Create a unique id for this user + // This should usually be undefined. It will only be defined when migrating teams from another system. + if (!team.id) { + team.id = `identity--${uuid.v4()}`; } - - async retrieveById(teamId) { - return await this.repository.retrieveById(teamId); - } - - async create(data) { - // Create the document - const team = await this.repository.createNewDocument(data); - - // Create a unique id for this user - // This should usually be undefined. It will only be defined when migrating teams from another system. - if (!team.id) { - team.id = `identity--${ uuid.v4() }`; - } - - // Add a timestamp recording when the team was first created - // This should usually be undefined. It will only be defined when migrating teams from another system. - if (!team.created) { - team.created = new Date().toISOString(); - } - // Add a timestamp recording when the team was last modified - if (!team.modified) { - team.modified = team.created; - } + // Add a timestamp recording when the team was first created + // This should usually be undefined. It will only be defined when migrating teams from another system. + if (!team.created) { + team.created = new Date().toISOString(); + } - // Save the document in the database - try { - return await this.repository.constructor.saveDocument(team); - } - catch (err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - throw new DuplicateIdError; - } - else { - throw err; - } - } + // Add a timestamp recording when the team was last modified + if (!team.modified) { + team.modified = team.created; } - async updateFull(teamId, data) { - return await this.repository.updateFull(teamId, data); + // Save the document in the database + try { + return await this.repository.constructor.saveDocument(team); + } catch (err) { + if (err.name === 'MongoServerError' && err.code === 11000) { + throw new DuplicateIdError(); + } else { + throw err; + } } + } - async delete(teamId) { - if (!teamId) { - throw new MissingParameterError; - } + async updateFull(teamId, data) { + return await this.repository.updateFull(teamId, data); + } - const team = await this.repository.model.findOneAndRemove({ 'id': teamId }); - //Note: userAccount is null if not found - return team; + async delete(teamId) { + if (!teamId) { + throw new MissingParameterError(); + } + + const team = await this.repository.model.findOneAndRemove({ id: teamId }); + //Note: userAccount is null if not found + return team; + } + async retrieveAllUsers(teamId, options) { + if (!teamId) { + throw new MissingParameterError('teamId'); } - async retrieveAllUsers(teamId, options) { - if (!teamId) { - throw new MissingParameterError('teamId'); - } + try { + const team = await this.repository.model.findOne({ id: teamId }).lean().exec(); + if (!team) { + throw new NotFoundError(); + } + const matchQuery = { id: { $in: team.userIDs } }; + const aggregation = [{ $sort: { username: 1 } }, { $match: matchQuery }]; + if (typeof options.search !== 'undefined') { + options.search = regexValidator.sanitizeRegex(options.search); + const match = { + $match: { + $or: [ + { username: { $regex: options.search, $options: 'i' } }, + { email: { $regex: options.search, $options: 'i' } }, + { displayName: { $regex: options.search, $options: 'i' } }, + ], + }, + }; + aggregation.push(match); + } + const facet = { + $facet: { + totalCount: [{ $count: 'totalCount' }], + documents: [], + }, + }; + if (options.offset) { + facet.$facet.documents.push({ $skip: options.offset }); + } else { + facet.$facet.documents.push({ $skip: 0 }); + } + if (options.limit) { + facet.$facet.documents.push({ $limit: options.limit }); + } + aggregation.push(facet); + try { + const results = await UserAccount.aggregate(aggregation); + const userAccounts = results[0].documents; + userAccounts.forEach((userAccount) => { + userAccountsService.constructor.addEffectiveRole(userAccount); + if (options.includeStixIdentity) { + userAccount.identity = + userAccountsService.constructor.userAccountAsIdentity(userAccount); + } + }); - try { - const team = await this.repository.model.findOne({ 'id': teamId }) - .lean() - .exec(); - if (!team) { - throw new NotFoundError; - } - const matchQuery = {'id': {$in: team.userIDs}}; - const aggregation = [ - { $sort: { 'username': 1 } }, - { $match: matchQuery } - ]; - if (typeof options.search !== 'undefined') { - options.search = regexValidator.sanitizeRegex(options.search); - const match = { $match: { $or: [ - { 'username': { '$regex': options.search, '$options': 'i' }}, - { 'email': { '$regex': options.search, '$options': 'i' }}, - { 'displayName': { '$regex': options.search, '$options': 'i' }} - ]}}; - aggregation.push(match); - } - const facet = { - $facet: { - totalCount: [ { $count: 'totalCount' }], - documents: [ ] - } - }; - if (options.offset) { - facet.$facet.documents.push({ $skip: options.offset }); - } - else { - facet.$facet.documents.push({ $skip: 0 }); - } - if (options.limit) { - facet.$facet.documents.push({ $limit: options.limit }); - } - aggregation.push(facet); - try { - const results = await UserAccount.aggregate(aggregation); - const userAccounts = results[0].documents; - userAccounts.forEach(userAccount => { - userAccountsService.constructor.addEffectiveRole(userAccount); - if (options.includeStixIdentity) { - userAccount.identity = userAccountsService.constructor.userAccountAsIdentity(userAccount); - } - }); - - if (options.includePagination) { - let derivedTotalCount = 0; - if (results[0].totalCount.length > 0) { - derivedTotalCount = results[0].totalCount[0].totalCount; - } - const returnValue = { - pagination: { - total: derivedTotalCount, - offset: options.offset, - limit: options.limit - }, - data: userAccounts - }; - return returnValue; - } else { - return userAccounts; - } - } - catch (err) { - if (err.name === 'CastError') { - throw new BadlyFormattedParameterError("teamId"); - } else { - throw err; - } - } + if (options.includePagination) { + let derivedTotalCount = 0; + if (results[0].totalCount.length > 0) { + derivedTotalCount = results[0].totalCount[0].totalCount; + } + const returnValue = { + pagination: { + total: derivedTotalCount, + offset: options.offset, + limit: options.limit, + }, + data: userAccounts, + }; + return returnValue; + } else { + return userAccounts; } - catch (err) { - if (err.name === 'CastError') { - throw new BadlyFormattedParameterError("teamId"); - } else { - throw err; - } + } catch (err) { + if (err.name === 'CastError') { + throw new BadlyFormattedParameterError('teamId'); + } else { + throw err; } + } + } catch (err) { + if (err.name === 'CastError') { + throw new BadlyFormattedParameterError('teamId'); + } else { + throw err; + } } + } } -module.exports = new TeamsService(null, teamsRepository); \ No newline at end of file +module.exports = new TeamsService(null, teamsRepository); diff --git a/app/services/techniques-service.js b/app/services/techniques-service.js index 2097d13d..8045d312 100644 --- a/app/services/techniques-service.js +++ b/app/services/techniques-service.js @@ -8,78 +8,86 @@ const BaseService = require('./_base.service'); const techniquesRepository = require('../repository/techniques-repository'); class TechniquesService extends BaseService { - static tacticsService = null; + static tacticsService = null; - static tacticMatchesTechnique(technique) { - return function(tactic) { - // A tactic matches if the technique has a kill chain phase such that: - // 1. The phase's kill_chain_name matches one of the tactic's kill chain names (which are derived from the tactic's x_mitre_domains) - // 2. The phase's phase_name matches the tactic's x_mitre_shortname + static tacticMatchesTechnique(technique) { + return function (tactic) { + // A tactic matches if the technique has a kill chain phase such that: + // 1. The phase's kill_chain_name matches one of the tactic's kill chain names (which are derived from the tactic's x_mitre_domains) + // 2. The phase's phase_name matches the tactic's x_mitre_shortname - // Convert the tactic's domain names to kill chain names - const tacticKillChainNames = tactic.stix.x_mitre_domains.map(domain => config.domainToKillChainMap[domain]); - return technique.stix.kill_chain_phases.some(phase => phase.phase_name === tactic.stix.x_mitre_shortname && tacticKillChainNames.includes(phase.kill_chain_name)); - } - } + // Convert the tactic's domain names to kill chain names + const tacticKillChainNames = tactic.stix.x_mitre_domains.map( + (domain) => config.domainToKillChainMap[domain], + ); + return technique.stix.kill_chain_phases.some( + (phase) => + phase.phase_name === tactic.stix.x_mitre_shortname && + tacticKillChainNames.includes(phase.kill_chain_name), + ); + }; + } - static getPageOfData(data, options) { - const startPos = options.offset; - const endPos = (options.limit === 0) ? data.length : Math.min(options.offset + options.limit, data.length); + static getPageOfData(data, options) { + const startPos = options.offset; + const endPos = + options.limit === 0 ? data.length : Math.min(options.offset + options.limit, data.length); - return data.slice(startPos, endPos); - } + return data.slice(startPos, endPos); + } - async retrieveTacticsForTechnique(stixId, modified, options) { - // Late binding to avoid circular dependency between modules - if (!TechniquesService.tacticsService) { - TechniquesService.tacticsService = require('./tactics-service'); - } + async retrieveTacticsForTechnique(stixId, modified, options) { + // Late binding to avoid circular dependency between modules + if (!TechniquesService.tacticsService) { + TechniquesService.tacticsService = require('./tactics-service'); + } - // Retrieve the tactics associated with the technique (the technique identified by stixId and modified date) - if (!stixId) { - throw new MissingParameterError; - } + // Retrieve the tactics associated with the technique (the technique identified by stixId and modified date) + if (!stixId) { + throw new MissingParameterError(); + } - if (!modified) { - throw new MissingParameterError; - } + if (!modified) { + throw new MissingParameterError(); + } - try { - const technique = await this.repository.model.findOne({ 'stix.id': stixId, 'stix.modified': modified }); - if (!technique) { - // Note: document is null if not found - return null; - } - else { - const allTactics = await TechniquesService.tacticsService.retrieveAll({}); - const filteredTactics = allTactics.filter(TechniquesService.tacticMatchesTechnique(technique)); - const pagedResults = TechniquesService.getPageOfData(filteredTactics, options); + try { + const technique = await this.repository.model.findOne({ + 'stix.id': stixId, + 'stix.modified': modified, + }); + if (!technique) { + // Note: document is null if not found + return null; + } else { + const allTactics = await TechniquesService.tacticsService.retrieveAll({}); + const filteredTactics = allTactics.filter( + TechniquesService.tacticMatchesTechnique(technique), + ); + const pagedResults = TechniquesService.getPageOfData(filteredTactics, options); - if (options.includePagination) { - const returnValue = { - pagination: { - total: pagedResults.length, - offset: options.offset, - limit: options.limit - }, - data: pagedResults - }; - return returnValue; - } - else { - return pagedResults; - } - } - } - catch(err) { - if (err.name === 'CastError') { - throw new BadlyFormattedParameterError; - } - else { - throw err; - } + if (options.includePagination) { + const returnValue = { + pagination: { + total: pagedResults.length, + offset: options.offset, + limit: options.limit, + }, + data: pagedResults, + }; + return returnValue; + } else { + return pagedResults; } + } + } catch (err) { + if (err.name === 'CastError') { + throw new BadlyFormattedParameterError(); + } else { + throw err; + } } + } } module.exports = new TechniquesService('attack-pattern', techniquesRepository); diff --git a/app/services/user-accounts-service.js b/app/services/user-accounts-service.js index 1b9ab775..cc32b97d 100644 --- a/app/services/user-accounts-service.js +++ b/app/services/user-accounts-service.js @@ -3,242 +3,242 @@ const uuid = require('uuid'); const TeamsRepository = require('../repository/teams-repository'); const UserAccountsRepository = require('../repository/user-accounts-repository'); -const { MissingParameterError, BadlyFormattedParameterError, DuplicateIdError, DuplicateEmailError } = require('../exceptions'); +const { + MissingParameterError, + BadlyFormattedParameterError, + DuplicateIdError, + DuplicateEmailError, +} = require('../exceptions'); class UserAccountsService { + constructor(type, repository) { + this.type = type; + this.repository = repository; + } - constructor(type, repository) { - this.type = type; - this.repository = repository; + // Helper function to determine if the last argument is a callback + static isCallback(arg) { + return typeof arg === 'function'; + } + + static addEffectiveRole(userAccount) { + // Initially, this forces all pending and inactive accounts to have the role 'none'. + // TBD: Make the role configurable + if (userAccount?.status === 'pending' || userAccount?.status === 'inactive') { + userAccount.role = 'none'; + } + } + + static userAccountAsIdentity(userAccount) { + return { + type: 'identity', + spec_version: '2.1', + id: userAccount.id, + created: userAccount.created, + modified: userAccount.modified, + name: userAccount.displayName, + identity_class: 'individual', + }; + } + + async retrieveAll(options) { + const results = await this.repository.retrieveAll(options); + + const userAccounts = results[0].documents; + userAccounts.forEach((userAccount) => { + UserAccountsService.addEffectiveRole(userAccount); + if (options.includeStixIdentity) { + userAccount.identity = UserAccountsService.userAccountAsIdentity(userAccount); + } + }); + + if (options.includePagination) { + let derivedTotalCount = 0; + if (results[0].totalCount.length > 0) { + derivedTotalCount = results[0].totalCount[0].totalCount; + } + const paginatedResults = { + pagination: { + total: derivedTotalCount, + offset: options.offset, + limit: options.limit, + }, + data: userAccounts, + }; + return paginatedResults; + } else { + return userAccounts; + } + } + + async retrieveById(userAccountId, options) { + try { + if (!userAccountId) { + throw new MissingParameterError('userAccountId'); + } + + const userAccount = await this.repository.retrieveOneById(userAccountId); + if (!userAccount) { + return null; // Document not found + } + + UserAccountsService.addEffectiveRole(userAccount); + + if (options.includeStixIdentity) { + userAccount.identity = UserAccountsService.userAccountAsIdentity(userAccount); + } + + return userAccount; + } catch (err) { + if (err.name === 'CastError') { + throw new BadlyFormattedParameterError('userId'); + } else { + throw err; + } } + } - // Helper function to determine if the last argument is a callback - static isCallback(arg) { - return typeof arg === 'function'; + async retrieveByEmail(email) { + if (!email) { + throw new MissingParameterError('email'); } - static addEffectiveRole(userAccount) { - // Initially, this forces all pending and inactive accounts to have the role 'none'. - // TBD: Make the role configurable - if (userAccount?.status === 'pending' || userAccount?.status === 'inactive') { - userAccount.role = 'none'; - } - } - - static userAccountAsIdentity(userAccount) { - return { - type: 'identity', - spec_version: '2.1', - id: userAccount.id, - created: userAccount.created, - modified: userAccount.modified, - name: userAccount.displayName, - identity_class: 'individual' - } - } - - async retrieveAll (options) { - const results = await this.repository.retrieveAll(options); - - const userAccounts = results[0].documents; - userAccounts.forEach(userAccount => { - UserAccountsService.addEffectiveRole(userAccount); - if (options.includeStixIdentity) { - userAccount.identity = UserAccountsService.userAccountAsIdentity(userAccount); - } - }); - - if (options.includePagination) { - let derivedTotalCount = 0; - if (results[0].totalCount.length > 0) { - derivedTotalCount = results[0].totalCount[0].totalCount; - } - const paginatedResults = { - pagination: { - total: derivedTotalCount, - offset: options.offset, - limit: options.limit - }, - data: userAccounts - }; - return paginatedResults; - } - else { - return userAccounts; - } - } - - async retrieveById(userAccountId, options) { - try { - if (!userAccountId) { - throw new MissingParameterError('userAccountId'); - } - - const userAccount = await this.repository.retrieveOneById(userAccountId); - if (!userAccount) { - return null; // Document not found - } - - UserAccountsService.addEffectiveRole(userAccount); - - if (options.includeStixIdentity) { - userAccount.identity = UserAccountsService.userAccountAsIdentity(userAccount); - } - - return userAccount; - } catch (err) { - if (err.name === 'CastError') { - throw new BadlyFormattedParameterError('userId'); - } else { - throw err; - } - } - } - - async retrieveByEmail(email) { - if (!email) { - throw new MissingParameterError("email"); - } - - try { - const userAccount = await this.repository.retrieveOneByEmail(email); - UserAccountsService.addEffectiveRole(userAccount); - - return userAccount; - } - catch(err) { - if (err.name === 'CastError') { - throw new BadlyFormattedParameterError("email"); - } else { - throw err; - } - } - } - - async create(data) { - // Check for a duplicate email - if (data.email) { - // Note: We could try to insert the new document without this check and allow Mongoose to throw a duplicate - // index error. But the Error that's thrown doesn't allow us to distinguish between a duplicate id (which is - // unexpected and may indicate a deeper problem) and a duplicate email (which is likely a client error). - // So we perform this check here to catch the duplicate email and then treat the duplicate index as a server - // error if it occurs. - const userAccount = await this.repository.retrieveOneByEmail(data.email); - if (userAccount) { - throw new DuplicateEmailError; - } - } - - // Create the document - const userAccount = await this.repository.createNewDocument(data); - - // Create a unique id for this user - // This should usually be undefined. It will only be defined when migrating user accounts from another system. - if (!userAccount.id) { - userAccount.id = `identity--${ uuid.v4() }`; - } - - // Add a timestamp recording when the user account was first created - // This should usually be undefined. It will only be defined when migrating user accounts from another system. - if (!userAccount.created) { - userAccount.created = new Date().toISOString(); - } - - // Add a timestamp recording when the user account was last modified - if (!userAccount.modified) { - userAccount.modified = userAccount.created; - } - - // Save the document in the database - try { - const savedUserAccount = await this.repository.saveDocument(userAccount); - UserAccountsService.addEffectiveRole(savedUserAccount); - - return savedUserAccount; - } - catch (err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - throw new DuplicateIdError; - } - else { - throw err; - } - } - } - - async updateFull (userAccountId, data) { - if (!userAccountId) { - throw new MissingParameterError('userAccountId'); - } - - return await this.repository.updateById(userAccountId, data); - } - - async delete (userAccountId) { - if (!userAccountId) { - throw new MissingParameterError('userAccountId'); - } - - const userAccount = await this.repository.findOneAndRemove(userAccountId); - return userAccount; - } - - async getLatest(userAccountId) { - const userAccount = await this.repository.retrieveOneById(userAccountId); - UserAccountsService.addEffectiveRole(userAccount); - - return userAccount; - } - - async addCreatedByUserAccount(attackObject) { - if (attackObject?.workspace?.workflow?.created_by_user_account) { - try { - // eslint-disable-next-line require-atomic-updates - attackObject.created_by_user_account = await this.getLatest(attackObject.workspace.workflow.created_by_user_account); - } - catch(err) { - // Ignore lookup errors - } - } - } - - async addCreatedByUserAccountToAll(attackObjects) { - for (const attackObject of attackObjects) { - // eslint-disable-next-line no-await-in-loop - await this.addCreatedByUserAccount(attackObject); - } - } - - static async retrieveTeamsByUserId (userAccountId, options) { - if (!userAccountId) { - throw new MissingParameterError('userAccountId'); - } - - const results = await TeamsRepository.retrieveByUserId(userAccountId, options); - const teams = results[0].documents; - - if (options.includePagination) { - let derivedTotalCount = 0; - - if (results[0].totalCount.length > 0) { - derivedTotalCount = results[0].totalCount[0].totalCount; - } - - const returnValue = { - pagination: { - total: derivedTotalCount, - offset: options.offset, - limit: options.limit - }, - data: teams - }; - - return returnValue; - } - else { - return teams; - } + try { + const userAccount = await this.repository.retrieveOneByEmail(email); + UserAccountsService.addEffectiveRole(userAccount); + + return userAccount; + } catch (err) { + if (err.name === 'CastError') { + throw new BadlyFormattedParameterError('email'); + } else { + throw err; + } + } + } + + async create(data) { + // Check for a duplicate email + if (data.email) { + // Note: We could try to insert the new document without this check and allow Mongoose to throw a duplicate + // index error. But the Error that's thrown doesn't allow us to distinguish between a duplicate id (which is + // unexpected and may indicate a deeper problem) and a duplicate email (which is likely a client error). + // So we perform this check here to catch the duplicate email and then treat the duplicate index as a server + // error if it occurs. + const userAccount = await this.repository.retrieveOneByEmail(data.email); + if (userAccount) { + throw new DuplicateEmailError(); + } + } + + // Create the document + const userAccount = await this.repository.createNewDocument(data); + + // Create a unique id for this user + // This should usually be undefined. It will only be defined when migrating user accounts from another system. + if (!userAccount.id) { + userAccount.id = `identity--${uuid.v4()}`; + } + + // Add a timestamp recording when the user account was first created + // This should usually be undefined. It will only be defined when migrating user accounts from another system. + if (!userAccount.created) { + userAccount.created = new Date().toISOString(); + } + + // Add a timestamp recording when the user account was last modified + if (!userAccount.modified) { + userAccount.modified = userAccount.created; + } + + // Save the document in the database + try { + const savedUserAccount = await this.repository.saveDocument(userAccount); + UserAccountsService.addEffectiveRole(savedUserAccount); + + return savedUserAccount; + } catch (err) { + if (err.name === 'MongoServerError' && err.code === 11000) { + throw new DuplicateIdError(); + } else { + throw err; + } + } + } + + async updateFull(userAccountId, data) { + if (!userAccountId) { + throw new MissingParameterError('userAccountId'); + } + + return await this.repository.updateById(userAccountId, data); + } + + async delete(userAccountId) { + if (!userAccountId) { + throw new MissingParameterError('userAccountId'); + } + + const userAccount = await this.repository.findOneAndRemove(userAccountId); + return userAccount; + } + + async getLatest(userAccountId) { + const userAccount = await this.repository.retrieveOneById(userAccountId); + UserAccountsService.addEffectiveRole(userAccount); + + return userAccount; + } + + async addCreatedByUserAccount(attackObject) { + if (attackObject?.workspace?.workflow?.created_by_user_account) { + try { + // eslint-disable-next-line require-atomic-updates + attackObject.created_by_user_account = await this.getLatest( + attackObject.workspace.workflow.created_by_user_account, + ); + } catch (err) { + // Ignore lookup errors + } + } + } + + async addCreatedByUserAccountToAll(attackObjects) { + for (const attackObject of attackObjects) { + // eslint-disable-next-line no-await-in-loop + await this.addCreatedByUserAccount(attackObject); + } + } + + static async retrieveTeamsByUserId(userAccountId, options) { + if (!userAccountId) { + throw new MissingParameterError('userAccountId'); + } + + const results = await TeamsRepository.retrieveByUserId(userAccountId, options); + const teams = results[0].documents; + + if (options.includePagination) { + let derivedTotalCount = 0; + + if (results[0].totalCount.length > 0) { + derivedTotalCount = results[0].totalCount[0].totalCount; + } + + const returnValue = { + pagination: { + total: derivedTotalCount, + offset: options.offset, + limit: options.limit, + }, + data: teams, + }; + + return returnValue; + } else { + return teams; } + } } -module.exports = new UserAccountsService(null, UserAccountsRepository); \ No newline at end of file +module.exports = new UserAccountsService(null, UserAccountsRepository); diff --git a/app/tests/api/assets/assets.spec.js b/app/tests/api/assets/assets.spec.js index 69cd231b..478b44b1 100644 --- a/app/tests/api/assets/assets.spec.js +++ b/app/tests/api/assets/assets.spec.js @@ -14,360 +14,358 @@ logger.level = 'debug'; // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - name: 'asset-1', - spec_version: '2.1', - type: 'x-mitre-asset', - description: 'This is an asset.', - x_mitre_sectors: [ 'sector placeholder 1' ], - x_mitre_related_assets: [ - { - name: 'related asset 1', - related_asset_sectors: [ 'related asset sector placeholder 1' ], - description: 'This is a related asset' - }, - { - name: 'related asset 2', - related_asset_sectors: [ 'related asset sector placeholder 2' ], - description: 'This is another related asset' - } - ], - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - x_mitre_version: "1.1" - } + }, + stix: { + name: 'asset-1', + spec_version: '2.1', + type: 'x-mitre-asset', + description: 'This is an asset.', + x_mitre_sectors: ['sector placeholder 1'], + x_mitre_related_assets: [ + { + name: 'related asset 1', + related_asset_sectors: ['related asset sector placeholder 1'], + description: 'This is a related asset', + }, + { + name: 'related asset 2', + related_asset_sectors: ['related asset sector placeholder 2'], + description: 'This is another related asset', + }, + ], + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + x_mitre_version: '1.1', + }, }; describe('Assets API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - it('GET /api/assets returns an empty array of assets', async function () { - const res = await request(app) - .get('/api/assets') - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const assets = res.body; - expect(assets).toBeDefined(); - expect(Array.isArray(assets)).toBe(true); - expect(assets.length).toBe(0); - }); - - it('POST /api/assets does not create an empty asset', async function () { - const body = { }; - await request(app) - .post('/api/assets') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(400); - - }); - - let asset1; - it('POST /api/assets creates an asset', async function () { - const timestamp = new Date().toISOString(); - initialObjectData.stix.created = timestamp; - initialObjectData.stix.modified = timestamp; - const body = initialObjectData; - const res = await request(app) - .post('/api/assets') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created asset - asset1 = res.body; - expect(asset1).toBeDefined(); - expect(asset1.stix).toBeDefined(); - expect(asset1.stix.id).toBeDefined(); - expect(asset1.stix.created).toBeDefined(); - expect(asset1.stix.modified).toBeDefined(); - expect(asset1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); - - expect(asset1.stix.x_mitre_sectors).toBeDefined(); - expect(Array.isArray(asset1.stix.x_mitre_sectors)).toBe(true); - expect(asset1.stix.x_mitre_sectors.length).toBe(1); - - expect(asset1.stix.x_mitre_related_assets).toBeDefined(); - expect(Array.isArray(asset1.stix.x_mitre_related_assets)).toBe(true); - expect(asset1.stix.x_mitre_related_assets.length).toBe(2); - }); - - it('GET /api/assets returns the added asset', async function () { - const res = await request(app) - .get('/api/assets') - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one asset in an array - const assets = res.body; - expect(assets).toBeDefined(); - expect(Array.isArray(assets)).toBe(true); - expect(assets.length).toBe(1); - }); - - it('GET /api/assets/:id should not return an asset when the id cannot be found', async function () { - await request(app) - .get('/api/assets/not-an-id') - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(404); - }); - - it('GET /api/assets/:id?versions=all should not return an asset when the id cannot be found', async function () { - await request(app) - .get('/api/assets/not-an-id?versions=all') - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(404); - }); - - it('GET /api/assets/:id returns the added asset', async function () { - const res = await request(app) - .get('/api/assets/' + asset1.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one asset in an array - const assets = res.body; - expect(assets).toBeDefined(); - expect(Array.isArray(assets)).toBe(true); - expect(assets.length).toBe(1); - - const asset = assets[0]; - expect(asset).toBeDefined(); - expect(asset.stix).toBeDefined(); - expect(asset.stix.id).toBe(asset1.stix.id); - expect(asset.stix.type).toBe(asset1.stix.type); - expect(asset.stix.name).toBe(asset1.stix.name); - expect(asset.stix.description).toBe(asset1.stix.description); - expect(asset.stix.spec_version).toBe(asset1.stix.spec_version); - expect(asset.stix.object_marking_refs).toEqual(expect.arrayContaining(asset1.stix.object_marking_refs)); - expect(asset.stix.created_by_ref).toBe(asset1.stix.created_by_ref); - expect(asset.stix.x_mitre_version).toBe(asset1.stix.x_mitre_version); - expect(asset.stix.x_mitre_attack_spec_version).toBe(asset1.stix.x_mitre_attack_spec_version); - - expect(asset.stix.x_mitre_sectors).toBeDefined(); - expect(Array.isArray(asset.stix.x_mitre_sectors)).toBe(true); - expect(asset.stix.x_mitre_sectors.length).toBe(1); - - expect(asset.stix.x_mitre_related_assets).toBeDefined(); - expect(Array.isArray(asset.stix.x_mitre_related_assets)).toBe(true); - expect(asset.stix.x_mitre_related_assets.length).toBe(2); - }); - - it('PUT /api/assets updates an asset', async function () { - const originalModified = asset1.stix.modified; - const timestamp = new Date().toISOString(); - asset1.stix.modified = timestamp; - asset1.stix.description = 'This is an updated asset.' - const body = asset1; - const res = await request(app) - .put('/api/assets/' + asset1.stix.id + '/modified/' + originalModified) - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the updated asset - const asset = res.body; - expect(asset).toBeDefined(); - expect(asset.stix.id).toBe(asset1.stix.id); - expect(asset.stix.modified).toBe(asset1.stix.modified); - }); - - it('POST /api/assets does not create an asset with the same id and modified date', async function () { - const body = asset1; - await request(app) - .post('/api/assets') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(409); - }); - - let asset2; - it('POST /api/assets should create a new version of an asset with a duplicate stix.id but different stix.modified date', async function () { - asset2 = _.cloneDeep(asset1); - asset2._id = undefined; - asset2.__t = undefined; - asset2.__v = undefined; - const timestamp = new Date().toISOString(); - asset2.stix.modified = timestamp; - const body = asset2; - const res = await request(app) - .post('/api/assets') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created asset - const asset = res.body; - expect(asset).toBeDefined(); - }); - - let asset3; - it('POST /api/assets should create a new version of an asset with a duplicate stix.id but different stix.modified date', async function () { - asset3 = _.cloneDeep(asset1); - asset3._id = undefined; - asset3.__t = undefined; - asset3.__v = undefined; - const timestamp = new Date().toISOString(); - asset3.stix.modified = timestamp; - const body = asset3; - const res = await request(app) - .post('/api/assets') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created asset - const asset = res.body; - expect(asset).toBeDefined(); - }); - - it('GET /api/assets returns the latest added asset', async function () { - const res = await request(app) - .get('/api/assets/' + asset3.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one asset in an array - const assets = res.body; - expect(assets).toBeDefined(); - expect(Array.isArray(assets)).toBe(true); - expect(assets.length).toBe(1); - const asset = assets[0]; - expect(asset.stix.id).toBe(asset3.stix.id); - expect(asset.stix.modified).toBe(asset3.stix.modified); - }); - - it('GET /api/assets returns all added assets', async function () { - const res = await request(app) - .get('/api/assets/' + asset1.stix.id + '?versions=all') - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get two assets in an array - const assets = res.body; - expect(assets).toBeDefined(); - expect(Array.isArray(assets)).toBe(true); - expect(assets.length).toBe(3); - }); - - it('GET /api/assets/:id/modified/:modified returns the first added asset', async function () { - const res = await request(app) - .get('/api/assets/' + asset1.stix.id + '/modified/' + asset1.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one asset in an array - const asset = res.body; - expect(asset).toBeDefined(); - expect(asset.stix).toBeDefined(); - expect(asset.stix.id).toBe(asset1.stix.id); - expect(asset.stix.modified).toBe(asset1.stix.modified); - }); - - it('GET /api/assets/:id/modified/:modified returns the second added asset', async function () { - const res = await request(app) - .get('/api/assets/' + asset2.stix.id + '/modified/' + asset2.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one asset in an array - const asset = res.body; - expect(asset).toBeDefined(); - expect(asset.stix).toBeDefined(); - expect(asset.stix.id).toBe(asset2.stix.id); - expect(asset.stix.modified).toBe(asset2.stix.modified); - }); - - it('DELETE /api/assets/:id should not delete an asset when the id cannot be found', async function () { - await request(app) - .delete('/api/assets/not-an-id') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(404); - }); - - it('DELETE /api/assets/:id/modified/:modified deletes an asset', async function () { - await request(app) - .delete('/api/assets/' + asset1.stix.id + '/modified/' + asset1.stix.modified) - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(204); - }); - - it('DELETE /api/assets/:id should delete all the assets with the same stix id', async function () { - await request(app) - .delete('/api/assets/' + asset2.stix.id) - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(204); - }); - - it('GET /api/assets returns an empty array of assets', async function () { - const res = await request(app) - .get('/api/assets') - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const assets = res.body; - expect(assets).toBeDefined(); - expect(Array.isArray(assets)).toBe(true); - expect(assets.length).toBe(0); - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + it('GET /api/assets returns an empty array of assets', async function () { + const res = await request(app) + .get('/api/assets') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const assets = res.body; + expect(assets).toBeDefined(); + expect(Array.isArray(assets)).toBe(true); + expect(assets.length).toBe(0); + }); + + it('POST /api/assets does not create an empty asset', async function () { + const body = {}; + await request(app) + .post('/api/assets') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + let asset1; + it('POST /api/assets creates an asset', async function () { + const timestamp = new Date().toISOString(); + initialObjectData.stix.created = timestamp; + initialObjectData.stix.modified = timestamp; + const body = initialObjectData; + const res = await request(app) + .post('/api/assets') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created asset + asset1 = res.body; + expect(asset1).toBeDefined(); + expect(asset1.stix).toBeDefined(); + expect(asset1.stix.id).toBeDefined(); + expect(asset1.stix.created).toBeDefined(); + expect(asset1.stix.modified).toBeDefined(); + expect(asset1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); + + expect(asset1.stix.x_mitre_sectors).toBeDefined(); + expect(Array.isArray(asset1.stix.x_mitre_sectors)).toBe(true); + expect(asset1.stix.x_mitre_sectors.length).toBe(1); + + expect(asset1.stix.x_mitre_related_assets).toBeDefined(); + expect(Array.isArray(asset1.stix.x_mitre_related_assets)).toBe(true); + expect(asset1.stix.x_mitre_related_assets.length).toBe(2); + }); + + it('GET /api/assets returns the added asset', async function () { + const res = await request(app) + .get('/api/assets') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one asset in an array + const assets = res.body; + expect(assets).toBeDefined(); + expect(Array.isArray(assets)).toBe(true); + expect(assets.length).toBe(1); + }); + + it('GET /api/assets/:id should not return an asset when the id cannot be found', async function () { + await request(app) + .get('/api/assets/not-an-id') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('GET /api/assets/:id?versions=all should not return an asset when the id cannot be found', async function () { + await request(app) + .get('/api/assets/not-an-id?versions=all') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('GET /api/assets/:id returns the added asset', async function () { + const res = await request(app) + .get('/api/assets/' + asset1.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one asset in an array + const assets = res.body; + expect(assets).toBeDefined(); + expect(Array.isArray(assets)).toBe(true); + expect(assets.length).toBe(1); + + const asset = assets[0]; + expect(asset).toBeDefined(); + expect(asset.stix).toBeDefined(); + expect(asset.stix.id).toBe(asset1.stix.id); + expect(asset.stix.type).toBe(asset1.stix.type); + expect(asset.stix.name).toBe(asset1.stix.name); + expect(asset.stix.description).toBe(asset1.stix.description); + expect(asset.stix.spec_version).toBe(asset1.stix.spec_version); + expect(asset.stix.object_marking_refs).toEqual( + expect.arrayContaining(asset1.stix.object_marking_refs), + ); + expect(asset.stix.created_by_ref).toBe(asset1.stix.created_by_ref); + expect(asset.stix.x_mitre_version).toBe(asset1.stix.x_mitre_version); + expect(asset.stix.x_mitre_attack_spec_version).toBe(asset1.stix.x_mitre_attack_spec_version); + + expect(asset.stix.x_mitre_sectors).toBeDefined(); + expect(Array.isArray(asset.stix.x_mitre_sectors)).toBe(true); + expect(asset.stix.x_mitre_sectors.length).toBe(1); + + expect(asset.stix.x_mitre_related_assets).toBeDefined(); + expect(Array.isArray(asset.stix.x_mitre_related_assets)).toBe(true); + expect(asset.stix.x_mitre_related_assets.length).toBe(2); + }); + + it('PUT /api/assets updates an asset', async function () { + const originalModified = asset1.stix.modified; + const timestamp = new Date().toISOString(); + asset1.stix.modified = timestamp; + asset1.stix.description = 'This is an updated asset.'; + const body = asset1; + const res = await request(app) + .put('/api/assets/' + asset1.stix.id + '/modified/' + originalModified) + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the updated asset + const asset = res.body; + expect(asset).toBeDefined(); + expect(asset.stix.id).toBe(asset1.stix.id); + expect(asset.stix.modified).toBe(asset1.stix.modified); + }); + + it('POST /api/assets does not create an asset with the same id and modified date', async function () { + const body = asset1; + await request(app) + .post('/api/assets') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(409); + }); + + let asset2; + it('POST /api/assets should create a new version of an asset with a duplicate stix.id but different stix.modified date', async function () { + asset2 = _.cloneDeep(asset1); + asset2._id = undefined; + asset2.__t = undefined; + asset2.__v = undefined; + const timestamp = new Date().toISOString(); + asset2.stix.modified = timestamp; + const body = asset2; + const res = await request(app) + .post('/api/assets') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created asset + const asset = res.body; + expect(asset).toBeDefined(); + }); + + let asset3; + it('POST /api/assets should create a new version of an asset with a duplicate stix.id but different stix.modified date', async function () { + asset3 = _.cloneDeep(asset1); + asset3._id = undefined; + asset3.__t = undefined; + asset3.__v = undefined; + const timestamp = new Date().toISOString(); + asset3.stix.modified = timestamp; + const body = asset3; + const res = await request(app) + .post('/api/assets') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created asset + const asset = res.body; + expect(asset).toBeDefined(); + }); + + it('GET /api/assets returns the latest added asset', async function () { + const res = await request(app) + .get('/api/assets/' + asset3.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one asset in an array + const assets = res.body; + expect(assets).toBeDefined(); + expect(Array.isArray(assets)).toBe(true); + expect(assets.length).toBe(1); + const asset = assets[0]; + expect(asset.stix.id).toBe(asset3.stix.id); + expect(asset.stix.modified).toBe(asset3.stix.modified); + }); + + it('GET /api/assets returns all added assets', async function () { + const res = await request(app) + .get('/api/assets/' + asset1.stix.id + '?versions=all') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get two assets in an array + const assets = res.body; + expect(assets).toBeDefined(); + expect(Array.isArray(assets)).toBe(true); + expect(assets.length).toBe(3); + }); + + it('GET /api/assets/:id/modified/:modified returns the first added asset', async function () { + const res = await request(app) + .get('/api/assets/' + asset1.stix.id + '/modified/' + asset1.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one asset in an array + const asset = res.body; + expect(asset).toBeDefined(); + expect(asset.stix).toBeDefined(); + expect(asset.stix.id).toBe(asset1.stix.id); + expect(asset.stix.modified).toBe(asset1.stix.modified); + }); + + it('GET /api/assets/:id/modified/:modified returns the second added asset', async function () { + const res = await request(app) + .get('/api/assets/' + asset2.stix.id + '/modified/' + asset2.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one asset in an array + const asset = res.body; + expect(asset).toBeDefined(); + expect(asset.stix).toBeDefined(); + expect(asset.stix.id).toBe(asset2.stix.id); + expect(asset.stix.modified).toBe(asset2.stix.modified); + }); + + it('DELETE /api/assets/:id should not delete an asset when the id cannot be found', async function () { + await request(app) + .delete('/api/assets/not-an-id') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('DELETE /api/assets/:id/modified/:modified deletes an asset', async function () { + await request(app) + .delete('/api/assets/' + asset1.stix.id + '/modified/' + asset1.stix.modified) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('DELETE /api/assets/:id should delete all the assets with the same stix id', async function () { + await request(app) + .delete('/api/assets/' + asset2.stix.id) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('GET /api/assets returns an empty array of assets', async function () { + const res = await request(app) + .get('/api/assets') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const assets = res.body; + expect(assets).toBeDefined(); + expect(Array.isArray(assets)).toBe(true); + expect(assets.length).toBe(0); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/api/attack-objects/attack-objects-1.json b/app/tests/api/attack-objects/attack-objects-1.json index c8d3d5be..23274d29 100644 --- a/app/tests/api/attack-objects/attack-objects-1.json +++ b/app/tests/api/attack-objects/attack-objects-1.json @@ -14,9 +14,7 @@ "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "created": "2022-08-17T10:10:10.000Z", "modified": "2022-08-17T10:10:10.000Z", - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "x_mitre_contents": [ { "object_ref": "attack-pattern--757471d4-d931-4109-82dd-cdd50c04744e", @@ -89,15 +87,9 @@ ] }, { - "x_mitre_platforms": [ - "Linux" - ], - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_platforms": ["Linux"], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "type": "attack-pattern", "id": "attack-pattern--757471d4-d931-4109-82dd-cdd50c04744e", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -129,15 +121,9 @@ "spec_version": "2.1" }, { - "x_mitre_platforms": [ - "Linux" - ], - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_platforms": ["Linux"], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "type": "attack-pattern", "id": "attack-pattern--9ca3f5e5-697d-41cd-ade4-c846ec12a816", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -165,15 +151,9 @@ "spec_version": "2.1" }, { - "x_mitre_platforms": [ - "Linux" - ], - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_platforms": ["Linux"], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "type": "attack-pattern", "id": "attack-pattern--dcbb7acb-0817-4e4f-b170-198c27d2e178", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -201,15 +181,9 @@ "spec_version": "2.1" }, { - "x_mitre_platforms": [ - "Linux" - ], - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_platforms": ["Linux"], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "type": "attack-pattern", "id": "attack-pattern--bcdab08e-9332-47ce-87b5-0837ac1550fd", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -241,15 +215,9 @@ "spec_version": "2.1" }, { - "x_mitre_platforms": [ - "Linux" - ], - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_platforms": ["Linux"], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "type": "attack-pattern", "id": "attack-pattern--5f1d1b95-ca0e-4d6a-a4d0-fcb568641a74", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -281,12 +249,8 @@ "spec_version": "2.1" }, { - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created": "2022-04-01T06:07:08.000Z", "description": "Enlil, ancient Mesopotamian god of wind, air, earth, and storms.", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -308,12 +272,8 @@ "spec_version": "2.1" }, { - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created": "2022-04-01T06:07:08.000Z", "description": "Enki, ancient Mesopotamian god of water and crafts.", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -335,12 +295,8 @@ "spec_version": "2.1" }, { - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created": "2022-03-01T06:07:08.000Z", "description": "Inanna, ancient Mesopotamian god of love, war, and fertility.", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -362,12 +318,8 @@ "spec_version": "2.1" }, { - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created": "2022-02-01T06:07:08.000Z", "description": "Nabu, ancient Mesopotamian god of literacy and wisdom.", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -389,12 +341,8 @@ "spec_version": "2.1" }, { - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created": "2022-01-01T06:07:08.000Z", "description": "Nanna-Suen, ancient Mesopotamian god of cattle.", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -416,12 +364,8 @@ "spec_version": "2.1" }, { - "x_mitre_domains": [ - "mobile-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_domains": ["mobile-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created": "2022-09-09T01:02:03.000Z", "description": "Nabu, ancient Mesopotamian god of mobile scribes. This tactic has the same x-mitre-shortname as another tactic, but a different domain.", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -450,25 +394,20 @@ "description": "This is a malware type of software.", "is_family": false, "external_references": [ - { "source_name": "mitre-attack", "external_id": "SX3333", "url": "https://attack.mitre.org/software/SX3333" }, + { + "source_name": "mitre-attack", + "external_id": "SX3333", + "url": "https://attack.mitre.org/software/SX3333" + }, { "source_name": "source-1", "external_id": "s1" } ], - "object_marking_refs": [ "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" ], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "x_mitre_version": "1.1", - "x_mitre_aliases": [ - "software-1" - ], - "x_mitre_platforms": [ - "platform-1" - ], - "x_mitre_contributors": [ - "contributor-1", - "contributor-2" - ], - "x_mitre_domains": [ - "mobile-attack" - ], + "x_mitre_aliases": ["software-1"], + "x_mitre_platforms": ["platform-1"], + "x_mitre_contributors": ["contributor-1", "contributor-2"], + "x_mitre_domains": ["mobile-attack"], "created": "2022-06-01T00:00:00.000Z", "modified": "2022-06-01T00:00:00.000Z" }, @@ -479,10 +418,14 @@ "type": "intrusion-set", "description": "This is a group.", "external_references": [ - { "source_name": "mitre-attack", "external_id": "GX1111", "url": "https://attack.mitre.org/groups/GX1111" }, + { + "source_name": "mitre-attack", + "external_id": "GX1111", + "url": "https://attack.mitre.org/groups/GX1111" + }, { "source_name": "source-1", "external_id": "s1" } ], - "object_marking_refs": [ "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" ], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "created": "2022-06-01T00:00:00.000Z", "modified": "2022-06-01T00:00:00.000Z" @@ -494,10 +437,14 @@ "type": "course-of-action", "description": "This is a mitigation.", "external_references": [ - { "source_name": "mitre-attack", "external_id": "T9999", "url": "https://attack.mitre.org/mitigations/T9999" }, + { + "source_name": "mitre-attack", + "external_id": "T9999", + "url": "https://attack.mitre.org/mitigations/T9999" + }, { "source_name": "source-1", "external_id": "s1" } ], - "object_marking_refs": [ "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" ], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "x_mitre_version": "1.1", "created": "2022-06-01T00:00:00.000Z", @@ -510,15 +457,13 @@ "relationship_type": "uses", "source_ref": "intrusion-set--925216d2-dd4c-4487-8d19-f96e81dabd5d", "target_ref": "attack-pattern--757471d4-d931-4109-82dd-cdd50c04744e", - "object_marking_refs": [ "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" ], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "created": "2022-06-01T00:00:00.000Z", "modified": "2022-06-01T00:00:00.000Z" }, { - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "id": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "type": "identity", "identity_class": "organization", @@ -540,9 +485,7 @@ "definition_type": "statement", "x_mitre_attack_spec_version": "2.1.0", "spec_version": "2.1", - "x_mitre_domains": [ - "ics-attack" - ] + "x_mitre_domains": ["ics-attack"] } ] } diff --git a/app/tests/api/attack-objects/attack-objects-2.json b/app/tests/api/attack-objects/attack-objects-2.json index 78a15280..5c0b7e5f 100644 --- a/app/tests/api/attack-objects/attack-objects-2.json +++ b/app/tests/api/attack-objects/attack-objects-2.json @@ -14,9 +14,7 @@ "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "created": "2022-08-17T10:10:10.000Z", "modified": "2022-08-18T10:10:10.000Z", - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "x_mitre_contents": [ { "object_ref": "attack-pattern--757471d4-d931-4109-82dd-cdd50c04744e", @@ -33,15 +31,9 @@ ] }, { - "x_mitre_platforms": [ - "Linux" - ], - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_platforms": ["Linux"], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "type": "attack-pattern", "id": "attack-pattern--757471d4-d931-4109-82dd-cdd50c04744e", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -73,9 +65,7 @@ "spec_version": "2.1" }, { - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "id": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "type": "identity", "identity_class": "organization", @@ -97,9 +87,7 @@ "definition_type": "statement", "x_mitre_attack_spec_version": "2.1.0", "spec_version": "2.1", - "x_mitre_domains": [ - "ics-attack" - ] + "x_mitre_domains": ["ics-attack"] } ] } diff --git a/app/tests/api/attack-objects/attack-objects-pagination.spec.js b/app/tests/api/attack-objects/attack-objects-pagination.spec.js index 86823d1d..482cafb1 100644 --- a/app/tests/api/attack-objects/attack-objects-pagination.spec.js +++ b/app/tests/api/attack-objects/attack-objects-pagination.spec.js @@ -4,38 +4,34 @@ const PaginationTests = require('../../shared/pagination'); // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - spec_version: '2.1', - type: 'attack-pattern', - description: 'This is a technique.', - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - kill_chain_phases: [ - { kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' } - ], - x_mitre_data_sources: [ 'data-source-1', 'data-source-2' ], - x_mitre_detection: 'detection text', - x_mitre_is_subtechnique: false, - x_mitre_impact_type: [ 'impact-1' ], - x_mitre_platforms: [ 'platform-1', 'platform-2' ] - } + }, + stix: { + spec_version: '2.1', + type: 'attack-pattern', + description: 'This is a technique.', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + kill_chain_phases: [{ kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' }], + x_mitre_data_sources: ['data-source-1', 'data-source-2'], + x_mitre_detection: 'detection text', + x_mitre_is_subtechnique: false, + x_mitre_impact_type: ['impact-1'], + x_mitre_platforms: ['platform-1', 'platform-2'], + }, }; // Use the techniques service for creating objects, but the attack-objects API for retrieving them // Include the state so that the placeholder organization identity isn't retrieved (which would throw off the numbers) const options = { - prefix: 'attack-pattern', - baseUrl: '/api/attack-objects', - label: 'Attack Objects', - state: 'work-in-progress' -} + prefix: 'attack-pattern', + baseUrl: '/api/attack-objects', + label: 'Attack Objects', + state: 'work-in-progress', +}; const paginationTests = new PaginationTests(techniquesService, initialObjectData, options); paginationTests.executeTests(); diff --git a/app/tests/api/attack-objects/attack-objects.spec.js b/app/tests/api/attack-objects/attack-objects.spec.js index 9d089af1..e1072fe5 100644 --- a/app/tests/api/attack-objects/attack-objects.spec.js +++ b/app/tests/api/attack-objects/attack-objects.spec.js @@ -9,274 +9,257 @@ const AttackObject = require('../../../models/attack-object-model'); const login = require('../../shared/login'); const logger = require('../../../lib/logger'); -const util = require("util"); -const collectionBundlesService = require("../../../services/collection-bundles-service"); +const util = require('util'); +const collectionBundlesService = require('../../../services/collection-bundles-service'); logger.level = 'debug'; // test malware object const malwareObject = { - workspace: { - workflow: { - state: 'work-in-progress', - }, + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - id: "malware--1c1ab115-f015-462c-92a0-f887277d8519", - name: "software-2", - "spec_version": "2.1", - type: "malware", - description: "This is a malware type of software.", - is_family: false, - object_marking_refs: [ "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" ], - x_mitre_version: "1.1", - x_mitre_contributors: [ - "contributor-mk", - "contributor-cm" - ], - x_mitre_domains: [ - "mobile-attack" - ], - created: "2023-03-01T00:00:00.000Z", - modified: "2023-03-01T00:00:00.000Z", - } + }, + stix: { + id: 'malware--1c1ab115-f015-462c-92a0-f887277d8519', + name: 'software-2', + spec_version: '2.1', + type: 'malware', + description: 'This is a malware type of software.', + is_family: false, + object_marking_refs: ['marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce'], + x_mitre_version: '1.1', + x_mitre_contributors: ['contributor-mk', 'contributor-cm'], + x_mitre_domains: ['mobile-attack'], + created: '2023-03-01T00:00:00.000Z', + modified: '2023-03-01T00:00:00.000Z', + }, }; async function readJson(path) { - const data = await fs.readFile(require.resolve(path)); - return JSON.parse(data); + const data = await fs.readFile(require.resolve(path)); + return JSON.parse(data); } describe('ATT&CK Objects API', function () { - let app; - let passportCookie; - - const importBundle = util.promisify(collectionBundlesService.importBundle); - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Wait until the indexes are created - await AttackObject.init(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - const collectionBundle1 = await readJson('./attack-objects-1.json'); - const collectionList1 = collectionBundle1.objects.filter(object => object.type === 'x-mitre-collection'); - - const importOptions = {}; - await importBundle(collectionList1[0], collectionBundle1, importOptions); - - const collectionBundle2 = await readJson('./attack-objects-2.json'); - const collectionList2 = collectionBundle2.objects.filter(object => object.type === 'x-mitre-collection'); - - await importBundle(collectionList2[0], collectionBundle2, importOptions); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - it('GET /api/attack-objects returns the ATT&CK objects imported from the collection bundles', async function () { - const res = await request(app) - .get('/api/attack-objects') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get ATT&CK objects in an array - const attackObjects = res.body; - expect(attackObjects).toBeDefined(); - expect(Array.isArray(attackObjects)).toBe(true); - - // We expect the placeholder identity and the new collection identity - const identities = attackObjects.filter(x => x.stix.type === 'identity'); - expect(identities.length).toBe(2); - - // We expect the 4 TLP marking definitions and the new collection identity - const markingDefinitions = attackObjects.filter(x => x.stix.type === 'marking-definition'); - expect(markingDefinitions.length).toBe(5); - - // Placeholder identity, 4 TLP marking definitions, 17 collection contents, 1 collection object - expect(attackObjects.length).toBe(1 + 4 + 17 + 1); - - }); - - it('GET /api/attack-objects returns all versions of the ATT&CK objects imported from the collection bundles', async function () { - const res = await request(app) - .get('/api/attack-objects?versions=all') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get ATT&CK objects in an array - const attackObjects = res.body; - expect(attackObjects).toBeDefined(); - expect(Array.isArray(attackObjects)).toBe(true); - - // We expect the placeholder identity and the new collection identity - const identities = attackObjects.filter(x => x.stix.type === 'identity'); - expect(identities.length).toBe(2); - - // We expect the 4 TLP marking definitions and the new collection identity - const markingDefinitions = attackObjects.filter(x => x.stix.type === 'marking-definition'); - expect(markingDefinitions.length).toBe(5); - - // Placeholder identity, 4 TLP marking definitions, 18 collection contents, 2 collection objects - expect(attackObjects.length).toBe(1 + 4 + 18 + 2); - - }); - - it('GET /api/attack-objects returns zero objects with an ATT&CK ID that does not exist', async function () { - const res = await request(app) - .get('/api/attack-objects?attackId=T1234') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const attackObjects = res.body; - expect(attackObjects).toBeDefined(); - expect(Array.isArray(attackObjects)).toBe(true); - expect(attackObjects.length).toBe(0); - - }); - - it('GET /api/attack-objects returns the group with ATT&CK ID GX1111', async function () { - const res = await request(app) - .get('/api/attack-objects?attackId=GX1111') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the matching group object - const attackObjects = res.body; - expect(attackObjects).toBeDefined(); - expect(Array.isArray(attackObjects)).toBe(true); - expect(attackObjects.length).toBe(1); - - - }); - - it('GET /api/attack-objects returns the software with ATT&CK ID SX3333', async function () { - const res = await request(app) - .get('/api/attack-objects?attackId=SX3333') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the matching software object - const attackObjects = res.body; - expect(attackObjects).toBeDefined(); - expect(Array.isArray(attackObjects)).toBe(true); - expect(attackObjects.length).toBe(1); - - - }); - - it('GET /api/attack-objects returns the technique with ATT&CK ID TX0001', async function () { - const res = await request(app) - .get('/api/attack-objects?attackId=TX0001') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the matching objects - const attackObjects = res.body; - expect(attackObjects).toBeDefined(); - expect(Array.isArray(attackObjects)).toBe(true); - expect(attackObjects.length).toBe(1); - - - }); - - it('GET /api/attack-objects returns the objects with the requested ATT&CK IDs', async function () { - const res = await request(app) - .get('/api/attack-objects?attackId=GX1111&attackId=SX3333&attackId=TX0001') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the matching objects - const attackObjects = res.body; - expect(attackObjects).toBeDefined(); - expect(Array.isArray(attackObjects)).toBe(true); - expect(attackObjects.length).toBe(3); - - - }); - - it('GET /api/attack-objects uses the search parameter to return the tactic objects', async function () { - const res = await request(app) - .get('/api/attack-objects?search=nabu') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the matching objects - const attackObjects = res.body; - expect(attackObjects).toBeDefined(); - expect(Array.isArray(attackObjects)).toBe(true); - expect(attackObjects.length).toBe(2); - expect(attackObjects[0].stix.type).toBe('x-mitre-tactic'); - expect(attackObjects[1].stix.type).toBe('x-mitre-tactic'); - - - }); - - let software1; - it('POST /api/software creates a software', async function () { - // Further setup - need to index malware object with in database first - const body = malwareObject; - const res = await request(app) - .post('/api/software') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - software1 = res.body; - expect(software1).toBeDefined(); - expect(software1.stix).toBeDefined(); - expect(software1.stix.id).toBeDefined(); - expect(software1.stix.created).toBeDefined(); - expect(software1.stix.modified).toBeDefined(); - - - }); - - it('GET /api/attack-objects uses the users parameter to return objects by user identity', async function () { - const res = await request(app) - .get(`/api/attack-objects?lastUpdatedBy=${software1.workspace.workflow.created_by_user_account}`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the matching objects - const attackObjects = res.body; - expect(attackObjects).toBeDefined(); - expect(Array.isArray(attackObjects)).toBe(true); - expect(attackObjects.length).toBe(1); - expect(attackObjects[0].stix.type).toBe('malware'); - - - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let passportCookie; + + const importBundle = util.promisify(collectionBundlesService.importBundle); + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Wait until the indexes are created + await AttackObject.init(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + const collectionBundle1 = await readJson('./attack-objects-1.json'); + const collectionList1 = collectionBundle1.objects.filter( + (object) => object.type === 'x-mitre-collection', + ); + + const importOptions = {}; + await importBundle(collectionList1[0], collectionBundle1, importOptions); + + const collectionBundle2 = await readJson('./attack-objects-2.json'); + const collectionList2 = collectionBundle2.objects.filter( + (object) => object.type === 'x-mitre-collection', + ); + + await importBundle(collectionList2[0], collectionBundle2, importOptions); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + it('GET /api/attack-objects returns the ATT&CK objects imported from the collection bundles', async function () { + const res = await request(app) + .get('/api/attack-objects') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get ATT&CK objects in an array + const attackObjects = res.body; + expect(attackObjects).toBeDefined(); + expect(Array.isArray(attackObjects)).toBe(true); + + // We expect the placeholder identity and the new collection identity + const identities = attackObjects.filter((x) => x.stix.type === 'identity'); + expect(identities.length).toBe(2); + + // We expect the 4 TLP marking definitions and the new collection identity + const markingDefinitions = attackObjects.filter((x) => x.stix.type === 'marking-definition'); + expect(markingDefinitions.length).toBe(5); + + // Placeholder identity, 4 TLP marking definitions, 17 collection contents, 1 collection object + expect(attackObjects.length).toBe(1 + 4 + 17 + 1); + }); + + it('GET /api/attack-objects returns all versions of the ATT&CK objects imported from the collection bundles', async function () { + const res = await request(app) + .get('/api/attack-objects?versions=all') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get ATT&CK objects in an array + const attackObjects = res.body; + expect(attackObjects).toBeDefined(); + expect(Array.isArray(attackObjects)).toBe(true); + + // We expect the placeholder identity and the new collection identity + const identities = attackObjects.filter((x) => x.stix.type === 'identity'); + expect(identities.length).toBe(2); + + // We expect the 4 TLP marking definitions and the new collection identity + const markingDefinitions = attackObjects.filter((x) => x.stix.type === 'marking-definition'); + expect(markingDefinitions.length).toBe(5); + + // Placeholder identity, 4 TLP marking definitions, 18 collection contents, 2 collection objects + expect(attackObjects.length).toBe(1 + 4 + 18 + 2); + }); + + it('GET /api/attack-objects returns zero objects with an ATT&CK ID that does not exist', async function () { + const res = await request(app) + .get('/api/attack-objects?attackId=T1234') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const attackObjects = res.body; + expect(attackObjects).toBeDefined(); + expect(Array.isArray(attackObjects)).toBe(true); + expect(attackObjects.length).toBe(0); + }); + + it('GET /api/attack-objects returns the group with ATT&CK ID GX1111', async function () { + const res = await request(app) + .get('/api/attack-objects?attackId=GX1111') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the matching group object + const attackObjects = res.body; + expect(attackObjects).toBeDefined(); + expect(Array.isArray(attackObjects)).toBe(true); + expect(attackObjects.length).toBe(1); + }); + + it('GET /api/attack-objects returns the software with ATT&CK ID SX3333', async function () { + const res = await request(app) + .get('/api/attack-objects?attackId=SX3333') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the matching software object + const attackObjects = res.body; + expect(attackObjects).toBeDefined(); + expect(Array.isArray(attackObjects)).toBe(true); + expect(attackObjects.length).toBe(1); + }); + + it('GET /api/attack-objects returns the technique with ATT&CK ID TX0001', async function () { + const res = await request(app) + .get('/api/attack-objects?attackId=TX0001') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the matching objects + const attackObjects = res.body; + expect(attackObjects).toBeDefined(); + expect(Array.isArray(attackObjects)).toBe(true); + expect(attackObjects.length).toBe(1); + }); + + it('GET /api/attack-objects returns the objects with the requested ATT&CK IDs', async function () { + const res = await request(app) + .get('/api/attack-objects?attackId=GX1111&attackId=SX3333&attackId=TX0001') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the matching objects + const attackObjects = res.body; + expect(attackObjects).toBeDefined(); + expect(Array.isArray(attackObjects)).toBe(true); + expect(attackObjects.length).toBe(3); + }); + + it('GET /api/attack-objects uses the search parameter to return the tactic objects', async function () { + const res = await request(app) + .get('/api/attack-objects?search=nabu') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the matching objects + const attackObjects = res.body; + expect(attackObjects).toBeDefined(); + expect(Array.isArray(attackObjects)).toBe(true); + expect(attackObjects.length).toBe(2); + expect(attackObjects[0].stix.type).toBe('x-mitre-tactic'); + expect(attackObjects[1].stix.type).toBe('x-mitre-tactic'); + }); + + let software1; + it('POST /api/software creates a software', async function () { + // Further setup - need to index malware object with in database first + const body = malwareObject; + const res = await request(app) + .post('/api/software') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + software1 = res.body; + expect(software1).toBeDefined(); + expect(software1.stix).toBeDefined(); + expect(software1.stix.id).toBeDefined(); + expect(software1.stix.created).toBeDefined(); + expect(software1.stix.modified).toBeDefined(); + }); + + it('GET /api/attack-objects uses the users parameter to return objects by user identity', async function () { + const res = await request(app) + .get( + `/api/attack-objects?lastUpdatedBy=${software1.workspace.workflow.created_by_user_account}`, + ) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the matching objects + const attackObjects = res.body; + expect(attackObjects).toBeDefined(); + expect(Array.isArray(attackObjects)).toBe(true); + expect(attackObjects.length).toBe(1); + expect(attackObjects[0].stix.type).toBe('malware'); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/api/campaigns/campaigns.spec.js b/app/tests/api/campaigns/campaigns.spec.js index e1869f73..c70970c4 100644 --- a/app/tests/api/campaigns/campaigns.spec.js +++ b/app/tests/api/campaigns/campaigns.spec.js @@ -17,461 +17,458 @@ logger.level = 'debug'; // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - name: 'campaign-1', - spec_version: '2.1', - type: 'campaign', - description: 'This is the initial campaign. Blue.', - aliases: [ 'campaign by another name' ], - first_seen: '2016-04-06T00:00:00.000Z', - last_seen: '2016-07-12T00:00:00.000Z', - x_mitre_first_seen_citation: '(Citation: Blue Spotter 1)', - x_mitre_last_seen_citation: '(Citation: Blue Spotter 2)', - created_by_ref: "identity--6444f546-6900-4456-b3b1-015c88d70dab" - } + }, + stix: { + name: 'campaign-1', + spec_version: '2.1', + type: 'campaign', + description: 'This is the initial campaign. Blue.', + aliases: ['campaign by another name'], + first_seen: '2016-04-06T00:00:00.000Z', + last_seen: '2016-07-12T00:00:00.000Z', + x_mitre_first_seen_citation: '(Citation: Blue Spotter 1)', + x_mitre_last_seen_citation: '(Citation: Blue Spotter 2)', + created_by_ref: 'identity--6444f546-6900-4456-b3b1-015c88d70dab', + }, }; const markingDefinitionData = { - workspace: { - workflow: { - state: 'reviewed' - } + workspace: { + workflow: { + state: 'reviewed', }, - stix: { - spec_version: '2.1', - type: 'marking-definition', - definition_type: 'statement', - definition: { statement: 'This is a marking definition.' }, - created_by_ref: "identity--6444f546-6900-4456-b3b1-015c88d70dab" - } + }, + stix: { + spec_version: '2.1', + type: 'marking-definition', + definition_type: 'statement', + definition: { statement: 'This is a marking definition.' }, + created_by_ref: 'identity--6444f546-6900-4456-b3b1-015c88d70dab', + }, }; async function addDefaultMarkingDefinition(markingDefinitionData) { - // Save the marking definition - const timestamp = new Date().toISOString(); - markingDefinitionData.stix.created = timestamp; - const savedMarkingDefinition = await markingDefinitionService.create(markingDefinitionData); + // Save the marking definition + const timestamp = new Date().toISOString(); + markingDefinitionData.stix.created = timestamp; + const savedMarkingDefinition = await markingDefinitionService.create(markingDefinitionData); - // Get the current list of default marking definitions - const defaultMarkingDefinitions = await systemConfigurationService.retrieveDefaultMarkingDefinitions({ refOnly: true }); + // Get the current list of default marking definitions + const defaultMarkingDefinitions = + await systemConfigurationService.retrieveDefaultMarkingDefinitions({ refOnly: true }); - // Add the new marking definition to the list and save it - defaultMarkingDefinitions.push(savedMarkingDefinition.stix.id); - await systemConfigurationService.setDefaultMarkingDefinitions(defaultMarkingDefinitions); + // Add the new marking definition to the list and save it + defaultMarkingDefinitions.push(savedMarkingDefinition.stix.id); + await systemConfigurationService.setDefaultMarkingDefinitions(defaultMarkingDefinitions); - return savedMarkingDefinition; + return savedMarkingDefinition; } describe('Campaigns API', function () { - let app; - let defaultMarkingDefinition1; - let defaultMarkingDefinition2; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Wait until the indexes are created - await Campaign.init(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - - defaultMarkingDefinition1 = await addDefaultMarkingDefinition(markingDefinitionData); - }); - - it('GET /api/campaigns returns an empty array of campaigns', async function () { - const res = await request(app) - .get('/api/campaigns') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const campaigns = res.body; - expect(campaigns).toBeDefined(); - expect(Array.isArray(campaigns)).toBe(true); - expect(campaigns.length).toBe(0); - - }); - - it('POST /api/campaigns does not create an empty campaign', async function () { - const body = { }; - await request(app) - .post('/api/campaigns') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - let campaign1; - it('POST /api/campaigns creates a campaign', async function () { - const timestamp = new Date().toISOString(); - initialObjectData.stix.created = timestamp; - initialObjectData.stix.modified = timestamp; - const body = initialObjectData; - const res = await request(app) - .post('/api/campaigns') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created campaign - campaign1 = res.body; - expect(campaign1).toBeDefined(); - expect(campaign1.stix).toBeDefined(); - expect(campaign1.stix.id).toBeDefined(); - expect(campaign1.stix.created).toBeDefined(); - expect(campaign1.stix.modified).toBeDefined(); - expect(campaign1.stix.first_seen).toBeDefined(); - expect(campaign1.stix.last_seen).toBeDefined(); - expect(campaign1.stix.x_mitre_first_seen_citation).toBeDefined(); - expect(campaign1.stix.x_mitre_last_seen_citation).toBeDefined(); - expect(campaign1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); - - expect(campaign1.stix.aliases).toBeDefined(); - expect(Array.isArray(campaign1.stix.aliases)).toBe(true); - expect(campaign1.stix.aliases.length).toBe(1); - - // object_marking_refs should contain the default marking definition - expect(campaign1.stix.object_marking_refs).toBeDefined(); - expect(Array.isArray(campaign1.stix.object_marking_refs)).toBe(true); - expect(campaign1.stix.object_marking_refs.length).toBe(1); - expect(campaign1.stix.object_marking_refs[0]).toBe(defaultMarkingDefinition1.stix.id); - - }); - - it('GET /api/campaigns returns the added campaign', async function () { - const res = await request(app) - .get('/api/campaigns') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one campaign in an array - const campaigns = res.body; - expect(campaigns).toBeDefined(); - expect(Array.isArray(campaigns)).toBe(true); - expect(campaigns.length).toBe(1); - }); - - it('GET /api/campaigns/:id should not return a campaign when the id cannot be found', async function () { - await request(app) - .get('/api/campaigns/not-an-id') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('GET /api/campaigns/:id returns the added campaign', async function () { - const res = await request(app) - .get('/api/campaigns/' + campaign1.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one campaign in an array - const campaigns = res.body; - expect(campaigns).toBeDefined(); - expect(Array.isArray(campaigns)).toBe(true); - expect(campaigns.length).toBe(1); - - const campaign = campaigns[0]; - expect(campaign).toBeDefined(); - expect(campaign.stix).toBeDefined(); - expect(campaign.stix.id).toBe(campaign1.stix.id); - expect(campaign.stix.type).toBe(campaign1.stix.type); - expect(campaign.stix.name).toBe(campaign1.stix.name); - expect(campaign.stix.description).toBe(campaign1.stix.description); - expect(campaign.stix.spec_version).toBe(campaign1.stix.spec_version); - expect(campaign.stix.object_marking_refs).toEqual(expect.arrayContaining(campaign1.stix.object_marking_refs)); - expect(campaign.stix.created_by_ref).toBe(campaign1.stix.created_by_ref); - expect(campaign.stix.x_mitre_attack_spec_version).toBe(campaign1.stix.x_mitre_attack_spec_version); - }); - - it('PUT /api/campaigns updates a campaign', async function () { - const originalModified = campaign1.stix.modified; - const timestamp = new Date().toISOString(); - campaign1.stix.modified = timestamp; - campaign1.stix.description = 'This is an updated campaign. Blue.' - const body = campaign1; - const res = await request(app) - .put('/api/campaigns/' + campaign1.stix.id + '/modified/' + originalModified) - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the updated campaign - const campaign = res.body; - expect(campaign).toBeDefined(); - expect(campaign.stix.id).toBe(campaign1.stix.id); - expect(campaign.stix.modified).toBe(campaign1.stix.modified); - - }); - - it('POST /api/campaigns does not create a campaign with the same id and modified date', async function () { - const body = campaign1; - await request(app) - .post('/api/campaigns') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(409); - }); - - let campaign2; - it('POST /api/campaigns should create a new version of a campaign with a duplicate stix.id but different stix.modified date', async function () { - // Add another default marking definition - markingDefinitionData.stix.definition.statement = 'This is the second default marking definition'; - defaultMarkingDefinition2 = await addDefaultMarkingDefinition(markingDefinitionData); - - campaign2 = _.cloneDeep(campaign1); - campaign2._id = undefined; - campaign2.__t = undefined; - campaign2.__v = undefined; - const timestamp = new Date().toISOString(); - campaign2.stix.modified = timestamp; - campaign2.stix.description = 'This is a new version of a campaign. Green.'; - - const body = campaign2; - const res = await request(app) - .post('/api/campaigns') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created campaign - const campaign = res.body; - expect(campaign).toBeDefined(); - }); - - it('GET /api/campaigns returns the latest added campaign', async function () { - const res = await request(app) - .get('/api/campaigns/' + campaign2.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one campaign in an array - const campaigns = res.body; - expect(campaigns).toBeDefined(); - expect(Array.isArray(campaigns)).toBe(true); - expect(campaigns.length).toBe(1); - const campaign = campaigns[0]; - expect(campaign.stix.id).toBe(campaign2.stix.id); - expect(campaign.stix.modified).toBe(campaign2.stix.modified); - - // object_marking_refs should contain the two default marking definition - expect(campaign.stix.object_marking_refs).toBeDefined(); - expect(Array.isArray(campaign.stix.object_marking_refs)).toBe(true); - expect(campaign.stix.object_marking_refs.length).toBe(2); - expect(campaign.stix.object_marking_refs.includes(defaultMarkingDefinition1.stix.id)).toBe(true); - expect(campaign.stix.object_marking_refs.includes(defaultMarkingDefinition2.stix.id)).toBe(true); - - }); - - it('GET /api/campaigns returns all added campaigns', async function () { - const res = await request(app) - .get('/api/campaigns/' + campaign1.stix.id + '?versions=all') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get two campaigns in an array - const campaigns = res.body; - expect(campaigns).toBeDefined(); - expect(Array.isArray(campaigns)).toBe(true); - expect(campaigns.length).toBe(2); - - }); - - it('GET /api/campaigns/:id/modified/:modified returns the first added campaign', async function () { - const res = await request(app) - .get('/api/campaigns/' + campaign1.stix.id + '/modified/' + campaign1.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one campaign - const campaign = res.body; - expect(campaign).toBeDefined(); - expect(campaign.stix).toBeDefined(); - expect(campaign.stix.id).toBe(campaign1.stix.id); - expect(campaign.stix.modified).toBe(campaign1.stix.modified); - - }); - - it('GET /api/campaigns/:id/modified/:modified returns the second added campaign', async function () { - const res = await request(app) - .get('/api/campaigns/' + campaign2.stix.id + '/modified/' + campaign2.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one campaign - const campaign = res.body; - expect(campaign).toBeDefined(); - expect(campaign.stix).toBeDefined(); - expect(campaign.stix.id).toBe(campaign2.stix.id); - expect(campaign.stix.modified).toBe(campaign2.stix.modified); - - }); - - let campaign3; - it('POST /api/campaigns should create a new campaign with a different stix.id', async function () { - const campaign = _.cloneDeep(initialObjectData); - campaign._id = undefined; - campaign.__t = undefined; - campaign.__v = undefined; - campaign.stix.id = undefined; - const timestamp = new Date().toISOString(); - campaign.stix.created = timestamp; - campaign.stix.modified = timestamp; - campaign.stix.name = 'Mr. Brown'; - campaign.stix.description = 'This is a new campaign. Red.'; - const body = campaign; - const res = await request(app) - .post('/api/campaigns') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created campaign - campaign3 = res.body; - expect(campaign3).toBeDefined(); - - }); - - it('GET /api/campaigns uses the search parameter to return the latest version of the campaign', async function () { - const res = await request(app) - .get('/api/campaigns?search=green') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one campaign in an array - const campaigns = res.body; - expect(campaigns).toBeDefined(); - expect(Array.isArray(campaigns)).toBe(true); - expect(campaigns.length).toBe(1); - - // We expect it to be the latest version of the campaign - const campaign = campaigns[0]; - expect(campaign).toBeDefined(); - expect(campaign.stix).toBeDefined(); - expect(campaign.stix.id).toBe(campaign2.stix.id); - expect(campaign.stix.modified).toBe(campaign2.stix.modified); - - }); - - it('GET /api/campaigns should not get the first version of the campaign when using the search parameter', async function () { - const res = await request(app) - .get('/api/campaigns?search=blue') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get zero campaigns in an array - const campaigns = res.body; - expect(campaigns).toBeDefined(); - expect(Array.isArray(campaigns)).toBe(true); - expect(campaigns.length).toBe(0); - - }); - - it('GET /api/campaigns uses the search parameter to return the campaign using the name property', async function () { - const res = await request(app) - .get('/api/campaigns?search=brown') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one campaign in an array - const campaigns = res.body; - expect(campaigns).toBeDefined(); - expect(Array.isArray(campaigns)).toBe(true); - expect(campaigns.length).toBe(1); - - // We expect it to be the third campaign - const campaign = campaigns[0]; - expect(campaign).toBeDefined(); - expect(campaign.stix).toBeDefined(); - expect(campaign.stix.id).toBe(campaign3.stix.id); - expect(campaign.stix.modified).toBe(campaign3.stix.modified); - - }); - - it('DELETE /api/campaigns/:id should not delete a campaign when the id cannot be found', async function () { - await request(app) - .delete('/api/campaigns/not-an-id') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('DELETE /api/campaigns/:id/modified/:modified deletes a campaign', async function () { - await request(app) - .delete('/api/campaigns/' + campaign3.stix.id + '/modified/' + campaign3.stix.modified) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - }); - - it('DELETE /api/campaigns/:id should delete all of the campaigns with the stix id', async function () { - await request(app) - .delete('/api/campaigns/' + campaign2.stix.id) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - }); - - it('GET /api/campaigns returns an empty array of campaigns', async function () { - const res = await request(app) - .get('/api/campaigns') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const campaigns = res.body; - expect(campaigns).toBeDefined(); - expect(Array.isArray(campaigns)).toBe(true); - expect(campaigns.length).toBe(0); - - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let defaultMarkingDefinition1; + let defaultMarkingDefinition2; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Wait until the indexes are created + await Campaign.init(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + + defaultMarkingDefinition1 = await addDefaultMarkingDefinition(markingDefinitionData); + }); + + it('GET /api/campaigns returns an empty array of campaigns', async function () { + const res = await request(app) + .get('/api/campaigns') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const campaigns = res.body; + expect(campaigns).toBeDefined(); + expect(Array.isArray(campaigns)).toBe(true); + expect(campaigns.length).toBe(0); + }); + + it('POST /api/campaigns does not create an empty campaign', async function () { + const body = {}; + await request(app) + .post('/api/campaigns') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + let campaign1; + it('POST /api/campaigns creates a campaign', async function () { + const timestamp = new Date().toISOString(); + initialObjectData.stix.created = timestamp; + initialObjectData.stix.modified = timestamp; + const body = initialObjectData; + const res = await request(app) + .post('/api/campaigns') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created campaign + campaign1 = res.body; + expect(campaign1).toBeDefined(); + expect(campaign1.stix).toBeDefined(); + expect(campaign1.stix.id).toBeDefined(); + expect(campaign1.stix.created).toBeDefined(); + expect(campaign1.stix.modified).toBeDefined(); + expect(campaign1.stix.first_seen).toBeDefined(); + expect(campaign1.stix.last_seen).toBeDefined(); + expect(campaign1.stix.x_mitre_first_seen_citation).toBeDefined(); + expect(campaign1.stix.x_mitre_last_seen_citation).toBeDefined(); + expect(campaign1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); + + expect(campaign1.stix.aliases).toBeDefined(); + expect(Array.isArray(campaign1.stix.aliases)).toBe(true); + expect(campaign1.stix.aliases.length).toBe(1); + + // object_marking_refs should contain the default marking definition + expect(campaign1.stix.object_marking_refs).toBeDefined(); + expect(Array.isArray(campaign1.stix.object_marking_refs)).toBe(true); + expect(campaign1.stix.object_marking_refs.length).toBe(1); + expect(campaign1.stix.object_marking_refs[0]).toBe(defaultMarkingDefinition1.stix.id); + }); + + it('GET /api/campaigns returns the added campaign', async function () { + const res = await request(app) + .get('/api/campaigns') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one campaign in an array + const campaigns = res.body; + expect(campaigns).toBeDefined(); + expect(Array.isArray(campaigns)).toBe(true); + expect(campaigns.length).toBe(1); + }); + + it('GET /api/campaigns/:id should not return a campaign when the id cannot be found', async function () { + await request(app) + .get('/api/campaigns/not-an-id') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('GET /api/campaigns/:id returns the added campaign', async function () { + const res = await request(app) + .get('/api/campaigns/' + campaign1.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one campaign in an array + const campaigns = res.body; + expect(campaigns).toBeDefined(); + expect(Array.isArray(campaigns)).toBe(true); + expect(campaigns.length).toBe(1); + + const campaign = campaigns[0]; + expect(campaign).toBeDefined(); + expect(campaign.stix).toBeDefined(); + expect(campaign.stix.id).toBe(campaign1.stix.id); + expect(campaign.stix.type).toBe(campaign1.stix.type); + expect(campaign.stix.name).toBe(campaign1.stix.name); + expect(campaign.stix.description).toBe(campaign1.stix.description); + expect(campaign.stix.spec_version).toBe(campaign1.stix.spec_version); + expect(campaign.stix.object_marking_refs).toEqual( + expect.arrayContaining(campaign1.stix.object_marking_refs), + ); + expect(campaign.stix.created_by_ref).toBe(campaign1.stix.created_by_ref); + expect(campaign.stix.x_mitre_attack_spec_version).toBe( + campaign1.stix.x_mitre_attack_spec_version, + ); + }); + + it('PUT /api/campaigns updates a campaign', async function () { + const originalModified = campaign1.stix.modified; + const timestamp = new Date().toISOString(); + campaign1.stix.modified = timestamp; + campaign1.stix.description = 'This is an updated campaign. Blue.'; + const body = campaign1; + const res = await request(app) + .put('/api/campaigns/' + campaign1.stix.id + '/modified/' + originalModified) + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the updated campaign + const campaign = res.body; + expect(campaign).toBeDefined(); + expect(campaign.stix.id).toBe(campaign1.stix.id); + expect(campaign.stix.modified).toBe(campaign1.stix.modified); + }); + + it('POST /api/campaigns does not create a campaign with the same id and modified date', async function () { + const body = campaign1; + await request(app) + .post('/api/campaigns') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(409); + }); + + let campaign2; + it('POST /api/campaigns should create a new version of a campaign with a duplicate stix.id but different stix.modified date', async function () { + // Add another default marking definition + markingDefinitionData.stix.definition.statement = + 'This is the second default marking definition'; + defaultMarkingDefinition2 = await addDefaultMarkingDefinition(markingDefinitionData); + + campaign2 = _.cloneDeep(campaign1); + campaign2._id = undefined; + campaign2.__t = undefined; + campaign2.__v = undefined; + const timestamp = new Date().toISOString(); + campaign2.stix.modified = timestamp; + campaign2.stix.description = 'This is a new version of a campaign. Green.'; + + const body = campaign2; + const res = await request(app) + .post('/api/campaigns') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created campaign + const campaign = res.body; + expect(campaign).toBeDefined(); + }); + + it('GET /api/campaigns returns the latest added campaign', async function () { + const res = await request(app) + .get('/api/campaigns/' + campaign2.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one campaign in an array + const campaigns = res.body; + expect(campaigns).toBeDefined(); + expect(Array.isArray(campaigns)).toBe(true); + expect(campaigns.length).toBe(1); + const campaign = campaigns[0]; + expect(campaign.stix.id).toBe(campaign2.stix.id); + expect(campaign.stix.modified).toBe(campaign2.stix.modified); + + // object_marking_refs should contain the two default marking definition + expect(campaign.stix.object_marking_refs).toBeDefined(); + expect(Array.isArray(campaign.stix.object_marking_refs)).toBe(true); + expect(campaign.stix.object_marking_refs.length).toBe(2); + expect(campaign.stix.object_marking_refs.includes(defaultMarkingDefinition1.stix.id)).toBe( + true, + ); + expect(campaign.stix.object_marking_refs.includes(defaultMarkingDefinition2.stix.id)).toBe( + true, + ); + }); + + it('GET /api/campaigns returns all added campaigns', async function () { + const res = await request(app) + .get('/api/campaigns/' + campaign1.stix.id + '?versions=all') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get two campaigns in an array + const campaigns = res.body; + expect(campaigns).toBeDefined(); + expect(Array.isArray(campaigns)).toBe(true); + expect(campaigns.length).toBe(2); + }); + + it('GET /api/campaigns/:id/modified/:modified returns the first added campaign', async function () { + const res = await request(app) + .get('/api/campaigns/' + campaign1.stix.id + '/modified/' + campaign1.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one campaign + const campaign = res.body; + expect(campaign).toBeDefined(); + expect(campaign.stix).toBeDefined(); + expect(campaign.stix.id).toBe(campaign1.stix.id); + expect(campaign.stix.modified).toBe(campaign1.stix.modified); + }); + + it('GET /api/campaigns/:id/modified/:modified returns the second added campaign', async function () { + const res = await request(app) + .get('/api/campaigns/' + campaign2.stix.id + '/modified/' + campaign2.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one campaign + const campaign = res.body; + expect(campaign).toBeDefined(); + expect(campaign.stix).toBeDefined(); + expect(campaign.stix.id).toBe(campaign2.stix.id); + expect(campaign.stix.modified).toBe(campaign2.stix.modified); + }); + + let campaign3; + it('POST /api/campaigns should create a new campaign with a different stix.id', async function () { + const campaign = _.cloneDeep(initialObjectData); + campaign._id = undefined; + campaign.__t = undefined; + campaign.__v = undefined; + campaign.stix.id = undefined; + const timestamp = new Date().toISOString(); + campaign.stix.created = timestamp; + campaign.stix.modified = timestamp; + campaign.stix.name = 'Mr. Brown'; + campaign.stix.description = 'This is a new campaign. Red.'; + const body = campaign; + const res = await request(app) + .post('/api/campaigns') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created campaign + campaign3 = res.body; + expect(campaign3).toBeDefined(); + }); + + it('GET /api/campaigns uses the search parameter to return the latest version of the campaign', async function () { + const res = await request(app) + .get('/api/campaigns?search=green') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one campaign in an array + const campaigns = res.body; + expect(campaigns).toBeDefined(); + expect(Array.isArray(campaigns)).toBe(true); + expect(campaigns.length).toBe(1); + + // We expect it to be the latest version of the campaign + const campaign = campaigns[0]; + expect(campaign).toBeDefined(); + expect(campaign.stix).toBeDefined(); + expect(campaign.stix.id).toBe(campaign2.stix.id); + expect(campaign.stix.modified).toBe(campaign2.stix.modified); + }); + + it('GET /api/campaigns should not get the first version of the campaign when using the search parameter', async function () { + const res = await request(app) + .get('/api/campaigns?search=blue') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get zero campaigns in an array + const campaigns = res.body; + expect(campaigns).toBeDefined(); + expect(Array.isArray(campaigns)).toBe(true); + expect(campaigns.length).toBe(0); + }); + + it('GET /api/campaigns uses the search parameter to return the campaign using the name property', async function () { + const res = await request(app) + .get('/api/campaigns?search=brown') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one campaign in an array + const campaigns = res.body; + expect(campaigns).toBeDefined(); + expect(Array.isArray(campaigns)).toBe(true); + expect(campaigns.length).toBe(1); + + // We expect it to be the third campaign + const campaign = campaigns[0]; + expect(campaign).toBeDefined(); + expect(campaign.stix).toBeDefined(); + expect(campaign.stix.id).toBe(campaign3.stix.id); + expect(campaign.stix.modified).toBe(campaign3.stix.modified); + }); + + it('DELETE /api/campaigns/:id should not delete a campaign when the id cannot be found', async function () { + await request(app) + .delete('/api/campaigns/not-an-id') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('DELETE /api/campaigns/:id/modified/:modified deletes a campaign', async function () { + await request(app) + .delete('/api/campaigns/' + campaign3.stix.id + '/modified/' + campaign3.stix.modified) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('DELETE /api/campaigns/:id should delete all of the campaigns with the stix id', async function () { + await request(app) + .delete('/api/campaigns/' + campaign2.stix.id) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('GET /api/campaigns returns an empty array of campaigns', async function () { + const res = await request(app) + .get('/api/campaigns') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const campaigns = res.body; + expect(campaigns).toBeDefined(); + expect(Array.isArray(campaigns)).toBe(true); + expect(campaigns.length).toBe(0); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/api/collection-bundles/collection-bundles.spec.js b/app/tests/api/collection-bundles/collection-bundles.spec.js index dc3b4eab..0ab7ac42 100644 --- a/app/tests/api/collection-bundles/collection-bundles.spec.js +++ b/app/tests/api/collection-bundles/collection-bundles.spec.js @@ -19,250 +19,223 @@ const currentAttackSpecVersion = config.app.attackSpecVersion; const incrementedAttackSpecVersion = semver.inc(currentAttackSpecVersion, 'major'); const collectionBundleData = { - type: 'bundle', - id: 'bundle--0cde353c-ea5b-4668-9f68-971946609282', - spec_version: '2.1', - objects: [ + type: 'bundle', + id: 'bundle--0cde353c-ea5b-4668-9f68-971946609282', + spec_version: '2.1', + objects: [ + { + id: collectionId, + created: collectionTimestamp, + modified: collectionTimestamp, + name: 'collection-1', + spec_version: '2.1', + type: 'x-mitre-collection', + description: 'This is a collection.', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + x_mitre_contents: [ { - id: collectionId, - created: collectionTimestamp, - modified: collectionTimestamp, - name: 'collection-1', - spec_version: '2.1', - type: 'x-mitre-collection', - description: 'This is a collection.', - external_references: [ - {source_name: 'source-1', external_id: 's1'} - ], - object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - x_mitre_contents: [ - { - "object_ref": "attack-pattern--2204c371-6100-4ae0-82f3-25c07c29772a", - "object_modified": "2020-03-30T14:03:43.761Z" - }, - { - "object_ref": "attack-pattern--82f04b1e-5371-4a6f-be06-411f0f43b483", - "object_modified": "2019-02-03T16:56:41.200Z" - }, - { - "object_ref": "attack-pattern--14fbfb6a-c4d9-4c3b-a7ef-f8df23e3b22b", - "object_modified": "2019-02-22T16:56:41.200Z", - }, - { - "object_ref": "not-a-type--a29c7d3a-3836-4219-b3db-ff946ea2251b", - "object_modified": "2020-05-30T14:03:43.761Z" - }, - { - "object_ref": "course-of-action--25dc1ce8-eb55-4333-ae30-a7cb4f5894a1", - "object_modified": "2018-10-17T00:14:20.652Z" - }, - { - "object_ref": "intrusion-set--bef4c620-0787-42a8-a96d-b7eb6e85917c", - "object_modified": "2020-10-06T23:32:21.793Z" - }, - { - "object_ref": "malware--04227b24-7817-4de1-9050-b7b1b57f5866", - "object_modified": "2020-03-30T18:17:52.697Z" - }, - { - "object_ref": "intrusion-set--d69e568e-9ac8-4c08-b32c-d93b43ba9172", - "object_modified": "2020-03-30T19:25:56.012Z" - }, - { - "object_ref": "note--99e32abc-535e-4756-99a2-8296df2492ae", - "object_modified": "2021-04-01T08:08:08.888Z" - } - ] + object_ref: 'attack-pattern--2204c371-6100-4ae0-82f3-25c07c29772a', + object_modified: '2020-03-30T14:03:43.761Z', }, { - id: 'attack-pattern--2204c371-6100-4ae0-82f3-25c07c29772a', - created: '2020-03-30T14:03:43.761Z', - modified: '2020-03-30T14:03:43.761Z', - name: 'attack-pattern-1', - x_mitre_version: '1.0', - spec_version: '2.1', - type: 'attack-pattern', - description: 'This is a technique.', - external_references: [ - { source_name: 'source-1', external_id: 's1' }, - { source_name: 'attack-pattern-1 source', description: 'this is a source description'} - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - kill_chain_phases: [ - { kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' } - ], - x_mitre_data_sources: [ 'data-source-1', 'data-source-2' ], - x_mitre_detection: 'detection text', - x_mitre_is_subtechnique: false, - x_mitre_impact_type: [ 'impact-1' ], - x_mitre_platforms: [ 'platform-1', 'platform-2' ] + object_ref: 'attack-pattern--82f04b1e-5371-4a6f-be06-411f0f43b483', + object_modified: '2019-02-03T16:56:41.200Z', }, { - id: 'attack-pattern--82f04b1e-5371-4a6f-be06-411f0f43b483', - created: '2019-02-03T16:56:41.200Z', - modified: '2019-02-03T16:56:41.200Z', - name: 'attack-pattern-2', - x_mitre_version: '1.0', - spec_version: '2.1', - type: 'attack-pattern', - description: 'This is another technique.', - external_references: [ - { source_name: 'source-1', external_id: 's1' }, - { source_name: 'attack-pattern-2 source', description: 'this is a source description 2'} - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - kill_chain_phases: [ - { kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' } - ], - x_mitre_data_sources: [ 'data-source-1', 'data-source-2' ], - x_mitre_detection: 'detection text', - x_mitre_is_subtechnique: false, - x_mitre_impact_type: [ 'impact-1' ], - x_mitre_platforms: [ 'platform-1', 'platform-2' ] + object_ref: 'attack-pattern--14fbfb6a-c4d9-4c3b-a7ef-f8df23e3b22b', + object_modified: '2019-02-22T16:56:41.200Z', }, { - id: 'attack-pattern--14fbfb6a-c4d9-4c3b-a7ef-f8df23e3b22b', - created: '2019-02-22T16:56:41.200Z', - modified: '2019-02-22T16:56:41.200Z', - name: 'attack-pattern-2', - x_mitre_version: '1.0', - type: 'attack-pattern', - description: 'This is technique that is missing a spec_version.', - external_references: [ - { source_name: 'source-1', external_id: 's1' }, - { source_name: 'attack-pattern-2 source', description: 'this is a source description 2'} - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - kill_chain_phases: [ - { kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' } - ], - x_mitre_data_sources: [ 'data-source-1', 'data-source-2' ], - x_mitre_detection: 'detection text', - x_mitre_is_subtechnique: false, - x_mitre_impact_type: [ 'impact-1' ], - x_mitre_platforms: [ 'platform-1', 'platform-2' ] + object_ref: 'not-a-type--a29c7d3a-3836-4219-b3db-ff946ea2251b', + object_modified: '2020-05-30T14:03:43.761Z', }, { - id: 'not-a-type--a29c7d3a-3836-4219-b3db-ff946ea2251b', - created: '2020-05-30T14:03:43.761Z', - modified: '2020-05-30T14:03:43.761Z', - name: 'not-a-type-1', - x_mitre_version: '1.0', - spec_version: '2.1', - type: 'not-a-type', - description: 'This is a not a known STIX type.' + object_ref: 'course-of-action--25dc1ce8-eb55-4333-ae30-a7cb4f5894a1', + object_modified: '2018-10-17T00:14:20.652Z', }, { - id: "course-of-action--25dc1ce8-eb55-4333-ae30-a7cb4f5894a1", - type: "course-of-action", - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - name: "mitigation-1", - description: "This is a mitigation", - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - x_mitre_version: "1.0", - modified: "2018-10-17T00:14:20.652Z", - created: "2017-10-25T14:48:53.732Z", - spec_version: "2.1", - x_mitre_domains: [ - "domain-1" - ] + object_ref: 'intrusion-set--bef4c620-0787-42a8-a96d-b7eb6e85917c', + object_modified: '2020-10-06T23:32:21.793Z', }, { - id: "course-of-action--e944670c-d03a-4e93-a21c-b3d4c53ec4c9", - type: "course-of-action", - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - name: "mitigation-2", - description: "This is a mitigation that isn't in the contents", - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - x_mitre_version: "1.0", - modified: "2018-10-17T00:14:20.652Z", - created: "2017-10-25T14:48:53.732Z", - spec_version: "2.1", - x_mitre_domains: [ - "domain-1" - ] + object_ref: 'malware--04227b24-7817-4de1-9050-b7b1b57f5866', + object_modified: '2020-03-30T18:17:52.697Z', }, { - id: "malware--04227b24-7817-4de1-9050-b7b1b57f5866", - type: "malware", - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - name: "software-1", - description: "This is a software with an alias", - external_references: [ - { source_name: 'source-1', external_id: 's1' }, - { source_name: 'malware-1 source', description: 'this is a source description'}, - { source_name: 'xyzzy', description: '(Citation: Adventure 1975)'} - ], - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - x_mitre_version: "1.0", - modified: "2020-03-30T18:17:52.697Z", - created: "2017-10-25T14:48:53.732Z", - spec_version: "2.1", - x_mitre_domains: [ - "domain-1" - ], - x_mitre_aliases: [ - "xyzzy" - ], + object_ref: 'intrusion-set--d69e568e-9ac8-4c08-b32c-d93b43ba9172', + object_modified: '2020-03-30T19:25:56.012Z', }, { - type: "intrusion-set", - id: "intrusion-set--d69e568e-9ac8-4c08-b32c-d93b43ba9172", - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - name: "group-1", - description: "This is a group with an alias", - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - external_references: [ - { source_name: "source-2", external_id: "g1" }, - { source_name: "group source", description: "This is a group description" }, - { source_name: "group-xyzzy", description: "(Citation: Red 1999)", } - ], - aliases: [ - "group-xyzzy" - ], - modified: "2020-03-30T19:25:56.012Z", - created: "2018-10-17T00:14:20.652Z", - x_mitre_version: "1.0", - spec_version: "2.1", - x_mitre_domains: [ - "domain-1" - ] + object_ref: 'note--99e32abc-535e-4756-99a2-8296df2492ae', + object_modified: '2021-04-01T08:08:08.888Z', }, + ], + }, + { + id: 'attack-pattern--2204c371-6100-4ae0-82f3-25c07c29772a', + created: '2020-03-30T14:03:43.761Z', + modified: '2020-03-30T14:03:43.761Z', + name: 'attack-pattern-1', + x_mitre_version: '1.0', + spec_version: '2.1', + type: 'attack-pattern', + description: 'This is a technique.', + external_references: [ + { source_name: 'source-1', external_id: 's1' }, { - type: 'note', - id: 'note--99e32abc-535e-4756-99a2-8296df2492ae', - created: '2021-04-01T08:08:08.888Z', - created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', - modified: '2021-04-01T08:08:08.888Z', - content: 'This is a note!', - object_refs: [ - 'attack-pattern--2204c371-6100-4ae0-82f3-25c07c29772a' - ], - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - x_mitre_version: '1.0', - spec_version: '2.1' - } - ] + source_name: 'attack-pattern-1 source', + description: 'this is a source description', + }, + ], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + kill_chain_phases: [{ kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' }], + x_mitre_data_sources: ['data-source-1', 'data-source-2'], + x_mitre_detection: 'detection text', + x_mitre_is_subtechnique: false, + x_mitre_impact_type: ['impact-1'], + x_mitre_platforms: ['platform-1', 'platform-2'], + }, + { + id: 'attack-pattern--82f04b1e-5371-4a6f-be06-411f0f43b483', + created: '2019-02-03T16:56:41.200Z', + modified: '2019-02-03T16:56:41.200Z', + name: 'attack-pattern-2', + x_mitre_version: '1.0', + spec_version: '2.1', + type: 'attack-pattern', + description: 'This is another technique.', + external_references: [ + { source_name: 'source-1', external_id: 's1' }, + { + source_name: 'attack-pattern-2 source', + description: 'this is a source description 2', + }, + ], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + kill_chain_phases: [{ kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' }], + x_mitre_data_sources: ['data-source-1', 'data-source-2'], + x_mitre_detection: 'detection text', + x_mitre_is_subtechnique: false, + x_mitre_impact_type: ['impact-1'], + x_mitre_platforms: ['platform-1', 'platform-2'], + }, + { + id: 'attack-pattern--14fbfb6a-c4d9-4c3b-a7ef-f8df23e3b22b', + created: '2019-02-22T16:56:41.200Z', + modified: '2019-02-22T16:56:41.200Z', + name: 'attack-pattern-2', + x_mitre_version: '1.0', + type: 'attack-pattern', + description: 'This is technique that is missing a spec_version.', + external_references: [ + { source_name: 'source-1', external_id: 's1' }, + { + source_name: 'attack-pattern-2 source', + description: 'this is a source description 2', + }, + ], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + kill_chain_phases: [{ kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' }], + x_mitre_data_sources: ['data-source-1', 'data-source-2'], + x_mitre_detection: 'detection text', + x_mitre_is_subtechnique: false, + x_mitre_impact_type: ['impact-1'], + x_mitre_platforms: ['platform-1', 'platform-2'], + }, + { + id: 'not-a-type--a29c7d3a-3836-4219-b3db-ff946ea2251b', + created: '2020-05-30T14:03:43.761Z', + modified: '2020-05-30T14:03:43.761Z', + name: 'not-a-type-1', + x_mitre_version: '1.0', + spec_version: '2.1', + type: 'not-a-type', + description: 'This is a not a known STIX type.', + }, + { + id: 'course-of-action--25dc1ce8-eb55-4333-ae30-a7cb4f5894a1', + type: 'course-of-action', + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + name: 'mitigation-1', + description: 'This is a mitigation', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + x_mitre_version: '1.0', + modified: '2018-10-17T00:14:20.652Z', + created: '2017-10-25T14:48:53.732Z', + spec_version: '2.1', + x_mitre_domains: ['domain-1'], + }, + { + id: 'course-of-action--e944670c-d03a-4e93-a21c-b3d4c53ec4c9', + type: 'course-of-action', + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + name: 'mitigation-2', + description: "This is a mitigation that isn't in the contents", + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + x_mitre_version: '1.0', + modified: '2018-10-17T00:14:20.652Z', + created: '2017-10-25T14:48:53.732Z', + spec_version: '2.1', + x_mitre_domains: ['domain-1'], + }, + { + id: 'malware--04227b24-7817-4de1-9050-b7b1b57f5866', + type: 'malware', + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + name: 'software-1', + description: 'This is a software with an alias', + external_references: [ + { source_name: 'source-1', external_id: 's1' }, + { source_name: 'malware-1 source', description: 'this is a source description' }, + { source_name: 'xyzzy', description: '(Citation: Adventure 1975)' }, + ], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + x_mitre_version: '1.0', + modified: '2020-03-30T18:17:52.697Z', + created: '2017-10-25T14:48:53.732Z', + spec_version: '2.1', + x_mitre_domains: ['domain-1'], + x_mitre_aliases: ['xyzzy'], + }, + { + type: 'intrusion-set', + id: 'intrusion-set--d69e568e-9ac8-4c08-b32c-d93b43ba9172', + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + name: 'group-1', + description: 'This is a group with an alias', + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + external_references: [ + { source_name: 'source-2', external_id: 'g1' }, + { source_name: 'group source', description: 'This is a group description' }, + { source_name: 'group-xyzzy', description: '(Citation: Red 1999)' }, + ], + aliases: ['group-xyzzy'], + modified: '2020-03-30T19:25:56.012Z', + created: '2018-10-17T00:14:20.652Z', + x_mitre_version: '1.0', + spec_version: '2.1', + x_mitre_domains: ['domain-1'], + }, + { + type: 'note', + id: 'note--99e32abc-535e-4756-99a2-8296df2492ae', + created: '2021-04-01T08:08:08.888Z', + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + modified: '2021-04-01T08:08:08.888Z', + content: 'This is a note!', + object_refs: ['attack-pattern--2204c371-6100-4ae0-82f3-25c07c29772a'], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + x_mitre_version: '1.0', + spec_version: '2.1', + }, + ], }; const collectionId2a = 'x-mitre-collection--7f478133-ad3b-4c8d-97a3-a18ef61b7dee'; @@ -271,773 +244,758 @@ const collectionTimestamp2a = new Date().toISOString(); const collectionTimestamp2b = new Date().toISOString(); const collectionBundleData2 = { - type: 'bundle', - id: 'bundle--d288edaa-6289-40b5-80cd-080e1cd7eb18', - spec_version: '2.1', - objects: [ - { - id: collectionId2a, - created: collectionTimestamp2a, - modified: collectionTimestamp2a, - name: 'collection-2a', - spec_version: '2.1', - type: 'x-mitre-collection', - description: 'This is a collection.', - external_references: [ - {source_name: 'source-1', external_id: 's1'} - ], - object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - x_mitre_contents: [] - }, - { - id: collectionId2b, - created: collectionTimestamp2b, - modified: collectionTimestamp2b, - name: 'collection-2b', - spec_version: '2.1', - type: 'x-mitre-collection', - description: 'This is a collection.', - external_references: [ - {source_name: 'source-1', external_id: 's1'} - ], - object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - x_mitre_contents: [] - } - ] + type: 'bundle', + id: 'bundle--d288edaa-6289-40b5-80cd-080e1cd7eb18', + spec_version: '2.1', + objects: [ + { + id: collectionId2a, + created: collectionTimestamp2a, + modified: collectionTimestamp2a, + name: 'collection-2a', + spec_version: '2.1', + type: 'x-mitre-collection', + description: 'This is a collection.', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + x_mitre_contents: [], + }, + { + id: collectionId2b, + created: collectionTimestamp2b, + modified: collectionTimestamp2b, + name: 'collection-2b', + spec_version: '2.1', + type: 'x-mitre-collection', + description: 'This is a collection.', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + x_mitre_contents: [], + }, + ], }; const collectionBundleData3 = { - type: 'bundle', - id: 'bundle--6a95e2a6-1be9-4cd8-8348-f6cbf6408343', - spec_version: '2.1', - objects: [] + type: 'bundle', + id: 'bundle--6a95e2a6-1be9-4cd8-8348-f6cbf6408343', + spec_version: '2.1', + objects: [], }; -const collectionId4 = 'x-mitre-collection--987ae650-ec5d-4530-90ad-49e05347271e' +const collectionId4 = 'x-mitre-collection--987ae650-ec5d-4530-90ad-49e05347271e'; const collectionTimestamp4 = new Date().toISOString(); const collectionBundleData4 = { - type: 'bundle', - id: 'bundle--ad851e77-ea3c-454a-b2ba-a73c59d3c70f', - spec_version: '2.1', - objects: [ + type: 'bundle', + id: 'bundle--ad851e77-ea3c-454a-b2ba-a73c59d3c70f', + spec_version: '2.1', + objects: [ + { + id: collectionId4, + created: collectionTimestamp4, + modified: collectionTimestamp4, + name: 'collection-4', + spec_version: '2.1', + type: 'x-mitre-collection', + description: 'This is a collection.', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + x_mitre_contents: [ { - id: collectionId4, - created: collectionTimestamp4, - modified: collectionTimestamp4, - name: 'collection-4', - spec_version: '2.1', - type: 'x-mitre-collection', - description: 'This is a collection.', - external_references: [ - {source_name: 'source-1', external_id: 's1'} - ], - object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - x_mitre_contents: [ - { - "object_ref": "attack-pattern--fb4f094c-ad39-4dba-b459-5e314f6d6c8d", - "object_modified": "2020-03-30T14:03:43.761Z" - } - ] + object_ref: 'attack-pattern--fb4f094c-ad39-4dba-b459-5e314f6d6c8d', + object_modified: '2020-03-30T14:03:43.761Z', }, + ], + }, + { + id: 'attack-pattern--fb4f094c-ad39-4dba-b459-5e314f6d6c8d', + created: '2020-03-30T14:03:43.761Z', + modified: '2020-03-30T14:03:43.761Z', + name: 'attack-pattern-1', + x_mitre_version: '1.0', + spec_version: '2.1', + type: 'attack-pattern', + description: 'This is a technique.', + external_references: [ + { source_name: 'source-1', external_id: 's1' }, { - id: 'attack-pattern--fb4f094c-ad39-4dba-b459-5e314f6d6c8d', - created: '2020-03-30T14:03:43.761Z', - modified: '2020-03-30T14:03:43.761Z', - name: 'attack-pattern-1', - x_mitre_version: '1.0', - spec_version: '2.1', - type: 'attack-pattern', - description: 'This is a technique.', - external_references: [ - { source_name: 'source-1', external_id: 's1' }, - { source_name: 'attack-pattern-1 source', description: 'this is a source description'} - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - kill_chain_phases: [ - { kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' } - ], - x_mitre_data_sources: [ 'data-source-1', 'data-source-2' ], - x_mitre_detection: 'detection text', - x_mitre_is_subtechnique: false, - x_mitre_impact_type: [ 'impact-1' ], - x_mitre_platforms: [ 'platform-1', 'platform-2' ] + source_name: 'attack-pattern-1 source', + description: 'this is a source description', }, + ], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + kill_chain_phases: [{ kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' }], + x_mitre_data_sources: ['data-source-1', 'data-source-2'], + x_mitre_detection: 'detection text', + x_mitre_is_subtechnique: false, + x_mitre_impact_type: ['impact-1'], + x_mitre_platforms: ['platform-1', 'platform-2'], + }, + { + id: 'attack-pattern--fb4f094c-ad39-4dba-b459-5e314f6d6c8d', + created: '2020-03-30T14:03:43.761Z', + modified: '2020-03-30T14:03:43.761Z', + name: 'attack-pattern-1', + x_mitre_version: '1.0', + spec_version: '2.1', + type: 'attack-pattern', + description: 'This is a technique.', + external_references: [ + { source_name: 'source-1', external_id: 's1' }, { - id: 'attack-pattern--fb4f094c-ad39-4dba-b459-5e314f6d6c8d', - created: '2020-03-30T14:03:43.761Z', - modified: '2020-03-30T14:03:43.761Z', - name: 'attack-pattern-1', - x_mitre_version: '1.0', - spec_version: '2.1', - type: 'attack-pattern', - description: 'This is a technique.', - external_references: [ - { source_name: 'source-1', external_id: 's1' }, - { source_name: 'attack-pattern-1 source', description: 'this is a source description'} - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - kill_chain_phases: [ - { kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' } - ], - x_mitre_data_sources: [ 'data-source-1', 'data-source-2' ], - x_mitre_detection: 'detection text', - x_mitre_is_subtechnique: false, - x_mitre_impact_type: [ 'impact-1' ], - x_mitre_platforms: [ 'platform-1', 'platform-2' ] - } - ] + source_name: 'attack-pattern-1 source', + description: 'this is a source description', + }, + ], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + kill_chain_phases: [{ kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' }], + x_mitre_data_sources: ['data-source-1', 'data-source-2'], + x_mitre_detection: 'detection text', + x_mitre_is_subtechnique: false, + x_mitre_impact_type: ['impact-1'], + x_mitre_platforms: ['platform-1', 'platform-2'], + }, + ], }; -const collectionId5 = 'x-mitre-collection--2a5ef1a2-effb-4951-a301-e9ed333cb4cb' +const collectionId5 = 'x-mitre-collection--2a5ef1a2-effb-4951-a301-e9ed333cb4cb'; const collectionTimestamp5 = new Date().toISOString(); const collectionBundleData5 = { - type: 'bundle', - id: 'bundle--45bad6cc-a31b-418b-acf8-cd8ff3a5c2ec', - objects: [ + type: 'bundle', + id: 'bundle--45bad6cc-a31b-418b-acf8-cd8ff3a5c2ec', + objects: [ + { + id: collectionId5, + created: collectionTimestamp5, + modified: collectionTimestamp5, + name: 'collection-4', + spec_version: '2.1', + type: 'x-mitre-collection', + description: 'This is a collection.', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + x_mitre_contents: [ { - id: collectionId5, - created: collectionTimestamp5, - modified: collectionTimestamp5, - name: 'collection-4', - spec_version: '2.1', - type: 'x-mitre-collection', - description: 'This is a collection.', - external_references: [ - {source_name: 'source-1', external_id: 's1'} - ], - object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - x_mitre_contents: [ - { - "object_ref": "attack-pattern--44fc382e-0b71-4f5d-9110-fb2e35452d98", - "object_modified": "2021-03-30T14:03:43.761Z" - } - ] + object_ref: 'attack-pattern--44fc382e-0b71-4f5d-9110-fb2e35452d98', + object_modified: '2021-03-30T14:03:43.761Z', }, + ], + }, + { + id: 'attack-pattern--44fc382e-0b71-4f5d-9110-fb2e35452d98', + created: '2020-03-30T14:03:43.761Z', + modified: '2021-03-30T14:03:43.761Z', + name: 'attack-pattern-5', + x_mitre_version: '1.0', + spec_version: '2.1', + x_mitre_attack_spec_version: incrementedAttackSpecVersion, + type: 'attack-pattern', + description: 'This is a technique.', + external_references: [ + { source_name: 'source-1', external_id: 's1' }, { - id: 'attack-pattern--44fc382e-0b71-4f5d-9110-fb2e35452d98', - created: '2020-03-30T14:03:43.761Z', - modified: '2021-03-30T14:03:43.761Z', - name: 'attack-pattern-5', - x_mitre_version: '1.0', - spec_version: '2.1', - x_mitre_attack_spec_version: incrementedAttackSpecVersion, - type: 'attack-pattern', - description: 'This is a technique.', - external_references: [ - { source_name: 'source-1', external_id: 's1' }, - { source_name: 'attack-pattern-1 source', description: 'this is a source description'} - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - kill_chain_phases: [ - { kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' } - ], - x_mitre_data_sources: [ 'data-source-1', 'data-source-2' ], - x_mitre_detection: 'detection text', - x_mitre_is_subtechnique: false, - x_mitre_impact_type: [ 'impact-1' ], - x_mitre_platforms: [ 'platform-1', 'platform-2' ] - } - ] + source_name: 'attack-pattern-1 source', + description: 'this is a source description', + }, + ], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + kill_chain_phases: [{ kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' }], + x_mitre_data_sources: ['data-source-1', 'data-source-2'], + x_mitre_detection: 'detection text', + x_mitre_is_subtechnique: false, + x_mitre_impact_type: ['impact-1'], + x_mitre_platforms: ['platform-1', 'platform-2'], + }, + ], }; -const collectionId6 = 'x-mitre-collection--5c48cab9-f320-407a-808f-455466372519' +const collectionId6 = 'x-mitre-collection--5c48cab9-f320-407a-808f-455466372519'; const collectionTimestamp6 = new Date().toISOString(); const collectionData6 = { - workspace: { - - }, - stix: { - id: collectionId6, - created: collectionTimestamp6, - modified: collectionTimestamp6, - name: 'Partial Collection', - spec_version: '2.1', - type: 'x-mitre-collection', - description: 'This is a collection containing a subset of the initial imported collection.', - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - x_mitre_contents: [ - { - "object_ref": "attack-pattern--2204c371-6100-4ae0-82f3-25c07c29772a", - "object_modified": "2020-03-30T14:03:43.761Z" - }, - { - "object_ref": "attack-pattern--82f04b1e-5371-4a6f-be06-411f0f43b483", - "object_modified": "2019-02-03T16:56:41.200Z" - }, - - ] - } + workspace: {}, + stix: { + id: collectionId6, + created: collectionTimestamp6, + modified: collectionTimestamp6, + name: 'Partial Collection', + spec_version: '2.1', + type: 'x-mitre-collection', + description: 'This is a collection containing a subset of the initial imported collection.', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + x_mitre_contents: [ + { + object_ref: 'attack-pattern--2204c371-6100-4ae0-82f3-25c07c29772a', + object_modified: '2020-03-30T14:03:43.761Z', + }, + { + object_ref: 'attack-pattern--82f04b1e-5371-4a6f-be06-411f0f43b483', + object_modified: '2019-02-03T16:56:41.200Z', + }, + ], + }, }; describe('Collection Bundles Basic API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - it('POST /api/collection-bundles does not import an empty collection bundle', function (done) { - const body = {}; - request(app) - .post('/api/collection-bundles') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('POST /api/collection-bundles does not import a collection bundle with multiple x-mitre-collection objects', function (done) { - const body = collectionBundleData2; - request(app) - .post('/api/collection-bundles') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400) - .end(function (err, res) { - if (err) { - done(err); - } else { - const errorResult = res.body; - expect(errorResult.bundleErrors.moreThanOneCollection).toBe(true); - - done(); - } - }); - }); - - it('POST /api/collection-bundles does not import a collection bundle with zero x-mitre-collection objects', function (done) { - const body = collectionBundleData3; - request(app) - .post('/api/collection-bundles') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400) - .end(function (err, res) { - if (err) { - done(err); - } else { - const errorResult = res.body; - expect(errorResult.bundleErrors.noCollection).toBe(true); - - done(); - } - }); - }); - - it('POST /api/collection-bundles does not import a collection bundle with duplicate objects in the bundle', function (done) { - const body = collectionBundleData4; - request(app) - .post('/api/collection-bundles') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400) - .end(function (err, res) { - if (err) { - done(err); - } else { - const errorResult = res.body; - expect(errorResult.objectErrors.summary.duplicateObjectInBundleCount).toBe(1); - - done(); - } - }); - }); - - it('POST /api/collection-bundles does not import a collection bundle with an attack spec version violation', function (done) { - const body = collectionBundleData5; - request(app) - .post('/api/collection-bundles') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400) - .end(function (err, res) { - if (err) { - done(err); - } else { - const errorResult = res.body; - expect(errorResult.objectErrors.summary.invalidAttackSpecVersionCount).toBe(1); - - done(); - } - }); - }); - - it('POST /api/collection-bundles DOES import a collection bundle with an attack spec version violation if forceImport is set', function (done) { - const body = collectionBundleData5; - request(app) - .post('/api/collection-bundles?forceImport=attack-spec-version-violations') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('POST /api/collection-bundles previews the import of a collection bundle (checkOnly)', function (done) { - const body = collectionBundleData; - request(app) - .post('/api/collection-bundles?checkOnly=true') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get the created collection object - const collection = res.body; - expect(collection).toBeDefined(); - expect(collection.workspace.import_categories.additions.length).toBe(8); - expect(collection.workspace.import_categories.errors.length).toBe(3); - done(); - } - }); - }); - - let collection1; - it('POST /api/collection-bundles previews the import of a collection bundle (previewOnly)', function (done) { - const body = collectionBundleData; - request(app) - .post('/api/collection-bundles?previewOnly=true') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get the created collection object - const collection = res.body; - expect(collection).toBeDefined(); - expect(collection.workspace.import_categories.additions.length).toBe(8); - expect(collection.workspace.import_categories.errors.length).toBe(3); - done(); - } - }); - }); - - it('POST /api/collection-bundles imports a collection bundle', function (done) { - const body = collectionBundleData; - request(app) - .post('/api/collection-bundles') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get the created collection object - collection1 = res.body; - expect(collection1).toBeDefined(); - expect(collection1.workspace.import_categories.additions.length).toBe(8); - expect(collection1.workspace.import_categories.errors.length).toBe(4); - done(); - } - }); - }); - - it('POST /api/collection-bundles does not show a successful preview with a duplicate collection bundle', function (done) { - const body = collectionBundleData; - request(app) - .post('/api/collection-bundles?checkOnly=true') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('POST /api/collection-bundles does not import a duplicate collection bundle', function (done) { - const body = collectionBundleData; - request(app) - .post('/api/collection-bundles') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('POST /api/collection-bundles DOES import a duplicate collection bundle if forceImport is set', function (done) { - const body = collectionBundleData; - request(app) - .post('/api/collection-bundles?forceImport=duplicate-collection') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('POST /api/collection-bundles imports an updated collection bundle', function (done) { - const updateTimestamp = new Date().toISOString(); - const updatedCollection = _.cloneDeep(collectionBundleData); - updatedCollection.objects[0].modified = updateTimestamp; - updatedCollection.objects[0].x_mitre_contents[0].object_modified = updateTimestamp; - updatedCollection.objects[1].modified = updateTimestamp; - updatedCollection.objects[1].x_mitre_version = '1.1'; - - const body = updatedCollection; - request(app) - .post('/api/collection-bundles') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get the created collection object - const collection2 = res.body; - expect(collection2).toBeDefined(); - expect(collection2.workspace.import_categories.changes.length).toBe(1); - expect(collection2.workspace.import_categories.duplicates.length).toBe(6); - expect(collection2.workspace.import_categories.errors.length).toBe(4); - done(); - } - }); - }); - - it('GET /api/references returns the malware added reference', function (done) { - request(app) - .get('/api/references?sourceName=' + encodeURIComponent('malware-1 source')) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get one reference in an array - const references = res.body; - expect(references).toBeDefined(); - expect(Array.isArray(references)).toBe(true); - expect(references.length).toBe(1); - - done(); - } - }); - }); - - it('GET /api/references does not return the malware alias', function (done) { - request(app) - .get('/api/references?sourceName=' + encodeURIComponent('xyzzy')) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get zero references in an array - const references = res.body; - expect(references).toBeDefined(); - expect(Array.isArray(references)).toBe(true); - expect(references.length).toBe(0); - - done(); - } - }); - }); - - it('GET /api/references returns the group added reference', function (done) { - request(app) - .get('/api/references?sourceName=' + encodeURIComponent('group source')) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get one reference in an array - const references = res.body; - expect(references).toBeDefined(); - expect(Array.isArray(references)).toBe(true); - expect(references.length).toBe(1); - - done(); - } - }); - }); - - it('GET /api/references does not return the group alias', function (done) { - request(app) - .get('/api/references?sourceName=' + encodeURIComponent('group-xyzzy')) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get zero references in an array - const references = res.body; - expect(references).toBeDefined(); - expect(Array.isArray(references)).toBe(true); - expect(references.length).toBe(0); - - done(); - } - }); - }); - - it('GET /api/collection-bundles does not export the collection bundle with a bad id', function (done) { - request(app) - .get('/api/collection-bundles?collectionId=not-an-id') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('GET /api/collection-bundles previews the export of the collection bundle', function (done) { - request(app) - .get(`/api/collection-bundles?previewOnly=true&collectionId=x-mitre-collection--30ee11cf-0a05-4d9e-ab54-9b8563669647`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the exported collection bundle - const collectionBundle = res.body; - expect(collectionBundle).toBeDefined(); - expect(Array.isArray(collectionBundle.objects)).toBe(true); - expect(collectionBundle.objects.length).toBe(7); - - done(); - } - }); - }); - - it('GET /api/collection-bundles exports the collection bundle', function (done) { - request(app) - .get(`/api/collection-bundles?collectionId=${ collectionId }`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the exported collection bundle - const collectionBundle = res.body; - expect(collectionBundle).toBeDefined(); - expect(Array.isArray(collectionBundle.objects)).toBe(true); - expect(collectionBundle.objects.length).toBe(7); - - done(); - } - }); - }); - - let exportedCollectionBundle; - it('GET /api/collection-bundles exports the collection bundle with id and modified', function (done) { - request(app) - .get(`/api/collection-bundles?collectionId=${ collectionId }&collectionModified=${ encodeURIComponent(collectionTimestamp) }`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the exported collection bundle - exportedCollectionBundle = res.body; - expect(exportedCollectionBundle).toBeDefined(); - expect(Array.isArray(exportedCollectionBundle.objects)).toBe(true); - expect(exportedCollectionBundle.objects.length).toBe(7); - - done(); - } - }); - }); - - it('POST /api/collections creates the collection with a subset of the imported data', function (done) { - const body = collectionData6; - request(app) - .post('/api/collections') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('GET /api/collection-bundles exports the subset collection without the note', function (done) { - request(app) - .get(`/api/collection-bundles?collectionId=${ collectionId6 }`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the exported collection bundle - const collectionBundle = res.body; - expect(collectionBundle).toBeDefined(); - expect(Array.isArray(collectionBundle.objects)).toBe(true); - expect(collectionBundle.objects.length).toBe(3); - - done(); - } - }); - }); - - it('GET /api/collection-bundles exports the subset collection with the note', function (done) { - request(app) - .get(`/api/collection-bundles?collectionId=${ collectionId6 }&includeNotes=true`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the exported collection bundle - const collectionBundle = res.body; - expect(collectionBundle).toBeDefined(); - expect(Array.isArray(collectionBundle.objects)).toBe(true); - expect(collectionBundle.objects.length).toBe(4); - - done(); - } - }); - }); - - it('POST /api/collection-bundles imports the previously exported collection bundle', function (done) { - // Update the exported collection bundle so it isn't a duplicate - const updateTimestamp = new Date().toISOString(); - const updatedCollection = _.cloneDeep(exportedCollectionBundle); - updatedCollection.objects[0].modified = updateTimestamp; - updatedCollection.objects[0].x_mitre_contents[0].object_modified = updateTimestamp; - updatedCollection.objects[1].modified = updateTimestamp; - updatedCollection.objects[1].x_mitre_version = '1.1'; - - const body = updatedCollection; - request(app) - .post('/api/collection-bundles') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get the created collection object - const collection = res.body; - expect(collection).toBeDefined(); - done(); - } - }); - }); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + it('POST /api/collection-bundles does not import an empty collection bundle', function (done) { + const body = {}; + request(app) + .post('/api/collection-bundles') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('POST /api/collection-bundles does not import a collection bundle with multiple x-mitre-collection objects', function (done) { + const body = collectionBundleData2; + request(app) + .post('/api/collection-bundles') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400) + .end(function (err, res) { + if (err) { + done(err); + } else { + const errorResult = res.body; + expect(errorResult.bundleErrors.moreThanOneCollection).toBe(true); + + done(); + } + }); + }); + + it('POST /api/collection-bundles does not import a collection bundle with zero x-mitre-collection objects', function (done) { + const body = collectionBundleData3; + request(app) + .post('/api/collection-bundles') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400) + .end(function (err, res) { + if (err) { + done(err); + } else { + const errorResult = res.body; + expect(errorResult.bundleErrors.noCollection).toBe(true); + + done(); + } + }); + }); + + it('POST /api/collection-bundles does not import a collection bundle with duplicate objects in the bundle', function (done) { + const body = collectionBundleData4; + request(app) + .post('/api/collection-bundles') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400) + .end(function (err, res) { + if (err) { + done(err); + } else { + const errorResult = res.body; + expect(errorResult.objectErrors.summary.duplicateObjectInBundleCount).toBe(1); + + done(); + } + }); + }); + + it('POST /api/collection-bundles does not import a collection bundle with an attack spec version violation', function (done) { + const body = collectionBundleData5; + request(app) + .post('/api/collection-bundles') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400) + .end(function (err, res) { + if (err) { + done(err); + } else { + const errorResult = res.body; + expect(errorResult.objectErrors.summary.invalidAttackSpecVersionCount).toBe(1); + + done(); + } + }); + }); + + it('POST /api/collection-bundles DOES import a collection bundle with an attack spec version violation if forceImport is set', function (done) { + const body = collectionBundleData5; + request(app) + .post('/api/collection-bundles?forceImport=attack-spec-version-violations') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('POST /api/collection-bundles previews the import of a collection bundle (checkOnly)', function (done) { + const body = collectionBundleData; + request(app) + .post('/api/collection-bundles?checkOnly=true') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the created collection object + const collection = res.body; + expect(collection).toBeDefined(); + expect(collection.workspace.import_categories.additions.length).toBe(8); + expect(collection.workspace.import_categories.errors.length).toBe(3); + done(); + } + }); + }); + + let collection1; + it('POST /api/collection-bundles previews the import of a collection bundle (previewOnly)', function (done) { + const body = collectionBundleData; + request(app) + .post('/api/collection-bundles?previewOnly=true') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the created collection object + const collection = res.body; + expect(collection).toBeDefined(); + expect(collection.workspace.import_categories.additions.length).toBe(8); + expect(collection.workspace.import_categories.errors.length).toBe(3); + done(); + } + }); + }); + + it('POST /api/collection-bundles imports a collection bundle', function (done) { + const body = collectionBundleData; + request(app) + .post('/api/collection-bundles') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the created collection object + collection1 = res.body; + expect(collection1).toBeDefined(); + expect(collection1.workspace.import_categories.additions.length).toBe(8); + expect(collection1.workspace.import_categories.errors.length).toBe(4); + done(); + } + }); + }); + + it('POST /api/collection-bundles does not show a successful preview with a duplicate collection bundle', function (done) { + const body = collectionBundleData; + request(app) + .post('/api/collection-bundles?checkOnly=true') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('POST /api/collection-bundles does not import a duplicate collection bundle', function (done) { + const body = collectionBundleData; + request(app) + .post('/api/collection-bundles') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('POST /api/collection-bundles DOES import a duplicate collection bundle if forceImport is set', function (done) { + const body = collectionBundleData; + request(app) + .post('/api/collection-bundles?forceImport=duplicate-collection') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('POST /api/collection-bundles imports an updated collection bundle', function (done) { + const updateTimestamp = new Date().toISOString(); + const updatedCollection = _.cloneDeep(collectionBundleData); + updatedCollection.objects[0].modified = updateTimestamp; + updatedCollection.objects[0].x_mitre_contents[0].object_modified = updateTimestamp; + updatedCollection.objects[1].modified = updateTimestamp; + updatedCollection.objects[1].x_mitre_version = '1.1'; + + const body = updatedCollection; + request(app) + .post('/api/collection-bundles') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the created collection object + const collection2 = res.body; + expect(collection2).toBeDefined(); + expect(collection2.workspace.import_categories.changes.length).toBe(1); + expect(collection2.workspace.import_categories.duplicates.length).toBe(6); + expect(collection2.workspace.import_categories.errors.length).toBe(4); + done(); + } + }); + }); + + it('GET /api/references returns the malware added reference', function (done) { + request(app) + .get('/api/references?sourceName=' + encodeURIComponent('malware-1 source')) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get one reference in an array + const references = res.body; + expect(references).toBeDefined(); + expect(Array.isArray(references)).toBe(true); + expect(references.length).toBe(1); + + done(); + } + }); + }); + + it('GET /api/references does not return the malware alias', function (done) { + request(app) + .get('/api/references?sourceName=' + encodeURIComponent('xyzzy')) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get zero references in an array + const references = res.body; + expect(references).toBeDefined(); + expect(Array.isArray(references)).toBe(true); + expect(references.length).toBe(0); + + done(); + } + }); + }); + + it('GET /api/references returns the group added reference', function (done) { + request(app) + .get('/api/references?sourceName=' + encodeURIComponent('group source')) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get one reference in an array + const references = res.body; + expect(references).toBeDefined(); + expect(Array.isArray(references)).toBe(true); + expect(references.length).toBe(1); + + done(); + } + }); + }); + + it('GET /api/references does not return the group alias', function (done) { + request(app) + .get('/api/references?sourceName=' + encodeURIComponent('group-xyzzy')) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get zero references in an array + const references = res.body; + expect(references).toBeDefined(); + expect(Array.isArray(references)).toBe(true); + expect(references.length).toBe(0); + + done(); + } + }); + }); + + it('GET /api/collection-bundles does not export the collection bundle with a bad id', function (done) { + request(app) + .get('/api/collection-bundles?collectionId=not-an-id') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('GET /api/collection-bundles previews the export of the collection bundle', function (done) { + request(app) + .get( + `/api/collection-bundles?previewOnly=true&collectionId=x-mitre-collection--30ee11cf-0a05-4d9e-ab54-9b8563669647`, + ) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the exported collection bundle + const collectionBundle = res.body; + expect(collectionBundle).toBeDefined(); + expect(Array.isArray(collectionBundle.objects)).toBe(true); + expect(collectionBundle.objects.length).toBe(7); + + done(); + } + }); + }); + + it('GET /api/collection-bundles exports the collection bundle', function (done) { + request(app) + .get(`/api/collection-bundles?collectionId=${collectionId}`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the exported collection bundle + const collectionBundle = res.body; + expect(collectionBundle).toBeDefined(); + expect(Array.isArray(collectionBundle.objects)).toBe(true); + expect(collectionBundle.objects.length).toBe(7); + + done(); + } + }); + }); + + let exportedCollectionBundle; + it('GET /api/collection-bundles exports the collection bundle with id and modified', function (done) { + request(app) + .get( + `/api/collection-bundles?collectionId=${collectionId}&collectionModified=${encodeURIComponent(collectionTimestamp)}`, + ) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the exported collection bundle + exportedCollectionBundle = res.body; + expect(exportedCollectionBundle).toBeDefined(); + expect(Array.isArray(exportedCollectionBundle.objects)).toBe(true); + expect(exportedCollectionBundle.objects.length).toBe(7); + + done(); + } + }); + }); + + it('POST /api/collections creates the collection with a subset of the imported data', function (done) { + const body = collectionData6; + request(app) + .post('/api/collections') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('GET /api/collection-bundles exports the subset collection without the note', function (done) { + request(app) + .get(`/api/collection-bundles?collectionId=${collectionId6}`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the exported collection bundle + const collectionBundle = res.body; + expect(collectionBundle).toBeDefined(); + expect(Array.isArray(collectionBundle.objects)).toBe(true); + expect(collectionBundle.objects.length).toBe(3); + + done(); + } + }); + }); + + it('GET /api/collection-bundles exports the subset collection with the note', function (done) { + request(app) + .get(`/api/collection-bundles?collectionId=${collectionId6}&includeNotes=true`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the exported collection bundle + const collectionBundle = res.body; + expect(collectionBundle).toBeDefined(); + expect(Array.isArray(collectionBundle.objects)).toBe(true); + expect(collectionBundle.objects.length).toBe(4); + + done(); + } + }); + }); + + it('POST /api/collection-bundles imports the previously exported collection bundle', function (done) { + // Update the exported collection bundle so it isn't a duplicate + const updateTimestamp = new Date().toISOString(); + const updatedCollection = _.cloneDeep(exportedCollectionBundle); + updatedCollection.objects[0].modified = updateTimestamp; + updatedCollection.objects[0].x_mitre_contents[0].object_modified = updateTimestamp; + updatedCollection.objects[1].modified = updateTimestamp; + updatedCollection.objects[1].x_mitre_version = '1.1'; + + const body = updatedCollection; + request(app) + .post('/api/collection-bundles') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the created collection object + const collection = res.body; + expect(collection).toBeDefined(); + done(); + } + }); + }); - after(async function() { - await database.closeConnection(); - }); + after(async function () { + await database.closeConnection(); + }); }); diff --git a/app/tests/api/collection-indexes/collection-indexes.spec.js b/app/tests/api/collection-indexes/collection-indexes.spec.js index 4d3d4353..f6da93b3 100644 --- a/app/tests/api/collection-indexes/collection-indexes.spec.js +++ b/app/tests/api/collection-indexes/collection-indexes.spec.js @@ -10,281 +10,286 @@ const login = require('../../shared/login'); // modified and created properties will be set before calling REST API const initialObjectData = { - "collection_index": { - "id": "bb8c95c0-4e8f-491e-a3c9-8b4207e43041", - "name": "MITRE ATT&CK", - "description": "All ATT&CK datasets", - "collections": [ - { - "versions": [ - { - "url": "https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v8.0/enterprise-attack/enterprise-attack.json", - "version": "8.0.0", - "modified": "2020-10-27T08:51:03.896157Z", - "release_notes": "The October 2020 (v8) ATT&CK release updates Techniques, Groups, and Software for both Enterprise and Mobile. The biggest changes are the deprecation of the PRE-ATT&CK domain, the addition of two new Tactics to replace [PRE-ATT&CK](/versions/v7/matrices/pre/), and the addition of the Network platform to Enterprise ATT&CK.\n\nThis version of ATT&CK for Enterprise contains 14 Tactics, 177 Techniques, and 348 Sub-techniques.\n\n#### Retirement of PRE-ATT&CK\nThis release deprecates and removes the PRE-ATT&CK domain from ATT&CK, replacing its scope with two new Tactics in Enterprise ATT&CK [Reconnaissance](/tactics/TA0043/) and [Resource Development](/tactics/TA0042/). A new platform has also been added to ATT&CK to represent the environment these techniques occur in, [PRE](/matrices/enterprise/pre/). The previous contents of PRE-ATT&CK have been preserved [here](/versions/v7/matrices/pre/). See [the accompanying blog post](https://medium.com/mitre-attack/the-retirement-of-pre-attack-4b73ffecd3d3) for more details.\n\n#### New techniques in Reconnaissance:\n\n* [Active Scanning](/techniques/T1595)\n\t* [Scanning IP Blocks](/techniques/T1595/001)\n\t* [Vulnerability Scanning](/techniques/T1595/002)\n* [Gather Victim Host Information](/techniques/T1592)\n\t* [Client Configurations](/techniques/T1592/004)\n\t* [Firmware](/techniques/T1592/003)\n\t* [Hardware](/techniques/T1592/001)\n\t* [Software](/techniques/T1592/002)\n* [Gather Victim Identity Information](/techniques/T1589)\n\t* [Credentials](/techniques/T1589/001)\n\t* [Email Addresses](/techniques/T1589/002)\n\t* [Employee Names](/techniques/T1589/003)\n* [Gather Victim Network Information](/techniques/T1590)\n\t* [DNS](/techniques/T1590/002)\n\t* [Domain Properties](/techniques/T1590/001)\n\t* [IP Addresses](/techniques/T1590/005)\n\t* [Network Security Appliances](/techniques/T1590/006)\n\t* [Network Topology](/techniques/T1590/004)\n\t* [Network Trust Dependencies](/techniques/T1590/003)\n* [Gather Victim Org Information](/techniques/T1591)\n\t* [Business Relationships](/techniques/T1591/002)\n\t* [Determine Physical Locations](/techniques/T1591/001)\n\t* [Identify Business Tempo](/techniques/T1591/003)\n\t* [Identify Roles](/techniques/T1591/004)\n* [Phishing for Information](/techniques/T1598)\n\t* [Spearphishing Attachment](/techniques/T1598/002)\n\t* [Spearphishing Link](/techniques/T1598/003)\n\t* [Spearphishing Service](/techniques/T1598/001)\n* [Search Closed Sources](/techniques/T1597)\n\t* [Purchase Technical Data](/techniques/T1597/002)\n\t* [Threat Intel Vendors](/techniques/T1597/001)\n* [Search Open Technical Databases](/techniques/T1596)\n\t* [CDNs](/techniques/T1596/004)\n\t* [DNS/Passive DNS](/techniques/T1596/001)\n\t* [Digital Certificates](/techniques/T1596/003)\n\t* [Scan Databases](/techniques/T1596/005)\n\t* [WHOIS](/techniques/T1596/002)\n* [Search Open Websites/Domains](/techniques/T1593)\n\t* [Search Engines](/techniques/T1593/002)\n\t* [Social Media](/techniques/T1593/001)\n* [Search Victim-Owned Websites](/techniques/T1594)\n\n#### New techniques in Resource Development:\n\n* [Acquire Infrastructure](/techniques/T1583)\n\t* [Botnet](/techniques/T1583/005)\n\t* [DNS Server](/techniques/T1583/002)\n\t* [Domains](/techniques/T1583/001)\n\t* [Server](/techniques/T1583/004)\n\t* [Virtual Private Server](/techniques/T1583/003)\n\t* [Web Services](/techniques/T1583/006)\n* [Compromise Accounts](/techniques/T1586)\n\t* [Email Accounts](/techniques/T1586/002)\n\t* [Social Media Accounts](/techniques/T1586/001)\n* [Compromise Infrastructure](/techniques/T1584)\n\t* [Botnet](/techniques/T1584/005)\n\t* [DNS Server](/techniques/T1584/002)\n\t* [Domains](/techniques/T1584/001)\n\t* [Server](/techniques/T1584/004)\n\t* [Virtual Private Server](/techniques/T1584/003)\n\t* [Web Services](/techniques/T1584/006)\n* [Develop Capabilities](/techniques/T1587)\n\t* [Code Signing Certificates](/techniques/T1587/002)\n\t* [Digital Certificates](/techniques/T1587/003)\n\t* [Exploits](/techniques/T1587/004)\n\t* [Malware](/techniques/T1587/001)\n* [Establish Accounts](/techniques/T1585)\n\t* [Email Accounts](/techniques/T1585/002)\n\t* [Social Media Accounts](/techniques/T1585/001)\n* [Obtain Capabilities](/techniques/T1588)\n\t* [Code Signing Certificates](/techniques/T1588/003)\n\t* [Digital Certificates](/techniques/T1588/004)\n\t* [Exploits](/techniques/T1588/005)\n\t* [Malware](/techniques/T1588/001)\n\t* [Tool](/techniques/T1588/002)\n\t* [Vulnerabilities](/techniques/T1588/006)\n\n\n#### ATT&CK for Network Infrastructure Devices\n\n13 techniques and 15 sub-techniques have been added or modified to cover adversary behavior against network infrastructure devices that constitute the fabric of enterprises' networks such as switches and routers. These techniques are represented by a new platform in ATT&CK for Enterprise, [Network](/matrices/enterprise/network/).\n\n#### New and updated techniques for Network:\n* [Exploit Public-Facing Application](/techniques/T1190)\n* [Command and Scripting Interpreter](/techniques/T1059)\n\t* [Network Device CLI](/techniques/T1059/008)\n* [Pre-OS Boot](/techniques/T1542)\n\t* [ROMMONkit](/techniques/T1542/004)\n\t* [TFTP Boot](/techniques/T1542/005)\n* [Traffic Signaling](/techniques/T1205)\n\t* [Port Knocking](/techniques/T1205/001)\n* [Modify Authentication Process](/techniques/T1556)\n\t* [Network Device Authentication](/techniques/T1556/004)\n* [Modify System Image](/techniques/T1601)\n\t* [Downgrade System Image](/techniques/T1601/002)\n\t* [Patch System Image](/techniques/T1601/001)\n* [Network Boundary Bridging](/techniques/T1599)\n\t* [Network Address Translation Traversal](/techniques/T1599/001)\n* [Weaken Encryption](/techniques/T1600)\n\t* [Disable Crypto Hardware](/techniques/T1600/002)\n\t* [Reduce Key Space](/techniques/T1600/001)\n* [Data from Configuration Repository](/techniques/T1602)\n\t* [Network Device Configuration Dump](/techniques/T1602/002)\n\t* [SNMP (MIB Dump)](/techniques/T1602/001)\n* [Input Capture](/techniques/T1056)\n\t* [Keylogging](/techniques/T1056/001)\n* [Non-Application Layer Protocol](/techniques/T1095)\n* [Proxy](/techniques/T1090)\n\t* [Multi-hop Proxy](/techniques/T1090/003)\t\n* [Automated Exfiltration](/techniques/T1020)\n\t* [Traffic Duplication](/techniques/T1020/001)\n\n\nMany of the new Network techniques and sub-techniques focus on embedded network devices running closed source proprietary operating systems. This is largely driven by behaviors present in reported in the wild intrusions. Many newer devices leverage commodity embedded operating systems such as Linux or BSD variants, but accounts of adversary activity against these have been more sparse. However, network infrastructure devices running proprietary operating systems are still widely deployed on the Internet and within enterprises.\n\nWe will continue to build out additional Network techniques and sub-techniques as they become known. We welcome contributions and feedback from the community and look to improve this representation of behaviors in the network infrastructure devices space.\n\n### Techniques\n\n**Enterprise**\n\nWe also added 1 additional new technique and 7 sub-techniques to Enterprise in this ATT&CK release beyond the scope of the above updates. All Enterprise technique changes, including this new technique and these new sub-techniques, are documented below.\n\nNew Techniques:\n\n* [Acquire Infrastructure](/techniques/T1583)\n\t* [Botnet](/techniques/T1583/005)\n\t* [DNS Server](/techniques/T1583/002)\n\t* [Domains](/techniques/T1583/001)\n\t* [Server](/techniques/T1583/004)\n\t* [Virtual Private Server](/techniques/T1583/003)\n\t* [Web Services](/techniques/T1583/006)\n* [Active Scanning](/techniques/T1595)\n\t* [Scanning IP Blocks](/techniques/T1595/001)\n\t* [Vulnerability Scanning](/techniques/T1595/002)\n* Automated Exfiltration: [Traffic Duplication](/techniques/T1020/001)\n* Boot or Logon Autostart Execution: [Print Processors](/techniques/T1547/012)\n* [Cloud Infrastructure Discovery](/techniques/T1580)\n* Command and Scripting Interpreter: [Network Device CLI](/techniques/T1059/008)\n* [Compromise Accounts](/techniques/T1586)\n\t* [Email Accounts](/techniques/T1586/002)\n\t* [Social Media Accounts](/techniques/T1586/001)\n* [Compromise Infrastructure](/techniques/T1584)\n\t* [Botnet](/techniques/T1584/005)\n\t* [DNS Server](/techniques/T1584/002)\n\t* [Domains](/techniques/T1584/001)\n\t* [Server](/techniques/T1584/004)\n\t* [Virtual Private Server](/techniques/T1584/003)\n\t* [Web Services](/techniques/T1584/006)\n* [Data from Configuration Repository](/techniques/T1602)\n\t* [Network Device Configuration Dump](/techniques/T1602/002)\n\t* [SNMP (MIB Dump)](/techniques/T1602/001)\n* [Develop Capabilities](/techniques/T1587)\n\t* [Code Signing Certificates](/techniques/T1587/002)\n\t* [Digital Certificates](/techniques/T1587/003)\n\t* [Exploits](/techniques/T1587/004)\n\t* [Malware](/techniques/T1587/001)\n* [Establish Accounts](/techniques/T1585)\n\t* [Email Accounts](/techniques/T1585/002)\n\t* [Social Media Accounts](/techniques/T1585/001)\n* [Gather Victim Host Information](/techniques/T1592)\n\t* [Client Configurations](/techniques/T1592/004)\n\t* [Firmware](/techniques/T1592/003)\n\t* [Hardware](/techniques/T1592/001)\n\t* [Software](/techniques/T1592/002)\n* [Gather Victim Identity Information](/techniques/T1589)\n\t* [Credentials](/techniques/T1589/001)\n\t* [Email Addresses](/techniques/T1589/002)\n\t* [Employee Names](/techniques/T1589/003)\n* [Gather Victim Network Information](/techniques/T1590)\n\t* [DNS](/techniques/T1590/002)\n\t* [Domain Properties](/techniques/T1590/001)\n\t* [IP Addresses](/techniques/T1590/005)\n\t* [Network Security Appliances](/techniques/T1590/006)\n\t* [Network Topology](/techniques/T1590/004)\n\t* [Network Trust Dependencies](/techniques/T1590/003)\n* [Gather Victim Org Information](/techniques/T1591)\n\t* [Business Relationships](/techniques/T1591/002)\n\t* [Determine Physical Locations](/techniques/T1591/001)\n\t* [Identify Business Tempo](/techniques/T1591/003)\n\t* [Identify Roles](/techniques/T1591/004)\n* Hide Artifacts: [VBA Stomping](/techniques/T1564/007)\n* Impair Defenses: [Disable Cloud Logs](/techniques/T1562/008)\n* Man-in-the-Middle: [ARP Cache Poisoning](/techniques/T1557/002)\n* Modify Authentication Process: [Network Device Authentication](/techniques/T1556/004)\n* [Modify System Image](/techniques/T1601)\n\t* [Downgrade System Image](/techniques/T1601/002)\n\t* [Patch System Image](/techniques/T1601/001)\n* [Network Boundary Bridging](/techniques/T1599)\n\t* [Network Address Translation Traversal](/techniques/T1599/001)\n* [Obtain Capabilities](/techniques/T1588)\n\t* [Code Signing Certificates](/techniques/T1588/003)\n\t* [Digital Certificates](/techniques/T1588/004)\n\t* [Exploits](/techniques/T1588/005)\n\t* [Malware](/techniques/T1588/001)\n\t* [Tool](/techniques/T1588/002)\n\t* [Vulnerabilities](/techniques/T1588/006)\n* [Phishing for Information](/techniques/T1598)\n\t* [Spearphishing Attachment](/techniques/T1598/002)\n\t* [Spearphishing Link](/techniques/T1598/003)\n\t* [Spearphishing Service](/techniques/T1598/001)\n* Pre-OS Boot: [ROMMONkit](/techniques/T1542/004)\n* Pre-OS Boot: [TFTP Boot](/techniques/T1542/005)\n* Scheduled Task/Job: [Systemd Timers](/techniques/T1053/006)\n* [Search Closed Sources](/techniques/T1597)\n\t* [Purchase Technical Data](/techniques/T1597/002)\n\t* [Threat Intel Vendors](/techniques/T1597/001)\n* [Search Open Technical Databases](/techniques/T1596)\n\t* [CDNs](/techniques/T1596/004)\n\t* [DNS/Passive DNS](/techniques/T1596/001)\n\t* [Digital Certificates](/techniques/T1596/003)\n\t* [Scan Databases](/techniques/T1596/005)\n\t* [WHOIS](/techniques/T1596/002)\n* [Search Open Websites/Domains](/techniques/T1593)\n\t* [Search Engines](/techniques/T1593/002)\n\t* [Social Media](/techniques/T1593/001)\n* [Search Victim-Owned Websites](/techniques/T1594)\n* Signed Binary Proxy Execution: [Verclsid](/techniques/T1218/012)\n* Steal or Forge Kerberos Tickets: [AS-REP Roasting](/techniques/T1558/004)\n* [Weaken Encryption](/techniques/T1600)\n\t* [Disable Crypto Hardware](/techniques/T1600/002)\n\t* [Reduce Key Space](/techniques/T1600/001)\n\n\nTechnique changes:\n\n* Abuse Elevation Control Mechanism: [Bypass User Account Control](/techniques/T1548/002)\n* [Account Discovery](/techniques/T1087)\n\t* [Cloud Account](/techniques/T1087/004)\n* Account Manipulation: [Additional Cloud Credentials](/techniques/T1098/001)\n* [Automated Exfiltration](/techniques/T1020)\n* [Boot or Logon Autostart Execution](/techniques/T1547)\n\t* [Registry Run Keys / Startup Folder](/techniques/T1547/001)\n* [Boot or Logon Initialization Scripts](/techniques/T1037)\n* Brute Force: [Credential Stuffing](/techniques/T1110/004)\n* Brute Force: [Password Cracking](/techniques/T1110/002)\n* Brute Force: [Password Guessing](/techniques/T1110/001)\n* Brute Force: [Password Spraying](/techniques/T1110/003)\n* [Command and Scripting Interpreter](/techniques/T1059)\n\t* [AppleScript](/techniques/T1059/002)\n\t* [Visual Basic](/techniques/T1059/005)\n* Create or Modify System Process: [Launch Daemon](/techniques/T1543/004)\n* Create or Modify System Process: [Systemd Service](/techniques/T1543/002)\n* Create or Modify System Process: [Windows Service](/techniques/T1543/003)\n* [Data from Information Repositories](/techniques/T1213)\n* Endpoint Denial of Service: [OS Exhaustion Flood](/techniques/T1499/001)\n* Endpoint Denial of Service: [Service Exhaustion Flood](/techniques/T1499/002)\n* [Event Triggered Execution](/techniques/T1546)\n\t* [Image File Execution Options Injection](/techniques/T1546/012)\n* [Exploit Public-Facing Application](/techniques/T1190)\n* [File and Directory Discovery](/techniques/T1083)\n* File and Directory Permissions Modification: [Windows File and Directory Permissions Modification](/techniques/T1222/001)\n* [Hardware Additions](/techniques/T1200)\n* Hijack Execution Flow: [LD_PRELOAD](/techniques/T1574/006)\n* Hijack Execution Flow: [Path Interception by Unquoted Path](/techniques/T1574/009)\n* Impair Defenses: [Impair Command History Logging](/techniques/T1562/003)\n* Indicator Removal on Host: [Clear Command History](/techniques/T1070/003)\n* [Input Capture](/techniques/T1056)\n\t* [Keylogging](/techniques/T1056/001)\n* [Man-in-the-Middle](/techniques/T1557)\n* [Modify Authentication Process](/techniques/T1556)\n* [Modify Registry](/techniques/T1112)\n* Network Denial of Service: [Direct Network Flood](/techniques/T1498/001)\n* Network Denial of Service: [Reflection Amplification](/techniques/T1498/002)\n* [Network Share Discovery](/techniques/T1135)\n* [Non-Application Layer Protocol](/techniques/T1095)\n* Obfuscated Files or Information: [Binary Padding](/techniques/T1027/001)\n* Obfuscated Files or Information: [Steganography](/techniques/T1027/003)\n* [Password Policy Discovery](/techniques/T1201)\n* [Permission Groups Discovery](/techniques/T1069)\n\t* [Cloud Groups](/techniques/T1069/003)\n* [Phishing](/techniques/T1566)\n\t* [Spearphishing Attachment](/techniques/T1566/001)\n\t* [Spearphishing Link](/techniques/T1566/002)\n\t* [Spearphishing via Service](/techniques/T1566/003)\n* [Pre-OS Boot](/techniques/T1542)\n\t* [Bootkit](/techniques/T1542/003)\n* [Proxy](/techniques/T1090)\n\t* [Domain Fronting](/techniques/T1090/004)\n\t* [Multi-hop Proxy](/techniques/T1090/003)\n* [Remote System Discovery](/techniques/T1018)\n* Server Software Component: [Web Shell](/techniques/T1505/003)\n* [Service Stop](/techniques/T1489)\n* Signed Binary Proxy Execution: [Control Panel](/techniques/T1218/002)\n* [Software Deployment Tools](/techniques/T1072)\n* [Software Discovery](/techniques/T1518)\n\t* [Security Software Discovery](/techniques/T1518/001)\n* [Steal or Forge Kerberos Tickets](/techniques/T1558)\n\t* [Kerberoasting](/techniques/T1558/003)\n* [Traffic Signaling](/techniques/T1205)\n\t* [Port Knocking](/techniques/T1205/001)\n* [Unsecured Credentials](/techniques/T1552)\n\t* [Cloud Instance Metadata API](/techniques/T1552/005)\n* Use Alternate Authentication Material: [Application Access Token](/techniques/T1550/001)\n* Use Alternate Authentication Material: [Web Session Cookie](/techniques/T1550/004)\n* Valid Accounts: [Cloud Accounts](/techniques/T1078/004)\n* Valid Accounts: [Default Accounts](/techniques/T1078/001)\n* Valid Accounts: [Domain Accounts](/techniques/T1078/002)\n\n\nMinor Technique changes:\n\n* [Abuse Elevation Control Mechanism](/techniques/T1548)\n* [Account Manipulation](/techniques/T1098)\n* [Application Layer Protocol](/techniques/T1071)\n\t* [DNS](/techniques/T1071/004)\n\t* [File Transfer Protocols](/techniques/T1071/002)\n\t* [Mail Protocols](/techniques/T1071/003)\n* [Archive Collected Data](/techniques/T1560)\n* [Brute Force](/techniques/T1110)\n* [Create or Modify System Process](/techniques/T1543)\n* [Data Encrypted for Impact](/techniques/T1486)\n* [Data Staged](/techniques/T1074)\n\t* [Remote Data Staging](/techniques/T1074/002)\n* [Domain Trust Discovery](/techniques/T1482)\n* [Dynamic Resolution](/techniques/T1568)\n\t* [Domain Generation Algorithms](/techniques/T1568/002)\n* Email Collection: [Email Forwarding Rule](/techniques/T1114/003)\n* [Endpoint Denial of Service](/techniques/T1499)\n* [File and Directory Permissions Modification](/techniques/T1222)\n* [Hide Artifacts](/techniques/T1564)\n\t* [Hidden Users](/techniques/T1564/002)\n* [Hijack Execution Flow](/techniques/T1574)\n\t* [DLL Side-Loading](/techniques/T1574/002)\n\t* [Dylib Hijacking](/techniques/T1574/004)\n\t* [Path Interception by PATH Environment Variable](/techniques/T1574/007)\n\t* [Path Interception by Search Order Hijacking](/techniques/T1574/008)\n\t* [Services File Permissions Weakness](/techniques/T1574/010)\n\t* [Services Registry Permissions Weakness](/techniques/T1574/011)\n* [Impair Defenses](/techniques/T1562)\n\t* [Disable or Modify Cloud Firewall](/techniques/T1562/007)\n* [Indicator Removal on Host](/techniques/T1070)\n* [Internal Spearphishing](/techniques/T1534)\n* Modify Authentication Process: [Domain Controller Authentication](/techniques/T1556/001)\n* [Modify Cloud Compute Infrastructure](/techniques/T1578)\n\t* [Create Cloud Instance](/techniques/T1578/002)\n\t* [Create Snapshot](/techniques/T1578/001)\n\t* [Delete Cloud Instance](/techniques/T1578/003)\n* [Network Denial of Service](/techniques/T1498)\n* [Obfuscated Files or Information](/techniques/T1027)\n* [Scheduled Task/Job](/techniques/T1053)\n* [Server Software Component](/techniques/T1505)\n* [Signed Binary Proxy Execution](/techniques/T1218)\n* [Supply Chain Compromise](/techniques/T1195)\n* [Use Alternate Authentication Material](/techniques/T1550)\n* [Valid Accounts](/techniques/T1078)\n\n\nTechnique revocations:\nNo changes\n\nTechnique deprecations:\nNo changes\n\n**Mobile**\n\nNew Techniques:\n\n* [Geofencing](/techniques/T1581)\n* [SMS Control](/techniques/T1582)\n\n\nTechnique changes:\n\n* [Delete Device Data](/techniques/T1447)\n* [Supply Chain Compromise](/techniques/T1474)\n* [URI Hijacking](/techniques/T1416)\n\n\nMinor Technique changes:\nNo changes\n\nTechnique revocations:\n\n* URL Scheme Hijacking (revoked by [URI Hijacking](/techniques/T1416))\n\n\nTechnique deprecations:\nNo changes\n\n### Software\n\n**Enterprise**\n\nNew Software:\n\n* [Anchor](/software/S0504)\n* [Bonadan](/software/S0486)\n* [Carberp](/software/S0484)\n* [CookieMiner](/software/S0492)\n* [CrackMapExec](/software/S0488)\n* [Cryptoistic](/software/S0498)\n* [Dacls](/software/S0497)\n* [Drovorub](/software/S0502)\n* [FatDuke](/software/S0512)\n* [FrameworkPOS](/software/S0503)\n* [GoldenSpy](/software/S0493)\n* [Hancitor](/software/S0499)\n* [IcedID](/software/S0483)\n* [Kessel](/software/S0487)\n* [MCMD](/software/S0500)\n* [Ngrok](/software/S0508)\n* [Pillowmint](/software/S0517)\n* [PipeMon](/software/S0501)\n* [PolyglotDuke](/software/S0518)\n* [RDAT](/software/S0495)\n* [REvil](/software/S0496)\n* [RegDuke](/software/S0511)\n* [SYNful Knock](/software/S0519)\n* [SoreFang](/software/S0516)\n* [StrongPity](/software/S0491)\n* [WellMail](/software/S0515)\n* [WellMess](/software/S0514)\n\n\nSoftware changes:\n\n* [BADNEWS](/software/S0128)\n* [Cobalt Strike](/software/S0154)\n* [Ebury](/software/S0377)\n* [Emotet](/software/S0367)\n* [InvisiMole](/software/S0260)\n* [KONNI](/software/S0356)\n* [LoudMiner](/software/S0451)\n* [Machete](/software/S0409)\n* [Maze](/software/S0449)\n* [Metamorfo](/software/S0455)\n* [MiniDuke](/software/S0051)\n* [NETWIRE](/software/S0198)\n* [OnionDuke](/software/S0052)\n* [SDelete](/software/S0195)\n* [TrickBot](/software/S0266)\n* [Trojan.Karagany](/software/S0094)\n* [Valak](/software/S0476)\n* [WEBC2](/software/S0109)\n* [gh0st RAT](/software/S0032)\n* [njRAT](/software/S0385)\n\n\nMinor Software changes:\n\n* [HiddenWasp](/software/S0394)\n* [JPIN](/software/S0201)\n* [OSX/Shlayer](/software/S0402)\n* [RATANKBA](/software/S0241)\n* [pwdump](/software/S0006)\n\n\nSoftware revocations:\nNo changes\n\nSoftware deprecations:\nNo changes\n\nSoftware deletions:\n\n* Twitoor\n\n\n**Mobile**\n\nNew Software:\n\n* [Desert Scorpion](/software/S0505)\n* [FakeSpy](/software/S0509)\n* [Mandrake](/software/S0485)\n* [Twitoor](/software/S0302)\n* [ViperRAT](/software/S0506)\n* [WolfRAT](/software/S0489)\n* [XLoader for iOS](/software/S0490)\n* [Zen](/software/S0494)\n* [eSurv](/software/S0507)\n\n\nSoftware changes:\n\n* [Anubis](/software/S0422)\n* [Bread](/software/S0432)\n* [Cerberus](/software/S0480)\n* [Corona Updates](/software/S0425)\n* [Dendroid](/software/S0301)\n* [Ginp](/software/S0423)\n* [Rotexy](/software/S0411)\n* [Stealth Mango](/software/S0328)\n* [TrickMo](/software/S0427)\n* [XLoader for Android](/software/S0318)\n\n\nMinor Software changes:\nNo changes\n\nSoftware revocations:\nNo changes\n\nSoftware deprecations:\nNo changes\n\n### Groups\n\n**Enterprise**\n\nNew Groups:\n\n* [Bouncing Golf](/groups/G0097)\n* [Chimera](/groups/G0114)\n* [GOLD SOUTHFIELD](/groups/G0115)\n\n\nGroup changes:\n\n* [APT1](/groups/G0006)\n* [APT16](/groups/G0023)\n* [APT17](/groups/G0025)\n* [APT28](/groups/G0007)\n* [APT29](/groups/G0016)\n* [APT30](/groups/G0013)\n* [APT37](/groups/G0067)\n* [APT39](/groups/G0087)\n* [Cleaver](/groups/G0003)\n* [Dragonfly](/groups/G0035)\n* [Dragonfly 2.0](/groups/G0074)\n* [FIN6](/groups/G0037)\n* [FIN7](/groups/G0046)\n* [Gamaredon Group](/groups/G0047)\n* [Lazarus Group](/groups/G0032)\n* [Machete](/groups/G0095)\n* [MuddyWater](/groups/G0069)\n* [Night Dragon](/groups/G0014)\n* [OilRig](/groups/G0049)\n* [PROMETHIUM](/groups/G0056)\n* [Patchwork](/groups/G0040)\n* [TEMP.Veles](/groups/G0088)\n* [Turla](/groups/G0010)\n* [Winnti Group](/groups/G0044)\n* [Wizard Spider](/groups/G0102)\n* [menuPass](/groups/G0045)\n\n\nMinor Group changes:\n\n* [APT-C-36](/groups/G0099)\n* [Honeybee](/groups/G0072)\n\n\nGroup revocations:\nNo changes\n\nGroup deprecations:\nNo changes\n\n**Mobile**\n\nNew Groups:\nNo changes\n\nGroup changes:\n\n* [APT28](/groups/G0007)\n\n\nMinor Group changes:\nNo changes\n\nGroup revocations:\nNo changes\n\nGroup deprecations:\nNo changes\n\n### Mitigations\n\n**Enterprise**\n\nNew Mitigations:\n\n* [Pre-compromise](/mitigations/M1056)\n\n\nMitigation changes:\n\n* [User Training](/mitigations/M1017)\n\n\nMinor Mitigation changes:\nNo changes\n\nMitigation revocations:\nNo changes\n\nMitigation deprecations:\nNo changes\n\n**Mobile**\n\nNew Mitigations:\nNo changes\n\nMitigation changes:\nNo changes\n\nMinor Mitigation changes:\nNo changes\n\nMitigation revocations:\nNo changes\n\nMitigation deprecations:\nNo changes\n" - }, - { - "url": "https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v7.2/enterprise-attack/enterprise-attack.json", - "version": "7.2.0", - "modified": "2020-07-15T10:41:41.536219Z" - } - ], - "description": "The Enterprise domain of the ATT&CK dataset", - "created": "2018-01-16T19:15:30.610172Z", - "id": "x-mitre-collection--0bbd7841-f053-471a-9900-da4af02e40c2", - "name": "Enterprise ATT&CK" - }, - { - "versions": [ - { - "url": "https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v8.0/mobile-attack/mobile-attack.json", - "version": "8.0.0", - "modified": "2020-10-27T08:51:03.896157Z" - } - ], - "description": "The Mobile domain of the ATT&CK dataset", - "created": "2018-01-16T19:15:30.610172Z", - "id": "x-mitre-collection--915b6504-bde8-40b5-bfda-0c3ecb46a9b9", - "name": "Mobile ATT&CK" - } - ] + collection_index: { + id: 'bb8c95c0-4e8f-491e-a3c9-8b4207e43041', + name: 'MITRE ATT&CK', + description: 'All ATT&CK datasets', + collections: [ + { + versions: [ + { + url: 'https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v8.0/enterprise-attack/enterprise-attack.json', + version: '8.0.0', + modified: '2020-10-27T08:51:03.896157Z', + release_notes: + "The October 2020 (v8) ATT&CK release updates Techniques, Groups, and Software for both Enterprise and Mobile. The biggest changes are the deprecation of the PRE-ATT&CK domain, the addition of two new Tactics to replace [PRE-ATT&CK](/versions/v7/matrices/pre/), and the addition of the Network platform to Enterprise ATT&CK.\n\nThis version of ATT&CK for Enterprise contains 14 Tactics, 177 Techniques, and 348 Sub-techniques.\n\n#### Retirement of PRE-ATT&CK\nThis release deprecates and removes the PRE-ATT&CK domain from ATT&CK, replacing its scope with two new Tactics in Enterprise ATT&CK [Reconnaissance](/tactics/TA0043/) and [Resource Development](/tactics/TA0042/). A new platform has also been added to ATT&CK to represent the environment these techniques occur in, [PRE](/matrices/enterprise/pre/). The previous contents of PRE-ATT&CK have been preserved [here](/versions/v7/matrices/pre/). See [the accompanying blog post](https://medium.com/mitre-attack/the-retirement-of-pre-attack-4b73ffecd3d3) for more details.\n\n#### New techniques in Reconnaissance:\n\n* [Active Scanning](/techniques/T1595)\n\t* [Scanning IP Blocks](/techniques/T1595/001)\n\t* [Vulnerability Scanning](/techniques/T1595/002)\n* [Gather Victim Host Information](/techniques/T1592)\n\t* [Client Configurations](/techniques/T1592/004)\n\t* [Firmware](/techniques/T1592/003)\n\t* [Hardware](/techniques/T1592/001)\n\t* [Software](/techniques/T1592/002)\n* [Gather Victim Identity Information](/techniques/T1589)\n\t* [Credentials](/techniques/T1589/001)\n\t* [Email Addresses](/techniques/T1589/002)\n\t* [Employee Names](/techniques/T1589/003)\n* [Gather Victim Network Information](/techniques/T1590)\n\t* [DNS](/techniques/T1590/002)\n\t* [Domain Properties](/techniques/T1590/001)\n\t* [IP Addresses](/techniques/T1590/005)\n\t* [Network Security Appliances](/techniques/T1590/006)\n\t* [Network Topology](/techniques/T1590/004)\n\t* [Network Trust Dependencies](/techniques/T1590/003)\n* [Gather Victim Org Information](/techniques/T1591)\n\t* [Business Relationships](/techniques/T1591/002)\n\t* [Determine Physical Locations](/techniques/T1591/001)\n\t* [Identify Business Tempo](/techniques/T1591/003)\n\t* [Identify Roles](/techniques/T1591/004)\n* [Phishing for Information](/techniques/T1598)\n\t* [Spearphishing Attachment](/techniques/T1598/002)\n\t* [Spearphishing Link](/techniques/T1598/003)\n\t* [Spearphishing Service](/techniques/T1598/001)\n* [Search Closed Sources](/techniques/T1597)\n\t* [Purchase Technical Data](/techniques/T1597/002)\n\t* [Threat Intel Vendors](/techniques/T1597/001)\n* [Search Open Technical Databases](/techniques/T1596)\n\t* [CDNs](/techniques/T1596/004)\n\t* [DNS/Passive DNS](/techniques/T1596/001)\n\t* [Digital Certificates](/techniques/T1596/003)\n\t* [Scan Databases](/techniques/T1596/005)\n\t* [WHOIS](/techniques/T1596/002)\n* [Search Open Websites/Domains](/techniques/T1593)\n\t* [Search Engines](/techniques/T1593/002)\n\t* [Social Media](/techniques/T1593/001)\n* [Search Victim-Owned Websites](/techniques/T1594)\n\n#### New techniques in Resource Development:\n\n* [Acquire Infrastructure](/techniques/T1583)\n\t* [Botnet](/techniques/T1583/005)\n\t* [DNS Server](/techniques/T1583/002)\n\t* [Domains](/techniques/T1583/001)\n\t* [Server](/techniques/T1583/004)\n\t* [Virtual Private Server](/techniques/T1583/003)\n\t* [Web Services](/techniques/T1583/006)\n* [Compromise Accounts](/techniques/T1586)\n\t* [Email Accounts](/techniques/T1586/002)\n\t* [Social Media Accounts](/techniques/T1586/001)\n* [Compromise Infrastructure](/techniques/T1584)\n\t* [Botnet](/techniques/T1584/005)\n\t* [DNS Server](/techniques/T1584/002)\n\t* [Domains](/techniques/T1584/001)\n\t* [Server](/techniques/T1584/004)\n\t* [Virtual Private Server](/techniques/T1584/003)\n\t* [Web Services](/techniques/T1584/006)\n* [Develop Capabilities](/techniques/T1587)\n\t* [Code Signing Certificates](/techniques/T1587/002)\n\t* [Digital Certificates](/techniques/T1587/003)\n\t* [Exploits](/techniques/T1587/004)\n\t* [Malware](/techniques/T1587/001)\n* [Establish Accounts](/techniques/T1585)\n\t* [Email Accounts](/techniques/T1585/002)\n\t* [Social Media Accounts](/techniques/T1585/001)\n* [Obtain Capabilities](/techniques/T1588)\n\t* [Code Signing Certificates](/techniques/T1588/003)\n\t* [Digital Certificates](/techniques/T1588/004)\n\t* [Exploits](/techniques/T1588/005)\n\t* [Malware](/techniques/T1588/001)\n\t* [Tool](/techniques/T1588/002)\n\t* [Vulnerabilities](/techniques/T1588/006)\n\n\n#### ATT&CK for Network Infrastructure Devices\n\n13 techniques and 15 sub-techniques have been added or modified to cover adversary behavior against network infrastructure devices that constitute the fabric of enterprises' networks such as switches and routers. These techniques are represented by a new platform in ATT&CK for Enterprise, [Network](/matrices/enterprise/network/).\n\n#### New and updated techniques for Network:\n* [Exploit Public-Facing Application](/techniques/T1190)\n* [Command and Scripting Interpreter](/techniques/T1059)\n\t* [Network Device CLI](/techniques/T1059/008)\n* [Pre-OS Boot](/techniques/T1542)\n\t* [ROMMONkit](/techniques/T1542/004)\n\t* [TFTP Boot](/techniques/T1542/005)\n* [Traffic Signaling](/techniques/T1205)\n\t* [Port Knocking](/techniques/T1205/001)\n* [Modify Authentication Process](/techniques/T1556)\n\t* [Network Device Authentication](/techniques/T1556/004)\n* [Modify System Image](/techniques/T1601)\n\t* [Downgrade System Image](/techniques/T1601/002)\n\t* [Patch System Image](/techniques/T1601/001)\n* [Network Boundary Bridging](/techniques/T1599)\n\t* [Network Address Translation Traversal](/techniques/T1599/001)\n* [Weaken Encryption](/techniques/T1600)\n\t* [Disable Crypto Hardware](/techniques/T1600/002)\n\t* [Reduce Key Space](/techniques/T1600/001)\n* [Data from Configuration Repository](/techniques/T1602)\n\t* [Network Device Configuration Dump](/techniques/T1602/002)\n\t* [SNMP (MIB Dump)](/techniques/T1602/001)\n* [Input Capture](/techniques/T1056)\n\t* [Keylogging](/techniques/T1056/001)\n* [Non-Application Layer Protocol](/techniques/T1095)\n* [Proxy](/techniques/T1090)\n\t* [Multi-hop Proxy](/techniques/T1090/003)\t\n* [Automated Exfiltration](/techniques/T1020)\n\t* [Traffic Duplication](/techniques/T1020/001)\n\n\nMany of the new Network techniques and sub-techniques focus on embedded network devices running closed source proprietary operating systems. This is largely driven by behaviors present in reported in the wild intrusions. Many newer devices leverage commodity embedded operating systems such as Linux or BSD variants, but accounts of adversary activity against these have been more sparse. However, network infrastructure devices running proprietary operating systems are still widely deployed on the Internet and within enterprises.\n\nWe will continue to build out additional Network techniques and sub-techniques as they become known. We welcome contributions and feedback from the community and look to improve this representation of behaviors in the network infrastructure devices space.\n\n### Techniques\n\n**Enterprise**\n\nWe also added 1 additional new technique and 7 sub-techniques to Enterprise in this ATT&CK release beyond the scope of the above updates. All Enterprise technique changes, including this new technique and these new sub-techniques, are documented below.\n\nNew Techniques:\n\n* [Acquire Infrastructure](/techniques/T1583)\n\t* [Botnet](/techniques/T1583/005)\n\t* [DNS Server](/techniques/T1583/002)\n\t* [Domains](/techniques/T1583/001)\n\t* [Server](/techniques/T1583/004)\n\t* [Virtual Private Server](/techniques/T1583/003)\n\t* [Web Services](/techniques/T1583/006)\n* [Active Scanning](/techniques/T1595)\n\t* [Scanning IP Blocks](/techniques/T1595/001)\n\t* [Vulnerability Scanning](/techniques/T1595/002)\n* Automated Exfiltration: [Traffic Duplication](/techniques/T1020/001)\n* Boot or Logon Autostart Execution: [Print Processors](/techniques/T1547/012)\n* [Cloud Infrastructure Discovery](/techniques/T1580)\n* Command and Scripting Interpreter: [Network Device CLI](/techniques/T1059/008)\n* [Compromise Accounts](/techniques/T1586)\n\t* [Email Accounts](/techniques/T1586/002)\n\t* [Social Media Accounts](/techniques/T1586/001)\n* [Compromise Infrastructure](/techniques/T1584)\n\t* [Botnet](/techniques/T1584/005)\n\t* [DNS Server](/techniques/T1584/002)\n\t* [Domains](/techniques/T1584/001)\n\t* [Server](/techniques/T1584/004)\n\t* [Virtual Private Server](/techniques/T1584/003)\n\t* [Web Services](/techniques/T1584/006)\n* [Data from Configuration Repository](/techniques/T1602)\n\t* [Network Device Configuration Dump](/techniques/T1602/002)\n\t* [SNMP (MIB Dump)](/techniques/T1602/001)\n* [Develop Capabilities](/techniques/T1587)\n\t* [Code Signing Certificates](/techniques/T1587/002)\n\t* [Digital Certificates](/techniques/T1587/003)\n\t* [Exploits](/techniques/T1587/004)\n\t* [Malware](/techniques/T1587/001)\n* [Establish Accounts](/techniques/T1585)\n\t* [Email Accounts](/techniques/T1585/002)\n\t* [Social Media Accounts](/techniques/T1585/001)\n* [Gather Victim Host Information](/techniques/T1592)\n\t* [Client Configurations](/techniques/T1592/004)\n\t* [Firmware](/techniques/T1592/003)\n\t* [Hardware](/techniques/T1592/001)\n\t* [Software](/techniques/T1592/002)\n* [Gather Victim Identity Information](/techniques/T1589)\n\t* [Credentials](/techniques/T1589/001)\n\t* [Email Addresses](/techniques/T1589/002)\n\t* [Employee Names](/techniques/T1589/003)\n* [Gather Victim Network Information](/techniques/T1590)\n\t* [DNS](/techniques/T1590/002)\n\t* [Domain Properties](/techniques/T1590/001)\n\t* [IP Addresses](/techniques/T1590/005)\n\t* [Network Security Appliances](/techniques/T1590/006)\n\t* [Network Topology](/techniques/T1590/004)\n\t* [Network Trust Dependencies](/techniques/T1590/003)\n* [Gather Victim Org Information](/techniques/T1591)\n\t* [Business Relationships](/techniques/T1591/002)\n\t* [Determine Physical Locations](/techniques/T1591/001)\n\t* [Identify Business Tempo](/techniques/T1591/003)\n\t* [Identify Roles](/techniques/T1591/004)\n* Hide Artifacts: [VBA Stomping](/techniques/T1564/007)\n* Impair Defenses: [Disable Cloud Logs](/techniques/T1562/008)\n* Man-in-the-Middle: [ARP Cache Poisoning](/techniques/T1557/002)\n* Modify Authentication Process: [Network Device Authentication](/techniques/T1556/004)\n* [Modify System Image](/techniques/T1601)\n\t* [Downgrade System Image](/techniques/T1601/002)\n\t* [Patch System Image](/techniques/T1601/001)\n* [Network Boundary Bridging](/techniques/T1599)\n\t* [Network Address Translation Traversal](/techniques/T1599/001)\n* [Obtain Capabilities](/techniques/T1588)\n\t* [Code Signing Certificates](/techniques/T1588/003)\n\t* [Digital Certificates](/techniques/T1588/004)\n\t* [Exploits](/techniques/T1588/005)\n\t* [Malware](/techniques/T1588/001)\n\t* [Tool](/techniques/T1588/002)\n\t* [Vulnerabilities](/techniques/T1588/006)\n* [Phishing for Information](/techniques/T1598)\n\t* [Spearphishing Attachment](/techniques/T1598/002)\n\t* [Spearphishing Link](/techniques/T1598/003)\n\t* [Spearphishing Service](/techniques/T1598/001)\n* Pre-OS Boot: [ROMMONkit](/techniques/T1542/004)\n* Pre-OS Boot: [TFTP Boot](/techniques/T1542/005)\n* Scheduled Task/Job: [Systemd Timers](/techniques/T1053/006)\n* [Search Closed Sources](/techniques/T1597)\n\t* [Purchase Technical Data](/techniques/T1597/002)\n\t* [Threat Intel Vendors](/techniques/T1597/001)\n* [Search Open Technical Databases](/techniques/T1596)\n\t* [CDNs](/techniques/T1596/004)\n\t* [DNS/Passive DNS](/techniques/T1596/001)\n\t* [Digital Certificates](/techniques/T1596/003)\n\t* [Scan Databases](/techniques/T1596/005)\n\t* [WHOIS](/techniques/T1596/002)\n* [Search Open Websites/Domains](/techniques/T1593)\n\t* [Search Engines](/techniques/T1593/002)\n\t* [Social Media](/techniques/T1593/001)\n* [Search Victim-Owned Websites](/techniques/T1594)\n* Signed Binary Proxy Execution: [Verclsid](/techniques/T1218/012)\n* Steal or Forge Kerberos Tickets: [AS-REP Roasting](/techniques/T1558/004)\n* [Weaken Encryption](/techniques/T1600)\n\t* [Disable Crypto Hardware](/techniques/T1600/002)\n\t* [Reduce Key Space](/techniques/T1600/001)\n\n\nTechnique changes:\n\n* Abuse Elevation Control Mechanism: [Bypass User Account Control](/techniques/T1548/002)\n* [Account Discovery](/techniques/T1087)\n\t* [Cloud Account](/techniques/T1087/004)\n* Account Manipulation: [Additional Cloud Credentials](/techniques/T1098/001)\n* [Automated Exfiltration](/techniques/T1020)\n* [Boot or Logon Autostart Execution](/techniques/T1547)\n\t* [Registry Run Keys / Startup Folder](/techniques/T1547/001)\n* [Boot or Logon Initialization Scripts](/techniques/T1037)\n* Brute Force: [Credential Stuffing](/techniques/T1110/004)\n* Brute Force: [Password Cracking](/techniques/T1110/002)\n* Brute Force: [Password Guessing](/techniques/T1110/001)\n* Brute Force: [Password Spraying](/techniques/T1110/003)\n* [Command and Scripting Interpreter](/techniques/T1059)\n\t* [AppleScript](/techniques/T1059/002)\n\t* [Visual Basic](/techniques/T1059/005)\n* Create or Modify System Process: [Launch Daemon](/techniques/T1543/004)\n* Create or Modify System Process: [Systemd Service](/techniques/T1543/002)\n* Create or Modify System Process: [Windows Service](/techniques/T1543/003)\n* [Data from Information Repositories](/techniques/T1213)\n* Endpoint Denial of Service: [OS Exhaustion Flood](/techniques/T1499/001)\n* Endpoint Denial of Service: [Service Exhaustion Flood](/techniques/T1499/002)\n* [Event Triggered Execution](/techniques/T1546)\n\t* [Image File Execution Options Injection](/techniques/T1546/012)\n* [Exploit Public-Facing Application](/techniques/T1190)\n* [File and Directory Discovery](/techniques/T1083)\n* File and Directory Permissions Modification: [Windows File and Directory Permissions Modification](/techniques/T1222/001)\n* [Hardware Additions](/techniques/T1200)\n* Hijack Execution Flow: [LD_PRELOAD](/techniques/T1574/006)\n* Hijack Execution Flow: [Path Interception by Unquoted Path](/techniques/T1574/009)\n* Impair Defenses: [Impair Command History Logging](/techniques/T1562/003)\n* Indicator Removal on Host: [Clear Command History](/techniques/T1070/003)\n* [Input Capture](/techniques/T1056)\n\t* [Keylogging](/techniques/T1056/001)\n* [Man-in-the-Middle](/techniques/T1557)\n* [Modify Authentication Process](/techniques/T1556)\n* [Modify Registry](/techniques/T1112)\n* Network Denial of Service: [Direct Network Flood](/techniques/T1498/001)\n* Network Denial of Service: [Reflection Amplification](/techniques/T1498/002)\n* [Network Share Discovery](/techniques/T1135)\n* [Non-Application Layer Protocol](/techniques/T1095)\n* Obfuscated Files or Information: [Binary Padding](/techniques/T1027/001)\n* Obfuscated Files or Information: [Steganography](/techniques/T1027/003)\n* [Password Policy Discovery](/techniques/T1201)\n* [Permission Groups Discovery](/techniques/T1069)\n\t* [Cloud Groups](/techniques/T1069/003)\n* [Phishing](/techniques/T1566)\n\t* [Spearphishing Attachment](/techniques/T1566/001)\n\t* [Spearphishing Link](/techniques/T1566/002)\n\t* [Spearphishing via Service](/techniques/T1566/003)\n* [Pre-OS Boot](/techniques/T1542)\n\t* [Bootkit](/techniques/T1542/003)\n* [Proxy](/techniques/T1090)\n\t* [Domain Fronting](/techniques/T1090/004)\n\t* [Multi-hop Proxy](/techniques/T1090/003)\n* [Remote System Discovery](/techniques/T1018)\n* Server Software Component: [Web Shell](/techniques/T1505/003)\n* [Service Stop](/techniques/T1489)\n* Signed Binary Proxy Execution: [Control Panel](/techniques/T1218/002)\n* [Software Deployment Tools](/techniques/T1072)\n* [Software Discovery](/techniques/T1518)\n\t* [Security Software Discovery](/techniques/T1518/001)\n* [Steal or Forge Kerberos Tickets](/techniques/T1558)\n\t* [Kerberoasting](/techniques/T1558/003)\n* [Traffic Signaling](/techniques/T1205)\n\t* [Port Knocking](/techniques/T1205/001)\n* [Unsecured Credentials](/techniques/T1552)\n\t* [Cloud Instance Metadata API](/techniques/T1552/005)\n* Use Alternate Authentication Material: [Application Access Token](/techniques/T1550/001)\n* Use Alternate Authentication Material: [Web Session Cookie](/techniques/T1550/004)\n* Valid Accounts: [Cloud Accounts](/techniques/T1078/004)\n* Valid Accounts: [Default Accounts](/techniques/T1078/001)\n* Valid Accounts: [Domain Accounts](/techniques/T1078/002)\n\n\nMinor Technique changes:\n\n* [Abuse Elevation Control Mechanism](/techniques/T1548)\n* [Account Manipulation](/techniques/T1098)\n* [Application Layer Protocol](/techniques/T1071)\n\t* [DNS](/techniques/T1071/004)\n\t* [File Transfer Protocols](/techniques/T1071/002)\n\t* [Mail Protocols](/techniques/T1071/003)\n* [Archive Collected Data](/techniques/T1560)\n* [Brute Force](/techniques/T1110)\n* [Create or Modify System Process](/techniques/T1543)\n* [Data Encrypted for Impact](/techniques/T1486)\n* [Data Staged](/techniques/T1074)\n\t* [Remote Data Staging](/techniques/T1074/002)\n* [Domain Trust Discovery](/techniques/T1482)\n* [Dynamic Resolution](/techniques/T1568)\n\t* [Domain Generation Algorithms](/techniques/T1568/002)\n* Email Collection: [Email Forwarding Rule](/techniques/T1114/003)\n* [Endpoint Denial of Service](/techniques/T1499)\n* [File and Directory Permissions Modification](/techniques/T1222)\n* [Hide Artifacts](/techniques/T1564)\n\t* [Hidden Users](/techniques/T1564/002)\n* [Hijack Execution Flow](/techniques/T1574)\n\t* [DLL Side-Loading](/techniques/T1574/002)\n\t* [Dylib Hijacking](/techniques/T1574/004)\n\t* [Path Interception by PATH Environment Variable](/techniques/T1574/007)\n\t* [Path Interception by Search Order Hijacking](/techniques/T1574/008)\n\t* [Services File Permissions Weakness](/techniques/T1574/010)\n\t* [Services Registry Permissions Weakness](/techniques/T1574/011)\n* [Impair Defenses](/techniques/T1562)\n\t* [Disable or Modify Cloud Firewall](/techniques/T1562/007)\n* [Indicator Removal on Host](/techniques/T1070)\n* [Internal Spearphishing](/techniques/T1534)\n* Modify Authentication Process: [Domain Controller Authentication](/techniques/T1556/001)\n* [Modify Cloud Compute Infrastructure](/techniques/T1578)\n\t* [Create Cloud Instance](/techniques/T1578/002)\n\t* [Create Snapshot](/techniques/T1578/001)\n\t* [Delete Cloud Instance](/techniques/T1578/003)\n* [Network Denial of Service](/techniques/T1498)\n* [Obfuscated Files or Information](/techniques/T1027)\n* [Scheduled Task/Job](/techniques/T1053)\n* [Server Software Component](/techniques/T1505)\n* [Signed Binary Proxy Execution](/techniques/T1218)\n* [Supply Chain Compromise](/techniques/T1195)\n* [Use Alternate Authentication Material](/techniques/T1550)\n* [Valid Accounts](/techniques/T1078)\n\n\nTechnique revocations:\nNo changes\n\nTechnique deprecations:\nNo changes\n\n**Mobile**\n\nNew Techniques:\n\n* [Geofencing](/techniques/T1581)\n* [SMS Control](/techniques/T1582)\n\n\nTechnique changes:\n\n* [Delete Device Data](/techniques/T1447)\n* [Supply Chain Compromise](/techniques/T1474)\n* [URI Hijacking](/techniques/T1416)\n\n\nMinor Technique changes:\nNo changes\n\nTechnique revocations:\n\n* URL Scheme Hijacking (revoked by [URI Hijacking](/techniques/T1416))\n\n\nTechnique deprecations:\nNo changes\n\n### Software\n\n**Enterprise**\n\nNew Software:\n\n* [Anchor](/software/S0504)\n* [Bonadan](/software/S0486)\n* [Carberp](/software/S0484)\n* [CookieMiner](/software/S0492)\n* [CrackMapExec](/software/S0488)\n* [Cryptoistic](/software/S0498)\n* [Dacls](/software/S0497)\n* [Drovorub](/software/S0502)\n* [FatDuke](/software/S0512)\n* [FrameworkPOS](/software/S0503)\n* [GoldenSpy](/software/S0493)\n* [Hancitor](/software/S0499)\n* [IcedID](/software/S0483)\n* [Kessel](/software/S0487)\n* [MCMD](/software/S0500)\n* [Ngrok](/software/S0508)\n* [Pillowmint](/software/S0517)\n* [PipeMon](/software/S0501)\n* [PolyglotDuke](/software/S0518)\n* [RDAT](/software/S0495)\n* [REvil](/software/S0496)\n* [RegDuke](/software/S0511)\n* [SYNful Knock](/software/S0519)\n* [SoreFang](/software/S0516)\n* [StrongPity](/software/S0491)\n* [WellMail](/software/S0515)\n* [WellMess](/software/S0514)\n\n\nSoftware changes:\n\n* [BADNEWS](/software/S0128)\n* [Cobalt Strike](/software/S0154)\n* [Ebury](/software/S0377)\n* [Emotet](/software/S0367)\n* [InvisiMole](/software/S0260)\n* [KONNI](/software/S0356)\n* [LoudMiner](/software/S0451)\n* [Machete](/software/S0409)\n* [Maze](/software/S0449)\n* [Metamorfo](/software/S0455)\n* [MiniDuke](/software/S0051)\n* [NETWIRE](/software/S0198)\n* [OnionDuke](/software/S0052)\n* [SDelete](/software/S0195)\n* [TrickBot](/software/S0266)\n* [Trojan.Karagany](/software/S0094)\n* [Valak](/software/S0476)\n* [WEBC2](/software/S0109)\n* [gh0st RAT](/software/S0032)\n* [njRAT](/software/S0385)\n\n\nMinor Software changes:\n\n* [HiddenWasp](/software/S0394)\n* [JPIN](/software/S0201)\n* [OSX/Shlayer](/software/S0402)\n* [RATANKBA](/software/S0241)\n* [pwdump](/software/S0006)\n\n\nSoftware revocations:\nNo changes\n\nSoftware deprecations:\nNo changes\n\nSoftware deletions:\n\n* Twitoor\n\n\n**Mobile**\n\nNew Software:\n\n* [Desert Scorpion](/software/S0505)\n* [FakeSpy](/software/S0509)\n* [Mandrake](/software/S0485)\n* [Twitoor](/software/S0302)\n* [ViperRAT](/software/S0506)\n* [WolfRAT](/software/S0489)\n* [XLoader for iOS](/software/S0490)\n* [Zen](/software/S0494)\n* [eSurv](/software/S0507)\n\n\nSoftware changes:\n\n* [Anubis](/software/S0422)\n* [Bread](/software/S0432)\n* [Cerberus](/software/S0480)\n* [Corona Updates](/software/S0425)\n* [Dendroid](/software/S0301)\n* [Ginp](/software/S0423)\n* [Rotexy](/software/S0411)\n* [Stealth Mango](/software/S0328)\n* [TrickMo](/software/S0427)\n* [XLoader for Android](/software/S0318)\n\n\nMinor Software changes:\nNo changes\n\nSoftware revocations:\nNo changes\n\nSoftware deprecations:\nNo changes\n\n### Groups\n\n**Enterprise**\n\nNew Groups:\n\n* [Bouncing Golf](/groups/G0097)\n* [Chimera](/groups/G0114)\n* [GOLD SOUTHFIELD](/groups/G0115)\n\n\nGroup changes:\n\n* [APT1](/groups/G0006)\n* [APT16](/groups/G0023)\n* [APT17](/groups/G0025)\n* [APT28](/groups/G0007)\n* [APT29](/groups/G0016)\n* [APT30](/groups/G0013)\n* [APT37](/groups/G0067)\n* [APT39](/groups/G0087)\n* [Cleaver](/groups/G0003)\n* [Dragonfly](/groups/G0035)\n* [Dragonfly 2.0](/groups/G0074)\n* [FIN6](/groups/G0037)\n* [FIN7](/groups/G0046)\n* [Gamaredon Group](/groups/G0047)\n* [Lazarus Group](/groups/G0032)\n* [Machete](/groups/G0095)\n* [MuddyWater](/groups/G0069)\n* [Night Dragon](/groups/G0014)\n* [OilRig](/groups/G0049)\n* [PROMETHIUM](/groups/G0056)\n* [Patchwork](/groups/G0040)\n* [TEMP.Veles](/groups/G0088)\n* [Turla](/groups/G0010)\n* [Winnti Group](/groups/G0044)\n* [Wizard Spider](/groups/G0102)\n* [menuPass](/groups/G0045)\n\n\nMinor Group changes:\n\n* [APT-C-36](/groups/G0099)\n* [Honeybee](/groups/G0072)\n\n\nGroup revocations:\nNo changes\n\nGroup deprecations:\nNo changes\n\n**Mobile**\n\nNew Groups:\nNo changes\n\nGroup changes:\n\n* [APT28](/groups/G0007)\n\n\nMinor Group changes:\nNo changes\n\nGroup revocations:\nNo changes\n\nGroup deprecations:\nNo changes\n\n### Mitigations\n\n**Enterprise**\n\nNew Mitigations:\n\n* [Pre-compromise](/mitigations/M1056)\n\n\nMitigation changes:\n\n* [User Training](/mitigations/M1017)\n\n\nMinor Mitigation changes:\nNo changes\n\nMitigation revocations:\nNo changes\n\nMitigation deprecations:\nNo changes\n\n**Mobile**\n\nNew Mitigations:\nNo changes\n\nMitigation changes:\nNo changes\n\nMinor Mitigation changes:\nNo changes\n\nMitigation revocations:\nNo changes\n\nMitigation deprecations:\nNo changes\n", + }, + { + url: 'https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v7.2/enterprise-attack/enterprise-attack.json', + version: '7.2.0', + modified: '2020-07-15T10:41:41.536219Z', + }, + ], + description: 'The Enterprise domain of the ATT&CK dataset', + created: '2018-01-16T19:15:30.610172Z', + id: 'x-mitre-collection--0bbd7841-f053-471a-9900-da4af02e40c2', + name: 'Enterprise ATT&CK', + }, + { + versions: [ + { + url: 'https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v8.0/mobile-attack/mobile-attack.json', + version: '8.0.0', + modified: '2020-10-27T08:51:03.896157Z', + }, + ], + description: 'The Mobile domain of the ATT&CK dataset', + created: '2018-01-16T19:15:30.610172Z', + id: 'x-mitre-collection--915b6504-bde8-40b5-bfda-0c3ecb46a9b9', + name: 'Mobile ATT&CK', + }, + ], + }, + workspace: { + remote_url: 'https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v8.0/collection_index.json', + update_policy: { + automatic: true, + interval: 300, + last_retrieval: '2020-11-23T13:35:54.375623Z', + subscriptions: ['x-mitre-collection--0bbd7841-f053-471a-9900-da4af02e40c2'], }, - "workspace": { - "remote_url": "https://raw.githubusercontent.com/mitre/cti/ATT%26CK-v8.0/collection_index.json", - "update_policy": { - "automatic": true, - "interval": 300, - "last_retrieval": "2020-11-23T13:35:54.375623Z", - "subscriptions": [ - "x-mitre-collection--0bbd7841-f053-471a-9900-da4af02e40c2" - ] - } - } -} + }, +}; describe('Collection Indexes Basic API', function () { - let app; - let passportCookie; + let app; + let passportCookie; - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); - // Initialize the express app - app = await require('../../../index').initializeApp(); + // Initialize the express app + app = await require('../../../index').initializeApp(); - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); - it('GET /api/collection-indexes returns an empty array', function (done) { - request(app) - .get('/api/collection-indexes') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get an empty array - const collectionIndexes = res.body; - expect(collectionIndexes).toBeDefined(); - expect(Array.isArray(collectionIndexes)).toBe(true); - expect(collectionIndexes.length).toBe(0); - done(); - } - }); - }); + it('GET /api/collection-indexes returns an empty array', function (done) { + request(app) + .get('/api/collection-indexes') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get an empty array + const collectionIndexes = res.body; + expect(collectionIndexes).toBeDefined(); + expect(Array.isArray(collectionIndexes)).toBe(true); + expect(collectionIndexes.length).toBe(0); + done(); + } + }); + }); - it('POST /api/collection-indexes does not create an empty collection index', function (done) { - const body = {}; - request(app) - .post('/api/collection-indexes') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); + it('POST /api/collection-indexes does not create an empty collection index', function (done) { + const body = {}; + request(app) + .post('/api/collection-indexes') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); - let collectionIndex1; - it('POST /api/collection-indexes creates a collection index', function (done) { - const timestamp = new Date().toISOString(); - initialObjectData.collection_index.created = timestamp; - initialObjectData.collection_index.modified = timestamp; - const body = initialObjectData; - request(app) - .post('/api/collection-indexes') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get the created collection index - collectionIndex1 = res.body; - expect(collectionIndex1).toBeDefined(); - expect(collectionIndex1.collection_index).toBeDefined(); - expect(collectionIndex1.collection_index.id).toBeDefined(); - expect(collectionIndex1.collection_index.created).toBeDefined(); - expect(collectionIndex1.collection_index.modified).toBeDefined(); - done(); - } - }); - }); + let collectionIndex1; + it('POST /api/collection-indexes creates a collection index', function (done) { + const timestamp = new Date().toISOString(); + initialObjectData.collection_index.created = timestamp; + initialObjectData.collection_index.modified = timestamp; + const body = initialObjectData; + request(app) + .post('/api/collection-indexes') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the created collection index + collectionIndex1 = res.body; + expect(collectionIndex1).toBeDefined(); + expect(collectionIndex1.collection_index).toBeDefined(); + expect(collectionIndex1.collection_index.id).toBeDefined(); + expect(collectionIndex1.collection_index.created).toBeDefined(); + expect(collectionIndex1.collection_index.modified).toBeDefined(); + done(); + } + }); + }); - it('GET /api/collection-indexes returns the added collection index', function (done) { - request(app) - .get('/api/collection-indexes') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get one collection index in an array - const collectionIndexes = res.body; - expect(collectionIndexes).toBeDefined(); - expect(Array.isArray(collectionIndexes)).toBe(true); - expect(collectionIndexes.length).toBe(1); - done(); - } - }); - }); + it('GET /api/collection-indexes returns the added collection index', function (done) { + request(app) + .get('/api/collection-indexes') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get one collection index in an array + const collectionIndexes = res.body; + expect(collectionIndexes).toBeDefined(); + expect(Array.isArray(collectionIndexes)).toBe(true); + expect(collectionIndexes.length).toBe(1); + done(); + } + }); + }); - it('GET /api/collection-indexes/:id should not return a collection index when the id cannot be found', function (done) { - request(app) - .get('/api/collection-indexes/not-an-id') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); + it('GET /api/collection-indexes/:id should not return a collection index when the id cannot be found', function (done) { + request(app) + .get('/api/collection-indexes/not-an-id') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); - it('GET /api/collection-indexes/:id returns the added collection index', function (done) { - request(app) - .get('/api/collection-indexes/' + collectionIndex1.collection_index.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get one collection index - const collectionIndex = res.body; - expect(collectionIndex).toBeDefined(); - expect(collectionIndex.collection_index.id).toBe(collectionIndex1.collection_index.id); - expect(collectionIndex.collection_index.name).toBe(collectionIndex1.collection_index.name); - expect(collectionIndex.collection_index.description).toBe(collectionIndex1.collection_index.description); - done(); - } - }); - }); + it('GET /api/collection-indexes/:id returns the added collection index', function (done) { + request(app) + .get('/api/collection-indexes/' + collectionIndex1.collection_index.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get one collection index + const collectionIndex = res.body; + expect(collectionIndex).toBeDefined(); + expect(collectionIndex.collection_index.id).toBe(collectionIndex1.collection_index.id); + expect(collectionIndex.collection_index.name).toBe( + collectionIndex1.collection_index.name, + ); + expect(collectionIndex.collection_index.description).toBe( + collectionIndex1.collection_index.description, + ); + done(); + } + }); + }); - it('PUT /api/collection-indexes updates a collection index', function (done) { - const timestamp = new Date().toISOString(); - collectionIndex1.collection_index.modified = timestamp; - collectionIndex1.collection_index.description = 'This is an updated collection index.' - const body = collectionIndex1; - request(app) - .put('/api/collection-indexes/' + collectionIndex1.collection_index.id) - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get the updated collection index - const collectionIndex = res.body; - expect(collectionIndex).toBeDefined(); - expect(collectionIndex.collection_index.id).toBe(collectionIndex1.collection_index.id); - expect(collectionIndex.collection_index.modified).toBe(collectionIndex1.collection_index.modified); - done(); - } - }); - }); + it('PUT /api/collection-indexes updates a collection index', function (done) { + const timestamp = new Date().toISOString(); + collectionIndex1.collection_index.modified = timestamp; + collectionIndex1.collection_index.description = 'This is an updated collection index.'; + const body = collectionIndex1; + request(app) + .put('/api/collection-indexes/' + collectionIndex1.collection_index.id) + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the updated collection index + const collectionIndex = res.body; + expect(collectionIndex).toBeDefined(); + expect(collectionIndex.collection_index.id).toBe(collectionIndex1.collection_index.id); + expect(collectionIndex.collection_index.modified).toBe( + collectionIndex1.collection_index.modified, + ); + done(); + } + }); + }); - it('POST /api/collection-indexes does not create a collection index with the same id', function (done) { - const body = collectionIndex1; - request(app) - .post('/api/collection-indexes') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(409) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); + it('POST /api/collection-indexes does not create a collection index with the same id', function (done) { + const body = collectionIndex1; + request(app) + .post('/api/collection-indexes') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(409) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); - it('DELETE /api/collection-indexes deletes a collection index', function (done) { - request(app) - .delete('/api/collection-indexes/' + collectionIndex1.collection_index.id) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); + it('DELETE /api/collection-indexes deletes a collection index', function (done) { + request(app) + .delete('/api/collection-indexes/' + collectionIndex1.collection_index.id) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); - it('GET /api/collection-indexes returns an empty array of collection indexes', function (done) { - request(app) - .get('/api/collection-indexes') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get an empty array - const collectionIndexes = res.body; - expect(collectionIndexes).toBeDefined(); - expect(Array.isArray(collectionIndexes)).toBe(true); - expect(collectionIndexes.length).toBe(0); - done(); - } - }); - }); + it('GET /api/collection-indexes returns an empty array of collection indexes', function (done) { + request(app) + .get('/api/collection-indexes') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get an empty array + const collectionIndexes = res.body; + expect(collectionIndexes).toBeDefined(); + expect(Array.isArray(collectionIndexes)).toBe(true); + expect(collectionIndexes.length).toBe(0); + done(); + } + }); + }); - after(async function() { - await database.closeConnection(); - }); + after(async function () { + await database.closeConnection(); + }); }); diff --git a/app/tests/api/collections/collections.spec.js b/app/tests/api/collections/collections.spec.js index b9d6b7d0..fe984afb 100644 --- a/app/tests/api/collections/collections.spec.js +++ b/app/tests/api/collections/collections.spec.js @@ -13,691 +13,710 @@ const databaseConfiguration = require('../../../lib/database-configuration'); // modified and created properties will be set before calling REST API const initialCollectionData = { - workspace: { - imported: new Date().toISOString(), - import_categories: { - additions: [], - changes: [], - minor_changes: [], - revocations: [], - deprecations: [], - supersedes_user_edits: [], - supersedes_collection_changes: [], - duplicates: [], - out_of_date: [], - errors: [] - } + workspace: { + imported: new Date().toISOString(), + import_categories: { + additions: [], + changes: [], + minor_changes: [], + revocations: [], + deprecations: [], + supersedes_user_edits: [], + supersedes_collection_changes: [], + duplicates: [], + out_of_date: [], + errors: [], }, - stix: { - id: 'x-mitre-collection--30ee11cf-0a05-4d9e-ab54-9b8563669647', - name: 'collection-1', - spec_version: '2.1', - type: 'x-mitre-collection', - description: 'This is a collection.', - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - x_mitre_contents: [] - } + }, + stix: { + id: 'x-mitre-collection--30ee11cf-0a05-4d9e-ab54-9b8563669647', + name: 'collection-1', + spec_version: '2.1', + type: 'x-mitre-collection', + description: 'This is a collection.', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + x_mitre_contents: [], + }, }; const mitigationData1 = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - name: 'course-of-action-1', - spec_version: '2.1', - type: 'course-of-action', - description: 'This is a mitigation.', - external_references: [ - { source_name: 'mitre-attack', external_id: 'T9999', url: 'https://attack.mitre.org/mitigations/T9999' }, - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - x_mitre_version: "1.1" - } + }, + stix: { + name: 'course-of-action-1', + spec_version: '2.1', + type: 'course-of-action', + description: 'This is a mitigation.', + external_references: [ + { + source_name: 'mitre-attack', + external_id: 'T9999', + url: 'https://attack.mitre.org/mitigations/T9999', + }, + { source_name: 'source-1', external_id: 's1' }, + ], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + x_mitre_version: '1.1', + }, }; const mitigationData2 = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - name: 'course-of-action-2', - spec_version: '2.1', - type: 'course-of-action', - description: 'This is another mitigation.', - external_references: [ - { source_name: 'mitre-attack', external_id: 'T9999', url: 'https://attack.mitre.org/mitigations/T9999' }, - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - x_mitre_version: "1.1" - } + }, + stix: { + name: 'course-of-action-2', + spec_version: '2.1', + type: 'course-of-action', + description: 'This is another mitigation.', + external_references: [ + { + source_name: 'mitre-attack', + external_id: 'T9999', + url: 'https://attack.mitre.org/mitigations/T9999', + }, + { source_name: 'source-1', external_id: 's1' }, + ], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + x_mitre_version: '1.1', + }, }; const softwareData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - name: 'software-1', - spec_version: '2.1', - type: 'malware', - description: 'This is a malware type of software.', - is_family: false, - external_references: [ - { source_name: 'mitre-attack', external_id: 'S3333', url: 'https://attack.mitre.org/software/S3333' }, - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - x_mitre_version: "1.1", - x_mitre_aliases: [ - "software-1" - ], - x_mitre_platforms: [ - "platform-1" - ], - x_mitre_contributors: [ - "contributor-1", - "contributor-2" - ], - x_mitre_domains: [ - "mobile-attack" - ] - } + }, + stix: { + name: 'software-1', + spec_version: '2.1', + type: 'malware', + description: 'This is a malware type of software.', + is_family: false, + external_references: [ + { + source_name: 'mitre-attack', + external_id: 'S3333', + url: 'https://attack.mitre.org/software/S3333', + }, + { source_name: 'source-1', external_id: 's1' }, + ], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + x_mitre_version: '1.1', + x_mitre_aliases: ['software-1'], + x_mitre_platforms: ['platform-1'], + x_mitre_contributors: ['contributor-1', 'contributor-2'], + x_mitre_domains: ['mobile-attack'], + }, }; describe('Collections (x-mitre-collection) Basic API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - it('POST /api/mitigations creates a mitigation', function (done) { - const timestamp = new Date().toISOString(); - mitigationData1.stix.created = timestamp; - mitigationData1.stix.modified = timestamp; - const body = mitigationData1; - request(app) - .post('/api/mitigations') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the created mitigation - const mitigation = res.body; - expect(mitigation).toBeDefined(); - expect(mitigation.stix).toBeDefined(); - expect(mitigation.stix.id).toBeDefined(); - - // Add this object to the collection data - const contentsObject = { - object_ref: mitigation.stix.id, - object_modified: mitigation.stix.modified - } - initialCollectionData.stix.x_mitre_contents.push(contentsObject); - - done(); - } - }); - }); - - let mitigation2; - it('POST /api/mitigations creates another mitigation', function (done) { - const timestamp = new Date().toISOString(); - mitigationData2.stix.created = timestamp; - mitigationData2.stix.modified = timestamp; - const body = mitigationData2; - request(app) - .post('/api/mitigations') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - mitigation2 = res.body; - - done(); - } - }); - }); - it('POST /api/software creates a software', function (done) { - const timestamp = new Date().toISOString(); - softwareData.stix.created = timestamp; - softwareData.stix.modified = timestamp; - const body = softwareData; - request(app) - .post('/api/software') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the created software - const software = res.body; - expect(software).toBeDefined(); - expect(software.stix).toBeDefined(); - expect(software.stix.id).toBeDefined(); - - // Add this object to the collection data - const contentsObject = { - object_ref: software.stix.id, - object_modified: software.stix.modified - } - initialCollectionData.stix.x_mitre_contents.push(contentsObject); - - done(); - } - }); - }); - - it('GET /api/collections returns an empty array of collections', function (done) { - request(app) - .get('/api/collections') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get an empty array - const collections = res.body; - expect(collections).toBeDefined(); - expect(Array.isArray(collections)).toBe(true); - expect(collections.length).toBe(0); - done(); - } - }); - }); - - it('POST /api/collections does not create an empty collection', function (done) { - const body = {}; - request(app) - .post('/api/collections') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - let collection1; - it('POST /api/collections creates a collection', function (done) { - const timestamp = new Date().toISOString(); - initialCollectionData.stix.created = timestamp; - initialCollectionData.stix.modified = timestamp; - const body = initialCollectionData; - request(app) - .post('/api/collections') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get the created collection - collection1 = res.body; - expect(collection1).toBeDefined(); - expect(collection1.stix).toBeDefined(); - expect(collection1.stix.id).toBeDefined(); - expect(collection1.stix.created).toBeDefined(); - expect(collection1.stix.modified).toBeDefined(); - expect(collection1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); - - done(); - } - }); - }); - - it('GET /api/collections returns the added collection', function (done) { - request(app) - .get('/api/collections') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get one collection in an array - const collections = res.body; - expect(collections).toBeDefined(); - expect(Array.isArray(collections)).toBe(true); - expect(collections.length).toBe(1); - done(); - } - }); - }); - - it('GET /api/collections/:id should not return a collection when the id cannot be found', function (done) { - request(app) - .get('/api/collections/not-an-id') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('GET /api/collections/:id returns the added collection', function (done) { - request(app) - .get('/api/collections/' + collection1.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get one collection in an array - const collections = res.body; - expect(collections).toBeDefined(); - expect(Array.isArray(collections)).toBe(true); - expect(collections.length).toBe(1); - - const collection = collections[0]; - expect(collection).toBeDefined(); - expect(collection.stix).toBeDefined(); - expect(collection.stix.id).toBe(collection1.stix.id); - expect(collection.stix.type).toBe(collection1.stix.type); - expect(collection.stix.name).toBe(collection1.stix.name); - expect(collection.stix.description).toBe(collection1.stix.description); - expect(collection.stix.spec_version).toBe(collection1.stix.spec_version); - expect(collection.stix.object_marking_refs).toEqual(expect.arrayContaining(collection1.stix.object_marking_refs)); - expect(collection.stix.created_by_ref).toBe(collection1.stix.created_by_ref); - expect(collection.stix.x_mitre_attack_spec_version).toBe(collection1.stix.x_mitre_attack_spec_version); - - expect(collection.contents).toBeUndefined(); - - done(); - } - }); - }); - - it('GET /api/collections/:id with retrieveContents flag returns the added collection with contents', function (done) { - request(app) - .get('/api/collections/' + collection1.stix.id + '?retrieveContents=true') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get one collection in an array - const collections = res.body; - expect(collections).toBeDefined(); - expect(Array.isArray(collections)).toBe(true); - expect(collections.length).toBe(1); - - const collection = collections[0]; - expect(collection).toBeDefined(); - expect(collection.stix).toBeDefined(); - expect(collection.stix.id).toBe(collection1.stix.id); - expect(collection.stix.type).toBe(collection1.stix.type); - expect(collection.stix.name).toBe(collection1.stix.name); - expect(collection.stix.description).toBe(collection1.stix.description); - expect(collection.stix.spec_version).toBe(collection1.stix.spec_version); - expect(collection.stix.object_marking_refs).toEqual(expect.arrayContaining(collection1.stix.object_marking_refs)); - expect(collection.stix.created_by_ref).toBe(collection1.stix.created_by_ref); - - expect(collection.contents).toBeDefined(); - expect(Array.isArray(collection.contents)).toBe(true); - expect(collection.contents.length).toBe(2); - - done(); - } - }); - }); - - it('POST /api/collections does not create a collection with the same id and modified date', function (done) { - const body = collection1; - request(app) - .post('/api/collections') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(409) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - let collection2; - it('POST /api/collections should create a new version of a collection with a duplicate stix.id but different stix.modified date', function (done) { - const collection = _.cloneDeep(collection1); - collection._id = undefined; - collection.__t = undefined; - collection.__v = undefined; - const timestamp = new Date().toISOString(); - collection.stix.modified = timestamp; - - // Add the second mitigation object to the collection data - const contentsObject = { - object_ref: mitigation2.stix.id, - object_modified: mitigation2.stix.modified + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + it('POST /api/mitigations creates a mitigation', function (done) { + const timestamp = new Date().toISOString(); + mitigationData1.stix.created = timestamp; + mitigationData1.stix.modified = timestamp; + const body = mitigationData1; + request(app) + .post('/api/mitigations') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the created mitigation + const mitigation = res.body; + expect(mitigation).toBeDefined(); + expect(mitigation.stix).toBeDefined(); + expect(mitigation.stix.id).toBeDefined(); + + // Add this object to the collection data + const contentsObject = { + object_ref: mitigation.stix.id, + object_modified: mitigation.stix.modified, + }; + initialCollectionData.stix.x_mitre_contents.push(contentsObject); + + done(); + } + }); + }); + + let mitigation2; + it('POST /api/mitigations creates another mitigation', function (done) { + const timestamp = new Date().toISOString(); + mitigationData2.stix.created = timestamp; + mitigationData2.stix.modified = timestamp; + const body = mitigationData2; + request(app) + .post('/api/mitigations') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + mitigation2 = res.body; + + done(); + } + }); + }); + it('POST /api/software creates a software', function (done) { + const timestamp = new Date().toISOString(); + softwareData.stix.created = timestamp; + softwareData.stix.modified = timestamp; + const body = softwareData; + request(app) + .post('/api/software') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the created software + const software = res.body; + expect(software).toBeDefined(); + expect(software.stix).toBeDefined(); + expect(software.stix.id).toBeDefined(); + + // Add this object to the collection data + const contentsObject = { + object_ref: software.stix.id, + object_modified: software.stix.modified, + }; + initialCollectionData.stix.x_mitre_contents.push(contentsObject); + + done(); + } + }); + }); + + it('GET /api/collections returns an empty array of collections', function (done) { + request(app) + .get('/api/collections') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get an empty array + const collections = res.body; + expect(collections).toBeDefined(); + expect(Array.isArray(collections)).toBe(true); + expect(collections.length).toBe(0); + done(); + } + }); + }); + + it('POST /api/collections does not create an empty collection', function (done) { + const body = {}; + request(app) + .post('/api/collections') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + let collection1; + it('POST /api/collections creates a collection', function (done) { + const timestamp = new Date().toISOString(); + initialCollectionData.stix.created = timestamp; + initialCollectionData.stix.modified = timestamp; + const body = initialCollectionData; + request(app) + .post('/api/collections') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the created collection + collection1 = res.body; + expect(collection1).toBeDefined(); + expect(collection1.stix).toBeDefined(); + expect(collection1.stix.id).toBeDefined(); + expect(collection1.stix.created).toBeDefined(); + expect(collection1.stix.modified).toBeDefined(); + expect(collection1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); + + done(); + } + }); + }); + + it('GET /api/collections returns the added collection', function (done) { + request(app) + .get('/api/collections') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get one collection in an array + const collections = res.body; + expect(collections).toBeDefined(); + expect(Array.isArray(collections)).toBe(true); + expect(collections.length).toBe(1); + done(); + } + }); + }); + + it('GET /api/collections/:id should not return a collection when the id cannot be found', function (done) { + request(app) + .get('/api/collections/not-an-id') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('GET /api/collections/:id returns the added collection', function (done) { + request(app) + .get('/api/collections/' + collection1.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get one collection in an array + const collections = res.body; + expect(collections).toBeDefined(); + expect(Array.isArray(collections)).toBe(true); + expect(collections.length).toBe(1); + + const collection = collections[0]; + expect(collection).toBeDefined(); + expect(collection.stix).toBeDefined(); + expect(collection.stix.id).toBe(collection1.stix.id); + expect(collection.stix.type).toBe(collection1.stix.type); + expect(collection.stix.name).toBe(collection1.stix.name); + expect(collection.stix.description).toBe(collection1.stix.description); + expect(collection.stix.spec_version).toBe(collection1.stix.spec_version); + expect(collection.stix.object_marking_refs).toEqual( + expect.arrayContaining(collection1.stix.object_marking_refs), + ); + expect(collection.stix.created_by_ref).toBe(collection1.stix.created_by_ref); + expect(collection.stix.x_mitre_attack_spec_version).toBe( + collection1.stix.x_mitre_attack_spec_version, + ); + + expect(collection.contents).toBeUndefined(); + + done(); + } + }); + }); + + it('GET /api/collections/:id with retrieveContents flag returns the added collection with contents', function (done) { + request(app) + .get('/api/collections/' + collection1.stix.id + '?retrieveContents=true') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get one collection in an array + const collections = res.body; + expect(collections).toBeDefined(); + expect(Array.isArray(collections)).toBe(true); + expect(collections.length).toBe(1); + + const collection = collections[0]; + expect(collection).toBeDefined(); + expect(collection.stix).toBeDefined(); + expect(collection.stix.id).toBe(collection1.stix.id); + expect(collection.stix.type).toBe(collection1.stix.type); + expect(collection.stix.name).toBe(collection1.stix.name); + expect(collection.stix.description).toBe(collection1.stix.description); + expect(collection.stix.spec_version).toBe(collection1.stix.spec_version); + expect(collection.stix.object_marking_refs).toEqual( + expect.arrayContaining(collection1.stix.object_marking_refs), + ); + expect(collection.stix.created_by_ref).toBe(collection1.stix.created_by_ref); + + expect(collection.contents).toBeDefined(); + expect(Array.isArray(collection.contents)).toBe(true); + expect(collection.contents.length).toBe(2); + + done(); + } + }); + }); + + it('POST /api/collections does not create a collection with the same id and modified date', function (done) { + const body = collection1; + request(app) + .post('/api/collections') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(409) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); } - collection.stix.x_mitre_contents.push(contentsObject); - - const body = collection; - request(app) - .post('/api/collections') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get the created collection - collection2 = res.body; - expect(collection2).toBeDefined(); - done(); - } - }); - }); - - it('GET /api/collections/:id returns the latest added collection', function (done) { - request(app) - .get('/api/collections/' + collection2.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get one collection in an array - const collections = res.body; - expect(collections).toBeDefined(); - expect(Array.isArray(collections)).toBe(true); - expect(collections.length).toBe(1); - const collection = collections[0]; - expect(collection.stix.id).toBe(collection2.stix.id); - expect(collection.stix.modified).toBe(collection2.stix.modified); - done(); - } - }); - }); - - it('GET /api/collections/:id/modified/:modified returns the proper collection', function (done) { - request(app) - .get('/api/collections/' + collection1.stix.id + '/modified/' + collection1.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get one collection - const collection = res.body; - expect(collection).toBeDefined(); - expect(collection.stix).toBeDefined(); - expect(collection.stix.id).toBe(collection1.stix.id); - expect(collection.stix.modified).toBe(collection1.stix.modified); - done(); - } - }); - }); - - it('GET /api/collections/:id/modified/:modified with retrieveContents flag returns the first version of the collection with its contents', function (done) { - request(app) - .get('/api/collections/' + collection1.stix.id + '/modified/' + collection1.stix.modified + '?retrieveContents=true') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get one collection - const collection = res.body; - expect(collection).toBeDefined(); - expect(collection.stix).toBeDefined(); - expect(collection.stix.id).toBe(collection1.stix.id); - expect(collection.stix.modified).toBe(collection1.stix.modified); - - expect(collection.contents).toBeDefined(); - expect(Array.isArray(collection.contents)).toBe(true); - expect(collection.contents.length).toBe(2); - done(); - } - }); - }); - - it('GET /api/collections/:id/modified/:modified with retrieveContents flag returns the second version of the collection with its contents', function (done) { - request(app) - .get('/api/collections/' + collection2.stix.id + '/modified/' + collection2.stix.modified + '?retrieveContents=true') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get one collection - const collection = res.body; - expect(collection).toBeDefined(); - expect(collection.stix).toBeDefined(); - expect(collection.stix.id).toBe(collection2.stix.id); - expect(collection.stix.modified).toBe(collection2.stix.modified); - - expect(collection.contents).toBeDefined(); - expect(Array.isArray(collection.contents)).toBe(true); - expect(collection.contents.length).toBe(3); - done(); - } - }); - }); - - it('GET /api/collections returns all versions of all added collections', function (done) { - request(app) - .get('/api/collections?versions=all') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get two collections in an array - const collections = res.body; - expect(collections).toBeDefined(); - expect(Array.isArray(collections)).toBe(true); - expect(collections.length).toBe(2); - done(); - } - }); - }); - - it('GET /api/collections returns all versions of one added collection', function (done) { - request(app) - .get('/api/collections/' + collection1.stix.id + '?versions=all') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get two collections in an array - const collections = res.body; - expect(collections).toBeDefined(); - expect(Array.isArray(collections)).toBe(true); - expect(collections.length).toBe(2); - done(); - } - }); - }); - - it('DELETE /api/collections/:id should not delete a collection when the id cannot be found', function (done) { - request(app) - .delete('/api/collections/not-an-id') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); - }); - - it('DELETE /api/collections/:id/modified/:modified deletes a collection and its contents', function (done) { - request(app) - .delete('/api/collections/' + collection2.stix.id + '/modified/' + collection2.stix.modified + '?deleteAllContents=true') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); - }); - - it('GET /api/mitigations/:id should not return a mitigation that was deleted when the collection was deleted', function (done) { - request(app) - .get(`/api/mitigations/${ mitigation2.stix.id }`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - // This should return all the objects, showing that the previous delete didn't remove objects that were in both - // versions of the collection - it('GET /api/collections/:id with retrieveContents flag returns the added collection with contents', function (done) { - request(app) - .get('/api/collections/' + collection1.stix.id + '?retrieveContents=true') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get one collection in an array - const collections = res.body; - expect(collections).toBeDefined(); - expect(Array.isArray(collections)).toBe(true); - expect(collections.length).toBe(1); - - const collection = collections[0]; - expect(collection).toBeDefined(); - expect(collection.stix).toBeDefined(); - expect(collection.stix.id).toBe(collection1.stix.id); - - expect(collection.contents).toBeDefined(); - expect(Array.isArray(collection.contents)).toBe(true); - expect(collection.contents.length).toBe(2); - - done(); - } - }); - }); - - it('DELETE /api/collections/:id should delete all of the collections with the stix id', function (done) { - request(app) - .delete('/api/collections/' + collection1.stix.id) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); - }); - - after(async function() { - await database.closeConnection(); - }); + }); + }); + + let collection2; + it('POST /api/collections should create a new version of a collection with a duplicate stix.id but different stix.modified date', function (done) { + const collection = _.cloneDeep(collection1); + collection._id = undefined; + collection.__t = undefined; + collection.__v = undefined; + const timestamp = new Date().toISOString(); + collection.stix.modified = timestamp; + + // Add the second mitigation object to the collection data + const contentsObject = { + object_ref: mitigation2.stix.id, + object_modified: mitigation2.stix.modified, + }; + collection.stix.x_mitre_contents.push(contentsObject); + + const body = collection; + request(app) + .post('/api/collections') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the created collection + collection2 = res.body; + expect(collection2).toBeDefined(); + done(); + } + }); + }); + + it('GET /api/collections/:id returns the latest added collection', function (done) { + request(app) + .get('/api/collections/' + collection2.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get one collection in an array + const collections = res.body; + expect(collections).toBeDefined(); + expect(Array.isArray(collections)).toBe(true); + expect(collections.length).toBe(1); + const collection = collections[0]; + expect(collection.stix.id).toBe(collection2.stix.id); + expect(collection.stix.modified).toBe(collection2.stix.modified); + done(); + } + }); + }); + + it('GET /api/collections/:id/modified/:modified returns the proper collection', function (done) { + request(app) + .get('/api/collections/' + collection1.stix.id + '/modified/' + collection1.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get one collection + const collection = res.body; + expect(collection).toBeDefined(); + expect(collection.stix).toBeDefined(); + expect(collection.stix.id).toBe(collection1.stix.id); + expect(collection.stix.modified).toBe(collection1.stix.modified); + done(); + } + }); + }); + + it('GET /api/collections/:id/modified/:modified with retrieveContents flag returns the first version of the collection with its contents', function (done) { + request(app) + .get( + '/api/collections/' + + collection1.stix.id + + '/modified/' + + collection1.stix.modified + + '?retrieveContents=true', + ) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get one collection + const collection = res.body; + expect(collection).toBeDefined(); + expect(collection.stix).toBeDefined(); + expect(collection.stix.id).toBe(collection1.stix.id); + expect(collection.stix.modified).toBe(collection1.stix.modified); + + expect(collection.contents).toBeDefined(); + expect(Array.isArray(collection.contents)).toBe(true); + expect(collection.contents.length).toBe(2); + done(); + } + }); + }); + + it('GET /api/collections/:id/modified/:modified with retrieveContents flag returns the second version of the collection with its contents', function (done) { + request(app) + .get( + '/api/collections/' + + collection2.stix.id + + '/modified/' + + collection2.stix.modified + + '?retrieveContents=true', + ) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get one collection + const collection = res.body; + expect(collection).toBeDefined(); + expect(collection.stix).toBeDefined(); + expect(collection.stix.id).toBe(collection2.stix.id); + expect(collection.stix.modified).toBe(collection2.stix.modified); + + expect(collection.contents).toBeDefined(); + expect(Array.isArray(collection.contents)).toBe(true); + expect(collection.contents.length).toBe(3); + done(); + } + }); + }); + + it('GET /api/collections returns all versions of all added collections', function (done) { + request(app) + .get('/api/collections?versions=all') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get two collections in an array + const collections = res.body; + expect(collections).toBeDefined(); + expect(Array.isArray(collections)).toBe(true); + expect(collections.length).toBe(2); + done(); + } + }); + }); + + it('GET /api/collections returns all versions of one added collection', function (done) { + request(app) + .get('/api/collections/' + collection1.stix.id + '?versions=all') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get two collections in an array + const collections = res.body; + expect(collections).toBeDefined(); + expect(Array.isArray(collections)).toBe(true); + expect(collections.length).toBe(2); + done(); + } + }); + }); + + it('DELETE /api/collections/:id should not delete a collection when the id cannot be found', function (done) { + request(app) + .delete('/api/collections/not-an-id') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('DELETE /api/collections/:id/modified/:modified deletes a collection and its contents', function (done) { + request(app) + .delete( + '/api/collections/' + + collection2.stix.id + + '/modified/' + + collection2.stix.modified + + '?deleteAllContents=true', + ) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('GET /api/mitigations/:id should not return a mitigation that was deleted when the collection was deleted', function (done) { + request(app) + .get(`/api/mitigations/${mitigation2.stix.id}`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + // This should return all the objects, showing that the previous delete didn't remove objects that were in both + // versions of the collection + it('GET /api/collections/:id with retrieveContents flag returns the added collection with contents', function (done) { + request(app) + .get('/api/collections/' + collection1.stix.id + '?retrieveContents=true') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get one collection in an array + const collections = res.body; + expect(collections).toBeDefined(); + expect(Array.isArray(collections)).toBe(true); + expect(collections.length).toBe(1); + + const collection = collections[0]; + expect(collection).toBeDefined(); + expect(collection.stix).toBeDefined(); + expect(collection.stix.id).toBe(collection1.stix.id); + + expect(collection.contents).toBeDefined(); + expect(Array.isArray(collection.contents)).toBe(true); + expect(collection.contents.length).toBe(2); + + done(); + } + }); + }); + + it('DELETE /api/collections/:id should delete all of the collections with the stix id', function (done) { + request(app) + .delete('/api/collections/' + collection1.stix.id) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + after(async function () { + await database.closeConnection(); + }); }); diff --git a/app/tests/api/data-components/data-components-pagination.spec.js b/app/tests/api/data-components/data-components-pagination.spec.js index cb2229e8..ce128627 100644 --- a/app/tests/api/data-components/data-components-pagination.spec.js +++ b/app/tests/api/data-components/data-components-pagination.spec.js @@ -4,27 +4,25 @@ const PaginationTests = require('../../shared/pagination'); // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - spec_version: '2.1', - type: 'x-mitre-data-component', - description: 'This is a data component.', - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--6444f546-6900-4456-b3b1-015c88d70dab" - } + }, + stix: { + spec_version: '2.1', + type: 'x-mitre-data-component', + description: 'This is a data component.', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--6444f546-6900-4456-b3b1-015c88d70dab', + }, }; const options = { - prefix: 'x-mitre-data-component', - baseUrl: '/api/data-components', - label: 'Data Components' -} + prefix: 'x-mitre-data-component', + baseUrl: '/api/data-components', + label: 'Data Components', +}; const paginationTests = new PaginationTests(dataComponentsService, initialObjectData, options); paginationTests.executeTests(); diff --git a/app/tests/api/data-components/data-components.spec.js b/app/tests/api/data-components/data-components.spec.js index 52c139f5..d35d041a 100644 --- a/app/tests/api/data-components/data-components.spec.js +++ b/app/tests/api/data-components/data-components.spec.js @@ -14,342 +14,340 @@ logger.level = 'debug'; // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - name: 'data-component-1', - spec_version: '2.1', - type: 'x-mitre-data-component', - description: 'This is a data component.', - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - x_mitre_version: "1.1", - x_mitre_domains: [ - 'enterprise-attack' - ], - x_mitre_modified_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5' - } + }, + stix: { + name: 'data-component-1', + spec_version: '2.1', + type: 'x-mitre-data-component', + description: 'This is a data component.', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + x_mitre_version: '1.1', + x_mitre_domains: ['enterprise-attack'], + x_mitre_modified_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + }, }; describe('Data Components API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - it('GET /api/data-components returns an empty array of data components', async function () { - const res = await request(app) - .get('/api/data-components') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const dataComponents = res.body; - expect(dataComponents).toBeDefined(); - expect(Array.isArray(dataComponents)).toBe(true); - expect(dataComponents.length).toBe(0); - - }); - - it('POST /api/data-components does not create an empty data component', async function () { - const body = { }; - await request(app) - .post('/api/data-components') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - - }); - - let dataComponent1; - it('POST /api/data-components creates a data component', async function () { - const timestamp = new Date().toISOString(); - initialObjectData.stix.created = timestamp; - initialObjectData.stix.modified = timestamp; - const body = initialObjectData; - const res = await request(app) - .post('/api/data-components') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created data component - dataComponent1 = res.body; - expect(dataComponent1).toBeDefined(); - expect(dataComponent1.stix).toBeDefined(); - expect(dataComponent1.stix.id).toBeDefined(); - expect(dataComponent1.stix.created).toBeDefined(); - expect(dataComponent1.stix.modified).toBeDefined(); - expect(dataComponent1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); - - }); - - it('GET /api/data-components returns the added data component', async function () { - const res = await request(app) - .get('/api/data-components') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - - // We expect to get one data component in an array - const dataComponent = res.body; - expect(dataComponent).toBeDefined(); - expect(Array.isArray(dataComponent)).toBe(true); - expect(dataComponent.length).toBe(1); - }); - - it('GET /api/data-components/:id should not return a data component when the id cannot be found', async function () { - await request(app) - .get('/api/data-components/not-an-id') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('GET /api/data-components/:id returns the added data component', async function () { - const res = await request(app) - .get('/api/data-components/' + dataComponent1.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - - // We expect to get one data component in an array - const dataComponents = res.body; - expect(dataComponents).toBeDefined(); - expect(Array.isArray(dataComponents)).toBe(true); - expect(dataComponents.length).toBe(1); - - const dataComponent = dataComponents[0]; - expect(dataComponent).toBeDefined(); - expect(dataComponent.stix).toBeDefined(); - expect(dataComponent.stix.id).toBe(dataComponent1.stix.id); - expect(dataComponent.stix.type).toBe(dataComponent1.stix.type); - expect(dataComponent.stix.name).toBe(dataComponent1.stix.name); - expect(dataComponent.stix.description).toBe(dataComponent1.stix.description); - expect(dataComponent.stix.spec_version).toBe(dataComponent1.stix.spec_version); - expect(dataComponent.stix.object_marking_refs).toEqual(expect.arrayContaining(dataComponent1.stix.object_marking_refs)); - expect(dataComponent.stix.created_by_ref).toBe(dataComponent1.stix.created_by_ref); - expect(dataComponent.stix.x_mitre_version).toBe(dataComponent1.stix.x_mitre_version); - expect(dataComponent.stix.x_mitre_attack_spec_version).toBe(dataComponent1.stix.x_mitre_attack_spec_version); - - }); - - it('PUT /api/data-components updates a data component', async function () { - const originalModified = dataComponent1.stix.modified; - const timestamp = new Date().toISOString(); - dataComponent1.stix.modified = timestamp; - dataComponent1.stix.description = 'This is an updated data component.' - const body = dataComponent1; - const res = await request(app) - .put('/api/data-components/' + dataComponent1.stix.id + '/modified/' + originalModified) - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - - // We expect to get the updated data component - const dataComponent = res.body; - expect(dataComponent).toBeDefined(); - expect(dataComponent.stix.id).toBe(dataComponent1.stix.id); - expect(dataComponent.stix.modified).toBe(dataComponent1.stix.modified); - - }); - - it('POST /api/data-components does not create a data component with the same id and modified date', async function () { - const body = dataComponent1; - await request(app) - .post('/api/data-components') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(409); - - }); - - let dataComponent2; - it('POST /api/data-components should create a new version of a data component with a duplicate stix.id but different stix.modified date', async function () { - dataComponent2 = _.cloneDeep(dataComponent1); - dataComponent2._id = undefined; - dataComponent2.__t = undefined; - dataComponent2.__v = undefined; - const timestamp = new Date().toISOString(); - dataComponent2.stix.modified = timestamp; - const body = dataComponent2; - const res = await request(app) - .post('/api/data-components') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created data component - const dataComponent = res.body; - expect(dataComponent).toBeDefined(); - - }); - - it('GET /api/data-components returns the latest added data component', async function () { - const res = await request(app) - .get('/api/data-components/' + dataComponent2.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one data component in an array - const dataComponents = res.body; - expect(dataComponents).toBeDefined(); - expect(Array.isArray(dataComponents)).toBe(true); - expect(dataComponents.length).toBe(1); - const dataComponent = dataComponents[0]; - expect(dataComponent.stix.id).toBe(dataComponent2.stix.id); - expect(dataComponent.stix.modified).toBe(dataComponent2.stix.modified); - - }); - - it('GET /api/data-components returns all added data component', async function () { - const res = await request(app) - .get('/api/data-components/' + dataComponent1.stix.id + '?versions=all') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - - // We expect to get two data components in an array - const dataComponents = res.body; - expect(dataComponents).toBeDefined(); - expect(Array.isArray(dataComponents)).toBe(true); - expect(dataComponents.length).toBe(2); - - }); - - it('GET /api/data-components/:id/modified/:modified returns the first added data component', async function () { - const res = await request(app) - .get('/api/data-components/' + dataComponent1.stix.id + '/modified/' + dataComponent1.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one data component in an array - const dataComponent = res.body; - expect(dataComponent).toBeDefined(); - expect(dataComponent.stix).toBeDefined(); - expect(dataComponent.stix.id).toBe(dataComponent1.stix.id); - expect(dataComponent.stix.modified).toBe(dataComponent1.stix.modified); - - }); - - it('GET /api/data-components/:id/modified/:modified returns the second added data component', async function () { - const res = await request(app) - .get('/api/data-components/' + dataComponent2.stix.id + '/modified/' + dataComponent2.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one data component in an array - const dataComponent = res.body; - expect(dataComponent).toBeDefined(); - expect(dataComponent.stix).toBeDefined(); - expect(dataComponent.stix.id).toBe(dataComponent2.stix.id); - expect(dataComponent.stix.modified).toBe(dataComponent2.stix.modified); - - }); - - let dataComponent3; - it('POST /api/data-components should create a new version of a data component with a duplicate stix.id but different stix.modified date', async function () { - dataComponent3 = _.cloneDeep(dataComponent1); - dataComponent3._id = undefined; - dataComponent3.__t = undefined; - dataComponent3.__v = undefined; - const timestamp = new Date().toISOString(); - dataComponent3.stix.modified = timestamp; - const body = dataComponent3; - const res = await request(app) - .post('/api/data-components') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created data component - const dataComponent = res.body; - expect(dataComponent).toBeDefined(); - - }); - - it('DELETE /api/data-components/:id should not delete a data component when the id cannot be found', async function () { - await request(app) - .delete('/api/data-components/not-an-id') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - - }); - - it('DELETE /api/data-components/:id/modified/:modified deletes a data component', async function () { - await request(app) - .delete('/api/data-components/' + dataComponent1.stix.id + '/modified/' + dataComponent1.stix.modified) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - - }); - - it('DELETE /api/data-components/:id should delete all the data components with the same stix id', async function () { - await request(app) - .delete('/api/data-components/' + dataComponent2.stix.id) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - - }); - - it('GET /api/data-components returns an empty array of data components', async function () { - const res = await request(app) - .get('/api/data-components') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - - // We expect to get an empty array - const dataComponents = res.body; - expect(dataComponents).toBeDefined(); - expect(Array.isArray(dataComponents)).toBe(true); - expect(dataComponents.length).toBe(0); - - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + it('GET /api/data-components returns an empty array of data components', async function () { + const res = await request(app) + .get('/api/data-components') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const dataComponents = res.body; + expect(dataComponents).toBeDefined(); + expect(Array.isArray(dataComponents)).toBe(true); + expect(dataComponents.length).toBe(0); + }); + + it('POST /api/data-components does not create an empty data component', async function () { + const body = {}; + await request(app) + .post('/api/data-components') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + let dataComponent1; + it('POST /api/data-components creates a data component', async function () { + const timestamp = new Date().toISOString(); + initialObjectData.stix.created = timestamp; + initialObjectData.stix.modified = timestamp; + const body = initialObjectData; + const res = await request(app) + .post('/api/data-components') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created data component + dataComponent1 = res.body; + expect(dataComponent1).toBeDefined(); + expect(dataComponent1.stix).toBeDefined(); + expect(dataComponent1.stix.id).toBeDefined(); + expect(dataComponent1.stix.created).toBeDefined(); + expect(dataComponent1.stix.modified).toBeDefined(); + expect(dataComponent1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); + }); + + it('GET /api/data-components returns the added data component', async function () { + const res = await request(app) + .get('/api/data-components') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one data component in an array + const dataComponent = res.body; + expect(dataComponent).toBeDefined(); + expect(Array.isArray(dataComponent)).toBe(true); + expect(dataComponent.length).toBe(1); + }); + + it('GET /api/data-components/:id should not return a data component when the id cannot be found', async function () { + await request(app) + .get('/api/data-components/not-an-id') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('GET /api/data-components/:id returns the added data component', async function () { + const res = await request(app) + .get('/api/data-components/' + dataComponent1.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one data component in an array + const dataComponents = res.body; + expect(dataComponents).toBeDefined(); + expect(Array.isArray(dataComponents)).toBe(true); + expect(dataComponents.length).toBe(1); + + const dataComponent = dataComponents[0]; + expect(dataComponent).toBeDefined(); + expect(dataComponent.stix).toBeDefined(); + expect(dataComponent.stix.id).toBe(dataComponent1.stix.id); + expect(dataComponent.stix.type).toBe(dataComponent1.stix.type); + expect(dataComponent.stix.name).toBe(dataComponent1.stix.name); + expect(dataComponent.stix.description).toBe(dataComponent1.stix.description); + expect(dataComponent.stix.spec_version).toBe(dataComponent1.stix.spec_version); + expect(dataComponent.stix.object_marking_refs).toEqual( + expect.arrayContaining(dataComponent1.stix.object_marking_refs), + ); + expect(dataComponent.stix.created_by_ref).toBe(dataComponent1.stix.created_by_ref); + expect(dataComponent.stix.x_mitre_version).toBe(dataComponent1.stix.x_mitre_version); + expect(dataComponent.stix.x_mitre_attack_spec_version).toBe( + dataComponent1.stix.x_mitre_attack_spec_version, + ); + }); + + it('PUT /api/data-components updates a data component', async function () { + const originalModified = dataComponent1.stix.modified; + const timestamp = new Date().toISOString(); + dataComponent1.stix.modified = timestamp; + dataComponent1.stix.description = 'This is an updated data component.'; + const body = dataComponent1; + const res = await request(app) + .put('/api/data-components/' + dataComponent1.stix.id + '/modified/' + originalModified) + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the updated data component + const dataComponent = res.body; + expect(dataComponent).toBeDefined(); + expect(dataComponent.stix.id).toBe(dataComponent1.stix.id); + expect(dataComponent.stix.modified).toBe(dataComponent1.stix.modified); + }); + + it('POST /api/data-components does not create a data component with the same id and modified date', async function () { + const body = dataComponent1; + await request(app) + .post('/api/data-components') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(409); + }); + + let dataComponent2; + it('POST /api/data-components should create a new version of a data component with a duplicate stix.id but different stix.modified date', async function () { + dataComponent2 = _.cloneDeep(dataComponent1); + dataComponent2._id = undefined; + dataComponent2.__t = undefined; + dataComponent2.__v = undefined; + const timestamp = new Date().toISOString(); + dataComponent2.stix.modified = timestamp; + const body = dataComponent2; + const res = await request(app) + .post('/api/data-components') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created data component + const dataComponent = res.body; + expect(dataComponent).toBeDefined(); + }); + + it('GET /api/data-components returns the latest added data component', async function () { + const res = await request(app) + .get('/api/data-components/' + dataComponent2.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one data component in an array + const dataComponents = res.body; + expect(dataComponents).toBeDefined(); + expect(Array.isArray(dataComponents)).toBe(true); + expect(dataComponents.length).toBe(1); + const dataComponent = dataComponents[0]; + expect(dataComponent.stix.id).toBe(dataComponent2.stix.id); + expect(dataComponent.stix.modified).toBe(dataComponent2.stix.modified); + }); + + it('GET /api/data-components returns all added data component', async function () { + const res = await request(app) + .get('/api/data-components/' + dataComponent1.stix.id + '?versions=all') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get two data components in an array + const dataComponents = res.body; + expect(dataComponents).toBeDefined(); + expect(Array.isArray(dataComponents)).toBe(true); + expect(dataComponents.length).toBe(2); + }); + + it('GET /api/data-components/:id/modified/:modified returns the first added data component', async function () { + const res = await request(app) + .get( + '/api/data-components/' + + dataComponent1.stix.id + + '/modified/' + + dataComponent1.stix.modified, + ) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one data component in an array + const dataComponent = res.body; + expect(dataComponent).toBeDefined(); + expect(dataComponent.stix).toBeDefined(); + expect(dataComponent.stix.id).toBe(dataComponent1.stix.id); + expect(dataComponent.stix.modified).toBe(dataComponent1.stix.modified); + }); + + it('GET /api/data-components/:id/modified/:modified returns the second added data component', async function () { + const res = await request(app) + .get( + '/api/data-components/' + + dataComponent2.stix.id + + '/modified/' + + dataComponent2.stix.modified, + ) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one data component in an array + const dataComponent = res.body; + expect(dataComponent).toBeDefined(); + expect(dataComponent.stix).toBeDefined(); + expect(dataComponent.stix.id).toBe(dataComponent2.stix.id); + expect(dataComponent.stix.modified).toBe(dataComponent2.stix.modified); + }); + + let dataComponent3; + it('POST /api/data-components should create a new version of a data component with a duplicate stix.id but different stix.modified date', async function () { + dataComponent3 = _.cloneDeep(dataComponent1); + dataComponent3._id = undefined; + dataComponent3.__t = undefined; + dataComponent3.__v = undefined; + const timestamp = new Date().toISOString(); + dataComponent3.stix.modified = timestamp; + const body = dataComponent3; + const res = await request(app) + .post('/api/data-components') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created data component + const dataComponent = res.body; + expect(dataComponent).toBeDefined(); + }); + + it('DELETE /api/data-components/:id should not delete a data component when the id cannot be found', async function () { + await request(app) + .delete('/api/data-components/not-an-id') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('DELETE /api/data-components/:id/modified/:modified deletes a data component', async function () { + await request(app) + .delete( + '/api/data-components/' + + dataComponent1.stix.id + + '/modified/' + + dataComponent1.stix.modified, + ) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('DELETE /api/data-components/:id should delete all the data components with the same stix id', async function () { + await request(app) + .delete('/api/data-components/' + dataComponent2.stix.id) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('GET /api/data-components returns an empty array of data components', async function () { + const res = await request(app) + .get('/api/data-components') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const dataComponents = res.body; + expect(dataComponents).toBeDefined(); + expect(Array.isArray(dataComponents)).toBe(true); + expect(dataComponents.length).toBe(0); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/api/data-sources/data-sources-pagination.spec.js b/app/tests/api/data-sources/data-sources-pagination.spec.js index 343b83f9..f9d1fb6d 100644 --- a/app/tests/api/data-sources/data-sources-pagination.spec.js +++ b/app/tests/api/data-sources/data-sources-pagination.spec.js @@ -4,27 +4,25 @@ const PaginationTests = require('../../shared/pagination'); // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - spec_version: '2.1', - type: 'x-mitre-data-source', - description: 'This is a data source.', - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--6444f546-6900-4456-b3b1-015c88d70dab" - } + }, + stix: { + spec_version: '2.1', + type: 'x-mitre-data-source', + description: 'This is a data source.', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--6444f546-6900-4456-b3b1-015c88d70dab', + }, }; const options = { - prefix: 'x-mitre-data-source', - baseUrl: '/api/data-sources', - label: 'Data Sources' -} + prefix: 'x-mitre-data-source', + baseUrl: '/api/data-sources', + label: 'Data Sources', +}; const paginationTests = new PaginationTests(dataSourcesService, initialObjectData, options); paginationTests.executeTests(); diff --git a/app/tests/api/data-sources/data-sources.spec.js b/app/tests/api/data-sources/data-sources.spec.js index c7d72033..e2a07c00 100644 --- a/app/tests/api/data-sources/data-sources.spec.js +++ b/app/tests/api/data-sources/data-sources.spec.js @@ -16,439 +16,404 @@ logger.level = 'debug'; // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialDataSourceData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - name: 'data-source-1', - spec_version: '2.1', - type: 'x-mitre-data-source', - description: 'This is a data source.', - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - x_mitre_version: "1.1", - x_mitre_platforms: [ - 'macOS', - 'Office 365', - 'Google Workspace', - 'Linux', - 'Network' - ], - x_mitre_collection_layers: [ - 'duis', - 'laboris' - ], - x_mitre_contributors: [ - 'Herbert Examplecontributor' - ], - x_mitre_domains: [ - 'enterprise-attack' - ], - x_mitre_modified_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5' - } + }, + stix: { + name: 'data-source-1', + spec_version: '2.1', + type: 'x-mitre-data-source', + description: 'This is a data source.', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + x_mitre_version: '1.1', + x_mitre_platforms: ['macOS', 'Office 365', 'Google Workspace', 'Linux', 'Network'], + x_mitre_collection_layers: ['duis', 'laboris'], + x_mitre_contributors: ['Herbert Examplecontributor'], + x_mitre_domains: ['enterprise-attack'], + x_mitre_modified_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + }, }; const initialDataComponentData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - name: 'data-component-1', - spec_version: '2.1', - type: 'x-mitre-data-component', - description: 'This is a data component.', - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - x_mitre_version: "1.1", - x_mitre_domains: [ - 'enterprise-attack' - ], - x_mitre_modified_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5' - } + }, + stix: { + name: 'data-component-1', + spec_version: '2.1', + type: 'x-mitre-data-component', + description: 'This is a data component.', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + x_mitre_version: '1.1', + x_mitre_domains: ['enterprise-attack'], + x_mitre_modified_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + }, }; async function loadDataComponents(baseDataComponent) { - const data1 = _.cloneDeep(baseDataComponent); - let timestamp = new Date().toISOString(); - data1.stix.created = timestamp; - data1.stix.modified = timestamp; - await dataComponentsService.create(data1); - - const data2 = _.cloneDeep(baseDataComponent); - timestamp = new Date().toISOString(); - data2.stix.created = timestamp; - data2.stix.modified = timestamp; - await dataComponentsService.create(data2); - - const data3 = _.cloneDeep(baseDataComponent); - timestamp = new Date().toISOString(); - data3.stix.created = timestamp; - data3.stix.modified = timestamp; - await dataComponentsService.create(data3); - - const data4 = _.cloneDeep(baseDataComponent); - timestamp = new Date().toISOString(); - data4.stix.created = timestamp; - data4.stix.modified = timestamp; - data4.stix.x_mitre_deprecated = true; - await dataComponentsService.create(data4); - - const data5 = _.cloneDeep(baseDataComponent); - timestamp = new Date().toISOString(); - data5.stix.created = timestamp; - data5.stix.modified = timestamp; - data5.stix.revoked = true; - await dataComponentsService.create(data5); + const data1 = _.cloneDeep(baseDataComponent); + let timestamp = new Date().toISOString(); + data1.stix.created = timestamp; + data1.stix.modified = timestamp; + await dataComponentsService.create(data1); + + const data2 = _.cloneDeep(baseDataComponent); + timestamp = new Date().toISOString(); + data2.stix.created = timestamp; + data2.stix.modified = timestamp; + await dataComponentsService.create(data2); + + const data3 = _.cloneDeep(baseDataComponent); + timestamp = new Date().toISOString(); + data3.stix.created = timestamp; + data3.stix.modified = timestamp; + await dataComponentsService.create(data3); + + const data4 = _.cloneDeep(baseDataComponent); + timestamp = new Date().toISOString(); + data4.stix.created = timestamp; + data4.stix.modified = timestamp; + data4.stix.x_mitre_deprecated = true; + await dataComponentsService.create(data4); + + const data5 = _.cloneDeep(baseDataComponent); + timestamp = new Date().toISOString(); + data5.stix.created = timestamp; + data5.stix.modified = timestamp; + data5.stix.revoked = true; + await dataComponentsService.create(data5); } describe('Data Sources API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - it('GET /api/data-sources returns an empty array of data sources', async function () { - const res = await request(app) - .get('/api/data-sources') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - // We expect to get an empty array - const dataSources = res.body; - expect(dataSources).toBeDefined(); - expect(Array.isArray(dataSources)).toBe(true); - expect(dataSources.length).toBe(0); - }); - - it('POST /api/data-sources does not create an empty data source', async function () { - const body = { }; - await request(app) - .post('/api/data-sources') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - - }); - - let dataSource1; - it('POST /api/data-sources creates a data source', async function () { - const timestamp = new Date().toISOString(); - initialDataSourceData.stix.created = timestamp; - initialDataSourceData.stix.modified = timestamp; - const body = initialDataSourceData; - const res = await request(app) - .post('/api/data-sources') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created data source - dataSource1 = res.body; - expect(dataSource1).toBeDefined(); - expect(dataSource1.stix).toBeDefined(); - expect(dataSource1.stix.id).toBeDefined(); - expect(dataSource1.stix.created).toBeDefined(); - expect(dataSource1.stix.modified).toBeDefined(); - expect(dataSource1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); - - - }); - - it('GET /api/data-sources returns the added data source', async function () { - const res = await request(app) - .get('/api/data-sources') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one data source in an array - const dataSource = res.body; - expect(dataSource).toBeDefined(); - expect(Array.isArray(dataSource)).toBe(true); - expect(dataSource.length).toBe(1); - - }); - - it('GET /api/data-sources/:id should not return a data source when the id cannot be found', async function () { - await request(app) - .get('/api/data-sources/not-an-id') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - - }); - - it('GET /api/data-sources/:id returns the added data source', async function () { - const res = await request(app) - .get('/api/data-sources/' + dataSource1.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one data source in an array - const dataSources = res.body; - expect(dataSources).toBeDefined(); - expect(Array.isArray(dataSources)).toBe(true); - expect(dataSources.length).toBe(1); - - const dataSource = dataSources[0]; - expect(dataSource).toBeDefined(); - expect(dataSource.stix).toBeDefined(); - expect(dataSource.stix.id).toBe(dataSource1.stix.id); - expect(dataSource.stix.type).toBe(dataSource1.stix.type); - expect(dataSource.stix.name).toBe(dataSource1.stix.name); - expect(dataSource.stix.description).toBe(dataSource1.stix.description); - expect(dataSource.stix.spec_version).toBe(dataSource1.stix.spec_version); - expect(dataSource.stix.object_marking_refs).toEqual(expect.arrayContaining(dataSource1.stix.object_marking_refs)); - expect(dataSource.stix.created_by_ref).toBe(dataSource1.stix.created_by_ref); - expect(dataSource.stix.x_mitre_version).toBe(dataSource1.stix.x_mitre_version); - expect(dataSource.stix.x_mitre_attack_spec_version).toBe(dataSource1.stix.x_mitre_attack_spec_version); - - - }); - - it('GET /api/data-sources/:id returns the added data source with data components', async function () { - initialDataComponentData.stix.x_mitre_data_source_ref = dataSource1.stix.id; - await loadDataComponents(initialDataComponentData); - - const res = await request(app) - .get(`/api/data-sources/${dataSource1.stix.id}?retrieveDataComponents=true`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one data source in an array - const dataSources = res.body; - expect(dataSources).toBeDefined(); - expect(Array.isArray(dataSources)).toBe(true); - expect(dataSources.length).toBe(1); - const dataSource = dataSources[0]; - expect(dataSource).toBeDefined(); - - // We expect to get 5 data components that reference this data source - expect(dataSource.dataComponents).toBeDefined(); - expect(dataSource.dataComponents.length).toBe(5); - }); - - it('PUT /api/data-sources updates a data source', async function () { - const originalModified = dataSource1.stix.modified; - const timestamp = new Date().toISOString(); - dataSource1.stix.modified = timestamp; - dataSource1.stix.description = 'This is an updated data source.' - const body = dataSource1; - const res = await request(app) - .put('/api/data-sources/' + dataSource1.stix.id + '/modified/' + originalModified) - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the updated data source - const dataSource = res.body; - expect(dataSource).toBeDefined(); - expect(dataSource.stix.id).toBe(dataSource1.stix.id); - expect(dataSource.stix.modified).toBe(dataSource1.stix.modified); - - }); - - it('POST /api/data-sources does not create a data source with the same id and modified date', async function () { - const body = dataSource1; - await request(app) - .post('/api/data-sources') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(409); - - }); - - let dataSource2; - it('POST /api/data-sources should create a new version of a data source with a duplicate stix.id but different stix.modified date', async function () { - dataSource2 = _.cloneDeep(dataSource1); - dataSource2._id = undefined; - dataSource2.__t = undefined; - dataSource2.__v = undefined; - const timestamp = new Date().toISOString(); - dataSource2.stix.modified = timestamp; - const body = dataSource2; - const res = await request(app) - .post('/api/data-sources') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created data source - const dataSource = res.body; - expect(dataSource).toBeDefined(); - - }); - - it('GET /api/data-sources returns the latest added data source', async function () { - const res = await request(app) - .get('/api/data-sources/' + dataSource2.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one data source in an array - const dataSources = res.body; - expect(dataSources).toBeDefined(); - expect(Array.isArray(dataSources)).toBe(true); - expect(dataSources.length).toBe(1); - const dataSource = dataSources[0]; - expect(dataSource.stix.id).toBe(dataSource2.stix.id); - expect(dataSource.stix.modified).toBe(dataSource2.stix.modified); - - }); - - it('GET /api/data-sources returns all added data source', async function () { - const res = await request(app) - .get('/api/data-sources/' + dataSource1.stix.id + '?versions=all') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get two data sources in an array - const dataSources = res.body; - expect(dataSources).toBeDefined(); - expect(Array.isArray(dataSources)).toBe(true); - expect(dataSources.length).toBe(2); - - }); - - it('GET /api/data-sources/:id/modified/:modified returns the first added data source', async function () { - const res = await request(app) - .get('/api/data-sources/' + dataSource1.stix.id + '/modified/' + dataSource1.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one data source in an array - const dataSource = res.body; - expect(dataSource).toBeDefined(); - expect(dataSource.stix).toBeDefined(); - expect(dataSource.stix.id).toBe(dataSource1.stix.id); - expect(dataSource.stix.modified).toBe(dataSource1.stix.modified); - - }); - - it('GET /api/data-sources/:id/modified/:modified returns the second added data source', async function () { - const res = await request(app) - .get('/api/data-sources/' + dataSource2.stix.id + '/modified/' + dataSource2.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one data source in an array - const dataSource = res.body; - expect(dataSource).toBeDefined(); - expect(dataSource.stix).toBeDefined(); - expect(dataSource.stix.id).toBe(dataSource2.stix.id); - expect(dataSource.stix.modified).toBe(dataSource2.stix.modified); - - }); - - let dataSource3; - it('POST /api/data-sources should create a new version of a data source with a duplicate stix.id but different stix.modified date', async function () { - dataSource3 = _.cloneDeep(dataSource1); - dataSource3._id = undefined; - dataSource3.__t = undefined; - dataSource3.__v = undefined; - const timestamp = new Date().toISOString(); - dataSource3.stix.modified = timestamp; - const body = dataSource3; - const res = await request(app) - .post('/api/data-sources') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created data source - const dataSource = res.body; - expect(dataSource).toBeDefined(); - - }); - - it('DELETE /api/data-sources/:id should not delete a data source when the id cannot be found', async function () { - await request(app) - .delete('/api/data-sources/not-an-id') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - - }); - - it('DELETE /api/data-sources/:id/modified/:modified deletes a data source', async function () { - await request(app) - .delete('/api/data-sources/' + dataSource1.stix.id + '/modified/' + dataSource1.stix.modified) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - - }); - - it('DELETE /api/data-sources/:id should delete all the data sources with the same stix id', async function () { - await request(app) - .delete('/api/data-sources/' + dataSource2.stix.id) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - - }); - - it('GET /api/data-sources returns an empty array of data sources', async function () { - const res = await request(app) - .get('/api/data-sources') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const dataSources = res.body; - expect(dataSources).toBeDefined(); - expect(Array.isArray(dataSources)).toBe(true); - expect(dataSources.length).toBe(0); - - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + it('GET /api/data-sources returns an empty array of data sources', async function () { + const res = await request(app) + .get('/api/data-sources') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + // We expect to get an empty array + const dataSources = res.body; + expect(dataSources).toBeDefined(); + expect(Array.isArray(dataSources)).toBe(true); + expect(dataSources.length).toBe(0); + }); + + it('POST /api/data-sources does not create an empty data source', async function () { + const body = {}; + await request(app) + .post('/api/data-sources') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + let dataSource1; + it('POST /api/data-sources creates a data source', async function () { + const timestamp = new Date().toISOString(); + initialDataSourceData.stix.created = timestamp; + initialDataSourceData.stix.modified = timestamp; + const body = initialDataSourceData; + const res = await request(app) + .post('/api/data-sources') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created data source + dataSource1 = res.body; + expect(dataSource1).toBeDefined(); + expect(dataSource1.stix).toBeDefined(); + expect(dataSource1.stix.id).toBeDefined(); + expect(dataSource1.stix.created).toBeDefined(); + expect(dataSource1.stix.modified).toBeDefined(); + expect(dataSource1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); + }); + + it('GET /api/data-sources returns the added data source', async function () { + const res = await request(app) + .get('/api/data-sources') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one data source in an array + const dataSource = res.body; + expect(dataSource).toBeDefined(); + expect(Array.isArray(dataSource)).toBe(true); + expect(dataSource.length).toBe(1); + }); + + it('GET /api/data-sources/:id should not return a data source when the id cannot be found', async function () { + await request(app) + .get('/api/data-sources/not-an-id') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('GET /api/data-sources/:id returns the added data source', async function () { + const res = await request(app) + .get('/api/data-sources/' + dataSource1.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one data source in an array + const dataSources = res.body; + expect(dataSources).toBeDefined(); + expect(Array.isArray(dataSources)).toBe(true); + expect(dataSources.length).toBe(1); + + const dataSource = dataSources[0]; + expect(dataSource).toBeDefined(); + expect(dataSource.stix).toBeDefined(); + expect(dataSource.stix.id).toBe(dataSource1.stix.id); + expect(dataSource.stix.type).toBe(dataSource1.stix.type); + expect(dataSource.stix.name).toBe(dataSource1.stix.name); + expect(dataSource.stix.description).toBe(dataSource1.stix.description); + expect(dataSource.stix.spec_version).toBe(dataSource1.stix.spec_version); + expect(dataSource.stix.object_marking_refs).toEqual( + expect.arrayContaining(dataSource1.stix.object_marking_refs), + ); + expect(dataSource.stix.created_by_ref).toBe(dataSource1.stix.created_by_ref); + expect(dataSource.stix.x_mitre_version).toBe(dataSource1.stix.x_mitre_version); + expect(dataSource.stix.x_mitre_attack_spec_version).toBe( + dataSource1.stix.x_mitre_attack_spec_version, + ); + }); + + it('GET /api/data-sources/:id returns the added data source with data components', async function () { + initialDataComponentData.stix.x_mitre_data_source_ref = dataSource1.stix.id; + await loadDataComponents(initialDataComponentData); + + const res = await request(app) + .get(`/api/data-sources/${dataSource1.stix.id}?retrieveDataComponents=true`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one data source in an array + const dataSources = res.body; + expect(dataSources).toBeDefined(); + expect(Array.isArray(dataSources)).toBe(true); + expect(dataSources.length).toBe(1); + const dataSource = dataSources[0]; + expect(dataSource).toBeDefined(); + + // We expect to get 5 data components that reference this data source + expect(dataSource.dataComponents).toBeDefined(); + expect(dataSource.dataComponents.length).toBe(5); + }); + + it('PUT /api/data-sources updates a data source', async function () { + const originalModified = dataSource1.stix.modified; + const timestamp = new Date().toISOString(); + dataSource1.stix.modified = timestamp; + dataSource1.stix.description = 'This is an updated data source.'; + const body = dataSource1; + const res = await request(app) + .put('/api/data-sources/' + dataSource1.stix.id + '/modified/' + originalModified) + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the updated data source + const dataSource = res.body; + expect(dataSource).toBeDefined(); + expect(dataSource.stix.id).toBe(dataSource1.stix.id); + expect(dataSource.stix.modified).toBe(dataSource1.stix.modified); + }); + + it('POST /api/data-sources does not create a data source with the same id and modified date', async function () { + const body = dataSource1; + await request(app) + .post('/api/data-sources') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(409); + }); + + let dataSource2; + it('POST /api/data-sources should create a new version of a data source with a duplicate stix.id but different stix.modified date', async function () { + dataSource2 = _.cloneDeep(dataSource1); + dataSource2._id = undefined; + dataSource2.__t = undefined; + dataSource2.__v = undefined; + const timestamp = new Date().toISOString(); + dataSource2.stix.modified = timestamp; + const body = dataSource2; + const res = await request(app) + .post('/api/data-sources') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created data source + const dataSource = res.body; + expect(dataSource).toBeDefined(); + }); + + it('GET /api/data-sources returns the latest added data source', async function () { + const res = await request(app) + .get('/api/data-sources/' + dataSource2.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one data source in an array + const dataSources = res.body; + expect(dataSources).toBeDefined(); + expect(Array.isArray(dataSources)).toBe(true); + expect(dataSources.length).toBe(1); + const dataSource = dataSources[0]; + expect(dataSource.stix.id).toBe(dataSource2.stix.id); + expect(dataSource.stix.modified).toBe(dataSource2.stix.modified); + }); + + it('GET /api/data-sources returns all added data source', async function () { + const res = await request(app) + .get('/api/data-sources/' + dataSource1.stix.id + '?versions=all') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get two data sources in an array + const dataSources = res.body; + expect(dataSources).toBeDefined(); + expect(Array.isArray(dataSources)).toBe(true); + expect(dataSources.length).toBe(2); + }); + + it('GET /api/data-sources/:id/modified/:modified returns the first added data source', async function () { + const res = await request(app) + .get('/api/data-sources/' + dataSource1.stix.id + '/modified/' + dataSource1.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one data source in an array + const dataSource = res.body; + expect(dataSource).toBeDefined(); + expect(dataSource.stix).toBeDefined(); + expect(dataSource.stix.id).toBe(dataSource1.stix.id); + expect(dataSource.stix.modified).toBe(dataSource1.stix.modified); + }); + + it('GET /api/data-sources/:id/modified/:modified returns the second added data source', async function () { + const res = await request(app) + .get('/api/data-sources/' + dataSource2.stix.id + '/modified/' + dataSource2.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one data source in an array + const dataSource = res.body; + expect(dataSource).toBeDefined(); + expect(dataSource.stix).toBeDefined(); + expect(dataSource.stix.id).toBe(dataSource2.stix.id); + expect(dataSource.stix.modified).toBe(dataSource2.stix.modified); + }); + + let dataSource3; + it('POST /api/data-sources should create a new version of a data source with a duplicate stix.id but different stix.modified date', async function () { + dataSource3 = _.cloneDeep(dataSource1); + dataSource3._id = undefined; + dataSource3.__t = undefined; + dataSource3.__v = undefined; + const timestamp = new Date().toISOString(); + dataSource3.stix.modified = timestamp; + const body = dataSource3; + const res = await request(app) + .post('/api/data-sources') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created data source + const dataSource = res.body; + expect(dataSource).toBeDefined(); + }); + + it('DELETE /api/data-sources/:id should not delete a data source when the id cannot be found', async function () { + await request(app) + .delete('/api/data-sources/not-an-id') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('DELETE /api/data-sources/:id/modified/:modified deletes a data source', async function () { + await request(app) + .delete('/api/data-sources/' + dataSource1.stix.id + '/modified/' + dataSource1.stix.modified) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('DELETE /api/data-sources/:id should delete all the data sources with the same stix id', async function () { + await request(app) + .delete('/api/data-sources/' + dataSource2.stix.id) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('GET /api/data-sources returns an empty array of data sources', async function () { + const res = await request(app) + .get('/api/data-sources') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const dataSources = res.body; + expect(dataSources).toBeDefined(); + expect(Array.isArray(dataSources)).toBe(true); + expect(dataSources.length).toBe(0); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/api/groups/groups-input-validation.spec.js b/app/tests/api/groups/groups-input-validation.spec.js index 96374ae0..ba2c73ca 100644 --- a/app/tests/api/groups/groups-input-validation.spec.js +++ b/app/tests/api/groups/groups-input-validation.spec.js @@ -12,160 +12,157 @@ logger.level = 'debug'; // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - name: 'intrusion-set-1', - spec_version: '2.1', - type: 'intrusion-set', - description: 'This is a group. Blue.', - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--6444f546-6900-4456-b3b1-015c88d70dab" - } + }, + stix: { + name: 'intrusion-set-1', + spec_version: '2.1', + type: 'intrusion-set', + description: 'This is a group. Blue.', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--6444f546-6900-4456-b3b1-015c88d70dab', + }, }; // getApp must be a function returning the Express app object // This is necessary so that it is available when the test cases are running // (it generally won't be available when the tests are initially created) function executeTests(getApp, propertyName, options) { - options = options ?? {}; - if (options.required) { - it(`POST /api/groups does not create a group when ${propertyName} is missing`, async function () { - const groupData = _.cloneDeep(initialObjectData); - const timestamp = new Date().toISOString(); - groupData.stix.created = timestamp; - groupData.stix.modified = timestamp; - _.set(groupData, propertyName, undefined); - const body = groupData; - await request(getApp()) - .post('/api/groups') - .send(body) - .set('Accept', 'application/json') - .expect(400); - }); - } - - it(`POST /api/groups does not create a group when ${ propertyName } is a number`, async function () { - const groupData = _.cloneDeep(initialObjectData); - const timestamp = new Date().toISOString(); - groupData.stix.created = timestamp; - groupData.stix.modified = timestamp; - _.set(groupData, propertyName, 9); - const body = groupData; - await request(getApp()) - .post('/api/groups') - .send(body) - .set('Accept', 'application/json') - .expect(400); - }); - - it(`POST /api/groups does not create a group when ${ propertyName } is an object`, async function () { - const groupData = _.cloneDeep(initialObjectData); - const timestamp = new Date().toISOString(); - groupData.stix.created = timestamp; - groupData.stix.modified = timestamp; - _.set(groupData, propertyName, { value: 'group-name' }); - const body = groupData; - await request(getApp()) - .post('/api/groups') - .send(body) - .set('Accept', 'application/json') - .expect(400); + options = options ?? {}; + if (options.required) { + it(`POST /api/groups does not create a group when ${propertyName} is missing`, async function () { + const groupData = _.cloneDeep(initialObjectData); + const timestamp = new Date().toISOString(); + groupData.stix.created = timestamp; + groupData.stix.modified = timestamp; + _.set(groupData, propertyName, undefined); + const body = groupData; + await request(getApp()) + .post('/api/groups') + .send(body) + .set('Accept', 'application/json') + .expect(400); }); + } + + it(`POST /api/groups does not create a group when ${propertyName} is a number`, async function () { + const groupData = _.cloneDeep(initialObjectData); + const timestamp = new Date().toISOString(); + groupData.stix.created = timestamp; + groupData.stix.modified = timestamp; + _.set(groupData, propertyName, 9); + const body = groupData; + await request(getApp()) + .post('/api/groups') + .send(body) + .set('Accept', 'application/json') + .expect(400); + }); + + it(`POST /api/groups does not create a group when ${propertyName} is an object`, async function () { + const groupData = _.cloneDeep(initialObjectData); + const timestamp = new Date().toISOString(); + groupData.stix.created = timestamp; + groupData.stix.modified = timestamp; + _.set(groupData, propertyName, { value: 'group-name' }); + const body = groupData; + await request(getApp()) + .post('/api/groups') + .send(body) + .set('Accept', 'application/json') + .expect(400); + }); } describe('Groups API Input Validation', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Wait until the indexes are created - await Group.init(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - it('POST /api/groups does not create an empty group', async function () { - const body = { }; - await request(app) - .post('/api/groups') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - it('POST /api/groups does not create a group when an unknown query parameter is provided', async function () { - const groupData = _.cloneDeep(initialObjectData); - const timestamp = new Date().toISOString(); - groupData.stix.created = timestamp; - groupData.stix.modified = timestamp; - const body = groupData; - await request(app) - .post('/api/groups?not-a-parameter=unexpectedvalue') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - it('POST /api/groups does not create a group when an invalid type is provided', async function () { - const groupData = _.cloneDeep(initialObjectData); - const timestamp = new Date().toISOString(); - groupData.stix.created = timestamp; - groupData.stix.modified = timestamp; - groupData.stix.type= 'not-a-type'; - const body = groupData; - await request(app) - .post('/api/groups') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - it('POST /api/groups does not create a group when an incorrect type is provided', async function () { - const groupData = _.cloneDeep(initialObjectData); - const timestamp = new Date().toISOString(); - groupData.stix.created = timestamp; - groupData.stix.modified = timestamp; - groupData.stix.type= 'malware'; - const body = groupData; - await request(app) - .post('/api/groups') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - executeTests(() => app, 'stix.type', { required: true }); - executeTests(() => app, 'stix.spec_version', { required: true }); - executeTests(() => app, 'stix.name', { required: true }); - executeTests(() => app, 'stix.description'); - executeTests(() => app, 'stix.x_mitre_modified_by_ref'); - executeTests(() => app, 'stix.x_mitre_version'); - executeTests(() => app, 'stix.x_mitre_attack_spec_version'); - - after(async function() { - await database.closeConnection(); - }); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Wait until the indexes are created + await Group.init(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + it('POST /api/groups does not create an empty group', async function () { + const body = {}; + await request(app) + .post('/api/groups') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + it('POST /api/groups does not create a group when an unknown query parameter is provided', async function () { + const groupData = _.cloneDeep(initialObjectData); + const timestamp = new Date().toISOString(); + groupData.stix.created = timestamp; + groupData.stix.modified = timestamp; + const body = groupData; + await request(app) + .post('/api/groups?not-a-parameter=unexpectedvalue') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + it('POST /api/groups does not create a group when an invalid type is provided', async function () { + const groupData = _.cloneDeep(initialObjectData); + const timestamp = new Date().toISOString(); + groupData.stix.created = timestamp; + groupData.stix.modified = timestamp; + groupData.stix.type = 'not-a-type'; + const body = groupData; + await request(app) + .post('/api/groups') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + it('POST /api/groups does not create a group when an incorrect type is provided', async function () { + const groupData = _.cloneDeep(initialObjectData); + const timestamp = new Date().toISOString(); + groupData.stix.created = timestamp; + groupData.stix.modified = timestamp; + groupData.stix.type = 'malware'; + const body = groupData; + await request(app) + .post('/api/groups') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + executeTests(() => app, 'stix.type', { required: true }); + executeTests(() => app, 'stix.spec_version', { required: true }); + executeTests(() => app, 'stix.name', { required: true }); + executeTests(() => app, 'stix.description'); + executeTests(() => app, 'stix.x_mitre_modified_by_ref'); + executeTests(() => app, 'stix.x_mitre_version'); + executeTests(() => app, 'stix.x_mitre_attack_spec_version'); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/api/groups/groups-pagination.spec.js b/app/tests/api/groups/groups-pagination.spec.js index 7284aaae..ee45c43a 100644 --- a/app/tests/api/groups/groups-pagination.spec.js +++ b/app/tests/api/groups/groups-pagination.spec.js @@ -4,27 +4,25 @@ const PaginationTests = require('../../shared/pagination'); // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - spec_version: '2.1', - type: 'intrusion-set', - description: 'This is a group. Blue.', - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--6444f546-6900-4456-b3b1-015c88d70dab" - } + }, + stix: { + spec_version: '2.1', + type: 'intrusion-set', + description: 'This is a group. Blue.', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--6444f546-6900-4456-b3b1-015c88d70dab', + }, }; const options = { - prefix: 'intrustion-set', - baseUrl: '/api/groups', - label: 'Groups' -} + prefix: 'intrustion-set', + baseUrl: '/api/groups', + label: 'Groups', +}; const paginationTests = new PaginationTests(groupsService, initialObjectData, options); paginationTests.executeTests(); diff --git a/app/tests/api/groups/groups.query.json b/app/tests/api/groups/groups.query.json index b51fe398..82bd3eeb 100644 --- a/app/tests/api/groups/groups.query.json +++ b/app/tests/api/groups/groups.query.json @@ -1,15 +1,13 @@ { "workspace": { - "workflow": { - - } + "workflow": {} }, "stix": { "spec_version": "2.1", "type": "intrusion-set", "description": "This is a group.", "external_references": [], - "object_marking_refs": [ "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" ], + "object_marking_refs": ["marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"], "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "x_mitre_version": "1.0" } diff --git a/app/tests/api/groups/groups.query.spec.js b/app/tests/api/groups/groups.query.spec.js index 7d6210b8..93700a02 100644 --- a/app/tests/api/groups/groups.query.spec.js +++ b/app/tests/api/groups/groups.query.spec.js @@ -17,340 +17,334 @@ const userAccountsService = require('../../../services/user-accounts-service'); const groupsService = require('../../../services/groups-service'); function asyncWait(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } async function readJson(path) { - const data = await fs.readFile(require.resolve(path)); - return JSON.parse(data); + const data = await fs.readFile(require.resolve(path)); + return JSON.parse(data); } function makeExternalReference(attackId) { - return { source_name: 'mitre-attack', external_id: attackId, url: `https://attack.mitre.org/groups/${ attackId }` }; + return { + source_name: 'mitre-attack', + external_id: attackId, + url: `https://attack.mitre.org/groups/${attackId}`, + }; } async function configureGroups(baseGroup, userAccountId1, userAccountId2) { - const groups = []; - // x_mitre_deprecated,revoked undefined (user account 1) - const data1a = _.cloneDeep(baseGroup); - data1a.stix.external_references.push(makeExternalReference('G0001')); - data1a.userAccountId = userAccountId1; - groups.push(data1a); - - // x_mitre_deprecated,revoked undefined (user account 2) - const data1b = _.cloneDeep(baseGroup); - data1b.stix.external_references.push(makeExternalReference('G0010')); - data1b.userAccountId = userAccountId2; - groups.push(data1b); - - // x_mitre_deprecated = false, revoked = false - const data2 = _.cloneDeep(baseGroup); - data2.stix.external_references.push(makeExternalReference('G0002')); - data2.stix.x_mitre_deprecated = false; - data2.stix.revoked = false; - data2.workspace.workflow = { state: 'work-in-progress' }; - data2.userAccountId = userAccountId1; - groups.push(data2); - - // x_mitre_deprecated = true, revoked = false - const data3 = _.cloneDeep(baseGroup); - data3.stix.external_references.push(makeExternalReference('G0003')); - data3.stix.x_mitre_deprecated = true; - data3.stix.revoked = false; - data3.workspace.workflow = { state: 'awaiting-review' }; - data3.userAccountId = userAccountId1; - groups.push(data3); - - // x_mitre_deprecated = false, revoked = true - const data4 = _.cloneDeep(baseGroup); - data4.stix.external_references.push(makeExternalReference('G0004')); - data4.stix.x_mitre_deprecated = false; - data4.stix.revoked = true; - data4.workspace.workflow = { state: 'awaiting-review' }; - data4.userAccountId = userAccountId1; - groups.push(data4); - - // multiple versions, last version has x_mitre_deprecated = true, revoked = true - const data5a = _.cloneDeep(baseGroup); - const id = `intrusion-set--${uuid.v4()}`; - data5a.stix.external_references.push(makeExternalReference('G0005')); - data5a.stix.id = id; - data5a.stix.name = 'multiple-versions' - data5a.workspace.workflow = { state: 'awaiting-review' }; - const createdTimestamp = new Date().toISOString(); - data5a.stix.created = createdTimestamp; - data5a.stix.modified = createdTimestamp; - data5a.userAccountId = userAccountId1; - groups.push(data5a); - - await asyncWait(10); // wait so the modified timestamp can change - const data5b = _.cloneDeep(baseGroup); - data5b.stix.external_references.push(makeExternalReference('G0005')); - data5b.stix.id = id; - data5b.stix.name = 'multiple-versions' - data5b.workspace.workflow = { state: 'awaiting-review' }; - data5b.stix.created = createdTimestamp; - let timestamp = new Date().toISOString(); - data5b.stix.modified = timestamp; - data5b.userAccountId = userAccountId1; - groups.push(data5b); - - await asyncWait(10); - const data5c = _.cloneDeep(baseGroup); - data5c.stix.external_references.push(makeExternalReference('G0005')); - data5c.stix.id = id; - data5c.stix.name = 'multiple-versions' - data5c.workspace.workflow = { state: 'awaiting-review' }; - data5c.stix.x_mitre_deprecated = true; - data5c.stix.revoked = true; - data5c.stix.created = createdTimestamp; - timestamp = new Date().toISOString(); - data5c.stix.modified = timestamp; - data5c.userAccountId = userAccountId2; - groups.push(data5c); - -// logger.info(JSON.stringify(groups, null, 4)); - - return groups; + const groups = []; + // x_mitre_deprecated,revoked undefined (user account 1) + const data1a = _.cloneDeep(baseGroup); + data1a.stix.external_references.push(makeExternalReference('G0001')); + data1a.userAccountId = userAccountId1; + groups.push(data1a); + + // x_mitre_deprecated,revoked undefined (user account 2) + const data1b = _.cloneDeep(baseGroup); + data1b.stix.external_references.push(makeExternalReference('G0010')); + data1b.userAccountId = userAccountId2; + groups.push(data1b); + + // x_mitre_deprecated = false, revoked = false + const data2 = _.cloneDeep(baseGroup); + data2.stix.external_references.push(makeExternalReference('G0002')); + data2.stix.x_mitre_deprecated = false; + data2.stix.revoked = false; + data2.workspace.workflow = { state: 'work-in-progress' }; + data2.userAccountId = userAccountId1; + groups.push(data2); + + // x_mitre_deprecated = true, revoked = false + const data3 = _.cloneDeep(baseGroup); + data3.stix.external_references.push(makeExternalReference('G0003')); + data3.stix.x_mitre_deprecated = true; + data3.stix.revoked = false; + data3.workspace.workflow = { state: 'awaiting-review' }; + data3.userAccountId = userAccountId1; + groups.push(data3); + + // x_mitre_deprecated = false, revoked = true + const data4 = _.cloneDeep(baseGroup); + data4.stix.external_references.push(makeExternalReference('G0004')); + data4.stix.x_mitre_deprecated = false; + data4.stix.revoked = true; + data4.workspace.workflow = { state: 'awaiting-review' }; + data4.userAccountId = userAccountId1; + groups.push(data4); + + // multiple versions, last version has x_mitre_deprecated = true, revoked = true + const data5a = _.cloneDeep(baseGroup); + const id = `intrusion-set--${uuid.v4()}`; + data5a.stix.external_references.push(makeExternalReference('G0005')); + data5a.stix.id = id; + data5a.stix.name = 'multiple-versions'; + data5a.workspace.workflow = { state: 'awaiting-review' }; + const createdTimestamp = new Date().toISOString(); + data5a.stix.created = createdTimestamp; + data5a.stix.modified = createdTimestamp; + data5a.userAccountId = userAccountId1; + groups.push(data5a); + + await asyncWait(10); // wait so the modified timestamp can change + const data5b = _.cloneDeep(baseGroup); + data5b.stix.external_references.push(makeExternalReference('G0005')); + data5b.stix.id = id; + data5b.stix.name = 'multiple-versions'; + data5b.workspace.workflow = { state: 'awaiting-review' }; + data5b.stix.created = createdTimestamp; + let timestamp = new Date().toISOString(); + data5b.stix.modified = timestamp; + data5b.userAccountId = userAccountId1; + groups.push(data5b); + + await asyncWait(10); + const data5c = _.cloneDeep(baseGroup); + data5c.stix.external_references.push(makeExternalReference('G0005')); + data5c.stix.id = id; + data5c.stix.name = 'multiple-versions'; + data5c.workspace.workflow = { state: 'awaiting-review' }; + data5c.stix.x_mitre_deprecated = true; + data5c.stix.revoked = true; + data5c.stix.created = createdTimestamp; + timestamp = new Date().toISOString(); + data5c.stix.modified = timestamp; + data5c.userAccountId = userAccountId2; + groups.push(data5c); + + // logger.info(JSON.stringify(groups, null, 4)); + + return groups; } async function loadGroups(groups) { - for (const group of groups) { - if (!group.stix.name) { - group.stix.name = `group-${group.stix.x_mitre_deprecated}-${group.stix.revoked}`; - } - - if (!group.stix.created) { - const timestamp = new Date().toISOString(); - group.stix.created = timestamp; - group.stix.modified = timestamp; - } - - // eslint-disable-next-line no-await-in-loop - await groupsService.create(group, { import: false, userAccountId: group.userAccountId }); + for (const group of groups) { + if (!group.stix.name) { + group.stix.name = `group-${group.stix.x_mitre_deprecated}-${group.stix.revoked}`; } + + if (!group.stix.created) { + const timestamp = new Date().toISOString(); + group.stix.created = timestamp; + group.stix.modified = timestamp; + } + + // eslint-disable-next-line no-await-in-loop + await groupsService.create(group, { import: false, userAccountId: group.userAccountId }); + } } const userAccountData1 = { - email: 'test-blue@test.org', - username: 'test-blue@test.org', - displayName: 'Test User Blue', - status: 'active', - role: 'editor' + email: 'test-blue@test.org', + username: 'test-blue@test.org', + displayName: 'Test User Blue', + status: 'active', + role: 'editor', }; const userAccountData2 = { - email: 'test-red@test.org', - username: 'test-red@test.org', - displayName: 'Test User Red', - status: 'active', - role: 'editor' + email: 'test-red@test.org', + username: 'test-red@test.org', + displayName: 'Test User Red', + status: 'active', + role: 'editor', }; let userAccount1; let userAccount2; describe('Groups API Queries', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - - userAccount1 = await userAccountsService.create(userAccountData1); - userAccount2 = await userAccountsService.create(userAccountData2); - - const baseGroup = await readJson('./groups.query.json'); - const groups = await configureGroups(baseGroup, userAccount1.id, userAccount2.id); - await loadGroups(groups); - }); - - it('GET /api/groups should return 3 of the preloaded groups', async function () { - const res = await request(app) - .get('/api/groups') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get both of the non-deprecated, non-revoked groups - const groups = res.body; - expect(groups).toBeDefined(); - expect(Array.isArray(groups)).toBe(true); - expect(groups.length).toBe(3); - - }); - - it('GET /api/groups should return groups with x_mitre_deprecated not set to true (false or undefined)', async function () { - const res = await request(app) - .get('/api/groups?includeDeprecated=false') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get both of the non-deprecated, non-revoked groups - const groups = res.body; - expect(groups).toBeDefined(); - expect(Array.isArray(groups)).toBe(true); - expect(groups.length).toBe(3); - - }); - - it('GET /api/groups should return all non-revoked groups', async function () { - const res = await request(app) - .get('/api/groups?includeDeprecated=true') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get all the non-revoked groups - const groups = res.body; - expect(groups).toBeDefined(); - expect(Array.isArray(groups)).toBe(true); - expect(groups.length).toBe(4); - - }); - - it('GET /api/groups should return groups with revoked not set to true (false or undefined)', async function () { - const res = await request(app) - .get('/api/groups?includeRevoked=false') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get all the non-revoked groups - const groups = res.body; - expect(groups).toBeDefined(); - expect(Array.isArray(groups)).toBe(true); - expect(groups.length).toBe(3); - - }); - - it('GET /api/groups should return all non-deprecated groups', async function () { - const res = await request(app) - .get('/api/groups?includeRevoked=true') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get all the non-deprecated groups - const groups = res.body; - expect(groups).toBeDefined(); - expect(Array.isArray(groups)).toBe(true); - expect(groups.length).toBe(4); - - }); - - it('GET /api/groups should return groups with workflow.state set to work-in-progress', async function () { - const res = await request(app) - .get('/api/groups?state=work-in-progress') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the group with the correct workflow.state - const groups = res.body; - expect(groups).toBeDefined(); - expect(Array.isArray(groups)).toBe(true); - expect(groups.length).toBe(1); - - const group = groups[0]; - expect(group.workspace.workflow.state).toEqual('work-in-progress'); - - }); - - it('GET /api/groups should return groups with the ATT&CK ID G0001', async function () { - const res = await request(app) - .get('/api/groups?search=G0001') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the latest group with the correct ATT&CK ID - const groups = res.body; - logger.info(`Received groups: ${groups}`); - console.log(`Received groups: ${JSON.stringify(groups)}`); - expect(groups).toBeDefined(); - expect(Array.isArray(groups)).toBe(true); - expect(groups.length).toBe(1); - - const group = groups[0]; - logger.info(`Received group: ${JSON.stringify(group)}`); - expect(group.workspace.attack_id).toEqual('G0001'); - - }); - - it('GET /api/groups should return groups created by userAccount1', async function () { - const res = await request(app) - .get(`/api/groups?lastUpdatedBy=${ userAccount1.id }`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the (non-deprecated, non-revoked) groups created by userAccount1 - const groups = res.body; - expect(groups).toBeDefined(); - expect(Array.isArray(groups)).toBe(true); - expect(groups.length).toBe(2); - - expect(groups[0].workspace.workflow.created_by_user_account).toEqual(userAccount1.id); - expect(groups[1].workspace.workflow.created_by_user_account).toEqual(userAccount1.id); - - }); - - it('GET /api/groups should return groups created by userAccount2', async function () { - const res = await request(app) - .get(`/api/groups?lastUpdatedBy=${ userAccount2.id }`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the (non-deprecated, non-revoked) group created by userAccount2 - const groups = res.body; - expect(groups).toBeDefined(); - expect(Array.isArray(groups)).toBe(true); - expect(groups.length).toBe(1); - - expect(groups[0].workspace.workflow.created_by_user_account).toEqual(userAccount2.id); - - }); - - it('GET /api/groups should return groups created by both userAccount1 and userAccount2', async function () { - const res = await request(app) - .get(`/api/groups?lastUpdatedBy=${ userAccount1.id }&lastUpdatedBy=${ userAccount2.id }`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the (non-deprecated, non-revoked) groups created by both user accounts - const groups = res.body; - expect(groups).toBeDefined(); - expect(Array.isArray(groups)).toBe(true); - expect(groups.length).toBe(3); - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + + userAccount1 = await userAccountsService.create(userAccountData1); + userAccount2 = await userAccountsService.create(userAccountData2); + + const baseGroup = await readJson('./groups.query.json'); + const groups = await configureGroups(baseGroup, userAccount1.id, userAccount2.id); + await loadGroups(groups); + }); + + it('GET /api/groups should return 3 of the preloaded groups', async function () { + const res = await request(app) + .get('/api/groups') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get both of the non-deprecated, non-revoked groups + const groups = res.body; + expect(groups).toBeDefined(); + expect(Array.isArray(groups)).toBe(true); + expect(groups.length).toBe(3); + }); + + it('GET /api/groups should return groups with x_mitre_deprecated not set to true (false or undefined)', async function () { + const res = await request(app) + .get('/api/groups?includeDeprecated=false') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get both of the non-deprecated, non-revoked groups + const groups = res.body; + expect(groups).toBeDefined(); + expect(Array.isArray(groups)).toBe(true); + expect(groups.length).toBe(3); + }); + + it('GET /api/groups should return all non-revoked groups', async function () { + const res = await request(app) + .get('/api/groups?includeDeprecated=true') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get all the non-revoked groups + const groups = res.body; + expect(groups).toBeDefined(); + expect(Array.isArray(groups)).toBe(true); + expect(groups.length).toBe(4); + }); + + it('GET /api/groups should return groups with revoked not set to true (false or undefined)', async function () { + const res = await request(app) + .get('/api/groups?includeRevoked=false') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get all the non-revoked groups + const groups = res.body; + expect(groups).toBeDefined(); + expect(Array.isArray(groups)).toBe(true); + expect(groups.length).toBe(3); + }); + + it('GET /api/groups should return all non-deprecated groups', async function () { + const res = await request(app) + .get('/api/groups?includeRevoked=true') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get all the non-deprecated groups + const groups = res.body; + expect(groups).toBeDefined(); + expect(Array.isArray(groups)).toBe(true); + expect(groups.length).toBe(4); + }); + + it('GET /api/groups should return groups with workflow.state set to work-in-progress', async function () { + const res = await request(app) + .get('/api/groups?state=work-in-progress') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the group with the correct workflow.state + const groups = res.body; + expect(groups).toBeDefined(); + expect(Array.isArray(groups)).toBe(true); + expect(groups.length).toBe(1); + + const group = groups[0]; + expect(group.workspace.workflow.state).toEqual('work-in-progress'); + }); + + it('GET /api/groups should return groups with the ATT&CK ID G0001', async function () { + const res = await request(app) + .get('/api/groups?search=G0001') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the latest group with the correct ATT&CK ID + const groups = res.body; + logger.info(`Received groups: ${groups}`); + console.log(`Received groups: ${JSON.stringify(groups)}`); + expect(groups).toBeDefined(); + expect(Array.isArray(groups)).toBe(true); + expect(groups.length).toBe(1); + + const group = groups[0]; + logger.info(`Received group: ${JSON.stringify(group)}`); + expect(group.workspace.attack_id).toEqual('G0001'); + }); + + it('GET /api/groups should return groups created by userAccount1', async function () { + const res = await request(app) + .get(`/api/groups?lastUpdatedBy=${userAccount1.id}`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the (non-deprecated, non-revoked) groups created by userAccount1 + const groups = res.body; + expect(groups).toBeDefined(); + expect(Array.isArray(groups)).toBe(true); + expect(groups.length).toBe(2); + + expect(groups[0].workspace.workflow.created_by_user_account).toEqual(userAccount1.id); + expect(groups[1].workspace.workflow.created_by_user_account).toEqual(userAccount1.id); + }); + + it('GET /api/groups should return groups created by userAccount2', async function () { + const res = await request(app) + .get(`/api/groups?lastUpdatedBy=${userAccount2.id}`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the (non-deprecated, non-revoked) group created by userAccount2 + const groups = res.body; + expect(groups).toBeDefined(); + expect(Array.isArray(groups)).toBe(true); + expect(groups.length).toBe(1); + + expect(groups[0].workspace.workflow.created_by_user_account).toEqual(userAccount2.id); + }); + + it('GET /api/groups should return groups created by both userAccount1 and userAccount2', async function () { + const res = await request(app) + .get(`/api/groups?lastUpdatedBy=${userAccount1.id}&lastUpdatedBy=${userAccount2.id}`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the (non-deprecated, non-revoked) groups created by both user accounts + const groups = res.body; + expect(groups).toBeDefined(); + expect(Array.isArray(groups)).toBe(true); + expect(groups.length).toBe(3); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/api/groups/groups.spec.js b/app/tests/api/groups/groups.spec.js index 8e7bbdf4..515084b7 100644 --- a/app/tests/api/groups/groups.spec.js +++ b/app/tests/api/groups/groups.spec.js @@ -17,476 +17,469 @@ logger.level = 'debug'; // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - name: 'intrusion-set-1', - spec_version: '2.1', - type: 'intrusion-set', - description: 'This is the initial group. Blue.', - created_by_ref: "identity--6444f546-6900-4456-b3b1-015c88d70dab" - } + }, + stix: { + name: 'intrusion-set-1', + spec_version: '2.1', + type: 'intrusion-set', + description: 'This is the initial group. Blue.', + created_by_ref: 'identity--6444f546-6900-4456-b3b1-015c88d70dab', + }, }; const markingDefinitionData = { - workspace: { - workflow: { - state: 'reviewed' - } + workspace: { + workflow: { + state: 'reviewed', }, - stix: { - spec_version: '2.1', - type: 'marking-definition', - definition_type: 'statement', - definition: { statement: 'This is a marking definition.' }, - created_by_ref: "identity--6444f546-6900-4456-b3b1-015c88d70dab" - } + }, + stix: { + spec_version: '2.1', + type: 'marking-definition', + definition_type: 'statement', + definition: { statement: 'This is a marking definition.' }, + created_by_ref: 'identity--6444f546-6900-4456-b3b1-015c88d70dab', + }, }; async function addDefaultMarkingDefinition(markingDefinitionData) { - // Save the marking definition - const timestamp = new Date().toISOString(); - markingDefinitionData.stix.created = timestamp; - const savedMarkingDefinition = await markingDefinitionService.create(markingDefinitionData); + // Save the marking definition + const timestamp = new Date().toISOString(); + markingDefinitionData.stix.created = timestamp; + const savedMarkingDefinition = await markingDefinitionService.create(markingDefinitionData); - // Get the current list of default marking definitions - const defaultMarkingDefinitions = await systemConfigurationService.retrieveDefaultMarkingDefinitions({ refOnly: true }); + // Get the current list of default marking definitions + const defaultMarkingDefinitions = + await systemConfigurationService.retrieveDefaultMarkingDefinitions({ refOnly: true }); - // Add the new marking definition to the list and save it - defaultMarkingDefinitions.push(savedMarkingDefinition.stix.id); - await systemConfigurationService.setDefaultMarkingDefinitions(defaultMarkingDefinitions); + // Add the new marking definition to the list and save it + defaultMarkingDefinitions.push(savedMarkingDefinition.stix.id); + await systemConfigurationService.setDefaultMarkingDefinitions(defaultMarkingDefinitions); - return savedMarkingDefinition; + return savedMarkingDefinition; } describe('Groups API', function () { - let app; - let defaultMarkingDefinition1; - let defaultMarkingDefinition2; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Wait until the indexes are created - await Group.init(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - - defaultMarkingDefinition1 = await addDefaultMarkingDefinition(markingDefinitionData); - }); - - it('GET /api/groups returns an empty array of groups', async function () { - const res = await request(app) - .get('/api/groups') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const groups = res.body; - expect(groups).toBeDefined(); - expect(Array.isArray(groups)).toBe(true); - expect(groups.length).toBe(0); - - }); - - it('POST /api/groups does not create an empty group', async function () { - const body = { }; - await request(app) - .post('/api/groups') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - let group1; - it('POST /api/groups creates a group', async function () { - const timestamp = new Date().toISOString(); - initialObjectData.stix.created = timestamp; - initialObjectData.stix.modified = timestamp; - const body = initialObjectData; - const res = await request(app) - .post('/api/groups') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - // We expect to get the created group - group1 = res.body; - expect(group1).toBeDefined(); - expect(group1.stix).toBeDefined(); - expect(group1.stix.id).toBeDefined(); - expect(group1.stix.created).toBeDefined(); - expect(group1.stix.modified).toBeDefined(); - expect(group1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); - - // object_marking_refs should contain the default marking definition - expect(group1.stix.object_marking_refs).toBeDefined(); - expect(Array.isArray(group1.stix.object_marking_refs)).toBe(true); - expect(group1.stix.object_marking_refs.length).toBe(1); - expect(group1.stix.object_marking_refs[0]).toBe(defaultMarkingDefinition1.stix.id); - - }); - - it('GET /api/groups returns the added group', async function () { - const res = await request(app) - .get('/api/groups') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one group in an array - const groups = res.body; - expect(groups).toBeDefined(); - expect(Array.isArray(groups)).toBe(true); - expect(groups.length).toBe(1); - }); - - it('GET /api/groups/:id should not return a group when the id cannot be found', async function () { - await request(app) - .get('/api/groups/not-an-id') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('GET /api/groups/:id returns the added group', async function () { - const res = await request(app) - .get('/api/groups/' + group1.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one group in an array - const groups = res.body; - expect(groups).toBeDefined(); - expect(Array.isArray(groups)).toBe(true); - expect(groups.length).toBe(1); - - const group = groups[0]; - expect(group).toBeDefined(); - expect(group.stix).toBeDefined(); - expect(group.stix.id).toBe(group1.stix.id); - expect(group.stix.type).toBe(group1.stix.type); - expect(group.stix.name).toBe(group1.stix.name); - expect(group.stix.description).toBe(group1.stix.description); - expect(group.stix.spec_version).toBe(group1.stix.spec_version); - expect(group.stix.object_marking_refs).toEqual(expect.arrayContaining(group1.stix.object_marking_refs)); - expect(group.stix.created_by_ref).toBe(group1.stix.created_by_ref); - expect(group.stix.x_mitre_attack_spec_version).toBe(group1.stix.x_mitre_attack_spec_version); - }); - - it('PUT /api/groups updates a group', async function () { - const originalModified = group1.stix.modified; - const timestamp = new Date().toISOString(); - group1.stix.modified = timestamp; - group1.stix.description = 'This is an updated group. Blue.' - const body = group1; - const res = await request(app) - .put('/api/groups/' + group1.stix.id + '/modified/' + originalModified) - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the updated group - const group = res.body; - expect(group).toBeDefined(); - expect(group.stix.id).toBe(group1.stix.id); - expect(group.stix.modified).toBe(group1.stix.modified); - }); - - it('POST /api/groups does not create a group with the same id and modified date', async function () { - const body = group1; - await request(app) - .post('/api/groups') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(409); - }); - - let group2; - it('POST /api/groups should create a new version of a group with a duplicate stix.id but different stix.modified date', async function () { - // Add another default marking definition - markingDefinitionData.stix.definition.statement = 'This is the second default marking definition'; - defaultMarkingDefinition2 = await addDefaultMarkingDefinition(markingDefinitionData); - - group2 = _.cloneDeep(group1); - group2._id = undefined; - group2.__t = undefined; - group2.__v = undefined; - const timestamp = new Date().toISOString(); - group2.stix.modified = timestamp; - group2.stix.description = 'This is a new version of a group. Green.'; - - const body = group2; - const res = await request(app) - .post('/api/groups') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created group - const group = res.body; - expect(group).toBeDefined(); - }); - - it('GET /api/groups returns the latest added group', async function () { - const res = await request(app) - .get('/api/groups/' + group2.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one group in an array - const groups = res.body; - expect(groups).toBeDefined(); - expect(Array.isArray(groups)).toBe(true); - expect(groups.length).toBe(1); - const group = groups[0]; - expect(group.stix.id).toBe(group2.stix.id); - expect(group.stix.modified).toBe(group2.stix.modified); - - // object_marking_refs should contain the two default marking definition - expect(group.stix.object_marking_refs).toBeDefined(); - expect(Array.isArray(group.stix.object_marking_refs)).toBe(true); - expect(group.stix.object_marking_refs.length).toBe(2); - expect(group.stix.object_marking_refs.includes(defaultMarkingDefinition1.stix.id)).toBe(true); - expect(group.stix.object_marking_refs.includes(defaultMarkingDefinition2.stix.id)).toBe(true); - }); - - it('GET /api/groups returns all added groups', async function () { - const res = await request(app) - .get('/api/groups/' + group1.stix.id + '?versions=all') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get two groups in an array - const groups = res.body; - expect(groups).toBeDefined(); - expect(Array.isArray(groups)).toBe(true); - expect(groups.length).toBe(2); - - }); - - it('GET /api/groups/:id/modified/:modified returns the first added group', async function () { - const res = await request(app) - .get('/api/groups/' + group1.stix.id + '/modified/' + group1.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one group - const group = res.body; - expect(group).toBeDefined(); - expect(group.stix).toBeDefined(); - expect(group.stix.id).toBe(group1.stix.id); - expect(group.stix.modified).toBe(group1.stix.modified); - }); - - it('GET /api/groups/:id/modified/:modified returns the second added group', async function () { - const res = await request(app) - .get('/api/groups/' + group2.stix.id + '/modified/' + group2.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - - // We expect to get one group - const group = res.body; - expect(group).toBeDefined(); - expect(group.stix).toBeDefined(); - expect(group.stix.id).toBe(group2.stix.id); - expect(group.stix.modified).toBe(group2.stix.modified); - - }); - - let group3; - it('POST /api/groups should create a new group with a different stix.id', async function () { - const group = _.cloneDeep(initialObjectData); - group._id = undefined; - group.__t = undefined; - group.__v = undefined; - group.stix.id = undefined; - const timestamp = new Date().toISOString(); - group.stix.created = timestamp; - group.stix.modified = timestamp; - group.stix.name = 'Mr. Brown'; - group.stix.description = 'This is a new group. Red.'; - const body = group; - const res = await request(app) - .post('/api/groups') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created group - group3 = res.body; - expect(group3).toBeDefined(); - - }); - - let group4; - it('POST /api/groups should create a new version of a group with a duplicate stix.id but different stix.modified date', async function () { - group4 = _.cloneDeep(group1); - group4._id = undefined; - group4.__t = undefined; - group4.__v = undefined; - const timestamp = new Date().toISOString(); - group4.stix.modified = timestamp; - group4.stix.description = 'This is a new version of a group. Yellow.'; - - const body = group4; - const res = await request(app) - .post('/api/groups') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created group - const group = res.body; - expect(group).toBeDefined(); - }); - - it('GET /api/groups uses the search parameter to return the latest version of the group', async function () { - const res = await request(app) - .get('/api/groups?search=yellow') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one group in an array - const groups = res.body; - expect(groups).toBeDefined(); - expect(Array.isArray(groups)).toBe(true); - expect(groups.length).toBe(1); - - // We expect it to be the latest version of the group - const group = groups[0]; - expect(group).toBeDefined(); - expect(group.stix).toBeDefined(); - expect(group.stix.id).toBe(group4.stix.id); - expect(group.stix.modified).toBe(group4.stix.modified); - - }); - - it('GET /api/groups should not get the first version of the group when using the search parameter', async function () { - const res = await request(app) - .get('/api/groups?search=blue') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get zero groups in an array - const groups = res.body; - expect(groups).toBeDefined(); - expect(Array.isArray(groups)).toBe(true); - expect(groups.length).toBe(0); - - }); - - it('GET /api/groups uses the search parameter to return the group using the name property', async function () { - const res = await request(app) - .get('/api/groups?search=brown') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one group in an array - const groups = res.body; - expect(groups).toBeDefined(); - expect(Array.isArray(groups)).toBe(true); - expect(groups.length).toBe(1); - - // We expect it to be the third group - const group = groups[0]; - expect(group).toBeDefined(); - expect(group.stix).toBeDefined(); - expect(group.stix.id).toBe(group3.stix.id); - expect(group.stix.modified).toBe(group3.stix.modified); - - }); - - it('DELETE /api/groups/:id should not delete a group when the id cannot be found', async function () { - await request(app) - .delete('/api/groups/not-an-id') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('DELETE /api/groups/:id/modified/:modified deletes a group', async function () { - await request(app) - .delete('/api/groups/' + group1.stix.id + '/modified/' + group1.stix.modified) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - }); - - it('DELETE /api/groups/:id should delete all the groups with the same stix id', async function () { - await request(app) - .delete('/api/groups/' + group2.stix.id) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - }); - - it('DELETE /api/groups/:id/modified/:modified should delete the third group', async function () { - await request(app) - .delete('/api/groups/' + group3.stix.id + '/modified/' + group3.stix.modified) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - }); - - it('GET /api/groups returns an empty array of groups', async function () { - const res = await request(app) - .get('/api/groups') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const groups = res.body; - expect(groups).toBeDefined(); - expect(Array.isArray(groups)).toBe(true); - expect(groups.length).toBe(0); - - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let defaultMarkingDefinition1; + let defaultMarkingDefinition2; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Wait until the indexes are created + await Group.init(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + + defaultMarkingDefinition1 = await addDefaultMarkingDefinition(markingDefinitionData); + }); + + it('GET /api/groups returns an empty array of groups', async function () { + const res = await request(app) + .get('/api/groups') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const groups = res.body; + expect(groups).toBeDefined(); + expect(Array.isArray(groups)).toBe(true); + expect(groups.length).toBe(0); + }); + + it('POST /api/groups does not create an empty group', async function () { + const body = {}; + await request(app) + .post('/api/groups') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + let group1; + it('POST /api/groups creates a group', async function () { + const timestamp = new Date().toISOString(); + initialObjectData.stix.created = timestamp; + initialObjectData.stix.modified = timestamp; + const body = initialObjectData; + const res = await request(app) + .post('/api/groups') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + // We expect to get the created group + group1 = res.body; + expect(group1).toBeDefined(); + expect(group1.stix).toBeDefined(); + expect(group1.stix.id).toBeDefined(); + expect(group1.stix.created).toBeDefined(); + expect(group1.stix.modified).toBeDefined(); + expect(group1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); + + // object_marking_refs should contain the default marking definition + expect(group1.stix.object_marking_refs).toBeDefined(); + expect(Array.isArray(group1.stix.object_marking_refs)).toBe(true); + expect(group1.stix.object_marking_refs.length).toBe(1); + expect(group1.stix.object_marking_refs[0]).toBe(defaultMarkingDefinition1.stix.id); + }); + + it('GET /api/groups returns the added group', async function () { + const res = await request(app) + .get('/api/groups') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one group in an array + const groups = res.body; + expect(groups).toBeDefined(); + expect(Array.isArray(groups)).toBe(true); + expect(groups.length).toBe(1); + }); + + it('GET /api/groups/:id should not return a group when the id cannot be found', async function () { + await request(app) + .get('/api/groups/not-an-id') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('GET /api/groups/:id returns the added group', async function () { + const res = await request(app) + .get('/api/groups/' + group1.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one group in an array + const groups = res.body; + expect(groups).toBeDefined(); + expect(Array.isArray(groups)).toBe(true); + expect(groups.length).toBe(1); + + const group = groups[0]; + expect(group).toBeDefined(); + expect(group.stix).toBeDefined(); + expect(group.stix.id).toBe(group1.stix.id); + expect(group.stix.type).toBe(group1.stix.type); + expect(group.stix.name).toBe(group1.stix.name); + expect(group.stix.description).toBe(group1.stix.description); + expect(group.stix.spec_version).toBe(group1.stix.spec_version); + expect(group.stix.object_marking_refs).toEqual( + expect.arrayContaining(group1.stix.object_marking_refs), + ); + expect(group.stix.created_by_ref).toBe(group1.stix.created_by_ref); + expect(group.stix.x_mitre_attack_spec_version).toBe(group1.stix.x_mitre_attack_spec_version); + }); + + it('PUT /api/groups updates a group', async function () { + const originalModified = group1.stix.modified; + const timestamp = new Date().toISOString(); + group1.stix.modified = timestamp; + group1.stix.description = 'This is an updated group. Blue.'; + const body = group1; + const res = await request(app) + .put('/api/groups/' + group1.stix.id + '/modified/' + originalModified) + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the updated group + const group = res.body; + expect(group).toBeDefined(); + expect(group.stix.id).toBe(group1.stix.id); + expect(group.stix.modified).toBe(group1.stix.modified); + }); + + it('POST /api/groups does not create a group with the same id and modified date', async function () { + const body = group1; + await request(app) + .post('/api/groups') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(409); + }); + + let group2; + it('POST /api/groups should create a new version of a group with a duplicate stix.id but different stix.modified date', async function () { + // Add another default marking definition + markingDefinitionData.stix.definition.statement = + 'This is the second default marking definition'; + defaultMarkingDefinition2 = await addDefaultMarkingDefinition(markingDefinitionData); + + group2 = _.cloneDeep(group1); + group2._id = undefined; + group2.__t = undefined; + group2.__v = undefined; + const timestamp = new Date().toISOString(); + group2.stix.modified = timestamp; + group2.stix.description = 'This is a new version of a group. Green.'; + + const body = group2; + const res = await request(app) + .post('/api/groups') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created group + const group = res.body; + expect(group).toBeDefined(); + }); + + it('GET /api/groups returns the latest added group', async function () { + const res = await request(app) + .get('/api/groups/' + group2.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one group in an array + const groups = res.body; + expect(groups).toBeDefined(); + expect(Array.isArray(groups)).toBe(true); + expect(groups.length).toBe(1); + const group = groups[0]; + expect(group.stix.id).toBe(group2.stix.id); + expect(group.stix.modified).toBe(group2.stix.modified); + + // object_marking_refs should contain the two default marking definition + expect(group.stix.object_marking_refs).toBeDefined(); + expect(Array.isArray(group.stix.object_marking_refs)).toBe(true); + expect(group.stix.object_marking_refs.length).toBe(2); + expect(group.stix.object_marking_refs.includes(defaultMarkingDefinition1.stix.id)).toBe(true); + expect(group.stix.object_marking_refs.includes(defaultMarkingDefinition2.stix.id)).toBe(true); + }); + + it('GET /api/groups returns all added groups', async function () { + const res = await request(app) + .get('/api/groups/' + group1.stix.id + '?versions=all') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get two groups in an array + const groups = res.body; + expect(groups).toBeDefined(); + expect(Array.isArray(groups)).toBe(true); + expect(groups.length).toBe(2); + }); + + it('GET /api/groups/:id/modified/:modified returns the first added group', async function () { + const res = await request(app) + .get('/api/groups/' + group1.stix.id + '/modified/' + group1.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one group + const group = res.body; + expect(group).toBeDefined(); + expect(group.stix).toBeDefined(); + expect(group.stix.id).toBe(group1.stix.id); + expect(group.stix.modified).toBe(group1.stix.modified); + }); + + it('GET /api/groups/:id/modified/:modified returns the second added group', async function () { + const res = await request(app) + .get('/api/groups/' + group2.stix.id + '/modified/' + group2.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one group + const group = res.body; + expect(group).toBeDefined(); + expect(group.stix).toBeDefined(); + expect(group.stix.id).toBe(group2.stix.id); + expect(group.stix.modified).toBe(group2.stix.modified); + }); + + let group3; + it('POST /api/groups should create a new group with a different stix.id', async function () { + const group = _.cloneDeep(initialObjectData); + group._id = undefined; + group.__t = undefined; + group.__v = undefined; + group.stix.id = undefined; + const timestamp = new Date().toISOString(); + group.stix.created = timestamp; + group.stix.modified = timestamp; + group.stix.name = 'Mr. Brown'; + group.stix.description = 'This is a new group. Red.'; + const body = group; + const res = await request(app) + .post('/api/groups') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created group + group3 = res.body; + expect(group3).toBeDefined(); + }); + + let group4; + it('POST /api/groups should create a new version of a group with a duplicate stix.id but different stix.modified date', async function () { + group4 = _.cloneDeep(group1); + group4._id = undefined; + group4.__t = undefined; + group4.__v = undefined; + const timestamp = new Date().toISOString(); + group4.stix.modified = timestamp; + group4.stix.description = 'This is a new version of a group. Yellow.'; + + const body = group4; + const res = await request(app) + .post('/api/groups') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created group + const group = res.body; + expect(group).toBeDefined(); + }); + + it('GET /api/groups uses the search parameter to return the latest version of the group', async function () { + const res = await request(app) + .get('/api/groups?search=yellow') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one group in an array + const groups = res.body; + expect(groups).toBeDefined(); + expect(Array.isArray(groups)).toBe(true); + expect(groups.length).toBe(1); + + // We expect it to be the latest version of the group + const group = groups[0]; + expect(group).toBeDefined(); + expect(group.stix).toBeDefined(); + expect(group.stix.id).toBe(group4.stix.id); + expect(group.stix.modified).toBe(group4.stix.modified); + }); + + it('GET /api/groups should not get the first version of the group when using the search parameter', async function () { + const res = await request(app) + .get('/api/groups?search=blue') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get zero groups in an array + const groups = res.body; + expect(groups).toBeDefined(); + expect(Array.isArray(groups)).toBe(true); + expect(groups.length).toBe(0); + }); + + it('GET /api/groups uses the search parameter to return the group using the name property', async function () { + const res = await request(app) + .get('/api/groups?search=brown') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one group in an array + const groups = res.body; + expect(groups).toBeDefined(); + expect(Array.isArray(groups)).toBe(true); + expect(groups.length).toBe(1); + + // We expect it to be the third group + const group = groups[0]; + expect(group).toBeDefined(); + expect(group.stix).toBeDefined(); + expect(group.stix.id).toBe(group3.stix.id); + expect(group.stix.modified).toBe(group3.stix.modified); + }); + + it('DELETE /api/groups/:id should not delete a group when the id cannot be found', async function () { + await request(app) + .delete('/api/groups/not-an-id') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('DELETE /api/groups/:id/modified/:modified deletes a group', async function () { + await request(app) + .delete('/api/groups/' + group1.stix.id + '/modified/' + group1.stix.modified) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('DELETE /api/groups/:id should delete all the groups with the same stix id', async function () { + await request(app) + .delete('/api/groups/' + group2.stix.id) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('DELETE /api/groups/:id/modified/:modified should delete the third group', async function () { + await request(app) + .delete('/api/groups/' + group3.stix.id + '/modified/' + group3.stix.modified) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('GET /api/groups returns an empty array of groups', async function () { + const res = await request(app) + .get('/api/groups') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const groups = res.body; + expect(groups).toBeDefined(); + expect(Array.isArray(groups)).toBe(true); + expect(groups.length).toBe(0); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/api/identities/identities.spec.js b/app/tests/api/identities/identities.spec.js index e3a2dcd9..ad9c9f3e 100644 --- a/app/tests/api/identities/identities.spec.js +++ b/app/tests/api/identities/identities.spec.js @@ -14,322 +14,320 @@ logger.level = 'debug'; // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - name: 'identity-1', - identity_class: 'organization', - spec_version: '2.1', - type: 'identity', - description: 'This is an identity.', - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ] - } + }, + stix: { + name: 'identity-1', + identity_class: 'organization', + spec_version: '2.1', + type: 'identity', + description: 'This is an identity.', + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + }, }; describe('Identity API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - it('GET /api/identities returns the placeholder identity', async function () { - const res = await request(app) - .get('/api/identities') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - - // We expect to get an empty array - const identities = res.body; - expect(identities).toBeDefined(); - expect(Array.isArray(identities)).toBe(true); - expect(identities.length).toBe(1); - }); - - it('POST /api/identities does not create an empty identity', async function () { - const body = { }; - await request(app) - .post('/api/identities') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - let identity1; - it('POST /api/identities creates an identity', async function () { - const timestamp = new Date().toISOString(); - initialObjectData.stix.created = timestamp; - initialObjectData.stix.modified = timestamp; - const body = initialObjectData; - const res = await request(app) - .post('/api/identities') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created identity - identity1 = res.body; - expect(identity1).toBeDefined(); - expect(identity1.stix).toBeDefined(); - expect(identity1.stix.id).toBeDefined(); - expect(identity1.stix.created).toBeDefined(); - expect(identity1.stix.modified).toBeDefined(); - expect(identity1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); - }); - - it('GET /api/identities returns the added identity', async function () { - const res = await request(app) - .get('/api/identities') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one identity in an array - const identities = res.body; - expect(identities).toBeDefined(); - expect(Array.isArray(identities)).toBe(true); - expect(identities.length).toBe(2); - }); - - it('GET /api/identities/:id should not return an identity when the id cannot be found', async function () { - await request(app) - .get('/api/identities/not-an-id') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('GET /api/identities/:id returns the added identity', async function () { - const res = await request(app) - .get('/api/identities/' + identity1.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one identity in an array - const identities = res.body; - expect(identities).toBeDefined(); - expect(Array.isArray(identities)).toBe(true); - expect(identities.length).toBe(1); - - const identity = identities[0]; - expect(identity).toBeDefined(); - expect(identity.stix).toBeDefined(); - expect(identity.stix.id).toBe(identity1.stix.id); - expect(identity.stix.type).toBe(identity1.stix.type); - expect(identity.stix.name).toBe(identity1.stix.name); - expect(identity.stix.description).toBe(identity1.stix.description); - expect(identity.stix.spec_version).toBe(identity1.stix.spec_version); - expect(identity.stix.object_marking_refs).toEqual(expect.arrayContaining(identity1.stix.object_marking_refs)); - expect(identity.stix.created_by_ref).toBe(identity1.stix.created_by_ref); - expect(identity.stix.x_mitre_attack_spec_version).toBe(identity1.stix.x_mitre_attack_spec_version); - - - }); - - it('PUT /api/identities updates an identity', async function () { - const originalModified = identity1.stix.modified; - const timestamp = new Date().toISOString(); - identity1.stix.modified = timestamp; - identity1.stix.description = 'This is an updated identity.' - const body = identity1; - const res = await request(app) - .put('/api/identities/' + identity1.stix.id + '/modified/' + originalModified) - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the updated identity - const identity = res.body; - expect(identity).toBeDefined(); - expect(identity.stix.id).toBe(identity1.stix.id); - expect(identity.stix.modified).toBe(identity1.stix.modified); - - }); - - it('POST /api/identities does not create an identity with the same id and modified date', async function () { - const body = identity1; - await request(app) - .post('/api/identities') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(409); - }); - - let identity2; - it('POST /api/identities should create a new version of an identity with a duplicate stix.id but different stix.modified date', async function () { - identity2 = _.cloneDeep(identity1); - identity2._id = undefined; - identity2.__t = undefined; - identity2.__v = undefined; - const timestamp = new Date().toISOString(); - identity2.stix.modified = timestamp; - const body = identity2; - const res = await request(app) - .post('/api/identities') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created identity - const identity = res.body; - expect(identity).toBeDefined(); - }); - - it('GET /api/identities returns the latest added identity', async function () { - const res = await request(app) - .get('/api/identities/' + identity2.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one identity in an array - const identities = res.body; - expect(identities).toBeDefined(); - expect(Array.isArray(identities)).toBe(true); - expect(identities.length).toBe(1); - const identity = identities[0]; - expect(identity.stix.id).toBe(identity2.stix.id); - expect(identity.stix.modified).toBe(identity2.stix.modified); - }); - - it('GET /api/identities returns all added identities', async function () { - const res = await request(app) - .get('/api/identities/' + identity1.stix.id + '?versions=all') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get two identities in an array - const identities = res.body; - expect(identities).toBeDefined(); - expect(Array.isArray(identities)).toBe(true); - expect(identities.length).toBe(2); - }); - - it('GET /api/identities/:id/modified/:modified returns the first added identity', async function () { - const res = await request(app) - .get('/api/identities/' + identity1.stix.id + '/modified/' + identity1.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one identity in an array - const identity = res.body; - expect(identity).toBeDefined(); - expect(identity.stix).toBeDefined(); - expect(identity.stix.id).toBe(identity1.stix.id); - expect(identity.stix.modified).toBe(identity1.stix.modified); - }); - - it('GET /api/identities/:id/modified/:modified returns the second added identity', async function () { - const res = await request(app) - .get('/api/identities/' + identity2.stix.id + '/modified/' + identity2.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one identity in an array - const identity = res.body; - expect(identity).toBeDefined(); - expect(identity.stix).toBeDefined(); - expect(identity.stix.id).toBe(identity2.stix.id); - expect(identity.stix.modified).toBe(identity2.stix.modified); - - }); - - let identity3; - it('POST /api/identities should create a new version of an identity with a duplicate stix.id but different stix.modified date', async function () { - identity3 = _.cloneDeep(identity1); - identity3._id = undefined; - identity3.__t = undefined; - identity3.__v = undefined; - const timestamp = new Date().toISOString(); - identity3.stix.modified = timestamp; - const body = identity3; - const res = await request(app) - .post('/api/identities') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created identity - const identity = res.body; - expect(identity).toBeDefined(); - }); - - it('DELETE /api/identities/:id should not delete a identity when the id cannot be found', async function () { - await request(app) - .delete('/api/identities/not-an-id') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('DELETE /api/identities/:id/modified/:modified deletes an identity', async function () { - await request(app) - .delete('/api/identities/' + identity1.stix.id + '/modified/' + identity1.stix.modified) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - }); - - it('DELETE /api/identities/:id should delete all the identities with the same stix id', async function () { - await request(app) - .delete('/api/identities/' + identity2.stix.id) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - }); - - it('GET /api/identities returns only the placeholder identity', async function () { - const res = await request(app) - .get('/api/identities') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const identities = res.body; - expect(identities).toBeDefined(); - expect(Array.isArray(identities)).toBe(true); - expect(identities.length).toBe(1); - - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + it('GET /api/identities returns the placeholder identity', async function () { + const res = await request(app) + .get('/api/identities') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const identities = res.body; + expect(identities).toBeDefined(); + expect(Array.isArray(identities)).toBe(true); + expect(identities.length).toBe(1); + }); + + it('POST /api/identities does not create an empty identity', async function () { + const body = {}; + await request(app) + .post('/api/identities') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + let identity1; + it('POST /api/identities creates an identity', async function () { + const timestamp = new Date().toISOString(); + initialObjectData.stix.created = timestamp; + initialObjectData.stix.modified = timestamp; + const body = initialObjectData; + const res = await request(app) + .post('/api/identities') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created identity + identity1 = res.body; + expect(identity1).toBeDefined(); + expect(identity1.stix).toBeDefined(); + expect(identity1.stix.id).toBeDefined(); + expect(identity1.stix.created).toBeDefined(); + expect(identity1.stix.modified).toBeDefined(); + expect(identity1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); + }); + + it('GET /api/identities returns the added identity', async function () { + const res = await request(app) + .get('/api/identities') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one identity in an array + const identities = res.body; + expect(identities).toBeDefined(); + expect(Array.isArray(identities)).toBe(true); + expect(identities.length).toBe(2); + }); + + it('GET /api/identities/:id should not return an identity when the id cannot be found', async function () { + await request(app) + .get('/api/identities/not-an-id') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('GET /api/identities/:id returns the added identity', async function () { + const res = await request(app) + .get('/api/identities/' + identity1.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one identity in an array + const identities = res.body; + expect(identities).toBeDefined(); + expect(Array.isArray(identities)).toBe(true); + expect(identities.length).toBe(1); + + const identity = identities[0]; + expect(identity).toBeDefined(); + expect(identity.stix).toBeDefined(); + expect(identity.stix.id).toBe(identity1.stix.id); + expect(identity.stix.type).toBe(identity1.stix.type); + expect(identity.stix.name).toBe(identity1.stix.name); + expect(identity.stix.description).toBe(identity1.stix.description); + expect(identity.stix.spec_version).toBe(identity1.stix.spec_version); + expect(identity.stix.object_marking_refs).toEqual( + expect.arrayContaining(identity1.stix.object_marking_refs), + ); + expect(identity.stix.created_by_ref).toBe(identity1.stix.created_by_ref); + expect(identity.stix.x_mitre_attack_spec_version).toBe( + identity1.stix.x_mitre_attack_spec_version, + ); + }); + + it('PUT /api/identities updates an identity', async function () { + const originalModified = identity1.stix.modified; + const timestamp = new Date().toISOString(); + identity1.stix.modified = timestamp; + identity1.stix.description = 'This is an updated identity.'; + const body = identity1; + const res = await request(app) + .put('/api/identities/' + identity1.stix.id + '/modified/' + originalModified) + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the updated identity + const identity = res.body; + expect(identity).toBeDefined(); + expect(identity.stix.id).toBe(identity1.stix.id); + expect(identity.stix.modified).toBe(identity1.stix.modified); + }); + + it('POST /api/identities does not create an identity with the same id and modified date', async function () { + const body = identity1; + await request(app) + .post('/api/identities') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(409); + }); + + let identity2; + it('POST /api/identities should create a new version of an identity with a duplicate stix.id but different stix.modified date', async function () { + identity2 = _.cloneDeep(identity1); + identity2._id = undefined; + identity2.__t = undefined; + identity2.__v = undefined; + const timestamp = new Date().toISOString(); + identity2.stix.modified = timestamp; + const body = identity2; + const res = await request(app) + .post('/api/identities') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created identity + const identity = res.body; + expect(identity).toBeDefined(); + }); + + it('GET /api/identities returns the latest added identity', async function () { + const res = await request(app) + .get('/api/identities/' + identity2.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one identity in an array + const identities = res.body; + expect(identities).toBeDefined(); + expect(Array.isArray(identities)).toBe(true); + expect(identities.length).toBe(1); + const identity = identities[0]; + expect(identity.stix.id).toBe(identity2.stix.id); + expect(identity.stix.modified).toBe(identity2.stix.modified); + }); + + it('GET /api/identities returns all added identities', async function () { + const res = await request(app) + .get('/api/identities/' + identity1.stix.id + '?versions=all') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get two identities in an array + const identities = res.body; + expect(identities).toBeDefined(); + expect(Array.isArray(identities)).toBe(true); + expect(identities.length).toBe(2); + }); + + it('GET /api/identities/:id/modified/:modified returns the first added identity', async function () { + const res = await request(app) + .get('/api/identities/' + identity1.stix.id + '/modified/' + identity1.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one identity in an array + const identity = res.body; + expect(identity).toBeDefined(); + expect(identity.stix).toBeDefined(); + expect(identity.stix.id).toBe(identity1.stix.id); + expect(identity.stix.modified).toBe(identity1.stix.modified); + }); + + it('GET /api/identities/:id/modified/:modified returns the second added identity', async function () { + const res = await request(app) + .get('/api/identities/' + identity2.stix.id + '/modified/' + identity2.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one identity in an array + const identity = res.body; + expect(identity).toBeDefined(); + expect(identity.stix).toBeDefined(); + expect(identity.stix.id).toBe(identity2.stix.id); + expect(identity.stix.modified).toBe(identity2.stix.modified); + }); + + let identity3; + it('POST /api/identities should create a new version of an identity with a duplicate stix.id but different stix.modified date', async function () { + identity3 = _.cloneDeep(identity1); + identity3._id = undefined; + identity3.__t = undefined; + identity3.__v = undefined; + const timestamp = new Date().toISOString(); + identity3.stix.modified = timestamp; + const body = identity3; + const res = await request(app) + .post('/api/identities') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created identity + const identity = res.body; + expect(identity).toBeDefined(); + }); + + it('DELETE /api/identities/:id should not delete a identity when the id cannot be found', async function () { + await request(app) + .delete('/api/identities/not-an-id') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('DELETE /api/identities/:id/modified/:modified deletes an identity', async function () { + await request(app) + .delete('/api/identities/' + identity1.stix.id + '/modified/' + identity1.stix.modified) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('DELETE /api/identities/:id should delete all the identities with the same stix id', async function () { + await request(app) + .delete('/api/identities/' + identity2.stix.id) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('GET /api/identities returns only the placeholder identity', async function () { + const res = await request(app) + .get('/api/identities') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const identities = res.body; + expect(identities).toBeDefined(); + expect(Array.isArray(identities)).toBe(true); + expect(identities.length).toBe(1); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/api/marking-definitions/marking-definitions.spec.js b/app/tests/api/marking-definitions/marking-definitions.spec.js index 872bce08..ca343763 100644 --- a/app/tests/api/marking-definitions/marking-definitions.spec.js +++ b/app/tests/api/marking-definitions/marking-definitions.spec.js @@ -13,194 +13,195 @@ logger.level = 'debug'; // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - spec_version: '2.1', - type: 'marking-definition', - definition_type: 'statement', - definition: { statement: 'This is a marking definition.' }, - created_by_ref: "identity--6444f546-6900-4456-b3b1-015c88d70dab" - } + }, + stix: { + spec_version: '2.1', + type: 'marking-definition', + definition_type: 'statement', + definition: { statement: 'This is a marking definition.' }, + created_by_ref: 'identity--6444f546-6900-4456-b3b1-015c88d70dab', + }, }; describe('Marking Definitions API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - it('GET /api/marking-definitions returns the pre-defined marking definitions', async function () { - const res = await request(app) - .get('/api/marking-definitions') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const markingDefinitions = res.body; - expect(markingDefinitions).toBeDefined(); - expect(Array.isArray(markingDefinitions)).toBe(true); - expect(markingDefinitions.length).toBe(4); - }); - - it('POST /api/marking-definitions does not create an empty marking definition', async function () { - const body = { }; - await request(app) - .post('/api/marking-definitions') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - let markingDefinition1; - it('POST /api/marking-definitions creates a marking definition', async function () { - const timestamp = new Date().toISOString(); - initialObjectData.stix.created = timestamp; - const body = initialObjectData; - const res = await request(app) - .post('/api/marking-definitions') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created marking definition - markingDefinition1 = res.body; - expect(markingDefinition1).toBeDefined(); - expect(markingDefinition1.stix).toBeDefined(); - expect(markingDefinition1.stix.id).toBeDefined(); - expect(markingDefinition1.stix.created).toBeDefined(); - // stix.modified does not exist for marking definitions - expect(markingDefinition1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); - - }); - - it('GET /api/marking-definitions returns the added marking definition', async function () { - const res = await request(app) - .get('/api/marking-definitions') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - - // We expect to get one additional marking definition in an array - const markingDefinitions = res.body; - expect(markingDefinitions).toBeDefined(); - expect(Array.isArray(markingDefinitions)).toBe(true); - - const addedMarkingDefinitions = markingDefinitions.filter(x => x.workspace.workflow.state === 'work-in-progress'); - expect(addedMarkingDefinitions.length).toBe(1); - - }); - - it('GET /api/marking-definitions/:id should not return a marking definition when the id cannot be found', async function () { - await request(app) - .get('/api/marking-definitions/not-an-id') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('GET /api/marking-definitions/:id returns the added marking definition', async function () { - const res = await request(app) - .get('/api/marking-definitions/' + markingDefinition1.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - - // We expect to get one marking definition in an array - const markingDefinitions = res.body; - expect(markingDefinitions).toBeDefined(); - expect(Array.isArray(markingDefinitions)).toBe(true); - expect(markingDefinitions.length).toBe(1); - - const markingDefinition = markingDefinitions[0]; - expect(markingDefinition).toBeDefined(); - expect(markingDefinition.stix).toBeDefined(); - expect(markingDefinition.stix.id).toBe(markingDefinition1.stix.id); - expect(markingDefinition.stix.type).toBe(markingDefinition1.stix.type); - expect(markingDefinition.stix.name).toBe(markingDefinition1.stix.name); - expect(markingDefinition.stix.description).toBe(markingDefinition1.stix.description); - expect(markingDefinition.stix.spec_version).toBe(markingDefinition1.stix.spec_version); - expect(markingDefinition.stix.object_marking_refs).toEqual(expect.arrayContaining(markingDefinition1.stix.object_marking_refs)); - expect(markingDefinition.stix.created_by_ref).toBe(markingDefinition1.stix.created_by_ref); - expect(markingDefinition.stix.x_mitre_attack_spec_version).toBe(markingDefinition1.stix.x_mitre_attack_spec_version); - - }); - - it('PUT /api/marking-definitions updates a marking definition', async function () { - markingDefinition1.stix.description = 'This is an updated marking definition.' - const body = markingDefinition1; - const res = await request(app) - .put('/api/marking-definitions/' + markingDefinition1.stix.id) - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the updated marking definition - const markingDefinition = res.body; - expect(markingDefinition).toBeDefined(); - expect(markingDefinition.stix.id).toBe(markingDefinition1.stix.id); - }); - - it('POST /api/marking-definitions does not create a marking definition with the same id', async function () { - const body = markingDefinition1; - await request(app) - .post('/api/marking-definitions') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - it('DELETE /api/marking-definitions deletes a marking definition', async function () { - await request(app) - .delete('/api/marking-definitions/' + markingDefinition1.stix.id) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - }); - - it('GET /api/marking-definitions returns the pre-defined marking definitions', async function () { - const res = await request(app) - .get('/api/marking-definitions') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const markingDefinitions = res.body; - expect(markingDefinitions).toBeDefined(); - expect(Array.isArray(markingDefinitions)).toBe(true); - expect(markingDefinitions.length).toBe(4); - - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + it('GET /api/marking-definitions returns the pre-defined marking definitions', async function () { + const res = await request(app) + .get('/api/marking-definitions') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const markingDefinitions = res.body; + expect(markingDefinitions).toBeDefined(); + expect(Array.isArray(markingDefinitions)).toBe(true); + expect(markingDefinitions.length).toBe(4); + }); + + it('POST /api/marking-definitions does not create an empty marking definition', async function () { + const body = {}; + await request(app) + .post('/api/marking-definitions') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + let markingDefinition1; + it('POST /api/marking-definitions creates a marking definition', async function () { + const timestamp = new Date().toISOString(); + initialObjectData.stix.created = timestamp; + const body = initialObjectData; + const res = await request(app) + .post('/api/marking-definitions') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created marking definition + markingDefinition1 = res.body; + expect(markingDefinition1).toBeDefined(); + expect(markingDefinition1.stix).toBeDefined(); + expect(markingDefinition1.stix.id).toBeDefined(); + expect(markingDefinition1.stix.created).toBeDefined(); + // stix.modified does not exist for marking definitions + expect(markingDefinition1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); + }); + + it('GET /api/marking-definitions returns the added marking definition', async function () { + const res = await request(app) + .get('/api/marking-definitions') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one additional marking definition in an array + const markingDefinitions = res.body; + expect(markingDefinitions).toBeDefined(); + expect(Array.isArray(markingDefinitions)).toBe(true); + + const addedMarkingDefinitions = markingDefinitions.filter( + (x) => x.workspace.workflow.state === 'work-in-progress', + ); + expect(addedMarkingDefinitions.length).toBe(1); + }); + + it('GET /api/marking-definitions/:id should not return a marking definition when the id cannot be found', async function () { + await request(app) + .get('/api/marking-definitions/not-an-id') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('GET /api/marking-definitions/:id returns the added marking definition', async function () { + const res = await request(app) + .get('/api/marking-definitions/' + markingDefinition1.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one marking definition in an array + const markingDefinitions = res.body; + expect(markingDefinitions).toBeDefined(); + expect(Array.isArray(markingDefinitions)).toBe(true); + expect(markingDefinitions.length).toBe(1); + + const markingDefinition = markingDefinitions[0]; + expect(markingDefinition).toBeDefined(); + expect(markingDefinition.stix).toBeDefined(); + expect(markingDefinition.stix.id).toBe(markingDefinition1.stix.id); + expect(markingDefinition.stix.type).toBe(markingDefinition1.stix.type); + expect(markingDefinition.stix.name).toBe(markingDefinition1.stix.name); + expect(markingDefinition.stix.description).toBe(markingDefinition1.stix.description); + expect(markingDefinition.stix.spec_version).toBe(markingDefinition1.stix.spec_version); + expect(markingDefinition.stix.object_marking_refs).toEqual( + expect.arrayContaining(markingDefinition1.stix.object_marking_refs), + ); + expect(markingDefinition.stix.created_by_ref).toBe(markingDefinition1.stix.created_by_ref); + expect(markingDefinition.stix.x_mitre_attack_spec_version).toBe( + markingDefinition1.stix.x_mitre_attack_spec_version, + ); + }); + + it('PUT /api/marking-definitions updates a marking definition', async function () { + markingDefinition1.stix.description = 'This is an updated marking definition.'; + const body = markingDefinition1; + const res = await request(app) + .put('/api/marking-definitions/' + markingDefinition1.stix.id) + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the updated marking definition + const markingDefinition = res.body; + expect(markingDefinition).toBeDefined(); + expect(markingDefinition.stix.id).toBe(markingDefinition1.stix.id); + }); + + it('POST /api/marking-definitions does not create a marking definition with the same id', async function () { + const body = markingDefinition1; + await request(app) + .post('/api/marking-definitions') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + it('DELETE /api/marking-definitions deletes a marking definition', async function () { + await request(app) + .delete('/api/marking-definitions/' + markingDefinition1.stix.id) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('GET /api/marking-definitions returns the pre-defined marking definitions', async function () { + const res = await request(app) + .get('/api/marking-definitions') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const markingDefinitions = res.body; + expect(markingDefinitions).toBeDefined(); + expect(Array.isArray(markingDefinitions)).toBe(true); + expect(markingDefinitions.length).toBe(4); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/api/matrices/matrices.spec.js b/app/tests/api/matrices/matrices.spec.js index 0acb9d67..1b0f1baa 100644 --- a/app/tests/api/matrices/matrices.spec.js +++ b/app/tests/api/matrices/matrices.spec.js @@ -14,333 +14,330 @@ logger.level = 'debug'; // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - name: 'matrix-1', - spec_version: '2.1', - type: 'x-mitre-matrix', - description: 'This is a matrix.', - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--6444f546-6900-4456-b3b1-015c88d70dab", - tactic_refs: [ - 'x-mitre-tactic--daa4cbb1-b4f4-4723-a824-7f1efd6e0592', - 'x-mitre-tactic--d679bca2-e57d-4935-8650-8031c87a4400', - ], - x_mitre_domains: [ 'mitre-attack' ], - x_mitre_version: '1.0' - } + }, + stix: { + name: 'matrix-1', + spec_version: '2.1', + type: 'x-mitre-matrix', + description: 'This is a matrix.', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--6444f546-6900-4456-b3b1-015c88d70dab', + tactic_refs: [ + 'x-mitre-tactic--daa4cbb1-b4f4-4723-a824-7f1efd6e0592', + 'x-mitre-tactic--d679bca2-e57d-4935-8650-8031c87a4400', + ], + x_mitre_domains: ['mitre-attack'], + x_mitre_version: '1.0', + }, }; describe('Matrices API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - it('GET /api/matrices returns an empty array of matrices', async function () { - const res = await request(app) - .get('/api/matrices') - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const matrices = res.body; - expect(matrices).toBeDefined(); - expect(Array.isArray(matrices)).toBe(true); - expect(matrices.length).toBe(0); - }); - - it('POST /api/matrices does not create an empty matrix', async function () { - const body = {}; - await request(app) - .post('/api/matrices') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(400); - }); - - let matrix1; - it('POST /api/matrices creates a matrix', async function () { - const timestamp = new Date().toISOString(); - initialObjectData.stix.created = timestamp; - initialObjectData.stix.modified = timestamp; - const body = initialObjectData; - const res = await request(app) - .post('/api/matrices') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created matrix - matrix1 = res.body; - expect(matrix1).toBeDefined(); - expect(matrix1.stix).toBeDefined(); - expect(matrix1.stix.id).toBeDefined(); - expect(matrix1.stix.created).toBeDefined(); - expect(matrix1.stix.modified).toBeDefined(); - expect(matrix1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); - }); - - it('GET /api/matrices returns the added matrix', async function () { - const res = await request(app) - .get('/api/matrices') - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one matrix in an array - const matrices = res.body; - expect(matrices).toBeDefined(); - expect(Array.isArray(matrices)).toBe(true); - expect(matrices.length).toBe(1); - - }); - - it('GET /api/matrices/:id should not return a matrix when the id cannot be found', async function () { - await request(app) - .get('/api/matrices/not-an-id') - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(404); - }); - - it('GET /api/matrices/:id returns the added matrix', async function () { - const res = await request(app) - .get('/api/matrices/' + matrix1.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one matrix in an array - const matrices = res.body; - expect(matrices).toBeDefined(); - expect(Array.isArray(matrices)).toBe(true); - expect(matrices.length).toBe(1); - - const matrix = matrices[0]; - expect(matrix).toBeDefined(); - expect(matrix.stix).toBeDefined(); - expect(matrix.stix.id).toBe(matrix1.stix.id); - expect(matrix.stix.created).toBeDefined(); - expect(matrix.stix.modified).toBeDefined(); - expect(matrix.stix.type).toBe(matrix1.stix.type); - expect(matrix.stix.name).toBe(matrix1.stix.name); - expect(matrix.stix.description).toBe(matrix1.stix.description); - expect(matrix.stix.spec_version).toBe(matrix1.stix.spec_version); - expect(matrix.stix.object_marking_refs).toEqual(expect.arrayContaining(matrix1.stix.object_marking_refs)); - expect(matrix.stix.created_by_ref).toBe(matrix1.stix.created_by_ref); - expect(matrix.stix.x_mitre_attack_spec_version).toBe(matrix1.stix.x_mitre_attack_spec_version); - }); - - // TODO Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. - it('PUT /api/matrices updates a matrix', async function () { - const originalModified = matrix1.stix.modified; - const timestamp = new Date().toISOString(); - matrix1.stix.modified = timestamp; - matrix1.stix.description = 'This is an updated matrix.'; - const body = matrix1; - - const res = await request(app) - .put('/api/matrices/' + matrix1.stix.id + '/modified/' + originalModified) - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the updated matrix - const matrix = res.body; - expect(matrix).toBeDefined(); - expect(matrix.stix.id).toBe(matrix1.stix.id); - expect(matrix.stix.modified).toBe(matrix1.stix.modified); - }); - - // TODO Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. - it('POST /api/matrices does not create a matrix with the same id and modified date', async function () { - const body = matrix1; - await request(app) - .post('/api/matrices') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(409); - }).timeout(10000); - - let matrix2; - it('POST /api/matrices should create a new version of a matrix with a duplicate stix.id but different stix.modified date', async function () { - matrix2 = _.cloneDeep(matrix1); - matrix2._id = undefined; - matrix2.__t = undefined; - matrix2.__v = undefined; - const timestamp = new Date().toISOString(); - matrix2.stix.modified = timestamp; - const body = matrix2; - - const res = await request(app) - .post('/api/matrices') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created matrix - const matrix = res.body; - expect(matrix).toBeDefined(); - }); - - let matrix3; - it('POST /api/matrices should create a new version of a matrix with a duplicate stix.id but different stix.modified date', async function () { - matrix3 = _.cloneDeep(matrix1); - matrix3._id = undefined; - matrix3.__t = undefined; - matrix3.__v = undefined; - const timestamp = new Date().toISOString(); - matrix3.stix.modified = timestamp; - const body = matrix3; - - const res = await request(app) - .post('/api/matrices') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created matrix - const matrix = res.body; - expect(matrix).toBeDefined(); - }); - - - it('GET /api/matrices returns the latest added matrix', async function () { - const res = await request(app) - .get('/api/matrices/' + matrix3.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(200) - .expect('Content-Type', /json/); - // We expect to get one matrix in an array - const matrices = res.body; - expect(matrices).toBeDefined(); - expect(Array.isArray(matrices)).toBe(true); - expect(matrices.length).toBe(1); - const matrix = matrices[0]; - expect(matrix.stix.id).toBe(matrix3.stix.id); - expect(matrix.stix.modified).toBe(matrix3.stix.modified); - }); - - it('GET /api/matrices returns all added matrices', async function () { - const res = await request(app) - .get('/api/matrices/' + matrix1.stix.id + '?versions=all') - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get two matrices in an array - const matrices = res.body; - expect(matrices).toBeDefined(); - expect(Array.isArray(matrices)).toBe(true); - expect(matrices.length).toBe(3); - }); - - it('GET /api/matrices/:id/modified/:modified returns the first added matrix', async function () { - const res = await request(app) - .get('/api/matrices/' + matrix1.stix.id + '/modified/' + matrix1.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(200) - .expect('Content-Type', /json/); - // We expect to get one matrix in an array - const matrix = res.body; - expect(matrix).toBeDefined(); - expect(matrix.stix).toBeDefined(); - expect(matrix.stix.id).toBe(matrix1.stix.id); - expect(matrix.stix.modified).toBe(matrix1.stix.modified); - }); - - it('GET /api/matrices/:id/modified/:modified returns the second added matrix', async function () { - const res = await request(app) - .get('/api/matrices/' + matrix2.stix.id + '/modified/' + matrix2.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one matrix in an array - const matrix = res.body; - expect(matrix).toBeDefined(); - expect(matrix.stix).toBeDefined(); - expect(matrix.stix.id).toBe(matrix2.stix.id); - expect(matrix.stix.modified).toBe(matrix2.stix.modified); - }); - - it('DELETE /api/matrices/:id should not delete a matrix when the id cannot be found', async function () { - await request(app) - .delete('/api/matrices/not-an-id') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(404); - }); - - it('DELETE /api/matrices/:id/modified/:modified deletes a matrix', async function () { - await request(app) - .delete('/api/matrices/' + matrix1.stix.id + '/modified/' + matrix1.stix.modified) - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(204); - }); - - it('DELETE /api/matrices/:id should delete all the matrices with the same stix id', async function () { - await request(app) - .delete('/api/matrices/' + matrix2.stix.id) - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(204); - }); - - it('GET /api/matrices returns an empty array of matrices', async function () { - const res = await request(app) - .get('/api/matrices') - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const matrices = res.body; - expect(matrices).toBeDefined(); - expect(Array.isArray(matrices)).toBe(true); - expect(matrices.length).toBe(0); - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + it('GET /api/matrices returns an empty array of matrices', async function () { + const res = await request(app) + .get('/api/matrices') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const matrices = res.body; + expect(matrices).toBeDefined(); + expect(Array.isArray(matrices)).toBe(true); + expect(matrices.length).toBe(0); + }); + + it('POST /api/matrices does not create an empty matrix', async function () { + const body = {}; + await request(app) + .post('/api/matrices') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + let matrix1; + it('POST /api/matrices creates a matrix', async function () { + const timestamp = new Date().toISOString(); + initialObjectData.stix.created = timestamp; + initialObjectData.stix.modified = timestamp; + const body = initialObjectData; + const res = await request(app) + .post('/api/matrices') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created matrix + matrix1 = res.body; + expect(matrix1).toBeDefined(); + expect(matrix1.stix).toBeDefined(); + expect(matrix1.stix.id).toBeDefined(); + expect(matrix1.stix.created).toBeDefined(); + expect(matrix1.stix.modified).toBeDefined(); + expect(matrix1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); + }); + + it('GET /api/matrices returns the added matrix', async function () { + const res = await request(app) + .get('/api/matrices') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one matrix in an array + const matrices = res.body; + expect(matrices).toBeDefined(); + expect(Array.isArray(matrices)).toBe(true); + expect(matrices.length).toBe(1); + }); + + it('GET /api/matrices/:id should not return a matrix when the id cannot be found', async function () { + await request(app) + .get('/api/matrices/not-an-id') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('GET /api/matrices/:id returns the added matrix', async function () { + const res = await request(app) + .get('/api/matrices/' + matrix1.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one matrix in an array + const matrices = res.body; + expect(matrices).toBeDefined(); + expect(Array.isArray(matrices)).toBe(true); + expect(matrices.length).toBe(1); + + const matrix = matrices[0]; + expect(matrix).toBeDefined(); + expect(matrix.stix).toBeDefined(); + expect(matrix.stix.id).toBe(matrix1.stix.id); + expect(matrix.stix.created).toBeDefined(); + expect(matrix.stix.modified).toBeDefined(); + expect(matrix.stix.type).toBe(matrix1.stix.type); + expect(matrix.stix.name).toBe(matrix1.stix.name); + expect(matrix.stix.description).toBe(matrix1.stix.description); + expect(matrix.stix.spec_version).toBe(matrix1.stix.spec_version); + expect(matrix.stix.object_marking_refs).toEqual( + expect.arrayContaining(matrix1.stix.object_marking_refs), + ); + expect(matrix.stix.created_by_ref).toBe(matrix1.stix.created_by_ref); + expect(matrix.stix.x_mitre_attack_spec_version).toBe(matrix1.stix.x_mitre_attack_spec_version); + }); + + // TODO Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. + it('PUT /api/matrices updates a matrix', async function () { + const originalModified = matrix1.stix.modified; + const timestamp = new Date().toISOString(); + matrix1.stix.modified = timestamp; + matrix1.stix.description = 'This is an updated matrix.'; + const body = matrix1; + + const res = await request(app) + .put('/api/matrices/' + matrix1.stix.id + '/modified/' + originalModified) + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the updated matrix + const matrix = res.body; + expect(matrix).toBeDefined(); + expect(matrix.stix.id).toBe(matrix1.stix.id); + expect(matrix.stix.modified).toBe(matrix1.stix.modified); + }); + + // TODO Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. + it('POST /api/matrices does not create a matrix with the same id and modified date', async function () { + const body = matrix1; + await request(app) + .post('/api/matrices') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(409); + }).timeout(10000); + + let matrix2; + it('POST /api/matrices should create a new version of a matrix with a duplicate stix.id but different stix.modified date', async function () { + matrix2 = _.cloneDeep(matrix1); + matrix2._id = undefined; + matrix2.__t = undefined; + matrix2.__v = undefined; + const timestamp = new Date().toISOString(); + matrix2.stix.modified = timestamp; + const body = matrix2; + + const res = await request(app) + .post('/api/matrices') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created matrix + const matrix = res.body; + expect(matrix).toBeDefined(); + }); + + let matrix3; + it('POST /api/matrices should create a new version of a matrix with a duplicate stix.id but different stix.modified date', async function () { + matrix3 = _.cloneDeep(matrix1); + matrix3._id = undefined; + matrix3.__t = undefined; + matrix3.__v = undefined; + const timestamp = new Date().toISOString(); + matrix3.stix.modified = timestamp; + const body = matrix3; + + const res = await request(app) + .post('/api/matrices') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created matrix + const matrix = res.body; + expect(matrix).toBeDefined(); + }); + + it('GET /api/matrices returns the latest added matrix', async function () { + const res = await request(app) + .get('/api/matrices/' + matrix3.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + // We expect to get one matrix in an array + const matrices = res.body; + expect(matrices).toBeDefined(); + expect(Array.isArray(matrices)).toBe(true); + expect(matrices.length).toBe(1); + const matrix = matrices[0]; + expect(matrix.stix.id).toBe(matrix3.stix.id); + expect(matrix.stix.modified).toBe(matrix3.stix.modified); + }); + + it('GET /api/matrices returns all added matrices', async function () { + const res = await request(app) + .get('/api/matrices/' + matrix1.stix.id + '?versions=all') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get two matrices in an array + const matrices = res.body; + expect(matrices).toBeDefined(); + expect(Array.isArray(matrices)).toBe(true); + expect(matrices.length).toBe(3); + }); + + it('GET /api/matrices/:id/modified/:modified returns the first added matrix', async function () { + const res = await request(app) + .get('/api/matrices/' + matrix1.stix.id + '/modified/' + matrix1.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + // We expect to get one matrix in an array + const matrix = res.body; + expect(matrix).toBeDefined(); + expect(matrix.stix).toBeDefined(); + expect(matrix.stix.id).toBe(matrix1.stix.id); + expect(matrix.stix.modified).toBe(matrix1.stix.modified); + }); + + it('GET /api/matrices/:id/modified/:modified returns the second added matrix', async function () { + const res = await request(app) + .get('/api/matrices/' + matrix2.stix.id + '/modified/' + matrix2.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one matrix in an array + const matrix = res.body; + expect(matrix).toBeDefined(); + expect(matrix.stix).toBeDefined(); + expect(matrix.stix.id).toBe(matrix2.stix.id); + expect(matrix.stix.modified).toBe(matrix2.stix.modified); + }); + + it('DELETE /api/matrices/:id should not delete a matrix when the id cannot be found', async function () { + await request(app) + .delete('/api/matrices/not-an-id') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('DELETE /api/matrices/:id/modified/:modified deletes a matrix', async function () { + await request(app) + .delete('/api/matrices/' + matrix1.stix.id + '/modified/' + matrix1.stix.modified) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('DELETE /api/matrices/:id should delete all the matrices with the same stix id', async function () { + await request(app) + .delete('/api/matrices/' + matrix2.stix.id) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('GET /api/matrices returns an empty array of matrices', async function () { + const res = await request(app) + .get('/api/matrices') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const matrices = res.body; + expect(matrices).toBeDefined(); + expect(Array.isArray(matrices)).toBe(true); + expect(matrices.length).toBe(0); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/api/mitigations/mitigations-pagination.spec.js b/app/tests/api/mitigations/mitigations-pagination.spec.js index e981536b..fa3fa401 100644 --- a/app/tests/api/mitigations/mitigations-pagination.spec.js +++ b/app/tests/api/mitigations/mitigations-pagination.spec.js @@ -4,28 +4,26 @@ const PaginationTests = require('../../shared/pagination'); // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - spec_version: '2.1', - type: 'course-of-action', - description: 'This is a mitigation.', - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - x_mitre_version: "1.1" - } + }, + stix: { + spec_version: '2.1', + type: 'course-of-action', + description: 'This is a mitigation.', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + x_mitre_version: '1.1', + }, }; const options = { - prefix: 'course-of-action', - baseUrl: '/api/mitigations', - label: 'Mitigations' -} + prefix: 'course-of-action', + baseUrl: '/api/mitigations', + label: 'Mitigations', +}; const paginationTests = new PaginationTests(mitigationsService, initialObjectData, options); paginationTests.executeTests(); diff --git a/app/tests/api/mitigations/mitigations.spec.js b/app/tests/api/mitigations/mitigations.spec.js index 6e166b56..ca5894c5 100644 --- a/app/tests/api/mitigations/mitigations.spec.js +++ b/app/tests/api/mitigations/mitigations.spec.js @@ -14,347 +14,332 @@ logger.level = 'debug'; // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - name: 'course-of-action-1', - spec_version: '2.1', - type: 'course-of-action', - description: 'This is a mitigation.', - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - labels: [ 'label1', 'label2' ], - x_mitre_version: "1.1" - } + }, + stix: { + name: 'course-of-action-1', + spec_version: '2.1', + type: 'course-of-action', + description: 'This is a mitigation.', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + labels: ['label1', 'label2'], + x_mitre_version: '1.1', + }, }; describe('Mitigations API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - it('GET /api/mitigations returns an empty array of mitigations', async function () { - const res = await request(app) - .get('/api/mitigations') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const mitigations = res.body; - expect(mitigations).toBeDefined(); - expect(Array.isArray(mitigations)).toBe(true); - expect(mitigations.length).toBe(0); - - }); - - it('POST /api/mitigations does not create an empty mitigation', async function () { - const body = { }; - await request(app) - .post('/api/mitigations') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - let mitigation1; - it('POST /api/mitigations creates a mitigation', async function () { - const timestamp = new Date().toISOString(); - initialObjectData.stix.created = timestamp; - initialObjectData.stix.modified = timestamp; - const body = initialObjectData; - const res = await request(app) - .post('/api/mitigations') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created mitigation - mitigation1 = res.body; - expect(mitigation1).toBeDefined(); - expect(mitigation1.stix).toBeDefined(); - expect(mitigation1.stix.id).toBeDefined(); - expect(mitigation1.stix.created).toBeDefined(); - expect(mitigation1.stix.modified).toBeDefined(); - expect(mitigation1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); - - expect(mitigation1.stix.labels).toBeDefined(); - expect(Array.isArray(mitigation1.stix.labels)).toBe(true); - expect(mitigation1.stix.labels.length).toBe(2); - - }); - - it('GET /api/mitigations returns the added mitigation', async function () { - const res = await request(app) - .get('/api/mitigations') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one mitigation in an array - const mitigations = res.body; - expect(mitigations).toBeDefined(); - expect(Array.isArray(mitigations)).toBe(true); - expect(mitigations.length).toBe(1); - }); - - it('GET /api/mitigations/:id should not return a mitigation when the id cannot be found', async function () { - await request(app) - .get('/api/mitigations/not-an-id') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('GET /api/mitigations/:id returns the added mitigation', async function () { - const res = await request(app) - .get('/api/mitigations/' + mitigation1.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - - // We expect to get one mitigation in an array - const mitigations = res.body; - expect(mitigations).toBeDefined(); - expect(Array.isArray(mitigations)).toBe(true); - expect(mitigations.length).toBe(1); - - const mitigation = mitigations[0]; - expect(mitigation).toBeDefined(); - expect(mitigation.stix).toBeDefined(); - expect(mitigation.stix.id).toBe(mitigation1.stix.id); - expect(mitigation.stix.type).toBe(mitigation1.stix.type); - expect(mitigation.stix.name).toBe(mitigation1.stix.name); - expect(mitigation.stix.description).toBe(mitigation1.stix.description); - expect(mitigation.stix.spec_version).toBe(mitigation1.stix.spec_version); - expect(mitigation.stix.object_marking_refs).toEqual(expect.arrayContaining(mitigation1.stix.object_marking_refs)); - expect(mitigation.stix.created_by_ref).toBe(mitigation1.stix.created_by_ref); - expect(mitigation.stix.x_mitre_version).toBe(mitigation1.stix.x_mitre_version); - expect(mitigation.stix.x_mitre_attack_spec_version).toBe(mitigation1.stix.x_mitre_attack_spec_version); - - expect(mitigation.stix.labels).toBeDefined(); - expect(Array.isArray(mitigation.stix.labels)).toBe(true); - expect(mitigation.stix.labels.length).toBe(mitigation1.stix.labels.length); - }); - - it('PUT /api/mitigations updates a mitigation', async function () { - const originalModified = mitigation1.stix.modified; - const timestamp = new Date().toISOString(); - mitigation1.stix.modified = timestamp; - mitigation1.stix.description = 'This is an updated mitigation.' - const body = mitigation1; - const res = await request(app) - .put('/api/mitigations/' + mitigation1.stix.id + '/modified/' + originalModified) - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - - // We expect to get the updated mitigation - const mitigation = res.body; - expect(mitigation).toBeDefined(); - expect(mitigation.stix.id).toBe(mitigation1.stix.id); - expect(mitigation.stix.modified).toBe(mitigation1.stix.modified); - - }); - - it('POST /api/mitigations does not create a mitigation with the same id and modified date', async function () { - const body = mitigation1; - await request(app) - .post('/api/mitigations') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(409); - }); - - let mitigation2; - it('POST /api/mitigations should create a new version of a mitigation with a duplicate stix.id but different stix.modified date', async function () { - mitigation2 = _.cloneDeep(mitigation1); - mitigation2._id = undefined; - mitigation2.__t = undefined; - mitigation2.__v = undefined; - const timestamp = new Date().toISOString(); - mitigation2.stix.modified = timestamp; - const body = mitigation2; - const res = await request(app) - .post('/api/mitigations') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - - // We expect to get the created mitigation - const mitigation = res.body; - expect(mitigation).toBeDefined(); - - }); - - let mitigation3; - it('POST /api/mitigations should create a new version of a mitigation with a duplicate stix.id but different stix.modified date', async function () { - mitigation3 = _.cloneDeep(mitigation1); - mitigation3._id = undefined; - mitigation3.__t = undefined; - mitigation3.__v = undefined; - const timestamp = new Date().toISOString(); - mitigation3.stix.modified = timestamp; - const body = mitigation3; - const res = await request(app) - .post('/api/mitigations') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - - // We expect to get the created mitigation - const mitigation = res.body; - expect(mitigation).toBeDefined(); - - }); - - it('GET /api/mitigations returns the latest added mitigation', async function () { - const res = await request(app) - .get('/api/mitigations/' + mitigation3.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - - // We expect to get one mitigation in an array - const mitigations = res.body; - expect(mitigations).toBeDefined(); - expect(Array.isArray(mitigations)).toBe(true); - expect(mitigations.length).toBe(1); - const mitigation = mitigations[0]; - expect(mitigation.stix.id).toBe(mitigation3.stix.id); - expect(mitigation.stix.modified).toBe(mitigation3.stix.modified); - }); - - it('GET /api/mitigations returns all added mitigations', async function () { - const res = await request(app) - .get('/api/mitigations/' + mitigation1.stix.id + '?versions=all') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get two mitigations in an array - const mitigations = res.body; - expect(mitigations).toBeDefined(); - expect(Array.isArray(mitigations)).toBe(true); - expect(mitigations.length).toBe(3); - - }); - - it('GET /api/mitigations/:id/modified/:modified returns the first added mitigation', async function () { - const res = await request(app) - .get('/api/mitigations/' + mitigation1.stix.id + '/modified/' + mitigation1.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - - // We expect to get one mitigation in an array - const mitigation = res.body; - expect(mitigation).toBeDefined(); - expect(mitigation.stix).toBeDefined(); - expect(mitigation.stix.id).toBe(mitigation1.stix.id); - expect(mitigation.stix.modified).toBe(mitigation1.stix.modified); - - }); - - it('GET /api/mitigations/:id/modified/:modified returns the second added mitigation', async function () { - const res = await request(app) - .get('/api/mitigations/' + mitigation2.stix.id + '/modified/' + mitigation2.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one mitigation in an array - const mitigation = res.body; - expect(mitigation).toBeDefined(); - expect(mitigation.stix).toBeDefined(); - expect(mitigation.stix.id).toBe(mitigation2.stix.id); - expect(mitigation.stix.modified).toBe(mitigation2.stix.modified); - - }); - - it('DELETE /api/mitigations/:id should not delete a mitigation when the id cannot be found', async function () { - await request(app) - .delete('/api/mitigations/not-an-id') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('DELETE /api/mitigations/:id/modified/:modified deletes a mitigation', async function () { - await request(app) - .delete('/api/mitigations/' + mitigation1.stix.id + '/modified/' + mitigation1.stix.modified) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - }); - - it('DELETE /api/mitigations/:id should delete all the mitigations with the same stix id', async function () { - await request(app) - .delete('/api/mitigations/' + mitigation2.stix.id) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - }); - - it('GET /api/mitigations returns an empty array of mitigations', async function () { - const res = await request(app) - .get('/api/mitigations') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const mitigations = res.body; - expect(mitigations).toBeDefined(); - expect(Array.isArray(mitigations)).toBe(true); - expect(mitigations.length).toBe(0); - - - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + it('GET /api/mitigations returns an empty array of mitigations', async function () { + const res = await request(app) + .get('/api/mitigations') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const mitigations = res.body; + expect(mitigations).toBeDefined(); + expect(Array.isArray(mitigations)).toBe(true); + expect(mitigations.length).toBe(0); + }); + + it('POST /api/mitigations does not create an empty mitigation', async function () { + const body = {}; + await request(app) + .post('/api/mitigations') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + let mitigation1; + it('POST /api/mitigations creates a mitigation', async function () { + const timestamp = new Date().toISOString(); + initialObjectData.stix.created = timestamp; + initialObjectData.stix.modified = timestamp; + const body = initialObjectData; + const res = await request(app) + .post('/api/mitigations') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created mitigation + mitigation1 = res.body; + expect(mitigation1).toBeDefined(); + expect(mitigation1.stix).toBeDefined(); + expect(mitigation1.stix.id).toBeDefined(); + expect(mitigation1.stix.created).toBeDefined(); + expect(mitigation1.stix.modified).toBeDefined(); + expect(mitigation1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); + + expect(mitigation1.stix.labels).toBeDefined(); + expect(Array.isArray(mitigation1.stix.labels)).toBe(true); + expect(mitigation1.stix.labels.length).toBe(2); + }); + + it('GET /api/mitigations returns the added mitigation', async function () { + const res = await request(app) + .get('/api/mitigations') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one mitigation in an array + const mitigations = res.body; + expect(mitigations).toBeDefined(); + expect(Array.isArray(mitigations)).toBe(true); + expect(mitigations.length).toBe(1); + }); + + it('GET /api/mitigations/:id should not return a mitigation when the id cannot be found', async function () { + await request(app) + .get('/api/mitigations/not-an-id') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('GET /api/mitigations/:id returns the added mitigation', async function () { + const res = await request(app) + .get('/api/mitigations/' + mitigation1.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one mitigation in an array + const mitigations = res.body; + expect(mitigations).toBeDefined(); + expect(Array.isArray(mitigations)).toBe(true); + expect(mitigations.length).toBe(1); + + const mitigation = mitigations[0]; + expect(mitigation).toBeDefined(); + expect(mitigation.stix).toBeDefined(); + expect(mitigation.stix.id).toBe(mitigation1.stix.id); + expect(mitigation.stix.type).toBe(mitigation1.stix.type); + expect(mitigation.stix.name).toBe(mitigation1.stix.name); + expect(mitigation.stix.description).toBe(mitigation1.stix.description); + expect(mitigation.stix.spec_version).toBe(mitigation1.stix.spec_version); + expect(mitigation.stix.object_marking_refs).toEqual( + expect.arrayContaining(mitigation1.stix.object_marking_refs), + ); + expect(mitigation.stix.created_by_ref).toBe(mitigation1.stix.created_by_ref); + expect(mitigation.stix.x_mitre_version).toBe(mitigation1.stix.x_mitre_version); + expect(mitigation.stix.x_mitre_attack_spec_version).toBe( + mitigation1.stix.x_mitre_attack_spec_version, + ); + + expect(mitigation.stix.labels).toBeDefined(); + expect(Array.isArray(mitigation.stix.labels)).toBe(true); + expect(mitigation.stix.labels.length).toBe(mitigation1.stix.labels.length); + }); + + it('PUT /api/mitigations updates a mitigation', async function () { + const originalModified = mitigation1.stix.modified; + const timestamp = new Date().toISOString(); + mitigation1.stix.modified = timestamp; + mitigation1.stix.description = 'This is an updated mitigation.'; + const body = mitigation1; + const res = await request(app) + .put('/api/mitigations/' + mitigation1.stix.id + '/modified/' + originalModified) + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the updated mitigation + const mitigation = res.body; + expect(mitigation).toBeDefined(); + expect(mitigation.stix.id).toBe(mitigation1.stix.id); + expect(mitigation.stix.modified).toBe(mitigation1.stix.modified); + }); + + it('POST /api/mitigations does not create a mitigation with the same id and modified date', async function () { + const body = mitigation1; + await request(app) + .post('/api/mitigations') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(409); + }); + + let mitigation2; + it('POST /api/mitigations should create a new version of a mitigation with a duplicate stix.id but different stix.modified date', async function () { + mitigation2 = _.cloneDeep(mitigation1); + mitigation2._id = undefined; + mitigation2.__t = undefined; + mitigation2.__v = undefined; + const timestamp = new Date().toISOString(); + mitigation2.stix.modified = timestamp; + const body = mitigation2; + const res = await request(app) + .post('/api/mitigations') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created mitigation + const mitigation = res.body; + expect(mitigation).toBeDefined(); + }); + + let mitigation3; + it('POST /api/mitigations should create a new version of a mitigation with a duplicate stix.id but different stix.modified date', async function () { + mitigation3 = _.cloneDeep(mitigation1); + mitigation3._id = undefined; + mitigation3.__t = undefined; + mitigation3.__v = undefined; + const timestamp = new Date().toISOString(); + mitigation3.stix.modified = timestamp; + const body = mitigation3; + const res = await request(app) + .post('/api/mitigations') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created mitigation + const mitigation = res.body; + expect(mitigation).toBeDefined(); + }); + + it('GET /api/mitigations returns the latest added mitigation', async function () { + const res = await request(app) + .get('/api/mitigations/' + mitigation3.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one mitigation in an array + const mitigations = res.body; + expect(mitigations).toBeDefined(); + expect(Array.isArray(mitigations)).toBe(true); + expect(mitigations.length).toBe(1); + const mitigation = mitigations[0]; + expect(mitigation.stix.id).toBe(mitigation3.stix.id); + expect(mitigation.stix.modified).toBe(mitigation3.stix.modified); + }); + + it('GET /api/mitigations returns all added mitigations', async function () { + const res = await request(app) + .get('/api/mitigations/' + mitigation1.stix.id + '?versions=all') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get two mitigations in an array + const mitigations = res.body; + expect(mitigations).toBeDefined(); + expect(Array.isArray(mitigations)).toBe(true); + expect(mitigations.length).toBe(3); + }); + + it('GET /api/mitigations/:id/modified/:modified returns the first added mitigation', async function () { + const res = await request(app) + .get('/api/mitigations/' + mitigation1.stix.id + '/modified/' + mitigation1.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one mitigation in an array + const mitigation = res.body; + expect(mitigation).toBeDefined(); + expect(mitigation.stix).toBeDefined(); + expect(mitigation.stix.id).toBe(mitigation1.stix.id); + expect(mitigation.stix.modified).toBe(mitigation1.stix.modified); + }); + + it('GET /api/mitigations/:id/modified/:modified returns the second added mitigation', async function () { + const res = await request(app) + .get('/api/mitigations/' + mitigation2.stix.id + '/modified/' + mitigation2.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one mitigation in an array + const mitigation = res.body; + expect(mitigation).toBeDefined(); + expect(mitigation.stix).toBeDefined(); + expect(mitigation.stix.id).toBe(mitigation2.stix.id); + expect(mitigation.stix.modified).toBe(mitigation2.stix.modified); + }); + + it('DELETE /api/mitigations/:id should not delete a mitigation when the id cannot be found', async function () { + await request(app) + .delete('/api/mitigations/not-an-id') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('DELETE /api/mitigations/:id/modified/:modified deletes a mitigation', async function () { + await request(app) + .delete('/api/mitigations/' + mitigation1.stix.id + '/modified/' + mitigation1.stix.modified) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('DELETE /api/mitigations/:id should delete all the mitigations with the same stix id', async function () { + await request(app) + .delete('/api/mitigations/' + mitigation2.stix.id) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('GET /api/mitigations returns an empty array of mitigations', async function () { + const res = await request(app) + .get('/api/mitigations') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const mitigations = res.body; + expect(mitigations).toBeDefined(); + expect(Array.isArray(mitigations)).toBe(true); + expect(mitigations.length).toBe(0); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/api/notes/notes.spec.js b/app/tests/api/notes/notes.spec.js index 8833d529..b47ed430 100644 --- a/app/tests/api/notes/notes.spec.js +++ b/app/tests/api/notes/notes.spec.js @@ -14,470 +14,430 @@ logger.level = 'debug'; // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - spec_version: '2.1', - type: 'note', - abstract: 'This is the abstract for a note. Ivory.', - content: 'This is the content for a note.', - authors: [ - 'Author 1', - 'Author 2' - ], - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--6444f546-6900-4456-b3b1-015c88d70dab", - object_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - } + }, + stix: { + spec_version: '2.1', + type: 'note', + abstract: 'This is the abstract for a note. Ivory.', + content: 'This is the content for a note.', + authors: ['Author 1', 'Author 2'], + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--6444f546-6900-4456-b3b1-015c88d70dab', + object_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + }, }; describe('Notes API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - it('GET /api/notes should return an empty array of notes', async function () { - const res = await request(app) - .get('/api/notes') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - - // We expect to get an empty array - const notes = res.body; - expect(notes).toBeDefined(); - expect(Array.isArray(notes)).toBe(true); - expect(notes.length).toBe(0); - - }); - - it('POST /api/notes should not create an empty note', async function () { - const body = { }; - await request(app) - .post('/api/notes') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - let note1; - it('POST /api/notes should create a note', async function () { - const timestamp = new Date().toISOString(); - initialObjectData.stix.created = timestamp; - initialObjectData.stix.modified = timestamp; - const body = initialObjectData; - const res = await request(app) - .post('/api/notes') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created note - note1 = res.body; - expect(note1).toBeDefined(); - expect(note1.stix).toBeDefined(); - expect(note1.stix.id).toBeDefined(); - expect(note1.stix.created).toBeDefined(); - expect(note1.stix.modified).toBeDefined(); - expect(note1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); - }); - - it('GET /api/notes should return the added note', async function () { - const res = await request(app) - .get('/api/notes') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - - // We expect to get one note in an array - const notes = res.body; - expect(notes).toBeDefined(); - expect(Array.isArray(notes)).toBe(true); - expect(notes.length).toBe(1); - - }); - - it('GET /api/notes can use the lastUpdatedBy parameter to return the note(s), if any, which were created by a certain user (expect 0 > for anonymous user)', async function () { - const res = await request(app) - .get(`/api/notes?lastUpdatedBy=${note1.workspace.workflow.created_by_user_account}`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // Only 1 note created by this user - const notes = res.body; - expect(notes).toBeDefined(); - expect(Array.isArray(notes)).toBe(true); - expect(notes.length).toBe(1); - - }); - - it('GET /api/notes can use the lastUpdatedBy parameter to return note(s), if any, which were created by a certain user (expect 0 for a fake user)', async function () { - const res = await request(app) - .get(`/api/notes?lastUpdatedBy=identity--11111111-1111-1111-1111-111111111111`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // Expect no notes to be created by this user - const notes = res.body; - expect(notes).toBeDefined(); - expect(Array.isArray(notes)).toBe(true); - expect(notes.length).toBe(0); - - }); - - it('GET /api/notes/:id should not return a note when the id cannot be found', async function () { - await request(app) - .get('/api/notes/not-an-id') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('GET /api/notes/:id should return the added note', async function () { - const res = await request(app) - .get('/api/notes/' + note1.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - - // We expect to get one note in an array - const notes = res.body; - expect(notes).toBeDefined(); - expect(Array.isArray(notes)).toBe(true); - expect(notes.length).toBe(1); - - const note = notes[0]; - expect(note).toBeDefined(); - expect(note.stix).toBeDefined(); - expect(note.stix.id).toBe(note1.stix.id); - expect(note.stix.type).toBe(note1.stix.type); - expect(note.stix.abstract).toBe(note1.stix.abstract); - expect(note.stix.content).toBe(note1.stix.content); - expect(note.stix.spec_version).toBe(note1.stix.spec_version); - expect(note.stix.object_refs).toEqual(expect.arrayContaining(note1.stix.object_refs)); - expect(note.stix.created_by_ref).toBe(note1.stix.created_by_ref); - expect(note.stix.x_mitre_attack_spec_version).toBe(note1.stix.x_mitre_attack_spec_version); - }); - - it('PUT /api/notes should update a note', async function () { - const originalModified = note1.stix.modified; - const timestamp = new Date().toISOString(); - note1.stix.modified = timestamp; - note1.stix.description = 'This is an updated note.' - const body = note1; - const res = await request(app) - .put('/api/notes/' + note1.stix.id + '/modified/' + originalModified) - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the updated note - const note = res.body; - expect(note).toBeDefined(); - expect(note.stix.id).toBe(note1.stix.id); - expect(note.stix.modified).toBe(note1.stix.modified); - - }); - - it('POST /api/notes should not create a note with the same id and modified date', async function () { - const body = note1; - await request(app) - .post('/api/notes') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(409); - - }); - - let note2; - it('POST /api/notes should create a new version of a note with a duplicate stix.id but different stix.modified date', async function () { - note2 = _.cloneDeep(note1); - note2._id = undefined; - note2.__t = undefined; - note2.__v = undefined; - const timestamp = new Date().toISOString(); - note2.stix.abstract = 'This is the abstract for a note.'; - note2.stix.content = 'Still a note. Parchment.' - note2.stix.modified = timestamp; - const body = note2; - const res = await request(app) - .post('/api/notes') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created note - note2 = res.body; - expect(note2).toBeDefined(); - - }); - - it('GET /api/notes should return the latest added note', async function () { - const res = await request(app) - .get('/api/notes/' + note2.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - - // We expect to get one note in an array - const notes = res.body; - expect(notes).toBeDefined(); - expect(Array.isArray(notes)).toBe(true); - expect(notes.length).toBe(1); - const note = notes[0]; - expect(note.stix.id).toBe(note2.stix.id); - expect(note.stix.modified).toBe(note2.stix.modified); - - }); - - let note3; - it('POST /api/notes should create a new note with a new stix.id', async function () { - note3 = _.cloneDeep(note1); - note3._id = undefined; - note3.__t = undefined; - note3.__v = undefined; - note3.stix.id = undefined; - const timestamp = new Date().toISOString(); - note3.stix.abstract = 'This is the abstract for a note.'; - note3.stix.content = 'Still a note.' - note3.stix.modified = timestamp; - const body = note3; - const res = await request(app) - .post('/api/notes') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - - // We expect to get the created note - note3 = res.body; - expect(note3).toBeDefined(); - - }); - - let note4; - it('POST /api/notes should create a new version of the last note with a duplicate stix.id but different stix.modified date', async function () { - note4 = _.cloneDeep(note3); - note4._id = undefined; - note4.__t = undefined; - note4.__v = undefined; - const timestamp = new Date().toISOString(); - note4.stix.abstract = 'This is the abstract for a note. Parchment'; - note4.stix.content = 'Still a note.' - note4.stix.modified = timestamp; - const body = note4; - const res = await request(app) - .post('/api/notes') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - - // We expect to get the created note - note4 = res.body; - expect(note4).toBeDefined(); - - }); - - it('GET /api/notes/:id should return all versions of the first note', async function () { - const res = await request(app) - .get('/api/notes/' + note1.stix.id + '?versions=all') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - - // We expect to get two notes in an array - const notes = res.body; - expect(notes).toBeDefined(); - expect(Array.isArray(notes)).toBe(true); - expect(notes.length).toBe(2); - - }); - - it('GET /api/notes/:id/modified/:modified should return the first added note', async function () { - const res = await request(app) - .get('/api/notes/' + note1.stix.id + '/modified/' + note1.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - - // We expect to get one note in an array - const note = res.body; - expect(note).toBeDefined(); - expect(note.stix).toBeDefined(); - expect(note.stix.id).toBe(note1.stix.id); - expect(note.stix.modified).toBe(note1.stix.modified); - - }); - - it('GET /api/notes/:id/modified/:modified should return the second added note', async function () { - const res = await request(app) - .get('/api/notes/' + note2.stix.id + '/modified/' + note2.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - - // We expect to get one note in an array - const note = res.body; - expect(note).toBeDefined(); - expect(note.stix).toBeDefined(); - expect(note.stix.id).toBe(note2.stix.id); - expect(note.stix.modified).toBe(note2.stix.modified); - - }); - - it('GET /api/notes should return the latest version of all the notes', async function () { - const res = await request(app) - .get('/api/notes/') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - - // We expect to get two notes in an array - const notes = res.body; - expect(notes).toBeDefined(); - expect(Array.isArray(notes)).toBe(true); - expect(notes.length).toBe(2); - - }); - - it('GET /api/notes uses the search parameter to return the latest version of both notes', async function () { - const res = await request(app) - .get('/api/notes?search=PARCHMENT') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - - // We expect to get two notes in an array - const notes = res.body; - expect(notes).toBeDefined(); - expect(Array.isArray(notes)).toBe(true); - expect(notes.length).toBe(2); - - }); - - it('GET /api/notes should not get the first version of the note when using the search parameter', async function () { - const res = await request(app) - .get('/api/notes?search=IVORY') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - - // We expect to get zero notes in an array - const notes = res.body; - expect(notes).toBeDefined(); - expect(Array.isArray(notes)).toBe(true); - expect(notes.length).toBe(0); - - }); - - it('DELETE /api/notes/:id should not delete a note when the id cannot be found', async function () { - await request(app) - .delete('/api/notes/not-an-id') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - - }); - - it('DELETE /api/notes/:id/modified/:modified should delete the first version of the note', async function () { - await request(app) - .delete('/api/notes/' + note1.stix.id + '/modified/' + note1.stix.modified) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - - }); - - it('DELETE /api/notes/:id/modified/:modified should delete the second version of the note', async function () { - await request(app) - .delete('/api/notes/' + note2.stix.id + '/modified/' + note2.stix.modified) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - - }); - - it('DELETE /api/notes/:id should delete all versions of the note', async function () { - await request(app) - .delete('/api/notes/' + note3.stix.id) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - - }); - - it('GET /api/notes should return an empty array of notes', async function () { - const res = await request(app) - .get('/api/notes') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - - - // We expect to get an empty array - const notes = res.body; - expect(notes).toBeDefined(); - expect(Array.isArray(notes)).toBe(true); - expect(notes.length).toBe(0); - - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + it('GET /api/notes should return an empty array of notes', async function () { + const res = await request(app) + .get('/api/notes') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const notes = res.body; + expect(notes).toBeDefined(); + expect(Array.isArray(notes)).toBe(true); + expect(notes.length).toBe(0); + }); + + it('POST /api/notes should not create an empty note', async function () { + const body = {}; + await request(app) + .post('/api/notes') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + let note1; + it('POST /api/notes should create a note', async function () { + const timestamp = new Date().toISOString(); + initialObjectData.stix.created = timestamp; + initialObjectData.stix.modified = timestamp; + const body = initialObjectData; + const res = await request(app) + .post('/api/notes') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created note + note1 = res.body; + expect(note1).toBeDefined(); + expect(note1.stix).toBeDefined(); + expect(note1.stix.id).toBeDefined(); + expect(note1.stix.created).toBeDefined(); + expect(note1.stix.modified).toBeDefined(); + expect(note1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); + }); + + it('GET /api/notes should return the added note', async function () { + const res = await request(app) + .get('/api/notes') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one note in an array + const notes = res.body; + expect(notes).toBeDefined(); + expect(Array.isArray(notes)).toBe(true); + expect(notes.length).toBe(1); + }); + + it('GET /api/notes can use the lastUpdatedBy parameter to return the note(s), if any, which were created by a certain user (expect 0 > for anonymous user)', async function () { + const res = await request(app) + .get(`/api/notes?lastUpdatedBy=${note1.workspace.workflow.created_by_user_account}`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // Only 1 note created by this user + const notes = res.body; + expect(notes).toBeDefined(); + expect(Array.isArray(notes)).toBe(true); + expect(notes.length).toBe(1); + }); + + it('GET /api/notes can use the lastUpdatedBy parameter to return note(s), if any, which were created by a certain user (expect 0 for a fake user)', async function () { + const res = await request(app) + .get(`/api/notes?lastUpdatedBy=identity--11111111-1111-1111-1111-111111111111`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // Expect no notes to be created by this user + const notes = res.body; + expect(notes).toBeDefined(); + expect(Array.isArray(notes)).toBe(true); + expect(notes.length).toBe(0); + }); + + it('GET /api/notes/:id should not return a note when the id cannot be found', async function () { + await request(app) + .get('/api/notes/not-an-id') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('GET /api/notes/:id should return the added note', async function () { + const res = await request(app) + .get('/api/notes/' + note1.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one note in an array + const notes = res.body; + expect(notes).toBeDefined(); + expect(Array.isArray(notes)).toBe(true); + expect(notes.length).toBe(1); + + const note = notes[0]; + expect(note).toBeDefined(); + expect(note.stix).toBeDefined(); + expect(note.stix.id).toBe(note1.stix.id); + expect(note.stix.type).toBe(note1.stix.type); + expect(note.stix.abstract).toBe(note1.stix.abstract); + expect(note.stix.content).toBe(note1.stix.content); + expect(note.stix.spec_version).toBe(note1.stix.spec_version); + expect(note.stix.object_refs).toEqual(expect.arrayContaining(note1.stix.object_refs)); + expect(note.stix.created_by_ref).toBe(note1.stix.created_by_ref); + expect(note.stix.x_mitre_attack_spec_version).toBe(note1.stix.x_mitre_attack_spec_version); + }); + + it('PUT /api/notes should update a note', async function () { + const originalModified = note1.stix.modified; + const timestamp = new Date().toISOString(); + note1.stix.modified = timestamp; + note1.stix.description = 'This is an updated note.'; + const body = note1; + const res = await request(app) + .put('/api/notes/' + note1.stix.id + '/modified/' + originalModified) + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the updated note + const note = res.body; + expect(note).toBeDefined(); + expect(note.stix.id).toBe(note1.stix.id); + expect(note.stix.modified).toBe(note1.stix.modified); + }); + + it('POST /api/notes should not create a note with the same id and modified date', async function () { + const body = note1; + await request(app) + .post('/api/notes') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(409); + }); + + let note2; + it('POST /api/notes should create a new version of a note with a duplicate stix.id but different stix.modified date', async function () { + note2 = _.cloneDeep(note1); + note2._id = undefined; + note2.__t = undefined; + note2.__v = undefined; + const timestamp = new Date().toISOString(); + note2.stix.abstract = 'This is the abstract for a note.'; + note2.stix.content = 'Still a note. Parchment.'; + note2.stix.modified = timestamp; + const body = note2; + const res = await request(app) + .post('/api/notes') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created note + note2 = res.body; + expect(note2).toBeDefined(); + }); + + it('GET /api/notes should return the latest added note', async function () { + const res = await request(app) + .get('/api/notes/' + note2.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one note in an array + const notes = res.body; + expect(notes).toBeDefined(); + expect(Array.isArray(notes)).toBe(true); + expect(notes.length).toBe(1); + const note = notes[0]; + expect(note.stix.id).toBe(note2.stix.id); + expect(note.stix.modified).toBe(note2.stix.modified); + }); + + let note3; + it('POST /api/notes should create a new note with a new stix.id', async function () { + note3 = _.cloneDeep(note1); + note3._id = undefined; + note3.__t = undefined; + note3.__v = undefined; + note3.stix.id = undefined; + const timestamp = new Date().toISOString(); + note3.stix.abstract = 'This is the abstract for a note.'; + note3.stix.content = 'Still a note.'; + note3.stix.modified = timestamp; + const body = note3; + const res = await request(app) + .post('/api/notes') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created note + note3 = res.body; + expect(note3).toBeDefined(); + }); + + let note4; + it('POST /api/notes should create a new version of the last note with a duplicate stix.id but different stix.modified date', async function () { + note4 = _.cloneDeep(note3); + note4._id = undefined; + note4.__t = undefined; + note4.__v = undefined; + const timestamp = new Date().toISOString(); + note4.stix.abstract = 'This is the abstract for a note. Parchment'; + note4.stix.content = 'Still a note.'; + note4.stix.modified = timestamp; + const body = note4; + const res = await request(app) + .post('/api/notes') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created note + note4 = res.body; + expect(note4).toBeDefined(); + }); + + it('GET /api/notes/:id should return all versions of the first note', async function () { + const res = await request(app) + .get('/api/notes/' + note1.stix.id + '?versions=all') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get two notes in an array + const notes = res.body; + expect(notes).toBeDefined(); + expect(Array.isArray(notes)).toBe(true); + expect(notes.length).toBe(2); + }); + + it('GET /api/notes/:id/modified/:modified should return the first added note', async function () { + const res = await request(app) + .get('/api/notes/' + note1.stix.id + '/modified/' + note1.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one note in an array + const note = res.body; + expect(note).toBeDefined(); + expect(note.stix).toBeDefined(); + expect(note.stix.id).toBe(note1.stix.id); + expect(note.stix.modified).toBe(note1.stix.modified); + }); + + it('GET /api/notes/:id/modified/:modified should return the second added note', async function () { + const res = await request(app) + .get('/api/notes/' + note2.stix.id + '/modified/' + note2.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one note in an array + const note = res.body; + expect(note).toBeDefined(); + expect(note.stix).toBeDefined(); + expect(note.stix.id).toBe(note2.stix.id); + expect(note.stix.modified).toBe(note2.stix.modified); + }); + + it('GET /api/notes should return the latest version of all the notes', async function () { + const res = await request(app) + .get('/api/notes/') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get two notes in an array + const notes = res.body; + expect(notes).toBeDefined(); + expect(Array.isArray(notes)).toBe(true); + expect(notes.length).toBe(2); + }); + + it('GET /api/notes uses the search parameter to return the latest version of both notes', async function () { + const res = await request(app) + .get('/api/notes?search=PARCHMENT') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get two notes in an array + const notes = res.body; + expect(notes).toBeDefined(); + expect(Array.isArray(notes)).toBe(true); + expect(notes.length).toBe(2); + }); + + it('GET /api/notes should not get the first version of the note when using the search parameter', async function () { + const res = await request(app) + .get('/api/notes?search=IVORY') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get zero notes in an array + const notes = res.body; + expect(notes).toBeDefined(); + expect(Array.isArray(notes)).toBe(true); + expect(notes.length).toBe(0); + }); + + it('DELETE /api/notes/:id should not delete a note when the id cannot be found', async function () { + await request(app) + .delete('/api/notes/not-an-id') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('DELETE /api/notes/:id/modified/:modified should delete the first version of the note', async function () { + await request(app) + .delete('/api/notes/' + note1.stix.id + '/modified/' + note1.stix.modified) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('DELETE /api/notes/:id/modified/:modified should delete the second version of the note', async function () { + await request(app) + .delete('/api/notes/' + note2.stix.id + '/modified/' + note2.stix.modified) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('DELETE /api/notes/:id should delete all versions of the note', async function () { + await request(app) + .delete('/api/notes/' + note3.stix.id) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('GET /api/notes should return an empty array of notes', async function () { + const res = await request(app) + .get('/api/notes') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const notes = res.body; + expect(notes).toBeDefined(); + expect(Array.isArray(notes)).toBe(true); + expect(notes.length).toBe(0); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/api/references/references.spec.js b/app/tests/api/references/references.spec.js index 5ed8bcfa..32aedffe 100644 --- a/app/tests/api/references/references.spec.js +++ b/app/tests/api/references/references.spec.js @@ -13,287 +13,287 @@ const Reference = require('../../../models/reference-model'); // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData1 = { - source_name: 'source 1', - description: 'This is a reference (#1)', - url: 'https://www.reference-site.com/myreferencelink1' + source_name: 'source 1', + description: 'This is a reference (#1)', + url: 'https://www.reference-site.com/myreferencelink1', }; const initialObjectData2 = { - source_name: 'source 2', - description: 'This is a reference (#2)', - url: 'https://www.reference-site.com/myreferencelink2' + source_name: 'source 2', + description: 'This is a reference (#2)', + url: 'https://www.reference-site.com/myreferencelink2', }; const initialObjectData3 = { - source_name: 'unique source 3', - description: 'This is a reference (#3)', - url: 'https://www.reference-site.com/myreferencelink3' + source_name: 'unique source 3', + description: 'This is a reference (#3)', + url: 'https://www.reference-site.com/myreferencelink3', }; describe('References API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Wait until the Reference indexes are created - await Reference.init(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - it('GET /api/references returns an empty array of references', async function () { - const res = await request(app) - .get('/api/references') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const references = res.body; - expect(references).toBeDefined(); - expect(Array.isArray(references)).toBe(true); - expect(references.length).toBe(0); - }); - - it('POST /api/references does not create an empty reference', async function () { - const body = { }; - await request(app) - .post('/api/references') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - let reference1; - it('POST /api/references creates a reference', async function () { - const body = initialObjectData1; - const res = await request(app) - .post('/api/references') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created reference - reference1 = res.body; - expect(reference1).toBeDefined(); - }); - - let reference2; - it('POST /api/references creates a second reference', async function () { - const body = initialObjectData2; - const res = await request(app) - .post('/api/references') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created reference - reference2 = res.body; - expect(reference2).toBeDefined(); - - }); - - let reference3; - it('POST /api/references creates a third reference', async function () { - const body = initialObjectData3; - const res = await request(app) - .post('/api/references') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created reference - reference3 = res.body; - expect(reference3).toBeDefined(); - }); - - it('GET /api/references returns the added references', async function () { - const res = await request(app) - .get('/api/references') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - - // We expect to get one reference in an array - const references = res.body; - expect(references).toBeDefined(); - expect(Array.isArray(references)).toBe(true); - expect(references.length).toBe(3); - }); - - it('GET /api/references should return an empty array of references when the source_name cannot be found', async function () { - const res = await request(app) - .get('/api/references?sourceName=notasourcename') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200); - - // We expect to get an empty array - const references = res.body; - expect(references).toBeDefined(); - expect(Array.isArray(references)).toBe(true); - expect(references.length).toBe(0); - - }); - - it('GET /api/references returns the first added reference', async function () { - const res = await request(app) - .get('/api/references?sourceName=' + encodeURIComponent(reference1.source_name)) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one reference in an array - const references = res.body; - expect(references).toBeDefined(); - expect(Array.isArray(references)).toBe(true); - expect(references.length).toBe(1); - - const reference = references[0]; - expect(reference).toBeDefined(); - expect(reference.source_name).toBe(reference1.source_name); - expect(reference.description).toBe(reference1.description); - expect(reference.url).toBe(reference1.url); - }); - - it('GET /api/references uses the search parameter to return the third added reference', async function () { - const res = await request(app) - .get('/api/references?search=' + encodeURIComponent('#3')) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one reference in an array - const references = res.body; - expect(references).toBeDefined(); - expect(Array.isArray(references)).toBe(true); - expect(references.length).toBe(1); - - const reference = references[0]; - expect(reference).toBeDefined(); - expect(reference.source_name).toBe(reference3.source_name); - expect(reference.description).toBe(reference3.description); - expect(reference.url).toBe(reference3.url); - }); - - it('GET /api/references uses the search parameter to return the third added reference searching fields in the source_name', async function () { - const res = await request(app) - .get('/api/references?search=' + encodeURIComponent('unique')) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one reference in an array - const references = res.body; - expect(references).toBeDefined(); - expect(Array.isArray(references)).toBe(true); - expect(references.length).toBe(1); - - const reference = references[0]; - expect(reference).toBeDefined(); - expect(reference.source_name).toBe(reference3.source_name); - expect(reference.description).toBe(reference3.description); - expect(reference.url).toBe(reference3.url); - - }); - - it('PUT /api/references does not update a reference when the source_name is missing', async function () { - const body = { description: 'This reference does not have a source_name', url: '' }; - await request(app) - .put('/api/references') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - it('PUT /api/references does not update a reference when the source_name is not in the database', async function () { - const body = { source_name: 'not-a-reference', description: 'This reference is not in the database', url: '' }; - await request(app) - .put('/api/references') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('PUT /api/references updates a reference', async function () { - reference1.description = 'This is a new description'; - const body = reference1; - const res = await request(app) - .put('/api/references') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the updated reference - const reference = res.body; - expect(reference).toBeDefined(); - expect(reference.source_name).toBe(reference1.source_name); - expect(reference.description).toBe(reference1.description); - }); - - it('POST /api/references does not create a reference with a duplicate source_name', async function () { - const body = reference1; - await request(app) - .post('/api/references') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(409); - }); - - it('DELETE /api/references does not delete a reference when the source_name is omitted', async function () { - await request(app) - .delete('/api/references') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - it('DELETE /api/references does not delete a reference with a non-existent source_name', async function () { - await request(app) - .delete('/api/references?sourceName=not-a-reference') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('DELETE /api/references deletes a reference', async function () { - await request(app) - .delete(`/api/references?sourceName=${ reference1.source_name }`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Wait until the Reference indexes are created + await Reference.init(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + it('GET /api/references returns an empty array of references', async function () { + const res = await request(app) + .get('/api/references') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const references = res.body; + expect(references).toBeDefined(); + expect(Array.isArray(references)).toBe(true); + expect(references.length).toBe(0); + }); + + it('POST /api/references does not create an empty reference', async function () { + const body = {}; + await request(app) + .post('/api/references') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + let reference1; + it('POST /api/references creates a reference', async function () { + const body = initialObjectData1; + const res = await request(app) + .post('/api/references') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created reference + reference1 = res.body; + expect(reference1).toBeDefined(); + }); + + let reference2; + it('POST /api/references creates a second reference', async function () { + const body = initialObjectData2; + const res = await request(app) + .post('/api/references') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created reference + reference2 = res.body; + expect(reference2).toBeDefined(); + }); + + let reference3; + it('POST /api/references creates a third reference', async function () { + const body = initialObjectData3; + const res = await request(app) + .post('/api/references') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created reference + reference3 = res.body; + expect(reference3).toBeDefined(); + }); + + it('GET /api/references returns the added references', async function () { + const res = await request(app) + .get('/api/references') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one reference in an array + const references = res.body; + expect(references).toBeDefined(); + expect(Array.isArray(references)).toBe(true); + expect(references.length).toBe(3); + }); + + it('GET /api/references should return an empty array of references when the source_name cannot be found', async function () { + const res = await request(app) + .get('/api/references?sourceName=notasourcename') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200); + + // We expect to get an empty array + const references = res.body; + expect(references).toBeDefined(); + expect(Array.isArray(references)).toBe(true); + expect(references.length).toBe(0); + }); + + it('GET /api/references returns the first added reference', async function () { + const res = await request(app) + .get('/api/references?sourceName=' + encodeURIComponent(reference1.source_name)) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one reference in an array + const references = res.body; + expect(references).toBeDefined(); + expect(Array.isArray(references)).toBe(true); + expect(references.length).toBe(1); + + const reference = references[0]; + expect(reference).toBeDefined(); + expect(reference.source_name).toBe(reference1.source_name); + expect(reference.description).toBe(reference1.description); + expect(reference.url).toBe(reference1.url); + }); + + it('GET /api/references uses the search parameter to return the third added reference', async function () { + const res = await request(app) + .get('/api/references?search=' + encodeURIComponent('#3')) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one reference in an array + const references = res.body; + expect(references).toBeDefined(); + expect(Array.isArray(references)).toBe(true); + expect(references.length).toBe(1); + + const reference = references[0]; + expect(reference).toBeDefined(); + expect(reference.source_name).toBe(reference3.source_name); + expect(reference.description).toBe(reference3.description); + expect(reference.url).toBe(reference3.url); + }); + + it('GET /api/references uses the search parameter to return the third added reference searching fields in the source_name', async function () { + const res = await request(app) + .get('/api/references?search=' + encodeURIComponent('unique')) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one reference in an array + const references = res.body; + expect(references).toBeDefined(); + expect(Array.isArray(references)).toBe(true); + expect(references.length).toBe(1); + + const reference = references[0]; + expect(reference).toBeDefined(); + expect(reference.source_name).toBe(reference3.source_name); + expect(reference.description).toBe(reference3.description); + expect(reference.url).toBe(reference3.url); + }); + + it('PUT /api/references does not update a reference when the source_name is missing', async function () { + const body = { description: 'This reference does not have a source_name', url: '' }; + await request(app) + .put('/api/references') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + it('PUT /api/references does not update a reference when the source_name is not in the database', async function () { + const body = { + source_name: 'not-a-reference', + description: 'This reference is not in the database', + url: '', + }; + await request(app) + .put('/api/references') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('PUT /api/references updates a reference', async function () { + reference1.description = 'This is a new description'; + const body = reference1; + const res = await request(app) + .put('/api/references') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the updated reference + const reference = res.body; + expect(reference).toBeDefined(); + expect(reference.source_name).toBe(reference1.source_name); + expect(reference.description).toBe(reference1.description); + }); + + it('POST /api/references does not create a reference with a duplicate source_name', async function () { + const body = reference1; + await request(app) + .post('/api/references') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(409); + }); + + it('DELETE /api/references does not delete a reference when the source_name is omitted', async function () { + await request(app) + .delete('/api/references') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + it('DELETE /api/references does not delete a reference with a non-existent source_name', async function () { + await request(app) + .delete('/api/references?sourceName=not-a-reference') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('DELETE /api/references deletes a reference', async function () { + await request(app) + .delete(`/api/references?sourceName=${reference1.source_name}`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + after(async function () { + await database.closeConnection(); + }); }); diff --git a/app/tests/api/relationships/relationships-pagination.spec.js b/app/tests/api/relationships/relationships-pagination.spec.js index 79ab4c5f..a196add8 100644 --- a/app/tests/api/relationships/relationships-pagination.spec.js +++ b/app/tests/api/relationships/relationships-pagination.spec.js @@ -6,30 +6,28 @@ const PaginationTests = require('../../shared/pagination'); const sourceRef1 = 'malware--67e6d66b-1b82-4699-b47a-e2efb6268d14'; const targetRef1 = 'attack-pattern--7b211ac6-c815-4189-93a9-ab415deca926'; const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - spec_version: '2.1', - type: 'relationship', - description: 'This is a relationship.', - source_ref: sourceRef1, - relationship_type: 'uses', - target_ref: targetRef1, - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--6444f546-6900-4456-b3b1-015c88d70dab" - } + }, + stix: { + spec_version: '2.1', + type: 'relationship', + description: 'This is a relationship.', + source_ref: sourceRef1, + relationship_type: 'uses', + target_ref: targetRef1, + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--6444f546-6900-4456-b3b1-015c88d70dab', + }, }; const options = { - prefix: 'relationship', - baseUrl: '/api/relationships', - label: 'Relationships' -} + prefix: 'relationship', + baseUrl: '/api/relationships', + label: 'Relationships', +}; const paginationTests = new PaginationTests(relationshipsService, initialObjectData, options); paginationTests.executeTests(); diff --git a/app/tests/api/relationships/relationships.spec.js b/app/tests/api/relationships/relationships.spec.js index e9914399..06f6536e 100644 --- a/app/tests/api/relationships/relationships.spec.js +++ b/app/tests/api/relationships/relationships.spec.js @@ -23,506 +23,490 @@ const targetRef3 = 'attack-pattern--0259baeb-9f63-4c69-bf10-eb038c390688'; // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - spec_version: '2.1', - type: 'relationship', - description: 'This is a relationship.', - source_ref: sourceRef1, - relationship_type: 'uses', - target_ref: targetRef1, - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--6444f546-6900-4456-b3b1-015c88d70dab" - } + }, + stix: { + spec_version: '2.1', + type: 'relationship', + description: 'This is a relationship.', + source_ref: sourceRef1, + relationship_type: 'uses', + target_ref: targetRef1, + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--6444f546-6900-4456-b3b1-015c88d70dab', + }, }; describe('Relationships API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - it('GET /api/relationships returns an empty array of relationships', async function () { - const res = await request(app) - .get('/api/relationships') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - // We expect to get an empty array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(0); - - }); - - it('POST /api/relationships does not create an empty relationship', async function () { - const body = { }; - await request(app) - .post('/api/relationships') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - - }); - - let relationship1a; - it('POST /api/relationships creates a relationship', async function () { - const timestamp = new Date().toISOString(); - initialObjectData.stix.created = timestamp; - initialObjectData.stix.modified = timestamp; - const body = initialObjectData; - const res = await request(app) - .post('/api/relationships') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created relationship - relationship1a = res.body; - expect(relationship1a).toBeDefined(); - expect(relationship1a.stix).toBeDefined(); - expect(relationship1a.stix.id).toBeDefined(); - expect(relationship1a.stix.created).toBeDefined(); - expect(relationship1a.stix.modified).toBeDefined(); - expect(relationship1a.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); - - - }); - - it('GET /api/relationships returns the added relationship', async function () { - const res = await request(app) - .get('/api/relationships') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one relationship in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(1); - - }); - - it('GET /api/relationships/:id should not return a relationship when the id cannot be found', async function () { - await request(app) - .get('/api/relationships/not-an-id') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - - }); - - it('GET /api/relationships/:id returns the added relationship', async function () { - const res = await request(app) - .get('/api/relationships/' + relationship1a.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one relationship in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(1); - - const relationship = relationships[0]; - expect(relationship).toBeDefined(); - expect(relationship.stix).toBeDefined(); - expect(relationship.stix.id).toBe(relationship1a.stix.id); - expect(relationship.stix.type).toBe(relationship1a.stix.type); - expect(relationship.stix.name).toBe(relationship1a.stix.name); - expect(relationship.stix.description).toBe(relationship1a.stix.description); - expect(relationship.stix.spec_version).toBe(relationship1a.stix.spec_version); - expect(relationship.stix.object_marking_refs).toEqual(expect.arrayContaining(relationship1a.stix.object_marking_refs)); - expect(relationship.stix.created_by_ref).toBe(relationship1a.stix.created_by_ref); - expect(relationship.stix.x_mitre_attack_spec_version).toBe(relationship1a.stix.x_mitre_attack_spec_version); - - - }); - - it('PUT /api/relationships updates a relationship', async function () { - const originalModified = relationship1a.stix.modified; - const timestamp = new Date().toISOString(); - relationship1a.stix.modified = timestamp; - relationship1a.stix.description = 'This is an updated relationship.' - const body = relationship1a; - const res = await request(app) - .put('/api/relationships/' + relationship1a.stix.id + '/modified/' + originalModified) - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the updated relationship - const relationship = res.body; - expect(relationship).toBeDefined(); - expect(relationship.stix.id).toBe(relationship1a.stix.id); - expect(relationship.stix.modified).toBe(relationship1a.stix.modified); - - }); - - it('POST /api/relationships does not create a relationship with the same id and modified date', async function () { - const body = relationship1a; - await request(app) - .post('/api/relationships') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(409); - }); - - let relationship1b; - it('POST /api/relationships should create a new version of a relationship with a duplicate stix.id but different stix.modified date', async function () { - relationship1b = _.cloneDeep(relationship1a); - relationship1b._id = undefined; - relationship1b.__t = undefined; - relationship1b.__v = undefined; - const timestamp = new Date().toISOString(); - relationship1b.stix.modified = timestamp; - const body = relationship1b; - const res = await request(app) - .post('/api/relationships') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created relationship - const relationship = res.body; - expect(relationship).toBeDefined(); - - }); - - let relationship1c; - it('POST /api/relationships should create a new version of a relationship with a duplicate stix.id but different stix.modified date', async function () { - relationship1c = _.cloneDeep(relationship1a); - relationship1c._id = undefined; - relationship1c.__t = undefined; - relationship1c.__v = undefined; - const timestamp = new Date().toISOString(); - relationship1c.stix.modified = timestamp; - const body = relationship1c; - const res = await request(app) - .post('/api/relationships') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created relationship - const relationship = res.body; - expect(relationship).toBeDefined(); - - }); - - - it('GET /api/relationships returns the latest added relationship', async function () { - const res = await request(app) - .get('/api/relationships') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one relationship in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(1); - const relationship = relationships[0]; - expect(relationship.stix.id).toBe(relationship1c.stix.id); - expect(relationship.stix.modified).toBe(relationship1c.stix.modified); - - }); - - it('GET /api/relationships returns all added relationships', async function () { - const res = await request(app) - .get('/api/relationships?versions=all') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get two relationships in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(3); - - }); - - it('GET /api/relationships/:stixId returns the latest added relationship', async function () { - const res = await request(app) - .get('/api/relationships/' + relationship1b.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one relationship in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(1); - const relationship = relationships[0]; - expect(relationship.stix.id).toBe(relationship1c.stix.id); - expect(relationship.stix.modified).toBe(relationship1c.stix.modified); - - }); - - it('GET /api/relationships/:stixId returns all added relationships', async function () { - const res = await request(app) - .get('/api/relationships/' + relationship1a.stix.id + '?versions=all') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get two relationships in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(3); - - }); - - it('GET /api/relationships/:id/modified/:modified returns the first added relationship', async function () { - const res = await request(app) - .get('/api/relationships/' + relationship1a.stix.id + '/modified/' + relationship1a.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one relationship in an array - const relationship = res.body; - expect(relationship).toBeDefined(); - expect(relationship.stix).toBeDefined(); - expect(relationship.stix.id).toBe(relationship1a.stix.id); - expect(relationship.stix.modified).toBe(relationship1a.stix.modified); - - }); - - it('GET /api/relationships/:id/modified/:modified returns the second added relationship', async function () { - const res = await request(app) - .get('/api/relationships/' + relationship1b.stix.id + '/modified/' + relationship1b.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one relationship in an array - const relationship = res.body; - expect(relationship).toBeDefined(); - expect(relationship.stix).toBeDefined(); - expect(relationship.stix.id).toBe(relationship1b.stix.id); - expect(relationship.stix.modified).toBe(relationship1b.stix.modified); - - }); - - let relationship2; - it('POST /api/relationships creates a relationship', async function () { - const timestamp = new Date().toISOString(); - initialObjectData.stix.created = timestamp; - initialObjectData.stix.modified = timestamp; - initialObjectData.stix.source_ref = sourceRef2; - initialObjectData.stix.target_ref = targetRef2; - const body = initialObjectData; - const res = await request(app) - .post('/api/relationships') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created relationship - relationship2 = res.body; - expect(relationship2).toBeDefined(); - - }); - - it('GET /api/relationships returns the (latest) relationship matching a source_ref', async function () { - const res = await request(app) - .get('/api/relationships?sourceRef=' + sourceRef1) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one relationship in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(1); - - }); - - it('GET /api/relationships returns the (latest) relationship matching a target_ref', async function () { - const res = await request(app) - .get('/api/relationships?targetRef=' + targetRef1) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one relationship in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(1); - - }); - - it('GET /api/relationships returns the (latest) relationship matching a sourceOrTargetRef', async function () { - const res = await request(app) - .get('/api/relationships?sourceOrTargetRef=' + sourceRef1) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one relationship in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(1); - - }); - - it('GET /api/relationships returns zero relationships for a non-matching source_ref', async function () { - const res = await request(app) - .get('/api/relationships?sourceRef=' + sourceRef3) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get zero relationships in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(0); - - }); - - it('GET /api/relationships returns zero relationships for a non-matching target_ref', async function () { - const res = await request(app) - .get('/api/relationships?targetRef=' + targetRef3) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one relationship in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(0); - - }); - - it('GET /api/relationships returns zero relationships for a non-matching sourceOrTargetRef', async function () { - const res = await request(app) - .get('/api/relationships?sourceOrTargetRef=' + sourceRef3) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one relationship in an array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(0); - - }); - - it('DELETE /api/relationships/:id should not delete a relationship when the id cannot be found', async function () { - await request(app) - .delete('/api/relationships/not-an-id') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - - - }); - - it('DELETE /api/relationships/:id/modified/:modified deletes a relationship', async function () { - await request(app) - .delete('/api/relationships/' + relationship1a.stix.id + '/modified/' + relationship1a.stix.modified) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - - - }); - - it('DELETE /api/relationships/:id should delete all the relationships with the same stix id', async function () { - await request(app) - .delete('/api/relationships/' + relationship1b.stix.id) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - - - }); - - it('DELETE /api/relationships should delete the third relationship', async function () { - await request(app) - .delete('/api/relationships/' + relationship2.stix.id + '/modified/' + relationship2.stix.modified) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - - - }); - - it('GET /api/relationships returns an empty array of relationships', async function () { - const res = await request(app) - .get('/api/relationships') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const relationships = res.body; - expect(relationships).toBeDefined(); - expect(Array.isArray(relationships)).toBe(true); - expect(relationships.length).toBe(0); - - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + it('GET /api/relationships returns an empty array of relationships', async function () { + const res = await request(app) + .get('/api/relationships') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + // We expect to get an empty array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(0); + }); + + it('POST /api/relationships does not create an empty relationship', async function () { + const body = {}; + await request(app) + .post('/api/relationships') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + let relationship1a; + it('POST /api/relationships creates a relationship', async function () { + const timestamp = new Date().toISOString(); + initialObjectData.stix.created = timestamp; + initialObjectData.stix.modified = timestamp; + const body = initialObjectData; + const res = await request(app) + .post('/api/relationships') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created relationship + relationship1a = res.body; + expect(relationship1a).toBeDefined(); + expect(relationship1a.stix).toBeDefined(); + expect(relationship1a.stix.id).toBeDefined(); + expect(relationship1a.stix.created).toBeDefined(); + expect(relationship1a.stix.modified).toBeDefined(); + expect(relationship1a.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); + }); + + it('GET /api/relationships returns the added relationship', async function () { + const res = await request(app) + .get('/api/relationships') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(1); + }); + + it('GET /api/relationships/:id should not return a relationship when the id cannot be found', async function () { + await request(app) + .get('/api/relationships/not-an-id') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('GET /api/relationships/:id returns the added relationship', async function () { + const res = await request(app) + .get('/api/relationships/' + relationship1a.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(1); + + const relationship = relationships[0]; + expect(relationship).toBeDefined(); + expect(relationship.stix).toBeDefined(); + expect(relationship.stix.id).toBe(relationship1a.stix.id); + expect(relationship.stix.type).toBe(relationship1a.stix.type); + expect(relationship.stix.name).toBe(relationship1a.stix.name); + expect(relationship.stix.description).toBe(relationship1a.stix.description); + expect(relationship.stix.spec_version).toBe(relationship1a.stix.spec_version); + expect(relationship.stix.object_marking_refs).toEqual( + expect.arrayContaining(relationship1a.stix.object_marking_refs), + ); + expect(relationship.stix.created_by_ref).toBe(relationship1a.stix.created_by_ref); + expect(relationship.stix.x_mitre_attack_spec_version).toBe( + relationship1a.stix.x_mitre_attack_spec_version, + ); + }); + + it('PUT /api/relationships updates a relationship', async function () { + const originalModified = relationship1a.stix.modified; + const timestamp = new Date().toISOString(); + relationship1a.stix.modified = timestamp; + relationship1a.stix.description = 'This is an updated relationship.'; + const body = relationship1a; + const res = await request(app) + .put('/api/relationships/' + relationship1a.stix.id + '/modified/' + originalModified) + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the updated relationship + const relationship = res.body; + expect(relationship).toBeDefined(); + expect(relationship.stix.id).toBe(relationship1a.stix.id); + expect(relationship.stix.modified).toBe(relationship1a.stix.modified); + }); + + it('POST /api/relationships does not create a relationship with the same id and modified date', async function () { + const body = relationship1a; + await request(app) + .post('/api/relationships') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(409); + }); + + let relationship1b; + it('POST /api/relationships should create a new version of a relationship with a duplicate stix.id but different stix.modified date', async function () { + relationship1b = _.cloneDeep(relationship1a); + relationship1b._id = undefined; + relationship1b.__t = undefined; + relationship1b.__v = undefined; + const timestamp = new Date().toISOString(); + relationship1b.stix.modified = timestamp; + const body = relationship1b; + const res = await request(app) + .post('/api/relationships') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created relationship + const relationship = res.body; + expect(relationship).toBeDefined(); + }); + + let relationship1c; + it('POST /api/relationships should create a new version of a relationship with a duplicate stix.id but different stix.modified date', async function () { + relationship1c = _.cloneDeep(relationship1a); + relationship1c._id = undefined; + relationship1c.__t = undefined; + relationship1c.__v = undefined; + const timestamp = new Date().toISOString(); + relationship1c.stix.modified = timestamp; + const body = relationship1c; + const res = await request(app) + .post('/api/relationships') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created relationship + const relationship = res.body; + expect(relationship).toBeDefined(); + }); + + it('GET /api/relationships returns the latest added relationship', async function () { + const res = await request(app) + .get('/api/relationships') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(1); + const relationship = relationships[0]; + expect(relationship.stix.id).toBe(relationship1c.stix.id); + expect(relationship.stix.modified).toBe(relationship1c.stix.modified); + }); + + it('GET /api/relationships returns all added relationships', async function () { + const res = await request(app) + .get('/api/relationships?versions=all') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get two relationships in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(3); + }); + + it('GET /api/relationships/:stixId returns the latest added relationship', async function () { + const res = await request(app) + .get('/api/relationships/' + relationship1b.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(1); + const relationship = relationships[0]; + expect(relationship.stix.id).toBe(relationship1c.stix.id); + expect(relationship.stix.modified).toBe(relationship1c.stix.modified); + }); + + it('GET /api/relationships/:stixId returns all added relationships', async function () { + const res = await request(app) + .get('/api/relationships/' + relationship1a.stix.id + '?versions=all') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get two relationships in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(3); + }); + + it('GET /api/relationships/:id/modified/:modified returns the first added relationship', async function () { + const res = await request(app) + .get( + '/api/relationships/' + + relationship1a.stix.id + + '/modified/' + + relationship1a.stix.modified, + ) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationship = res.body; + expect(relationship).toBeDefined(); + expect(relationship.stix).toBeDefined(); + expect(relationship.stix.id).toBe(relationship1a.stix.id); + expect(relationship.stix.modified).toBe(relationship1a.stix.modified); + }); + + it('GET /api/relationships/:id/modified/:modified returns the second added relationship', async function () { + const res = await request(app) + .get( + '/api/relationships/' + + relationship1b.stix.id + + '/modified/' + + relationship1b.stix.modified, + ) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationship = res.body; + expect(relationship).toBeDefined(); + expect(relationship.stix).toBeDefined(); + expect(relationship.stix.id).toBe(relationship1b.stix.id); + expect(relationship.stix.modified).toBe(relationship1b.stix.modified); + }); + + let relationship2; + it('POST /api/relationships creates a relationship', async function () { + const timestamp = new Date().toISOString(); + initialObjectData.stix.created = timestamp; + initialObjectData.stix.modified = timestamp; + initialObjectData.stix.source_ref = sourceRef2; + initialObjectData.stix.target_ref = targetRef2; + const body = initialObjectData; + const res = await request(app) + .post('/api/relationships') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created relationship + relationship2 = res.body; + expect(relationship2).toBeDefined(); + }); + + it('GET /api/relationships returns the (latest) relationship matching a source_ref', async function () { + const res = await request(app) + .get('/api/relationships?sourceRef=' + sourceRef1) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(1); + }); + + it('GET /api/relationships returns the (latest) relationship matching a target_ref', async function () { + const res = await request(app) + .get('/api/relationships?targetRef=' + targetRef1) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(1); + }); + + it('GET /api/relationships returns the (latest) relationship matching a sourceOrTargetRef', async function () { + const res = await request(app) + .get('/api/relationships?sourceOrTargetRef=' + sourceRef1) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(1); + }); + + it('GET /api/relationships returns zero relationships for a non-matching source_ref', async function () { + const res = await request(app) + .get('/api/relationships?sourceRef=' + sourceRef3) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get zero relationships in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(0); + }); + + it('GET /api/relationships returns zero relationships for a non-matching target_ref', async function () { + const res = await request(app) + .get('/api/relationships?targetRef=' + targetRef3) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(0); + }); + + it('GET /api/relationships returns zero relationships for a non-matching sourceOrTargetRef', async function () { + const res = await request(app) + .get('/api/relationships?sourceOrTargetRef=' + sourceRef3) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one relationship in an array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(0); + }); + + it('DELETE /api/relationships/:id should not delete a relationship when the id cannot be found', async function () { + await request(app) + .delete('/api/relationships/not-an-id') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('DELETE /api/relationships/:id/modified/:modified deletes a relationship', async function () { + await request(app) + .delete( + '/api/relationships/' + + relationship1a.stix.id + + '/modified/' + + relationship1a.stix.modified, + ) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('DELETE /api/relationships/:id should delete all the relationships with the same stix id', async function () { + await request(app) + .delete('/api/relationships/' + relationship1b.stix.id) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('DELETE /api/relationships should delete the third relationship', async function () { + await request(app) + .delete( + '/api/relationships/' + relationship2.stix.id + '/modified/' + relationship2.stix.modified, + ) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('GET /api/relationships returns an empty array of relationships', async function () { + const res = await request(app) + .get('/api/relationships') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const relationships = res.body; + expect(relationships).toBeDefined(); + expect(Array.isArray(relationships)).toBe(true); + expect(relationships.length).toBe(0); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/api/session/session.spec.js b/app/tests/api/session/session.spec.js index 29bab293..c29eba8f 100644 --- a/app/tests/api/session/session.spec.js +++ b/app/tests/api/session/session.spec.js @@ -8,60 +8,58 @@ const logger = require('../../../lib/logger'); logger.level = 'debug'; describe('Session API', function () { - let app; + let app; - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); - // Initialize the express app - app = await require('../../../index').initializeApp(); - }); + // Initialize the express app + app = await require('../../../index').initializeApp(); + }); - // it('GET /api/session', function (done) { - // request(app) - // .get('/api/session') - // .set('Accept', 'application/json') - // .expect(200) - // .expect('Content-Type', /json/) - // .end(function(err, res) { - // if (err) { - // done(err); - // } - // else { - // // We expect to get the current session - // const session = res.body; - // expect(session).toBeDefined(); - // - // done(); - // } - // }); - // }); + // it('GET /api/session', function (done) { + // request(app) + // .get('/api/session') + // .set('Accept', 'application/json') + // .expect(200) + // .expect('Content-Type', /json/) + // .end(function(err, res) { + // if (err) { + // done(err); + // } + // else { + // // We expect to get the current session + // const session = res.body; + // expect(session).toBeDefined(); + // + // done(); + // } + // }); + // }); - // Temporary change: /api/session returns 401 if the user is not logged in. - // This will be fixed with a general purpose solution for logging in when - // running tests, but is changed to expect the 401 for now. - it('GET /api/session', function (done) { - request(app) - .get('/api/session') - .set('Accept', 'application/json') - .expect(401) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); - }); + // Temporary change: /api/session returns 401 if the user is not logged in. + // This will be fixed with a general purpose solution for logging in when + // running tests, but is changed to expect the 401 for now. + it('GET /api/session', function (done) { + request(app) + .get('/api/session') + .set('Accept', 'application/json') + .expect(401) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); - after(async function() { - await database.closeConnection(); - }); + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/api/software/software-pagination.spec.js b/app/tests/api/software/software-pagination.spec.js index 446242c8..7a92211e 100644 --- a/app/tests/api/software/software-pagination.spec.js +++ b/app/tests/api/software/software-pagination.spec.js @@ -4,42 +4,31 @@ const PaginationTests = require('../../shared/pagination'); // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - spec_version: '2.1', - type: 'malware', - description: 'This is a malware type of software.', - is_family: true, - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - x_mitre_version: "1.1", - x_mitre_aliases: [ - "software-1" - ], - x_mitre_platforms: [ - "platform-1" - ], - x_mitre_contributors: [ - "contributor-1", - "contributor-2" - ], - x_mitre_domains: [ - "mobile-attack" - ] - } + }, + stix: { + spec_version: '2.1', + type: 'malware', + description: 'This is a malware type of software.', + is_family: true, + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + x_mitre_version: '1.1', + x_mitre_aliases: ['software-1'], + x_mitre_platforms: ['platform-1'], + x_mitre_contributors: ['contributor-1', 'contributor-2'], + x_mitre_domains: ['mobile-attack'], + }, }; const options = { - prefix: 'software', - baseUrl: '/api/software', - label: 'Software' -} + prefix: 'software', + baseUrl: '/api/software', + label: 'Software', +}; const paginationTests = new PaginationTests(softwareService, initialObjectData, options); paginationTests.executeTests(); diff --git a/app/tests/api/software/software.spec.js b/app/tests/api/software/software.spec.js index 16f1f77d..5052b621 100644 --- a/app/tests/api/software/software.spec.js +++ b/app/tests/api/software/software.spec.js @@ -14,37 +14,26 @@ logger.level = 'debug'; // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - name: 'software-1', - spec_version: '2.1', - type: 'malware', - description: 'This is a malware type of software.', - is_family: true, - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - x_mitre_version: "1.1", - x_mitre_aliases: [ - "software-1" - ], - x_mitre_platforms: [ - "platform-1" - ], - x_mitre_contributors: [ - "contributor-1", - "contributor-2" - ], - x_mitre_domains: [ - "mobile-attack" - ] - } + }, + stix: { + name: 'software-1', + spec_version: '2.1', + type: 'malware', + description: 'This is a malware type of software.', + is_family: true, + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + x_mitre_version: '1.1', + x_mitre_aliases: ['software-1'], + x_mitre_platforms: ['platform-1'], + x_mitre_contributors: ['contributor-1', 'contributor-2'], + x_mitre_domains: ['mobile-attack'], + }, }; // Software missing required property stix.name @@ -60,366 +49,365 @@ const invalidToolIncludesIsFamily = _.cloneDeep(initialObjectData); invalidToolIncludesIsFamily.stix.type = 'tool'; describe('Software API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - it('GET /api/software returns an empty array of software', async function () { - const res = await request(app) - .get('/api/software') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const software = res.body; - expect(software).toBeDefined(); - expect(Array.isArray(software)).toBe(true); - expect(software.length).toBe(0); - }); - - it('POST /api/software does not create an empty software', async function () { - const body = { }; - await request(app) - .post('/api/software') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - it('POST /api/software does not create a software missing the name property', async function () { - const timestamp = new Date().toISOString(); - invalidMissingName.stix.created = timestamp; - invalidMissingName.stix.modified = timestamp; - const body = invalidMissingName; - await request(app) - .post('/api/software') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - it('POST /api/software does not create a software (tool) with the is_family property', async function () { - const timestamp = new Date().toISOString(); - invalidToolIncludesIsFamily.stix.created = timestamp; - invalidToolIncludesIsFamily.stix.modified = timestamp; - const body = invalidToolIncludesIsFamily; - await request(app) - .post('/api/software') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - let software1; - it('POST /api/software creates a software', async function () { - const timestamp = new Date().toISOString(); - initialObjectData.stix.created = timestamp; - initialObjectData.stix.modified = timestamp; - const body = initialObjectData; - const res = await request(app) - .post('/api/software') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created software - software1 = res.body; - expect(software1).toBeDefined(); - expect(software1.stix).toBeDefined(); - expect(software1.stix.id).toBeDefined(); - expect(software1.stix.created).toBeDefined(); - expect(software1.stix.modified).toBeDefined(); - expect(software1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); - - }); - - it('GET /api/software returns the added software', async function () { - const res = await request(app) - .get('/api/software') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one software in an array - const software = res.body; - expect(software).toBeDefined(); - expect(Array.isArray(software)).toBe(true); - expect(software.length).toBe(1); - }); - - it('GET /api/software/:id should not return a software when the id cannot be found', async function () { - await request(app) - .get('/api/software/not-an-id') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('GET /api/software/:id returns the added software', async function () { - const res = await request(app) - .get('/api/software/' + software1.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one software in an array - const softwareObjects = res.body; - expect(softwareObjects).toBeDefined(); - expect(Array.isArray(softwareObjects)).toBe(true); - expect(softwareObjects.length).toBe(1); - - const software= softwareObjects[0]; - expect(software).toBeDefined(); - expect(software.stix).toBeDefined(); - expect(software.stix.id).toBe(software1.stix.id); - expect(software.stix.type).toBe(software1.stix.type); - expect(software.stix.name).toBe(software1.stix.name); - expect(software.stix.description).toBe(software1.stix.description); - expect(software.stix.is_family).toBe(software1.stix.is_family); - expect(software.stix.spec_version).toBe(software1.stix.spec_version); - expect(software.stix.object_marking_refs).toEqual(expect.arrayContaining(software1.stix.object_marking_refs)); - expect(software.stix.created_by_ref).toBe(software1.stix.created_by_ref); - expect(software.stix.x_mitre_version).toBe(software1.stix.x_mitre_version); - expect(software.stix.x_mitre_aliases).toEqual(expect.arrayContaining(software1.stix.x_mitre_aliases)); - expect(software.stix.x_mitre_platforms).toEqual(expect.arrayContaining(software1.stix.x_mitre_platforms)); - expect(software.stix.x_mitre_contributors).toEqual(expect.arrayContaining(software1.stix.x_mitre_contributors)); - expect(software.stix.x_mitre_attack_spec_version).toBe(software1.stix.x_mitre_attack_spec_version); - - }); - - it('PUT /api/software updates a software', async function () { - const originalModified = software1.stix.modified; - const timestamp = new Date().toISOString(); - software1.stix.modified = timestamp; - software1.stix.description = 'This is an updated software.' - const body = software1; - const res = await request(app) - .put('/api/software/' + software1.stix.id + '/modified/' + originalModified) - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the updated software - const software = res.body; - expect(software).toBeDefined(); - expect(software.stix.id).toBe(software1.stix.id); - expect(software.stix.modified).toBe(software1.stix.modified); - - }); - - it('POST /api/software does not create a software with the same id and modified date', async function () { - const body = software1; - await request(app) - .post('/api/software') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(409); - }); - - let software2; - it('POST /api/software should create a new version of a software with a duplicate stix.id but different stix.modified date', async function () { - software2 = _.cloneDeep(software1); - software2._id = undefined; - software2.__t = undefined; - software2.__v = undefined; - const timestamp = new Date().toISOString(); - software2.stix.modified = timestamp; - const body = software2; - const res = await request(app) - .post('/api/software') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created software - const software = res.body; - expect(software).toBeDefined(); - - }); - - it('GET /api/software returns the latest added software', async function () { - const res = await request(app) - .get('/api/software/' + software2.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one software in an array - const software = res.body; - expect(software).toBeDefined(); - expect(Array.isArray(software)).toBe(true); - expect(software.length).toBe(1); - const softwre = software[0]; - expect(softwre.stix.id).toBe(software2.stix.id); - expect(softwre.stix.modified).toBe(software2.stix.modified); - }); - - it('GET /api/software returns all added software', async function () { - const res = await request(app) - .get('/api/software/' + software1.stix.id + '?versions=all') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get two software in an array - const software = res.body; - expect(software).toBeDefined(); - expect(Array.isArray(software)).toBe(true); - expect(software.length).toBe(2); - - }); - - it('GET /api/software/:id/modified/:modified returns the first added software', async function () { - const res = await request(app) - .get('/api/software/' + software1.stix.id + '/modified/' + software1.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one software in an array - const software = res.body; - expect(software).toBeDefined(); - expect(software.stix).toBeDefined(); - expect(software.stix.id).toBe(software1.stix.id); - expect(software.stix.modified).toBe(software1.stix.modified); - - }); - - it('GET /api/software/:id/modified/:modified returns the second added software', async function () { - const res = await request(app) - .get('/api/software/' + software2.stix.id + '/modified/' + software2.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one software in an array - const software = res.body; - expect(software).toBeDefined(); - expect(software.stix).toBeDefined(); - expect(software.stix.id).toBe(software2.stix.id); - expect(software.stix.modified).toBe(software2.stix.modified); - - }); - - let software3; - it('POST /api/software should create a new version of a software with a duplicate stix.id but different stix.modified date', async function () { - software3 = _.cloneDeep(software1); - software3._id = undefined; - software3.__t = undefined; - software3.__v = undefined; - const timestamp = new Date().toISOString(); - software3.stix.modified = timestamp; - const body = software3; - const res = await request(app) - .post('/api/software') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created software - const software = res.body; - expect(software).toBeDefined(); - - }); - - it('DELETE /api/software/:id should not delete a software when the id cannot be found', async function () { - await request(app) - .delete('/api/software/not-an-id') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('DELETE /api/software/:id/modified/:modified deletes a software', async function () { - await request(app) - .delete('/api/software/' + software1.stix.id + '/modified/' + software1.stix.modified) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - }); - - it('DELETE /api/software/:id should delete all the software with the same stix id', async function () { - await request(app) - .delete('/api/software/' + software2.stix.id) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - }); - - it('GET /api/software returns an empty array of software', async function () { - const res = await request(app) - .get('/api/software') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const software = res.body; - expect(software).toBeDefined(); - expect(Array.isArray(software)).toBe(true); - expect(software.length).toBe(0); - - }); - - it('POST /api/software creates a software (malware) missing the is_family property using a default value', async function () { - const timestamp = new Date().toISOString(); - invalidMalwareMissingIsFamily.stix.created = timestamp; - invalidMalwareMissingIsFamily.stix.modified = timestamp; - const body = invalidMalwareMissingIsFamily; - const res = await request(app) - .post('/api/software') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created software - const malware = res.body; - expect(malware).toBeDefined(); - expect(malware.stix).toBeDefined(); - expect(malware.stix.id).toBeDefined(); - expect(malware.stix.created).toBeDefined(); - expect(malware.stix.modified).toBeDefined(); - expect(typeof malware.stix.is_family).toBe('boolean'); - expect(malware.stix.is_family).toBe(true); - - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + it('GET /api/software returns an empty array of software', async function () { + const res = await request(app) + .get('/api/software') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const software = res.body; + expect(software).toBeDefined(); + expect(Array.isArray(software)).toBe(true); + expect(software.length).toBe(0); + }); + + it('POST /api/software does not create an empty software', async function () { + const body = {}; + await request(app) + .post('/api/software') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + it('POST /api/software does not create a software missing the name property', async function () { + const timestamp = new Date().toISOString(); + invalidMissingName.stix.created = timestamp; + invalidMissingName.stix.modified = timestamp; + const body = invalidMissingName; + await request(app) + .post('/api/software') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + it('POST /api/software does not create a software (tool) with the is_family property', async function () { + const timestamp = new Date().toISOString(); + invalidToolIncludesIsFamily.stix.created = timestamp; + invalidToolIncludesIsFamily.stix.modified = timestamp; + const body = invalidToolIncludesIsFamily; + await request(app) + .post('/api/software') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + let software1; + it('POST /api/software creates a software', async function () { + const timestamp = new Date().toISOString(); + initialObjectData.stix.created = timestamp; + initialObjectData.stix.modified = timestamp; + const body = initialObjectData; + const res = await request(app) + .post('/api/software') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created software + software1 = res.body; + expect(software1).toBeDefined(); + expect(software1.stix).toBeDefined(); + expect(software1.stix.id).toBeDefined(); + expect(software1.stix.created).toBeDefined(); + expect(software1.stix.modified).toBeDefined(); + expect(software1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); + }); + + it('GET /api/software returns the added software', async function () { + const res = await request(app) + .get('/api/software') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one software in an array + const software = res.body; + expect(software).toBeDefined(); + expect(Array.isArray(software)).toBe(true); + expect(software.length).toBe(1); + }); + + it('GET /api/software/:id should not return a software when the id cannot be found', async function () { + await request(app) + .get('/api/software/not-an-id') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('GET /api/software/:id returns the added software', async function () { + const res = await request(app) + .get('/api/software/' + software1.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one software in an array + const softwareObjects = res.body; + expect(softwareObjects).toBeDefined(); + expect(Array.isArray(softwareObjects)).toBe(true); + expect(softwareObjects.length).toBe(1); + + const software = softwareObjects[0]; + expect(software).toBeDefined(); + expect(software.stix).toBeDefined(); + expect(software.stix.id).toBe(software1.stix.id); + expect(software.stix.type).toBe(software1.stix.type); + expect(software.stix.name).toBe(software1.stix.name); + expect(software.stix.description).toBe(software1.stix.description); + expect(software.stix.is_family).toBe(software1.stix.is_family); + expect(software.stix.spec_version).toBe(software1.stix.spec_version); + expect(software.stix.object_marking_refs).toEqual( + expect.arrayContaining(software1.stix.object_marking_refs), + ); + expect(software.stix.created_by_ref).toBe(software1.stix.created_by_ref); + expect(software.stix.x_mitre_version).toBe(software1.stix.x_mitre_version); + expect(software.stix.x_mitre_aliases).toEqual( + expect.arrayContaining(software1.stix.x_mitre_aliases), + ); + expect(software.stix.x_mitre_platforms).toEqual( + expect.arrayContaining(software1.stix.x_mitre_platforms), + ); + expect(software.stix.x_mitre_contributors).toEqual( + expect.arrayContaining(software1.stix.x_mitre_contributors), + ); + expect(software.stix.x_mitre_attack_spec_version).toBe( + software1.stix.x_mitre_attack_spec_version, + ); + }); + + it('PUT /api/software updates a software', async function () { + const originalModified = software1.stix.modified; + const timestamp = new Date().toISOString(); + software1.stix.modified = timestamp; + software1.stix.description = 'This is an updated software.'; + const body = software1; + const res = await request(app) + .put('/api/software/' + software1.stix.id + '/modified/' + originalModified) + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the updated software + const software = res.body; + expect(software).toBeDefined(); + expect(software.stix.id).toBe(software1.stix.id); + expect(software.stix.modified).toBe(software1.stix.modified); + }); + + it('POST /api/software does not create a software with the same id and modified date', async function () { + const body = software1; + await request(app) + .post('/api/software') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(409); + }); + + let software2; + it('POST /api/software should create a new version of a software with a duplicate stix.id but different stix.modified date', async function () { + software2 = _.cloneDeep(software1); + software2._id = undefined; + software2.__t = undefined; + software2.__v = undefined; + const timestamp = new Date().toISOString(); + software2.stix.modified = timestamp; + const body = software2; + const res = await request(app) + .post('/api/software') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created software + const software = res.body; + expect(software).toBeDefined(); + }); + + it('GET /api/software returns the latest added software', async function () { + const res = await request(app) + .get('/api/software/' + software2.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one software in an array + const software = res.body; + expect(software).toBeDefined(); + expect(Array.isArray(software)).toBe(true); + expect(software.length).toBe(1); + const softwre = software[0]; + expect(softwre.stix.id).toBe(software2.stix.id); + expect(softwre.stix.modified).toBe(software2.stix.modified); + }); + + it('GET /api/software returns all added software', async function () { + const res = await request(app) + .get('/api/software/' + software1.stix.id + '?versions=all') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get two software in an array + const software = res.body; + expect(software).toBeDefined(); + expect(Array.isArray(software)).toBe(true); + expect(software.length).toBe(2); + }); + + it('GET /api/software/:id/modified/:modified returns the first added software', async function () { + const res = await request(app) + .get('/api/software/' + software1.stix.id + '/modified/' + software1.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one software in an array + const software = res.body; + expect(software).toBeDefined(); + expect(software.stix).toBeDefined(); + expect(software.stix.id).toBe(software1.stix.id); + expect(software.stix.modified).toBe(software1.stix.modified); + }); + + it('GET /api/software/:id/modified/:modified returns the second added software', async function () { + const res = await request(app) + .get('/api/software/' + software2.stix.id + '/modified/' + software2.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one software in an array + const software = res.body; + expect(software).toBeDefined(); + expect(software.stix).toBeDefined(); + expect(software.stix.id).toBe(software2.stix.id); + expect(software.stix.modified).toBe(software2.stix.modified); + }); + + let software3; + it('POST /api/software should create a new version of a software with a duplicate stix.id but different stix.modified date', async function () { + software3 = _.cloneDeep(software1); + software3._id = undefined; + software3.__t = undefined; + software3.__v = undefined; + const timestamp = new Date().toISOString(); + software3.stix.modified = timestamp; + const body = software3; + const res = await request(app) + .post('/api/software') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created software + const software = res.body; + expect(software).toBeDefined(); + }); + + it('DELETE /api/software/:id should not delete a software when the id cannot be found', async function () { + await request(app) + .delete('/api/software/not-an-id') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('DELETE /api/software/:id/modified/:modified deletes a software', async function () { + await request(app) + .delete('/api/software/' + software1.stix.id + '/modified/' + software1.stix.modified) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('DELETE /api/software/:id should delete all the software with the same stix id', async function () { + await request(app) + .delete('/api/software/' + software2.stix.id) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('GET /api/software returns an empty array of software', async function () { + const res = await request(app) + .get('/api/software') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const software = res.body; + expect(software).toBeDefined(); + expect(Array.isArray(software)).toBe(true); + expect(software.length).toBe(0); + }); + + it('POST /api/software creates a software (malware) missing the is_family property using a default value', async function () { + const timestamp = new Date().toISOString(); + invalidMalwareMissingIsFamily.stix.created = timestamp; + invalidMalwareMissingIsFamily.stix.modified = timestamp; + const body = invalidMalwareMissingIsFamily; + const res = await request(app) + .post('/api/software') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created software + const malware = res.body; + expect(malware).toBeDefined(); + expect(malware.stix).toBeDefined(); + expect(malware.stix.id).toBeDefined(); + expect(malware.stix.created).toBeDefined(); + expect(malware.stix.modified).toBeDefined(); + expect(typeof malware.stix.is_family).toBe('boolean'); + expect(malware.stix.is_family).toBe(true); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/api/stix-bundles/stix-bundles.spec.js b/app/tests/api/stix-bundles/stix-bundles.spec.js index 5dc9000d..2b0624b1 100644 --- a/app/tests/api/stix-bundles/stix-bundles.spec.js +++ b/app/tests/api/stix-bundles/stix-bundles.spec.js @@ -16,655 +16,614 @@ const collectionId = 'x-mitre-collection--30ee11cf-0a05-4d9e-ab54-9b8563669647'; const collectionTimestamp = new Date().toISOString(); const initialObjectData = { - type: 'bundle', - id: 'bundle--0cde353c-ea5b-4668-9f68-971946609282', - spec_version: '2.1', - objects: [ - { - id: collectionId, - created: collectionTimestamp, - modified: collectionTimestamp, - name: 'collection-1', - spec_version: '2.1', - type: 'x-mitre-collection', - description: 'This is a collection.', - external_references: [], - object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - x_mitre_contents: [ - { - "object_ref": "attack-pattern--2204c371-6100-4ae0-82f3-25c07c29772a", - "object_modified": "2020-03-30T14:03:43.761Z" - }, - { - "object_ref": "attack-pattern--1eaebf46-e361-4437-bc23-d5d65a3b92e3", - "object_modified": "2020-02-17T13:14:31.140Z", - }, - { - "object_ref": "attack-pattern--82f04b1e-5371-4a6f-be06-411f0f43b483", - "object_modified": "2019-02-03T16:56:41.200Z" - }, - { - "object_ref": "attack-pattern--2bb2861b-fb40-42dc-b15f-1a6b64b6a39f", - "object_modified": "2019-02-03T16:56:41.200Z" - }, - { - "object_ref": "course-of-action--25dc1ce8-eb55-4333-ae30-a7cb4f5894a1", - "object_modified": "2018-10-17T00:14:20.652Z" - }, - { - "object_ref": "course-of-action--e944670c-d03a-4e93-a21c-b3d4c53ec4c9", - "object_modified": "2018-10-17T00:14:20.652Z" - }, - { - "object_ref": "malware--04227b24-7817-4de1-9050-b7b1b57f5866", - "object_modified": "2020-03-30T18:17:52.697Z" - }, - { - "object_ref": "intrusion-set--8a831aaa-f3e0-47a3-bed8-a9ced744dd12", - "object_modified": "2020-06-03T20:22:40.401Z" - }, - { - "object_ref": "intrusion-set--ed0222fb-b970-4337-b9a2-62aeb02860e5", - "object_modified": "2023-05-02T20:19:40.401Z" - }, - { - "object_ref": "relationship--12098dee-27b3-4d0b-a15a-6b5955ba8879", - "object_modified": "2019-09-04T14:32:13.000Z" - }, - { - "object_ref": "intrusion-set--6b9ebeb5-20bf-48b0-afb7-988d769a2f01", - "object_modified": "2020-05-15T15:44:47.629Z" - }, - { - "object_ref": "campaign--a3038910-f8ca-4ba8-b116-21d0f333f231", - "object_modified": "2020-07-03T20:22:40.401Z" - }, - { - "object_ref": "campaign--649b389e-1f7a-4696-8a95-04d0851bd551", - "object_modified": "2020-07-03T20:22:40.401Z" - }, - { - "object_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - "object_modified": "2017-06-01T00:00:00.000Z" - }, - { - "object_ref": "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168", - "object_modified": "2017-06-01T00:00:00Z" - }, - { - "object_ref": "note--6b9456275-20bf-48b0-afb7-988d769a2f99", - "object_modified": "2020-04-12T15:44:47.629Z" - }, - { - "object_ref": "x-mitre-data-source--880b771b-17a8-4a6c-a259-9027c395010c", - "object_modified": "2020-04-12T15:44:47.629Z" - }, - { - "object_ref": "x-mitre-data-source--3e396a50-dd74-45cf-b8a3-974ab80c9a3e", - "object_modified": "2020-04-12T15:44:47.629Z" - }, - { - "object_ref": "x-mitre-data-component--47667153-e24d-4514-bdf4-5720312d9e7d", - "object_modified": "2020-04-12T15:44:47.629Z" - }, - { - "object_ref": "x-mitre-data-component--f8b4833e-a6d4-4a05-ba6e-1936d4109d0a", - "object_modified": "2020-04-12T15:44:47.629Z" - }, - { - "object_ref": "relationship--caa8928b-0bf6-45cd-8504-6c27b9cd96a8", - "object_modified": "2019-09-04T14:32:13.000Z" - }, - { - "object_ref": "relationship--b0c6c76c-7699-447f-9f3f-573aec51431c", - "object_modified": "2019-09-04T14:32:13.000Z" - }, - { - "object_ref": "relationship--e7f994c6-3e08-4aea-a30e-97cc6fe610c6", - "object_modified": "2019-09-04T14:32:13.000Z" - }, - { - "object_ref": "relationship--89586929-ca62-423f-94bf-cc03ec8161bb", - "object_modified": "2021-06-06T14:00:00.000Z" - }, - { - "object_ref": "relationship--21d3572e-398d-4473-93eb-eb9a2a069d53", - "object_modified": "2021-06-07T14:00:00.000Z" - }, - { - "object_ref": "relationship--a5a80c31-0dde-4fd7-a520-a7593d21c954", - "object_modified": "2021-06-08T14:00:00.000Z" - }, - { - "object_ref": "relationship--1e1c5e5a-2a3e-423f-b1d0-67b7dc5b90cc", - "object_modified": "2023-06-08T14:00:00.000Z" - }, - { - "object_ref": "relationship--d5426745-9530-485e-a757-d8c540f600f8", - "object_modified": "2021-06-06T14:00:00.000Z" - }, - ] - }, - { - id: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - name: "The MITRE Corporation", - identity_class: "organization", - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - type: "identity", - modified: "2017-06-01T00:00:00.000Z", - created: "2017-06-01T00:00:00.000Z", - spec_version: '2.1' - }, - { - type: "marking-definition", - id: "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168", - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - created: "2017-06-01T00:00:00Z", - definition_type: "statement", - definition: { - statement: "Copyright 2015-2021, The MITRE Corporation. MITRE ATT&CK and ATT&CK are registered trademarks of The MITRE Corporation." - }, - spec_version: '2.1' - }, - { - id: 'attack-pattern--2204c371-6100-4ae0-82f3-25c07c29772a', - created: '2020-03-30T14:03:43.761Z', - modified: '2020-03-30T14:03:43.761Z', - name: 'attack-pattern-1', - x_mitre_version: '1.0', - spec_version: '2.1', - type: 'attack-pattern', - description: 'This is a technique.', - external_references: [ - { source_name: 'mitre-attack', external_id: 'T1' }, - { source_name: 'attack-pattern-1 source', description: 'this is a source description'} - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - kill_chain_phases: [ - { kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' } - ], - x_mitre_data_sources: [ 'Command: Command Execution', 'Network Traffic: Network Traffic Flow' ], - x_mitre_detection: 'detection text', - x_mitre_is_subtechnique: false, - x_mitre_impact_type: [ 'impact-1' ], - x_mitre_platforms: [ 'platform-1', 'platform-2' ], - x_mitre_domains: [ enterpriseDomain ] - }, - { - id: 'attack-pattern--1eaebf46-e361-4437-bc23-d5d65a3b92e3', - created: '2020-02-12T18:55:24.728Z', - modified: '2020-02-17T13:14:31.140Z', - name: 'attack-pattern-2', - x_mitre_version: '1.0', - spec_version: '2.1', - type: 'attack-pattern', - description: 'This is a technique.', - external_references: [ - { source_name: 'mitre-attack', external_id: 'T1' }, - { source_name: 'attack-pattern-1 source', description: 'this is a source description'} - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - kill_chain_phases: [ - { kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' } - ], - x_mitre_detection: 'detection text', - x_mitre_is_subtechnique: false, - x_mitre_impact_type: [ 'impact-1' ], - x_mitre_platforms: [ 'platform-1', 'platform-2' ], - x_mitre_domains: [ mobileDomain ] - }, - { - id: 'attack-pattern--82f04b1e-5371-4a6f-be06-411f0f43b483', - created: '2019-02-03T16:56:41.200Z', - modified: '2019-02-03T16:56:41.200Z', - name: 'attack-pattern-3', - x_mitre_version: '1.0', - spec_version: '2.1', - type: 'attack-pattern', - description: 'This is another technique.', - external_references: [ - { source_name: 'mitre-attack', external_id: 'T1' }, - { source_name: 'attack-pattern-2 source', description: 'this is a source description 2'} - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - kill_chain_phases: [ - { kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' } - ], - x_mitre_data_sources: [ 'Operational Databases: Device Alarm', 'Network Traffic: Network Traffic Flow' ], - x_mitre_detection: 'detection text', - x_mitre_is_subtechnique: false, - x_mitre_impact_type: [ 'impact-1' ], - x_mitre_platforms: [ 'platform-1', 'platform-2' ], - x_mitre_domains: [ icsDomain ] - }, - { - id: 'attack-pattern--2bb2861b-fb40-42dc-b15f-1a6b64b6a39f', - created: '2019-02-03T16:56:41.200Z', - modified: '2019-02-03T16:56:41.200Z', - name: 'attack-pattern-4', - x_mitre_version: '1.0', - spec_version: '2.1', - type: 'attack-pattern', - description: 'This is another technique.', - external_references: [ - { source_name: 'mitre-attack', external_id: 'T1' }, - { source_name: 'attack-pattern-2 source', description: 'this is a source description 2'} - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - kill_chain_phases: [ - { kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' } - ], - x_mitre_data_sources: [ 'Command: Command Execution', 'Operational Databases: Device Alarm', 'Network Traffic: Network Traffic Flow' ], - x_mitre_detection: 'detection text', - x_mitre_is_subtechnique: false, - x_mitre_impact_type: [ 'impact-1' ], - x_mitre_platforms: [ 'platform-1', 'platform-2' ], - x_mitre_domains: [ enterpriseDomain, icsDomain ] - }, - { - id: "course-of-action--25dc1ce8-eb55-4333-ae30-a7cb4f5894a1", - type: "course-of-action", - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - name: "mitigation-1", - description: "This is a mitigation", - external_references: [ - { source_name: 'mitre-attack', external_id: 'M1' } - ], - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - x_mitre_version: "1.0", - modified: "2018-10-17T00:14:20.652Z", - created: "2017-10-25T14:48:53.732Z", - spec_version: "2.1", - x_mitre_domains: [ enterpriseDomain ] - }, - { - id: "course-of-action--e944670c-d03a-4e93-a21c-b3d4c53ec4c9", - type: "course-of-action", - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - name: "mitigation-2", - description: "This is a mitigation that isn't in the contents", - external_references: [ - { source_name: 'mitre-attack', external_id: 'M1' } - ], - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - x_mitre_version: "1.0", - modified: "2018-10-17T00:14:20.652Z", - created: "2017-10-25T14:48:53.732Z", - spec_version: "2.1", - x_mitre_domains: [ enterpriseDomain ], - x_mitre_deprecated: true - }, - { - id: "malware--04227b24-7817-4de1-9050-b7b1b57f5866", - type: "malware", - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - name: "software-1", - description: "This is a software with an alias", - external_references: [ - { source_name: 'mitre-attack', external_id: 'S1' }, - { source_name: 'malware-1 source', description: 'this is a source description'}, - { source_name: 'xyzzy', description: '(Citation: Adventure 1975)'} - ], - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - x_mitre_version: "1.0", - modified: "2020-03-30T18:17:52.697Z", - created: "2017-10-25T14:48:53.732Z", - spec_version: "2.1", - x_mitre_domains: [ enterpriseDomain ], - x_mitre_aliases: [ "xyzzy" ] - }, - { - type: "intrusion-set", - id: "intrusion-set--8a831aaa-f3e0-47a3-bed8-a9ced744dd12", - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - name: "Dark Caracal", - description: "[Dark Caracal](https://attack.mitre.org/groups/G0070) is threat group that has been attributed to the Lebanese General Directorate of General Security (GDGS) and has operated since at least 2012. (Citation: Lookout Dark Caracal Jan 2018)", - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - external_references: [ - { - source_name: "mitre-attack", - url: "https://attack.mitre.org/groups/G0070", - external_id: "G0070" - }, - { - source_name: "Dark Caracal", - description: "(Citation: Lookout Dark Caracal Jan 2018)" - }, - { - url: "https://info.lookout.com/rs/051-ESQ-475/images/Lookout_Dark-Caracal_srr_20180118_us_v.1.0.pdf", - description: "Blaich, A., et al. (2018, January 18). Dark Caracal: Cyber-espionage at a Global Scale. Retrieved April 11, 2018.", - source_name: "Lookout Dark Caracal Jan 2018" - } - ], - aliases: [ - "Dark Caracal" - ], - modified: "2020-06-03T20:22:40.401Z", - created: "2018-10-17T00:14:20.652Z", - spec_version: "2.1", - x_mitre_version: "1.2" - }, - { - type: "intrusion-set", - id: "intrusion-set--ed0222fb-b970-4337-b9a2-62aeb02860e5", - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - name: "Another group", - description: "This is another group. It isn't referenced by a technique, but is associated with a campaign", - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - external_references: [ - { - source_name: "mitre-attack", - url: "https://attack.mitre.org/groups/G0999", - external_id: "G0999" - } - ], - aliases: [ - "Some group alias" - ], - modified: "2023-05-02T20:19:40.401Z", - created: "2018-10-17T00:19:20.652Z", - spec_version: "2.1", - x_mitre_version: "1.2" - }, - { - type: "campaign", - id: "campaign--a3038910-f8ca-4ba8-b116-21d0f333f231", - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - name: "campaign-1", - description: "This is a campaign", - first_seen: "2016-04-06T00:00:00.000Z", - last_seen: "2016-07-12T00:00:00.000Z", - x_mitre_first_seen_citation: "(Citation: Article 1)", - x_mitre_last_seen_citation: "(Citation: Article 2)", - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - external_references: [ - { - source_name: "mitre-attack", - url: "https://attack.mitre.org/campaigns/C0001", - external_id: "C0001" - } - ], - aliases: [ - "Another campaign name" - ], - modified: "2020-07-03T20:22:40.401Z", - created: "2018-11-17T00:14:20.652Z", - spec_version: "2.1", - x_mitre_version: "1.2" - }, - { - type: "campaign", - id: "campaign--649b389e-1f7a-4696-8a95-04d0851bd551", - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - name: "campaign-2", - description: "This is another campaign", - first_seen: "2016-04-06T00:00:00.000Z", - last_seen: "2016-07-12T00:00:00.000Z", - x_mitre_first_seen_citation: "(Citation: Article 1)", - x_mitre_last_seen_citation: "(Citation: Article 2)", - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - external_references: [ - { - source_name: "mitre-attack", - url: "https://attack.mitre.org/campaigns/C0002", - external_id: "C0002" - } - ], - aliases: [ - "Another campaign name" - ], - modified: "2020-07-03T20:22:40.401Z", - created: "2018-11-17T00:14:20.652Z", - spec_version: "2.1", - x_mitre_version: "1.2" - }, - { - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - source_ref: "intrusion-set--8a831aaa-f3e0-47a3-bed8-a9ced744dd12", - target_ref: "attack-pattern--2204c371-6100-4ae0-82f3-25c07c29772a", - external_references: [], - description: "Test relationship", - relationship_type: "uses", - id: "relationship--12098dee-27b3-4d0b-a15a-6b5955ba8879", - type: "relationship", - modified: "2019-09-04T14:32:13.000Z", - created: "2019-09-04T14:28:16.426Z", - spec_version: "2.1" - }, - { - external_references: [], - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - description: "This is a group that isn't in the domain", - name: "Dark Hydra", - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - id: "intrusion-set--6b9ebeb5-20bf-48b0-afb7-988d769a2f01", - type: "intrusion-set", - aliases: [ - "Hydra" - ], - modified: "2020-05-15T15:44:47.629Z", - created: "2018-10-17T00:14:20.652Z", - x_mitre_version: "1.2", - spec_version: "2.1" - }, - { - type: 'note', - id: 'note--6b9456275-20bf-48b0-afb7-988d769a2f99', - spec_version: '2.1', - abstract: 'This is the abstract for a note.', - content: 'This is the content for a note.', - authors: [ - 'Author 1', - 'Author 2' - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - object_refs: [ 'malware--04227b24-7817-4de1-9050-b7b1b57f5866' ], - modified: "2020-04-12T15:44:47.629Z", - created: "2019-10-22T00:14:20.652Z" - }, - { - type: 'x-mitre-data-source', - id: 'x-mitre-data-source--880b771b-17a8-4a6c-a259-9027c395010c', - name: 'Command', - spec_version: '2.1', - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - modified: "2020-04-12T15:44:47.629Z", - created: "2019-10-22T00:14:20.652Z", - external_references: [ - {source_name: 'mitre-attack', external_id: 'DS1'} - ], - }, - { - type: 'x-mitre-data-source', - id: 'x-mitre-data-source--3e396a50-dd74-45cf-b8a3-974ab80c9a3e', - name: 'Network Traffic', - spec_version: '2.1', - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - modified: "2020-04-12T15:44:47.629Z", - created: "2019-10-22T00:14:20.652Z", - external_references: [ - {source_name: 'mitre-attack', external_id: 'DS1'} - ], - }, - { - type: 'x-mitre-data-component', - id: 'x-mitre-data-component--47667153-e24d-4514-bdf4-5720312d9e7d', - name: 'Command Execution', - spec_version: '2.1', - x_mitre_data_source_ref: 'x-mitre-data-source--880b771b-17a8-4a6c-a259-9027c395010c', - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - modified: "2020-04-12T15:44:47.629Z", - created: "2019-10-22T00:14:20.652Z" - }, - { - type: 'x-mitre-data-component', - id: 'x-mitre-data-component--f8b4833e-a6d4-4a05-ba6e-1936d4109d0a', - name: 'Network Traffic Flow', - spec_version: '2.1', - x_mitre_data_source_ref: 'x-mitre-data-source--3e396a50-dd74-45cf-b8a3-974ab80c9a3e', - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - modified: "2020-04-12T15:44:47.629Z", - created: "2019-10-22T00:14:20.652Z" - }, - { - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - source_ref: "x-mitre-data-component--47667153-e24d-4514-bdf4-5720312d9e7d", - target_ref: "attack-pattern--2204c371-6100-4ae0-82f3-25c07c29772a", - external_references: [], - description: "Detects relationship", - relationship_type: "detects", - id: "relationship--caa8928b-0bf6-45cd-8504-6c27b9cd96a8", - type: "relationship", - modified: "2019-09-04T14:32:13.000Z", - created: "2019-09-04T14:28:16.426Z", - spec_version: "2.1" - }, - { - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - source_ref: "x-mitre-data-component--47667153-e24d-4514-bdf4-5720312d9e7d", - target_ref: "attack-pattern--2bb2861b-fb40-42dc-b15f-1a6b64b6a39f", - external_references: [], - description: "Detects relationship", - relationship_type: "detects", - id: "relationship--e7f994c6-3e08-4aea-a30e-97cc6fe610c6", - type: "relationship", - modified: "2019-09-04T14:32:13.000Z", - created: "2019-09-04T14:28:16.426Z", - spec_version: "2.1" - }, - { - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - source_ref: "x-mitre-data-component--f8b4833e-a6d4-4a05-ba6e-1936d4109d0a", - target_ref: "attack-pattern--2204c371-6100-4ae0-82f3-25c07c29772a", - external_references: [], - description: "Test relationship", - relationship_type: "detects", - id: "relationship--b0c6c76c-7699-447f-9f3f-573aec51431c", - type: "relationship", - modified: "2019-09-04T14:32:13.000Z", - created: "2019-09-04T14:28:16.426Z", - spec_version: "2.1" - }, - { - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - source_ref: "campaign--a3038910-f8ca-4ba8-b116-21d0f333f231", - target_ref: "attack-pattern--2bb2861b-fb40-42dc-b15f-1a6b64b6a39f", - external_references: [], - description: "Campaign uses technique", - relationship_type: "uses", - id: "relationship--89586929-ca62-423f-94bf-cc03ec8161bb", - type: "relationship", - modified: "2021-06-06T14:00:00.000Z", - created: "2021-06-06T14:00:00.000Z", - spec_version: "2.1" - }, - { - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - source_ref: "campaign--649b389e-1f7a-4696-8a95-04d0851bd551", - target_ref: "attack-pattern--82f04b1e-5371-4a6f-be06-411f0f43b483", - external_references: [], - description: "Campaign uses technique", - relationship_type: "uses", - id: "relationship--d5426745-9530-485e-a757-d8c540f600f8", - type: "relationship", - modified: "2021-06-06T14:00:00.000Z", - created: "2021-06-06T14:00:00.000Z", - spec_version: "2.1" - }, { - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - source_ref: "campaign--a3038910-f8ca-4ba8-b116-21d0f333f231", - target_ref: "malware--04227b24-7817-4de1-9050-b7b1b57f5866", - external_references: [], - description: "Campaign uses software", - relationship_type: "uses", - id: "relationship--21d3572e-398d-4473-93eb-eb9a2a069d53", - type: "relationship", - modified: "2021-06-07T14:00:00.000Z", - created: "2021-06-07T14:00:00.000Z", - spec_version: "2.1" - }, - { - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - source_ref: "campaign--a3038910-f8ca-4ba8-b116-21d0f333f231", - target_ref: "intrusion-set--8a831aaa-f3e0-47a3-bed8-a9ced744dd12", - external_references: [], - description: "Campaign attributed to group", - relationship_type: "attributed-to", - id: "relationship--a5a80c31-0dde-4fd7-a520-a7593d21c954", - type: "relationship", - modified: "2021-06-08T14:00:00.000Z", - created: "2021-06-08T14:00:00.000Z", - spec_version: "2.1" - }, - { - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - object_marking_refs: [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], - source_ref: "campaign--649b389e-1f7a-4696-8a95-04d0851bd551", - target_ref: "intrusion-set--ed0222fb-b970-4337-b9a2-62aeb02860e5", - external_references: [], - description: "Campaign attributed to group and is the only reference to the group", - relationship_type: "attributed-to", - id: "relationship--1e1c5e5a-2a3e-423f-b1d0-67b7dc5b90cc", - type: "relationship", - modified: "2023-06-08T14:00:00.000Z", - created: "2023-06-08T14:00:00.000Z", - spec_version: "2.1" - } - ] + type: 'bundle', + id: 'bundle--0cde353c-ea5b-4668-9f68-971946609282', + spec_version: '2.1', + objects: [ + { + id: collectionId, + created: collectionTimestamp, + modified: collectionTimestamp, + name: 'collection-1', + spec_version: '2.1', + type: 'x-mitre-collection', + description: 'This is a collection.', + external_references: [], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + x_mitre_contents: [ + { + object_ref: 'attack-pattern--2204c371-6100-4ae0-82f3-25c07c29772a', + object_modified: '2020-03-30T14:03:43.761Z', + }, + { + object_ref: 'attack-pattern--1eaebf46-e361-4437-bc23-d5d65a3b92e3', + object_modified: '2020-02-17T13:14:31.140Z', + }, + { + object_ref: 'attack-pattern--82f04b1e-5371-4a6f-be06-411f0f43b483', + object_modified: '2019-02-03T16:56:41.200Z', + }, + { + object_ref: 'attack-pattern--2bb2861b-fb40-42dc-b15f-1a6b64b6a39f', + object_modified: '2019-02-03T16:56:41.200Z', + }, + { + object_ref: 'course-of-action--25dc1ce8-eb55-4333-ae30-a7cb4f5894a1', + object_modified: '2018-10-17T00:14:20.652Z', + }, + { + object_ref: 'course-of-action--e944670c-d03a-4e93-a21c-b3d4c53ec4c9', + object_modified: '2018-10-17T00:14:20.652Z', + }, + { + object_ref: 'malware--04227b24-7817-4de1-9050-b7b1b57f5866', + object_modified: '2020-03-30T18:17:52.697Z', + }, + { + object_ref: 'intrusion-set--8a831aaa-f3e0-47a3-bed8-a9ced744dd12', + object_modified: '2020-06-03T20:22:40.401Z', + }, + { + object_ref: 'intrusion-set--ed0222fb-b970-4337-b9a2-62aeb02860e5', + object_modified: '2023-05-02T20:19:40.401Z', + }, + { + object_ref: 'relationship--12098dee-27b3-4d0b-a15a-6b5955ba8879', + object_modified: '2019-09-04T14:32:13.000Z', + }, + { + object_ref: 'intrusion-set--6b9ebeb5-20bf-48b0-afb7-988d769a2f01', + object_modified: '2020-05-15T15:44:47.629Z', + }, + { + object_ref: 'campaign--a3038910-f8ca-4ba8-b116-21d0f333f231', + object_modified: '2020-07-03T20:22:40.401Z', + }, + { + object_ref: 'campaign--649b389e-1f7a-4696-8a95-04d0851bd551', + object_modified: '2020-07-03T20:22:40.401Z', + }, + { + object_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + object_modified: '2017-06-01T00:00:00.000Z', + }, + { + object_ref: 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168', + object_modified: '2017-06-01T00:00:00Z', + }, + { + object_ref: 'note--6b9456275-20bf-48b0-afb7-988d769a2f99', + object_modified: '2020-04-12T15:44:47.629Z', + }, + { + object_ref: 'x-mitre-data-source--880b771b-17a8-4a6c-a259-9027c395010c', + object_modified: '2020-04-12T15:44:47.629Z', + }, + { + object_ref: 'x-mitre-data-source--3e396a50-dd74-45cf-b8a3-974ab80c9a3e', + object_modified: '2020-04-12T15:44:47.629Z', + }, + { + object_ref: 'x-mitre-data-component--47667153-e24d-4514-bdf4-5720312d9e7d', + object_modified: '2020-04-12T15:44:47.629Z', + }, + { + object_ref: 'x-mitre-data-component--f8b4833e-a6d4-4a05-ba6e-1936d4109d0a', + object_modified: '2020-04-12T15:44:47.629Z', + }, + { + object_ref: 'relationship--caa8928b-0bf6-45cd-8504-6c27b9cd96a8', + object_modified: '2019-09-04T14:32:13.000Z', + }, + { + object_ref: 'relationship--b0c6c76c-7699-447f-9f3f-573aec51431c', + object_modified: '2019-09-04T14:32:13.000Z', + }, + { + object_ref: 'relationship--e7f994c6-3e08-4aea-a30e-97cc6fe610c6', + object_modified: '2019-09-04T14:32:13.000Z', + }, + { + object_ref: 'relationship--89586929-ca62-423f-94bf-cc03ec8161bb', + object_modified: '2021-06-06T14:00:00.000Z', + }, + { + object_ref: 'relationship--21d3572e-398d-4473-93eb-eb9a2a069d53', + object_modified: '2021-06-07T14:00:00.000Z', + }, + { + object_ref: 'relationship--a5a80c31-0dde-4fd7-a520-a7593d21c954', + object_modified: '2021-06-08T14:00:00.000Z', + }, + { + object_ref: 'relationship--1e1c5e5a-2a3e-423f-b1d0-67b7dc5b90cc', + object_modified: '2023-06-08T14:00:00.000Z', + }, + { + object_ref: 'relationship--d5426745-9530-485e-a757-d8c540f600f8', + object_modified: '2021-06-06T14:00:00.000Z', + }, + ], + }, + { + id: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + name: 'The MITRE Corporation', + identity_class: 'organization', + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + type: 'identity', + modified: '2017-06-01T00:00:00.000Z', + created: '2017-06-01T00:00:00.000Z', + spec_version: '2.1', + }, + { + type: 'marking-definition', + id: 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168', + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + created: '2017-06-01T00:00:00Z', + definition_type: 'statement', + definition: { + statement: + 'Copyright 2015-2021, The MITRE Corporation. MITRE ATT&CK and ATT&CK are registered trademarks of The MITRE Corporation.', + }, + spec_version: '2.1', + }, + { + id: 'attack-pattern--2204c371-6100-4ae0-82f3-25c07c29772a', + created: '2020-03-30T14:03:43.761Z', + modified: '2020-03-30T14:03:43.761Z', + name: 'attack-pattern-1', + x_mitre_version: '1.0', + spec_version: '2.1', + type: 'attack-pattern', + description: 'This is a technique.', + external_references: [ + { source_name: 'mitre-attack', external_id: 'T1' }, + { + source_name: 'attack-pattern-1 source', + description: 'this is a source description', + }, + ], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + kill_chain_phases: [{ kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' }], + x_mitre_data_sources: ['Command: Command Execution', 'Network Traffic: Network Traffic Flow'], + x_mitre_detection: 'detection text', + x_mitre_is_subtechnique: false, + x_mitre_impact_type: ['impact-1'], + x_mitre_platforms: ['platform-1', 'platform-2'], + x_mitre_domains: [enterpriseDomain], + }, + { + id: 'attack-pattern--1eaebf46-e361-4437-bc23-d5d65a3b92e3', + created: '2020-02-12T18:55:24.728Z', + modified: '2020-02-17T13:14:31.140Z', + name: 'attack-pattern-2', + x_mitre_version: '1.0', + spec_version: '2.1', + type: 'attack-pattern', + description: 'This is a technique.', + external_references: [ + { source_name: 'mitre-attack', external_id: 'T1' }, + { + source_name: 'attack-pattern-1 source', + description: 'this is a source description', + }, + ], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + kill_chain_phases: [{ kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' }], + x_mitre_detection: 'detection text', + x_mitre_is_subtechnique: false, + x_mitre_impact_type: ['impact-1'], + x_mitre_platforms: ['platform-1', 'platform-2'], + x_mitre_domains: [mobileDomain], + }, + { + id: 'attack-pattern--82f04b1e-5371-4a6f-be06-411f0f43b483', + created: '2019-02-03T16:56:41.200Z', + modified: '2019-02-03T16:56:41.200Z', + name: 'attack-pattern-3', + x_mitre_version: '1.0', + spec_version: '2.1', + type: 'attack-pattern', + description: 'This is another technique.', + external_references: [ + { source_name: 'mitre-attack', external_id: 'T1' }, + { + source_name: 'attack-pattern-2 source', + description: 'this is a source description 2', + }, + ], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + kill_chain_phases: [{ kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' }], + x_mitre_data_sources: [ + 'Operational Databases: Device Alarm', + 'Network Traffic: Network Traffic Flow', + ], + x_mitre_detection: 'detection text', + x_mitre_is_subtechnique: false, + x_mitre_impact_type: ['impact-1'], + x_mitre_platforms: ['platform-1', 'platform-2'], + x_mitre_domains: [icsDomain], + }, + { + id: 'attack-pattern--2bb2861b-fb40-42dc-b15f-1a6b64b6a39f', + created: '2019-02-03T16:56:41.200Z', + modified: '2019-02-03T16:56:41.200Z', + name: 'attack-pattern-4', + x_mitre_version: '1.0', + spec_version: '2.1', + type: 'attack-pattern', + description: 'This is another technique.', + external_references: [ + { source_name: 'mitre-attack', external_id: 'T1' }, + { + source_name: 'attack-pattern-2 source', + description: 'this is a source description 2', + }, + ], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + kill_chain_phases: [{ kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' }], + x_mitre_data_sources: [ + 'Command: Command Execution', + 'Operational Databases: Device Alarm', + 'Network Traffic: Network Traffic Flow', + ], + x_mitre_detection: 'detection text', + x_mitre_is_subtechnique: false, + x_mitre_impact_type: ['impact-1'], + x_mitre_platforms: ['platform-1', 'platform-2'], + x_mitre_domains: [enterpriseDomain, icsDomain], + }, + { + id: 'course-of-action--25dc1ce8-eb55-4333-ae30-a7cb4f5894a1', + type: 'course-of-action', + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + name: 'mitigation-1', + description: 'This is a mitigation', + external_references: [{ source_name: 'mitre-attack', external_id: 'M1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + x_mitre_version: '1.0', + modified: '2018-10-17T00:14:20.652Z', + created: '2017-10-25T14:48:53.732Z', + spec_version: '2.1', + x_mitre_domains: [enterpriseDomain], + }, + { + id: 'course-of-action--e944670c-d03a-4e93-a21c-b3d4c53ec4c9', + type: 'course-of-action', + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + name: 'mitigation-2', + description: "This is a mitigation that isn't in the contents", + external_references: [{ source_name: 'mitre-attack', external_id: 'M1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + x_mitre_version: '1.0', + modified: '2018-10-17T00:14:20.652Z', + created: '2017-10-25T14:48:53.732Z', + spec_version: '2.1', + x_mitre_domains: [enterpriseDomain], + x_mitre_deprecated: true, + }, + { + id: 'malware--04227b24-7817-4de1-9050-b7b1b57f5866', + type: 'malware', + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + name: 'software-1', + description: 'This is a software with an alias', + external_references: [ + { source_name: 'mitre-attack', external_id: 'S1' }, + { source_name: 'malware-1 source', description: 'this is a source description' }, + { source_name: 'xyzzy', description: '(Citation: Adventure 1975)' }, + ], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + x_mitre_version: '1.0', + modified: '2020-03-30T18:17:52.697Z', + created: '2017-10-25T14:48:53.732Z', + spec_version: '2.1', + x_mitre_domains: [enterpriseDomain], + x_mitre_aliases: ['xyzzy'], + }, + { + type: 'intrusion-set', + id: 'intrusion-set--8a831aaa-f3e0-47a3-bed8-a9ced744dd12', + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + name: 'Dark Caracal', + description: + '[Dark Caracal](https://attack.mitre.org/groups/G0070) is threat group that has been attributed to the Lebanese General Directorate of General Security (GDGS) and has operated since at least 2012. (Citation: Lookout Dark Caracal Jan 2018)', + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + external_references: [ + { + source_name: 'mitre-attack', + url: 'https://attack.mitre.org/groups/G0070', + external_id: 'G0070', + }, + { + source_name: 'Dark Caracal', + description: '(Citation: Lookout Dark Caracal Jan 2018)', + }, + { + url: 'https://info.lookout.com/rs/051-ESQ-475/images/Lookout_Dark-Caracal_srr_20180118_us_v.1.0.pdf', + description: + 'Blaich, A., et al. (2018, January 18). Dark Caracal: Cyber-espionage at a Global Scale. Retrieved April 11, 2018.', + source_name: 'Lookout Dark Caracal Jan 2018', + }, + ], + aliases: ['Dark Caracal'], + modified: '2020-06-03T20:22:40.401Z', + created: '2018-10-17T00:14:20.652Z', + spec_version: '2.1', + x_mitre_version: '1.2', + }, + { + type: 'intrusion-set', + id: 'intrusion-set--ed0222fb-b970-4337-b9a2-62aeb02860e5', + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + name: 'Another group', + description: + "This is another group. It isn't referenced by a technique, but is associated with a campaign", + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + external_references: [ + { + source_name: 'mitre-attack', + url: 'https://attack.mitre.org/groups/G0999', + external_id: 'G0999', + }, + ], + aliases: ['Some group alias'], + modified: '2023-05-02T20:19:40.401Z', + created: '2018-10-17T00:19:20.652Z', + spec_version: '2.1', + x_mitre_version: '1.2', + }, + { + type: 'campaign', + id: 'campaign--a3038910-f8ca-4ba8-b116-21d0f333f231', + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + name: 'campaign-1', + description: 'This is a campaign', + first_seen: '2016-04-06T00:00:00.000Z', + last_seen: '2016-07-12T00:00:00.000Z', + x_mitre_first_seen_citation: '(Citation: Article 1)', + x_mitre_last_seen_citation: '(Citation: Article 2)', + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + external_references: [ + { + source_name: 'mitre-attack', + url: 'https://attack.mitre.org/campaigns/C0001', + external_id: 'C0001', + }, + ], + aliases: ['Another campaign name'], + modified: '2020-07-03T20:22:40.401Z', + created: '2018-11-17T00:14:20.652Z', + spec_version: '2.1', + x_mitre_version: '1.2', + }, + { + type: 'campaign', + id: 'campaign--649b389e-1f7a-4696-8a95-04d0851bd551', + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + name: 'campaign-2', + description: 'This is another campaign', + first_seen: '2016-04-06T00:00:00.000Z', + last_seen: '2016-07-12T00:00:00.000Z', + x_mitre_first_seen_citation: '(Citation: Article 1)', + x_mitre_last_seen_citation: '(Citation: Article 2)', + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + external_references: [ + { + source_name: 'mitre-attack', + url: 'https://attack.mitre.org/campaigns/C0002', + external_id: 'C0002', + }, + ], + aliases: ['Another campaign name'], + modified: '2020-07-03T20:22:40.401Z', + created: '2018-11-17T00:14:20.652Z', + spec_version: '2.1', + x_mitre_version: '1.2', + }, + { + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + source_ref: 'intrusion-set--8a831aaa-f3e0-47a3-bed8-a9ced744dd12', + target_ref: 'attack-pattern--2204c371-6100-4ae0-82f3-25c07c29772a', + external_references: [], + description: 'Test relationship', + relationship_type: 'uses', + id: 'relationship--12098dee-27b3-4d0b-a15a-6b5955ba8879', + type: 'relationship', + modified: '2019-09-04T14:32:13.000Z', + created: '2019-09-04T14:28:16.426Z', + spec_version: '2.1', + }, + { + external_references: [], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + description: "This is a group that isn't in the domain", + name: 'Dark Hydra', + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + id: 'intrusion-set--6b9ebeb5-20bf-48b0-afb7-988d769a2f01', + type: 'intrusion-set', + aliases: ['Hydra'], + modified: '2020-05-15T15:44:47.629Z', + created: '2018-10-17T00:14:20.652Z', + x_mitre_version: '1.2', + spec_version: '2.1', + }, + { + type: 'note', + id: 'note--6b9456275-20bf-48b0-afb7-988d769a2f99', + spec_version: '2.1', + abstract: 'This is the abstract for a note.', + content: 'This is the content for a note.', + authors: ['Author 1', 'Author 2'], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + object_refs: ['malware--04227b24-7817-4de1-9050-b7b1b57f5866'], + modified: '2020-04-12T15:44:47.629Z', + created: '2019-10-22T00:14:20.652Z', + }, + { + type: 'x-mitre-data-source', + id: 'x-mitre-data-source--880b771b-17a8-4a6c-a259-9027c395010c', + name: 'Command', + spec_version: '2.1', + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + modified: '2020-04-12T15:44:47.629Z', + created: '2019-10-22T00:14:20.652Z', + external_references: [{ source_name: 'mitre-attack', external_id: 'DS1' }], + }, + { + type: 'x-mitre-data-source', + id: 'x-mitre-data-source--3e396a50-dd74-45cf-b8a3-974ab80c9a3e', + name: 'Network Traffic', + spec_version: '2.1', + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + modified: '2020-04-12T15:44:47.629Z', + created: '2019-10-22T00:14:20.652Z', + external_references: [{ source_name: 'mitre-attack', external_id: 'DS1' }], + }, + { + type: 'x-mitre-data-component', + id: 'x-mitre-data-component--47667153-e24d-4514-bdf4-5720312d9e7d', + name: 'Command Execution', + spec_version: '2.1', + x_mitre_data_source_ref: 'x-mitre-data-source--880b771b-17a8-4a6c-a259-9027c395010c', + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + modified: '2020-04-12T15:44:47.629Z', + created: '2019-10-22T00:14:20.652Z', + }, + { + type: 'x-mitre-data-component', + id: 'x-mitre-data-component--f8b4833e-a6d4-4a05-ba6e-1936d4109d0a', + name: 'Network Traffic Flow', + spec_version: '2.1', + x_mitre_data_source_ref: 'x-mitre-data-source--3e396a50-dd74-45cf-b8a3-974ab80c9a3e', + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + modified: '2020-04-12T15:44:47.629Z', + created: '2019-10-22T00:14:20.652Z', + }, + { + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + source_ref: 'x-mitre-data-component--47667153-e24d-4514-bdf4-5720312d9e7d', + target_ref: 'attack-pattern--2204c371-6100-4ae0-82f3-25c07c29772a', + external_references: [], + description: 'Detects relationship', + relationship_type: 'detects', + id: 'relationship--caa8928b-0bf6-45cd-8504-6c27b9cd96a8', + type: 'relationship', + modified: '2019-09-04T14:32:13.000Z', + created: '2019-09-04T14:28:16.426Z', + spec_version: '2.1', + }, + { + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + source_ref: 'x-mitre-data-component--47667153-e24d-4514-bdf4-5720312d9e7d', + target_ref: 'attack-pattern--2bb2861b-fb40-42dc-b15f-1a6b64b6a39f', + external_references: [], + description: 'Detects relationship', + relationship_type: 'detects', + id: 'relationship--e7f994c6-3e08-4aea-a30e-97cc6fe610c6', + type: 'relationship', + modified: '2019-09-04T14:32:13.000Z', + created: '2019-09-04T14:28:16.426Z', + spec_version: '2.1', + }, + { + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + source_ref: 'x-mitre-data-component--f8b4833e-a6d4-4a05-ba6e-1936d4109d0a', + target_ref: 'attack-pattern--2204c371-6100-4ae0-82f3-25c07c29772a', + external_references: [], + description: 'Test relationship', + relationship_type: 'detects', + id: 'relationship--b0c6c76c-7699-447f-9f3f-573aec51431c', + type: 'relationship', + modified: '2019-09-04T14:32:13.000Z', + created: '2019-09-04T14:28:16.426Z', + spec_version: '2.1', + }, + { + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + source_ref: 'campaign--a3038910-f8ca-4ba8-b116-21d0f333f231', + target_ref: 'attack-pattern--2bb2861b-fb40-42dc-b15f-1a6b64b6a39f', + external_references: [], + description: 'Campaign uses technique', + relationship_type: 'uses', + id: 'relationship--89586929-ca62-423f-94bf-cc03ec8161bb', + type: 'relationship', + modified: '2021-06-06T14:00:00.000Z', + created: '2021-06-06T14:00:00.000Z', + spec_version: '2.1', + }, + { + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + source_ref: 'campaign--649b389e-1f7a-4696-8a95-04d0851bd551', + target_ref: 'attack-pattern--82f04b1e-5371-4a6f-be06-411f0f43b483', + external_references: [], + description: 'Campaign uses technique', + relationship_type: 'uses', + id: 'relationship--d5426745-9530-485e-a757-d8c540f600f8', + type: 'relationship', + modified: '2021-06-06T14:00:00.000Z', + created: '2021-06-06T14:00:00.000Z', + spec_version: '2.1', + }, + { + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + source_ref: 'campaign--a3038910-f8ca-4ba8-b116-21d0f333f231', + target_ref: 'malware--04227b24-7817-4de1-9050-b7b1b57f5866', + external_references: [], + description: 'Campaign uses software', + relationship_type: 'uses', + id: 'relationship--21d3572e-398d-4473-93eb-eb9a2a069d53', + type: 'relationship', + modified: '2021-06-07T14:00:00.000Z', + created: '2021-06-07T14:00:00.000Z', + spec_version: '2.1', + }, + { + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + source_ref: 'campaign--a3038910-f8ca-4ba8-b116-21d0f333f231', + target_ref: 'intrusion-set--8a831aaa-f3e0-47a3-bed8-a9ced744dd12', + external_references: [], + description: 'Campaign attributed to group', + relationship_type: 'attributed-to', + id: 'relationship--a5a80c31-0dde-4fd7-a520-a7593d21c954', + type: 'relationship', + modified: '2021-06-08T14:00:00.000Z', + created: '2021-06-08T14:00:00.000Z', + spec_version: '2.1', + }, + { + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + source_ref: 'campaign--649b389e-1f7a-4696-8a95-04d0851bd551', + target_ref: 'intrusion-set--ed0222fb-b970-4337-b9a2-62aeb02860e5', + external_references: [], + description: 'Campaign attributed to group and is the only reference to the group', + relationship_type: 'attributed-to', + id: 'relationship--1e1c5e5a-2a3e-423f-b1d0-67b7dc5b90cc', + type: 'relationship', + modified: '2023-06-08T14:00:00.000Z', + created: '2023-06-08T14:00:00.000Z', + spec_version: '2.1', + }, + ], }; // function printBundleCount(bundle) { @@ -697,172 +656,170 @@ const initialObjectData = { // } describe('STIX Bundles Basic API', function () { - let app; - let passportCookie; + let app; + let passportCookie; - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); - // Initialize the express app - app = await require('../../../index').initializeApp(); + // Initialize the express app + app = await require('../../../index').initializeApp(); - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); - it('POST /api/collection-bundles imports a collection bundle', function (done) { - const body = initialObjectData; - request(app) - .post('/api/collection-bundles') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get the created collection object - const collection = res.body; - expect(collection).toBeDefined(); - expect(collection.workspace.import_categories.additions.length).toBe(initialObjectData.objects[0].x_mitre_contents.length); - expect(collection.workspace.import_categories.errors.length).toBe(0); - done(); - } - }); - }); + it('POST /api/collection-bundles imports a collection bundle', function (done) { + const body = initialObjectData; + request(app) + .post('/api/collection-bundles') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the created collection object + const collection = res.body; + expect(collection).toBeDefined(); + expect(collection.workspace.import_categories.additions.length).toBe( + initialObjectData.objects[0].x_mitre_contents.length, + ); + expect(collection.workspace.import_categories.errors.length).toBe(0); + done(); + } + }); + }); - it('GET /api/stix-bundles exports an empty STIX bundle', function (done) { - request(app) - .get('/api/stix-bundles?domain=not-a-domain') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get the exported STIX bundle - const stixBundle = res.body; - expect(stixBundle).toBeDefined(); - expect(Array.isArray(stixBundle.objects)).toBe(true); - expect(stixBundle.objects.length).toBe(0); - done(); - } - }); - }); + it('GET /api/stix-bundles exports an empty STIX bundle', function (done) { + request(app) + .get('/api/stix-bundles?domain=not-a-domain') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the exported STIX bundle + const stixBundle = res.body; + expect(stixBundle).toBeDefined(); + expect(Array.isArray(stixBundle.objects)).toBe(true); + expect(stixBundle.objects.length).toBe(0); + done(); + } + }); + }); - it('GET /api/stix-bundles exports the STIX bundle for the enterprise domain', function (done) { - request(app) - .get(`/api/stix-bundles?domain=${ enterpriseDomain }&includeNotes=true`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the exported STIX bundle - const stixBundle = res.body; - expect(stixBundle).toBeDefined(); - expect(Array.isArray(stixBundle.objects)).toBe(true); + it('GET /api/stix-bundles exports the STIX bundle for the enterprise domain', function (done) { + request(app) + .get(`/api/stix-bundles?domain=${enterpriseDomain}&includeNotes=true`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the exported STIX bundle + const stixBundle = res.body; + expect(stixBundle).toBeDefined(); + expect(Array.isArray(stixBundle.objects)).toBe(true); - // 4 primary objects, 7 relationship objects, 6 secondary objects, - // 1 note, 1 identity, 1 marking definition - //printBundleCount(stixBundle); - expect(stixBundle.objects.length).toBe(20); + // 4 primary objects, 7 relationship objects, 6 secondary objects, + // 1 note, 1 identity, 1 marking definition + //printBundleCount(stixBundle); + expect(stixBundle.objects.length).toBe(20); - done(); - } - }); - }); + done(); + } + }); + }); - it('GET /api/stix-bundles exports the STIX bundle for the enterprise domain including deprecated objects', function (done) { - request(app) - .get(`/api/stix-bundles?domain=${ enterpriseDomain }&includeDeprecated=true&includeNotes=true`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the exported STIX bundle - const stixBundle = res.body; - expect(stixBundle).toBeDefined(); - expect(Array.isArray(stixBundle.objects)).toBe(true); - // 5 primary objects, 7 relationship objects, 6 secondary objects, - // 1 note, 1 identity, 1 marking definition - expect(stixBundle.objects.length).toBe(21); + it('GET /api/stix-bundles exports the STIX bundle for the enterprise domain including deprecated objects', function (done) { + request(app) + .get(`/api/stix-bundles?domain=${enterpriseDomain}&includeDeprecated=true&includeNotes=true`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the exported STIX bundle + const stixBundle = res.body; + expect(stixBundle).toBeDefined(); + expect(Array.isArray(stixBundle.objects)).toBe(true); + // 5 primary objects, 7 relationship objects, 6 secondary objects, + // 1 note, 1 identity, 1 marking definition + expect(stixBundle.objects.length).toBe(21); - done(); - } - }); - }); + done(); + } + }); + }); - it('GET /api/stix-bundles exports the STIX bundle for the mobile domain', function (done) { - request(app) - .get(`/api/stix-bundles?domain=${ mobileDomain }`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the exported STIX bundle - const stixBundle = res.body; - expect(stixBundle).toBeDefined(); - expect(Array.isArray(stixBundle.objects)).toBe(true); - // 1 primary objects, 1 identity, 1 marking definition - expect(stixBundle.objects.length).toBe(3); + it('GET /api/stix-bundles exports the STIX bundle for the mobile domain', function (done) { + request(app) + .get(`/api/stix-bundles?domain=${mobileDomain}`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the exported STIX bundle + const stixBundle = res.body; + expect(stixBundle).toBeDefined(); + expect(Array.isArray(stixBundle.objects)).toBe(true); + // 1 primary objects, 1 identity, 1 marking definition + expect(stixBundle.objects.length).toBe(3); - done(); - } - }); - }); + done(); + } + }); + }); - it('GET /api/stix-bundles exports the STIX bundle for the ics domain', function (done) { - request(app) - .get(`/api/stix-bundles?domain=${ icsDomain }`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the exported STIX bundle - const stixBundle = res.body; - expect(stixBundle).toBeDefined(); - expect(Array.isArray(stixBundle.objects)).toBe(true); - // 3 primary objects, 5 relationship objects, 5 secondary objects, - // 1 identity, 1 marking definition - expect(stixBundle.objects.length).toBe(15); + it('GET /api/stix-bundles exports the STIX bundle for the ics domain', function (done) { + request(app) + .get(`/api/stix-bundles?domain=${icsDomain}`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the exported STIX bundle + const stixBundle = res.body; + expect(stixBundle).toBeDefined(); + expect(Array.isArray(stixBundle.objects)).toBe(true); + // 3 primary objects, 5 relationship objects, 5 secondary objects, + // 1 identity, 1 marking definition + expect(stixBundle.objects.length).toBe(15); - const groupObjects = stixBundle.objects.filter(o => o.type === 'intrusion-set'); - expect(groupObjects.length).toBe(2); + const groupObjects = stixBundle.objects.filter((o) => o.type === 'intrusion-set'); + expect(groupObjects.length).toBe(2); - done(); - } - }); - }); + done(); + } + }); + }); - after(async function() { - await database.closeConnection(); - }); + after(async function () { + await database.closeConnection(); + }); }); diff --git a/app/tests/api/system-configuration/create-object-identity.spec.js b/app/tests/api/system-configuration/create-object-identity.spec.js index ef50df72..a0f0cf2d 100644 --- a/app/tests/api/system-configuration/create-object-identity.spec.js +++ b/app/tests/api/system-configuration/create-object-identity.spec.js @@ -11,204 +11,196 @@ const logger = require('../../../lib/logger'); logger.level = 'debug'; const initialTacticData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - name: 'x-mitre-tactic-1', - spec_version: '2.1', - type: 'x-mitre-tactic', - description: 'This is a tactic. yellow.', - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ] - } + }, + stix: { + name: 'x-mitre-tactic-1', + spec_version: '2.1', + type: 'x-mitre-tactic', + description: 'This is a tactic. yellow.', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + }, }; const newIdentityData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - name: 'identity-1', - identity_class: 'organization', - spec_version: '2.1', - type: 'identity', - description: 'This is an identity.', - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ] - } + }, + stix: { + name: 'identity-1', + identity_class: 'organization', + spec_version: '2.1', + type: 'identity', + description: 'This is an identity.', + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + }, }; describe('Create Object with Organization Identity API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - let placeholderIdentity; - it('GET /api/config/organization-identity returns the organizaton identity', function (done) { - request(app) - .get('/api/config/organization-identity') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the organization identity - placeholderIdentity = res.body; - expect(placeholderIdentity).toBeDefined(); - - done(); - } - }); - }); - - let tactic1; - it('POST /api/tactics creates a tactic', function (done) { - initialTacticData.stix.id = `x-mitre-tactic--${uuid.v4()}`; - const timestamp = new Date().toISOString(); - initialTacticData.stix.created = timestamp; - initialTacticData.stix.modified = timestamp; - const body = initialTacticData; - request(app) - .post('/api/tactics') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the created tactic - tactic1 = res.body; - expect(tactic1).toBeDefined(); - expect(tactic1.stix).toBeDefined(); - expect(tactic1.stix.id).toBeDefined(); - expect(tactic1.stix.created).toBeDefined(); - expect(tactic1.stix.modified).toBeDefined(); - expect(tactic1.stix.created_by_ref).toBeDefined(); - expect(tactic1.stix.x_mitre_modified_by_ref).toBeDefined(); - expect(tactic1.stix.created_by_ref).toBe(placeholderIdentity.stix.id); - expect(tactic1.stix.x_mitre_modified_by_ref).toBe(placeholderIdentity.stix.id); - done(); - } - }); - }); - - let newIdentity; - it('POST /api/identities creates an identity', function (done) { - const timestamp = new Date().toISOString(); - newIdentityData.stix.created = timestamp; - newIdentityData.stix.modified = timestamp; - const body = newIdentityData; - request(app) - .post('/api/identities') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the created identity - newIdentity = res.body; - expect(newIdentity).toBeDefined(); - expect(newIdentity.stix).toBeDefined(); - expect(newIdentity.stix.id).toBeDefined(); - expect(newIdentity.stix.created).toBeDefined(); - expect(newIdentity.stix.modified).toBeDefined(); - done(); - } - }); - }); - - it('POST /api/config/organization-identity sets the organization identity', function (done) { - const body = { - id: newIdentity.stix.id - }; - request(app) - .post('/api/config/organization-identity') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect the response body to be empty - done(); - } - }); - }); - - it('POST /api/tactics creates a new version of the tactic', function (done) { - const tactic2 = _.cloneDeep(tactic1); - tactic2._id = undefined; - tactic2.__t = undefined; - tactic2.__v = undefined; - const timestamp = new Date().toISOString(); - tactic2.stix.modified = timestamp; - const body = tactic2; - request(app) - .post('/api/tactics') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the created tactic - const tactic = res.body; - expect(tactic).toBeDefined(); - expect(tactic.stix).toBeDefined(); - expect(tactic.stix.id).toBeDefined(); - expect(tactic.stix.created).toBeDefined(); - expect(tactic.stix.modified).toBeDefined(); - expect(tactic.stix.created_by_ref).toBeDefined(); - expect(tactic.stix.x_mitre_modified_by_ref).toBeDefined(); - expect(tactic.stix.created_by_ref).toBe(placeholderIdentity.stix.id); - expect(tactic.stix.x_mitre_modified_by_ref).toBe(newIdentity.stix.id); // Should match the new identity - done(); - } - }); - }); - - after(async function() { - await database.closeConnection(); - }); -}); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + let placeholderIdentity; + it('GET /api/config/organization-identity returns the organizaton identity', function (done) { + request(app) + .get('/api/config/organization-identity') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the organization identity + placeholderIdentity = res.body; + expect(placeholderIdentity).toBeDefined(); + + done(); + } + }); + }); + + let tactic1; + it('POST /api/tactics creates a tactic', function (done) { + initialTacticData.stix.id = `x-mitre-tactic--${uuid.v4()}`; + const timestamp = new Date().toISOString(); + initialTacticData.stix.created = timestamp; + initialTacticData.stix.modified = timestamp; + const body = initialTacticData; + request(app) + .post('/api/tactics') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the created tactic + tactic1 = res.body; + expect(tactic1).toBeDefined(); + expect(tactic1.stix).toBeDefined(); + expect(tactic1.stix.id).toBeDefined(); + expect(tactic1.stix.created).toBeDefined(); + expect(tactic1.stix.modified).toBeDefined(); + expect(tactic1.stix.created_by_ref).toBeDefined(); + expect(tactic1.stix.x_mitre_modified_by_ref).toBeDefined(); + expect(tactic1.stix.created_by_ref).toBe(placeholderIdentity.stix.id); + expect(tactic1.stix.x_mitre_modified_by_ref).toBe(placeholderIdentity.stix.id); + done(); + } + }); + }); + + let newIdentity; + it('POST /api/identities creates an identity', function (done) { + const timestamp = new Date().toISOString(); + newIdentityData.stix.created = timestamp; + newIdentityData.stix.modified = timestamp; + const body = newIdentityData; + request(app) + .post('/api/identities') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the created identity + newIdentity = res.body; + expect(newIdentity).toBeDefined(); + expect(newIdentity.stix).toBeDefined(); + expect(newIdentity.stix.id).toBeDefined(); + expect(newIdentity.stix.created).toBeDefined(); + expect(newIdentity.stix.modified).toBeDefined(); + done(); + } + }); + }); + + it('POST /api/config/organization-identity sets the organization identity', function (done) { + const body = { + id: newIdentity.stix.id, + }; + request(app) + .post('/api/config/organization-identity') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect the response body to be empty + done(); + } + }); + }); + + it('POST /api/tactics creates a new version of the tactic', function (done) { + const tactic2 = _.cloneDeep(tactic1); + tactic2._id = undefined; + tactic2.__t = undefined; + tactic2.__v = undefined; + const timestamp = new Date().toISOString(); + tactic2.stix.modified = timestamp; + const body = tactic2; + request(app) + .post('/api/tactics') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the created tactic + const tactic = res.body; + expect(tactic).toBeDefined(); + expect(tactic.stix).toBeDefined(); + expect(tactic.stix.id).toBeDefined(); + expect(tactic.stix.created).toBeDefined(); + expect(tactic.stix.modified).toBeDefined(); + expect(tactic.stix.created_by_ref).toBeDefined(); + expect(tactic.stix.x_mitre_modified_by_ref).toBeDefined(); + expect(tactic.stix.created_by_ref).toBe(placeholderIdentity.stix.id); + expect(tactic.stix.x_mitre_modified_by_ref).toBe(newIdentity.stix.id); // Should match the new identity + done(); + } + }); + }); + after(async function () { + await database.closeConnection(); + }); +}); diff --git a/app/tests/api/system-configuration/system-configuration.spec.js b/app/tests/api/system-configuration/system-configuration.spec.js index b7534d7a..17279e35 100644 --- a/app/tests/api/system-configuration/system-configuration.spec.js +++ b/app/tests/api/system-configuration/system-configuration.spec.js @@ -11,269 +11,273 @@ logger.level = 'debug'; const amberStixId = 'marking-definition--f88d31f6-486f-44da-b317-01333bde0b82'; const markingDefinitionData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - spec_version: '2.1', - type: 'marking-definition', - definition_type: 'statement', - definition: { statement: 'This is a marking definition.' }, - created_by_ref: "identity--6444f546-6900-4456-b3b1-015c88d70dab" - } + }, + stix: { + spec_version: '2.1', + type: 'marking-definition', + definition_type: 'statement', + definition: { statement: 'This is a marking definition.' }, + created_by_ref: 'identity--6444f546-6900-4456-b3b1-015c88d70dab', + }, }; describe('System Configuration API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - it('GET /api/config/system-version returns the system version info', async function () { - const res = await request(app) - .get('/api/config/system-version') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the system version info - const systemVersionInfo = res.body; - expect(systemVersionInfo).toBeDefined(); - expect(systemVersionInfo.version).toBeDefined(); - expect(systemVersionInfo.attackSpecVersion).toBeDefined(); - }); - - it('GET /api/config/allowed-values returns the allowed values', async function () { - const res = await request(app) - .get('/api/config/allowed-values') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the list of allowed values - const allowedValues = res.body; - expect(allowedValues).toBeDefined(); - expect(Array.isArray(allowedValues)).toBe(true); - - const expectedObjectType = 'technique'; - const expectedPropertyName = 'x_mitre_platforms'; - const expectedDomainName = 'enterprise-attack'; - const expectedPropertyValue = 'Linux'; - - // Test some content - const techniqueAllowedValues = allowedValues.find(item => item.objectType === expectedObjectType); - expect(techniqueAllowedValues).toBeDefined(); - - const propertyAllowedValues = techniqueAllowedValues.properties.find(item => item.propertyName === expectedPropertyName); - expect(propertyAllowedValues).toBeDefined(); - - const domainAllowedValues = propertyAllowedValues.domains.find(item => item.domainName === expectedDomainName); - expect(domainAllowedValues).toBeDefined(); - expect(domainAllowedValues.allowedValues).toContain(expectedPropertyValue); - }); - - it('GET /api/config/organization-identity returns the organizaton identity', async function () { - const res = await request(app) - .get('/api/config/organization-identity') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the organization identity - const identity = res.body; - expect(identity).toBeDefined(); - }); - - it('GET /api/config/authn returns the available authentication mechanisms', async function () { - const res = await request(app) - .get('/api/config/authn') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the list of authentication mechanisms - const authnConfig = res.body; - expect(authnConfig).toBeDefined(); - expect(authnConfig.mechanisms).toBeDefined(); - expect(Array.isArray(authnConfig.mechanisms)).toBe(true); - }); - - let amberTlpMarkingDefinition; - it('GET /api/marking-definitions returns the static TLP marking definitions', async function () { - const res = await request(app) - .get('/api/marking-definitions') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the pre-defined TLP marking definitions - const markingDefinitions = res.body; - expect(markingDefinitions).toBeDefined(); - expect(Array.isArray(markingDefinitions)).toBe(true) - expect(markingDefinitions.length).toBe(4); - - amberTlpMarkingDefinition = markingDefinitions.find(x => x.stix.id === amberStixId); - expect(amberTlpMarkingDefinition).toBeDefined(); - }); - - - it('PUT /api/marking-definitions fails to update a static marking definition', async function () { - amberTlpMarkingDefinition.stix.description = 'This is an updated marking definition.' - const body = amberTlpMarkingDefinition; - await request(app) - .put('/api/marking-definitions/' + amberTlpMarkingDefinition.stix.id) - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - it('GET /api/config/default-marking-definitions returns an empty array since no default has been set', async function () { - const res = await request(app) - .get('/api/config/default-marking-definitions') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const defaultMarkingDefinitions = res.body; - expect(defaultMarkingDefinitions).toBeDefined(); - expect(Array.isArray(defaultMarkingDefinitions)).toBe(true) - expect(defaultMarkingDefinitions.length).toBe(0); - }); - - let markingDefinition; - it('POST /api/marking-definitions creates a marking definition', async function () { - const timestamp = new Date().toISOString(); - markingDefinitionData.stix.created = timestamp; - const body = markingDefinitionData; - const res = await request(app) - .post('/api/marking-definitions') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created marking definition - markingDefinition = res.body; - expect(markingDefinition).toBeDefined(); - }); - - it('POST /api/config/default-marking-definitions sets the default marking definitions', async function () { - const body = [markingDefinition.stix.id]; - const res = await request(app) - .post('/api/config/default-marking-definitions') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - - // We expect the response body to be an empty object - expect(res.body).toBeDefined(); - expect(Object.getOwnPropertyNames(res.body)).toHaveLength(0); - }); - - it('GET /api/config/default-marking-definitions returns an array containing the marking definition', async function () { - const res = await request(app) - .get('/api/config/default-marking-definitions') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const defaultMarkingDefinitions = res.body; - expect(defaultMarkingDefinitions).toBeDefined(); - expect(Array.isArray(defaultMarkingDefinitions)).toBe(true) - expect(defaultMarkingDefinitions.length).toBe(1); - expect(defaultMarkingDefinitions[0].stix.id).toBe(markingDefinition.stix.id); - }); - - it('GET /api/config/default-marking-definitions returns an array containing the marking definition reference', async function () { - const res = await request(app) - .get('/api/config/default-marking-definitions?refOnly=true') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const defaultMarkingDefinitions = res.body; - expect(defaultMarkingDefinitions).toBeDefined(); - expect(Array.isArray(defaultMarkingDefinitions)).toBe(true) - expect(defaultMarkingDefinitions.length).toBe(1); - expect(defaultMarkingDefinitions[0]).toBe(markingDefinition.stix.id); - }); - - it('GET /api/config/organization-namespace returns the default namespace', async function () { - const res = await request(app) - .get('/api/config/organization-namespace') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the default namespace - const namespace = res.body; - expect(namespace).toBeDefined(); - expect(namespace.range_start).toBeNull(); - expect(namespace.prefix).toBeNull(); - }); - - const testNamespace = { range_start: 3000, prefix: 'TESTORG' }; - it('POST /api/config/organization-namespace sets the organization namespace', async function () { - const body = testNamespace; - const res = await request(app) - .post('/api/config/organization-namespace') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - - // We expect the response body to be an empty object - expect(res.body).toBeDefined(); - expect(Object.getOwnPropertyNames(res.body)).toHaveLength(0); - }); - - it('GET /api/config/organization-namespace returns the updated namespace', async function () { - const res = await request(app) - .get('/api/config/organization-namespace') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the default namespace - const namespace = res.body; - expect(namespace).toBeDefined(); - expect(namespace.range_start).toBe(testNamespace.range_start); - expect(namespace.prefix).toBe(testNamespace.prefix); - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + it('GET /api/config/system-version returns the system version info', async function () { + const res = await request(app) + .get('/api/config/system-version') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the system version info + const systemVersionInfo = res.body; + expect(systemVersionInfo).toBeDefined(); + expect(systemVersionInfo.version).toBeDefined(); + expect(systemVersionInfo.attackSpecVersion).toBeDefined(); + }); + + it('GET /api/config/allowed-values returns the allowed values', async function () { + const res = await request(app) + .get('/api/config/allowed-values') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the list of allowed values + const allowedValues = res.body; + expect(allowedValues).toBeDefined(); + expect(Array.isArray(allowedValues)).toBe(true); + + const expectedObjectType = 'technique'; + const expectedPropertyName = 'x_mitre_platforms'; + const expectedDomainName = 'enterprise-attack'; + const expectedPropertyValue = 'Linux'; + + // Test some content + const techniqueAllowedValues = allowedValues.find( + (item) => item.objectType === expectedObjectType, + ); + expect(techniqueAllowedValues).toBeDefined(); + + const propertyAllowedValues = techniqueAllowedValues.properties.find( + (item) => item.propertyName === expectedPropertyName, + ); + expect(propertyAllowedValues).toBeDefined(); + + const domainAllowedValues = propertyAllowedValues.domains.find( + (item) => item.domainName === expectedDomainName, + ); + expect(domainAllowedValues).toBeDefined(); + expect(domainAllowedValues.allowedValues).toContain(expectedPropertyValue); + }); + + it('GET /api/config/organization-identity returns the organizaton identity', async function () { + const res = await request(app) + .get('/api/config/organization-identity') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the organization identity + const identity = res.body; + expect(identity).toBeDefined(); + }); + + it('GET /api/config/authn returns the available authentication mechanisms', async function () { + const res = await request(app) + .get('/api/config/authn') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the list of authentication mechanisms + const authnConfig = res.body; + expect(authnConfig).toBeDefined(); + expect(authnConfig.mechanisms).toBeDefined(); + expect(Array.isArray(authnConfig.mechanisms)).toBe(true); + }); + + let amberTlpMarkingDefinition; + it('GET /api/marking-definitions returns the static TLP marking definitions', async function () { + const res = await request(app) + .get('/api/marking-definitions') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the pre-defined TLP marking definitions + const markingDefinitions = res.body; + expect(markingDefinitions).toBeDefined(); + expect(Array.isArray(markingDefinitions)).toBe(true); + expect(markingDefinitions.length).toBe(4); + + amberTlpMarkingDefinition = markingDefinitions.find((x) => x.stix.id === amberStixId); + expect(amberTlpMarkingDefinition).toBeDefined(); + }); + + it('PUT /api/marking-definitions fails to update a static marking definition', async function () { + amberTlpMarkingDefinition.stix.description = 'This is an updated marking definition.'; + const body = amberTlpMarkingDefinition; + await request(app) + .put('/api/marking-definitions/' + amberTlpMarkingDefinition.stix.id) + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + it('GET /api/config/default-marking-definitions returns an empty array since no default has been set', async function () { + const res = await request(app) + .get('/api/config/default-marking-definitions') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const defaultMarkingDefinitions = res.body; + expect(defaultMarkingDefinitions).toBeDefined(); + expect(Array.isArray(defaultMarkingDefinitions)).toBe(true); + expect(defaultMarkingDefinitions.length).toBe(0); + }); + + let markingDefinition; + it('POST /api/marking-definitions creates a marking definition', async function () { + const timestamp = new Date().toISOString(); + markingDefinitionData.stix.created = timestamp; + const body = markingDefinitionData; + const res = await request(app) + .post('/api/marking-definitions') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created marking definition + markingDefinition = res.body; + expect(markingDefinition).toBeDefined(); + }); + + it('POST /api/config/default-marking-definitions sets the default marking definitions', async function () { + const body = [markingDefinition.stix.id]; + const res = await request(app) + .post('/api/config/default-marking-definitions') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + + // We expect the response body to be an empty object + expect(res.body).toBeDefined(); + expect(Object.getOwnPropertyNames(res.body)).toHaveLength(0); + }); + + it('GET /api/config/default-marking-definitions returns an array containing the marking definition', async function () { + const res = await request(app) + .get('/api/config/default-marking-definitions') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const defaultMarkingDefinitions = res.body; + expect(defaultMarkingDefinitions).toBeDefined(); + expect(Array.isArray(defaultMarkingDefinitions)).toBe(true); + expect(defaultMarkingDefinitions.length).toBe(1); + expect(defaultMarkingDefinitions[0].stix.id).toBe(markingDefinition.stix.id); + }); + + it('GET /api/config/default-marking-definitions returns an array containing the marking definition reference', async function () { + const res = await request(app) + .get('/api/config/default-marking-definitions?refOnly=true') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const defaultMarkingDefinitions = res.body; + expect(defaultMarkingDefinitions).toBeDefined(); + expect(Array.isArray(defaultMarkingDefinitions)).toBe(true); + expect(defaultMarkingDefinitions.length).toBe(1); + expect(defaultMarkingDefinitions[0]).toBe(markingDefinition.stix.id); + }); + + it('GET /api/config/organization-namespace returns the default namespace', async function () { + const res = await request(app) + .get('/api/config/organization-namespace') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the default namespace + const namespace = res.body; + expect(namespace).toBeDefined(); + expect(namespace.range_start).toBeNull(); + expect(namespace.prefix).toBeNull(); + }); + + const testNamespace = { range_start: 3000, prefix: 'TESTORG' }; + it('POST /api/config/organization-namespace sets the organization namespace', async function () { + const body = testNamespace; + const res = await request(app) + .post('/api/config/organization-namespace') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + + // We expect the response body to be an empty object + expect(res.body).toBeDefined(); + expect(Object.getOwnPropertyNames(res.body)).toHaveLength(0); + }); + + it('GET /api/config/organization-namespace returns the updated namespace', async function () { + const res = await request(app) + .get('/api/config/organization-namespace') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the default namespace + const namespace = res.body; + expect(namespace).toBeDefined(); + expect(namespace.range_start).toBe(testNamespace.range_start); + expect(namespace.prefix).toBe(testNamespace.prefix); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/api/tactics/tactics.spec.js b/app/tests/api/tactics/tactics.spec.js index 1673e540..34cab369 100644 --- a/app/tests/api/tactics/tactics.spec.js +++ b/app/tests/api/tactics/tactics.spec.js @@ -14,373 +14,363 @@ logger.level = 'debug'; // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - name: 'x-mitre-tactic-1', - spec_version: '2.1', - type: 'x-mitre-tactic', - description: 'This is a tactic. yellow.', - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ] - } + }, + stix: { + name: 'x-mitre-tactic-1', + spec_version: '2.1', + type: 'x-mitre-tactic', + description: 'This is a tactic. yellow.', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + }, }; describe('Tactics API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - it('GET /api/tactics returns an empty array of tactics', async function () { - const res = await request(app) - .get('/api/tactics') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const tactics = res.body; - expect(tactics).toBeDefined(); - expect(Array.isArray(tactics)).toBe(true); - expect(tactics.length).toBe(0); - }); - - it('POST /api/tactics does not create an empty tactic', async function () { - const body = { }; - await request(app) - .post('/api/tactics') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - let tactic1; - it('POST /api/tactics creates a tactic', async function () { - const timestamp = new Date().toISOString(); - initialObjectData.stix.created = timestamp; - initialObjectData.stix.modified = timestamp; - const body = initialObjectData; - const res = await request(app) - .post('/api/tactics') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created tactic - tactic1 = res.body; - expect(tactic1).toBeDefined(); - expect(tactic1.stix).toBeDefined(); - expect(tactic1.stix.id).toBeDefined(); - expect(tactic1.stix.created).toBeDefined(); - expect(tactic1.stix.modified).toBeDefined(); - expect(tactic1.stix.created_by_ref).toBeDefined(); - expect(tactic1.stix.x_mitre_modified_by_ref).toBeDefined(); - expect(tactic1.stix.created_by_ref).toBe(tactic1.stix.x_mitre_modified_by_ref); - expect(tactic1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); - }); - - it('GET /api/tactics returns the added tactic', async function () { - const res = await request(app) - .get('/api/tactics') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one tactic in an array - const tactics = res.body; - expect(tactics).toBeDefined(); - expect(Array.isArray(tactics)).toBe(true); - expect(tactics.length).toBe(1); - }); - - it('GET /api/tactics/:id should not return a tactic when the id cannot be found', async function () { - await request(app) - .get('/api/tactics/not-an-id') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('GET /api/tactics/:id returns the added tactic', async function () { - const res = await request(app) - .get('/api/tactics/' + tactic1.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one tactic in an array - const tactics = res.body; - expect(tactics).toBeDefined(); - expect(Array.isArray(tactics)).toBe(true); - expect(tactics.length).toBe(1); - - const tactic = tactics[0]; - expect(tactic).toBeDefined(); - expect(tactic.stix).toBeDefined(); - expect(tactic.stix.id).toBe(tactic1.stix.id); - expect(tactic.stix.type).toBe(tactic1.stix.type); - expect(tactic.stix.name).toBe(tactic1.stix.name); - expect(tactic.stix.description).toBe(tactic1.stix.description); - expect(tactic.stix.spec_version).toBe(tactic1.stix.spec_version); - expect(tactic.stix.object_marking_refs).toEqual(expect.arrayContaining(tactic1.stix.object_marking_refs)); - expect(tactic.stix.created_by_ref).toBe(tactic1.stix.created_by_ref); - expect(tactic.stix.x_mitre_modified_by_ref).toBe(tactic1.stix.x_mitre_modified_by_ref); - expect(tactic.stix.x_mitre_attack_spec_version).toBe(tactic1.stix.x_mitre_attack_spec_version); - - expect(tactic.stix.x_mitre_deprecated).not.toBeDefined(); - }); - - it('PUT /api/tactics updates a tactic', async function () { - const originalModified = tactic1.stix.modified; - const timestamp = new Date().toISOString(); - tactic1.stix.modified = timestamp; - tactic1.stix.description = 'This is an updated tactic.' - const body = tactic1; - const res = await request(app) - .put('/api/tactics/' + tactic1.stix.id + '/modified/' + originalModified) - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the updated tactic - const tactic = res.body; - expect(tactic).toBeDefined(); - expect(tactic.stix.id).toBe(tactic1.stix.id); - expect(tactic.stix.modified).toBe(tactic1.stix.modified); - - }); - - it('POST /api/tactics does not create a tactic with the same id and modified date', async function () { - const body = tactic1; - // We expect to get the created tactic - await request(app) - .post('/api/tactics') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(409); - }); - - let tactic2; - it('POST /api/tactics should create a new version of a tactic with a duplicate stix.id but different stix.modified date', async function () { - tactic2 = _.cloneDeep(tactic1); - tactic2._id = undefined; - tactic2.__t = undefined; - tactic2.__v = undefined; - const timestamp = new Date().toISOString(); - tactic2.stix.description = 'Still a tactic. Red.' - tactic2.stix.modified = timestamp; - const body = tactic2; - const res = await request(app) - .post('/api/tactics') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created tactic - const tactic = res.body; - expect(tactic).toBeDefined(); - - }); - - let tactic3; - it('POST /api/tactics should create a new version of a tactic with a duplicate stix.id but different stix.modified date', async function () { - tactic3 = _.cloneDeep(tactic1); - tactic3._id = undefined; - tactic3.__t = undefined; - tactic3.__v = undefined; - const timestamp = new Date().toISOString(); - tactic3.stix.description = 'Still a tactic. Violet.' - tactic3.stix.modified = timestamp; - const body = tactic3; - const res = await request(app) - .post('/api/tactics') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created tactic - const tactic = res.body; - expect(tactic).toBeDefined(); - }); - - - it('GET /api/tactics returns the latest added tactic', async function () { - const res = await request(app) - .get('/api/tactics/' + tactic3.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one tactic in an array - const tactics = res.body; - expect(tactics).toBeDefined(); - expect(Array.isArray(tactics)).toBe(true); - expect(tactics.length).toBe(1); - const tactic = tactics[0]; - expect(tactic.stix.id).toBe(tactic3.stix.id); - expect(tactic.stix.modified).toBe(tactic3.stix.modified); - - }); - - it('GET /api/tactics returns all added tactics', async function () { - const res = await request(app) - .get('/api/tactics/' + tactic1.stix.id + '?versions=all') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get two tactics in an array - const tactics = res.body; - expect(tactics).toBeDefined(); - expect(Array.isArray(tactics)).toBe(true); - expect(tactics.length).toBe(3); - - }); - - it('GET /api/tactics/:id/modified/:modified returns the first added tactic', async function () { - const res = await request(app) - .get('/api/tactics/' + tactic1.stix.id + '/modified/' + tactic1.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one tactic in an array - const tactic = res.body; - expect(tactic).toBeDefined(); - expect(tactic.stix).toBeDefined(); - expect(tactic.stix.id).toBe(tactic1.stix.id); - expect(tactic.stix.modified).toBe(tactic1.stix.modified); - - }); - - it('GET /api/tactics/:id/modified/:modified returns the second added tactic', async function () { - const res = await request(app) - .get('/api/tactics/' + tactic2.stix.id + '/modified/' + tactic2.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one tactic in an array - const tactic = res.body; - expect(tactic).toBeDefined(); - expect(tactic.stix).toBeDefined(); - expect(tactic.stix.id).toBe(tactic2.stix.id); - expect(tactic.stix.modified).toBe(tactic2.stix.modified); - }); - - it('GET /api/tactics uses the search parameter to return the latest version of the tactic', async function () { - const res = await request(app) - .get('/api/tactics?search=violet') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one tactic in an array - const tactics = res.body; - expect(tactics).toBeDefined(); - expect(Array.isArray(tactics)).toBe(true); - expect(tactics.length).toBe(1); - - // We expect it to be the latest version of the tactic - const tactic = tactics[0]; - expect(tactic).toBeDefined(); - expect(tactic.stix).toBeDefined(); - expect(tactic.stix.id).toBe(tactic3.stix.id); - expect(tactic.stix.modified).toBe(tactic3.stix.modified); - - }); - - it('GET /api/tactics should not get the first version of the tactic when using the search parameter', async function () { - const res = await request(app) - .get('/api/tactics?search=yellow') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get zero tactics in an array - const tactics = res.body; - expect(tactics).toBeDefined(); - expect(Array.isArray(tactics)).toBe(true); - expect(tactics.length).toBe(0); - - }); - - it('DELETE /api/tactics/:id should not delete a tactic when the id cannot be found', async function () { - await request(app) - .delete('/api/tactics/not-an-id') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('DELETE /api/tactics/:id/modified/:modified deletes a tactic', async function () { - await request(app) - .delete('/api/tactics/' + tactic1.stix.id + '/modified/' + tactic1.stix.modified) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - }); - - it('DELETE /api/tactics/:id should delete all the tactics with the same stix id', async function () { - await request(app) - .delete('/api/tactics/' + tactic2.stix.id) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - }); - - it('GET /api/tactics returns an empty array of tactics', async function () { - const res = await request(app) - .get('/api/tactics') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - const tactics = res.body; - expect(tactics).toBeDefined(); - expect(Array.isArray(tactics)).toBe(true); - expect(tactics.length).toBe(0); - - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + it('GET /api/tactics returns an empty array of tactics', async function () { + const res = await request(app) + .get('/api/tactics') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const tactics = res.body; + expect(tactics).toBeDefined(); + expect(Array.isArray(tactics)).toBe(true); + expect(tactics.length).toBe(0); + }); + + it('POST /api/tactics does not create an empty tactic', async function () { + const body = {}; + await request(app) + .post('/api/tactics') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + let tactic1; + it('POST /api/tactics creates a tactic', async function () { + const timestamp = new Date().toISOString(); + initialObjectData.stix.created = timestamp; + initialObjectData.stix.modified = timestamp; + const body = initialObjectData; + const res = await request(app) + .post('/api/tactics') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created tactic + tactic1 = res.body; + expect(tactic1).toBeDefined(); + expect(tactic1.stix).toBeDefined(); + expect(tactic1.stix.id).toBeDefined(); + expect(tactic1.stix.created).toBeDefined(); + expect(tactic1.stix.modified).toBeDefined(); + expect(tactic1.stix.created_by_ref).toBeDefined(); + expect(tactic1.stix.x_mitre_modified_by_ref).toBeDefined(); + expect(tactic1.stix.created_by_ref).toBe(tactic1.stix.x_mitre_modified_by_ref); + expect(tactic1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); + }); + + it('GET /api/tactics returns the added tactic', async function () { + const res = await request(app) + .get('/api/tactics') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one tactic in an array + const tactics = res.body; + expect(tactics).toBeDefined(); + expect(Array.isArray(tactics)).toBe(true); + expect(tactics.length).toBe(1); + }); + + it('GET /api/tactics/:id should not return a tactic when the id cannot be found', async function () { + await request(app) + .get('/api/tactics/not-an-id') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('GET /api/tactics/:id returns the added tactic', async function () { + const res = await request(app) + .get('/api/tactics/' + tactic1.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one tactic in an array + const tactics = res.body; + expect(tactics).toBeDefined(); + expect(Array.isArray(tactics)).toBe(true); + expect(tactics.length).toBe(1); + + const tactic = tactics[0]; + expect(tactic).toBeDefined(); + expect(tactic.stix).toBeDefined(); + expect(tactic.stix.id).toBe(tactic1.stix.id); + expect(tactic.stix.type).toBe(tactic1.stix.type); + expect(tactic.stix.name).toBe(tactic1.stix.name); + expect(tactic.stix.description).toBe(tactic1.stix.description); + expect(tactic.stix.spec_version).toBe(tactic1.stix.spec_version); + expect(tactic.stix.object_marking_refs).toEqual( + expect.arrayContaining(tactic1.stix.object_marking_refs), + ); + expect(tactic.stix.created_by_ref).toBe(tactic1.stix.created_by_ref); + expect(tactic.stix.x_mitre_modified_by_ref).toBe(tactic1.stix.x_mitre_modified_by_ref); + expect(tactic.stix.x_mitre_attack_spec_version).toBe(tactic1.stix.x_mitre_attack_spec_version); + + expect(tactic.stix.x_mitre_deprecated).not.toBeDefined(); + }); + + it('PUT /api/tactics updates a tactic', async function () { + const originalModified = tactic1.stix.modified; + const timestamp = new Date().toISOString(); + tactic1.stix.modified = timestamp; + tactic1.stix.description = 'This is an updated tactic.'; + const body = tactic1; + const res = await request(app) + .put('/api/tactics/' + tactic1.stix.id + '/modified/' + originalModified) + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the updated tactic + const tactic = res.body; + expect(tactic).toBeDefined(); + expect(tactic.stix.id).toBe(tactic1.stix.id); + expect(tactic.stix.modified).toBe(tactic1.stix.modified); + }); + + it('POST /api/tactics does not create a tactic with the same id and modified date', async function () { + const body = tactic1; + // We expect to get the created tactic + await request(app) + .post('/api/tactics') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(409); + }); + + let tactic2; + it('POST /api/tactics should create a new version of a tactic with a duplicate stix.id but different stix.modified date', async function () { + tactic2 = _.cloneDeep(tactic1); + tactic2._id = undefined; + tactic2.__t = undefined; + tactic2.__v = undefined; + const timestamp = new Date().toISOString(); + tactic2.stix.description = 'Still a tactic. Red.'; + tactic2.stix.modified = timestamp; + const body = tactic2; + const res = await request(app) + .post('/api/tactics') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created tactic + const tactic = res.body; + expect(tactic).toBeDefined(); + }); + + let tactic3; + it('POST /api/tactics should create a new version of a tactic with a duplicate stix.id but different stix.modified date', async function () { + tactic3 = _.cloneDeep(tactic1); + tactic3._id = undefined; + tactic3.__t = undefined; + tactic3.__v = undefined; + const timestamp = new Date().toISOString(); + tactic3.stix.description = 'Still a tactic. Violet.'; + tactic3.stix.modified = timestamp; + const body = tactic3; + const res = await request(app) + .post('/api/tactics') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created tactic + const tactic = res.body; + expect(tactic).toBeDefined(); + }); + + it('GET /api/tactics returns the latest added tactic', async function () { + const res = await request(app) + .get('/api/tactics/' + tactic3.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one tactic in an array + const tactics = res.body; + expect(tactics).toBeDefined(); + expect(Array.isArray(tactics)).toBe(true); + expect(tactics.length).toBe(1); + const tactic = tactics[0]; + expect(tactic.stix.id).toBe(tactic3.stix.id); + expect(tactic.stix.modified).toBe(tactic3.stix.modified); + }); + + it('GET /api/tactics returns all added tactics', async function () { + const res = await request(app) + .get('/api/tactics/' + tactic1.stix.id + '?versions=all') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get two tactics in an array + const tactics = res.body; + expect(tactics).toBeDefined(); + expect(Array.isArray(tactics)).toBe(true); + expect(tactics.length).toBe(3); + }); + + it('GET /api/tactics/:id/modified/:modified returns the first added tactic', async function () { + const res = await request(app) + .get('/api/tactics/' + tactic1.stix.id + '/modified/' + tactic1.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one tactic in an array + const tactic = res.body; + expect(tactic).toBeDefined(); + expect(tactic.stix).toBeDefined(); + expect(tactic.stix.id).toBe(tactic1.stix.id); + expect(tactic.stix.modified).toBe(tactic1.stix.modified); + }); + + it('GET /api/tactics/:id/modified/:modified returns the second added tactic', async function () { + const res = await request(app) + .get('/api/tactics/' + tactic2.stix.id + '/modified/' + tactic2.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one tactic in an array + const tactic = res.body; + expect(tactic).toBeDefined(); + expect(tactic.stix).toBeDefined(); + expect(tactic.stix.id).toBe(tactic2.stix.id); + expect(tactic.stix.modified).toBe(tactic2.stix.modified); + }); + + it('GET /api/tactics uses the search parameter to return the latest version of the tactic', async function () { + const res = await request(app) + .get('/api/tactics?search=violet') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one tactic in an array + const tactics = res.body; + expect(tactics).toBeDefined(); + expect(Array.isArray(tactics)).toBe(true); + expect(tactics.length).toBe(1); + + // We expect it to be the latest version of the tactic + const tactic = tactics[0]; + expect(tactic).toBeDefined(); + expect(tactic.stix).toBeDefined(); + expect(tactic.stix.id).toBe(tactic3.stix.id); + expect(tactic.stix.modified).toBe(tactic3.stix.modified); + }); + + it('GET /api/tactics should not get the first version of the tactic when using the search parameter', async function () { + const res = await request(app) + .get('/api/tactics?search=yellow') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get zero tactics in an array + const tactics = res.body; + expect(tactics).toBeDefined(); + expect(Array.isArray(tactics)).toBe(true); + expect(tactics.length).toBe(0); + }); + + it('DELETE /api/tactics/:id should not delete a tactic when the id cannot be found', async function () { + await request(app) + .delete('/api/tactics/not-an-id') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('DELETE /api/tactics/:id/modified/:modified deletes a tactic', async function () { + await request(app) + .delete('/api/tactics/' + tactic1.stix.id + '/modified/' + tactic1.stix.modified) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('DELETE /api/tactics/:id should delete all the tactics with the same stix id', async function () { + await request(app) + .delete('/api/tactics/' + tactic2.stix.id) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('GET /api/tactics returns an empty array of tactics', async function () { + const res = await request(app) + .get('/api/tactics') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + const tactics = res.body; + expect(tactics).toBeDefined(); + expect(Array.isArray(tactics)).toBe(true); + expect(tactics.length).toBe(0); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/api/tactics/tactics.techniques.json b/app/tests/api/tactics/tactics.techniques.json index 4710bf28..0c6ad3e8 100644 --- a/app/tests/api/tactics/tactics.techniques.json +++ b/app/tests/api/tactics/tactics.techniques.json @@ -14,9 +14,7 @@ "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "created": "2022-08-17T10:10:10.000Z", "modified": "2022-08-17T10:10:10.000Z", - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "x_mitre_contents": [ { "object_ref": "attack-pattern--757471d4-d931-4109-82dd-cdd50c04744e", @@ -73,15 +71,9 @@ ] }, { - "x_mitre_platforms": [ - "Linux" - ], - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_platforms": ["Linux"], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "type": "attack-pattern", "id": "attack-pattern--757471d4-d931-4109-82dd-cdd50c04744e", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -113,15 +105,9 @@ "spec_version": "2.1" }, { - "x_mitre_platforms": [ - "Linux" - ], - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_platforms": ["Linux"], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "type": "attack-pattern", "id": "attack-pattern--9ca3f5e5-697d-41cd-ade4-c846ec12a816", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -149,15 +135,9 @@ "spec_version": "2.1" }, { - "x_mitre_platforms": [ - "Linux" - ], - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_platforms": ["Linux"], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "type": "attack-pattern", "id": "attack-pattern--dcbb7acb-0817-4e4f-b170-198c27d2e178", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -185,15 +165,9 @@ "spec_version": "2.1" }, { - "x_mitre_platforms": [ - "Linux" - ], - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_platforms": ["Linux"], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "type": "attack-pattern", "id": "attack-pattern--bcdab08e-9332-47ce-87b5-0837ac1550fd", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -225,15 +199,9 @@ "spec_version": "2.1" }, { - "x_mitre_platforms": [ - "Linux" - ], - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_platforms": ["Linux"], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "type": "attack-pattern", "id": "attack-pattern--5f1d1b95-ca0e-4d6a-a4d0-fcb568641a74", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -265,12 +233,8 @@ "spec_version": "2.1" }, { - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created": "2022-04-01T06:07:08.000Z", "description": "Enlil, ancient Mesopotamian god of wind, air, earth, and storms.", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -292,12 +256,8 @@ "spec_version": "2.1" }, { - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created": "2022-04-01T06:07:08.000Z", "description": "Enki, ancient Mesopotamian god of water and crafts.", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -319,12 +279,8 @@ "spec_version": "2.1" }, { - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created": "2022-03-01T06:07:08.000Z", "description": "Inanna, ancient Mesopotamian god of love, war, and fertility.", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -346,12 +302,8 @@ "spec_version": "2.1" }, { - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created": "2022-02-01T06:07:08.000Z", "description": "Nabu, ancient Mesopotamian god of literacy and wisdom.", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -373,12 +325,8 @@ "spec_version": "2.1" }, { - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created": "2022-01-01T06:07:08.000Z", "description": "Nanna-Suen, ancient Mesopotamian god of cattle.", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -400,12 +348,8 @@ "spec_version": "2.1" }, { - "x_mitre_domains": [ - "mobile-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_domains": ["mobile-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created": "2022-09-09T01:02:03.000Z", "description": "Nabu, ancient Mesopotamian god of mobile scribes. This tactic has the same x-mitre-shortname as another tactic, but a different domain.", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -427,9 +371,7 @@ "spec_version": "2.1" }, { - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "id": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "type": "identity", "identity_class": "organization", @@ -451,9 +393,7 @@ "definition_type": "statement", "x_mitre_attack_spec_version": "2.1.0", "spec_version": "2.1", - "x_mitre_domains": [ - "ics-attack" - ] + "x_mitre_domains": ["ics-attack"] } ] } diff --git a/app/tests/api/tactics/tactics.techniques.spec.js b/app/tests/api/tactics/tactics.techniques.spec.js index 6cd4598d..18b846be 100644 --- a/app/tests/api/tactics/tactics.techniques.spec.js +++ b/app/tests/api/tactics/tactics.techniques.spec.js @@ -15,109 +15,112 @@ const databaseConfiguration = require('../../../lib/database-configuration'); const collectionBundlesService = require('../../../services/collection-bundles-service'); async function readJson(path) { - const data = await fs.readFile(require.resolve(path)); - return JSON.parse(data); + const data = await fs.readFile(require.resolve(path)); + return JSON.parse(data); } const importBundle = util.promisify(collectionBundlesService.importBundle); describe('Tactics with Techniques API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - const collectionBundle = await readJson('./tactics.techniques.json'); - const collections = collectionBundle.objects.filter(object => object.type === 'x-mitre-collection'); - - const importOptions = {}; - await importBundle(collections[0], collectionBundle, importOptions); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - let tactic1; - let tactic2; - it('GET /api/tactics should return the preloaded tactics', async function () { - const res = await request(app) - .get('/api/tactics') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - const tactics = res.body; - expect(tactics).toBeDefined(); - expect(Array.isArray(tactics)).toBe(true); - expect(tactics.length).toBe(6); - - tactic1 = tactics.find(t => t.stix.x_mitre_shortname === 'enlil'); - tactic2 = tactics.find(t => t.stix.x_mitre_shortname === 'nabu'); - }); - - it('GET /api/techniques should return the preloaded techniques', async function () { - const res = await request(app) - .get('/api/techniques') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(5); - }); - - it('GET /api/tactics/:id/modified/:modified/techniques should not return the techniques when the tactic cannot be found', async function () { - await request(app) - .get(`/api/tactics/not-an-id/modified/2022-01-01T00:00:00.000Z/techniques`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('GET /api/tactics/:id/modified/:modified/techniques should return the techniques for tactic 1', async function () { - const res = await request(app) - .get(`/api/tactics/${ tactic1.stix.id }/modified/${ tactic1.stix.modified }/techniques`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - const tactics = res.body; - expect(tactics).toBeDefined(); - expect(Array.isArray(tactics)).toBe(true); - expect(tactics.length).toBe(2); - }); - - it('GET /api/tactics/:id/modified/:modified/techniques should return the first page of techniques for tactic 2', async function () { - const res = await request(app) - .get(`/api/tactics/${ tactic2.stix.id }/modified/${ tactic2.stix.modified }/techniques?offset=0&limit=2&includePagination=true`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - const results = res.body; - const tactics = results.data; - expect(tactics).toBeDefined(); - expect(Array.isArray(tactics)).toBe(true); - expect(tactics.length).toBe(2); - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + const collectionBundle = await readJson('./tactics.techniques.json'); + const collections = collectionBundle.objects.filter( + (object) => object.type === 'x-mitre-collection', + ); + + const importOptions = {}; + await importBundle(collections[0], collectionBundle, importOptions); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + let tactic1; + let tactic2; + it('GET /api/tactics should return the preloaded tactics', async function () { + const res = await request(app) + .get('/api/tactics') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + const tactics = res.body; + expect(tactics).toBeDefined(); + expect(Array.isArray(tactics)).toBe(true); + expect(tactics.length).toBe(6); + + tactic1 = tactics.find((t) => t.stix.x_mitre_shortname === 'enlil'); + tactic2 = tactics.find((t) => t.stix.x_mitre_shortname === 'nabu'); + }); + + it('GET /api/techniques should return the preloaded techniques', async function () { + const res = await request(app) + .get('/api/techniques') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(5); + }); + + it('GET /api/tactics/:id/modified/:modified/techniques should not return the techniques when the tactic cannot be found', async function () { + await request(app) + .get(`/api/tactics/not-an-id/modified/2022-01-01T00:00:00.000Z/techniques`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('GET /api/tactics/:id/modified/:modified/techniques should return the techniques for tactic 1', async function () { + const res = await request(app) + .get(`/api/tactics/${tactic1.stix.id}/modified/${tactic1.stix.modified}/techniques`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + const tactics = res.body; + expect(tactics).toBeDefined(); + expect(Array.isArray(tactics)).toBe(true); + expect(tactics.length).toBe(2); + }); + + it('GET /api/tactics/:id/modified/:modified/techniques should return the first page of techniques for tactic 2', async function () { + const res = await request(app) + .get( + `/api/tactics/${tactic2.stix.id}/modified/${tactic2.stix.modified}/techniques?offset=0&limit=2&includePagination=true`, + ) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + const results = res.body; + const tactics = results.data; + expect(tactics).toBeDefined(); + expect(Array.isArray(tactics)).toBe(true); + expect(tactics.length).toBe(2); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/api/teams/teams-invalid.spec.js b/app/tests/api/teams/teams-invalid.spec.js index cae18180..ae1c6e13 100644 --- a/app/tests/api/teams/teams-invalid.spec.js +++ b/app/tests/api/teams/teams-invalid.spec.js @@ -11,37 +11,37 @@ const teams = require('./teams.invalid.json'); const login = require('../../shared/login'); describe('Teams API Test Invalid Data', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + for (const teamData of teams) { + it(`POST /api/teams does not create a user account with invalid data (${teamData.description})`, async function () { + const body = teamData; + await request(app) + .post('/api/teams') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); }); + } - for (const teamData of teams) { - it(`POST /api/teams does not create a user account with invalid data (${ teamData.description })`, async function () { - const body = teamData; - await request(app) - .post('/api/teams') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - } - - after(async function() { - await database.closeConnection(); - }); + after(async function () { + await database.closeConnection(); + }); }); diff --git a/app/tests/api/teams/teams.spec.js b/app/tests/api/teams/teams.spec.js index 078a159d..3a60c8f0 100644 --- a/app/tests/api/teams/teams.spec.js +++ b/app/tests/api/teams/teams.spec.js @@ -13,204 +13,196 @@ const login = require('../../shared/login'); // need an example user to add to the team const exampleUser = { - "id": "1", - "email": "user1@test.com", - "username": "user1@test.com", - "displayName": "User 1", - "status": "active", - "role": "visitor", - "modified": new Date(), - "created": new Date(), + id: '1', + email: 'user1@test.com', + username: 'user1@test.com', + displayName: 'User 1', + status: 'active', + role: 'visitor', + modified: new Date(), + created: new Date(), }; // teamsId property will be created by REST API const initialObjectData = { - name: 'teamName', - description: 'teamDescription', - userIDs: [exampleUser.id], + name: 'teamName', + description: 'teamDescription', + userIDs: [exampleUser.id], }; describe('Teams API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Wait until the indexes are created - await UserAccount.init(); - await Team.init(); - - // Add an example user - const user1 = new UserAccount(exampleUser) - await user1.save(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - it('POST /api/teams does not create an empty team', async function () { - const body = {}; - await request(app) - .post('/api/teams') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - let team1; - it('POST /api/teams creates a team', async function () { - const body = initialObjectData; - const res = await request(app) - .post('/api/teams') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created team - team1 = res.body; - expect(team1).toBeDefined(); - expect(team1.id).toBeDefined(); - }); - - it('GET /api/teams returns the added team', async function () { - const res = await request(app) - .get('/api/teams') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the added team - const teams = res.body; - expect(teams).toBeDefined(); - expect(Array.isArray(teams)).toBe(true); - expect(teams.length).toBe(1); - - }); - - it('GET /api/teams/:id should not return a team when the id cannot be found', async function () { - await request(app) - .get('/api/teams/not-an-id') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('GET /api/teams/:id returns the added team', async function () { - const res = await request(app) - .get('/api/teams/' + team1.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one team in an array - const team = res.body; - expect(team).toBeDefined(); - expect(team.id).toBe(team1.id); - expect(team.name).toBe(team1.name); - expect(team.description).toBe(team1.description); - expect(team.userIDs.length).toBe(team1.userIDs.length); - expect(team.userIDs[0]).toBe(team1.userIDs[0]); - - // The created and modified timestamps should match - expect(team.created).toBeDefined(); - expect(team.modified).toBeDefined(); - expect(team.created).toEqual(team.modified); - - - }); - - - it('PUT /api/teams updates a team', async function () { - const body = team1; - body.description = 'updated'; - const res = await request(app) - .put('/api/teams/' + team1.id) - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the updated team - const team = res.body; - expect(team).toBeDefined(); - expect(team.id).toBe(team1.id); - - // The modified timestamp should be different from the created timestamp - expect(team.created).toBeDefined(); - expect(team.modified).toBeDefined(); - expect(team.created).not.toEqual(team.modified); - - - }); - - it('GET /api/teams uses the search parameter to return the team', async function () { - const res = await request(app) - .get('/api/teams?search=team') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one team in an array - const teams = res.body; - expect(teams).toBeDefined(); - expect(Array.isArray(teams)).toBe(true); - expect(teams.length).toBe(1); - - - }); - - it('POST /api/teams does not create a team with a duplicate name', async function () { - const body = initialObjectData; - await request(app) - .post('/api/teams') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(409); - }); - - it('GET /api/teams/:id/users returns a list of users', async function () { - const body = initialObjectData; - const res = await request(app) - .get(`/api/teams/${team1.id}/users`) - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one team in an array - const users = res.body; - expect(users).toBeDefined(); - expect(Array.isArray(users)).toBe(true); - expect(users.length).toBe(1); - expect(users[0].id).toBe(exampleUser.id); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Wait until the indexes are created + await UserAccount.init(); + await Team.init(); + + // Add an example user + const user1 = new UserAccount(exampleUser); + await user1.save(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + it('POST /api/teams does not create an empty team', async function () { + const body = {}; + await request(app) + .post('/api/teams') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + let team1; + it('POST /api/teams creates a team', async function () { + const body = initialObjectData; + const res = await request(app) + .post('/api/teams') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created team + team1 = res.body; + expect(team1).toBeDefined(); + expect(team1.id).toBeDefined(); + }); + + it('GET /api/teams returns the added team', async function () { + const res = await request(app) + .get('/api/teams') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the added team + const teams = res.body; + expect(teams).toBeDefined(); + expect(Array.isArray(teams)).toBe(true); + expect(teams.length).toBe(1); + }); + + it('GET /api/teams/:id should not return a team when the id cannot be found', async function () { + await request(app) + .get('/api/teams/not-an-id') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('GET /api/teams/:id returns the added team', async function () { + const res = await request(app) + .get('/api/teams/' + team1.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one team in an array + const team = res.body; + expect(team).toBeDefined(); + expect(team.id).toBe(team1.id); + expect(team.name).toBe(team1.name); + expect(team.description).toBe(team1.description); + expect(team.userIDs.length).toBe(team1.userIDs.length); + expect(team.userIDs[0]).toBe(team1.userIDs[0]); + + // The created and modified timestamps should match + expect(team.created).toBeDefined(); + expect(team.modified).toBeDefined(); + expect(team.created).toEqual(team.modified); }); - it('DELETE /api/teams deletes a teams', async function () { - await request(app) - .delete('/api/teams/' + team1.id) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - }); + it('PUT /api/teams updates a team', async function () { + const body = team1; + body.description = 'updated'; + const res = await request(app) + .put('/api/teams/' + team1.id) + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the updated team + const team = res.body; + expect(team).toBeDefined(); + expect(team.id).toBe(team1.id); + + // The modified timestamp should be different from the created timestamp + expect(team.created).toBeDefined(); + expect(team.modified).toBeDefined(); + expect(team.created).not.toEqual(team.modified); + }); - after(async function() { - await database.closeConnection(); - }); + it('GET /api/teams uses the search parameter to return the team', async function () { + const res = await request(app) + .get('/api/teams?search=team') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one team in an array + const teams = res.body; + expect(teams).toBeDefined(); + expect(Array.isArray(teams)).toBe(true); + expect(teams.length).toBe(1); + }); + + it('POST /api/teams does not create a team with a duplicate name', async function () { + const body = initialObjectData; + await request(app) + .post('/api/teams') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(409); + }); + + it('GET /api/teams/:id/users returns a list of users', async function () { + const body = initialObjectData; + const res = await request(app) + .get(`/api/teams/${team1.id}/users`) + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one team in an array + const users = res.body; + expect(users).toBeDefined(); + expect(Array.isArray(users)).toBe(true); + expect(users.length).toBe(1); + expect(users[0].id).toBe(exampleUser.id); + }); + + it('DELETE /api/teams deletes a teams', async function () { + await request(app) + .delete('/api/teams/' + team1.id) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + after(async function () { + await database.closeConnection(); + }); }); diff --git a/app/tests/api/teams/teams.valid.json b/app/tests/api/teams/teams.valid.json index f2141724..8694fdeb 100644 --- a/app/tests/api/teams/teams.valid.json +++ b/app/tests/api/teams/teams.valid.json @@ -2,6 +2,6 @@ { "name": "team1", "description": "exampleTeam", - "userIDs":[] + "userIDs": [] } ] diff --git a/app/tests/api/techniques/techniques-pagination.spec.js b/app/tests/api/techniques/techniques-pagination.spec.js index 0a095451..404aa76e 100644 --- a/app/tests/api/techniques/techniques-pagination.spec.js +++ b/app/tests/api/techniques/techniques-pagination.spec.js @@ -4,35 +4,31 @@ const PaginationTests = require('../../shared/pagination'); // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - spec_version: '2.1', - type: 'attack-pattern', - description: 'This is a technique.', - external_references: [ - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - kill_chain_phases: [ - { kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' } - ], - x_mitre_data_sources: [ 'data-source-1', 'data-source-2' ], - x_mitre_detection: 'detection text', - x_mitre_is_subtechnique: false, - x_mitre_impact_type: [ 'impact-1' ], - x_mitre_platforms: [ 'platform-1', 'platform-2' ] - } + }, + stix: { + spec_version: '2.1', + type: 'attack-pattern', + description: 'This is a technique.', + external_references: [{ source_name: 'source-1', external_id: 's1' }], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + kill_chain_phases: [{ kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' }], + x_mitre_data_sources: ['data-source-1', 'data-source-2'], + x_mitre_detection: 'detection text', + x_mitre_is_subtechnique: false, + x_mitre_impact_type: ['impact-1'], + x_mitre_platforms: ['platform-1', 'platform-2'], + }, }; const options = { - prefix: 'attack-pattern', - baseUrl: '/api/techniques', - label: 'Techniques' -} + prefix: 'attack-pattern', + baseUrl: '/api/techniques', + label: 'Techniques', +}; const paginationTests = new PaginationTests(techniquesService, initialObjectData, options); paginationTests.executeTests(); diff --git a/app/tests/api/techniques/techniques.query.json b/app/tests/api/techniques/techniques.query.json index 9ccb3466..fb6d51f2 100644 --- a/app/tests/api/techniques/techniques.query.json +++ b/app/tests/api/techniques/techniques.query.json @@ -1,25 +1,19 @@ { "workspace": { - "workflow": { - - } + "workflow": {} }, "stix": { "spec_version": "2.1", "type": "attack-pattern", "description": "This is a technique.", - "external_references": [ - { "source_name": "source-1", "external_id": "s1" } - ], - "object_marking_refs": [ "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" ], + "external_references": [{ "source_name": "source-1", "external_id": "s1" }], + "object_marking_refs": ["marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"], "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - "kill_chain_phases": [ - { "kill_chain_name": "kill-chain-name-1", "phase_name": "phase-1" } - ], - "x_mitre_data_sources": [ "data-source-1", "data-source-2" ], + "kill_chain_phases": [{ "kill_chain_name": "kill-chain-name-1", "phase_name": "phase-1" }], + "x_mitre_data_sources": ["data-source-1", "data-source-2"], "x_mitre_detection": "detection text", "x_mitre_is_subtechnique": false, - "x_mitre_impact_type": [ "impact-1" ], - "x_mitre_platforms": [ "platform-1", "platform-2" ] + "x_mitre_impact_type": ["impact-1"], + "x_mitre_platforms": ["platform-1", "platform-2"] } } diff --git a/app/tests/api/techniques/techniques.query.spec.js b/app/tests/api/techniques/techniques.query.spec.js index 161f105d..4414a962 100644 --- a/app/tests/api/techniques/techniques.query.spec.js +++ b/app/tests/api/techniques/techniques.query.spec.js @@ -16,316 +16,304 @@ const databaseConfiguration = require('../../../lib/database-configuration'); const techniquesService = require('../../../services/techniques-service'); function asyncWait(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } async function readJson(path) { - const data = await fs.readFile(require.resolve(path)); - return JSON.parse(data); + const data = await fs.readFile(require.resolve(path)); + return JSON.parse(data); } async function configureTechniques(baseTechnique) { - const techniques = []; - // x_mitre_deprecated,revoked undefined - // state undefined - const data1 = _.cloneDeep(baseTechnique); - techniques.push(data1); - - // x_mitre_deprecated = false, revoked = false - // state = work-in-progress - const data2 = _.cloneDeep(baseTechnique); - data2.stix.x_mitre_deprecated = false; - data2.stix.revoked = false; - data2.stix.x_mitre_domains = [ 'mobile-attack' ]; - data2.stix.x_mitre_platforms.push('platform-3'); - data2.workspace.workflow = { state: 'work-in-progress' }; - techniques.push(data2); - - // x_mitre_deprecated = true, revoked = false - // state = awaiting-review - const data3 = _.cloneDeep(baseTechnique); - data3.stix.x_mitre_deprecated = true; - data3.stix.revoked = false; - data3.workspace.workflow = { state: 'awaiting-review' }; - techniques.push(data3); - - // x_mitre_deprecated = false, revoked = true - // state = awaiting-review - const data4 = _.cloneDeep(baseTechnique); - data4.stix.x_mitre_deprecated = false; - data4.stix.revoked = true; - data4.workspace.workflow = { state: 'awaiting-review' }; - techniques.push(data4); - - // multiple versions, last version has x_mitre_deprecated = true, revoked = true - // state = awaiting-review - const data5a = _.cloneDeep(baseTechnique); - const id = `attack-pattern--${uuid.v4()}`; - data5a.stix.id = id; - data5a.stix.name = 'multiple-versions' - data5a.workspace.workflow = { state: 'awaiting-review' }; - const createdTimestamp = new Date().toISOString(); - data5a.stix.created = createdTimestamp; - data5a.stix.modified = createdTimestamp; - techniques.push(data5a); - - await asyncWait(10); // wait so the modified timestamp can change - const data5b = _.cloneDeep(baseTechnique); - data5b.stix.id = id; - data5b.stix.name = 'multiple-versions' - data5b.workspace.workflow = { state: 'awaiting-review' }; - data5b.stix.created = createdTimestamp; - let timestamp = new Date().toISOString(); - data5b.stix.modified = timestamp; - techniques.push(data5b); - - await asyncWait(10); - const data5c = _.cloneDeep(baseTechnique); - data5c.stix.id = id; - data5c.stix.name = 'multiple-versions' - data5c.workspace.workflow = { state: 'awaiting-review' }; - data5c.stix.x_mitre_deprecated = true; - data5c.stix.revoked = true; - data5c.stix.created = createdTimestamp; - timestamp = new Date().toISOString(); - data5c.stix.modified = timestamp; - techniques.push(data5c); - - // x_mitre_deprecated,revoked undefined - // state = work-in-progress - const data6 = _.cloneDeep(baseTechnique); - data6.stix.x_mitre_deprecated = false; - data6.stix.revoked = false; - data6.workspace.workflow = { state: 'work-in-progress' }; - techniques.push(data6); - - // x_mitre_deprecated,revoked undefined - // state = reviewed - const data7 = _.cloneDeep(baseTechnique); - data7.stix.x_mitre_deprecated = false; - data7.stix.revoked = false; - data7.workspace.workflow = { state: 'reviewed' }; - techniques.push(data7); - - return techniques; + const techniques = []; + // x_mitre_deprecated,revoked undefined + // state undefined + const data1 = _.cloneDeep(baseTechnique); + techniques.push(data1); + + // x_mitre_deprecated = false, revoked = false + // state = work-in-progress + const data2 = _.cloneDeep(baseTechnique); + data2.stix.x_mitre_deprecated = false; + data2.stix.revoked = false; + data2.stix.x_mitre_domains = ['mobile-attack']; + data2.stix.x_mitre_platforms.push('platform-3'); + data2.workspace.workflow = { state: 'work-in-progress' }; + techniques.push(data2); + + // x_mitre_deprecated = true, revoked = false + // state = awaiting-review + const data3 = _.cloneDeep(baseTechnique); + data3.stix.x_mitre_deprecated = true; + data3.stix.revoked = false; + data3.workspace.workflow = { state: 'awaiting-review' }; + techniques.push(data3); + + // x_mitre_deprecated = false, revoked = true + // state = awaiting-review + const data4 = _.cloneDeep(baseTechnique); + data4.stix.x_mitre_deprecated = false; + data4.stix.revoked = true; + data4.workspace.workflow = { state: 'awaiting-review' }; + techniques.push(data4); + + // multiple versions, last version has x_mitre_deprecated = true, revoked = true + // state = awaiting-review + const data5a = _.cloneDeep(baseTechnique); + const id = `attack-pattern--${uuid.v4()}`; + data5a.stix.id = id; + data5a.stix.name = 'multiple-versions'; + data5a.workspace.workflow = { state: 'awaiting-review' }; + const createdTimestamp = new Date().toISOString(); + data5a.stix.created = createdTimestamp; + data5a.stix.modified = createdTimestamp; + techniques.push(data5a); + + await asyncWait(10); // wait so the modified timestamp can change + const data5b = _.cloneDeep(baseTechnique); + data5b.stix.id = id; + data5b.stix.name = 'multiple-versions'; + data5b.workspace.workflow = { state: 'awaiting-review' }; + data5b.stix.created = createdTimestamp; + let timestamp = new Date().toISOString(); + data5b.stix.modified = timestamp; + techniques.push(data5b); + + await asyncWait(10); + const data5c = _.cloneDeep(baseTechnique); + data5c.stix.id = id; + data5c.stix.name = 'multiple-versions'; + data5c.workspace.workflow = { state: 'awaiting-review' }; + data5c.stix.x_mitre_deprecated = true; + data5c.stix.revoked = true; + data5c.stix.created = createdTimestamp; + timestamp = new Date().toISOString(); + data5c.stix.modified = timestamp; + techniques.push(data5c); + + // x_mitre_deprecated,revoked undefined + // state = work-in-progress + const data6 = _.cloneDeep(baseTechnique); + data6.stix.x_mitre_deprecated = false; + data6.stix.revoked = false; + data6.workspace.workflow = { state: 'work-in-progress' }; + techniques.push(data6); + + // x_mitre_deprecated,revoked undefined + // state = reviewed + const data7 = _.cloneDeep(baseTechnique); + data7.stix.x_mitre_deprecated = false; + data7.stix.revoked = false; + data7.workspace.workflow = { state: 'reviewed' }; + techniques.push(data7); + + return techniques; } async function loadTechniques(techniques) { - for (const technique of techniques) { - if (!technique.stix.name) { - technique.stix.name = `attack-pattern-${technique.stix.x_mitre_deprecated}-${technique.stix.revoked}`; - } - - if (!technique.stix.created) { - const timestamp = new Date().toISOString(); - technique.stix.created = timestamp; - technique.stix.modified = timestamp; - } - - // eslint-disable-next-line no-await-in-loop - await techniquesService.create(technique); + for (const technique of techniques) { + if (!technique.stix.name) { + technique.stix.name = `attack-pattern-${technique.stix.x_mitre_deprecated}-${technique.stix.revoked}`; } + + if (!technique.stix.created) { + const timestamp = new Date().toISOString(); + technique.stix.created = timestamp; + technique.stix.modified = timestamp; + } + + // eslint-disable-next-line no-await-in-loop + await techniquesService.create(technique); + } } describe('Techniques Query API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - const baseTechnique = await readJson('./techniques.query.json'); - const techniques = await configureTechniques(baseTechnique); - await loadTechniques(techniques); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - it('GET /api/techniques should return the preloaded techniques (not deprecated, not revoked)', async function () { - const res = await request(app) - .get('/api/techniques') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // Expect techniques 1, 2, 6, and 7 - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(4); - - }); - - it('GET /api/techniques should return techniques with x_mitre_deprecated not set to true (false or undefined)', async function () { - const res = await request(app) - .get('/api/techniques?includeDeprecated=false') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // Expect techniques 1, 2, 6, and 7 - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(4); - - }); - - it('GET /api/techniques should include deprecated techniques (excluding revoked)', async function () { - const res = await request(app) - .get('/api/techniques?includeDeprecated=true') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // Expect techniques 1, 2, 3, 6, and 7 - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(5); - - }); - - it('GET /api/techniques should return techniques with revoked not set to true (false or undefined)', async function () { - const res = await request(app) - .get('/api/techniques?includeRevoked=false') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // Expect techniques 1,2, 6, and 7 - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(4); - - }); - - it('GET /api/techniques should include revoked techniques (but not deprecated)', async function () { - const res = await request(app) - .get('/api/techniques?includeRevoked=true') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // Expect techniques 1, 2, 4, 6, and 7 - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(5); - - }); - - it('GET /api/techniques should return techniques with workflow.state set to work-in-progress', async function () { - const res = await request(app) - .get('/api/techniques?state=work-in-progress') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // Expect techniques 2 and 6 - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(2); - - }); - - it('GET /api/techniques should return techniques with workflow.state set to work-in-progress or reviewed', async function () { - const res = await request(app) - .get('/api/techniques?state=work-in-progress&state=reviewed') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // Expect techniques 2, 6, and 7 - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(3); - - }); - - it('GET /api/techniques should return techniques containing the domain', async function () { - const res = await request(app) - .get('/api/techniques?domain=mobile-attack') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // Expect technique 2 - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(1); - - }); - - it('GET /api/techniques should not return any techniques when searching for a non-existent domain', async function () { - const res = await request(app) - .get('/api/techniques?domain=not-a-domain') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(0); - - }); - - it('GET /api/techniques should return techniques containing the platform', async function () { - const res = await request(app) - .get('/api/techniques?platform=platform-3') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // Expect technique 2 - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(1); - - }); - - it('GET /api/techniques should not return any techniques when searching for a non-existent platform', async function () { - const res = await request(app) - .get('/api/techniques?platform=not-a-platform') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(0); - - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + const baseTechnique = await readJson('./techniques.query.json'); + const techniques = await configureTechniques(baseTechnique); + await loadTechniques(techniques); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + it('GET /api/techniques should return the preloaded techniques (not deprecated, not revoked)', async function () { + const res = await request(app) + .get('/api/techniques') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // Expect techniques 1, 2, 6, and 7 + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(4); + }); + + it('GET /api/techniques should return techniques with x_mitre_deprecated not set to true (false or undefined)', async function () { + const res = await request(app) + .get('/api/techniques?includeDeprecated=false') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // Expect techniques 1, 2, 6, and 7 + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(4); + }); + + it('GET /api/techniques should include deprecated techniques (excluding revoked)', async function () { + const res = await request(app) + .get('/api/techniques?includeDeprecated=true') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // Expect techniques 1, 2, 3, 6, and 7 + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(5); + }); + + it('GET /api/techniques should return techniques with revoked not set to true (false or undefined)', async function () { + const res = await request(app) + .get('/api/techniques?includeRevoked=false') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // Expect techniques 1,2, 6, and 7 + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(4); + }); + + it('GET /api/techniques should include revoked techniques (but not deprecated)', async function () { + const res = await request(app) + .get('/api/techniques?includeRevoked=true') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // Expect techniques 1, 2, 4, 6, and 7 + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(5); + }); + + it('GET /api/techniques should return techniques with workflow.state set to work-in-progress', async function () { + const res = await request(app) + .get('/api/techniques?state=work-in-progress') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // Expect techniques 2 and 6 + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(2); + }); + + it('GET /api/techniques should return techniques with workflow.state set to work-in-progress or reviewed', async function () { + const res = await request(app) + .get('/api/techniques?state=work-in-progress&state=reviewed') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // Expect techniques 2, 6, and 7 + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(3); + }); + + it('GET /api/techniques should return techniques containing the domain', async function () { + const res = await request(app) + .get('/api/techniques?domain=mobile-attack') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // Expect technique 2 + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(1); + }); + + it('GET /api/techniques should not return any techniques when searching for a non-existent domain', async function () { + const res = await request(app) + .get('/api/techniques?domain=not-a-domain') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(0); + }); + + it('GET /api/techniques should return techniques containing the platform', async function () { + const res = await request(app) + .get('/api/techniques?platform=platform-3') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // Expect technique 2 + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(1); + }); + + it('GET /api/techniques should not return any techniques when searching for a non-existent platform', async function () { + const res = await request(app) + .get('/api/techniques?platform=not-a-platform') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(0); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/api/techniques/techniques.spec.js b/app/tests/api/techniques/techniques.spec.js index ed9c63a1..02259b5a 100644 --- a/app/tests/api/techniques/techniques.spec.js +++ b/app/tests/api/techniques/techniques.spec.js @@ -14,420 +14,427 @@ logger.level = 'debug'; // modified and created properties will be set before calling REST API // stix.id property will be created by REST API const initialObjectData = { - workspace: { - workflow: { - state: 'work-in-progress' - } + workspace: { + workflow: { + state: 'work-in-progress', }, - stix: { - name: 'attack-pattern-1', - spec_version: '2.1', - type: 'attack-pattern', - description: 'This is a technique. Orange.', - external_references: [ - { source_name: 'mitre-attack', external_id: 'T9999', url: 'https://attack.mitre.org/techniques/T9999' }, - { source_name: 'source-1', external_id: 's1' } - ], - object_marking_refs: [ 'marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168' ], - created_by_ref: "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", - kill_chain_phases: [ - { kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' } - ], - x_mitre_modified_by_ref: "identity--d6424da5-85a0-496e-ae17-494499271108", - x_mitre_data_sources: [ 'data-source-1', 'data-source-2' ], - x_mitre_detection: 'detection text', - x_mitre_is_subtechnique: false, - x_mitre_impact_type: [ 'impact-1' ], - x_mitre_platforms: [ 'platform-1', 'platform-2' ], - x_mitre_network_requirements: true - } + }, + stix: { + name: 'attack-pattern-1', + spec_version: '2.1', + type: 'attack-pattern', + description: 'This is a technique. Orange.', + external_references: [ + { + source_name: 'mitre-attack', + external_id: 'T9999', + url: 'https://attack.mitre.org/techniques/T9999', + }, + { source_name: 'source-1', external_id: 's1' }, + ], + object_marking_refs: ['marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168'], + created_by_ref: 'identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5', + kill_chain_phases: [{ kill_chain_name: 'kill-chain-name-1', phase_name: 'phase-1' }], + x_mitre_modified_by_ref: 'identity--d6424da5-85a0-496e-ae17-494499271108', + x_mitre_data_sources: ['data-source-1', 'data-source-2'], + x_mitre_detection: 'detection text', + x_mitre_is_subtechnique: false, + x_mitre_impact_type: ['impact-1'], + x_mitre_platforms: ['platform-1', 'platform-2'], + x_mitre_network_requirements: true, + }, }; describe('Techniques Basic API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - it('GET /api/techniques returns an empty array of techniques', async function () { - const res = await request(app) - .get('/api/techniques') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(0); - }); - - it('POST /api/techniques does not create an empty technique', async function () { - const body = {}; - await request(app) - .post('/api/techniques') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - let technique1; - it('POST /api/techniques creates a technique', async function () { - const timestamp = new Date().toISOString(); - initialObjectData.stix.created = timestamp; - initialObjectData.stix.modified = timestamp; - const body = initialObjectData; - const res = await request(app) - .post('/api/techniques') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created technique - technique1 = res.body; - expect(technique1).toBeDefined(); - expect(technique1.stix).toBeDefined(); - expect(technique1.stix.id).toBeDefined(); - expect(technique1.stix.created).toBeDefined(); - expect(technique1.stix.modified).toBeDefined(); - expect(technique1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); - expect(technique1.workspace.workflow.created_by_user_account).toBeDefined(); - expect(technique1.workspace.attack_id).toBeDefined(); - - }); - - it('GET /api/techniques returns the added technique', async function () { - const res = await request(app) - .get('/api/techniques') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one technique in an array - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(1); - - }); - - it('GET /api/techniques/:id should not return a technique when the id cannot be found', async function () { - await request(app) - .get('/api/techniques/not-an-id') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('GET /api/techniques/:id returns the added technique', async function () { - const res = await request(app) - .get('/api/techniques/' + technique1.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one technique in an array - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(1); - - const technique = techniques[0]; - expect(technique).toBeDefined(); - expect(technique.stix).toBeDefined(); - expect(technique.stix.id).toBe(technique1.stix.id); - expect(technique.stix.type).toBe(technique1.stix.type); - expect(technique.stix.name).toBe(technique1.stix.name); - expect(technique.stix.description).toBe(technique1.stix.description); - expect(technique.stix.spec_version).toBe(technique1.stix.spec_version); - expect(technique.stix.object_marking_refs).toEqual(expect.arrayContaining(technique1.stix.object_marking_refs)); - expect(technique.stix.created_by_ref).toBe(technique1.stix.created_by_ref); - expect(technique.stix.x_mitre_modified_by_ref).toBe(technique1.stix.x_mitre_modified_by_ref); - expect(technique.stix.x_mitre_data_sources).toEqual(expect.arrayContaining(technique1.stix.x_mitre_data_sources)); - expect(technique.stix.x_mitre_detection).toBe(technique1.stix.x_mitre_detection); - expect(technique.stix.x_mitre_is_subtechnique).toBe(technique1.stix.x_mitre_is_subtechnique); - expect(technique.stix.x_mitre_impact_type).toEqual(expect.arrayContaining(technique1.stix.x_mitre_impact_type)); - expect(technique.stix.x_mitre_network_requirements).toEqual(technique1.stix.x_mitre_network_requirements); - expect(technique.stix.x_mitre_platforms).toEqual(expect.arrayContaining(technique1.stix.x_mitre_platforms)); - expect(technique.stix.x_mitre_attack_spec_version).toBe(technique1.stix.x_mitre_attack_spec_version); - - expect(technique.workspace.attack_id).toEqual(technique1.workspace.attack_id); - - expect(technique.stix.x_mitre_deprecated).not.toBeDefined(); - expect(technique.stix.x_mitre_defense_bypassed).not.toBeDefined(); - expect(technique.stix.x_mitre_permissions_required).not.toBeDefined(); - expect(technique.stix.x_mitre_system_requirements).not.toBeDefined(); - expect(technique.stix.x_mitre_tactic_type).not.toBeDefined(); - - expect(technique.created_by_identity).toBeDefined(); - expect(technique.modified_by_identity).toBeDefined(); - expect(technique.created_by_user_account).toBeDefined(); - - }); - - it('PUT /api/techniques updates a technique', async function () { - const originalModified = technique1.stix.modified; - const timestamp = new Date().toISOString(); - technique1.stix.modified = timestamp; - technique1.stix.description = 'This is an updated technique.' - const body = technique1; - const res = await request(app) - .put('/api/techniques/' + technique1.stix.id + '/modified/' + originalModified) - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the updated technique - const technique = res.body; - expect(technique).toBeDefined(); - expect(technique.stix.id).toBe(technique1.stix.id); - expect(technique.stix.modified).toBe(technique1.stix.modified); - }); - - it('POST /api/techniques does not create a technique with the same id and modified date', async function () { - const body = technique1; - await request(app) - .post('/api/techniques') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(409); - }); - - let technique2; - it('POST /api/techniques should create a new version of a technique with a duplicate stix.id but different stix.modified date', async function () { - technique2 = _.cloneDeep(technique1); - technique2._id = undefined; - technique2.__t = undefined; - technique2.__v = undefined; - const timestamp = new Date().toISOString(); - technique2.stix.modified = timestamp; - technique2.stix.description = 'Still a technique. Purple!' - const body = technique2; - const res = await request(app) - .post('/api/techniques') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created technique - const technique = res.body; - expect(technique).toBeDefined(); - - }); - - - let technique3; - it('POST /api/techniques should create a new version of a technique with a duplicate stix.id but different stix.modified date', async function () { - technique3 = _.cloneDeep(technique1); - technique3._id = undefined; - technique3.__t = undefined; - technique3.__v = undefined; - const timestamp = new Date().toISOString(); - technique3.stix.modified = timestamp; - technique3.stix.description = 'Still a technique. Blue!' - const body = technique3; - const res = await request(app) - .post('/api/techniques') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created technique - const technique = res.body; - expect(technique).toBeDefined(); - }); - - it('GET /api/techniques returns the latest added technique', async function () { - const res = await request(app) - .get('/api/techniques/' + technique3.stix.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one technique in an array - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(1); - const technique = techniques[0]; - expect(technique.stix.id).toBe(technique3.stix.id); - expect(technique.stix.modified).toBe(technique3.stix.modified); - }); - - it('GET /api/techniques returns all added techniques', async function () { - const res = await request(app) - .get('/api/techniques/' + technique1.stix.id + '?versions=all') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get two techniques in an array - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(3); - }); - - it('GET /api/techniques/:id/modified/:modified returns the first added technique', async function () { - const res = await request(app) - .get('/api/techniques/' + technique1.stix.id + '/modified/' + technique1.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one technique in an array - const technique = res.body; - expect(technique).toBeDefined(); - expect(technique.stix).toBeDefined(); - expect(technique.stix.id).toBe(technique1.stix.id); - expect(technique.stix.modified).toBe(technique1.stix.modified); - }); - - it('GET /api/techniques/:id/modified/:modified returns the second added technique', async function () { - const res = await request(app) - .get('/api/techniques/' + technique2.stix.id + '/modified/' + technique2.stix.modified) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one technique in an array - const technique = res.body; - expect(technique).toBeDefined(); - expect(technique.stix).toBeDefined(); - expect(technique.stix.id).toBe(technique2.stix.id); - expect(technique.stix.modified).toBe(technique2.stix.modified); - }); - - it('GET /api/techniques uses the search parameter to return the latest version of the technique', async function () { - const res = await request(app) - .get('/api/techniques?search=blue') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one technique in an array - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(1); - - // We expect it to be the latest version of the technique - const technique = techniques[0]; - expect(technique).toBeDefined(); - expect(technique.stix).toBeDefined(); - expect(technique.stix.id).toBe(technique3.stix.id); - expect(technique.stix.modified).toBe(technique3.stix.modified); - - }); - - it('GET /api/techniques uses the search parameter (ATT&CK ID) to return the latest version of the technique', async function () { - const res = await request(app) - .get('/api/techniques?search=T9999') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one technique in an array - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(1); - - // We expect it to be the latest version of the technique - const technique = techniques[0]; - expect(technique).toBeDefined(); - expect(technique.stix).toBeDefined(); - expect(technique.stix.id).toBe(technique3.stix.id); - expect(technique.stix.modified).toBe(technique3.stix.modified); - expect(technique.workspace.attack_id).toEqual('T9999'); - }); - - it('GET /api/techniques should not get the first version of the techniques when using the search parameter', async function () { - const res = await request(app) - .get('/api/techniques?search=orange') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get zero techniques in an array - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(0); - }); - - it('DELETE /api/techniques/:id should not delete a technique when the id cannot be found', async function () { - await request(app) - .delete('/api/techniques/not-an-id') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('DELETE /api/techniques/:id/modified/:modified deletes a technique', async function () { - await request(app) - .delete('/api/techniques/' + technique1.stix.id + '/modified/' + technique1.stix.modified) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - }); - - it('DELETE /api/techniques/:id should delete all the techniques with the same stix id', async function () { - await request(app) - .delete('/api/techniques/' + technique2.stix.id) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - }); - - - it('GET /api/techniques returns an empty array of techniques', async function () { - const res = await request(app) - .get('/api/techniques') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(0); - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + it('GET /api/techniques returns an empty array of techniques', async function () { + const res = await request(app) + .get('/api/techniques') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(0); + }); + + it('POST /api/techniques does not create an empty technique', async function () { + const body = {}; + await request(app) + .post('/api/techniques') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + let technique1; + it('POST /api/techniques creates a technique', async function () { + const timestamp = new Date().toISOString(); + initialObjectData.stix.created = timestamp; + initialObjectData.stix.modified = timestamp; + const body = initialObjectData; + const res = await request(app) + .post('/api/techniques') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created technique + technique1 = res.body; + expect(technique1).toBeDefined(); + expect(technique1.stix).toBeDefined(); + expect(technique1.stix.id).toBeDefined(); + expect(technique1.stix.created).toBeDefined(); + expect(technique1.stix.modified).toBeDefined(); + expect(technique1.stix.x_mitre_attack_spec_version).toBe(config.app.attackSpecVersion); + expect(technique1.workspace.workflow.created_by_user_account).toBeDefined(); + expect(technique1.workspace.attack_id).toBeDefined(); + }); + + it('GET /api/techniques returns the added technique', async function () { + const res = await request(app) + .get('/api/techniques') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one technique in an array + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(1); + }); + + it('GET /api/techniques/:id should not return a technique when the id cannot be found', async function () { + await request(app) + .get('/api/techniques/not-an-id') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('GET /api/techniques/:id returns the added technique', async function () { + const res = await request(app) + .get('/api/techniques/' + technique1.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one technique in an array + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(1); + + const technique = techniques[0]; + expect(technique).toBeDefined(); + expect(technique.stix).toBeDefined(); + expect(technique.stix.id).toBe(technique1.stix.id); + expect(technique.stix.type).toBe(technique1.stix.type); + expect(technique.stix.name).toBe(technique1.stix.name); + expect(technique.stix.description).toBe(technique1.stix.description); + expect(technique.stix.spec_version).toBe(technique1.stix.spec_version); + expect(technique.stix.object_marking_refs).toEqual( + expect.arrayContaining(technique1.stix.object_marking_refs), + ); + expect(technique.stix.created_by_ref).toBe(technique1.stix.created_by_ref); + expect(technique.stix.x_mitre_modified_by_ref).toBe(technique1.stix.x_mitre_modified_by_ref); + expect(technique.stix.x_mitre_data_sources).toEqual( + expect.arrayContaining(technique1.stix.x_mitre_data_sources), + ); + expect(technique.stix.x_mitre_detection).toBe(technique1.stix.x_mitre_detection); + expect(technique.stix.x_mitre_is_subtechnique).toBe(technique1.stix.x_mitre_is_subtechnique); + expect(technique.stix.x_mitre_impact_type).toEqual( + expect.arrayContaining(technique1.stix.x_mitre_impact_type), + ); + expect(technique.stix.x_mitre_network_requirements).toEqual( + technique1.stix.x_mitre_network_requirements, + ); + expect(technique.stix.x_mitre_platforms).toEqual( + expect.arrayContaining(technique1.stix.x_mitre_platforms), + ); + expect(technique.stix.x_mitre_attack_spec_version).toBe( + technique1.stix.x_mitre_attack_spec_version, + ); + + expect(technique.workspace.attack_id).toEqual(technique1.workspace.attack_id); + + expect(technique.stix.x_mitre_deprecated).not.toBeDefined(); + expect(technique.stix.x_mitre_defense_bypassed).not.toBeDefined(); + expect(technique.stix.x_mitre_permissions_required).not.toBeDefined(); + expect(technique.stix.x_mitre_system_requirements).not.toBeDefined(); + expect(technique.stix.x_mitre_tactic_type).not.toBeDefined(); + + expect(technique.created_by_identity).toBeDefined(); + expect(technique.modified_by_identity).toBeDefined(); + expect(technique.created_by_user_account).toBeDefined(); + }); + + it('PUT /api/techniques updates a technique', async function () { + const originalModified = technique1.stix.modified; + const timestamp = new Date().toISOString(); + technique1.stix.modified = timestamp; + technique1.stix.description = 'This is an updated technique.'; + const body = technique1; + const res = await request(app) + .put('/api/techniques/' + technique1.stix.id + '/modified/' + originalModified) + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the updated technique + const technique = res.body; + expect(technique).toBeDefined(); + expect(technique.stix.id).toBe(technique1.stix.id); + expect(technique.stix.modified).toBe(technique1.stix.modified); + }); + + it('POST /api/techniques does not create a technique with the same id and modified date', async function () { + const body = technique1; + await request(app) + .post('/api/techniques') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(409); + }); + + let technique2; + it('POST /api/techniques should create a new version of a technique with a duplicate stix.id but different stix.modified date', async function () { + technique2 = _.cloneDeep(technique1); + technique2._id = undefined; + technique2.__t = undefined; + technique2.__v = undefined; + const timestamp = new Date().toISOString(); + technique2.stix.modified = timestamp; + technique2.stix.description = 'Still a technique. Purple!'; + const body = technique2; + const res = await request(app) + .post('/api/techniques') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created technique + const technique = res.body; + expect(technique).toBeDefined(); + }); + + let technique3; + it('POST /api/techniques should create a new version of a technique with a duplicate stix.id but different stix.modified date', async function () { + technique3 = _.cloneDeep(technique1); + technique3._id = undefined; + technique3.__t = undefined; + technique3.__v = undefined; + const timestamp = new Date().toISOString(); + technique3.stix.modified = timestamp; + technique3.stix.description = 'Still a technique. Blue!'; + const body = technique3; + const res = await request(app) + .post('/api/techniques') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created technique + const technique = res.body; + expect(technique).toBeDefined(); + }); + + it('GET /api/techniques returns the latest added technique', async function () { + const res = await request(app) + .get('/api/techniques/' + technique3.stix.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one technique in an array + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(1); + const technique = techniques[0]; + expect(technique.stix.id).toBe(technique3.stix.id); + expect(technique.stix.modified).toBe(technique3.stix.modified); + }); + + it('GET /api/techniques returns all added techniques', async function () { + const res = await request(app) + .get('/api/techniques/' + technique1.stix.id + '?versions=all') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get two techniques in an array + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(3); + }); + + it('GET /api/techniques/:id/modified/:modified returns the first added technique', async function () { + const res = await request(app) + .get('/api/techniques/' + technique1.stix.id + '/modified/' + technique1.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one technique in an array + const technique = res.body; + expect(technique).toBeDefined(); + expect(technique.stix).toBeDefined(); + expect(technique.stix.id).toBe(technique1.stix.id); + expect(technique.stix.modified).toBe(technique1.stix.modified); + }); + + it('GET /api/techniques/:id/modified/:modified returns the second added technique', async function () { + const res = await request(app) + .get('/api/techniques/' + technique2.stix.id + '/modified/' + technique2.stix.modified) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one technique in an array + const technique = res.body; + expect(technique).toBeDefined(); + expect(technique.stix).toBeDefined(); + expect(technique.stix.id).toBe(technique2.stix.id); + expect(technique.stix.modified).toBe(technique2.stix.modified); + }); + + it('GET /api/techniques uses the search parameter to return the latest version of the technique', async function () { + const res = await request(app) + .get('/api/techniques?search=blue') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one technique in an array + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(1); + + // We expect it to be the latest version of the technique + const technique = techniques[0]; + expect(technique).toBeDefined(); + expect(technique.stix).toBeDefined(); + expect(technique.stix.id).toBe(technique3.stix.id); + expect(technique.stix.modified).toBe(technique3.stix.modified); + }); + + it('GET /api/techniques uses the search parameter (ATT&CK ID) to return the latest version of the technique', async function () { + const res = await request(app) + .get('/api/techniques?search=T9999') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one technique in an array + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(1); + + // We expect it to be the latest version of the technique + const technique = techniques[0]; + expect(technique).toBeDefined(); + expect(technique.stix).toBeDefined(); + expect(technique.stix.id).toBe(technique3.stix.id); + expect(technique.stix.modified).toBe(technique3.stix.modified); + expect(technique.workspace.attack_id).toEqual('T9999'); + }); + + it('GET /api/techniques should not get the first version of the techniques when using the search parameter', async function () { + const res = await request(app) + .get('/api/techniques?search=orange') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get zero techniques in an array + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(0); + }); + + it('DELETE /api/techniques/:id should not delete a technique when the id cannot be found', async function () { + await request(app) + .delete('/api/techniques/not-an-id') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('DELETE /api/techniques/:id/modified/:modified deletes a technique', async function () { + await request(app) + .delete('/api/techniques/' + technique1.stix.id + '/modified/' + technique1.stix.modified) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('DELETE /api/techniques/:id should delete all the techniques with the same stix id', async function () { + await request(app) + .delete('/api/techniques/' + technique2.stix.id) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + it('GET /api/techniques returns an empty array of techniques', async function () { + const res = await request(app) + .get('/api/techniques') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(0); + }); + + after(async function () { + await database.closeConnection(); + }); }); diff --git a/app/tests/api/techniques/techniques.tactics.json b/app/tests/api/techniques/techniques.tactics.json index 8a6cf90c..f3cb0ee3 100644 --- a/app/tests/api/techniques/techniques.tactics.json +++ b/app/tests/api/techniques/techniques.tactics.json @@ -14,9 +14,7 @@ "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "created": "2022-08-17T10:10:10.000Z", "modified": "2022-08-17T10:10:10.000Z", - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "x_mitre_contents": [ { "object_ref": "attack-pattern--757471d4-d931-4109-82dd-cdd50c04744e", @@ -61,15 +59,9 @@ ] }, { - "x_mitre_platforms": [ - "Linux" - ], - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_platforms": ["Linux"], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "type": "attack-pattern", "id": "attack-pattern--757471d4-d931-4109-82dd-cdd50c04744e", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -101,15 +93,9 @@ "spec_version": "2.1" }, { - "x_mitre_platforms": [ - "Linux" - ], - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_platforms": ["Linux"], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "type": "attack-pattern", "id": "attack-pattern--fb93f707-fd21-421f-a8b3-4baee9211b3a", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -153,12 +139,8 @@ "spec_version": "2.1" }, { - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created": "2022-04-01T06:07:08.000Z", "description": "Enlil, ancient Mesopotamian god of wind, air, earth, and storms.", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -180,12 +162,8 @@ "spec_version": "2.1" }, { - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created": "2022-04-01T06:07:08.000Z", "description": "Enki, ancient Mesopotamian god of water and crafts.", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -207,12 +185,8 @@ "spec_version": "2.1" }, { - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created": "2022-03-01T06:07:08.000Z", "description": "Inanna, ancient Mesopotamian god of love, war, and fertility.", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -234,12 +208,8 @@ "spec_version": "2.1" }, { - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created": "2022-02-01T06:07:08.000Z", "description": "Nabu, ancient Mesopotamian god of literacy and wisdom.", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -261,12 +231,8 @@ "spec_version": "2.1" }, { - "x_mitre_domains": [ - "enterprise-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_domains": ["enterprise-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created": "2022-01-01T06:07:08.000Z", "description": "Nanna-Suen, ancient Mesopotamian god of cattle.", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -288,12 +254,8 @@ "spec_version": "2.1" }, { - "x_mitre_domains": [ - "mobile-attack" - ], - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "x_mitre_domains": ["mobile-attack"], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "created": "2022-09-09T01:02:03.000Z", "description": "Nabu, ancient Mesopotamian god of mobile scribes. This tactic has the same x-mitre-shortname as another tactic, but a different domain.", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", @@ -315,9 +277,7 @@ "spec_version": "2.1" }, { - "object_marking_refs": [ - "marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce" - ], + "object_marking_refs": ["marking-definition--c2a0b8f8-51d4-4702-8e42-ce7a65235bce"], "id": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "type": "identity", "identity_class": "organization", @@ -326,9 +286,7 @@ "name": "The MITRE Corporation", "spec_version": "2.1", "x_mitre_attack_spec_version": "2.1.0", - "x_mitre_domains": [ - "ics-attack" - ], + "x_mitre_domains": ["ics-attack"], "x_mitre_version": "1.0" }, { @@ -342,9 +300,7 @@ "definition_type": "statement", "x_mitre_attack_spec_version": "2.1.0", "spec_version": "2.1", - "x_mitre_domains": [ - "ics-attack" - ] + "x_mitre_domains": ["ics-attack"] } ] } diff --git a/app/tests/api/techniques/techniques.tactics.spec.js b/app/tests/api/techniques/techniques.tactics.spec.js index 561952d6..dd60b0fd 100644 --- a/app/tests/api/techniques/techniques.tactics.spec.js +++ b/app/tests/api/techniques/techniques.tactics.spec.js @@ -15,127 +15,129 @@ const databaseConfiguration = require('../../../lib/database-configuration'); const collectionBundlesService = require('../../../services/collection-bundles-service'); async function readJson(path) { - const data = await fs.readFile(require.resolve(path)); - return JSON.parse(data); + const data = await fs.readFile(require.resolve(path)); + return JSON.parse(data); } const importBundle = util.promisify(collectionBundlesService.importBundle); describe('Techniques with Tactics API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - const collectionBundle = await readJson('./techniques.tactics.json'); - const collections = collectionBundle.objects.filter(object => object.type === 'x-mitre-collection'); - - const importOptions = {}; - await importBundle(collections[0], collectionBundle, importOptions); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - const techniqueId1 = 'attack-pattern--757471d4-d931-4109-82dd-cdd50c04744e'; - const techniqueId2 = 'attack-pattern--fb93f707-fd21-421f-a8b3-4baee9211b3a'; - - let technique1; - let technique2; - it('GET /api/techniques should return the preloaded technique', async function () { - const res = await request(app) - .get('/api/techniques') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - const techniques = res.body; - expect(techniques).toBeDefined(); - expect(Array.isArray(techniques)).toBe(true); - expect(techniques.length).toBe(2); - - technique1 = techniques.find(t => t.stix.id === techniqueId1); - technique2 = techniques.find(t => t.stix.id === techniqueId2); - }); - - it('GET /api/tactics should return the preloaded tactics', async function () { - const res = await request(app) - .get('/api/tactics') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - const tactics = res.body; - expect(tactics).toBeDefined(); - expect(Array.isArray(tactics)).toBe(true); - expect(tactics.length).toBe(6); - - }); - - it('GET /api/techniques/:id/modified/:modified/tactics should not return the tactics when the technique cannot be found', async function () { - await request(app) - .get(`/api/techniques/not-an-id/modified/2022-01-01T00:00:00.000Z/tactics`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - }); - - it('GET /api/techniques/:id/modified/:modified/tactics should return the tactics for technique 1', async function () { - const res = await request(app) - .get(`/api/techniques/${ technique1.stix.id }/modified/${ technique1.stix.modified }/tactics`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - const tactics = res.body; - expect(tactics).toBeDefined(); - expect(Array.isArray(tactics)).toBe(true); - expect(tactics.length).toBe(2); - }); - - it('GET /api/techniques/:id/modified/:modified/tactics should return the tactics for technique 2', async function () { - const res = await request(app) - .get(`/api/techniques/${ technique2.stix.id }/modified/${ technique2.stix.modified }/tactics`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - const tactics = res.body; - expect(tactics).toBeDefined(); - expect(Array.isArray(tactics)).toBe(true); - expect(tactics.length).toBe(5); - }); - - it('GET /api/techniques/:id/modified/:modified/tactics should return the first page of tactics for technique 2', async function () { - const res = await request(app) - .get(`/api/techniques/${ technique2.stix.id }/modified/${ technique2.stix.modified }/tactics?offset=0&limit=2&includePagination=true`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - const results = res.body; - const tactics = results.data; - expect(tactics).toBeDefined(); - expect(Array.isArray(tactics)).toBe(true); - expect(tactics.length).toBe(2); - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + const collectionBundle = await readJson('./techniques.tactics.json'); + const collections = collectionBundle.objects.filter( + (object) => object.type === 'x-mitre-collection', + ); + + const importOptions = {}; + await importBundle(collections[0], collectionBundle, importOptions); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + const techniqueId1 = 'attack-pattern--757471d4-d931-4109-82dd-cdd50c04744e'; + const techniqueId2 = 'attack-pattern--fb93f707-fd21-421f-a8b3-4baee9211b3a'; + + let technique1; + let technique2; + it('GET /api/techniques should return the preloaded technique', async function () { + const res = await request(app) + .get('/api/techniques') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + const techniques = res.body; + expect(techniques).toBeDefined(); + expect(Array.isArray(techniques)).toBe(true); + expect(techniques.length).toBe(2); + + technique1 = techniques.find((t) => t.stix.id === techniqueId1); + technique2 = techniques.find((t) => t.stix.id === techniqueId2); + }); + + it('GET /api/tactics should return the preloaded tactics', async function () { + const res = await request(app) + .get('/api/tactics') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + const tactics = res.body; + expect(tactics).toBeDefined(); + expect(Array.isArray(tactics)).toBe(true); + expect(tactics.length).toBe(6); + }); + + it('GET /api/techniques/:id/modified/:modified/tactics should not return the tactics when the technique cannot be found', async function () { + await request(app) + .get(`/api/techniques/not-an-id/modified/2022-01-01T00:00:00.000Z/tactics`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('GET /api/techniques/:id/modified/:modified/tactics should return the tactics for technique 1', async function () { + const res = await request(app) + .get(`/api/techniques/${technique1.stix.id}/modified/${technique1.stix.modified}/tactics`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + const tactics = res.body; + expect(tactics).toBeDefined(); + expect(Array.isArray(tactics)).toBe(true); + expect(tactics.length).toBe(2); + }); + + it('GET /api/techniques/:id/modified/:modified/tactics should return the tactics for technique 2', async function () { + const res = await request(app) + .get(`/api/techniques/${technique2.stix.id}/modified/${technique2.stix.modified}/tactics`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + const tactics = res.body; + expect(tactics).toBeDefined(); + expect(Array.isArray(tactics)).toBe(true); + expect(tactics.length).toBe(5); + }); + + it('GET /api/techniques/:id/modified/:modified/tactics should return the first page of tactics for technique 2', async function () { + const res = await request(app) + .get( + `/api/techniques/${technique2.stix.id}/modified/${technique2.stix.modified}/tactics?offset=0&limit=2&includePagination=true`, + ) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + const results = res.body; + const tactics = results.data; + expect(tactics).toBeDefined(); + expect(Array.isArray(tactics)).toBe(true); + expect(tactics.length).toBe(2); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/api/user-accounts/user-accounts-invalid.spec.js b/app/tests/api/user-accounts/user-accounts-invalid.spec.js index b9341eab..be21b482 100644 --- a/app/tests/api/user-accounts/user-accounts-invalid.spec.js +++ b/app/tests/api/user-accounts/user-accounts-invalid.spec.js @@ -11,37 +11,37 @@ const userAccounts = require('./user-accounts.invalid.json'); const login = require('../../shared/login'); describe('User Accounts API Test Invalid Data', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + for (const userAccountData of userAccounts) { + it(`POST /api/user-accounts does not create a user account with invalid data (${userAccountData.username})`, async function () { + const body = userAccountData; + await request(app) + .post('/api/user-accounts') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); }); + } - for (const userAccountData of userAccounts) { - it(`POST /api/user-accounts does not create a user account with invalid data (${ userAccountData.username })`, async function () { - const body = userAccountData; - await request(app) - .post('/api/user-accounts') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - } - - after(async function() { - await database.closeConnection(); - }); + after(async function () { + await database.closeConnection(); + }); }); diff --git a/app/tests/api/user-accounts/user-accounts.spec.js b/app/tests/api/user-accounts/user-accounts.spec.js index 87a46a84..f5b4257f 100644 --- a/app/tests/api/user-accounts/user-accounts.spec.js +++ b/app/tests/api/user-accounts/user-accounts.spec.js @@ -14,283 +14,272 @@ const login = require('../../shared/login'); // userId property will be created by REST API const initialObjectData = { - email: 'user@org.com', - username: 'user@org.com', - displayName: 'UserFirst UserLast', - status: 'active', - role: 'editor' + email: 'user@org.com', + username: 'user@org.com', + displayName: 'UserFirst UserLast', + status: 'active', + role: 'editor', }; describe('User Accounts API', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Wait until the indexes are created - await UserAccount.init(); - await Team.init(); - - // Initialize the express app - app = await require('../../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); - - it('GET /api/user-accounts returns an array with the anonymous user account', async function () { - const res = await request(app) - .get('/api/user-accounts') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an array with one entry (the anonymous user account) - const userAccounts = res.body; - expect(userAccounts).toBeDefined(); - expect(Array.isArray(userAccounts)).toBe(true); - expect(userAccounts.length).toBe(1); - }); - - it('POST /api/user-accounts does not create an empty user account', async function () { - const body = {}; - await request(app) - .post('/api/user-accounts') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - - }); - - let userAccount1; - it('POST /api/user-accounts creates a user account', async function () { - const body = initialObjectData; - const res = await request(app) - .post('/api/user-accounts') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created user account - userAccount1 = res.body; - expect(userAccount1).toBeDefined(); - expect(userAccount1.id).toBeDefined(); - - }); - - it('GET /api/user-accounts returns the added user account', async function () { - const res = await request(app) - .get('/api/user-accounts') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the added user account and the anonymous user account - const userAccounts = res.body; - expect(userAccounts).toBeDefined(); - expect(Array.isArray(userAccounts)).toBe(true); - expect(userAccounts.length).toBe(2); - - }); - - it('GET /api/user-accounts/:id should not return a user account when the id cannot be found', async function () { - await request(app) - .get('/api/user-accounts/not-an-id') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404); - - - }); - - it('GET /api/user-accounts/:id returns the added user account', async function () { - const res = await request(app) - .get('/api/user-accounts/' + userAccount1.id) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one user account in an array - const userAccount = res.body; - expect(userAccount).toBeDefined(); - expect(userAccount.id).toBe(userAccount1.id); - expect(userAccount.email).toBe(userAccount1.email); - expect(userAccount.username).toBe(userAccount1.username); - expect(userAccount.displayName).toBe(userAccount1.displayName); - expect(userAccount.status).toBe(userAccount1.status); - expect(userAccount.role).toBe(userAccount1.role); - - // The created and modified timestamps should match - expect(userAccount.created).toBeDefined(); - expect(userAccount.modified).toBeDefined(); - expect(userAccount.created).toEqual(userAccount.modified); - - - }); - - it('GET /api/user-accounts/:id returns the added user account with a derived STIX Identity object', async function () { - const res = await request(app) - .get('/api/user-accounts/' + userAccount1.id + '?includeStixIdentity=true') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one user account in an array - const userAccount = res.body; - expect(userAccount).toBeDefined(); - expect(userAccount.id).toBe(userAccount1.id); - expect(userAccount.email).toBe(userAccount1.email); - expect(userAccount.username).toBe(userAccount1.username); - expect(userAccount.displayName).toBe(userAccount1.displayName); - expect(userAccount.status).toBe(userAccount1.status); - expect(userAccount.role).toBe(userAccount1.role); - - // The created and modified timestamps should match - expect(userAccount.created).toBeDefined(); - expect(userAccount.modified).toBeDefined(); - expect(userAccount.created).toEqual(userAccount.modified); - - // The added STIX identity should have the correct data - const identity = userAccount.identity; - expect(identity).toBeDefined(); - expect(identity.id).toBe(userAccount1.id); - expect(identity.type).toBe('identity'); - expect(identity.created).toBe(userAccount1.created); - expect(identity.modified).toBe(userAccount1.modified); - expect(identity.identity_class).toBe('individual'); - - - }); - - it('PUT /api/user-accounts updates a user account', async function () { - const body = userAccount1; - body.role = 'admin'; - const res = await request(app) - .put('/api/user-accounts/' + userAccount1.id) - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the updated user account - const userAccount = res.body; - expect(userAccount).toBeDefined(); - expect(userAccount.identity).toBe(userAccount1.identity); - - // The modified timestamp should be different from the created timestamp - expect(userAccount.created).toBeDefined(); - expect(userAccount.modified).toBeDefined(); - expect(userAccount.role).toBe('admin'); - expect(userAccount.created).not.toEqual(userAccount.modified); - }); - - it('GET /api/user-accounts uses the search parameter to return the user account', async function () { - const res = await request(app) - .get('/api/user-accounts?search=first') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get one user account in an array - const userAccounts = res.body; - expect(userAccounts).toBeDefined(); - expect(Array.isArray(userAccounts)).toBe(true); - expect(userAccounts.length).toBe(1); - }); - - it('POST /api/user-accounts does not create a user account with a duplicate email', async function () { - const body = initialObjectData; - await request(app) - .post('/api/user-accounts') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - - it('DELETE /api/user-accounts deletes a user account', async function () { - await request(app) - .delete('/api/user-accounts/' + userAccount1.id) - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204); - }); - - let anonymousUserId = null; - it('GET /api/user-accounts returns an array with the anonymous user account', async function () { - const res = await request(app) - .get('/api/user-accounts') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get an empty array - const userAccount = res.body; - expect(userAccount).toBeDefined(); - expect(Array.isArray(userAccount)).toBe(true); - expect(userAccount.length).toBe(1); - anonymousUserId = userAccount[0].id; - - }); - - it('GET /api/user-accounts/{id}/teams should return an empty array when no teams have been associated with a user', async function () { - const res = await request(app) - .get(`/api/user-accounts/${ anonymousUserId }/teams`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - const teams = res.body; - expect(teams).toBeDefined(); - expect(Array.isArray(teams)).toBe(true); - expect(teams.length).toBe(0); - - }); - - it('GET /api/user-accounts/{id}/teams should return an array with the list of teams a user is associated with', async function () { - // Add a new team to the database with the anonymous user as a member - const timestamp = new Date().toISOString(); - const teamData = { - userIDs: [ anonymousUserId ], - name: 'Example Team', - created: timestamp, - modified: timestamp - }; - await teamsService.create(teamData); - - const res = await request(app) - .get(`/api/user-accounts/${ anonymousUserId }/teams`) - .set('Accept', 'application/json') - .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) - .expect(200) - .expect('Content-Type', /json/) - - const teams = res.body; - expect(teams).toBeDefined(); - expect(Array.isArray(teams)).toBe(true); - expect(teams.length).toBe(1); - expect(teams[0].name).toBe(teamData.name); - expect(teams[0].userIDs.length).toBe(1); - expect(teams[0].userIDs[0]).toBe(anonymousUserId); - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Wait until the indexes are created + await UserAccount.init(); + await Team.init(); + + // Initialize the express app + app = await require('../../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + it('GET /api/user-accounts returns an array with the anonymous user account', async function () { + const res = await request(app) + .get('/api/user-accounts') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an array with one entry (the anonymous user account) + const userAccounts = res.body; + expect(userAccounts).toBeDefined(); + expect(Array.isArray(userAccounts)).toBe(true); + expect(userAccounts.length).toBe(1); + }); + + it('POST /api/user-accounts does not create an empty user account', async function () { + const body = {}; + await request(app) + .post('/api/user-accounts') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + let userAccount1; + it('POST /api/user-accounts creates a user account', async function () { + const body = initialObjectData; + const res = await request(app) + .post('/api/user-accounts') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created user account + userAccount1 = res.body; + expect(userAccount1).toBeDefined(); + expect(userAccount1.id).toBeDefined(); + }); + + it('GET /api/user-accounts returns the added user account', async function () { + const res = await request(app) + .get('/api/user-accounts') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the added user account and the anonymous user account + const userAccounts = res.body; + expect(userAccounts).toBeDefined(); + expect(Array.isArray(userAccounts)).toBe(true); + expect(userAccounts.length).toBe(2); + }); + + it('GET /api/user-accounts/:id should not return a user account when the id cannot be found', async function () { + await request(app) + .get('/api/user-accounts/not-an-id') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); + }); + + it('GET /api/user-accounts/:id returns the added user account', async function () { + const res = await request(app) + .get('/api/user-accounts/' + userAccount1.id) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one user account in an array + const userAccount = res.body; + expect(userAccount).toBeDefined(); + expect(userAccount.id).toBe(userAccount1.id); + expect(userAccount.email).toBe(userAccount1.email); + expect(userAccount.username).toBe(userAccount1.username); + expect(userAccount.displayName).toBe(userAccount1.displayName); + expect(userAccount.status).toBe(userAccount1.status); + expect(userAccount.role).toBe(userAccount1.role); + + // The created and modified timestamps should match + expect(userAccount.created).toBeDefined(); + expect(userAccount.modified).toBeDefined(); + expect(userAccount.created).toEqual(userAccount.modified); + }); + + it('GET /api/user-accounts/:id returns the added user account with a derived STIX Identity object', async function () { + const res = await request(app) + .get('/api/user-accounts/' + userAccount1.id + '?includeStixIdentity=true') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one user account in an array + const userAccount = res.body; + expect(userAccount).toBeDefined(); + expect(userAccount.id).toBe(userAccount1.id); + expect(userAccount.email).toBe(userAccount1.email); + expect(userAccount.username).toBe(userAccount1.username); + expect(userAccount.displayName).toBe(userAccount1.displayName); + expect(userAccount.status).toBe(userAccount1.status); + expect(userAccount.role).toBe(userAccount1.role); + + // The created and modified timestamps should match + expect(userAccount.created).toBeDefined(); + expect(userAccount.modified).toBeDefined(); + expect(userAccount.created).toEqual(userAccount.modified); + + // The added STIX identity should have the correct data + const identity = userAccount.identity; + expect(identity).toBeDefined(); + expect(identity.id).toBe(userAccount1.id); + expect(identity.type).toBe('identity'); + expect(identity.created).toBe(userAccount1.created); + expect(identity.modified).toBe(userAccount1.modified); + expect(identity.identity_class).toBe('individual'); + }); + + it('PUT /api/user-accounts updates a user account', async function () { + const body = userAccount1; + body.role = 'admin'; + const res = await request(app) + .put('/api/user-accounts/' + userAccount1.id) + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the updated user account + const userAccount = res.body; + expect(userAccount).toBeDefined(); + expect(userAccount.identity).toBe(userAccount1.identity); + + // The modified timestamp should be different from the created timestamp + expect(userAccount.created).toBeDefined(); + expect(userAccount.modified).toBeDefined(); + expect(userAccount.role).toBe('admin'); + expect(userAccount.created).not.toEqual(userAccount.modified); + }); + + it('GET /api/user-accounts uses the search parameter to return the user account', async function () { + const res = await request(app) + .get('/api/user-accounts?search=first') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get one user account in an array + const userAccounts = res.body; + expect(userAccounts).toBeDefined(); + expect(Array.isArray(userAccounts)).toBe(true); + expect(userAccounts.length).toBe(1); + }); + + it('POST /api/user-accounts does not create a user account with a duplicate email', async function () { + const body = initialObjectData; + await request(app) + .post('/api/user-accounts') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + + it('DELETE /api/user-accounts deletes a user account', async function () { + await request(app) + .delete('/api/user-accounts/' + userAccount1.id) + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); + }); + + let anonymousUserId = null; + it('GET /api/user-accounts returns an array with the anonymous user account', async function () { + const res = await request(app) + .get('/api/user-accounts') + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const userAccount = res.body; + expect(userAccount).toBeDefined(); + expect(Array.isArray(userAccount)).toBe(true); + expect(userAccount.length).toBe(1); + anonymousUserId = userAccount[0].id; + }); + + it('GET /api/user-accounts/{id}/teams should return an empty array when no teams have been associated with a user', async function () { + const res = await request(app) + .get(`/api/user-accounts/${anonymousUserId}/teams`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + const teams = res.body; + expect(teams).toBeDefined(); + expect(Array.isArray(teams)).toBe(true); + expect(teams.length).toBe(0); + }); + + it('GET /api/user-accounts/{id}/teams should return an array with the list of teams a user is associated with', async function () { + // Add a new team to the database with the anonymous user as a member + const timestamp = new Date().toISOString(); + const teamData = { + userIDs: [anonymousUserId], + name: 'Example Team', + created: timestamp, + modified: timestamp, + }; + await teamsService.create(teamData); + + const res = await request(app) + .get(`/api/user-accounts/${anonymousUserId}/teams`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + const teams = res.body; + expect(teams).toBeDefined(); + expect(Array.isArray(teams)).toBe(true); + expect(teams.length).toBe(1); + expect(teams[0].name).toBe(teamData.name); + expect(teams[0].userIDs.length).toBe(1); + expect(teams[0].userIDs[0]).toBe(anonymousUserId); + }); + + after(async function () { + await database.closeConnection(); + }); }); diff --git a/app/tests/authn/README.md b/app/tests/authn/README.md index c09f19cc..31e1a87f 100644 --- a/app/tests/authn/README.md +++ b/app/tests/authn/README.md @@ -18,4 +18,3 @@ docker run -p 8080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin --name ``` This starts the keycloak server and adds an admin user. - diff --git a/app/tests/authn/anonymous-authn.spec.js b/app/tests/authn/anonymous-authn.spec.js index d40c2f6c..2c46fe86 100644 --- a/app/tests/authn/anonymous-authn.spec.js +++ b/app/tests/authn/anonymous-authn.spec.js @@ -11,138 +11,136 @@ logger.level = 'debug'; const passportCookieName = 'connect.sid'; describe('Anonymous User Authentication', function () { - let app; - - before(async function() { - // Configure the test to use anonymous authentication - process.env.AUTHN_MECHANISM = 'anonymous'; - - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../index').initializeApp(); - }); - - it('GET /api/session returns not authorized (before logging in)', function (done) { - request(app) - .get('/api/session') - .set('Accept', 'application/json') - .expect(401) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - let passportCookie; - it('GET /api/authn/anonymous/login successfully logs the user in', function (done) { - request(app) - .get('/api/authn/anonymous/login') - .set('Accept', 'application/json') - .expect(200) - .end(function (err, res) { - if (err) { - done(err); - } else { - // Save the cookie for later tests - const cookies = setCookieParser(res); - passportCookie = cookies.find(c => c.name === passportCookieName); - expect(passportCookie).toBeDefined(); - - done(); - } - }); - }); - - it('GET /api/session returns the user session', function (done) { - request(app) - .get('/api/session') - .set('Accept', 'application/json') - .set('Cookie', `${ passportCookieName }=${ passportCookie.value }`) - .expect(200) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the current session - const session = res.body; - expect(session).toBeDefined(); - - done(); - } - }); - }); - - it('GET /api/authn/anonymous/logout successfully logs the user out', function (done) { - request(app) - .get('/api/authn/anonymous/logout') - .set('Accept', 'application/json') - .set('Cookie', `${ passportCookieName }=${ passportCookie.value }`) - .expect(200) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('GET /api/session returns not authorized (after logging out)', function (done) { - request(app) - .get('/api/session') - .set('Accept', 'application/json') - .set('Cookie', `${ passportCookieName }=${ passportCookie.value }`) - .expect(401) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('GET /api/authn/oidc/login cannot log in using incorrect authentication mechanism', function (done) { - const encodedDestination = encodeURIComponent('http://localhost/startPage'); - request(app) - .get(`/api/authn/oidc/login?destination=${ encodedDestination }`) - .set('Accept', 'application/json') - .expect(404) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('GET /api/authn/oidc/logout cannot log out using incorrect authentication mechanism', function (done) { - request(app) - .get('/api/authn/oidc/logout') - .set('Accept', 'application/json') - .expect(404) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + + before(async function () { + // Configure the test to use anonymous authentication + process.env.AUTHN_MECHANISM = 'anonymous'; + + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../index').initializeApp(); + }); + + it('GET /api/session returns not authorized (before logging in)', function (done) { + request(app) + .get('/api/session') + .set('Accept', 'application/json') + .expect(401) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + let passportCookie; + it('GET /api/authn/anonymous/login successfully logs the user in', function (done) { + request(app) + .get('/api/authn/anonymous/login') + .set('Accept', 'application/json') + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + // Save the cookie for later tests + const cookies = setCookieParser(res); + passportCookie = cookies.find((c) => c.name === passportCookieName); + expect(passportCookie).toBeDefined(); + + done(); + } + }); + }); + + it('GET /api/session returns the user session', function (done) { + request(app) + .get('/api/session') + .set('Accept', 'application/json') + .set('Cookie', `${passportCookieName}=${passportCookie.value}`) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the current session + const session = res.body; + expect(session).toBeDefined(); + + done(); + } + }); + }); + + it('GET /api/authn/anonymous/logout successfully logs the user out', function (done) { + request(app) + .get('/api/authn/anonymous/logout') + .set('Accept', 'application/json') + .set('Cookie', `${passportCookieName}=${passportCookie.value}`) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('GET /api/session returns not authorized (after logging out)', function (done) { + request(app) + .get('/api/session') + .set('Accept', 'application/json') + .set('Cookie', `${passportCookieName}=${passportCookie.value}`) + .expect(401) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('GET /api/authn/oidc/login cannot log in using incorrect authentication mechanism', function (done) { + const encodedDestination = encodeURIComponent('http://localhost/startPage'); + request(app) + .get(`/api/authn/oidc/login?destination=${encodedDestination}`) + .set('Accept', 'application/json') + .expect(404) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('GET /api/authn/oidc/logout cannot log out using incorrect authentication mechanism', function (done) { + request(app) + .get('/api/authn/oidc/logout') + .set('Accept', 'application/json') + .expect(404) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/authn/basic-apikey-service.spec.js b/app/tests/authn/basic-apikey-service.spec.js index c3a548f4..c5c8cdb2 100644 --- a/app/tests/authn/basic-apikey-service.spec.js +++ b/app/tests/authn/basic-apikey-service.spec.js @@ -14,92 +14,90 @@ const logger = require('../../lib/logger'); logger.level = 'debug'; describe('Basic Apikey Service Authentication', function () { - let app; - - before(async function() { - // Configure the test to use anonymous authentication - process.env.AUTHN_MECHANISM = 'anonymous'; - - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../index').initializeApp(); - }); - - it('GET /api/session returns not authorized when called without apikey', function (done) { - request(app) - .get('/api/session') - .set('Accept', 'application/json') - .expect(401) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('GET /api/session returns bad request with an invalid apikey', function (done) { - request(app) - .get('/api/session') - .set('Accept', 'application/json') - .set('Authorization', `Basic abcd`) - .expect(400) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('GET /api/session returns not authorized with an incorrect apikey', function (done) { - const encodedApikey = Buffer.from(`${ serviceName }:abcd`).toString('base64'); - request(app) - .get('/api/session') - .set('Accept', 'application/json') - .set('Authorization', `Basic ${ encodedApikey }`) - .expect(401) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('GET /api/session returns the session', function (done) { - const encodedApikey = Buffer.from(`${ serviceName }:${ apikey }`).toString('base64'); - request(app) - .get('/api/session') - .set('Accept', 'application/json') - .set('Authorization', `Basic ${ encodedApikey }`) - .expect(200) - .end(function (err, res) { - if (err) { - done(err); - } - else { - // We expect to get the current session - const session = res.body; - expect(session).toBeDefined(); - expect(session.serviceName).toBe(serviceName); - - done(); - } - }); - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + + before(async function () { + // Configure the test to use anonymous authentication + process.env.AUTHN_MECHANISM = 'anonymous'; + + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../index').initializeApp(); + }); + + it('GET /api/session returns not authorized when called without apikey', function (done) { + request(app) + .get('/api/session') + .set('Accept', 'application/json') + .expect(401) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('GET /api/session returns bad request with an invalid apikey', function (done) { + request(app) + .get('/api/session') + .set('Accept', 'application/json') + .set('Authorization', `Basic abcd`) + .expect(400) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('GET /api/session returns not authorized with an incorrect apikey', function (done) { + const encodedApikey = Buffer.from(`${serviceName}:abcd`).toString('base64'); + request(app) + .get('/api/session') + .set('Accept', 'application/json') + .set('Authorization', `Basic ${encodedApikey}`) + .expect(401) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('GET /api/session returns the session', function (done) { + const encodedApikey = Buffer.from(`${serviceName}:${apikey}`).toString('base64'); + request(app) + .get('/api/session') + .set('Accept', 'application/json') + .set('Authorization', `Basic ${encodedApikey}`) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the current session + const session = res.body; + expect(session).toBeDefined(); + expect(session.serviceName).toBe(serviceName); + + done(); + } + }); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/authn/challenge-apikey-service.spec.js b/app/tests/authn/challenge-apikey-service.spec.js index 713fa144..a7dd6dc9 100644 --- a/app/tests/authn/challenge-apikey-service.spec.js +++ b/app/tests/authn/challenge-apikey-service.spec.js @@ -15,176 +15,173 @@ const logger = require('../../lib/logger'); logger.level = 'debug'; describe('Challenge Apikey Service Authentication', function () { - let app; - - before(async function() { - // Configure the test to use anonymous authentication - process.env.AUTHN_MECHANISM = 'anonymous'; - - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../index').initializeApp(); - }); - - it('GET /api/session returns not authorized when called without token', function (done) { - request(app) - .get('/api/session') - .set('Accept', 'application/json') - .expect(401) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('GET /api/session returns not authorized with an invalid token', function (done) { - request(app) - .get('/api/session') - .set('Accept', 'application/json') - .set('Authorization', `Bearer abcd`) - .expect(401) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('GET /api/authn/service/apikey-challenge fails with an unknown service name', function (done) { - request(app) - .get(`/api/authn/service/apikey-challenge?serviceName=notaservice`) - .set('Accept', 'application/json') - .expect(404) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - let challengeString; - it('GET /api/authn/service/apikey-challenge successfully retrieves the challenge string', function (done) { - request(app) - .get(`/api/authn/service/apikey-challenge?serviceName=${ serviceName }`) - .set('Accept', 'application/json') - .expect(200) - .end(function (err, res) { - if (err) { - done(err); - } else { - const data = res.body; - expect(data).toBeDefined(); - expect(data.challenge).toBeDefined(); - - challengeString = data.challenge; - - done(); - } - }); - }); - - it('GET /api/authn/service/apikey-token fails with a bad challenge hash', function (done) { - const hmac = crypto.createHmac('sha256', 'not the apikey'); - hmac.update(challengeString); - const challengeHash = hmac.digest('hex'); - request(app) - .get(`/api/authn/service/apikey-token?serviceName=${ serviceName }`) - .set('Accept', 'application/json') - .set('Authorization', `Apikey ${ challengeHash }`) - .expect(400) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - // Get another challenge. The failed challenge hash uses the previously retrieved challenge. - it('GET /api/authn/service/apikey-challenge successfully retrieves a second challenge string', function (done) { - request(app) - .get(`/api/authn/service/apikey-challenge?serviceName=${ serviceName }`) - .set('Accept', 'application/json') - .expect(200) - .end(function (err, res) { - if (err) { - done(err); - } else { - const data = res.body; - expect(data).toBeDefined(); - expect(data.challenge).toBeDefined(); - - challengeString = data.challenge; - - done(); - } - }); - }); - - let token; - it('GET /api/authn/service/apikey-token returns the access token', function (done) { - const hmac = crypto.createHmac('sha256', apikey); - hmac.update(challengeString); - const challengeHash = hmac.digest('hex'); - request(app) - .get(`/api/authn/service/apikey-token?serviceName=${ serviceName }`) - .set('Accept', 'application/json') - .set('Authorization', `Apikey ${ challengeHash }`) - .expect(200) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the current session - const data = res.body; - expect(data).toBeDefined(); - expect(data.access_token).toBeDefined(); - - token = data.access_token; - - done(); - } - }); - }); - - it('GET /api/session returns the session', function (done) { - request(app) - .get('/api/session') - .set('Accept', 'application/json') - .set('Authorization', `Bearer ${ token }`) - .expect(200) - .end(function (err, res) { - if (err) { - done(err); - } - else { - // We expect to get the current session - const session = res.body; - expect(session).toBeDefined(); - expect(session.serviceName).toBe(serviceName); - - done(); - } - }); - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + + before(async function () { + // Configure the test to use anonymous authentication + process.env.AUTHN_MECHANISM = 'anonymous'; + + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../index').initializeApp(); + }); + + it('GET /api/session returns not authorized when called without token', function (done) { + request(app) + .get('/api/session') + .set('Accept', 'application/json') + .expect(401) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('GET /api/session returns not authorized with an invalid token', function (done) { + request(app) + .get('/api/session') + .set('Accept', 'application/json') + .set('Authorization', `Bearer abcd`) + .expect(401) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('GET /api/authn/service/apikey-challenge fails with an unknown service name', function (done) { + request(app) + .get(`/api/authn/service/apikey-challenge?serviceName=notaservice`) + .set('Accept', 'application/json') + .expect(404) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + let challengeString; + it('GET /api/authn/service/apikey-challenge successfully retrieves the challenge string', function (done) { + request(app) + .get(`/api/authn/service/apikey-challenge?serviceName=${serviceName}`) + .set('Accept', 'application/json') + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + const data = res.body; + expect(data).toBeDefined(); + expect(data.challenge).toBeDefined(); + + challengeString = data.challenge; + + done(); + } + }); + }); + + it('GET /api/authn/service/apikey-token fails with a bad challenge hash', function (done) { + const hmac = crypto.createHmac('sha256', 'not the apikey'); + hmac.update(challengeString); + const challengeHash = hmac.digest('hex'); + request(app) + .get(`/api/authn/service/apikey-token?serviceName=${serviceName}`) + .set('Accept', 'application/json') + .set('Authorization', `Apikey ${challengeHash}`) + .expect(400) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + // Get another challenge. The failed challenge hash uses the previously retrieved challenge. + it('GET /api/authn/service/apikey-challenge successfully retrieves a second challenge string', function (done) { + request(app) + .get(`/api/authn/service/apikey-challenge?serviceName=${serviceName}`) + .set('Accept', 'application/json') + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + const data = res.body; + expect(data).toBeDefined(); + expect(data.challenge).toBeDefined(); + + challengeString = data.challenge; + + done(); + } + }); + }); + + let token; + it('GET /api/authn/service/apikey-token returns the access token', function (done) { + const hmac = crypto.createHmac('sha256', apikey); + hmac.update(challengeString); + const challengeHash = hmac.digest('hex'); + request(app) + .get(`/api/authn/service/apikey-token?serviceName=${serviceName}`) + .set('Accept', 'application/json') + .set('Authorization', `Apikey ${challengeHash}`) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the current session + const data = res.body; + expect(data).toBeDefined(); + expect(data.access_token).toBeDefined(); + + token = data.access_token; + + done(); + } + }); + }); + + it('GET /api/session returns the session', function (done) { + request(app) + .get('/api/session') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${token}`) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the current session + const session = res.body; + expect(session).toBeDefined(); + expect(session.serviceName).toBe(serviceName); + + done(); + } + }); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/authn/oidc-authn.spec.js b/app/tests/authn/oidc-authn.spec.js index 78b4d07a..47c50095 100644 --- a/app/tests/authn/oidc-authn.spec.js +++ b/app/tests/authn/oidc-authn.spec.js @@ -22,287 +22,284 @@ const oidcClientSecret = 'a58c55d9-8408-45de-a9ef-a55b433291de'; const localServerHost = 'localhost'; const localServerPort = 3000; -const localServerRedirectUrl = `http://${ localServerHost }:${localServerPort }/api/authn/oidc/*`; +const localServerRedirectUrl = `http://${localServerHost}:${localServerPort}/api/authn/oidc/*`; // Keycloak stores firstName and lastName separately, but combines them to make the name claim const testUser = { - email: 'test@test.com', - username: 'test@test.com', - password: 'testuser', - firstName: 'Test', - lastName: 'User' -} + email: 'test@test.com', + username: 'test@test.com', + password: 'testuser', + firstName: 'Test', + lastName: 'User', +}; function extractFormAction(html) { - const documentRoot = parse5.parse(html); - const formElement = parse5Query.queryOne(documentRoot).getElementsByTagName('form'); - if (formElement) { - const action = formElement.attrs.find(e => e.name === 'action'); - return action; - } - else { - return null; - } + const documentRoot = parse5.parse(html); + const formElement = parse5Query.queryOne(documentRoot).getElementsByTagName('form'); + if (formElement) { + const action = formElement.attrs.find((e) => e.name === 'action'); + return action; + } else { + return null; + } } let server; function startServer(app, port) { - server = app.listen(port, function () { - const host = server.address().address; - const port = server.address().port; + server = app.listen(port, function () { + const host = server.address().address; + const port = server.address().port; - logger.info(`Listening at http://${host}:${port}`); - }) + logger.info(`Listening at http://${host}:${port}`); + }); } function updateCookies(newCookies, cookieMap) { - for (const cookie of newCookies) { - const headerString = libCookie.serialize(cookie.name, cookie.value, cookie); - cookieMap.set(cookie.name, headerString); - } + for (const cookie of newCookies) { + const headerString = libCookie.serialize(cookie.name, cookie.value, cookie); + cookieMap.set(cookie.name, headerString); + } } describe('OIDC User Authentication', function () { - let app; - - before(async function() { - // Configure the test to use OIDC authentication - process.env.AUTHN_MECHANISM = 'oidc'; - process.env.AUTHN_OIDC_ISSUER_URL = `http://${ oidcHost }/realms/${ oidcRealm }/.well-known/openid-configuration`; - process.env.AUTHN_OIDC_CLIENT_ID = oidcClientId; - - config.reloadConfig(); - - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the OIDC server - const options = { - basePath: oidcHost, - realmName: oidcRealm, - clientId: oidcClientId, - description: 'client', - standardFlowEnabled: true, - redirectUris: [ localServerRedirectUrl ], - clientSecret: oidcClientSecret - }; - const clientCredentials = await keycloak.initializeKeycloak(options); - // eslint-disable-next-line require-atomic-updates - config.userAuthn.oidc.clientSecret = clientCredentials.value; - - // Add a test user - await keycloak.addUsersToKeycloak(options, testUser); - - // Initialize the express app - app = await require('../../index').initializeApp(); - - // Open a port to receive redirects from the identity provider - startServer(app, localServerPort); - }); - - it('GET /api/session returns not authorized (before logging in)', function (done) { - request(app) - .get('/api/session') - .set('Accept', 'application/json') - .expect(401) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - const apiCookies = new Map(); - let redirectPath; - const destination = `${ localServerHost }:${ localServerPort }/login-page`; - it('GET /api/authn/oidc/login successfully receives a redirect to the identity provider', function (done) { - const encodedDestination = encodeURIComponent(destination); - request(app) - .get(`/api/authn/oidc/login?destination=${ encodedDestination }`) - .expect(302) - .end(function (err, res) { - if (err) { - done(err); - } else { - // Save the cookies for later tests - const newCookies = setCookieParser(res); - updateCookies(newCookies, apiCookies); - - // Get the redirect location - redirectPath = res.headers.location; - - done(); - } - }); - }); - - let signInPath; - const ipCookies = new Map(); - it('redirect successfully receives a challenge from the identity provider', function (done) { - const url = new URL(redirectPath); - const server = `${ url.protocol }//${ url.host }`; - const path = url.pathname; - const search = url.search; - request(server) - .get(path + search) - .expect(200) - .end(function (err, res) { - if (err) { - done(err); - } else { - // Save the cookies for later tests - const newCookies = setCookieParser(res); - updateCookies(newCookies, ipCookies); - - const action = extractFormAction(res.text); - signInPath = action.value; - - done(); - } - }); - }); - - it('POST formpath successfully signs into the identity provider', function (done) { - const signinUrl = new URL(signInPath); - const server = `${ signinUrl.protocol }//${ signinUrl.host }`; - const path = signinUrl.pathname; - const search = signinUrl.search; - request(server) - .post(path + search) - .set('Cookie', Array.from(ipCookies.values())) - .send(`username=${ testUser.username }`) - .send(`password=${ testUser.password }`) - .send('credentialId=') - .expect(302) - .end(function (err, res) { - if (err) { - done(err); - } else { - // Get the redirect location - redirectPath = res.headers.location; - - done(); - } - }); - }); - - it('redirect successfully completes the sign in process', function (done) { - const url = new URL(redirectPath); - const server = `${ url.protocol }//${ url.host }`; - const path = url.pathname; - const search = url.search; - request(server) - .get(path + search) - .set('Cookie', Array.from(apiCookies.values())) - .expect(302) - .end(function (err, res) { - if (err) { - done(err); - } else { - // Session ID is changed after login, save the cookie for later tests - const newCookies = setCookieParser(res); - updateCookies(newCookies, apiCookies); - - // Get the redirect location - redirectPath = res.headers.location; - - // This should be the destination provided at the start of the sign in process - expect(redirectPath).toBe(destination); - - done(); - } - }); - }); - - it('GET /api/session returns the user session', function (done) { - request(app) - .get('/api/session') - .set('Accept', 'application/json') - .set('Cookie', Array.from(apiCookies.values())) - .expect(200) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the current session - const session = res.body; - expect(session).toBeDefined(); - expect(session.email).toBe('test@test.com'); - - done(); - } - }); - }); - - it('GET /api/authn/oidc/logout successfully logs the user out', function (done) { - request(app) - .get('/api/authn/oidc/logout') - .set('Accept', 'application/json') - .set('Cookie', Array.from(apiCookies.values())) - .expect(200) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('GET /api/session returns not authorized (after logging out)', function (done) { - request(app) - .get('/api/session') - .set('Accept', 'application/json') - .set('Cookie', Array.from(apiCookies.values())) - .expect(401) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('GET /api/authn/anonymous/login cannot log in using incorrect authentication mechanism', function (done) { - request(app) - .get('/api/authn/anonymous/login') - .set('Accept', 'application/json') - .expect(404) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('GET /api/authn/anonymous/logout cannot log out using incorrect authentication mechanism', function (done) { - request(app) - .get('/api/authn/anonymous/logout') - .set('Accept', 'application/json') - .expect(404) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - after(async function() { - await database.closeConnection(); - await server.close(); - }); + let app; + + before(async function () { + // Configure the test to use OIDC authentication + process.env.AUTHN_MECHANISM = 'oidc'; + process.env.AUTHN_OIDC_ISSUER_URL = `http://${oidcHost}/realms/${oidcRealm}/.well-known/openid-configuration`; + process.env.AUTHN_OIDC_CLIENT_ID = oidcClientId; + + config.reloadConfig(); + + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the OIDC server + const options = { + basePath: oidcHost, + realmName: oidcRealm, + clientId: oidcClientId, + description: 'client', + standardFlowEnabled: true, + redirectUris: [localServerRedirectUrl], + clientSecret: oidcClientSecret, + }; + const clientCredentials = await keycloak.initializeKeycloak(options); + // eslint-disable-next-line require-atomic-updates + config.userAuthn.oidc.clientSecret = clientCredentials.value; + + // Add a test user + await keycloak.addUsersToKeycloak(options, testUser); + + // Initialize the express app + app = await require('../../index').initializeApp(); + + // Open a port to receive redirects from the identity provider + startServer(app, localServerPort); + }); + + it('GET /api/session returns not authorized (before logging in)', function (done) { + request(app) + .get('/api/session') + .set('Accept', 'application/json') + .expect(401) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + const apiCookies = new Map(); + let redirectPath; + const destination = `${localServerHost}:${localServerPort}/login-page`; + it('GET /api/authn/oidc/login successfully receives a redirect to the identity provider', function (done) { + const encodedDestination = encodeURIComponent(destination); + request(app) + .get(`/api/authn/oidc/login?destination=${encodedDestination}`) + .expect(302) + .end(function (err, res) { + if (err) { + done(err); + } else { + // Save the cookies for later tests + const newCookies = setCookieParser(res); + updateCookies(newCookies, apiCookies); + + // Get the redirect location + redirectPath = res.headers.location; + + done(); + } + }); + }); + + let signInPath; + const ipCookies = new Map(); + it('redirect successfully receives a challenge from the identity provider', function (done) { + const url = new URL(redirectPath); + const server = `${url.protocol}//${url.host}`; + const path = url.pathname; + const search = url.search; + request(server) + .get(path + search) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + // Save the cookies for later tests + const newCookies = setCookieParser(res); + updateCookies(newCookies, ipCookies); + + const action = extractFormAction(res.text); + signInPath = action.value; + + done(); + } + }); + }); + + it('POST formpath successfully signs into the identity provider', function (done) { + const signinUrl = new URL(signInPath); + const server = `${signinUrl.protocol}//${signinUrl.host}`; + const path = signinUrl.pathname; + const search = signinUrl.search; + request(server) + .post(path + search) + .set('Cookie', Array.from(ipCookies.values())) + .send(`username=${testUser.username}`) + .send(`password=${testUser.password}`) + .send('credentialId=') + .expect(302) + .end(function (err, res) { + if (err) { + done(err); + } else { + // Get the redirect location + redirectPath = res.headers.location; + + done(); + } + }); + }); + + it('redirect successfully completes the sign in process', function (done) { + const url = new URL(redirectPath); + const server = `${url.protocol}//${url.host}`; + const path = url.pathname; + const search = url.search; + request(server) + .get(path + search) + .set('Cookie', Array.from(apiCookies.values())) + .expect(302) + .end(function (err, res) { + if (err) { + done(err); + } else { + // Session ID is changed after login, save the cookie for later tests + const newCookies = setCookieParser(res); + updateCookies(newCookies, apiCookies); + + // Get the redirect location + redirectPath = res.headers.location; + + // This should be the destination provided at the start of the sign in process + expect(redirectPath).toBe(destination); + + done(); + } + }); + }); + + it('GET /api/session returns the user session', function (done) { + request(app) + .get('/api/session') + .set('Accept', 'application/json') + .set('Cookie', Array.from(apiCookies.values())) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the current session + const session = res.body; + expect(session).toBeDefined(); + expect(session.email).toBe('test@test.com'); + + done(); + } + }); + }); + + it('GET /api/authn/oidc/logout successfully logs the user out', function (done) { + request(app) + .get('/api/authn/oidc/logout') + .set('Accept', 'application/json') + .set('Cookie', Array.from(apiCookies.values())) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('GET /api/session returns not authorized (after logging out)', function (done) { + request(app) + .get('/api/session') + .set('Accept', 'application/json') + .set('Cookie', Array.from(apiCookies.values())) + .expect(401) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('GET /api/authn/anonymous/login cannot log in using incorrect authentication mechanism', function (done) { + request(app) + .get('/api/authn/anonymous/login') + .set('Accept', 'application/json') + .expect(404) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('GET /api/authn/anonymous/logout cannot log out using incorrect authentication mechanism', function (done) { + request(app) + .get('/api/authn/anonymous/logout') + .set('Accept', 'application/json') + .expect(404) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + after(async function () { + await database.closeConnection(); + await server.close(); + }); }); - diff --git a/app/tests/authn/oidc-client-credentials-service.spec.js b/app/tests/authn/oidc-client-credentials-service.spec.js index 5ee17375..6461ad75 100644 --- a/app/tests/authn/oidc-client-credentials-service.spec.js +++ b/app/tests/authn/oidc-client-credentials-service.spec.js @@ -8,8 +8,8 @@ const database = require('../../lib/database-in-memory'); const databaseConfiguration = require('../../lib/database-configuration'); const logger = require('../../lib/logger'); -const keycloak = require("../shared/keycloak"); -const config = require("../../config/config"); +const keycloak = require('../shared/keycloak'); +const config = require('../../config/config'); logger.level = 'debug'; const oidcHost = 'localhost:8080'; @@ -23,109 +23,107 @@ const oidcServiceClientSecret = '774ca536-b281-4783-bfed-cc362c39405b'; const localServerHost = 'localhost'; const localServerPort = 3000; -const localServerRedirectUrl = `http://${ localServerHost }:${localServerPort }/api/authn/oidc/*`; -const jwksUri = `http://${ oidcHost }/realms/${ oidcRealm }/protocol/openid-connect/certs`; +const localServerRedirectUrl = `http://${localServerHost}:${localServerPort}/api/authn/oidc/*`; +const jwksUri = `http://${oidcHost}/realms/${oidcRealm}/protocol/openid-connect/certs`; describe('Client Credentials Service Authentication', function () { - let app; - let accessToken; - - before(async function() { - // Configure the test to use anonymous authentication - process.env.AUTHN_MECHANISM = 'anonymous'; - process.env.JWKS_URI = jwksUri; - - config.reloadConfig(); - - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the OIDC server - const options = { - basePath: oidcHost, - realmName: oidcRealm, - clientId: oidcRestApiClientId, - description: 'client', - standardFlowEnabled: true, - redirectUris: [ localServerRedirectUrl ], - clientSecret: oidcRestApiClientSecret - }; - await keycloak.initializeKeycloak(options); - // eslint-disable-next-line require-atomic-updates - - const clientOptions = { - basePath: oidcHost, - realmName: oidcRealm, - clientId: oidcServiceClientId, - description: 'client', - clientSecret: oidcServiceClientSecret, - standardFlowEnabled: false, - serviceAccountsEnabled: true - }; - await keycloak.addClientToKeycloak(clientOptions); - - accessToken = await keycloak.getAccessTokenToClient(clientOptions); - - // Initialize the express app - app = await require('../../index').initializeApp(); - }); - - it('GET /api/session returns not authorized when called without token', function (done) { - request(app) - .get('/api/session') - .set('Accept', 'application/json') - .expect(401) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('GET /api/session returns not authorized with an invalid token', function (done) { - request(app) - .get('/api/session') - .set('Accept', 'application/json') - .set('Authorization', `Bearer abcd`) - .expect(401) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('GET /api/session returns the session when called with a valid token', function (done) { - request(app) - .get('/api/session') - .set('Accept', 'application/json') - .set('Authorization', `Bearer ${ accessToken }`) - .expect(200) - .end(function (err, res) { - if (err) { - done(err); - } - else { - // We expect to get the current session - const session = res.body; - expect(session).toBeDefined(); - expect(session.clientId).toBe(oidcServiceClientId); - - done(); - } - }); - }); - - after(async function() { - await database.closeConnection(); - }); + let app; + let accessToken; + + before(async function () { + // Configure the test to use anonymous authentication + process.env.AUTHN_MECHANISM = 'anonymous'; + process.env.JWKS_URI = jwksUri; + + config.reloadConfig(); + + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the OIDC server + const options = { + basePath: oidcHost, + realmName: oidcRealm, + clientId: oidcRestApiClientId, + description: 'client', + standardFlowEnabled: true, + redirectUris: [localServerRedirectUrl], + clientSecret: oidcRestApiClientSecret, + }; + await keycloak.initializeKeycloak(options); + // eslint-disable-next-line require-atomic-updates + + const clientOptions = { + basePath: oidcHost, + realmName: oidcRealm, + clientId: oidcServiceClientId, + description: 'client', + clientSecret: oidcServiceClientSecret, + standardFlowEnabled: false, + serviceAccountsEnabled: true, + }; + await keycloak.addClientToKeycloak(clientOptions); + + accessToken = await keycloak.getAccessTokenToClient(clientOptions); + + // Initialize the express app + app = await require('../../index').initializeApp(); + }); + + it('GET /api/session returns not authorized when called without token', function (done) { + request(app) + .get('/api/session') + .set('Accept', 'application/json') + .expect(401) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('GET /api/session returns not authorized with an invalid token', function (done) { + request(app) + .get('/api/session') + .set('Accept', 'application/json') + .set('Authorization', `Bearer abcd`) + .expect(401) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('GET /api/session returns the session when called with a valid token', function (done) { + request(app) + .get('/api/session') + .set('Accept', 'application/json') + .set('Authorization', `Bearer ${accessToken}`) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the current session + const session = res.body; + expect(session).toBeDefined(); + expect(session.clientId).toBe(oidcServiceClientId); + + done(); + } + }); + }); + + after(async function () { + await database.closeConnection(); + }); }); - diff --git a/app/tests/authn/oidc-register.spec.js b/app/tests/authn/oidc-register.spec.js index 3a17c2ac..2d9842f5 100644 --- a/app/tests/authn/oidc-register.spec.js +++ b/app/tests/authn/oidc-register.spec.js @@ -22,334 +22,330 @@ const oidcClientSecret = 'a58c55d9-8408-45de-a9ef-a55b433291de'; const localServerHost = 'localhost'; const localServerPort = 3000; -const localServerRedirectUrl = `http://${ localServerHost }:${localServerPort }/api/authn/oidc/*`; +const localServerRedirectUrl = `http://${localServerHost}:${localServerPort}/api/authn/oidc/*`; const testUser = { - email: 'test@test.com', - username: 'test@test.com', - password: 'testuser', - firstName: 'Test', - lastName: 'User' -} + email: 'test@test.com', + username: 'test@test.com', + password: 'testuser', + firstName: 'Test', + lastName: 'User', +}; function extractFormAction(html) { - const documentRoot = parse5.parse(html); - const formElement = parse5Query.queryOne(documentRoot).getElementsByTagName('form'); - if (formElement) { - const action = formElement.attrs.find(e => e.name === 'action'); - return action; - } - else { - return null; - } + const documentRoot = parse5.parse(html); + const formElement = parse5Query.queryOne(documentRoot).getElementsByTagName('form'); + if (formElement) { + const action = formElement.attrs.find((e) => e.name === 'action'); + return action; + } else { + return null; + } } let server; function startServer(app, port) { - server = app.listen(port, function () { - const host = server.address().address; - const port = server.address().port; + server = app.listen(port, function () { + const host = server.address().address; + const port = server.address().port; - logger.info(`Listening at http://${host}:${port}`); - }) + logger.info(`Listening at http://${host}:${port}`); + }); } function updateCookies(newCookies, cookieMap) { - for (const cookie of newCookies) { - const headerString = libCookie.serialize(cookie.name, cookie.value, cookie); - cookieMap.set(cookie.name, headerString); - } + for (const cookie of newCookies) { + const headerString = libCookie.serialize(cookie.name, cookie.value, cookie); + cookieMap.set(cookie.name, headerString); + } } describe('OIDC User Account Registration', function () { - let app; - - before(async function() { - // Configure the test to use OIDC authentication - process.env.AUTHN_MECHANISM = 'oidc'; - process.env.AUTHN_OIDC_ISSUER_URL = `http://${ oidcHost }/realms/${ oidcRealm }/.well-known/openid-configuration`; - process.env.AUTHN_OIDC_CLIENT_ID = oidcClientId; - - config.reloadConfig(); - - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the OIDC server - const options = { - basePath: oidcHost, - realmName: oidcRealm, - clientId: oidcClientId, - description: 'client', - standardFlowEnabled: true, - redirectUris: [ localServerRedirectUrl ], - clientSecret: oidcClientSecret - }; - const clientCredentials = await keycloak.initializeKeycloak(options); - // eslint-disable-next-line require-atomic-updates - config.userAuthn.oidc.clientSecret = clientCredentials.value; - - // Add a test user - await keycloak.addUsersToKeycloak(options, testUser); - - // Initialize the express app - app = await require('../../index').initializeApp(); - - // Open a port to receive redirects from the identity provider - startServer(app, localServerPort); - }); - - const apiCookies = new Map(); - let redirectPath; - const destination = `${ localServerHost }:${ localServerPort }/login-page`; - it('GET /api/authn/oidc/login successfully receives a redirect to the identity provider', function (done) { - const encodedDestination = encodeURIComponent(destination); - request(app) - .get(`/api/authn/oidc/login?destination=${ encodedDestination }`) - .expect(302) - .end(function (err, res) { - if (err) { - done(err); - } else { - // Save the cookies for later tests - const newCookies = setCookieParser(res); - updateCookies(newCookies, apiCookies); - - // Get the redirect location - redirectPath = res.headers.location; - - done(); - } - }); - }); - - let signInPath; - const ipCookies = new Map(); - it('redirect successfully receives a challenge from the identity provider', function (done) { - const url = new URL(redirectPath); - const server = `${ url.protocol }//${ url.host }`; - const path = url.pathname; - const search = url.search; - request(server) - .get(path + search) - .expect(200) - .end(function (err, res) { - if (err) { - done(err); - } else { - // Save the cookies for later tests - const newCookies = setCookieParser(res); - updateCookies(newCookies, ipCookies); - - const action = extractFormAction(res.text); - signInPath = action.value; - - done(); - } - }); - }); - - it('POST formpath successfully signs into the identity provider', function (done) { - const signinUrl = new URL(signInPath); - const server = `${ signinUrl.protocol }//${ signinUrl.host }`; - const path = signinUrl.pathname; - const search = signinUrl.search; - request(server) - .post(path + search) - .set('Cookie', Array.from(ipCookies.values())) - .send(`username=${ testUser.username }`) - .send(`password=${ testUser.password }`) - .send('credentialId=') - .expect(302) - .end(function (err, res) { - if (err) { - done(err); - } else { - // Get the redirect location - redirectPath = res.headers.location; - - done(); - } - }); - }); - - it('redirect successfully completes the sign in process', function (done) { - const url = new URL(redirectPath); - const server = `${ url.protocol }//${ url.host }`; - const path = url.pathname; - const search = url.search; - request(server) - .get(path + search) - .set('Cookie', Array.from(apiCookies.values())) - .expect(302) - .end(function (err, res) { - if (err) { - done(err); - } else { - // Session ID is changed after login, save the cookie for later tests - const newCookies = setCookieParser(res); - updateCookies(newCookies, apiCookies); - - // Get the redirect location - redirectPath = res.headers.location; - - // This should be the destination provided at the start of the sign in process - expect(redirectPath).toBe(destination); - - done(); - } - }); - }); - - it('GET /api/session returns the user session', function (done) { - request(app) - .get('/api/session') - .set('Accept', 'application/json') - .set('Cookie', Array.from(apiCookies.values())) - .expect(200) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the current session - const session = res.body; - expect(session).toBeDefined(); - expect(session.email).toBe(testUser.email); - expect(session.registered).toBe(false); - expect(session.name).toBe(testUser.username) - expect(session.displayName).toBe(`${ testUser.firstName} ${ testUser.lastName }`); - - done(); - } - }); - }); - - let userAccount; - it('POST /api/user-accounts/register successfully registers the user', function (done) { - request(app) - .post('/api/user-accounts/register') - .set('Cookie', Array.from(apiCookies.values())) - .expect(201) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get the new user account - userAccount = res.body; - expect(userAccount).toBeDefined(); - expect(userAccount.email).toBe(testUser.email); - expect(userAccount.username).toBe(testUser.username); - expect(userAccount.displayName).toBe(`${ testUser.firstName} ${ testUser.lastName }`); - expect(userAccount.status).toBe('pending'); - expect(userAccount.role).toBe('none'); - - done(); - } - }); - }); - - it('GET /api/session returns the user session', function (done) { - request(app) - .get('/api/session') - .set('Accept', 'application/json') - .set('Cookie', Array.from(apiCookies.values())) - .expect(200) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the current session with a registered user - const session = res.body; - expect(session).toBeDefined(); - expect(session.email).toBe(testUser.email); - expect(session.registered).toBe(true); - expect(session.name).toBe(testUser.username); - expect(session.displayName).toBe(`${ testUser.firstName} ${ testUser.lastName }`); - - done(); - } - }); - }); - - it('GET /api/authn/oidc/logout successfully logs the user out', function (done) { - request(app) - .get('/api/authn/oidc/logout') - .set('Accept', 'application/json') - .set('Cookie', Array.from(apiCookies.values())) - .expect(200) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - it('GET /api/session returns not authorized (after logging out)', function (done) { - request(app) - .get('/api/session') - .set('Accept', 'application/json') - .set('Cookie', Array.from(apiCookies.values())) - .expect(401) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - // it('GET /api/user-accounts/:id returns the added user account', function (done) { - // request(app) - // .get('/api/user-accounts/' + userAccount.id) - // .set('Accept', 'application/json') - // .expect(200) - // .expect('Content-Type', /json/) - // .end(function (err, res) { - // if (err) { - // done(err); - // } else { - // // We expect to get one user account in an array - // const retrievedUserAccount = res.body; - // expect(retrievedUserAccount).toBeDefined(); - // expect(retrievedUserAccount.id).toBe(userAccount.id); - // expect(retrievedUserAccount.email).toBe(userAccount.email); - // expect(retrievedUserAccount.username).toBe(userAccount.username); - // expect(retrievedUserAccount.displayName).toBe(userAccount.displayName); - // expect(retrievedUserAccount.status).toBe(userAccount.status); - // expect(retrievedUserAccount.role).toBe(userAccount.role); - // - // done(); - // } - // }); - // }); - - it('POST /api/user-accounts/register does not register a user when logged out', function (done) { - request(app) - .post('/api/user-accounts/register') - .set('Cookie', Array.from(apiCookies.values())) - .expect(400) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); - }); - - after(async function() { - await database.closeConnection(); - await server.close(); - }); + let app; + + before(async function () { + // Configure the test to use OIDC authentication + process.env.AUTHN_MECHANISM = 'oidc'; + process.env.AUTHN_OIDC_ISSUER_URL = `http://${oidcHost}/realms/${oidcRealm}/.well-known/openid-configuration`; + process.env.AUTHN_OIDC_CLIENT_ID = oidcClientId; + + config.reloadConfig(); + + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the OIDC server + const options = { + basePath: oidcHost, + realmName: oidcRealm, + clientId: oidcClientId, + description: 'client', + standardFlowEnabled: true, + redirectUris: [localServerRedirectUrl], + clientSecret: oidcClientSecret, + }; + const clientCredentials = await keycloak.initializeKeycloak(options); + // eslint-disable-next-line require-atomic-updates + config.userAuthn.oidc.clientSecret = clientCredentials.value; + + // Add a test user + await keycloak.addUsersToKeycloak(options, testUser); + + // Initialize the express app + app = await require('../../index').initializeApp(); + + // Open a port to receive redirects from the identity provider + startServer(app, localServerPort); + }); + + const apiCookies = new Map(); + let redirectPath; + const destination = `${localServerHost}:${localServerPort}/login-page`; + it('GET /api/authn/oidc/login successfully receives a redirect to the identity provider', function (done) { + const encodedDestination = encodeURIComponent(destination); + request(app) + .get(`/api/authn/oidc/login?destination=${encodedDestination}`) + .expect(302) + .end(function (err, res) { + if (err) { + done(err); + } else { + // Save the cookies for later tests + const newCookies = setCookieParser(res); + updateCookies(newCookies, apiCookies); + + // Get the redirect location + redirectPath = res.headers.location; + + done(); + } + }); + }); + + let signInPath; + const ipCookies = new Map(); + it('redirect successfully receives a challenge from the identity provider', function (done) { + const url = new URL(redirectPath); + const server = `${url.protocol}//${url.host}`; + const path = url.pathname; + const search = url.search; + request(server) + .get(path + search) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + // Save the cookies for later tests + const newCookies = setCookieParser(res); + updateCookies(newCookies, ipCookies); + + const action = extractFormAction(res.text); + signInPath = action.value; + + done(); + } + }); + }); + + it('POST formpath successfully signs into the identity provider', function (done) { + const signinUrl = new URL(signInPath); + const server = `${signinUrl.protocol}//${signinUrl.host}`; + const path = signinUrl.pathname; + const search = signinUrl.search; + request(server) + .post(path + search) + .set('Cookie', Array.from(ipCookies.values())) + .send(`username=${testUser.username}`) + .send(`password=${testUser.password}`) + .send('credentialId=') + .expect(302) + .end(function (err, res) { + if (err) { + done(err); + } else { + // Get the redirect location + redirectPath = res.headers.location; + + done(); + } + }); + }); + + it('redirect successfully completes the sign in process', function (done) { + const url = new URL(redirectPath); + const server = `${url.protocol}//${url.host}`; + const path = url.pathname; + const search = url.search; + request(server) + .get(path + search) + .set('Cookie', Array.from(apiCookies.values())) + .expect(302) + .end(function (err, res) { + if (err) { + done(err); + } else { + // Session ID is changed after login, save the cookie for later tests + const newCookies = setCookieParser(res); + updateCookies(newCookies, apiCookies); + + // Get the redirect location + redirectPath = res.headers.location; + + // This should be the destination provided at the start of the sign in process + expect(redirectPath).toBe(destination); + + done(); + } + }); + }); + + it('GET /api/session returns the user session', function (done) { + request(app) + .get('/api/session') + .set('Accept', 'application/json') + .set('Cookie', Array.from(apiCookies.values())) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the current session + const session = res.body; + expect(session).toBeDefined(); + expect(session.email).toBe(testUser.email); + expect(session.registered).toBe(false); + expect(session.name).toBe(testUser.username); + expect(session.displayName).toBe(`${testUser.firstName} ${testUser.lastName}`); + + done(); + } + }); + }); + + let userAccount; + it('POST /api/user-accounts/register successfully registers the user', function (done) { + request(app) + .post('/api/user-accounts/register') + .set('Cookie', Array.from(apiCookies.values())) + .expect(201) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the new user account + userAccount = res.body; + expect(userAccount).toBeDefined(); + expect(userAccount.email).toBe(testUser.email); + expect(userAccount.username).toBe(testUser.username); + expect(userAccount.displayName).toBe(`${testUser.firstName} ${testUser.lastName}`); + expect(userAccount.status).toBe('pending'); + expect(userAccount.role).toBe('none'); + + done(); + } + }); + }); + + it('GET /api/session returns the user session', function (done) { + request(app) + .get('/api/session') + .set('Accept', 'application/json') + .set('Cookie', Array.from(apiCookies.values())) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get the current session with a registered user + const session = res.body; + expect(session).toBeDefined(); + expect(session.email).toBe(testUser.email); + expect(session.registered).toBe(true); + expect(session.name).toBe(testUser.username); + expect(session.displayName).toBe(`${testUser.firstName} ${testUser.lastName}`); + + done(); + } + }); + }); + + it('GET /api/authn/oidc/logout successfully logs the user out', function (done) { + request(app) + .get('/api/authn/oidc/logout') + .set('Accept', 'application/json') + .set('Cookie', Array.from(apiCookies.values())) + .expect(200) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + it('GET /api/session returns not authorized (after logging out)', function (done) { + request(app) + .get('/api/session') + .set('Accept', 'application/json') + .set('Cookie', Array.from(apiCookies.values())) + .expect(401) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + // it('GET /api/user-accounts/:id returns the added user account', function (done) { + // request(app) + // .get('/api/user-accounts/' + userAccount.id) + // .set('Accept', 'application/json') + // .expect(200) + // .expect('Content-Type', /json/) + // .end(function (err, res) { + // if (err) { + // done(err); + // } else { + // // We expect to get one user account in an array + // const retrievedUserAccount = res.body; + // expect(retrievedUserAccount).toBeDefined(); + // expect(retrievedUserAccount.id).toBe(userAccount.id); + // expect(retrievedUserAccount.email).toBe(userAccount.email); + // expect(retrievedUserAccount.username).toBe(userAccount.username); + // expect(retrievedUserAccount.displayName).toBe(userAccount.displayName); + // expect(retrievedUserAccount.status).toBe(userAccount.status); + // expect(retrievedUserAccount.role).toBe(userAccount.role); + // + // done(); + // } + // }); + // }); + + it('POST /api/user-accounts/register does not register a user when logged out', function (done) { + request(app) + .post('/api/user-accounts/register') + .set('Cookie', Array.from(apiCookies.values())) + .expect(400) + .end(function (err, res) { + if (err) { + done(err); + } else { + done(); + } + }); + }); + + after(async function () { + await database.closeConnection(); + await server.close(); + }); }); - diff --git a/app/tests/config/config.spec.js b/app/tests/config/config.spec.js index ac322a96..bec1de85 100644 --- a/app/tests/config/config.spec.js +++ b/app/tests/config/config.spec.js @@ -9,46 +9,47 @@ const database = require('../../lib/database-in-memory'); const databaseConfiguration = require('../../lib/database-configuration'); describe('App Configuration', function () { - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - }); - - it('The config values should be set by the config file', function(done) { - // Defaults, not set by config file - expect(config.app.env).toBe('development'); - expect(config.server.port).toBe(3000); - - // Set by config file - expect(config.app.name).toBe('test-config'); - expect(config.collectionIndex.defaultInterval).toBe(100); - expect(config.configurationFiles.staticMarkingDefinitionsPath).toBe('./app/tests/config/test-static-marking-definitions'); + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + }); + + it('The config values should be set by the config file', function (done) { + // Defaults, not set by config file + expect(config.app.env).toBe('development'); + expect(config.server.port).toBe(3000); + + // Set by config file + expect(config.app.name).toBe('test-config'); + expect(config.collectionIndex.defaultInterval).toBe(100); + expect(config.configurationFiles.staticMarkingDefinitionsPath).toBe( + './app/tests/config/test-static-marking-definitions', + ); + + done(); + }); + + it('The static marking definitions should be created', function (done) { + const options = {}; + markingDefinitionsService.retrieveAll(options, function (err, markingDefinitions) { + if (err) { + done(err); + } else { + // We expect to get two marking definitions + expect(markingDefinitions).toBeDefined(); + expect(Array.isArray(markingDefinitions)).toBe(true); + expect(markingDefinitions.length).toBe(2); done(); + } }); + }); - it('The static marking definitions should be created', function(done) { - const options = {}; - markingDefinitionsService.retrieveAll(options, function(err, markingDefinitions) { - if (err) { - done(err); - } - else { - // We expect to get two marking definitions - expect(markingDefinitions).toBeDefined(); - expect(Array.isArray(markingDefinitions)).toBe(true); - expect(markingDefinitions.length).toBe(2); - - done(); - } - }); - }); - - after(async function() { - await database.closeConnection(); - }); + after(async function () { + await database.closeConnection(); + }); }); diff --git a/app/tests/config/test-config.json b/app/tests/config/test-config.json index cfa90ad2..9c0093bc 100644 --- a/app/tests/config/test-config.json +++ b/app/tests/config/test-config.json @@ -2,7 +2,7 @@ "app": { "name": "test-config" }, - "configurationFiles" : { + "configurationFiles": { "staticMarkingDefinitionsPath": "./app/tests/config/test-static-marking-definitions" }, "collectionIndex": { diff --git a/app/tests/fuzz/user-accounts-fuzz.spec.js b/app/tests/fuzz/user-accounts-fuzz.spec.js index c4e95a25..b099a7a4 100644 --- a/app/tests/fuzz/user-accounts-fuzz.spec.js +++ b/app/tests/fuzz/user-accounts-fuzz.spec.js @@ -15,128 +15,128 @@ const propertyFuzzLimit = 100; const objectFuzzLimit = 100; function fuzzString(maxByteLength) { - const stringBase = 'ascii'; - const byteLength = crypto.randomInt(0, maxByteLength + 1); - const buffer = crypto.randomBytes(byteLength); - return buffer.toString(stringBase); + const stringBase = 'ascii'; + const byteLength = crypto.randomInt(0, maxByteLength + 1); + const buffer = crypto.randomBytes(byteLength); + return buffer.toString(stringBase); } function escapeUnprintable(unsafe) { - const box = String.fromCharCode(9618); - // eslint-disable-next-line no-control-regex - const charsRegex = /[\u0000-\u002F\u003A-\u0040\u005B-\u0060\u007B-\u00FF]/g; - return unsafe.replace(charsRegex, c => box); + const box = String.fromCharCode(9618); + // eslint-disable-next-line no-control-regex + const charsRegex = /[\u0000-\u002F\u003A-\u0040\u005B-\u0060\u007B-\u00FF]/g; + return unsafe.replace(charsRegex, (c) => box); } describe('User Accounts API Test Invalid Data', function () { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + // Data Fuzzing I + for (let i = 0; i < parameterFuzzLimit1; i++) { + const userAccountData = { + email: fuzzString(600), + username: fuzzString(600), + displayName: fuzzString(600), + status: fuzzString(600), + role: fuzzString(600), + }; + + it(`POST /api/user-accounts does not create a user account with invalid data (${escapeUnprintable(userAccountData.username)})`, async function () { + const body = userAccountData; + await request(app) + .post('/api/user-accounts') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); }); - - // Data Fuzzing I - for (let i = 0; i < parameterFuzzLimit1; i++) { - const userAccountData = { - "email": fuzzString(600), - "username": fuzzString(600), - "displayName": fuzzString(600), - "status": fuzzString(600), - "role": fuzzString(600) - }; - - it(`POST /api/user-accounts does not create a user account with invalid data (${ escapeUnprintable(userAccountData.username) })`, async function () { - const body = userAccountData; - await request(app) - .post('/api/user-accounts') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - } - - // Data Fuzzing II - for (let i = 0; i < parameterFuzzLimit2; i++) { - const userAccountData = { - "email": fuzzString(600), - "username": fuzzString(600), - "displayName": fuzzString(600), - "status": 'active', - "role": 'admin' - }; - - it(`POST /api/user-accounts does create a user account with fuzzed data (${ escapeUnprintable(userAccountData.username) })`, async function () { - const body = userAccountData; - await request(app) - .post('/api/user-accounts') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201); - }); - } - - // Property Fuzzing - for (let i = 0; i < propertyFuzzLimit; i++) { - const parameterName = fuzzString(60); - const userAccountData = { - "email": fuzzString(60), - "username": fuzzString(60), - "displayName": fuzzString(60), - "status": fuzzString(60), - "role": fuzzString(60), - [parameterName]: fuzzString(60) - }; - - it(`POST /api/user-accounts does not create a user account with invalid property (${ escapeUnprintable(parameterName) })`, async function () { - const body = userAccountData; - await request(app) - .post('/api/user-accounts') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - } - - // Object Fuzzing - for (let i = 0; i < objectFuzzLimit; i++) { - const parameterName = fuzzString(60); - const userAccountData = { - "email": fuzzString(60), - "username": { - [parameterName]: fuzzString(60) - }, - "displayName": fuzzString(60), - "status": 'active', - "role": 'admin' - }; - - it(`POST /api/user-accounts does not create a user account with a non-string username (${ escapeUnprintable(parameterName) })`, async function () { - const body = userAccountData; - await request(app) - .post('/api/user-accounts') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400); - }); - } - - after(async function() { - await database.closeConnection(); + } + + // Data Fuzzing II + for (let i = 0; i < parameterFuzzLimit2; i++) { + const userAccountData = { + email: fuzzString(600), + username: fuzzString(600), + displayName: fuzzString(600), + status: 'active', + role: 'admin', + }; + + it(`POST /api/user-accounts does create a user account with fuzzed data (${escapeUnprintable(userAccountData.username)})`, async function () { + const body = userAccountData; + await request(app) + .post('/api/user-accounts') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201); + }); + } + + // Property Fuzzing + for (let i = 0; i < propertyFuzzLimit; i++) { + const parameterName = fuzzString(60); + const userAccountData = { + email: fuzzString(60), + username: fuzzString(60), + displayName: fuzzString(60), + status: fuzzString(60), + role: fuzzString(60), + [parameterName]: fuzzString(60), + }; + + it(`POST /api/user-accounts does not create a user account with invalid property (${escapeUnprintable(parameterName)})`, async function () { + const body = userAccountData; + await request(app) + .post('/api/user-accounts') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); }); + } + + // Object Fuzzing + for (let i = 0; i < objectFuzzLimit; i++) { + const parameterName = fuzzString(60); + const userAccountData = { + email: fuzzString(60), + username: { + [parameterName]: fuzzString(60), + }, + displayName: fuzzString(60), + status: 'active', + role: 'admin', + }; + + it(`POST /api/user-accounts does not create a user account with a non-string username (${escapeUnprintable(parameterName)})`, async function () { + const body = userAccountData; + await request(app) + .post('/api/user-accounts') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); + } + + after(async function () { + await database.closeConnection(); + }); }); diff --git a/app/tests/import/collection-bundles-enterprise.spec.js b/app/tests/import/collection-bundles-enterprise.spec.js index e66f0895..a8afa6ad 100644 --- a/app/tests/import/collection-bundles-enterprise.spec.js +++ b/app/tests/import/collection-bundles-enterprise.spec.js @@ -7,7 +7,7 @@ logger.level = 'debug'; const database = require('../../lib/database-in-memory'); const databaseConfiguration = require('../../lib/database-configuration'); -const path = require("path"); +const path = require('path'); const login = require('../shared/login'); @@ -17,130 +17,130 @@ const testFilePath = './test-files'; const collectionBundleFilenames = []; const directory = path.join(__dirname, testFilePath); if (fs.existsSync(directory)) { - fs.readdirSync(directory).forEach(filename => { - if (filename.endsWith('.json')) { - collectionBundleFilenames.push(filename); - } - }); + fs.readdirSync(directory).forEach((filename) => { + if (filename.endsWith('.json')) { + collectionBundleFilenames.push(filename); + } + }); } async function readJson(filePath) { - const fullPath = require.resolve(filePath); - const data = await fs.promises.readFile(fullPath); - return JSON.parse(data); + const fullPath = require.resolve(filePath); + const data = await fs.promises.readFile(fullPath); + return JSON.parse(data); } -describe('Collection Bundles API Full-Size Test', function() { - let app; - let passportCookie; - - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); - - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); - - // Initialize the express app - app = await require('../../index').initializeApp(); - - // Log into the app - passportCookie = await login.loginAnonymous(app); +describe('Collection Bundles API Full-Size Test', function () { + let app; + let passportCookie; + + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); + + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); + + // Initialize the express app + app = await require('../../index').initializeApp(); + + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); + + // Create one test suite for each collection bundle + for (const filename of collectionBundleFilenames) { + describe(`Test suite for the ${filename} collection bundle`, function () { + let collectionBundle; + + before(async function () { + const filePath = testFilePath + '/' + filename; + collectionBundle = await readJson(filePath); + }); + + it(`POST /api/collection-bundles previews the import of the ${filename} collection bundle (checkOnly)`, async function () { + this.timeout(30000); + const body = collectionBundle; + const res = await request(app) + .post('/api/collection-bundles?checkOnly=true') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created collection object + const collection = res.body; + expect(collection).toBeDefined(); + + // MITRE marking definition is missing from x_mitre_contents in the bundle + expect(collection.workspace.import_categories.errors.length).toBe(1); + }); + + it(`POST /api/collection-bundles previews the import of the ${filename} collection bundle (previewOnly)`, async function () { + this.timeout(30000); + const body = collectionBundle; + const res = await request(app) + .post('/api/collection-bundles?previewOnly=true') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created collection object + const collection = res.body; + expect(collection).toBeDefined(); + + // MITRE marking definition is missing from x_mitre_contents in the bundle + expect(collection.workspace.import_categories.errors.length).toBe(1); + }); + + it(`POST /api/collection-bundles imports the ${filename} collection bundle`, async function () { + this.timeout(60000); + const body = collectionBundle; + const res = await request(app) + .post('/api/collection-bundles') + .send(body) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(201) + .expect('Content-Type', /json/); + + // We expect to get the created collection object + const collection = res.body; + expect(collection).toBeDefined(); + + // MITRE marking definition is missing from x_mitre_contents in the bundle + expect(collection.workspace.import_categories.errors.length).toBe(1); + }); + + const domain = 'enterprise-attack'; + it('GET /api/stix-bundles exports the STIX bundle', async function () { + const res = await request(app) + .get(`/api/stix-bundles?domain=${domain}`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get the exported stix bundle + const stixBundle = res.body; + expect(stixBundle).toBeDefined(); + expect(Array.isArray(stixBundle.objects)).toBe(true); + + // We expect to get at most one of any object + const objectMap = new Map(); + for (const stixObject of stixBundle.objects) { + expect(objectMap.get(stixObject.id)).toBeUndefined(); + objectMap.set(stixObject.id, stixObject.id); + } + }); }); + } - // Create one test suite for each collection bundle - for (const filename of collectionBundleFilenames) { - describe(`Test suite for the ${ filename } collection bundle`, function () { - let collectionBundle; - - before(async function() { - const filePath = testFilePath + '/' + filename; - collectionBundle = await readJson(filePath); - }); - - it(`POST /api/collection-bundles previews the import of the ${ filename } collection bundle (checkOnly)`, async function() { - this.timeout(30000); - const body = collectionBundle; - const res = await request(app) - .post('/api/collection-bundles?checkOnly=true') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created collection object - const collection = res.body; - expect(collection).toBeDefined(); - - // MITRE marking definition is missing from x_mitre_contents in the bundle - expect(collection.workspace.import_categories.errors.length).toBe(1); - }); - - it(`POST /api/collection-bundles previews the import of the ${ filename } collection bundle (previewOnly)`, async function() { - this.timeout(30000); - const body = collectionBundle; - const res = await request(app) - .post('/api/collection-bundles?previewOnly=true') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created collection object - const collection = res.body; - expect(collection).toBeDefined(); - - // MITRE marking definition is missing from x_mitre_contents in the bundle - expect(collection.workspace.import_categories.errors.length).toBe(1); - }); - - it(`POST /api/collection-bundles imports the ${ filename } collection bundle`, async function() { - this.timeout(60000); - const body = collectionBundle; - const res = await request(app) - .post('/api/collection-bundles') - .send(body) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(201) - .expect('Content-Type', /json/); - - // We expect to get the created collection object - const collection = res.body; - expect(collection).toBeDefined(); - - // MITRE marking definition is missing from x_mitre_contents in the bundle - expect(collection.workspace.import_categories.errors.length).toBe(1); - }); - - const domain = 'enterprise-attack'; - it('GET /api/stix-bundles exports the STIX bundle', async function() { - const res = await request(app) - .get(`/api/stix-bundles?domain=${ domain }`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/); - - // We expect to get the exported stix bundle - const stixBundle = res.body; - expect(stixBundle).toBeDefined(); - expect(Array.isArray(stixBundle.objects)).toBe(true); - - // We expect to get at most one of any object - const objectMap = new Map(); - for (const stixObject of stixBundle.objects) { - expect(objectMap.get(stixObject.id)).toBeUndefined(); - objectMap.set(stixObject.id, stixObject.id); - } - }); - }); - } - - after(async function() { - await database.closeConnection(); - }); + after(async function () { + await database.closeConnection(); + }); }); diff --git a/app/tests/integration-test/README.md b/app/tests/integration-test/README.md index dd630b03..bc261a38 100644 --- a/app/tests/integration-test/README.md +++ b/app/tests/integration-test/README.md @@ -1,13 +1,13 @@ ## Integration Test -### 1 Start the Servers +### 1 Start the Servers - MongoDB - REST API - Mock Remote Host - Collection Manager -### 2 Clear the Database +### 2 Clear the Database In the attack-workbench-rest-api project, run: @@ -17,7 +17,7 @@ node ./scripts/clearDatabase.js Also, delete the collection index manually. (The script only deletes ATT&CK objects and references.) -### 3 Initialize Data +### 3 Initialize Data In this project, run: @@ -26,6 +26,7 @@ bash ./tests/integration-test/initialize-data.sh ``` This script will: + - Clear the test directories - Copy the collection index v1 file to the index test directory - Copy the collection bundle Blue v1 to the bundle test directory @@ -33,7 +34,7 @@ This script will: Because the collection index is initialized with a subscription for the Blue collection, this should cause the Collection Manager to import the collection bundle Blue v1. -### 4 Update the Collection Index +### 4 Update the Collection Index In this project, run: @@ -42,12 +43,13 @@ bash ./tests/integration-test/update-collection-a.sh ``` This script will: + - Copy collection index v2 to the index test directory, overwriting v1 - Copy the collection bundles Blue v2, Red v1, and Green v1 to the bundle test directory Due to the subscription to the Blue collection, this should cause the Collection Manager to import the collection bundle Blue v2. -### 5 Add a New Subscription +### 5 Add a New Subscription In this project, run: @@ -56,6 +58,7 @@ bash ./tests/integration-test/update-subscription.sh ``` This script will: + - Modify the collection index in the database, adding a subscription to the Green collection Due to the added subscription to the Green collection, this should cause the Collection Manager to import the collection bundle Green v1. diff --git a/app/tests/integration-test/initialize-data.js b/app/tests/integration-test/initialize-data.js index 49687fe6..b5ebbba8 100644 --- a/app/tests/integration-test/initialize-data.js +++ b/app/tests/integration-test/initialize-data.js @@ -9,54 +9,51 @@ const passportCookieName = 'connect.sid'; let passportCookie; async function login(url) { - const res = await superagent.get(url); - const cookies = setCookieParser(res); - passportCookie = cookies.find(c => c.name === passportCookieName); + const res = await superagent.get(url); + const cookies = setCookieParser(res); + passportCookie = cookies.find((c) => c.name === passportCookieName); } function post(url, data) { - return superagent - .post(url) - .set('Cookie', `${ passportCookieName }=${ passportCookie.value }`) - .send(data); + return superagent + .post(url) + .set('Cookie', `${passportCookieName}=${passportCookie.value}`) + .send(data); } async function initializeData() { - // Read the collection index v1 from the file - const collectionIndexJson = require('./mock-data/collection-index-v1.json'); - - // Create the collection index object, including a subscription to the Blue collection - const collectionIndex = { - collection_index: collectionIndexJson, - workspace: { - remote_url: 'http://localhost/collection-indexes/collection-index.json', - update_policy: { - automatic: true, - interval: 30, - last_retrieval: new Date().toISOString(), - subscriptions: [ - collectionIndexJson.collections[0].id - ] - } - } - }; - - // Log into the Workbench REST API - const loginUrl = 'http://localhost:3000/api/authn/anonymous/login'; - await login(loginUrl); - - // Import the collection index v1 into the database - const postCollectionIndexesUrl = 'http://localhost:3000/api/collection-indexes'; - await post(postCollectionIndexesUrl, collectionIndex); + // Read the collection index v1 from the file + const collectionIndexJson = require('./mock-data/collection-index-v1.json'); + + // Create the collection index object, including a subscription to the Blue collection + const collectionIndex = { + collection_index: collectionIndexJson, + workspace: { + remote_url: 'http://localhost/collection-indexes/collection-index.json', + update_policy: { + automatic: true, + interval: 30, + last_retrieval: new Date().toISOString(), + subscriptions: [collectionIndexJson.collections[0].id], + }, + }, + }; + + // Log into the Workbench REST API + const loginUrl = 'http://localhost:3000/api/authn/anonymous/login'; + await login(loginUrl); + + // Import the collection index v1 into the database + const postCollectionIndexesUrl = 'http://localhost:3000/api/collection-indexes'; + await post(postCollectionIndexesUrl, collectionIndex); } initializeData() - .then(() => { - console.log('initializeData() - Terminating normally'); - process.exit(); - }) - .catch(err => { - console.log('initializeData() - Error: ' + err); - process.exit(1); - }); - + .then(() => { + console.log('initializeData() - Terminating normally'); + process.exit(); + }) + .catch((err) => { + console.log('initializeData() - Error: ' + err); + process.exit(1); + }); diff --git a/app/tests/integration-test/mock-data/blue-v1.json b/app/tests/integration-test/mock-data/blue-v1.json index 68370c12..0e26b724 100644 --- a/app/tests/integration-test/mock-data/blue-v1.json +++ b/app/tests/integration-test/mock-data/blue-v1.json @@ -13,9 +13,7 @@ "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "created": "2020-01-01T01:01:01.111111Z", "modified": "2020-01-01T01:01:01.111111Z", - "object_marking_refs": [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], + "object_marking_refs": ["marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"], "x_mitre_contents": [ { "object_ref": "intrusion-set--e094675a-4c0f-4f90-96e8-1ac0f349ebb8", @@ -36,9 +34,7 @@ ] }, { - "object_marking_refs": [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], + "object_marking_refs": ["marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"], "type": "intrusion-set", "name": "BLUE BANANA", "x_mitre_version": "1.0", @@ -60,9 +56,7 @@ "x_mitre_modified_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5" }, { - "object_marking_refs": [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], + "object_marking_refs": ["marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"], "type": "intrusion-set", "name": "BLUE BOXCARS", "x_mitre_version": "1.0", @@ -87,9 +81,7 @@ "modified": "2017-06-01T00:00:00.000Z", "type": "identity", "identity_class": "organization", - "object_marking_refs": [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], + "object_marking_refs": ["marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"], "name": "The MITRE Corporation", "id": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "created": "2017-06-01T00:00:00.000Z", diff --git a/app/tests/integration-test/mock-data/blue-v2.json b/app/tests/integration-test/mock-data/blue-v2.json index c4655ee7..ee9ebcd2 100644 --- a/app/tests/integration-test/mock-data/blue-v2.json +++ b/app/tests/integration-test/mock-data/blue-v2.json @@ -13,9 +13,7 @@ "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "created": "2020-01-01T01:01:01.111111Z", "modified": "2020-02-02T02:02:02.222222Z", - "object_marking_refs": [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], + "object_marking_refs": ["marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"], "x_mitre_contents": [ { "object_ref": "intrusion-set--e094675a-4c0f-4f90-96e8-1ac0f349ebb8", @@ -40,9 +38,7 @@ ] }, { - "object_marking_refs": [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], + "object_marking_refs": ["marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"], "type": "intrusion-set", "name": "BLUE BANANA", "x_mitre_version": "1.0", @@ -64,9 +60,7 @@ "x_mitre_modified_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5" }, { - "object_marking_refs": [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], + "object_marking_refs": ["marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"], "type": "intrusion-set", "name": "BLUE BOXCARS", "x_mitre_version": "1.0", @@ -88,9 +82,7 @@ "x_mitre_modified_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5" }, { - "object_marking_refs": [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], + "object_marking_refs": ["marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"], "type": "intrusion-set", "name": "BLUE BILLBOARDS", "x_mitre_version": "1.0", @@ -115,9 +107,7 @@ "modified": "2017-06-01T00:00:00.000Z", "type": "identity", "identity_class": "organization", - "object_marking_refs": [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], + "object_marking_refs": ["marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"], "name": "The MITRE Corporation", "id": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "created": "2017-06-01T00:00:00.000Z", diff --git a/app/tests/integration-test/mock-data/green-v1.json b/app/tests/integration-test/mock-data/green-v1.json index b277fcc4..ca5bb099 100644 --- a/app/tests/integration-test/mock-data/green-v1.json +++ b/app/tests/integration-test/mock-data/green-v1.json @@ -13,9 +13,7 @@ "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "created": "2020-01-21T01:01:01.111111Z", "modified": "2020-01-21T01:01:01.111111Z", - "object_marking_refs": [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], + "object_marking_refs": ["marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"], "x_mitre_contents": [ { "object_ref": "intrusion-set--91421801-9285-4668-88d5-f8ccbb436215", @@ -36,9 +34,7 @@ ] }, { - "object_marking_refs": [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], + "object_marking_refs": ["marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"], "type": "intrusion-set", "name": "GREEN BANANA", "x_mitre_version": "1.0", @@ -60,9 +56,7 @@ "x_mitre_modified_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5" }, { - "object_marking_refs": [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], + "object_marking_refs": ["marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"], "type": "intrusion-set", "name": "GREEN BOXCARS", "x_mitre_version": "1.0", @@ -87,9 +81,7 @@ "modified": "2017-06-01T00:00:00.000Z", "type": "identity", "identity_class": "organization", - "object_marking_refs": [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], + "object_marking_refs": ["marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"], "name": "The MITRE Corporation", "id": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "created": "2017-06-01T00:00:00.000Z", diff --git a/app/tests/integration-test/mock-data/red-v1.json b/app/tests/integration-test/mock-data/red-v1.json index 6fb33b2d..207cb09d 100644 --- a/app/tests/integration-test/mock-data/red-v1.json +++ b/app/tests/integration-test/mock-data/red-v1.json @@ -13,9 +13,7 @@ "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "created": "2020-01-11T01:01:01.111111Z", "modified": "2020-01-11T01:01:01.111111Z", - "object_marking_refs": [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], + "object_marking_refs": ["marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"], "x_mitre_contents": [ { "object_ref": "intrusion-set--c7900f4e-a4a7-4346-94d4-26fb493013c2", @@ -36,9 +34,7 @@ ] }, { - "object_marking_refs": [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], + "object_marking_refs": ["marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"], "type": "intrusion-set", "name": "RED BANANA", "x_mitre_version": "1.0", @@ -60,9 +56,7 @@ "x_mitre_modified_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5" }, { - "object_marking_refs": [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], + "object_marking_refs": ["marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"], "type": "intrusion-set", "name": "RED BOXCARS", "x_mitre_version": "1.0", @@ -87,9 +81,7 @@ "modified": "2017-06-01T00:00:00.000Z", "type": "identity", "identity_class": "organization", - "object_marking_refs": [ - "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" - ], + "object_marking_refs": ["marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168"], "name": "The MITRE Corporation", "id": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "created": "2017-06-01T00:00:00.000Z", diff --git a/app/tests/integration-test/mock-remote-host/index.js b/app/tests/integration-test/mock-remote-host/index.js index be7b852b..4da7b891 100644 --- a/app/tests/integration-test/mock-remote-host/index.js +++ b/app/tests/integration-test/mock-remote-host/index.js @@ -1,55 +1,54 @@ #!/usr/bin/env node function initializeApp() { - const express = require('express'); - const app = express(); + const express = require('express'); + const app = express(); - // Set up the static routes - app.use('/collection-indexes', express.static('mock-data/collection-indexes')); - app.use('/collection-bundles', express.static('mock-data/collection-bundles')); + // Set up the static routes + app.use('/collection-indexes', express.static('mock-data/collection-indexes')); + app.use('/collection-bundles', express.static('mock-data/collection-bundles')); - return app; + return app; } async function runMockRemoteHost() { - // Create the express app - const app = initializeApp(); - - const port = process.env.PORT ?? 80; - const server = app.listen(port, function () { - const host = server.address().address; - const port = server.address().port; - - console.info(`Listening at http://${ host }:${ port }`); - console.info('Mock Remote Host start up complete'); - }) - - // Listen for a ctrl-c - process.on('SIGINT', () => { - console.info('SIGINT received, stopping HTTP server'); - server.close(); - }); - - // Docker terminates a container with a SIGTERM - process.on('SIGTERM', () => { - console.info('SIGTERM received, stopping HTTP server'); - server.close(); - }); - - // Wait for the server to close - const events = require('events'); - await events.once(server, 'close'); - - console.info('Mock Remote Host terminating'); + // Create the express app + const app = initializeApp(); + + const port = process.env.PORT ?? 80; + const server = app.listen(port, function () { + const host = server.address().address; + const port = server.address().port; + + console.info(`Listening at http://${host}:${port}`); + console.info('Mock Remote Host start up complete'); + }); + + // Listen for a ctrl-c + process.on('SIGINT', () => { + console.info('SIGINT received, stopping HTTP server'); + server.close(); + }); + + // Docker terminates a container with a SIGTERM + process.on('SIGTERM', () => { + console.info('SIGTERM received, stopping HTTP server'); + server.close(); + }); + + // Wait for the server to close + const events = require('events'); + await events.once(server, 'close'); + + console.info('Mock Remote Host terminating'); } runMockRemoteHost() - .then(() => { - console.log('runMockRemoteHost() - Terminating normally'); - process.exit(); - }) - .catch(err => { - console.log('runMockRemoteHost() - Error: ' + err); - process.exit(1); - }); - + .then(() => { + console.log('runMockRemoteHost() - Terminating normally'); + process.exit(); + }) + .catch((err) => { + console.log('runMockRemoteHost() - Error: ' + err); + process.exit(1); + }); diff --git a/app/tests/integration-test/update-subscription.js b/app/tests/integration-test/update-subscription.js index abcd62fd..cf9a7e90 100644 --- a/app/tests/integration-test/update-subscription.js +++ b/app/tests/integration-test/update-subscription.js @@ -9,52 +9,51 @@ const passportCookieName = 'connect.sid'; let passportCookie; async function login(url) { - const res = await superagent.get(url); - const cookies = setCookieParser(res); - passportCookie = cookies.find(c => c.name === passportCookieName); + const res = await superagent.get(url); + const cookies = setCookieParser(res); + passportCookie = cookies.find((c) => c.name === passportCookieName); } async function get(url) { - const res = await superagent - .get(url) - .set('Cookie', `${ passportCookieName }=${ passportCookie.value }`); + const res = await superagent + .get(url) + .set('Cookie', `${passportCookieName}=${passportCookie.value}`); - return res.body; + return res.body; } function put(url, data) { - return superagent - .put(url) - .set('Cookie', `${ passportCookieName }=${ passportCookie.value }`) - .send(data); + return superagent + .put(url) + .set('Cookie', `${passportCookieName}=${passportCookie.value}`) + .send(data); } async function updateSubscription() { - // Log into the Workbench REST API - const loginUrl = 'http://localhost:3000/api/authn/anonymous/login'; - await login(loginUrl); - - // Get the collection index from the server - const collectionIndexId = '43f56ef6-99a3-455d-9acc-88fce5e9dcd7'; - const getCollectionIndexesUrl = `http://localhost:3000/api/collection-indexes/${ collectionIndexId }`; - const collectionIndex = await get(getCollectionIndexesUrl); - - // Add the subscription to the Green collection - const collectionId = collectionIndex.collection_index.collections[2].id; - collectionIndex.workspace.update_policy.subscriptions.push(collectionId); - - // Write the updated collection index to the server - const putCollectionIndexesUrl = `http://localhost:3000/api/collection-indexes/${ collectionIndexId }`; - await put(putCollectionIndexesUrl, collectionIndex); + // Log into the Workbench REST API + const loginUrl = 'http://localhost:3000/api/authn/anonymous/login'; + await login(loginUrl); + + // Get the collection index from the server + const collectionIndexId = '43f56ef6-99a3-455d-9acc-88fce5e9dcd7'; + const getCollectionIndexesUrl = `http://localhost:3000/api/collection-indexes/${collectionIndexId}`; + const collectionIndex = await get(getCollectionIndexesUrl); + + // Add the subscription to the Green collection + const collectionId = collectionIndex.collection_index.collections[2].id; + collectionIndex.workspace.update_policy.subscriptions.push(collectionId); + + // Write the updated collection index to the server + const putCollectionIndexesUrl = `http://localhost:3000/api/collection-indexes/${collectionIndexId}`; + await put(putCollectionIndexesUrl, collectionIndex); } updateSubscription() - .then(() => { - console.log('updateSubscription() - Terminating normally'); - process.exit(); - }) - .catch(err => { - console.log('updateSubscription() - Error: ' + err); - process.exit(1); - }); - + .then(() => { + console.log('updateSubscription() - Terminating normally'); + process.exit(); + }) + .catch((err) => { + console.log('updateSubscription() - Error: ' + err); + process.exit(1); + }); diff --git a/app/tests/openapi/validate-open-api.spec.js b/app/tests/openapi/validate-open-api.spec.js index 004f5e3e..41cea449 100644 --- a/app/tests/openapi/validate-open-api.spec.js +++ b/app/tests/openapi/validate-open-api.spec.js @@ -1,22 +1,22 @@ const OpenAPISchemaValidator = require('openapi-schema-validator').default; -const refParser = require("@apidevtools/json-schema-ref-parser"); +const refParser = require('@apidevtools/json-schema-ref-parser'); const config = require('../../config/config'); const { expect } = require('expect'); const validator = new OpenAPISchemaValidator({ version: 3 }); describe('OpenAPI Spec Validation', function () { - it('The OpenAPI spec should exist', async function() { - const openApiDoc = await refParser.dereference(config.openApi.specPath); - expect(openApiDoc).toBeDefined(); - }); + it('The OpenAPI spec should exist', async function () { + const openApiDoc = await refParser.dereference(config.openApi.specPath); + expect(openApiDoc).toBeDefined(); + }); - it('The OpenAPI spec should be valid', async function() { - const openApiDoc = await refParser.dereference(config.openApi.specPath); - const results = validator.validate(openApiDoc); + it('The OpenAPI spec should be valid', async function () { + const openApiDoc = await refParser.dereference(config.openApi.specPath); + const results = validator.validate(openApiDoc); - expect(results).toBeDefined(); - expect(results.errors).toBeDefined(); - expect(results.errors.length).toBe(0); - }); + expect(results).toBeDefined(); + expect(results.errors).toBeDefined(); + expect(results.errors.length).toBe(0); + }); }); diff --git a/app/tests/shared/keycloak.js b/app/tests/shared/keycloak.js index 8350b855..84e4519a 100644 --- a/app/tests/shared/keycloak.js +++ b/app/tests/shared/keycloak.js @@ -8,115 +8,109 @@ const defaultAdminUsername = 'admin'; const defaultAdminPassword = 'admin'; async function deleteRealm(basePath, realmName, token) { - // Delete the realm if it exists - try { - await request - .delete(`${ basePath }/admin/realms/${ realmName }`) - .set('Authorization', `bearer ${token}`); - console.info(`Deleted existing realm ${ realmName }`); - } - catch(err) { - if (err.status === 404) { - // Realm not found is ok - } - else { - logger.error('Unable to delete realm'); - throw err; - } - } + // Delete the realm if it exists + try { + await request + .delete(`${basePath}/admin/realms/${realmName}`) + .set('Authorization', `bearer ${token}`); + console.info(`Deleted existing realm ${realmName}`); + } catch (err) { + if (err.status === 404) { + // Realm not found is ok + } else { + logger.error('Unable to delete realm'); + throw err; + } + } } async function createRealm(basePath, realmName, token) { - const realmData = { - realm: realmName, - enabled: true - }; - - try { - await request - .post(`${ basePath }/admin/realms`) - .set('Authorization', `bearer ${token}`) - .send(realmData); - console.info(`Created realm ${ realmName }`); - } - catch (err) { - logger.error(`Unable to create realm ${ realmName }`); - throw err; - } + const realmData = { + realm: realmName, + enabled: true, + }; + + try { + await request + .post(`${basePath}/admin/realms`) + .set('Authorization', `bearer ${token}`) + .send(realmData); + console.info(`Created realm ${realmName}`); + } catch (err) { + logger.error(`Unable to create realm ${realmName}`); + throw err; + } } async function createClient(options, token) { - const clientData = { - clientId: options.clientId, - name: options.clientId, - description: options.description, - enabled: true, - redirectUris: options.redirectUris, - standardFlowEnabled: options.standardFlowEnabled, - serviceAccountsEnabled: options.serviceAccountsEnabled - }; - if (options.clientSecret) { - clientData.secret = options.clientSecret; - } - - try { - await request - .post(`${ options.basePath }/admin/realms/${ options.realmName }/clients`) - .set('Authorization', `bearer ${token}`) - .send(clientData); - console.info(`Created client ${ options.clientId }`); - } - catch (err) { - logger.error('Unable to create client'); - throw err; - } + const clientData = { + clientId: options.clientId, + name: options.clientId, + description: options.description, + enabled: true, + redirectUris: options.redirectUris, + standardFlowEnabled: options.standardFlowEnabled, + serviceAccountsEnabled: options.serviceAccountsEnabled, + }; + if (options.clientSecret) { + clientData.secret = options.clientSecret; + } + + try { + await request + .post(`${options.basePath}/admin/realms/${options.realmName}/clients`) + .set('Authorization', `bearer ${token}`) + .send(clientData); + console.info(`Created client ${options.clientId}`); + } catch (err) { + logger.error('Unable to create client'); + throw err; + } } async function getClient(options, token) { - try { - const res = await request - .get(`${ options.basePath }/admin/realms/${ options.realmName }/clients?clientId=${ options.clientId }`) - .set('Authorization', `bearer ${token}`); - - if (res.body.length === 1) { - return res.body[0]; - } - else { - return null; - } - } - catch (err) { - logger.error('Unable to get client'); - throw err; - } + try { + const res = await request + .get( + `${options.basePath}/admin/realms/${options.realmName}/clients?clientId=${options.clientId}`, + ) + .set('Authorization', `bearer ${token}`); + + if (res.body.length === 1) { + return res.body[0]; + } else { + return null; + } + } catch (err) { + logger.error('Unable to get client'); + throw err; + } } async function createClientSecret(basePath, realmName, idOfClient, token) { - try { - const res = await request - .post(`${ basePath }/admin/realms/${ realmName }/clients/${ idOfClient }/client-secret`) - .set('Authorization', `bearer ${token}`); - - return res.body; - } - catch (err) { - logger.error('Unable to create client secret'); - throw err; - } + try { + const res = await request + .post(`${basePath}/admin/realms/${realmName}/clients/${idOfClient}/client-secret`) + .set('Authorization', `bearer ${token}`); + + return res.body; + } catch (err) { + logger.error('Unable to create client secret'); + throw err; + } } async function getClientSecret(basePath, realmName, idOfClient, token) { - try { - const res = await request - .get(`${ basePath }/admin/realms/${ realmName }/clients/${ idOfClient }/client-secret`) - .set('Authorization', `bearer ${token}`); - - return res.body; - } - catch (err) { - logger.error('Unable to create client secret'); - throw err; - } + try { + const res = await request + .get(`${basePath}/admin/realms/${realmName}/clients/${idOfClient}/client-secret`) + .set('Authorization', `bearer ${token}`); + + return res.body; + } catch (err) { + logger.error('Unable to create client secret'); + throw err; + } } // async function getWellKnownConfiguration(basePath, realmName, token) { @@ -133,99 +127,114 @@ async function getClientSecret(basePath, realmName, idOfClient, token) { // } async function createUser(basePath, realmName, userOptions, token) { - const userData = { - email: userOptions.email, - username: userOptions.username, - firstName: userOptions.firstName, - lastName: userOptions.lastName, - enabled: true, - credentials: [ - { - type: 'password', - value: userOptions.password, - temporary: false - } - ] - }; - - try { - await request - .post(`${ basePath }/admin/realms/${ realmName }/users`) - .set('Authorization', `bearer ${ token }`) - .send(userData); - console.info(`Added user '${ userOptions.username }' to the realm '${ realmName }' on the Keycloak server`); - } - catch (err) { - logger.error('Unable to create user'); - throw err; - } + const userData = { + email: userOptions.email, + username: userOptions.username, + firstName: userOptions.firstName, + lastName: userOptions.lastName, + enabled: true, + credentials: [ + { + type: 'password', + value: userOptions.password, + temporary: false, + }, + ], + }; + + try { + await request + .post(`${basePath}/admin/realms/${realmName}/users`) + .set('Authorization', `bearer ${token}`) + .send(userData); + console.info( + `Added user '${userOptions.username}' to the realm '${realmName}' on the Keycloak server`, + ); + } catch (err) { + logger.error('Unable to create user'); + throw err; + } } async function getAuthorizationToken(basePath) { - console.info(`Requesting authorization token from ${ basePath }`); - const res = await request - .post(`${ basePath }/realms/master/protocol/openid-connect/token`) - .send(`client_id=${ adminClientId }`) - .send(`username=${ defaultAdminUsername }`) - .send(`password=${ defaultAdminPassword }`) - .send(`grant_type=password`); - - const adminAccessToken = res.body.access_token; - return adminAccessToken; + console.info(`Requesting authorization token from ${basePath}`); + const res = await request + .post(`${basePath}/realms/master/protocol/openid-connect/token`) + .send(`client_id=${adminClientId}`) + .send(`username=${defaultAdminUsername}`) + .send(`password=${defaultAdminPassword}`) + .send(`grant_type=password`); + + const adminAccessToken = res.body.access_token; + return adminAccessToken; } exports.initializeKeycloak = async function (options) { - const adminAccessToken = await getAuthorizationToken(options.basePath); - - // Configure the server - await deleteRealm(options.basePath, options.realmName, adminAccessToken); - await createRealm(options.basePath, options.realmName, adminAccessToken); - await createClient(options, adminAccessToken); - const client = await getClient(options, adminAccessToken); - if (!options.clientSecret) { - console.info(`clientSecret not provided, creating new clientSecret`); - await createClientSecret(options.basePath, options.realmName, client.id, adminAccessToken); - } - - const clientCredentials = await getClientSecret(options.basePath, options.realmName, client.id, adminAccessToken); - return clientCredentials; + const adminAccessToken = await getAuthorizationToken(options.basePath); + + // Configure the server + await deleteRealm(options.basePath, options.realmName, adminAccessToken); + await createRealm(options.basePath, options.realmName, adminAccessToken); + await createClient(options, adminAccessToken); + const client = await getClient(options, adminAccessToken); + if (!options.clientSecret) { + console.info(`clientSecret not provided, creating new clientSecret`); + await createClientSecret(options.basePath, options.realmName, client.id, adminAccessToken); + } + + const clientCredentials = await getClientSecret( + options.basePath, + options.realmName, + client.id, + adminAccessToken, + ); + return clientCredentials; }; exports.addUsersToKeycloak = async function (serverOptions, users) { - const adminAccessToken = await getAuthorizationToken(serverOptions.basePath); - - if (!Array.isArray(users)) { - users = [ users ]; - } - - for (const user of users) { - // eslint-disable-next-line no-await-in-loop - await createUser(serverOptions.basePath, serverOptions.realmName, user, adminAccessToken); - } + const adminAccessToken = await getAuthorizationToken(serverOptions.basePath); - // await getWellKnownConfiguration(serverOptions.basePath, serverOptions.realmName, adminAccessToken); -} + if (!Array.isArray(users)) { + users = [users]; + } -exports.addClientToKeycloak = async function(clientOptions) { - const adminAccessToken = await getAuthorizationToken(clientOptions.basePath); + for (const user of users) { + // eslint-disable-next-line no-await-in-loop + await createUser(serverOptions.basePath, serverOptions.realmName, user, adminAccessToken); + } - // Configure the server - await createClient(clientOptions, adminAccessToken); - const client = await getClient(clientOptions, adminAccessToken); - if (!clientOptions.clientSecret) { - console.info(`clientSecret not provided, creating new clientSecret`); - await createClientSecret(clientOptions.basePath, clientOptions.realmName, client.id, adminAccessToken); - } -} + // await getWellKnownConfiguration(serverOptions.basePath, serverOptions.realmName, adminAccessToken); +}; -exports.getAccessTokenToClient = async function(clientOptions) { - console.info(`Requesting client access token for ${ clientOptions.clientId } from ${ clientOptions.basePath }`); - const res = await request - .post(`${ clientOptions.basePath }/realms/${ clientOptions.realmName }/protocol/openid-connect/token`) - .send(`client_id=${ clientOptions.clientId }`) - .send(`client_secret=${ clientOptions.clientSecret }`) - .send(`grant_type=client_credentials`); +exports.addClientToKeycloak = async function (clientOptions) { + const adminAccessToken = await getAuthorizationToken(clientOptions.basePath); + + // Configure the server + await createClient(clientOptions, adminAccessToken); + const client = await getClient(clientOptions, adminAccessToken); + if (!clientOptions.clientSecret) { + console.info(`clientSecret not provided, creating new clientSecret`); + await createClientSecret( + clientOptions.basePath, + clientOptions.realmName, + client.id, + adminAccessToken, + ); + } +}; - const clientAccessToken = res.body.access_token; - return clientAccessToken; -} +exports.getAccessTokenToClient = async function (clientOptions) { + console.info( + `Requesting client access token for ${clientOptions.clientId} from ${clientOptions.basePath}`, + ); + const res = await request + .post( + `${clientOptions.basePath}/realms/${clientOptions.realmName}/protocol/openid-connect/token`, + ) + .send(`client_id=${clientOptions.clientId}`) + .send(`client_secret=${clientOptions.clientSecret}`) + .send(`grant_type=client_credentials`); + + const clientAccessToken = res.body.access_token; + return clientAccessToken; +}; diff --git a/app/tests/shared/login.js b/app/tests/shared/login.js index 17d726ad..3d5bd85b 100644 --- a/app/tests/shared/login.js +++ b/app/tests/shared/login.js @@ -1,20 +1,20 @@ 'use strict'; -const request = require("supertest"); -const setCookieParser = require("set-cookie-parser"); +const request = require('supertest'); +const setCookieParser = require('set-cookie-parser'); const passportCookieName = 'connect.sid'; exports.passportCookieName = passportCookieName; -exports.loginAnonymous = async function(app) { - const res = await request(app) - .get('/api/authn/anonymous/login') - .set('Accept', 'application/json') - .expect(200); +exports.loginAnonymous = async function (app) { + const res = await request(app) + .get('/api/authn/anonymous/login') + .set('Accept', 'application/json') + .expect(200); - // Save the cookie for later tests - const cookies = setCookieParser(res); - const passportCookie = cookies.find(c => c.name === passportCookieName); + // Save the cookie for later tests + const cookies = setCookieParser(res); + const passportCookie = cookies.find((c) => c.name === passportCookieName); - return passportCookie; -} + return passportCookie; +}; diff --git a/app/tests/shared/pagination.js b/app/tests/shared/pagination.js index fbe44e4c..83dd47e5 100644 --- a/app/tests/shared/pagination.js +++ b/app/tests/shared/pagination.js @@ -11,287 +11,293 @@ const databaseConfiguration = require('../../lib/database-configuration'); const login = require('./login'); function PaginationTests(service, initialObjectAData, options) { - this.service = service; - this.initialObjectData = initialObjectAData; + this.service = service; + this.initialObjectData = initialObjectAData; - this.options = { - numberOfObjects: 45, - prefix: options.prefix ?? 'test-object', - baseUrl: options.baseUrl, - label: options.label ?? 'TestObjects' - }; + this.options = { + numberOfObjects: 45, + prefix: options.prefix ?? 'test-object', + baseUrl: options.baseUrl, + label: options.label ?? 'TestObjects', + }; - this.options.stateQuery = options.state ? `&state=${ options.state }` : ''; + this.options.stateQuery = options.state ? `&state=${options.state}` : ''; } module.exports = PaginationTests; -PaginationTests.prototype.loadObjects = async function() { - // Initialize the data - for (let i = 0; i < this.options.numberOfObjects; i++) { - const data = _.cloneDeep(this.initialObjectData); - data.stix.name = `${ this.options.prefix }-${ i }`; - - const timestamp = new Date(); - data.stix.created = timestamp.toISOString(); - data.stix.modified = timestamp.toISOString(); - - try { - // eslint-disable-next-line no-await-in-loop - await this.service.create(data, { import: false }); - } - catch(err) { - console.log(err); - } +PaginationTests.prototype.loadObjects = async function () { + // Initialize the data + for (let i = 0; i < this.options.numberOfObjects; i++) { + const data = _.cloneDeep(this.initialObjectData); + data.stix.name = `${this.options.prefix}-${i}`; + + const timestamp = new Date(); + data.stix.created = timestamp.toISOString(); + data.stix.modified = timestamp.toISOString(); + + try { + // eslint-disable-next-line no-await-in-loop + await this.service.create(data, { import: false }); + } catch (err) { + console.log(err); } -} + } +}; -PaginationTests.prototype.executeTests = function() { - const self = this; +PaginationTests.prototype.executeTests = function () { + const self = this; - describe(`${ this.options.label } Pagination API`, function () { - let app; - let passportCookie; + describe(`${this.options.label} Pagination API`, function () { + let app; + let passportCookie; - before(async function() { - // Establish the database connection - // Use an in-memory database that we spin up for the test - await database.initializeConnection(); + before(async function () { + // Establish the database connection + // Use an in-memory database that we spin up for the test + await database.initializeConnection(); - // Check for a valid database configuration - await databaseConfiguration.checkSystemConfiguration(); + // Check for a valid database configuration + await databaseConfiguration.checkSystemConfiguration(); - // Initialize the express app - app = await require('../../index').initializeApp(); + // Initialize the express app + app = await require('../../index').initializeApp(); - // Log into the app - passportCookie = await login.loginAnonymous(app); - }); + // Log into the app + passportCookie = await login.loginAnonymous(app); + }); - it(`GET ${ self.options.baseUrl } return an empty page`, function (done) { - request(app) - .get(`${ self.options.baseUrl }?offset=0&limit=10${ self.options.stateQuery }`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get an array with zero test objects - const testObjects = res.body; - expect(testObjects).toBeDefined(); - expect(Array.isArray(testObjects)).toBe(true); - expect(testObjects.length).toBe(0); - done(); - } - }); + it(`GET ${self.options.baseUrl} return an empty page`, function (done) { + request(app) + .get(`${self.options.baseUrl}?offset=0&limit=10${self.options.stateQuery}`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get an array with zero test objects + const testObjects = res.body; + expect(testObjects).toBeDefined(); + expect(Array.isArray(testObjects)).toBe(true); + expect(testObjects.length).toBe(0); + done(); + } }); + }); - it(`GET ${ self.options.baseUrl } return an empty page with offset`, function (done) { - request(app) - .get(`${ self.options.baseUrl }?offset=10&limit=10${ self.options.stateQuery }`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get an array with zero test objects - const testObjects = res.body; - expect(testObjects).toBeDefined(); - expect(Array.isArray(testObjects)).toBe(true); - expect(testObjects.length).toBe(0); - done(); - } - }); + it(`GET ${self.options.baseUrl} return an empty page with offset`, function (done) { + request(app) + .get(`${self.options.baseUrl}?offset=10&limit=10${self.options.stateQuery}`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get an array with zero test objects + const testObjects = res.body; + expect(testObjects).toBeDefined(); + expect(Array.isArray(testObjects)).toBe(true); + expect(testObjects.length).toBe(0); + done(); + } }); + }); - it(`GET ${ self.options.baseUrl } return an empty page with pagination data`, function (done) { - request(app) - .get(`${ self.options.baseUrl }?offset=0&limit=10&includePagination=true${ self.options.stateQuery }`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get an array with zero test objects - const testObjects = res.body.data; - expect(testObjects).toBeDefined(); - expect(Array.isArray(testObjects)).toBe(true); - expect(testObjects.length).toBe(0); - - // We expect pagination data to be included - const pagination = res.body.pagination; - expect(pagination).toBeDefined(); - expect(pagination.total).toBe(0); - expect(pagination.limit).toBe(10); - expect(pagination.offset).toBe(0); - - done(); - } - }); - }); + it(`GET ${self.options.baseUrl} return an empty page with pagination data`, function (done) { + request(app) + .get( + `${self.options.baseUrl}?offset=0&limit=10&includePagination=true${self.options.stateQuery}`, + ) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get an array with zero test objects + const testObjects = res.body.data; + expect(testObjects).toBeDefined(); + expect(Array.isArray(testObjects)).toBe(true); + expect(testObjects.length).toBe(0); + + // We expect pagination data to be included + const pagination = res.body.pagination; + expect(pagination).toBeDefined(); + expect(pagination.total).toBe(0); + expect(pagination.limit).toBe(10); + expect(pagination.offset).toBe(0); - it(`GET ${ self.options.baseUrl } return an empty page with offset with pagination data`, function (done) { - request(app) - .get(`${ self.options.baseUrl }?offset=10&limit=10&includePagination=true${ self.options.stateQuery }`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get an array with zero test objects - const testObjects = res.body.data; - expect(testObjects).toBeDefined(); - expect(Array.isArray(testObjects)).toBe(true); - expect(testObjects.length).toBe(0); - - // We expect pagination data to be included - const pagination = res.body.pagination; - expect(pagination).toBeDefined(); - expect(pagination.total).toBe(0); - expect(pagination.limit).toBe(10); - expect(pagination.offset).toBe(10); - - done(); - } - }); + done(); + } }); + }); - it(`GET ${ self.options.baseUrl } returns the array of preloaded objects`, async function () { - await self.loadObjects(); - const res = await request(app) - .get(`${ self.options.baseUrl }?offset=0${ self.options.stateQuery }`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .send(); - - // We expect to get all the test objects - const testObjects = res.body; + it(`GET ${self.options.baseUrl} return an empty page with offset with pagination data`, function (done) { + request(app) + .get( + `${self.options.baseUrl}?offset=10&limit=10&includePagination=true${self.options.stateQuery}`, + ) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get an array with zero test objects + const testObjects = res.body.data; expect(testObjects).toBeDefined(); expect(Array.isArray(testObjects)).toBe(true); - expect(testObjects.length).toBe(self.options.numberOfObjects); - }); + expect(testObjects.length).toBe(0); - const pageSizeList = [5, 10, 20]; - const offset = 10; - pageSizeList.forEach((function(pageSize) { - it(`GET ${ self.options.baseUrl } returns a page of preloaded objects`, function (done) { - request(app) - .get(`${ self.options.baseUrl }?offset=${ offset }&limit=${ pageSize }${ self.options.stateQuery }`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get an array with one page of test objects - const testObjects = res.body; - expect(testObjects).toBeDefined(); - expect(Array.isArray(testObjects)).toBe(true); - expect(testObjects.length).toBe(pageSize); - done(); - } - }); - }); - - it(`GET ${ self.options.baseUrl } returns a page of preloaded objects with pagination data`, function (done) { - request(app) - .get(`${ self.options.baseUrl }?offset=${ offset }&limit=${ pageSize }&includePagination=true${ self.options.stateQuery }`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get an array with one page of test objects - const testObjects = res.body.data; - expect(testObjects).toBeDefined(); - expect(Array.isArray(testObjects)).toBe(true); - expect(testObjects.length).toBe(pageSize); - - // We expect pagination data to be included - const pagination = res.body.pagination; - expect(pagination).toBeDefined(); - expect(pagination.total).toBe(self.options.numberOfObjects); - expect(pagination.limit).toBe(pageSize); - expect(pagination.offset).toBe(offset); - - done(); - } - }); - }); - })); - - it(`GET ${ self.options.baseUrl } return a partial page of preloaded objects`, function (done) { - request(app) - .get(`${ self.options.baseUrl }?offset=40&limit=20${ self.options.stateQuery }`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get an array with one page of test objects - const testObjects = res.body; - expect(testObjects).toBeDefined(); - expect(Array.isArray(testObjects)).toBe(true); - expect(testObjects.length).toBe(5); - done(); - } - }); - }); + // We expect pagination data to be included + const pagination = res.body.pagination; + expect(pagination).toBeDefined(); + expect(pagination.total).toBe(0); + expect(pagination.limit).toBe(10); + expect(pagination.offset).toBe(10); - it(`GET ${ self.options.baseUrl } return a partial page of preloaded objects with pagination data`, function (done) { - request(app) - .get(`${ self.options.baseUrl }?offset=40&limit=20&includePagination=true${ self.options.stateQuery }`) - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get an array with one page of test objects - const testObjects = res.body.data; - expect(testObjects).toBeDefined(); - expect(Array.isArray(testObjects)).toBe(true); - expect(testObjects.length).toBe(5); - - // We expect pagination data to be included - const pagination = res.body.pagination; - expect(pagination).toBeDefined(); - expect(pagination.total).toBe(45); - expect(pagination.limit).toBe(20); - expect(pagination.offset).toBe(40); - done(); - } - }); + done(); + } }); + }); - after(async function() { - await database.closeConnection(); - }); + it(`GET ${self.options.baseUrl} returns the array of preloaded objects`, async function () { + await self.loadObjects(); + const res = await request(app) + .get(`${self.options.baseUrl}?offset=0${self.options.stateQuery}`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .send(); + + // We expect to get all the test objects + const testObjects = res.body; + expect(testObjects).toBeDefined(); + expect(Array.isArray(testObjects)).toBe(true); + expect(testObjects.length).toBe(self.options.numberOfObjects); }); -}; + const pageSizeList = [5, 10, 20]; + const offset = 10; + pageSizeList.forEach(function (pageSize) { + it(`GET ${self.options.baseUrl} returns a page of preloaded objects`, function (done) { + request(app) + .get( + `${self.options.baseUrl}?offset=${offset}&limit=${pageSize}${self.options.stateQuery}`, + ) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get an array with one page of test objects + const testObjects = res.body; + expect(testObjects).toBeDefined(); + expect(Array.isArray(testObjects)).toBe(true); + expect(testObjects.length).toBe(pageSize); + done(); + } + }); + }); + + it(`GET ${self.options.baseUrl} returns a page of preloaded objects with pagination data`, function (done) { + request(app) + .get( + `${self.options.baseUrl}?offset=${offset}&limit=${pageSize}&includePagination=true${self.options.stateQuery}`, + ) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get an array with one page of test objects + const testObjects = res.body.data; + expect(testObjects).toBeDefined(); + expect(Array.isArray(testObjects)).toBe(true); + expect(testObjects.length).toBe(pageSize); + + // We expect pagination data to be included + const pagination = res.body.pagination; + expect(pagination).toBeDefined(); + expect(pagination.total).toBe(self.options.numberOfObjects); + expect(pagination.limit).toBe(pageSize); + expect(pagination.offset).toBe(offset); + + done(); + } + }); + }); + }); + + it(`GET ${self.options.baseUrl} return a partial page of preloaded objects`, function (done) { + request(app) + .get(`${self.options.baseUrl}?offset=40&limit=20${self.options.stateQuery}`) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get an array with one page of test objects + const testObjects = res.body; + expect(testObjects).toBeDefined(); + expect(Array.isArray(testObjects)).toBe(true); + expect(testObjects.length).toBe(5); + done(); + } + }); + }); + it(`GET ${self.options.baseUrl} return a partial page of preloaded objects with pagination data`, function (done) { + request(app) + .get( + `${self.options.baseUrl}?offset=40&limit=20&includePagination=true${self.options.stateQuery}`, + ) + .set('Accept', 'application/json') + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/) + .end(function (err, res) { + if (err) { + done(err); + } else { + // We expect to get an array with one page of test objects + const testObjects = res.body.data; + expect(testObjects).toBeDefined(); + expect(Array.isArray(testObjects)).toBe(true); + expect(testObjects.length).toBe(5); + + // We expect pagination data to be included + const pagination = res.body.pagination; + expect(pagination).toBeDefined(); + expect(pagination.total).toBe(45); + expect(pagination.limit).toBe(20); + expect(pagination.offset).toBe(40); + done(); + } + }); + }); + after(async function () { + await database.closeConnection(); + }); + }); +}; diff --git a/bin/www b/bin/www index 7d92398c..c90edb78 100644 --- a/bin/www +++ b/bin/www @@ -1,125 +1,122 @@ #!/usr/bin/env node const errors = { - noDatabaseConnection: 'No database connection', - databaseMigrationFailed: 'Database migration failed' + noDatabaseConnection: 'No database connection', + databaseMigrationFailed: 'Database migration failed', }; async function runServer() { - // Configure the logger - const logger = require('../app/lib/logger'); - - const os = require('os'); - logger.info('** hostname = ' + os.hostname()); - logger.info('** type = ' + os.type()); - logger.info('** platform = ' + os.platform()); - logger.info('** arch = ' + os.arch()); - logger.info('** release = ' + os.release()); - logger.info('** uptime = ' + os.uptime()); - logger.info('** versions = ' + JSON.stringify(process.versions)); - - const config = require('../app/config/config'); - logger.info(`Log level set to ${ config.logging.logLevel }`); - - // Establish the database connection - logger.info('Setting up the database connection...'); - try { - await require('../app/lib/database-connection').initializeConnection(); - } - catch(err) { - logger.error('Unable to connect to database'); - logger.error(err.message); - logger.error('Database connection is required; terminating app'); - throw new Error(errors.noDatabaseConnection); + // Configure the logger + const logger = require('../app/lib/logger'); + + const os = require('os'); + logger.info('** hostname = ' + os.hostname()); + logger.info('** type = ' + os.type()); + logger.info('** platform = ' + os.platform()); + logger.info('** arch = ' + os.arch()); + logger.info('** release = ' + os.release()); + logger.info('** uptime = ' + os.uptime()); + logger.info('** versions = ' + JSON.stringify(process.versions)); + + const config = require('../app/config/config'); + logger.info(`Log level set to ${config.logging.logLevel}`); + + // Establish the database connection + logger.info('Setting up the database connection...'); + try { + await require('../app/lib/database-connection').initializeConnection(); + } catch (err) { + logger.error('Unable to connect to database'); + logger.error(err.message); + logger.error('Database connection is required; terminating app'); + throw new Error(errors.noDatabaseConnection); + } + + // Apply any database migration actions required + const migrateDatabase = require('../app/lib/migration/migrate-database'); + try { + await migrateDatabase.migrateDatabase(); + } catch (err) { + logger.error('Unable to perform database migration'); + logger.error(err.message); + logger.error('Database migration is required; terminating app'); + throw new Error(errors.databaseMigrationFailed); + } + + // Check for valid database configuration + const databaseConfiguration = require('../app/lib/database-configuration'); + await databaseConfiguration.checkSystemConfiguration(); + + // Create the app + const app = await require('../app').initializeApp(); + + // Create the scheduler + const scheduler = require('../app/scheduler/scheduler'); + scheduler.initializeScheduler(); + + // Start the server + logger.info('Starting the HTTP server...'); + const server = app.listen(config.server.port, function () { + const host = server.address().address; + const port = server.address().port; + + logger.info(`Listening at http://${host}:${port}`); + logger.info('ATT&CK Workbench REST API start up complete'); + }); + + server.on('error', function (err) { + if (err.code === 'EADDRINUSE') { + logger.error('Unable to start the HTTP server'); + logger.error(err.message); + logger.error('HTTP server is required; terminating app'); + // Don't need to throw error, this error is automatically raised to the calling function } + }); - // Apply any database migration actions required - const migrateDatabase = require('../app/lib/migration/migrate-database'); - try { - await migrateDatabase.migrateDatabase(); - } - catch (err) { - logger.error('Unable to perform database migration'); - logger.error(err.message); - logger.error('Database migration is required; terminating app'); - throw new Error(errors.databaseMigrationFailed); - } + // Listen for a ctrl-c + process.on('SIGINT', () => { + logger.info('SIGINT received, stopping HTTP server'); + server.close(); + }); + + // Docker terminates a container with a SIGTERM + process.on('SIGTERM', () => { + logger.info('SIGTERM received, stopping HTTP server'); + server.close(); + }); + + process.on('uncaughtException', (err, origin) => { + logger.error(`Uncaught exception: ${err}`); + logger.error(`Exception origin: ${origin}`); + logger.error(err.stack); - // Check for valid database configuration - const databaseConfiguration = require('../app/lib/database-configuration'); - await databaseConfiguration.checkSystemConfiguration(); - - // Create the app - const app = await require('../app').initializeApp(); - - // Create the scheduler - const scheduler = require('../app/scheduler/scheduler'); - scheduler.initializeScheduler(); - - // Start the server - logger.info('Starting the HTTP server...'); - const server = app.listen(config.server.port, function () { - const host = server.address().address; - const port = server.address().port; - - logger.info(`Listening at http://${host}:${port}`); - logger.info('ATT&CK Workbench REST API start up complete'); - }); - - server.on('error', function(err) { - if (err.code === 'EADDRINUSE') { - logger.error('Unable to start the HTTP server'); - logger.error(err.message); - logger.error('HTTP server is required; terminating app'); - // Don't need to throw error, this error is automatically raised to the calling function - } - }); - - // Listen for a ctrl-c - process.on('SIGINT', () => { - logger.info('SIGINT received, stopping HTTP server'); - server.close(); - }); - - // Docker terminates a container with a SIGTERM - process.on('SIGTERM', () => { - logger.info('SIGTERM received, stopping HTTP server'); - server.close(); - }); - - process.on('uncaughtException', (err, origin) => { - logger.error(`Uncaught exception: ${ err }`); - logger.error(`Exception origin: ${ origin }`); - logger.error(err.stack); - - logger.error('Terminating app after uncaught exception'); - - process.exit(1); - }); - - // Wait for the server to close - const events = require('events'); - await events.once(server, 'close'); - - logger.info('ATT&CK Workbench REST API terminating'); + logger.error('Terminating app after uncaught exception'); + + process.exit(1); + }); + + // Wait for the server to close + const events = require('events'); + await events.once(server, 'close'); + + logger.info('ATT&CK Workbench REST API terminating'); } runServer() - .then(() => { - console.log('runServer() - Terminating normally'); - process.exit(); - }) - .catch(err => { - if (err.code === 'EADDRINUSE' || Object.values(errors).includes(err.message)) { - // Trap the explicitly handled errors - console.error('runServer() - Terminating after error'); - } - else { - // Trap and log any other error - console.error('runServer() - Terminating after unexpected error:'); - console.error(err.message); - console.error(err.stack); - } - - process.exit(1); - }); + .then(() => { + console.log('runServer() - Terminating normally'); + process.exit(); + }) + .catch((err) => { + if (err.code === 'EADDRINUSE' || Object.values(errors).includes(err.message)) { + // Trap the explicitly handled errors + console.error('runServer() - Terminating after error'); + } else { + // Trap and log any other error + console.error('runServer() - Terminating after unexpected error:'); + console.error(err.message); + console.error(err.stack); + } + + process.exit(1); + }); diff --git a/docs/authentication.md b/docs/authentication.md index 4e49a797..95953d60 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -5,6 +5,7 @@ Please refer to [doc ref goes here] for instructions on configuring authenticati The ATT&CK Workbench REST API can be configured to use one of the implemented user authentication mechanisms. The currently implemented user authentication mechanisms are: + - Anonymous - OpenID Connect (OIDC) @@ -23,6 +24,7 @@ The REST API uses the passport module for authentication which will facilitate t #### General Endpoints ##### Get Config + ``` GET /api/config/authn ``` @@ -32,14 +34,15 @@ This is intended to be used by the client to determine which authentication mech Authentication Config Object: -| Property | Type | Description | -|----------------|------------|--------------------------------------------------------------------------------| +| Property | Type | Description | +| -------------- | ---------- | ------------------------------------------------------------------------------- | | **mechanisms** | [ string ] | Configured user authentication mechanisms (allowed values: `oidc`, `anonymous`) | Note: The current release of the ATT&CK Workbench REST API only allows one user authentication mechanism to be configured at a time. Multiple simultaneous mechanisms may be supported in a future release. ##### Get Session + ``` GET /api/session ``` @@ -48,8 +51,8 @@ Retrieves the current user session object for a logged in user. If the user is n User Session Object: -| Property | Type | Description | -|-------------------|---------|-------------------------------------------------| +| Property | Type | Description | +| ----------------- | ------- | ----------------------------------------------- | | **strategy** | string | authentication strategy used | | **userAccountId** | string | STIX identity assigned to this user | | **email** | string | email address | @@ -61,13 +64,13 @@ User Session Object: A user who is in the process of registering and has logged in but has not been added to the database - #### Anonymous Endpoints Anonymous authentication is primarily intended to be used when the ATT&CK Workbench is deployed on a machine for local use by a single user. It does not provide any authentication or authorization of access to the system, and does not provide attribution of changes to individual users. ##### Log In + ``` GET /api/authn/anonymous/login ``` @@ -75,6 +78,7 @@ GET /api/authn/anonymous/login Logs the user into the REST API. Does not require credentials. ##### Log Out + ``` GET /api/authn/anonymous/logout ``` @@ -86,6 +90,7 @@ Logs the user out of the REST API. OIDC authentication is intended for use in an organizational setting and can be tied into the organization's single-sign on configuration. ##### Log In + ``` GET /api/authn/oidc/login?destination= ``` @@ -98,6 +103,7 @@ Therefore, the call to this endpoint must be a standard HTTP request (not an XHR The `destination` query string parameter provides a URL that the client will be redirected to after a successful login. ##### OIDC Callback + ``` GET /api/authn/oidc/callback ``` @@ -109,6 +115,7 @@ This endpoint will respond with a redirect to the `destination` provided in the In most cases this will be the start page of the client application which should verify the login by requesting the current user session object. ##### Log Out + ``` GET /api/authn/oidc/logout ``` @@ -119,14 +126,16 @@ It does not log the user out of the OIDC Identity Provider. ### User Authentication Workflow 1. The client starts by calling `GET /api/session` - * If logged in, will receive the user session object - * If not logged in, will receive 401 Not Authorized + + - If logged in, will receive the user session object + - If not logged in, will receive 401 Not Authorized 2. To log in, the client will first call `GET /api/config/authn` to get the authentication config object 3. After getting the authentication config object - * If the supported authentication is anonymous, call `GET /api/authn/anonymous/login` - * If the supported authentication is oidc, navigate to `GET /api/authn/oidc/login` + + - If the supported authentication is anonymous, call `GET /api/authn/anonymous/login` + - If the supported authentication is oidc, navigate to `GET /api/authn/oidc/login` 4. After logging in, call `GET /api/session` to get the user session object @@ -157,12 +166,14 @@ The JWT must then be provided in subsequent requests. The use of a JWT allows for a login session to expire, forcing the service to periodically obtain a new token. The service obtains the access token through a challenge-response protocol: + 1. The service starts by sending a request to the REST API challenge endpoint. It will receive a nonce (a base-64 encoded string generated using the Node crypto module) in the response. 2. The service must then create a SHA256 hash of the nonce using its configured API key. 3. The service then sends the hash to the REST API token endpoint. It will receive a JWT in the response. 4. The service must include the JWT in requests when accessing resource endpoints. #### Request Challenge Endpoint + ``` GET /api/authn/service/apikey-challenge?serviceName=MyServiceName ``` @@ -171,13 +182,15 @@ Requests a challenge string from the REST API. The request must include the serv The response will include the challenge that can be used when requesting an access token. Sample response: + ```json { - "challenge": "PH2ev0gz+DEUVMbB9d8jT8uowru5Qp495yAHqASi1axVxywVZ4/GxTnuPlpfayJ+" + "challenge": "PH2ev0gz+DEUVMbB9d8jT8uowru5Qp495yAHqASi1axVxywVZ4/GxTnuPlpfayJ+" } ``` #### Request Token endpoint + ``` GET /api/authn/service/apikey-token?serviceName=MyServiceName Authorization: Apikey 1092d306081afd94d405b15887312373066bc96c222b04a03cfef436a0b0ecaa @@ -189,9 +202,10 @@ The hash must be a SHA256 hash of the challenge using the shared api key value. The response will include the JWT. Sample response: + ```json { - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXJ2aWNlTmFtZSI6ImFwaWtleS10ZXN0LXNlcnZpY2UiLCJleHAiOjE2Mzk2MDQ5NDEsImlhdCI6MTYzOTYwNDY0MX0.QHPTHMzceeONvMdPpr2h6tCBwrpkpGydOV6i0DUhNMw" + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXJ2aWNlTmFtZSI6ImFwaWtleS10ZXN0LXNlcnZpY2UiLCJleHAiOjE2Mzk2MDQ5NDEsImlhdCI6MTYzOTYwNDY0MX0.QHPTHMzceeONvMdPpr2h6tCBwrpkpGydOV6i0DUhNMw" } ``` @@ -203,8 +217,8 @@ The service name and API key are used as the userid and password. This method is more vulnerable than other methods but is simpler to implement in a client service. If this method is used, the following configuration steps are recommended: - * Because the API key is passed in each request, the server should be configured with HTTPS. - * Services that are configured to use this method should be configured with a role that allows access to a limited set of endpoints. +- Because the API key is passed in each request, the server should be configured with HTTPS. +- Services that are configured to use this method should be configured with a role that allows access to a limited set of endpoints. ### OIDC Client Credentials Flow Authentication @@ -228,16 +242,19 @@ When using the API Key Challenge or OIDC Client Credentials Flow authentication The JWT must be provided using the `Authorization` header with the `Bearer` authentication scheme: Sample request + ``` GET /api/techniques Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXJ2aWNlTmFtZSI6ImFwaWtleS10ZXN0LXNlcnZpY2UiLCJleHAiOjE2Mzk2MDQ5NDEsImlhdCI6MTYzOTYwNDY0MX0.QHPTHMzceeONvMdPpr2h6tCBwrpkpGydOV6i0DUhNMw ``` #### API Key Basic Authentication + When using the API Key Basic authentication method, the API Key must be provided on all REST API calls in order to authenticate the service. The API Key must be provided using the `Authorization` header with the `Basic` authentication scheme. The service name and API key must be base-64 encoded. Sample request + ``` GET /api/stix-bundles?domain=mobile-attack Authorization: Basic YXBpa2V5LXRlc3Qtc2VydmljZTp4eXp6eQ== diff --git a/docs/data-model.md b/docs/data-model.md index 72c128e5..a1f61d9d 100644 --- a/docs/data-model.md +++ b/docs/data-model.md @@ -2,6 +2,7 @@ The ATT&CK Workbench database supports the following ATT&CK object types (with the corresponding STIX types where applicable): + - Collection (x-mitre-collection) - Matrix (x-mitre-matrix) - Technique (attack-pattern) @@ -29,7 +30,7 @@ Objects in the `attackObjects` collection follow a consistent pattern, though each type has a number of properties that are specific to that type. The `stix` property holds a STIX formatted object and contains the data that is eligible to be exported. -The `workspace` property holds the data that is used within the workspace to manage the object. +The `workspace` property holds the data that is used within the workspace to manage the object. ### Unique Identifiers @@ -56,6 +57,7 @@ objects stored in the `attackObjects` collection. ### Sample Technique A technique is stored in the database in the following format: + ```json { "_id": ObjectId("5fb1b9d8e1af0600177092ec"), @@ -68,7 +70,7 @@ A technique is stored in the database in the following format: "modified": "2020-10-08T17:36:01.675Z", "created_by_ref": "identity--c78cb6e5-0c4b-4611-8297-d1b8b55e40b5", "description": "Adversaries may attempt to find group and permission settings. This information can help adversaries determine which user accounts and groups are available, the membership of users in particular groups, and which users and groups have elevated permissions.", - "spec_version": "2.1", + "spec_version": "2.1", "external_references": [ { "source_name": "mitre-attack", diff --git a/docs/docker.md b/docs/docker.md index f8cfbe00..69442deb 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -1,30 +1,39 @@ # Standalone Docker Installation + The ATT&CK Workbench Frontend project includes a `docker-compose.yml` file that can be used to install all of the ATT&CK Workbench components as part of a Docker Compose installation. -This document describes an alternate method of installing the ATT&CK Workbench REST API service in a Docker container without using Docker Compose. +This document describes an alternate method of installing the ATT&CK Workbench REST API service in a Docker container without using Docker Compose. ## Create a Docker Image for the REST API + Creating the Docker image builds the REST API service, including downloading all dependencies. -This step requires access to a Docker registry, either Docker Hub or a privately hosted registry. +This step requires access to a Docker registry, either Docker Hub or a privately hosted registry. + ```shell docker build --tag attack-workbench/rest-api . ``` ## Create the Docker Network + The REST API needs to be able to communicate with the MongoDB service, and needs to be reachable by the ATT&CK Workbench Frontend application. Create a Docker network to support this network communication. This only needs to be done once. + ```shell docker network create attack-workbench-network ``` ## Create and Run a Docker Container for MongoDB + The REST API requires access to an instance of MongoDB. + ```shell docker run --name attack-workbench-mongodb -d --network attack-workbench-network mongo:latest ``` ## Create and Run a Docker Container for the REST API + This command will run the REST API by creating a Docker container and starting it. + ```shell docker run -p 3000:3000 -d \ --name attack-workbench-rest-api \ @@ -32,11 +41,14 @@ docker run -p 3000:3000 -d \ --network attack-workbench-network \ attack-workbench/rest-api ``` + #### REST API Port + The REST API service listens on container port 3000 by default. This command maps port 3000 (in the container) to port 3000 (on the host), making the api accessible from the host. #### MongoDB + This command sets the `DATABASE_URL` environment variable to configure the REST API service to access the MongoDB instance running on the `attack-workbench-mongodb` host on the default port of `27017`. ## Configuring the REST API @@ -59,6 +71,7 @@ For example, this configuration file disables CORS and provides the URL for the ``` If this file is located on the host system at `~/attack-workbench-settings/aw-prod.json`, you can use the following command to run the REST API in a Docker container with that configuration file. + ```shell docker run -p 3000:3000 -d \ --name attack-workbench-rest-api \ @@ -67,5 +80,3 @@ docker run -p 3000:3000 -d \ --network attack-workbench-network \ attack-workbench/rest-api ``` - - diff --git a/docs/link-by-id.md b/docs/link-by-id.md index 84de9d32..066b6baa 100644 --- a/docs/link-by-id.md +++ b/docs/link-by-id.md @@ -1,26 +1,25 @@ - ## Object Format in the Database Object containing the LinkById to another object: ```js { - stix: { - name: 'Initial Object' - description: 'This is a reference to another object (LinkById: S0565).' - external_references: [ - { - source_name: 'mitre-attack', - url: 'https://attack.mitre.org/techniques/T9901', - external_id: 'T9901' - }, - { - source_name: 'S0565', - url: 'https://attack.mitre.org/software/S0565', - description: 'Referenced Object' - } - ] - } + stix: { + name: 'Initial Object'; + description: 'This is a reference to another object (LinkById: S0565).'; + external_references: [ + { + source_name: 'mitre-attack', + url: 'https://attack.mitre.org/techniques/T9901', + external_id: 'T9901', + }, + { + source_name: 'S0565', + url: 'https://attack.mitre.org/software/S0565', + description: 'Referenced Object', + }, + ]; + } } ``` @@ -28,17 +27,17 @@ The object referenced by the first object: ```js { - stix: { - name: 'Referenced Object' - description: 'This is another object.' - external_references: [ - { - source_name: 'mitre-attack', - url: 'https://attack.mitre.org/software/S0565', - external_id: 'S0565' - } - ] - } + stix: { + name: 'Referenced Object'; + description: 'This is another object.'; + external_references: [ + { + source_name: 'mitre-attack', + url: 'https://attack.mitre.org/software/S0565', + external_id: 'S0565', + }, + ]; + } } ``` @@ -50,29 +49,30 @@ External references added to support the LinkById must NOT be added to the full The object containing the LinkById to another object will be modified when exported. These fields will be checked for a LinkById marker: + - description - ... (tbd) -Each occurrence of "(LinkById: _ref_)" will be replaced by a link to the referenced object using the format `[extref.description](extref.url)`, using the external_reference element where the `extref.source_name` matches the _ref_ in the LinkById. +Each occurrence of "(LinkById: _ref_)" will be replaced by a link to the referenced object using the format `[extref.description](extref.url)`, using the external*reference element where the `extref.source_name` matches the \_ref* in the LinkById. ```js { - stix: { - name: 'Initial Object' - description: 'This is a reference to another object [Referenced Object](https://attack.mitre.org/software/S0565).' - external_references: [ - { - source_name: 'mitre-attack', - url: 'https://attack.mitre.org/techniques/T9901', - external_id: 'T9901' - }, - { - source_name: 'S0565', - url: 'https://attack.mitre.org/software/S0565', - description: 'Referenced Object' - } - ] - } + stix: { + name: 'Initial Object'; + description: 'This is a reference to another object [Referenced Object](https://attack.mitre.org/software/S0565).'; + external_references: [ + { + source_name: 'mitre-attack', + url: 'https://attack.mitre.org/techniques/T9901', + external_id: 'T9901', + }, + { + source_name: 'S0565', + url: 'https://attack.mitre.org/software/S0565', + description: 'Referenced Object', + }, + ]; + } } ``` diff --git a/docs/user-management.md b/docs/user-management.md index 67ac0569..c3337f20 100644 --- a/docs/user-management.md +++ b/docs/user-management.md @@ -9,19 +9,19 @@ Each registered user has an associated user account document in the database. The User Account document has the following properties: -| property | type | description | -|-----------------|---------|------------------------------------------------------------------------------------------| -| **id** | string | Unique id for this user, assigned when the user account is created | -| **email** | string | User's email address | -| **username** | string | Name used to authenticate with the identity provider (commonly the user's email address) | -| **displayName** | string | User's display name | -| **status** | string | `pending`, `active`, or `inactive` | -| **role** | string | `visitor`, `editor`, `admin`, or undefined | +| property | type | description | +| --------------- | ------ | ---------------------------------------------------------------------------------------- | +| **id** | string | Unique id for this user, assigned when the user account is created | +| **email** | string | User's email address | +| **username** | string | Name used to authenticate with the identity provider (commonly the user's email address) | +| **displayName** | string | User's display name | +| **status** | string | `pending`, `active`, or `inactive` | +| **role** | string | `visitor`, `editor`, `admin`, or undefined | ## User Status | status | description | -|------------|--------------------------------------------------------------------------| +| ---------- | ------------------------------------------------------------------------ | | `pending` | The user has registered with the workspace and is waiting to be approved | | `active` | The user has been registered and approved | | `inactive` | The user is no longer active | @@ -33,10 +33,11 @@ The User Account document has the following properties: 3. Later, a user with the `admin` role marks the user as inactive. This results in the user's status changing to `inactive`. ## Roles + The ATT&CK Workbench supports the following roles: | role | description | -|-----------|-------------------------------------------------------------------------------------| +| --------- | ----------------------------------------------------------------------------------- | | `none` | No access to the system allowed. | | `visitor` | Read access to all of the ATT&CK objects in the workspace. | | `editor` | Read and write access to all of the ATT&CK objects in the workspace, except for ??? | @@ -53,13 +54,13 @@ For users who are registered and active, their effective role will always be the Some organizations may want to allow access to the system for users who aren't registered and active. The effective roles for these users is as specified in the following table: -| authentication | user registered and logged in? | user status | effective role | default | -|----------------|--------------------------------|-------------|-----------------|---------| -| anonymous | no | -- | `admin` | -- | -| OIDC | no | -- | configurable | `none` | -| OIDC | yes | pending | configurable | `none` | -| OIDC | yes | active | as assigned | -- | -| OIDC | yes | inactive | configurable | `none` | +| authentication | user registered and logged in? | user status | effective role | default | +| -------------- | ------------------------------ | ----------- | -------------- | ------- | +| anonymous | no | -- | `admin` | -- | +| OIDC | no | -- | configurable | `none` | +| OIDC | yes | pending | configurable | `none` | +| OIDC | yes | active | as assigned | -- | +| OIDC | yes | inactive | configurable | `none` | Note that the default OIDC configuration only allows registered and active users to access the system. The system must be specifically configured to allow other users access. @@ -71,6 +72,7 @@ These endpoints are disabled if the app is configured to use the anonymous authe The STIX ID of the corresponding identity object is used by the user management endpoints as the unique identifier for a user. ##### Get Users + ``` GET /api/user-accounts ``` @@ -84,6 +86,7 @@ Query string parameters for searching are TBD. This endpoint will only be available to users with the `admin` role. ##### Get User + ``` GET /api/user-accounts/:id ``` @@ -95,6 +98,7 @@ Retrieve a user account document by its id. This endpoint will only be available to users with the `admin` role or for a logged in user with the matching user account `id`. ##### Register User + ``` POST /api/user-accounts/register ``` @@ -109,11 +113,12 @@ The user document will have the `email` and `username` properties set based on t This endpoint will only be available for a logged in user who is in the process of registering. ##### Update User + ``` PUT /api/user-accounts/:id ``` -Update an existing user document in the database. +Update an existing user document in the database. ###### Authorization diff --git a/migrations/20220705205741-move-relationships.js b/migrations/20220705205741-move-relationships.js index 1c198ef9..6a218ee8 100644 --- a/migrations/20220705205741-move-relationships.js +++ b/migrations/20220705205741-move-relationships.js @@ -1,58 +1,80 @@ 'use strict'; module.exports = { - async up(db, client) { - const attackObjectsCollection = await db.collection('attackObjects'); - const relationshipsCollection = await db.collection('relationships'); - - // Move relationship objects from the attackObjects collection to the relationships collection - const oldRelationships = await attackObjectsCollection.find({ '__t': 'RelationshipModel' }).toArray(); - console.log(`Found ${ oldRelationships.length } relationships in the attackObjects collection`); - - for (const relationship of oldRelationships) { - // The relationship should not exist in the relationships collection - const newRelationship = await relationshipsCollection.findOne({ 'stix.id': relationship.stix.id, 'stix.modified': relationship.stix.modified }); - if (newRelationship) { - console.log('Relationship already exists in relationships collection. Will delete from attackObjects collection without adding to relationships collection.'); - attackObjectsCollection.findOneAndDelete({ 'stix.id': relationship.stix.id, 'stix.modified': relationship.stix.modified }); - } - else { -// console.log(`Moving relationships ${ relationship.stix.id }/${ relationship.stix.modified }`); - // Add the relationship to the relationships collection - relationship._id = undefined; - relationship.__t = undefined; - relationshipsCollection.insertOne(relationship); - - // Remove the relationship from the attackObjects collection - attackObjectsCollection.findOneAndDelete({ 'stix.id': relationship.stix.id, 'stix.modified': relationship.stix.modified }); - } - } - }, - - async down(db, client) { - const attackObjectsCollection = await db.collection('attackObjects'); - const relationshipsCollection = await db.collection('relationships'); - - // Move relationship objects from the relationship collection to the attackObjects collection - const newRelationships = await relationshipsCollection.find({ }).toArray(); - console.log(`Found ${ newRelationships.length } relationships in the relationships collection`); - - for (const relationship of newRelationships) { - // The relationship should not exist in the attackObjects collection - const oldRelationship = await attackObjectsCollection.findOne({ 'stix.id': relationship.stix.id, 'stix.modified': relationship.stix.modified }); - if (oldRelationship) { - console.log('Relationship already exists in attackObjects collection. Will delete from attackObjects collection without adding to attackObjects collection.'); - relationshipsCollection.findOneAndDelete({ 'stix.id': relationship.stix.id, 'stix.modified': relationship.stix.modified }); - } - else { - // Add the relationship to the attackObjects collection - relationship._id = undefined; - relationship.__t = 'RelationshipModel'; - attackObjectsCollection.insertOne(relationship); - - // Remove the relationship from the relationships collection - relationshipsCollection.findOneAndDelete({ 'stix.id': relationship.stix.id, 'stix.modified': relationship.stix.modified }); - } - } + async up(db, client) { + const attackObjectsCollection = await db.collection('attackObjects'); + const relationshipsCollection = await db.collection('relationships'); + + // Move relationship objects from the attackObjects collection to the relationships collection + const oldRelationships = await attackObjectsCollection + .find({ __t: 'RelationshipModel' }) + .toArray(); + console.log(`Found ${oldRelationships.length} relationships in the attackObjects collection`); + + for (const relationship of oldRelationships) { + // The relationship should not exist in the relationships collection + const newRelationship = await relationshipsCollection.findOne({ + 'stix.id': relationship.stix.id, + 'stix.modified': relationship.stix.modified, + }); + if (newRelationship) { + console.log( + 'Relationship already exists in relationships collection. Will delete from attackObjects collection without adding to relationships collection.', + ); + attackObjectsCollection.findOneAndDelete({ + 'stix.id': relationship.stix.id, + 'stix.modified': relationship.stix.modified, + }); + } else { + // console.log(`Moving relationships ${ relationship.stix.id }/${ relationship.stix.modified }`); + // Add the relationship to the relationships collection + relationship._id = undefined; + relationship.__t = undefined; + relationshipsCollection.insertOne(relationship); + + // Remove the relationship from the attackObjects collection + attackObjectsCollection.findOneAndDelete({ + 'stix.id': relationship.stix.id, + 'stix.modified': relationship.stix.modified, + }); + } + } + }, + + async down(db, client) { + const attackObjectsCollection = await db.collection('attackObjects'); + const relationshipsCollection = await db.collection('relationships'); + + // Move relationship objects from the relationship collection to the attackObjects collection + const newRelationships = await relationshipsCollection.find({}).toArray(); + console.log(`Found ${newRelationships.length} relationships in the relationships collection`); + + for (const relationship of newRelationships) { + // The relationship should not exist in the attackObjects collection + const oldRelationship = await attackObjectsCollection.findOne({ + 'stix.id': relationship.stix.id, + 'stix.modified': relationship.stix.modified, + }); + if (oldRelationship) { + console.log( + 'Relationship already exists in attackObjects collection. Will delete from attackObjects collection without adding to attackObjects collection.', + ); + relationshipsCollection.findOneAndDelete({ + 'stix.id': relationship.stix.id, + 'stix.modified': relationship.stix.modified, + }); + } else { + // Add the relationship to the attackObjects collection + relationship._id = undefined; + relationship.__t = 'RelationshipModel'; + attackObjectsCollection.insertOne(relationship); + + // Remove the relationship from the relationships collection + relationshipsCollection.findOneAndDelete({ + 'stix.id': relationship.stix.id, + 'stix.modified': relationship.stix.modified, + }); + } } + }, }; diff --git a/migrations/sample-migration.js b/migrations/sample-migration.js index 7fe309dc..3fca747c 100644 --- a/migrations/sample-migration.js +++ b/migrations/sample-migration.js @@ -1,16 +1,16 @@ 'use strict'; module.exports = { - async up(db, client) { - // TODO write your migration here. - // See https://github.com/seppevs/migrate-mongo/#creating-a-new-migration-script - // Example: - // await db.collection('albums').updateOne({artist: 'The Beatles'}, {$set: {blacklisted: true}}); - }, + async up(db, client) { + // TODO write your migration here. + // See https://github.com/seppevs/migrate-mongo/#creating-a-new-migration-script + // Example: + // await db.collection('albums').updateOne({artist: 'The Beatles'}, {$set: {blacklisted: true}}); + }, - async down(db, client) { - // TODO write the statements to rollback your migration (if possible) - // Example: - // await db.collection('albums').updateOne({artist: 'The Beatles'}, {$set: {blacklisted: false}}); - } + async down(db, client) { + // TODO write the statements to rollback your migration (if possible) + // Example: + // await db.collection('albums').updateOne({artist: 'The Beatles'}, {$set: {blacklisted: false}}); + }, }; diff --git a/resources/postman-exports/README.md b/resources/postman-exports/README.md index 62558938..6094650d 100644 --- a/resources/postman-exports/README.md +++ b/resources/postman-exports/README.md @@ -1,4 +1,5 @@ This directory holds collections and environments exported from the Postman application. + - The collections contain sample REST API requests that can be used during software development. - The environments contain variables that can be used with the sample requests to simplify configuring the requests. diff --git a/resources/postman-exports/attack-workbench-rest-api.postman_collection.json b/resources/postman-exports/attack-workbench-rest-api.postman_collection.json index a2ae849b..404a8db5 100644 --- a/resources/postman-exports/attack-workbench-rest-api.postman_collection.json +++ b/resources/postman-exports/attack-workbench-rest-api.postman_collection.json @@ -1,272 +1,228 @@ { - "info": { - "_postman_id": "e364a961-d0e0-41bd-9ca7-1619b7383dc2", - "name": "attack-workbench-rest-api", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" - }, - "item": [ - { - "name": "sessions", - "item": [ - { - "name": "Retrieve Current Session", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/api/session", - "host": [ - "{{base_url}}" - ], - "path": [ - "api", - "session" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "authn", - "item": [ - { - "name": "anonymous", - "item": [ - { - "name": "Log In", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/api/authn/anonymous/login", - "host": [ - "{{base_url}}" - ], - "path": [ - "api", - "authn", - "anonymous", - "login" - ] - } - }, - "response": [] - }, - { - "name": "Log Out", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/api/authn/anonymous/logout", - "host": [ - "{{base_url}}" - ], - "path": [ - "api", - "authn", - "anonymous", - "logout" - ] - } - }, - "response": [] - } - ] - }, - { - "name": "apikey-bearer", - "item": [ - { - "name": "Challenge", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "if (pm.response.code === 200) {", - " var jsonData = JSON.parse(responseBody);", - "", - " var challenge = jsonData.challenge;", - " postman.setEnvironmentVariable(\"challenge\", challenge);", - "", - " var apikey = pm.variables.get(\"apikey\");", - " var challengeHash = CryptoJS.HmacSHA256(challenge, apikey).toString();", - " postman.setEnvironmentVariable(\"challenge_hash\", challengeHash);", - "}", - "else {", - " postman.setEnvironmentVariable(\"challenge\", null);", - " postman.setEnvironmentVariable(\"challenge_hash\", null);", - "}" - ], - "type": "text/javascript" - } - } - ], - "protocolProfileBehavior": { - "followRedirects": false - }, - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/api/authn/service/apikey-challenge?serviceName={{apikey_service_name}}", - "host": [ - "{{base_url}}" - ], - "path": [ - "api", - "authn", - "service", - "apikey-challenge" - ], - "query": [ - { - "key": "serviceName", - "value": "{{apikey_service_name}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Token", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "if (pm.response.code === 200) {", - " var jsonData = JSON.parse(responseBody);", - "", - " var accessToken = jsonData.access_token;", - " postman.setEnvironmentVariable(\"access_token\", accessToken);", - "}", - "else {", - " postman.setEnvironmentVariable(\"access_token\", null);", - "}" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "apikey", - "apikey": [ - { - "key": "value", - "value": "apikey {{challenge_hash}}", - "type": "string" - }, - { - "key": "key", - "value": "Authorization", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{base_url}}/api/authn/service/apikey-token?serviceName={{apikey_service_name}}", - "host": [ - "{{base_url}}" - ], - "path": [ - "api", - "authn", - "service", - "apikey-token" - ], - "query": [ - { - "key": "serviceName", - "value": "{{apikey_service_name}}" - } - ] - } - }, - "response": [] - }, - { - "name": "Session", - "request": { - "auth": { - "type": "bearer", - "bearer": [ - { - "key": "token", - "value": "{{access_token}}", - "type": "string" - } - ] - }, - "method": "GET", - "header": [ - { - "key": "Authorization", - "value": "Bearer {{access_token}}", - "type": "default", - "disabled": true - } - ], - "url": { - "raw": "{{base_url}}/api/session", - "host": [ - "{{base_url}}" - ], - "path": [ - "api", - "session" - ] - } - }, - "response": [] - } - ] - } - ] - } - ], - "event": [ - { - "listen": "prerequest", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - }, - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "" - ] - } - } - ], - "variable": [ - { - "key": "base_url", - "value": "http://localhost:3000" - } - ] -} \ No newline at end of file + "info": { + "_postman_id": "e364a961-d0e0-41bd-9ca7-1619b7383dc2", + "name": "attack-workbench-rest-api", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "sessions", + "item": [ + { + "name": "Retrieve Current Session", + "event": [ + { + "listen": "test", + "script": { + "exec": [""], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/session", + "host": ["{{base_url}}"], + "path": ["api", "session"] + } + }, + "response": [] + } + ] + }, + { + "name": "authn", + "item": [ + { + "name": "anonymous", + "item": [ + { + "name": "Log In", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/authn/anonymous/login", + "host": ["{{base_url}}"], + "path": ["api", "authn", "anonymous", "login"] + } + }, + "response": [] + }, + { + "name": "Log Out", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/authn/anonymous/logout", + "host": ["{{base_url}}"], + "path": ["api", "authn", "anonymous", "logout"] + } + }, + "response": [] + } + ] + }, + { + "name": "apikey-bearer", + "item": [ + { + "name": "Challenge", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.response.code === 200) {", + " var jsonData = JSON.parse(responseBody);", + "", + " var challenge = jsonData.challenge;", + " postman.setEnvironmentVariable(\"challenge\", challenge);", + "", + " var apikey = pm.variables.get(\"apikey\");", + " var challengeHash = CryptoJS.HmacSHA256(challenge, apikey).toString();", + " postman.setEnvironmentVariable(\"challenge_hash\", challengeHash);", + "}", + "else {", + " postman.setEnvironmentVariable(\"challenge\", null);", + " postman.setEnvironmentVariable(\"challenge_hash\", null);", + "}" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "followRedirects": false + }, + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/authn/service/apikey-challenge?serviceName={{apikey_service_name}}", + "host": ["{{base_url}}"], + "path": ["api", "authn", "service", "apikey-challenge"], + "query": [ + { + "key": "serviceName", + "value": "{{apikey_service_name}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Token", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "if (pm.response.code === 200) {", + " var jsonData = JSON.parse(responseBody);", + "", + " var accessToken = jsonData.access_token;", + " postman.setEnvironmentVariable(\"access_token\", accessToken);", + "}", + "else {", + " postman.setEnvironmentVariable(\"access_token\", null);", + "}" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "apikey {{challenge_hash}}", + "type": "string" + }, + { + "key": "key", + "value": "Authorization", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/api/authn/service/apikey-token?serviceName={{apikey_service_name}}", + "host": ["{{base_url}}"], + "path": ["api", "authn", "service", "apikey-token"], + "query": [ + { + "key": "serviceName", + "value": "{{apikey_service_name}}" + } + ] + } + }, + "response": [] + }, + { + "name": "Session", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "{{access_token}}", + "type": "string" + } + ] + }, + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{access_token}}", + "type": "default", + "disabled": true + } + ], + "url": { + "raw": "{{base_url}}/api/session", + "host": ["{{base_url}}"], + "path": ["api", "session"] + } + }, + "response": [] + } + ] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [""] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [""] + } + } + ], + "variable": [ + { + "key": "base_url", + "value": "http://localhost:3000" + } + ] +} diff --git a/resources/postman-exports/test-service-authentication.postman_environment.json b/resources/postman-exports/test-service-authentication.postman_environment.json index 757accb9..4d71456a 100644 --- a/resources/postman-exports/test-service-authentication.postman_environment.json +++ b/resources/postman-exports/test-service-authentication.postman_environment.json @@ -1,39 +1,39 @@ { - "id": "93316f8d-dc07-41ed-9096-814192ad3feb", - "name": "Test Service Authentication", - "values": [ - { - "key": "apikey_service_name", - "value": "apikey-test-service", - "type": "default", - "enabled": true - }, - { - "key": "apikey", - "value": "xyzzy", - "type": "default", - "enabled": true - }, - { - "key": "challenge", - "value": "", - "type": "default", - "enabled": true - }, - { - "key": "challenge_hash", - "value": "", - "type": "default", - "enabled": true - }, - { - "key": "access_token", - "value": "", - "type": "default", - "enabled": true - } - ], - "_postman_variable_scope": "environment", - "_postman_exported_at": "2022-01-21T22:08:11.930Z", - "_postman_exported_using": "Postman/9.9.3" -} \ No newline at end of file + "id": "93316f8d-dc07-41ed-9096-814192ad3feb", + "name": "Test Service Authentication", + "values": [ + { + "key": "apikey_service_name", + "value": "apikey-test-service", + "type": "default", + "enabled": true + }, + { + "key": "apikey", + "value": "xyzzy", + "type": "default", + "enabled": true + }, + { + "key": "challenge", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "challenge_hash", + "value": "", + "type": "default", + "enabled": true + }, + { + "key": "access_token", + "value": "", + "type": "default", + "enabled": true + } + ], + "_postman_variable_scope": "environment", + "_postman_exported_at": "2022-01-21T22:08:11.930Z", + "_postman_exported_using": "Postman/9.9.3" +} diff --git a/resources/sample-configurations/navigator-basic-apikey.json b/resources/sample-configurations/navigator-basic-apikey.json index 2336087f..01cf3d09 100644 --- a/resources/sample-configurations/navigator-basic-apikey.json +++ b/resources/sample-configurations/navigator-basic-apikey.json @@ -1,14 +1,14 @@ { - "serviceAuthn": { - "basicApikey": { - "enable": true, - "serviceAccounts": [ - { - "name": "navigator", - "apikey": "sample-navigator-apikey", - "serviceRole": "read-only" - } - ] + "serviceAuthn": { + "basicApikey": { + "enable": true, + "serviceAccounts": [ + { + "name": "navigator", + "apikey": "sample-navigator-apikey", + "serviceRole": "read-only" } + ] } -} \ No newline at end of file + } +} diff --git a/scripts/clearDatabase.js b/scripts/clearDatabase.js index 7d99f524..cc52b6c2 100644 --- a/scripts/clearDatabase.js +++ b/scripts/clearDatabase.js @@ -18,23 +18,23 @@ const Relationship = require('../app/models/relationship-model'); const Reference = require('../app/models/reference-model'); async function clearDatabase() { - // Establish the database connection - console.log('Setting up the database connection'); - await require('../app/lib/database-connection').initializeConnection(); + // Establish the database connection + console.log('Setting up the database connection'); + await require('../app/lib/database-connection').initializeConnection(); - let result = await AttackObject.deleteMany(); - console.log(`Deleted ${ result.deletedCount } objects from the attackObjects collection.`); + let result = await AttackObject.deleteMany(); + console.log(`Deleted ${result.deletedCount} objects from the attackObjects collection.`); - result = await Relationship.deleteMany(); - console.log(`Deleted ${ result.deletedCount } objects from the relationships collection.`); + result = await Relationship.deleteMany(); + console.log(`Deleted ${result.deletedCount} objects from the relationships collection.`); - result = await Reference.deleteMany(); - console.log(`Deleted ${ result.deletedCount } objects from the references collection.`); + result = await Reference.deleteMany(); + console.log(`Deleted ${result.deletedCount} objects from the references collection.`); } clearDatabase() - .then(() => process.exit()) - .catch(err => { - console.log('clearDatabase() - Error: ' + err); - process.exit(1); - }); + .then(() => process.exit()) + .catch((err) => { + console.log('clearDatabase() - Error: ' + err); + process.exit(1); + }); diff --git a/scripts/configureKeycloak.js b/scripts/configureKeycloak.js index cc333398..9578dc49 100644 --- a/scripts/configureKeycloak.js +++ b/scripts/configureKeycloak.js @@ -31,51 +31,86 @@ const oidcClientRedirectUrl = 'http://localhost:3000/api/authn/oidc/*'; const databaseUrl = process.env.DATABASE_URL || 'mongodb://localhost/attack-workspace'; async function configureKeycloak() { - // Initialize the Keycloak server - const options = { - basePath: oidcHost, - realmName: oidcRealm, - clientId: oidcClientId, - description: 'client', - standardFlowEnabled: true, - redirectUris: [ oidcClientRedirectUrl ], - clientSecret: oidcClientSecret - }; - await keycloak.initializeKeycloak(options); + // Initialize the Keycloak server + const options = { + basePath: oidcHost, + realmName: oidcRealm, + clientId: oidcClientId, + description: 'client', + standardFlowEnabled: true, + redirectUris: [oidcClientRedirectUrl], + clientSecret: oidcClientSecret, + }; + await keycloak.initializeKeycloak(options); - // Add the initial user accounts to Keycloak - const adminUser = { email: 'admin@test.com', username: 'admin@test.com', password: 'testuser', firstName: 'Admin', lastName: 'User' }; - const editorUser = { email: 'editor@test.com', username: 'editor@test.com', password: 'testuser', firstName: 'Editor', lastName: 'User' }; - const visitorUser = { email: 'visitor@test.com', username: 'visitor@test.com', password: 'testuser', firstName: 'Visitor', lastName: 'User' }; - const keycloakUsers = [ adminUser, editorUser, visitorUser ]; - await keycloak.addUsersToKeycloak(options, keycloakUsers); + // Add the initial user accounts to Keycloak + const adminUser = { + email: 'admin@test.com', + username: 'admin@test.com', + password: 'testuser', + firstName: 'Admin', + lastName: 'User', + }; + const editorUser = { + email: 'editor@test.com', + username: 'editor@test.com', + password: 'testuser', + firstName: 'Editor', + lastName: 'User', + }; + const visitorUser = { + email: 'visitor@test.com', + username: 'visitor@test.com', + password: 'testuser', + firstName: 'Visitor', + lastName: 'User', + }; + const keycloakUsers = [adminUser, editorUser, visitorUser]; + await keycloak.addUsersToKeycloak(options, keycloakUsers); - // Establish the database connection - console.log('Setting up the database connection'); - await require('../app/lib/database-connection').initializeConnection({ databaseUrl }); + // Establish the database connection + console.log('Setting up the database connection'); + await require('../app/lib/database-connection').initializeConnection({ databaseUrl }); - // Add the initial user accounts to the REST API - const adminWBUser = { email: adminUser.email, username: adminUser.username, displayName: `${ adminUser.firstName } ${ adminUser.lastName }`, status: 'active', role: 'admin' }; - const editorWBUser = { email: editorUser.email, username: editorUser.username, displayName: `${ editorUser.firstName } ${ editorUser.lastName }`, status: 'active', role: 'editor' }; - const visitorWBUser = { email: visitorUser.email, username: visitorUser.username, displayName: `${ visitorUser.firstName } ${ visitorUser.lastName }`, status: 'active', role: 'visitor' }; - const workbenchUsers = [ adminWBUser, editorWBUser, visitorWBUser ]; - for (const user of workbenchUsers) { - try { - // eslint-disable-next-line no-await-in-loop - await userAccountService.create(user); - console.log(`Added user ${ user.email } to the Workbench database`); - } - catch(err) { - console.error(`Unable to add user ${ user.email } to the Workbench database: ${ err }`); - } + // Add the initial user accounts to the REST API + const adminWBUser = { + email: adminUser.email, + username: adminUser.username, + displayName: `${adminUser.firstName} ${adminUser.lastName}`, + status: 'active', + role: 'admin', + }; + const editorWBUser = { + email: editorUser.email, + username: editorUser.username, + displayName: `${editorUser.firstName} ${editorUser.lastName}`, + status: 'active', + role: 'editor', + }; + const visitorWBUser = { + email: visitorUser.email, + username: visitorUser.username, + displayName: `${visitorUser.firstName} ${visitorUser.lastName}`, + status: 'active', + role: 'visitor', + }; + const workbenchUsers = [adminWBUser, editorWBUser, visitorWBUser]; + for (const user of workbenchUsers) { + try { + // eslint-disable-next-line no-await-in-loop + await userAccountService.create(user); + console.log(`Added user ${user.email} to the Workbench database`); + } catch (err) { + console.error(`Unable to add user ${user.email} to the Workbench database: ${err}`); } + } - console.log(`Keycloak configuration complete`); + console.log(`Keycloak configuration complete`); } configureKeycloak() - .then(() => process.exit()) - .catch(err => { - console.log('configureKeycloak() - Error: ' + err); - process.exit(1); - }); + .then(() => process.exit()) + .catch((err) => { + console.log('configureKeycloak() - Error: ' + err); + process.exit(1); + }); diff --git a/scripts/loadBundle.js b/scripts/loadBundle.js index 69d7b519..95caad03 100644 --- a/scripts/loadBundle.js +++ b/scripts/loadBundle.js @@ -15,63 +15,66 @@ 'use strict'; const collectionBundleService = require('../app/services/collection-bundles-service'); -const { promises: fs } = require("fs"); +const { promises: fs } = require('fs'); async function readJson(path) { - const filePath = require.resolve(path); - const data = await fs.readFile(filePath); - return JSON.parse(data); + const filePath = require.resolve(path); + const data = await fs.readFile(filePath); + return JSON.parse(data); } async function loadBundle() { - // Establish the database connection - console.log('Setting up the database connection'); - await require('../app/lib/database-connection').initializeConnection(); + // Establish the database connection + console.log('Setting up the database connection'); + await require('../app/lib/database-connection').initializeConnection(); - const filename = 'mobile-attack-10.0.json'; + const filename = 'mobile-attack-10.0.json'; - const collectionBundlesDirectory = '../app/tests/import/test-files'; - const filePath = collectionBundlesDirectory + '/' + filename; - const bundle = await readJson(filePath); + const collectionBundlesDirectory = '../app/tests/import/test-files'; + const filePath = collectionBundlesDirectory + '/' + filename; + const bundle = await readJson(filePath); - const options = {}; + const options = {}; - // Find the x-mitre-collection objects - const collections = bundle.objects.filter(object => object.type === 'x-mitre-collection'); + // Find the x-mitre-collection objects + const collections = bundle.objects.filter((object) => object.type === 'x-mitre-collection'); - // The bundle must have an x-mitre-collection object - if (collections.length === 0) { - console.warn("Unable to import collection bundle. Missing x-mitre-collection object."); - throw(new Error('Unable to import collection bundle. Missing x-mitre-collection object.')); - } - else if (collections.length > 1) { - console.warn("Unable to import collection bundle. More than one x-mitre-collection object."); - throw(new Error('Unable to import collection bundle. More than one x-mitre-collection object.')); - } + // The bundle must have an x-mitre-collection object + if (collections.length === 0) { + console.warn('Unable to import collection bundle. Missing x-mitre-collection object.'); + throw new Error('Unable to import collection bundle. Missing x-mitre-collection object.'); + } else if (collections.length > 1) { + console.warn('Unable to import collection bundle. More than one x-mitre-collection object.'); + throw new Error('Unable to import collection bundle. More than one x-mitre-collection object.'); + } - // The collection must have an id. - if (!collections[0].id) { - console.warn('Unable to import collection bundle. x-mitre-collection missing id'); - throw(new Error('Unable to import collection bundle. x-mitre-collection missing id')); - } + // The collection must have an id. + if (!collections[0].id) { + console.warn('Unable to import collection bundle. x-mitre-collection missing id'); + throw new Error('Unable to import collection bundle. x-mitre-collection missing id'); + } - console.log('Importing bundle into database...'); - return new Promise((resolve, reject) => { - collectionBundleService.importBundle(collections[0], bundle, options, (err, importedCollection) => { - if (err) { - reject(err); - } - else { - console.log('Bundle imported'); - resolve(); - } - }); - }); + console.log('Importing bundle into database...'); + return new Promise((resolve, reject) => { + collectionBundleService.importBundle( + collections[0], + bundle, + options, + (err, importedCollection) => { + if (err) { + reject(err); + } else { + console.log('Bundle imported'); + resolve(); + } + }, + ); + }); } loadBundle() - .then(() => process.exit()) - .catch(err => { - console.log('loadBundle() - Error: ' + err); - process.exit(1); - }); + .then(() => process.exit()) + .catch((err) => { + console.log('loadBundle() - Error: ' + err); + process.exit(1); + }); diff --git a/scripts/validateBundle.js b/scripts/validateBundle.js index 5746cebd..abd00881 100644 --- a/scripts/validateBundle.js +++ b/scripts/validateBundle.js @@ -13,50 +13,51 @@ 'use strict'; const collectionBundleService = require('../app/services/collection-bundles-service'); -const { promises: fs } = require("fs"); +const { promises: fs } = require('fs'); async function readJson(path) { - const filePath = require.resolve(path); - const data = await fs.readFile(filePath); - return JSON.parse(data); + const filePath = require.resolve(path); + const data = await fs.readFile(filePath); + return JSON.parse(data); } async function validateBundle() { - const filename = 'ics-attack-10.1.json'; - - const collectionBundlesDirectory = '../app/tests/import/test-files'; - const filePath = collectionBundlesDirectory + '/' + filename; - const bundle = await readJson(filePath); - - const options = {}; - - // Find the x-mitre-collection objects - const collections = bundle.objects.filter(object => object.type === 'x-mitre-collection'); - - // The bundle must have an x-mitre-collection object - if (collections.length === 0) { - console.warn("Unable to validate collection bundle. Missing x-mitre-collection object."); - throw(new Error('Unable to validate collection bundle. Missing x-mitre-collection object.')); - } - else if (collections.length > 1) { - console.warn("Unable to validate collection bundle. More than one x-mitre-collection object."); - throw(new Error('Unable to validate collection bundle. More than one x-mitre-collection object.')); - } - - // The collection must have an id. - if (!collections[0].id) { - console.warn('Unable to validate collection bundle. x-mitre-collection missing id'); - throw(new Error('Unable to validate collection bundle. x-mitre-collection missing id')); - } - - console.log('Validating bundle...'); - const validationResult = collectionBundleService.validateBundle(bundle, options); - console.log(JSON.stringify(validationResult, null, 2)); + const filename = 'ics-attack-10.1.json'; + + const collectionBundlesDirectory = '../app/tests/import/test-files'; + const filePath = collectionBundlesDirectory + '/' + filename; + const bundle = await readJson(filePath); + + const options = {}; + + // Find the x-mitre-collection objects + const collections = bundle.objects.filter((object) => object.type === 'x-mitre-collection'); + + // The bundle must have an x-mitre-collection object + if (collections.length === 0) { + console.warn('Unable to validate collection bundle. Missing x-mitre-collection object.'); + throw new Error('Unable to validate collection bundle. Missing x-mitre-collection object.'); + } else if (collections.length > 1) { + console.warn('Unable to validate collection bundle. More than one x-mitre-collection object.'); + throw new Error( + 'Unable to validate collection bundle. More than one x-mitre-collection object.', + ); + } + + // The collection must have an id. + if (!collections[0].id) { + console.warn('Unable to validate collection bundle. x-mitre-collection missing id'); + throw new Error('Unable to validate collection bundle. x-mitre-collection missing id'); + } + + console.log('Validating bundle...'); + const validationResult = collectionBundleService.validateBundle(bundle, options); + console.log(JSON.stringify(validationResult, null, 2)); } validateBundle() - .then(() => process.exit()) - .catch(err => { - console.log('validateBundle() - Error: ' + err); - process.exit(1); - }); + .then(() => process.exit()) + .catch((err) => { + console.log('validateBundle() - Error: ' + err); + process.exit(1); + }); From df4c70056b3df6cf4a84fb4346af059b9f096845 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Wed, 8 Jan 2025 15:26:03 -0500 Subject: [PATCH 14/26] fix: remove random artifact from past contributor --- ...factor-teams-service-for-improved-structure-and-asynchronicity | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 289-refactor-teams-service-for-improved-structure-and-asynchronicity diff --git a/289-refactor-teams-service-for-improved-structure-and-asynchronicity b/289-refactor-teams-service-for-improved-structure-and-asynchronicity deleted file mode 100644 index e69de29b..00000000 From 9d528756a5c75d0c87910b443298c36f663a0a2e Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Wed, 8 Jan 2025 15:33:59 -0500 Subject: [PATCH 15/26] feat: introduce magic string anti-pattern for object types This change consolidates object type declarations for Mongoose documents with 'type' properties. --- app/lib/types.js | 20 ++++ app/services/assets-service.js | 4 +- app/services/campaigns-service.js | 4 +- app/services/collection-bundles-service.js | 35 ++++--- app/services/data-components-service.js | 4 +- app/services/data-sources-service.js | 3 +- app/services/groups-service.js | 3 +- app/services/identities-service.js | 105 +------------------- app/services/marking-definitions-service.js | 4 +- app/services/matrices-service.js | 7 +- app/services/mitigations-service.js | 4 +- app/services/notes-service.js | 5 +- app/services/relationships-service.js | 8 +- app/services/software-service.js | 10 +- app/services/tactics-service.js | 4 +- app/services/techniques-service.js | 8 +- 16 files changed, 79 insertions(+), 149 deletions(-) create mode 100644 app/lib/types.js diff --git a/app/lib/types.js b/app/lib/types.js new file mode 100644 index 00000000..1b8bb6df --- /dev/null +++ b/app/lib/types.js @@ -0,0 +1,20 @@ +'use strict'; + +module.exports = { + Asset: 'x-mitre-asset', + Campaign: 'campaign', + Collection: 'x-mitre-collection', + Group: 'intrusion-set', + Mitigation: 'course-of-action', + Tool: 'tool', + Tactic: 'x-mitre-tactic', + Malware: 'malware', + Matrix: 'x-mitre-matrix', + Relationship: 'relationship', + MarkingDefinition: 'marking-definition', + Identity: 'identity', + Note: 'note', + DataSource: 'x-mitre-data-source', + DataComponent: 'x-mitre-data-component', + Technique: 'attack-pattern', +}; diff --git a/app/services/assets-service.js b/app/services/assets-service.js index 5f22e5ea..931ade13 100644 --- a/app/services/assets-service.js +++ b/app/services/assets-service.js @@ -1,9 +1,9 @@ 'use strict'; const assetsRepository = require('../repository/assets-repository'); - const BaseService = require('./_base.service'); +const { Asset: AssetType } = require('../lib/types'); class AssetsService extends BaseService {} -module.exports = new AssetsService('x-mitre-asset', assetsRepository); +module.exports = new AssetsService(AssetType, assetsRepository); diff --git a/app/services/campaigns-service.js b/app/services/campaigns-service.js index ab384132..9194e246 100644 --- a/app/services/campaigns-service.js +++ b/app/services/campaigns-service.js @@ -1,9 +1,9 @@ 'use strict'; const campaignsRepository = require('../repository/campaigns-repository'); - const BaseService = require('./_base.service'); +const { Campaign: CampaignType } = require('../lib/types'); class CampaignService extends BaseService {} -module.exports = new CampaignService('campaign', campaignsRepository); +module.exports = new CampaignService(CampaignType, campaignsRepository); diff --git a/app/services/collection-bundles-service.js b/app/services/collection-bundles-service.js index 5f8d7f11..54549b71 100644 --- a/app/services/collection-bundles-service.js +++ b/app/services/collection-bundles-service.js @@ -22,6 +22,9 @@ const dataSourcesService = require('../services/data-sources-service'); const dataComponentsService = require('../services/data-components-service'); const Collection = require('../models/collection-model'); +const Note = require('../models/note-model'); + +const workbenchObjectTypes = require('../lib/types'); const logger = require('../lib/logger'); const config = require('../config/config'); @@ -30,7 +33,6 @@ const async = require('async'); const systemConfigurationService = require('./system-configuration-service'); const linkById = require('../lib/linkById'); -const Note = require('../models/note-model'); const { DuplicateIdError } = require('../exceptions'); const forceImportParameters = { @@ -291,33 +293,36 @@ exports.importBundle = function (collection, data, options, callback) { } let service; - if (importObject.type === 'attack-pattern') { + if (importObject.type === workbenchObjectTypes.Technique) { service = techniquesService; - } else if (importObject.type === 'x-mitre-tactic') { + } else if (importObject.type === workbenchObjectTypes.Tactic) { service = tacticsService; - } else if (importObject.type === 'intrusion-set') { + } else if (importObject.type === workbenchObjectTypes.Group) { service = groupsService; - } else if (importObject.type === 'campaign') { + } else if (importObject.type === workbenchObjectTypes.Campaign) { service = campaignsService; - } else if (importObject.type === 'course-of-action') { + } else if (importObject.type === workbenchObjectTypes.Mitigation) { service = mitigationsService; - } else if (importObject.type === 'malware' || importObject.type === 'tool') { + } else if ( + importObject.type === workbenchObjectTypes.Malware || + importObject.type === workbenchObjectTypes.Tool + ) { service = softwareService; - } else if (importObject.type === 'x-mitre-matrix') { + } else if (importObject.type === workbenchObjectTypes.Matrix) { service = matricesService; - } else if (importObject.type === 'relationship') { + } else if (importObject.type === workbenchObjectTypes.Relationship) { service = relationshipService; - } else if (importObject.type === 'marking-definition') { + } else if (importObject.type === workbenchObjectTypes.MarkingDefinition) { service = markingDefinitionsService; - } else if (importObject.type === 'identity') { + } else if (importObject.type === workbenchObjectTypes.Identity) { service = identitiesService; - } else if (importObject.type === 'note') { + } else if (importObject.type === workbenchObjectTypes.Note) { service = notesService; - } else if (importObject.type === 'x-mitre-data-source') { + } else if (importObject.type === workbenchObjectTypes.DataSource) { service = dataSourcesService; - } else if (importObject.type === 'x-mitre-data-component') { + } else if (importObject.type === workbenchObjectTypes.DataComponent) { service = dataComponentsService; - } else if (importObject.type === 'x-mitre-asset') { + } else if (importObject.type === workbenchObjectTypes.Asset) { service = assetsService; } diff --git a/app/services/data-components-service.js b/app/services/data-components-service.js index 32e07617..07ee7529 100644 --- a/app/services/data-components-service.js +++ b/app/services/data-components-service.js @@ -1,9 +1,9 @@ 'use strict'; const dataComponentsRepository = require('../repository/data-components-repository.js'); - const BaseService = require('./_base.service'); +const { DataComponent: DataComponentType } = require('../lib/types.js'); class DataComponentsService extends BaseService {} -module.exports = new DataComponentsService('x-mitre-data-component', dataComponentsRepository); +module.exports = new DataComponentsService(DataComponentType, dataComponentsRepository); diff --git a/app/services/data-sources-service.js b/app/services/data-sources-service.js index 3deb642b..fc8601ae 100644 --- a/app/services/data-sources-service.js +++ b/app/services/data-sources-service.js @@ -4,6 +4,7 @@ const dataSourcesRepository = require('../repository/data-sources-repository'); const identitiesService = require('./identities-service'); const dataComponentsService = require('./data-components-service'); const BaseService = require('./_base.service'); +const { DataSource: DataSourceType } = require('../lib/types'); const { MissingParameterError, BadlyFormattedParameterError, @@ -161,4 +162,4 @@ class DataSourcesService extends BaseService { } } -module.exports = new DataSourcesService('x-mitre-data-source', dataSourcesRepository); +module.exports = new DataSourcesService(DataSourceType, dataSourcesRepository); diff --git a/app/services/groups-service.js b/app/services/groups-service.js index b833852f..840de2af 100644 --- a/app/services/groups-service.js +++ b/app/services/groups-service.js @@ -2,7 +2,8 @@ const BaseService = require('./_base.service'); const groupsRepository = require('../repository/groups-repository'); +const { Group: GroupType } = require('../lib/types'); class GroupsService extends BaseService {} -module.exports = new GroupsService('intrusion-set', groupsRepository); +module.exports = new GroupsService(GroupType, groupsRepository); diff --git a/app/services/identities-service.js b/app/services/identities-service.js index ddc45b5b..140c2d5a 100644 --- a/app/services/identities-service.js +++ b/app/services/identities-service.js @@ -7,17 +7,7 @@ const userAccountsService = require('./user-accounts-service'); const identitiesRepository = require('../repository/identities-repository'); const BaseService = require('./_base.service'); const { InvalidTypeError } = require('../exceptions'); - -const errors = { - missingParameter: 'Missing required parameter', - badlyFormattedParameter: 'Badly formatted parameter', - duplicateId: 'Duplicate id', - notFound: 'Document not found', - invalidQueryStringParameter: 'Invalid query string parameter', -}; -exports.errors = errors; - -const identityType = 'identity'; +const { Identity: IdentityType } = require('../lib/stix-types'); class IdentitiesService extends BaseService { async addCreatedByAndModifiedByIdentitiesToAll(attackObjects) { @@ -61,95 +51,4 @@ class IdentitiesService extends BaseService { return savedIdentity; } - async addCreatedByIdentity(attackObject, cache) { - // Use the cache if the caller provides one - if (cache) { - const identityObject = cache.get(attackObject.stix.created_by_ref); - if (identityObject) { - attackObject.created_by_identity = identityObject; - } - } - - if (!attackObject.created_by_identity) { - // No cache or not found in cache - try { - // eslint-disable-next-line require-atomic-updates - const identityObject = await this.repository.retrieveLatestByStixId( - attackObject.stix.created_by_ref, - ); - attackObject.created_by_identity = identityObject; - - if (cache) { - cache.set(attackObject.stix.created_by_ref, identityObject); - } - } catch (err) { - // Ignore lookup errors - } - } - } - - async addModifiedByIdentity(attackObject, cache) { - // Use the cache if the caller provides one - if (cache) { - const identityObject = cache.get(attackObject.stix.x_mitre_modified_by_ref); - if (identityObject) { - attackObject.modified_by_identity = identityObject; - } - } - - if (!attackObject.modified_by_identity) { - // No cache or not found in cache - try { - // eslint-disable-next-line require-atomic-updates - const identityObject = await this.repository.retrieveLatestByStixId( - attackObject.stix.x_mitre_modified_by_ref, - ); - attackObject.modified_by_identity = identityObject; - - if (cache) { - cache.set(attackObject.stix.x_mitre_modified_by_ref, identityObject); - } - } catch (err) { - // Ignore lookup errors - } - } - } - - static async addCreatedByUserAccountWithCache(attackObject, cache) { - const userAccountRef = attackObject?.workspace?.workflow?.created_by_user_account; - if (userAccountRef) { - // Use the cache if the caller provides one - if (cache) { - const userAccountObject = cache.get(userAccountRef); - if (userAccountObject) { - attackObject.created_by_user_account = userAccountObject; - } - } - - if (!attackObject.created_by_user_account) { - // No cache or not found in cache - await userAccountsService.addCreatedByUserAccount(attackObject); - if (cache) { - cache.set(userAccountRef, attackObject.created_by_user_account); - } - } - } - } - - async addCreatedByAndModifiedByIdentities(attackObject, identityCache, userAccountCache) { - if (attackObject && attackObject.stix && attackObject.stix.created_by_ref) { - await this.addCreatedByIdentity(attackObject, identityCache); - } - - if (attackObject && attackObject.stix && attackObject.stix.x_mitre_modified_by_ref) { - await this.addModifiedByIdentity(attackObject, identityCache); - } - - // Add user account data - if (attackObject?.workspace?.workflow?.created_by_user_account) { - await IdentitiesService.addCreatedByUserAccountWithCache(attackObject, userAccountCache); - } - } -} - -module.exports = new IdentitiesService(identityType, identitiesRepository); +module.exports = new IdentitiesService(IdentityType, identitiesRepository); diff --git a/app/services/marking-definitions-service.js b/app/services/marking-definitions-service.js index 6d15a81d..6edbca81 100644 --- a/app/services/marking-definitions-service.js +++ b/app/services/marking-definitions-service.js @@ -6,6 +6,8 @@ const identitiesService = require('./identities-service'); const config = require('../config/config'); const BaseService = require('./_base.service'); const markingDefinitionsRepository = require('../repository/marking-definitions-repository'); +const { MarkingDefinition: MarkingDefinitionType } = require('../lib/types'); + const { MissingParameterError, BadlyFormattedParameterError, @@ -129,4 +131,4 @@ class MarkingDefinitionsService extends BaseService { } } -module.exports = new MarkingDefinitionsService('marking-definition', markingDefinitionsRepository); +module.exports = new MarkingDefinitionsService(MarkingDefinitionType, markingDefinitionsRepository); diff --git a/app/services/matrices-service.js b/app/services/matrices-service.js index 9a2c95c0..4832cc62 100644 --- a/app/services/matrices-service.js +++ b/app/services/matrices-service.js @@ -1,11 +1,12 @@ 'use strict'; const util = require('util'); -const { GenericServiceError, MissingParameterError } = require('../exceptions'); +const BaseService = require('./_base.service'); const matrixRepository = require('../repository/matrix-repository'); +const { Matrix: MatrixType } = require('../lib/types'); -const BaseService = require('./_base.service'); +const { GenericServiceError, MissingParameterError } = require('../exceptions'); class MatrixService extends BaseService { constructor(type, repository) { @@ -119,4 +120,4 @@ class MatrixService extends BaseService { } } -module.exports = new MatrixService('x-mitre-matrix', matrixRepository); +module.exports = new MatrixService(MatrixType, matrixRepository); diff --git a/app/services/mitigations-service.js b/app/services/mitigations-service.js index 3684647b..78c4b161 100644 --- a/app/services/mitigations-service.js +++ b/app/services/mitigations-service.js @@ -1,9 +1,9 @@ 'use strict'; const mitigationsRepository = require('../repository/mitigations-repository'); - const BaseService = require('./_base.service'); +const { Mitigation: MitigationType } = require('../lib/types'); class MitigationsService extends BaseService {} -module.exports = new MitigationsService('course-of-action', mitigationsRepository); +module.exports = new MitigationsService(MitigationType, mitigationsRepository); diff --git a/app/services/notes-service.js b/app/services/notes-service.js index 8074bb2d..95e2fc91 100644 --- a/app/services/notes-service.js +++ b/app/services/notes-service.js @@ -1,8 +1,9 @@ 'use strict'; const notesRepository = require('../repository/notes-repository'); - const BaseService = require('./_base.service'); +const { Note: NoteType } = require('../lib/types'); + const { BadlyFormattedParameterError, DuplicateIdError, @@ -49,4 +50,4 @@ class NotesService extends BaseService { } } -module.exports = new NotesService('note', notesRepository); +module.exports = new NotesService(NoteType, notesRepository); diff --git a/app/services/relationships-service.js b/app/services/relationships-service.js index d37fc8a6..7a1625b6 100644 --- a/app/services/relationships-service.js +++ b/app/services/relationships-service.js @@ -3,11 +3,7 @@ //** core dependencies **/ const BaseService = require('./_base.service'); const relationshipsRepository = require('../repository/relationships-repository'); - -// ** service dependencies **/ -const identitiesService = require('./identities-service'); -const systemConfigService = require('./system-configuration-service'); -const config = require('../config/config'); +const { Relationship: RelationshipType } = require('../lib/types'); // ** misc **/ const uuid = require('uuid'); @@ -154,4 +150,4 @@ class RelationshipsService extends BaseService { } } -module.exports = new RelationshipsService('relationship', relationshipsRepository); +module.exports = new RelationshipsService(RelationshipType, relationshipsRepository); diff --git a/app/services/software-service.js b/app/services/software-service.js index c6be1b27..fe1b3f34 100644 --- a/app/services/software-service.js +++ b/app/services/software-service.js @@ -9,6 +9,8 @@ const { PropertyNotAllowedError, InvalidTypeError } = require('../exceptions'); const BaseService = require('./_base.service'); const softwareRepository = require('../repository/software-repository'); +const { Malware: MalwareType, Tool: ToolType } = require('../lib/types'); + class SoftwareService extends BaseService { async create(data, options, callback) { // This function handles two use cases: @@ -19,13 +21,13 @@ class SoftwareService extends BaseService { // is_family defaults to true for malware, not allowed for tools try { - if (data?.stix?.type !== 'malware' && data?.stix?.type !== 'tool') { + if (data?.stix?.type !== MalwareType && data?.stix?.type !== ToolType) { throw new InvalidTypeError(); } - if (data.stix && data.stix.type === 'malware' && typeof data.stix.is_family !== 'boolean') { + if (data.stix && data.stix.type === MalwareType && typeof data.stix.is_family !== 'boolean') { data.stix.is_family = true; - } else if (data.stix && data.stix.type === 'tool' && data.stix.is_family !== undefined) { + } else if (data.stix && data.stix.type === ToolType && data.stix.is_family !== undefined) { throw new PropertyNotAllowedError(); } @@ -41,7 +43,7 @@ class SoftwareService extends BaseService { } // Set the default marking definitions - await systemConfigurationService.setDefaultMarkingDefinitionsForObject(data); + await this.setDefaultMarkingDefinitionsForObject(data); // Get the organization identity const organizationIdentityRef = diff --git a/app/services/tactics-service.js b/app/services/tactics-service.js index def7e34a..ec470f7c 100644 --- a/app/services/tactics-service.js +++ b/app/services/tactics-service.js @@ -6,6 +6,8 @@ const { BadlyFormattedParameterError, MissingParameterError } = require('../exce const BaseService = require('./_base.service'); const tacticsRepository = require('../repository/tactics-repository'); +const { Tactic: TacticType } = require('../lib/types'); +const { BadlyFormattedParameterError, MissingParameterError } = require('../exceptions'); class TacticsService extends BaseService { static techniquesService = null; @@ -87,4 +89,4 @@ class TacticsService extends BaseService { } } -module.exports = new TacticsService('x-mitre-tactic', tacticsRepository); +module.exports = new TacticsService(TacticType, tacticsRepository); diff --git a/app/services/techniques-service.js b/app/services/techniques-service.js index 8045d312..03c94d88 100644 --- a/app/services/techniques-service.js +++ b/app/services/techniques-service.js @@ -1,12 +1,12 @@ 'use strict'; const config = require('../config/config'); - -const { BadlyFormattedParameterError, MissingParameterError } = require('../exceptions'); - const BaseService = require('./_base.service'); const techniquesRepository = require('../repository/techniques-repository'); +const { Technique: TechniqueType } = require('../lib/types'); +const { BadlyFormattedParameterError, MissingParameterError } = require('../exceptions'); + class TechniquesService extends BaseService { static tacticsService = null; @@ -90,4 +90,4 @@ class TechniquesService extends BaseService { } } -module.exports = new TechniquesService('attack-pattern', techniquesRepository); +module.exports = new TechniquesService(TechniqueType, techniquesRepository); From f513deac1b517f44f3cec2c4b2a7d33050648f64 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Wed, 8 Jan 2025 15:35:24 -0500 Subject: [PATCH 16/26] fix: refactor tests/shared/pagination from callbacks to promises --- app/tests/shared/pagination.js | 251 +++++++++++++-------------------- 1 file changed, 100 insertions(+), 151 deletions(-) diff --git a/app/tests/shared/pagination.js b/app/tests/shared/pagination.js index 83dd47e5..d16e3da9 100644 --- a/app/tests/shared/pagination.js +++ b/app/tests/shared/pagination.js @@ -66,108 +66,82 @@ PaginationTests.prototype.executeTests = function () { passportCookie = await login.loginAnonymous(app); }); - it(`GET ${self.options.baseUrl} return an empty page`, function (done) { - request(app) + it(`GET ${self.options.baseUrl} return an empty page`, async function () { + const res = await request(app) .get(`${self.options.baseUrl}?offset=0&limit=10${self.options.stateQuery}`) .set('Accept', 'application/json') .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get an array with zero test objects - const testObjects = res.body; - expect(testObjects).toBeDefined(); - expect(Array.isArray(testObjects)).toBe(true); - expect(testObjects.length).toBe(0); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get an array with zero test objects + const testObjects = res.body; + expect(testObjects).toBeDefined(); + expect(Array.isArray(testObjects)).toBe(true); + expect(testObjects.length).toBe(0); }); - it(`GET ${self.options.baseUrl} return an empty page with offset`, function (done) { - request(app) + it(`GET ${self.options.baseUrl} return an empty page with offset`, async function () { + const res = await request(app) .get(`${self.options.baseUrl}?offset=10&limit=10${self.options.stateQuery}`) .set('Accept', 'application/json') .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get an array with zero test objects - const testObjects = res.body; - expect(testObjects).toBeDefined(); - expect(Array.isArray(testObjects)).toBe(true); - expect(testObjects.length).toBe(0); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get an array with zero test objects + const testObjects = res.body; + expect(testObjects).toBeDefined(); + expect(Array.isArray(testObjects)).toBe(true); + expect(testObjects.length).toBe(0); }); - it(`GET ${self.options.baseUrl} return an empty page with pagination data`, function (done) { - request(app) + it(`GET ${self.options.baseUrl} return an empty page with pagination data`, async function () { + const res = await request(app) .get( `${self.options.baseUrl}?offset=0&limit=10&includePagination=true${self.options.stateQuery}`, ) .set('Accept', 'application/json') .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get an array with zero test objects - const testObjects = res.body.data; - expect(testObjects).toBeDefined(); - expect(Array.isArray(testObjects)).toBe(true); - expect(testObjects.length).toBe(0); - - // We expect pagination data to be included - const pagination = res.body.pagination; - expect(pagination).toBeDefined(); - expect(pagination.total).toBe(0); - expect(pagination.limit).toBe(10); - expect(pagination.offset).toBe(0); - - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get an array with zero test objects + const testObjects = res.body.data; + expect(testObjects).toBeDefined(); + expect(Array.isArray(testObjects)).toBe(true); + expect(testObjects.length).toBe(0); + + // We expect pagination data to be included + const pagination = res.body.pagination; + expect(pagination).toBeDefined(); + expect(pagination.total).toBe(0); + expect(pagination.limit).toBe(10); + expect(pagination.offset).toBe(0); }); - it(`GET ${self.options.baseUrl} return an empty page with offset with pagination data`, function (done) { - request(app) + it(`GET ${self.options.baseUrl} return an empty page with offset with pagination data`, async function () { + const res = await request(app) .get( `${self.options.baseUrl}?offset=10&limit=10&includePagination=true${self.options.stateQuery}`, ) .set('Accept', 'application/json') .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get an array with zero test objects - const testObjects = res.body.data; - expect(testObjects).toBeDefined(); - expect(Array.isArray(testObjects)).toBe(true); - expect(testObjects.length).toBe(0); - - // We expect pagination data to be included - const pagination = res.body.pagination; - expect(pagination).toBeDefined(); - expect(pagination.total).toBe(0); - expect(pagination.limit).toBe(10); - expect(pagination.offset).toBe(10); - - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get an array with zero test objects + const testObjects = res.body.data; + expect(testObjects).toBeDefined(); + expect(Array.isArray(testObjects)).toBe(true); + expect(testObjects.length).toBe(0); + + // We expect pagination data to be included + const pagination = res.body.pagination; + expect(pagination).toBeDefined(); + expect(pagination.total).toBe(0); + expect(pagination.limit).toBe(10); + expect(pagination.offset).toBe(10); }); it(`GET ${self.options.baseUrl} returns the array of preloaded objects`, async function () { @@ -190,110 +164,85 @@ PaginationTests.prototype.executeTests = function () { const pageSizeList = [5, 10, 20]; const offset = 10; pageSizeList.forEach(function (pageSize) { - it(`GET ${self.options.baseUrl} returns a page of preloaded objects`, function (done) { - request(app) + it(`GET ${self.options.baseUrl} returns a page of preloaded objects`, async function () { + const res = await request(app) .get( `${self.options.baseUrl}?offset=${offset}&limit=${pageSize}${self.options.stateQuery}`, ) .set('Accept', 'application/json') .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get an array with one page of test objects - const testObjects = res.body; - expect(testObjects).toBeDefined(); - expect(Array.isArray(testObjects)).toBe(true); - expect(testObjects.length).toBe(pageSize); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get an array with one page of test objects + const testObjects = res.body; + expect(testObjects).toBeDefined(); + expect(Array.isArray(testObjects)).toBe(true); + expect(testObjects.length).toBe(pageSize); }); - it(`GET ${self.options.baseUrl} returns a page of preloaded objects with pagination data`, function (done) { - request(app) + it(`GET ${self.options.baseUrl} returns a page of preloaded objects with pagination data`, async function () { + const res = await request(app) .get( `${self.options.baseUrl}?offset=${offset}&limit=${pageSize}&includePagination=true${self.options.stateQuery}`, ) .set('Accept', 'application/json') .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get an array with one page of test objects - const testObjects = res.body.data; - expect(testObjects).toBeDefined(); - expect(Array.isArray(testObjects)).toBe(true); - expect(testObjects.length).toBe(pageSize); - - // We expect pagination data to be included - const pagination = res.body.pagination; - expect(pagination).toBeDefined(); - expect(pagination.total).toBe(self.options.numberOfObjects); - expect(pagination.limit).toBe(pageSize); - expect(pagination.offset).toBe(offset); - - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get an array with one page of test objects + const testObjects = res.body.data; + expect(testObjects).toBeDefined(); + expect(Array.isArray(testObjects)).toBe(true); + expect(testObjects.length).toBe(pageSize); + + // We expect pagination data to be included + const pagination = res.body.pagination; + expect(pagination).toBeDefined(); + expect(pagination.total).toBe(self.options.numberOfObjects); + expect(pagination.limit).toBe(pageSize); + expect(pagination.offset).toBe(offset); }); }); - it(`GET ${self.options.baseUrl} return a partial page of preloaded objects`, function (done) { - request(app) + it(`GET ${self.options.baseUrl} return a partial page of preloaded objects`, async function () { + const res = await request(app) .get(`${self.options.baseUrl}?offset=40&limit=20${self.options.stateQuery}`) .set('Accept', 'application/json') .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get an array with one page of test objects - const testObjects = res.body; - expect(testObjects).toBeDefined(); - expect(Array.isArray(testObjects)).toBe(true); - expect(testObjects.length).toBe(5); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get an array with one page of test objects + const testObjects = res.body; + expect(testObjects).toBeDefined(); + expect(Array.isArray(testObjects)).toBe(true); + expect(testObjects.length).toBe(5); }); - it(`GET ${self.options.baseUrl} return a partial page of preloaded objects with pagination data`, function (done) { - request(app) + it(`GET ${self.options.baseUrl} return a partial page of preloaded objects with pagination data`, async function () { + const res = await request(app) .get( `${self.options.baseUrl}?offset=40&limit=20&includePagination=true${self.options.stateQuery}`, ) .set('Accept', 'application/json') .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get an array with one page of test objects - const testObjects = res.body.data; - expect(testObjects).toBeDefined(); - expect(Array.isArray(testObjects)).toBe(true); - expect(testObjects.length).toBe(5); - - // We expect pagination data to be included - const pagination = res.body.pagination; - expect(pagination).toBeDefined(); - expect(pagination.total).toBe(45); - expect(pagination.limit).toBe(20); - expect(pagination.offset).toBe(40); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get an array with one page of test objects + const testObjects = res.body.data; + expect(testObjects).toBeDefined(); + expect(Array.isArray(testObjects)).toBe(true); + expect(testObjects.length).toBe(5); + + // We expect pagination data to be included + const pagination = res.body.pagination; + expect(pagination).toBeDefined(); + expect(pagination.total).toBe(45); + expect(pagination.limit).toBe(20); + expect(pagination.offset).toBe(40); }); after(async function () { From 4bb170e9965a5008389e2169b6fa0d5fb5cae470 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Wed, 8 Jan 2025 15:37:42 -0500 Subject: [PATCH 17/26] style: cleanup + formatting --- app/services/references-service.js | 4 ++-- app/services/tactics-service.js | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/services/references-service.js b/app/services/references-service.js index a8963bc2..f2674612 100644 --- a/app/services/references-service.js +++ b/app/services/references-service.js @@ -1,6 +1,6 @@ 'use strict'; -const baseService = require('./_base.service'); +const BaseService = require('./_base.service'); const referencesRepository = require('../repository/references-repository'); const { MissingParameterError } = require('../exceptions'); @@ -11,7 +11,7 @@ class ReferencesService { async retrieveAll(options) { const results = await this.repository.retrieveAll(options); - const paginatedResults = baseService.paginate(options, results); + const paginatedResults = BaseService.paginate(options, results); return paginatedResults; } diff --git a/app/services/tactics-service.js b/app/services/tactics-service.js index ec470f7c..a24f0da9 100644 --- a/app/services/tactics-service.js +++ b/app/services/tactics-service.js @@ -1,11 +1,9 @@ 'use strict'; const config = require('../config/config'); - -const { BadlyFormattedParameterError, MissingParameterError } = require('../exceptions'); - const BaseService = require('./_base.service'); const tacticsRepository = require('../repository/tactics-repository'); + const { Tactic: TacticType } = require('../lib/types'); const { BadlyFormattedParameterError, MissingParameterError } = require('../exceptions'); From b5ee996dd62490e3471c049f95cf1b43fdb8308c Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Wed, 8 Jan 2025 15:38:55 -0500 Subject: [PATCH 18/26] refactor: remove unused variables Remove custom errors dictionary from AttackObjectsRepository (no longer in use). --- app/repository/attack-objects-repository.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/app/repository/attack-objects-repository.js b/app/repository/attack-objects-repository.js index 4db0226f..4316ff2b 100644 --- a/app/repository/attack-objects-repository.js +++ b/app/repository/attack-objects-repository.js @@ -7,17 +7,6 @@ const { lastUpdatedByQueryHelper } = require('../lib/request-parameter-helper'); const regexValidator = require('../lib/regex'); class AttackObjectsRepository extends BaseRepository { - errors = { - missingParameter: 'Missing required parameter', - badlyFormattedParameter: 'Badly formatted parameter', - duplicateId: 'Duplicate id', - notFound: 'Document not found', - invalidQueryStringParameter: 'Invalid query string parameter', - duplicateCollection: 'Duplicate collection', - }; - - identitiesService; - async retrieveAll(options) { // Build the query const query = {}; From 9c2a8f64f71b46f9d5bd1319eecefe4eb12b8411 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:19:46 -0500 Subject: [PATCH 19/26] refactor: redo relationships-repository from scratch The previous implementation was throwing a bunch of issues. It was easier to just start from 'develop' and and refactor it from scratch. --- app/repository/relationships-repository.js | 146 ++++++++++----------- 1 file changed, 69 insertions(+), 77 deletions(-) diff --git a/app/repository/relationships-repository.js b/app/repository/relationships-repository.js index f1943868..3edbf069 100644 --- a/app/repository/relationships-repository.js +++ b/app/repository/relationships-repository.js @@ -2,107 +2,99 @@ const BaseRepository = require('./_base.repository'); const Relationship = require('../models/relationship-model'); -const { DatabaseError } = require('../exceptions'); class RelationshipsRepository extends BaseRepository { - /** - * Extends BaseRepository.retrieveAll() to include relationship-specific query parameters - * and data lookup functionality. - * - * Additional options supported beyond base implementation: - * @param {Object} options - * @param {string} options.sourceRef - Filter by source reference - * @param {string} options.targetRef - Filter by target reference - * @param {string} options.sourceOrTargetRef - Filter by either source or target reference - * @param {string} options.relationshipType - Filter by relationship type - * @param {boolean} options.lookupRefs - Include source/target object data via $lookup - * - * @returns {Promise} Array of relationship documents with optional source/target data - */ async retrieveAll(options) { try { - const query = this._buildBaseQuery(options); - const aggregation = RelationshipsRepository._buildAggregation(options, query); - return await this.model.aggregate(aggregation).exec(); - } catch (err) { - throw new DatabaseError(err); - } - } - - _buildBaseQuery(options) { - const query = super._buildBaseQuery(options); + // Build the query + const query = {}; + if (!options.includeRevoked) { + query['stix.revoked'] = { $in: [null, false] }; + } + if (!options.includeDeprecated) { + query['stix.x_mitre_deprecated'] = { $in: [null, false] }; + } + if (typeof options.state !== 'undefined') { + if (Array.isArray(options.state)) { + query['workspace.workflow.state'] = { $in: options.state }; + } else { + query['workspace.workflow.state'] = options.state; + } + } + if (typeof options.sourceRef !== 'undefined') { + query['stix.source_ref'] = options.sourceRef; + } + if (typeof options.targetRef !== 'undefined') { + query['stix.target_ref'] = options.targetRef; + } + if (typeof options.sourceOrTargetRef !== 'undefined') { + query.$or = [ + { 'stix.source_ref': options.sourceOrTargetRef }, + { 'stix.target_ref': options.sourceOrTargetRef }, + ]; + } + if (typeof options.relationshipType !== 'undefined') { + query['stix.relationship_type'] = options.relationshipType; + } + if (typeof options.lastUpdatedBy !== 'undefined') { + query['workspace.workflow.created_by_user_account'] = lastUpdatedByQueryHelper( + options.lastUpdatedBy, + ); + } - if (options.sourceRef) { - query['stix.source_ref'] = options.sourceRef; - } - if (options.targetRef) { - query['stix.target_ref'] = options.targetRef; - } - if (options.sourceOrTargetRef) { - query.$or = [ - { 'stix.source_ref': options.sourceOrTargetRef }, - { 'stix.target_ref': options.sourceOrTargetRef }, - ]; - } - if (options.relationshipType) { - query['stix.relationship_type'] = options.relationshipType; - } - return query; - } + // Build the aggregation + const aggregation = []; + if (options.versions === 'latest') { + aggregation.push({ $sort: { 'stix.id': 1, 'stix.modified': -1 } }); + aggregation.push({ $group: { _id: '$stix.id', document: { $first: '$$ROOT' } } }); + aggregation.push({ $replaceRoot: { newRoot: '$document' } }); + } - static _buildAggregation(options, query) { - const aggregation = [{ $sort: { 'stix.id': 1, 'stix.modified': -1 } }]; + // Add stages for sorting, query, and reference lookups + aggregation.push({ $sort: { 'stix.id': 1 } }); + aggregation.push({ $match: query }); - if (options.versions === 'latest') { - aggregation.push( - { $group: { _id: '$stix.id', document: { $first: '$$ROOT' } } }, - { $replaceRoot: { newRoot: '$document' } }, - ); - } - - aggregation.push({ $sort: { 'stix.id': 1 } }, { $match: query }); - - if (options.lookupRefs) { - aggregation.push( - { + if (options.lookupRefs) { + aggregation.push({ $lookup: { from: 'attackObjects', localField: 'stix.source_ref', foreignField: 'stix.id', as: 'source_objects', }, - }, - { + }); + aggregation.push({ $lookup: { from: 'attackObjects', localField: 'stix.target_ref', foreignField: 'stix.id', as: 'target_objects', }, + }); + } + + const facet = { + $facet: { + totalCount: [{ $count: 'totalCount' }], + documents: [], }, - ); - } + }; - return RelationshipsRepository._addPaginationStages(aggregation, options); - } + if (options.offset) { + facet.$facet.documents.push({ $skip: options.offset }); + } else { + facet.$facet.documents.push({ $skip: 0 }); + } + if (options.limit) { + facet.$facet.documents.push({ $limit: options.limit }); + } - static _addPaginationStages(aggregation, options) { - const facet = { - $facet: { - totalCount: [{ $count: 'totalCount' }], - documents: [], - }, - }; + aggregation.push(facet); - if (options.offset) { - facet.$facet.documents.push({ $skip: options.offset }); - } - if (options.limit) { - facet.$facet.documents.push({ $limit: options.limit }); + return await this.model.aggregate(aggregation).exec(); + } catch (err) { + throw new DatabaseError(err); } - - aggregation.push(facet); - return aggregation; } } From bc93e3df69407a5aef25bbe2134142f7046d2fa1 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:21:47 -0500 Subject: [PATCH 20/26] fix: circular dependencies Identitied four methods implemented in subclasses that were being called from the parent class (BaseService) and migrated them to the parent class. --- app/services/_base.service.js | 190 +++++++--- app/services/collections-service.js | 13 +- app/services/marking-definitions-service.js | 7 +- app/services/system-configuration-service.js | 375 ++++++++++--------- 4 files changed, 342 insertions(+), 243 deletions(-) diff --git a/app/services/_base.service.js b/app/services/_base.service.js index 9465f2ed..bfcb1926 100644 --- a/app/services/_base.service.js +++ b/app/services/_base.service.js @@ -1,7 +1,6 @@ 'use strict'; const uuid = require('uuid'); -const systemConfigurationService = require('./system-configuration-service'); const config = require('../config/config'); const { DatabaseError, @@ -9,36 +8,29 @@ const { MissingParameterError, InvalidQueryStringParameterError, InvalidTypeError, + OrganizationIdentityNotSetError, } = require('../exceptions'); const AbstractService = require('./_abstract.service'); +// Import required repositories +const systemConfigurationRepository = require('../repository/system-configurations-repository'); +const identitiesRepository = require('../repository/identities-repository'); +const userAccountsService = require('./user-accounts-service'); + class BaseService extends AbstractService { constructor(type, repository) { super(); this.type = type; this.repository = repository; - } - - static attackObjectsService; - - static identitiesService; - - static systemConfigurationService; - static requireServices() { - // Late binding to avoid circular dependencies - if (!BaseService.identitiesService) { - BaseService.identitiesService = require('./identities-service'); - } - if (!BaseService.systemConfigurationService) { - BaseService.systemConfigurationService = require('./system-configuration-service'); - } + // Initialize caches for identity lookups + this.identityCache = new Map(); + this.userAccountCache = new Map(); } - // Helper function to determine if the last argument is a callback - static isCallback(arg) { - return typeof arg === 'function'; - } + // ============================ + // Pagination and Utility Methods + // ============================ static paginate(options, results) { if (options.includePagination) { @@ -59,8 +51,123 @@ class BaseService extends AbstractService { } } + static isCallback(arg) { + return typeof arg === 'function'; + } + + // ============================ + // System Configuration Methods + // ============================ + + async retrieveOrganizationIdentityRef() { + const systemConfig = await systemConfigurationRepository.retrieveOne(); + + if (systemConfig && systemConfig.organization_identity_ref) { + return systemConfig.organization_identity_ref; + } else { + throw new OrganizationIdentityNotSetError(); + } + } + + async setDefaultMarkingDefinitionsForObject(attackObject) { + const systemConfig = await systemConfigurationRepository.retrieveOne({ lean: true }); + if (!systemConfig) return; + + const defaultMarkingDefinitions = systemConfig.default_marking_definitions || []; + + if (attackObject.stix.object_marking_refs) { + attackObject.stix.object_marking_refs = attackObject.stix.object_marking_refs.concat( + defaultMarkingDefinitions.filter((e) => !attackObject.stix.object_marking_refs.includes(e)), + ); + } else { + attackObject.stix.object_marking_refs = defaultMarkingDefinitions; + } + } + + // ============================ + // Identity Management Methods + // ============================ + + async addCreatedByAndModifiedByIdentitiesToAll(attackObjects) { + for (const attackObject of attackObjects) { + await this.addCreatedByAndModifiedByIdentities(attackObject); + } + } + + async addCreatedByAndModifiedByIdentities(attackObject) { + if (attackObject?.stix?.created_by_ref) { + await this.addCreatedByIdentity(attackObject); + } + + if (attackObject?.stix?.x_mitre_modified_by_ref) { + await this.addModifiedByIdentity(attackObject); + } + + if (attackObject?.workspace?.workflow?.created_by_user_account) { + await this.addCreatedByUserAccountWithCache(attackObject); + } + } + + async addCreatedByIdentity(attackObject) { + if (this.identityCache.has(attackObject.stix.created_by_ref)) { + attackObject.created_by_identity = this.identityCache.get(attackObject.stix.created_by_ref); + return; + } + + if (!attackObject.created_by_identity) { + try { + const identityObject = await identitiesRepository.retrieveLatestByStixId( + attackObject.stix.created_by_ref, + ); + attackObject.created_by_identity = identityObject; + this.identityCache.set(attackObject.stix.created_by_ref, identityObject); + } catch (err) { + // Ignore lookup errors + } + } + } + + async addModifiedByIdentity(attackObject) { + if (this.identityCache.has(attackObject.stix.x_mitre_modified_by_ref)) { + attackObject.modified_by_identity = this.identityCache.get( + attackObject.stix.x_mitre_modified_by_ref, + ); + return; + } + + if (!attackObject.modified_by_identity) { + try { + const identityObject = await identitiesRepository.retrieveLatestByStixId( + attackObject.stix.x_mitre_modified_by_ref, + ); + attackObject.modified_by_identity = identityObject; + this.identityCache.set(attackObject.stix.x_mitre_modified_by_ref, identityObject); + } catch (err) { + // Ignore lookup errors + } + } + } + + async addCreatedByUserAccountWithCache(attackObject) { + const userAccountRef = attackObject?.workspace?.workflow?.created_by_user_account; + if (!userAccountRef) return; + + if (this.userAccountCache.has(userAccountRef)) { + attackObject.created_by_user_account = this.userAccountCache.get(userAccountRef); + return; + } + + if (!attackObject.created_by_user_account) { + await userAccountsService.addCreatedByUserAccount(attackObject); + this.userAccountCache.set(userAccountRef, attackObject.created_by_user_account); + } + } + + // ============================ + // CRUD Operations + // ============================ + async retrieveAll(options, callback) { - BaseService.requireServices(); if (BaseService.isCallback(arguments[arguments.length - 1])) { callback = arguments[arguments.length - 1]; } @@ -69,7 +176,7 @@ class BaseService extends AbstractService { try { results = await this.repository.retrieveAll(options); } catch (err) { - const databaseError = new DatabaseError(err); // Let the DatabaseError bubble up + const databaseError = new DatabaseError(err); if (callback) { return callback(databaseError); } @@ -77,9 +184,7 @@ class BaseService extends AbstractService { } try { - await BaseService.identitiesService.addCreatedByAndModifiedByIdentitiesToAll( - results[0].documents, - ); + await this.addCreatedByAndModifiedByIdentitiesToAll(results[0].documents); } catch (err) { const identityError = new IdentityServiceError({ details: err.message, @@ -99,8 +204,6 @@ class BaseService extends AbstractService { } async retrieveById(stixId, options, callback) { - BaseService.requireServices(); - if (BaseService.isCallback(arguments[arguments.length - 1])) { callback = arguments[arguments.length - 1]; } @@ -118,7 +221,7 @@ class BaseService extends AbstractService { const documents = await this.repository.retrieveAllById(stixId); try { - await BaseService.identitiesService.addCreatedByAndModifiedByIdentitiesToAll(documents); + await this.addCreatedByAndModifiedByIdentitiesToAll(documents); } catch (err) { const identityError = new IdentityServiceError({ details: err.message, @@ -138,7 +241,7 @@ class BaseService extends AbstractService { if (document) { try { - await BaseService.identitiesService.addCreatedByAndModifiedByIdentities(document); + await this.addCreatedByAndModifiedByIdentities(document); } catch (err) { const identityError = new IdentityServiceError({ details: err.message, @@ -170,13 +273,11 @@ class BaseService extends AbstractService { if (callback) { return callback(err); } - throw err; // Let the DatabaseError bubble up + throw err; } } async retrieveVersionById(stixId, modified, callback) { - BaseService.requireServices(); - if (BaseService.isCallback(arguments[arguments.length - 1])) { callback = arguments[arguments.length - 1]; } @@ -196,7 +297,6 @@ class BaseService extends AbstractService { throw err; } - // eslint-disable-next-line no-useless-catch try { const document = await this.repository.retrieveOneByVersion(stixId, modified); @@ -207,7 +307,7 @@ class BaseService extends AbstractService { return null; } else { try { - await BaseService.identitiesService.addCreatedByAndModifiedByIdentities(document); + await this.addCreatedByAndModifiedByIdentities(document); } catch (err) { const identityError = new IdentityServiceError({ details: err.message, @@ -227,13 +327,11 @@ class BaseService extends AbstractService { if (callback) { return callback(err); } - throw err; // Let the DatabaseError bubble up + throw err; } } async create(data, options, callback) { - BaseService.requireServices(); - if (BaseService.isCallback(arguments[arguments.length - 1])) { callback = arguments[arguments.length - 1]; } @@ -242,14 +340,7 @@ class BaseService extends AbstractService { throw new InvalidTypeError(); } - // eslint-disable-next-line no-useless-catch try { - // This function handles two use cases: - // 1. This is a completely new object. Create a new object and generate the stix.id if not already - // provided. Set both stix.created_by_ref and stix.x_mitre_modified_by_ref to the organization identity. - // 2. This is a new version of an existing object. Create a new object with the specified id. - // Set stix.x_mitre_modified_by_ref to the organization identity. - options = options || {}; if (!options.import) { // Set the ATT&CK Spec Version @@ -262,11 +353,10 @@ class BaseService extends AbstractService { } // Set the default marking definitions - await BaseService.systemConfigurationService.setDefaultMarkingDefinitionsForObject(data); + await this.setDefaultMarkingDefinitionsForObject(data); // Get the organization identity - const organizationIdentityRef = - await systemConfigurationService.retrieveOrganizationIdentityRef(); + const organizationIdentityRef = await this.retrieveOrganizationIdentityRef(); // Check for an existing object let existingObject; @@ -282,7 +372,6 @@ class BaseService extends AbstractService { // New object // Assign a new STIX id if not already provided if (!data.stix.id) { - // const stixIdPrefix = getStixIdPrefixFromModel(this.model.modelName, data.stix.type); data.stix.id = `${data.stix.type}--${uuid.v4()}`; } @@ -325,7 +414,6 @@ class BaseService extends AbstractService { } let document; - // eslint-disable-next-line no-useless-catch try { document = await this.repository.retrieveOneByVersion(stixId, stixModified); } catch (err) { @@ -388,7 +476,7 @@ class BaseService extends AbstractService { } throw err; } - // eslint-disable-next-line no-useless-catch + try { const document = await this.repository.findOneAndRemove(stixId, stixModified); @@ -422,7 +510,7 @@ class BaseService extends AbstractService { } throw err; } - // eslint-disable-next-line no-useless-catch + try { const res = await this.repository.deleteMany(stixId); if (callback) { diff --git a/app/services/collections-service.js b/app/services/collections-service.js index 9f076260..2d60be24 100644 --- a/app/services/collections-service.js +++ b/app/services/collections-service.js @@ -6,12 +6,16 @@ const superagent = require('superagent'); const Collection = require('../models/collection-model'); const AttackObject = require('../models/attack-object-model'); -const systemConfigurationService = require('./system-configuration-service'); const attackObjectsService = require('./attack-objects-service'); +const BaseService = require('./_base.service'); const identitiesService = require('./identities-service'); const config = require('../config/config'); const regexValidator = require('../lib/regex'); const { lastUpdatedByQueryHelper } = require('../lib/request-parameter-helper'); +const { Collection: CollectionType } = require('../lib/types'); + +// type and repository arguments not needed to call BaseService.setDefaultMarkingDefinitionsForObject +const baseService = new BaseService(null, null); const errors = { missingParameter: 'Missing required parameter', @@ -293,11 +297,10 @@ exports.create = async function (data, options) { } // Set the default marking definitions - await systemConfigurationService.setDefaultMarkingDefinitionsForObject(collection); + await baseService.setDefaultMarkingDefinitionsForObject(collection); // Get the organization identity - const organizationIdentityRef = - await systemConfigurationService.retrieveOrganizationIdentityRef(); + const organizationIdentityRef = await baseService.retrieveOrganizationIdentityRef(); // Check for an existing object let existingObject; @@ -312,7 +315,7 @@ exports.create = async function (data, options) { } else { // New object // Assign a new STIX id if not already provided - collection.stix.id = collection.stix.id || `x-mitre-collection--${uuid.v4()}`; + collection.stix.id = collection.stix.id || `${CollectionType}--${uuid.v4()}`; // Set the created_by_ref and x_mitre_modified_by_ref properties collection.stix.created_by_ref = organizationIdentityRef; diff --git a/app/services/marking-definitions-service.js b/app/services/marking-definitions-service.js index 6edbca81..d8ee1960 100644 --- a/app/services/marking-definitions-service.js +++ b/app/services/marking-definitions-service.js @@ -1,8 +1,6 @@ 'use strict'; const uuid = require('uuid'); -const systemConfigurationService = require('./system-configuration-service'); -const identitiesService = require('./identities-service'); const config = require('../config/config'); const BaseService = require('./_base.service'); const markingDefinitionsRepository = require('../repository/marking-definitions-repository'); @@ -40,8 +38,7 @@ class MarkingDefinitionsService extends BaseService { } // Get the organization identity - const organizationIdentityRef = - await systemConfigurationService.retrieveOrganizationIdentityRef(); + const organizationIdentityRef = await this.retrieveOrganizationIdentityRef(); // Check for an existing object let existingObject; @@ -98,7 +95,7 @@ class MarkingDefinitionsService extends BaseService { // Note: document is null if not found if (markingDefinition) { - await identitiesService.addCreatedByAndModifiedByIdentities(markingDefinition); + await this.addCreatedByAndModifiedByIdentities(markingDefinition); if (callback) { return callback(null, [markingDefinition]); } diff --git a/app/services/system-configuration-service.js b/app/services/system-configuration-service.js index 1d6485f3..ba25d07d 100644 --- a/app/services/system-configuration-service.js +++ b/app/services/system-configuration-service.js @@ -4,6 +4,9 @@ const fs = require('fs'); const config = require('../config/config'); const systemConfigurationRepository = require('../repository/system-configurations-repository'); const userAccountsService = require('./user-accounts-service'); +const identitiesService = require('./identities-service'); +const markingDefinitionsService = require('./marking-definitions-service'); +const BaseService = require('./_base.service'); const { SystemConfigurationNotFound, OrganizationIdentityNotSetError, @@ -14,234 +17,242 @@ const { } = require('../exceptions'); let allowedValues; -let markingDefinitionsService; -let identitiesService; -// NOTE: Some parts of the system configuration are stored in the systemconfiguration collection in the database -// (the systemconfiguration collection should have exactly one document) -// Other parts of the system configuration are read from the config module (which is prepared at start-up -// based on environment variables and an optional configuration file) - -exports.retrieveSystemVersion = function () { - const systemVersionInfo = { - version: config.app.version, - attackSpecVersion: config.app.attackSpecVersion, - }; - - return systemVersionInfo; -}; +class SystemConfigurationService extends BaseService { + /** + * @public + * CRUD Operation: Read + * Returns the system version information + */ + retrieveSystemVersion() { + return { + version: config.app.version, + attackSpecVersion: config.app.attackSpecVersion, + }; + } -async function retrieveAllowedValues() { - if (allowedValues) { - return allowedValues; - } else { + /** + * @public + * CRUD Operation: Read + * Returns allowed values for system configuration + */ + async retrieveAllowedValues() { + if (allowedValues) { + return allowedValues; + } const data = await fs.promises.readFile(config.configurationFiles.allowedValues); allowedValues = JSON.parse(data); return allowedValues; } -} -exports.retrieveAllowedValues = retrieveAllowedValues; - -async function retrieveAllowedValuesForType(objectType) { - const values = await retrieveAllowedValues(); - - return values.find((element) => element.objectType === objectType); -} -exports.retrieveAllowedValuesForType = retrieveAllowedValuesForType; - -async function retrieveAllowedValuesForTypeAndProperty(type, propertyName) { - const values = await retrieveAllowedValuesForType(type); - - return values?.properties.find((element) => element.propertyName === propertyName); -} -exports.retrieveAllowedValuesForTypeAndProperty = retrieveAllowedValuesForTypeAndProperty; - -async function retrieveAllowedValuesForTypePropertyDomain(objectType, propertyName, domainName) { - const values = await retrieveAllowedValuesForTypeAndProperty(objectType, propertyName); - return values?.domains.find((element) => element.domainName === domainName); -} -exports.retrieveAllowedValuesForTypePropertyDomain = retrieveAllowedValuesForTypePropertyDomain; - -exports.retrieveOrganizationIdentityRef = async function () { - // There should be exactly one system configuration document - const systemConfig = await systemConfigurationRepository.retrieveOne(); + /** + * @internal + * Helper method for retrieving allowed values for a specific type + */ + async retrieveAllowedValuesForType(objectType) { + const values = await this.retrieveAllowedValues(); + return values.find((element) => element.objectType === objectType); + } - if (systemConfig && systemConfig.organization_identity_ref) { - return systemConfig.organization_identity_ref; - } else { - throw new OrganizationIdentityNotSetError(); + /** + * @internal + * Helper method for retrieving allowed values for a specific type and property + */ + async retrieveAllowedValuesForTypeAndProperty(type, propertyName) { + const values = await this.retrieveAllowedValuesForType(type); + return values?.properties.find((element) => element.propertyName === propertyName); } -}; -exports.retrieveOrganizationIdentity = async function () { - if (!identitiesService) { - identitiesService = require('./identities-service'); + /** + * @internal + * Helper method for retrieving allowed values for a specific domain + */ + async retrieveAllowedValuesForTypePropertyDomain(objectType, propertyName, domainName) { + const values = await this.retrieveAllowedValuesForTypeAndProperty(objectType, propertyName); + return values?.domains.find((element) => element.domainName === domainName); } - // There should be exactly one system configuration document - const systemConfig = await systemConfigurationRepository.retrieveOne({ lean: true }); - if (systemConfig && systemConfig.organization_identity_ref) { + /** + * @public + * CRUD Operation: Read + * Returns the organization identity + */ + async retrieveOrganizationIdentity() { + // if (!identitiesService) { + // identitiesService = require('./identities-service'); + // } + + const systemConfig = await this.repository.retrieveOne({ lean: true }); + if (!systemConfig?.organization_identity_ref) { + throw new OrganizationIdentityNotSetError(); + } + const identities = await identitiesService.retrieveById( systemConfig.organization_identity_ref, { versions: 'latest' }, ); + if (identities.length === 1) { return identities[0]; - } else { - throw new OrganizationIdentityNotFoundError(systemConfig.organization_identity_ref); } - } else { - throw new OrganizationIdentityNotSetError(); + throw new OrganizationIdentityNotFoundError(systemConfig.organization_identity_ref); } -}; - -exports.setOrganizationIdentity = async function (stixId) { - // There should be exactly one system configuration document - const systemConfig = await systemConfigurationRepository.retrieveOne(); - - if (systemConfig) { - // The document exists already. Set the identity reference. - systemConfig.organization_identity_ref = stixId; - await systemConfigurationRepository.constructor.saveDocument(systemConfig); - } else { - // The document doesn't exist yet. Create a new one. - const systemConfigData = { - organization_identity_ref: stixId, - }; - const systemConfig = systemConfigurationRepository.createNewDocument(systemConfigData); - await systemConfigurationRepository.constructor.saveDocument(systemConfig); - } -}; -const retrieveDefaultMarkingDefinitions = async function (options) { - if (!markingDefinitionsService) { - markingDefinitionsService = require('./marking-definitions-service'); + /** + * @public + * CRUD Operation: Update + * Sets the organization identity + */ + async setOrganizationIdentity(stixId) { + const systemConfig = await this.repository.retrieveOne(); + + if (systemConfig) { + systemConfig.organization_identity_ref = stixId; + await this.repository.constructor.saveDocument(systemConfig); + } else { + const systemConfigData = { organization_identity_ref: stixId }; + const newConfig = this.repository.createNewDocument(systemConfigData); + await this.repository.constructor.saveDocument(newConfig); + } } - options = options ?? {}; - // There should be exactly one system configuration document - const systemConfig = await systemConfigurationRepository.retrieveOne({ lean: true }); + /** + * @public + * CRUD Operation: Read + * Returns the default marking definitions + */ + async retrieveDefaultMarkingDefinitions(options = {}) { + // if (!markingDefinitionsService) { + // markingDefinitionsService = require('./marking-definitions-service'); + // } - if (systemConfig) { - if (systemConfig.default_marking_definitions) { - if (options.refOnly) { - return systemConfig.default_marking_definitions; + const systemConfig = await this.repository.retrieveOne({ lean: true }); + if (!systemConfig) return []; + + if (!systemConfig.default_marking_definitions) return []; + + if (options.refOnly) { + return systemConfig.default_marking_definitions; + } + + const defaultMarkingDefinitions = []; + for (const stixId of systemConfig.default_marking_definitions) { + const markingDefinition = await markingDefinitionsService.retrieveById(stixId); + if (markingDefinition.length === 1) { + defaultMarkingDefinitions.push(markingDefinition[0]); } else { - const defaultMarkingDefinitions = []; - for (const stixId of systemConfig.default_marking_definitions) { - // eslint-disable-next-line no-await-in-loop - const markingDefinition = await markingDefinitionsService.retrieveById(stixId); - if (markingDefinition.length === 1) { - defaultMarkingDefinitions.push(markingDefinition[0]); - } else { - throw new DefaultMarkingDefinitionsNotFoundError(); - } - } - return defaultMarkingDefinitions; + throw new DefaultMarkingDefinitionsNotFoundError(); } - } else { - // default_marking_definitions not set - return []; } - } else { - // No system config - return []; - } -}; -exports.retrieveDefaultMarkingDefinitions = retrieveDefaultMarkingDefinitions; - -// ** migrated from attack-objects-service ** -exports.setDefaultMarkingDefinitionsForObject = async function (attackObject) { - // Add any default marking definitions that are not in the current list for this object - const defaultMarkingDefinitions = await retrieveDefaultMarkingDefinitions({ refOnly: true }); - if (attackObject.stix.object_marking_refs) { - attackObject.stix.object_marking_refs = attackObject.stix.object_marking_refs.concat( - defaultMarkingDefinitions.filter((e) => !attackObject.stix.object_marking_refs.includes(e)), - ); - } else { - attackObject.stix.object_marking_refs = defaultMarkingDefinitions; + return defaultMarkingDefinitions; } -}; - -exports.setDefaultMarkingDefinitions = async function (stixIds) { - // There should be exactly one system configuration document - const systemConfig = await systemConfigurationRepository.retrieveOne(); - - if (systemConfig) { - // The document exists already. Set the default marking definitions. - systemConfig.default_marking_definitions = stixIds; - await systemConfigurationRepository.constructor.saveDocument(systemConfig); - } else { - // The document doesn't exist yet. Create a new one. - const systemConfigData = { - default_marking_definitions: stixIds, - }; - const systemConfig = systemConfigurationRepository.createNewDocument(systemConfigData); - await systemConfigurationRepository.constructor.saveDocument(systemConfig); + + /** + * @public + * CRUD Operation: Update + * Sets the default marking definitions + */ + async setDefaultMarkingDefinitions(stixIds) { + const systemConfig = await this.repository.retrieveOne(); + + if (systemConfig) { + systemConfig.default_marking_definitions = stixIds; + await this.repository.constructor.saveDocument(systemConfig); + } else { + const systemConfigData = { default_marking_definitions: stixIds }; + const newConfig = this.repository.createNewDocument(systemConfigData); + await this.repository.constructor.saveDocument(newConfig); + } } -}; -exports.retrieveAnonymousUserAccount = async function () { - // There should be exactly one system configuration document - const systemConfig = await systemConfigurationRepository.retrieveOne({ lean: true }); + /** + * @internal + * Internal method for user account management + */ + async retrieveAnonymousUserAccount() { + const systemConfig = await this.repository.retrieveOne({ lean: true }); + + if (!systemConfig?.anonymous_user_account_id) { + throw new AnonymousUserAccountNotSetError(); + } - if (systemConfig && systemConfig.anonymous_user_account_id) { const userAccount = await userAccountsService.retrieveById( systemConfig.anonymous_user_account_id, {}, ); + if (userAccount) { return userAccount; - } else { - throw new AnonymousUserAccountNotFoundError(systemConfig.anonymous_user_account_id); } - } else { - throw new AnonymousUserAccountNotSetError(); + throw new AnonymousUserAccountNotFoundError(systemConfig.anonymous_user_account_id); } -}; -exports.setAnonymousUserAccountId = async function (userAccountId) { - // There should be exactly one system configuration document - const systemConfig = await systemConfigurationRepository.retrieveOne(); + /** + * @internal + * Internal method for user account management + */ + async setAnonymousUserAccountId(userAccountId) { + const systemConfig = await this.repository.retrieveOne(); + + if (!systemConfig) { + throw new SystemConfigurationNotFound(); + } - if (systemConfig) { - // The document exists already. Set the anonymous user account id. systemConfig.anonymous_user_account_id = userAccountId; - await systemConfigurationRepository.constructor.saveDocument(systemConfig); - } else { - throw new SystemConfigurationNotFound(); + await this.repository.constructor.saveDocument(systemConfig); } -}; - -exports.retrieveAuthenticationConfig = function () { - // We only support a one mechanism at a time, but may support multiples in the future, - // so return an array of mechanisms - const authenticationConfig = { - mechanisms: [{ authnType: config.userAuthn.mechanism }], - }; - return authenticationConfig; -}; - -exports.retrieveOrganizationNamespace = async function () { - // There should be exactly one system configuration document - const systemConfig = await systemConfigurationRepository.retrieveOne({ lean: true }); - - if (systemConfig) { + + /** + * @public + * CRUD Operation: Read + * Returns the authentication configuration + */ + retrieveAuthenticationConfig() { + return { + mechanisms: [{ authnType: config.userAuthn.mechanism }], + }; + } + + /** + * @public + * CRUD Operation: Read + * Returns the organization namespace + */ + async retrieveOrganizationNamespace() { + const systemConfig = await this.repository.retrieveOne({ lean: true }); + + if (!systemConfig) { + throw new SystemConfigurationNotFound(); + } + return systemConfig.organization_namespace; - } else { - throw new SystemConfigurationNotFound(); } -}; -exports.setOrganizationNamespace = async function (namespace) { - // There should be exactly one system configuration document - const systemConfig = await systemConfigurationRepository.retrieveOne(); + /** + * @public + * CRUD Operation: Update + * Sets the organization namespace + */ + async setOrganizationNamespace(namespace) { + const systemConfig = await this.repository.retrieveOne(); + + if (!systemConfig) { + throw new SystemConfigurationNotFound(); + } - if (systemConfig) { systemConfig.organization_namespace = namespace; - await systemConfigurationRepository.constructor.saveDocument(systemConfig); - } else { - throw new SystemConfigurationNotFound(); + await this.repository.constructor.saveDocument(systemConfig); + } + + /** + * Override of base class create() because: + * 1. create() requires a STIX `type` -- this service does not define a type + */ + async create(data, options, callback) { + throw new NotImplementedError(this.constructor.name, 'create'); } -}; +} + +// Export an instance of the service +// Pass null for type since SystemConfiguration isn't a STIX type +module.exports = new SystemConfigurationService(null, systemConfigurationRepository); From 6122e447b140bfb9b0a68a72b5e35b83c09d8bd5 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:22:04 -0500 Subject: [PATCH 21/26] refactor: attack-objects-service --- app/services/attack-objects-service.js | 179 ++++++++++++------------- 1 file changed, 86 insertions(+), 93 deletions(-) diff --git a/app/services/attack-objects-service.js b/app/services/attack-objects-service.js index 1dacef36..cbd58239 100644 --- a/app/services/attack-objects-service.js +++ b/app/services/attack-objects-service.js @@ -1,44 +1,25 @@ 'use strict'; -const Relationship = require('../models/relationship-model'); +const util = require('util'); const attackObjectsRepository = require('../repository/attack-objects-repository'); const BaseService = require('./_base.service'); - const identitiesService = require('./identities-service'); const relationshipsService = require('./relationships-service'); -const { DatabaseError, IdentityServiceError, NotImplementedError } = require('../exceptions'); +const { NotImplementedError } = require('../exceptions'); class AttackObjectsService extends BaseService { - async retrieveAll(options, callback) { - if (AttackObjectsService.isCallback(arguments[arguments.length - 1])) { - callback = arguments[arguments.length - 1]; - } - - let results; - try { - results = await this.repository.retrieveAll(options); - } catch (err) { - const databaseError = new DatabaseError(err); // Let the DatabaseError bubble up - if (callback) { - return callback(databaseError); - } - throw databaseError; - } - - try { - await identitiesService.addCreatedByAndModifiedByIdentitiesToAll(results[0].documents); - } catch (err) { - const identityError = new IdentityServiceError({ - details: err.message, - cause: err, - }); - if (callback) { - return callback(identityError); - } - throw identityError; - } - // Add relationships from separate collection + /** + * Override of base class retrieveAll() because: + * 1. Adds special handling for relationships + * 2. Uses custom pagination logic + */ + async retrieveAll(options) { + // Get attack objects from repository + const results = await this.repository.retrieveAll(options); + let documents = results[0].documents; + + // Add relationships from separate collection if not filtering by attackId or search if (!options.attackId && !options.search) { const relationshipsOptions = { includeRevoked: options.includeRevoked, @@ -50,82 +31,94 @@ class AttackObjectsService extends BaseService { lastUpdatedBy: options.lastUpdatedBy, }; const relationships = await relationshipsService.retrieveAll(relationshipsOptions); - if (relationships.length > 0) { - results[0].documents = results[0].documents.concat(relationships); - results[0].totalCount[0].totalCount += 1; - } + documents = documents.concat(relationships); } - const paginatedResults = AttackObjectsService.paginate(options, results); - if (callback) { - return callback(null, paginatedResults); + // Add identities + await identitiesService.addCreatedByAndModifiedByIdentitiesToAll(documents); + + // Handle pagination + if (options.includePagination) { + return { + pagination: { + total: results[0].totalCount[0]?.totalCount || 0, + offset: options.offset, + limit: options.limit, + }, + data: documents, + }; } - return paginatedResults; - } - - retrieveById(stixId, options, callback) { - throw new NotImplementedError(this.constructor.name, 'retrieveById'); - } - - create(data, options, callback) { - throw new NotImplementedError(this.constructor.name, 'create'); - } - - updateFull(stixId, stixModified, data, callback) { - throw new NotImplementedError(this.constructor.name, 'updateFull'); + return documents; } - deleteVersionById(stixId, stixModified, callback) { - throw new NotImplementedError(this.constructor.name, 'deleteVersionById'); - } + /** + * Override of base class retrieveVersionById() because: + * 1. Adds special handling for relationships + */ + async retrieveVersionById(stixId, modified) { + // Handle relationships separately + if (stixId.startsWith('relationship')) { + const retrieveRelationshipVersionById = util.promisify( + relationshipsService.retrieveVersionById, + ); + const relationship = await retrieveRelationshipVersionById(stixId, modified); + await identitiesService.addCreatedByAndModifiedByIdentities(relationship); + return relationship; + } - deleteById(stixId, callback) { - throw new NotImplementedError(this.constructor.name, 'deleteById'); + // Otherwise use base class implementation + return super.retrieveVersionById(stixId, modified); } - // Record that this object is part of a collection + /** + * Record that this object is part of a collection + */ async insertCollection(stixId, modified, collectionId, collectionModified) { - let attackObject; - if (stixId.startsWith('relationship')) { - // TBD: Use relationships service when that is converted to async - attackObject = await Relationship.findOne({ - 'stix.id': stixId, - 'stix.modified': modified, - }); - } else { - attackObject = await this.repository.retrieveOneByVersion(stixId, modified); + // Validate inputs + if (!stixId || !modified || !collectionId || !collectionModified) { + throw new Error('Missing required parameter'); } - if (attackObject) { - // Create the collection reference - const collection = { - collection_ref: collectionId, - collection_modified: collectionModified, - }; + // Get the document + const document = await this.retrieveVersionById(stixId, modified); + if (!document) { + throw new Error('Document not found'); + } - // Make sure the exports array exists and add the collection reference - if (!attackObject.workspace.collections) { - attackObject.workspace.collections = []; - } - - // Check to see if the collection is already added - // (collection with same id and version should only be created--and therefore objects added--one time) - const duplicateCollection = attackObject.workspace.collections.find( - (item) => - item.collection_ref === collection.collection_ref && - item.collection_modified === collection.collection_modified, - ); - if (duplicateCollection) { - throw new Error(this.errors.duplicateCollection); - } + // Create the collection reference + const collection = { + collection_ref: collectionId, + collection_modified: collectionModified, + }; - attackObject.workspace.collections.push(collection); + // Initialize collections array if needed + if (!document.workspace.collections) { + document.workspace.collections = []; + } - await attackObject.save(); - } else { - throw new Error(this.errors.notFound); + // Check for duplicate collection + const isDuplicate = document.workspace.collections.some( + (item) => + item.collection_ref === collection.collection_ref && + item.collection_modified === collection.collection_modified, + ); + if (isDuplicate) { + throw new Error('Duplicate collection'); } + + // Add collection and save + document.workspace.collections.push(collection); + return this.repository.saveDocument(document); + } + + /** + * Override of base class create() because: + * 1. create() requires a STIX `type` -- this service does not define a type + */ + async create(data, options, callback) { + throw new NotImplementedError(this.constructor.name, 'create'); } } -module.exports = new AttackObjectsService('not-a-valid-type', attackObjectsRepository); +// Export an instance of the service +module.exports = new AttackObjectsService(null, attackObjectsRepository); From 5ea3ab6bc6e4eb079a292816ec8062d2201beef6 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:22:27 -0500 Subject: [PATCH 22/26] refactor: identities-service --- app/services/identities-service.js | 80 +++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 19 deletions(-) diff --git a/app/services/identities-service.js b/app/services/identities-service.js index 140c2d5a..f797ab51 100644 --- a/app/services/identities-service.js +++ b/app/services/identities-service.js @@ -1,30 +1,25 @@ 'use strict'; const uuid = require('uuid'); -const systemConfigurationService = require('./system-configuration-service'); const config = require('../config/config'); -const userAccountsService = require('./user-accounts-service'); const identitiesRepository = require('../repository/identities-repository'); const BaseService = require('./_base.service'); const { InvalidTypeError } = require('../exceptions'); const { Identity: IdentityType } = require('../lib/stix-types'); class IdentitiesService extends BaseService { - async addCreatedByAndModifiedByIdentitiesToAll(attackObjects) { - const identityCache = new Map(); - const userAccountCache = new Map(); - for (const attackObject of attackObjects) { - // eslint-disable-next-line no-await-in-loop - await this.addCreatedByAndModifiedByIdentities(attackObject, identityCache, userAccountCache); - } - } - + /** + * @public + * CRUD Operation: Create + * + * Creates a new identity object + * + * Override of base class create() because: + * 1. Does not set created_by_ref or x_mitre_modified_by_ref + * 2. Does not check for existing identity object + */ async create(data, options) { - // This function overrides the base class create() because - // 1. It does not set the created_by_ref or x_mitre_modified_by_ref properties - // 2. It does not check for an existing identity object - - if (data?.stix?.type !== identityType) { + if (data?.stix?.type !== IdentityType) { throw new InvalidTypeError(); } @@ -40,15 +35,62 @@ class IdentitiesService extends BaseService { } // Set the default marking definitions - await systemConfigurationService.setDefaultMarkingDefinitionsForObject(data); + await this.setDefaultMarkingDefinitionsForObject(data); // Assign a new STIX id if not already provided data.stix.id = data.stix.id || `identity--${uuid.v4()}`; } // Save the document in the database - const savedIdentity = await this.repository.save(data); - return savedIdentity; + try { + return await this.repository.save(data); + } catch (err) { + th + } } + /** + * @public + * CRUD Operation: Read + * Inherited from BaseService + * retrieveAll(options) + */ + + /** + * @public + * CRUD Operation: Read + * Inherited from BaseService + * retrieveById(stixId, options) + */ + + /** + * @public + * CRUD Operation: Read + * Inherited from BaseService + * retrieveVersionById(stixId, modified) + */ + + /** + * @public + * CRUD Operation: Update + * Inherited from BaseService + * updateFull(stixId, modified, data) + */ + + /** + * @public + * CRUD Operation: Delete + * Inherited from BaseService + * deleteVersionById(stixId, modified) + */ + + /** + * @public + * CRUD Operation: Delete + * Inherited from BaseService + * deleteById(stixId) + */ +} + +// Export an instance of the service module.exports = new IdentitiesService(IdentityType, identitiesRepository); From b6e350f46aa41ffc670c2fe48b1a31be3faa7d45 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:22:37 -0500 Subject: [PATCH 23/26] refactor: relationships-service --- app/services/relationships-service.js | 169 ++++++++------------------ 1 file changed, 53 insertions(+), 116 deletions(-) diff --git a/app/services/relationships-service.js b/app/services/relationships-service.js index 7a1625b6..84096183 100644 --- a/app/services/relationships-service.js +++ b/app/services/relationships-service.js @@ -1,152 +1,89 @@ 'use strict'; -//** core dependencies **/ const BaseService = require('./_base.service'); const relationshipsRepository = require('../repository/relationships-repository'); const { Relationship: RelationshipType } = require('../lib/types'); -// ** misc **/ -const uuid = require('uuid'); -const { InvalidTypeError } = require('../exceptions'); +// Map STIX types to ATT&CK types +const objectTypeMap = new Map([ + ['malware', 'software'], + ['tool', 'software'], + ['attack-pattern', 'technique'], + ['intrusion-set', 'group'], + ['campaign', 'campaign'], + ['x-mitre-asset', 'asset'], + ['course-of-action', 'mitigation'], + ['x-mitre-tactic', 'tactic'], + ['x-mitre-matrix', 'matrix'], + ['x-mitre-data-component', 'data-component'], +]); class RelationshipsService extends BaseService { - static objectTypeMap = new Map([ - ['malware', 'software'], - ['tool', 'software'], - ['attack-pattern', 'technique'], - ['intrusion-set', 'group'], - ['campaign', 'campaign'], - ['x-mitre-asset', 'asset'], - ['course-of-action', 'mitigation'], - ['x-mitre-tactic', 'tactic'], - ['x-mitre-matrix', 'matrix'], - ['x-mitre-data-component', 'data-component'], - ]); - async retrieveAll(options) { - // First get results from repository - const results = await this.repository.retrieveAll(options); + let results = await super.retrieveAll(options); - // Apply source/target type filtering if needed - if (options.sourceType || options.targetType) { - const [{ documents, totalCount }] = results; - let filteredDocs = documents; + if (options.lookupRefs) { + results = Array.isArray(results) ? results : results.data; // Filter by source type if specified if (options.sourceType) { - filteredDocs = filteredDocs.filter((document) => { - if (!document.source_objects?.length) { + results = results.filter((document) => { + if (document.source_objects?.length === 0) { return false; } - // Sort by modified date to get the latest version document.source_objects.sort((a, b) => b.stix.modified - a.stix.modified); - return ( - RelationshipsService.objectTypeMap.get(document.source_objects[0].stix.type) === - options.sourceType - ); + return objectTypeMap.get(document.source_objects[0].stix.type) === options.sourceType; }); } // Filter by target type if specified if (options.targetType) { - filteredDocs = filteredDocs.filter((document) => { - if (!document.target_objects?.length) { + results = results.filter((document) => { + if (document.target_objects?.length === 0) { return false; } - // Sort by modified date to get the latest version document.target_objects.sort((a, b) => b.stix.modified - a.stix.modified); - return ( - RelationshipsService.objectTypeMap.get(document.target_objects[0].stix.type) === - options.targetType - ); + return objectTypeMap.get(document.target_objects[0].stix.type) === options.targetType; }); } - // Update results with filtered documents and recalculate total count - results[0].documents = filteredDocs; - if (totalCount?.length > 0) { - results[0].totalCount[0].totalCount = filteredDocs.length; - } - } - - // Add identity information if requested - if (options.includeIdentities) { - await identitiesService.addCreatedByAndModifiedByIdentitiesToAll(results[0].documents); - } - - // Format and return results - return RelationshipsService._formatResults(results, options); - } - - static _formatResults(results, options) { - const [{ documents, totalCount }] = results; - - // Move source/target objects to single properties - for (const document of documents) { - if (Array.isArray(document.source_objects)) { - document.source_object = document.source_objects[0]; - delete document.source_objects; - } - if (Array.isArray(document.target_objects)) { - document.target_object = document.target_objects[0]; - delete document.target_objects; - } - } - - // Return with or without pagination wrapper - return options.includePagination - ? { - pagination: { - total: totalCount[0]?.totalCount || 0, - offset: options.offset, - limit: options.limit, - }, - data: documents, + // Move latest source and target objects to non-array properties + for (const document of results) { + if (Array.isArray(document.source_objects)) { + if (document.source_objects.length === 0) { + document.source_objects = undefined; + } else { + document.source_object = document.source_objects[0]; + document.source_objects = undefined; + } } - : documents; - } - - async create(data, options = {}) { - if (data?.stix?.type !== 'relationship') { - throw new InvalidTypeError(); - } - - if (!options.import) { - await this._enrichMetadata(data, options); - } - - return this.repository.save(data); - } - async _enrichMetadata(data, options) { - data.stix.x_mitre_attack_spec_version = - data.stix.x_mitre_attack_spec_version ?? config.app.attackSpecVersion; - - if (options.userAccountId) { - data.workspace.workflow.created_by_user_account = options.userAccountId; + if (Array.isArray(document.target_objects)) { + if (document.target_objects.length === 0) { + document.target_objects = undefined; + } else { + document.target_object = document.target_objects[0]; + document.target_objects = undefined; + } + } + } } - // Set the default marking definitions - await systemConfigService.setDefaultMarkingDefinitionsForObject(data); - - const organizationIdentityRef = - await this.systemConfigService.retrieveOrganizationIdentityRef(); - - // Check for existing object - let existingObject; - if (data.stix.id) { - existingObject = await this.repository.retrieveOneById(data.stix.id); + // this does not work: + // return RelationshipsService.paginate(options, results); + + if (options.includePagination) { + return { + pagination: { + total: results.length, + offset: options.offset, + limit: options.limit, + }, + data: results, + }; } - if (existingObject) { - // New version - only set modified_by_ref - data.stix.x_mitre_modified_by_ref = organizationIdentityRef; - } else { - // New object - set both refs and generate id if needed - data.stix.id = data.stix.id || `relationship--${uuid.v4()}`; - data.stix.created_by_ref = organizationIdentityRef; - data.stix.x_mitre_modified_by_ref = organizationIdentityRef; - } + return results; } } From ea58ced65deaf5a9c08e69ad061c06bb4c630615 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:43:48 -0500 Subject: [PATCH 24/26] fix: modify eslint config to interop with prettier --- .eslintrc.yml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 6f95cbef..98e09a80 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -22,9 +22,7 @@ rules: array-callback-return: 'off' array-element-newline: 'off' arrow-body-style: error - arrow-parens: - - error - - as-needed + arrow-parens: 'off' # Let Prettier handle arrow function parentheses arrow-spacing: - error - after: true @@ -68,9 +66,7 @@ rules: id-blacklist: error id-length: 'off' id-match: error - implicit-arrow-linebreak: - - error - - beside + implicit-arrow-linebreak: 'off' # Allow arrow function expressions to span multiple lines indent: 'off' indent-legacy: 'off' init-declarations: 'off' @@ -221,9 +217,7 @@ rules: one-var: 'off' one-var-declaration-per-line: 'off' operator-assignment: 'off' - operator-linebreak: - - error - - after + operator-linebreak: 'off' # Let Prettier handle operator line breaks padded-blocks: 'off' padding-line-between-statements: error prefer-arrow-callback: 'off' From 99fd996eddc92c1f70d7319b06565cf3048fa903 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:45:01 -0500 Subject: [PATCH 25/26] fix: resolve linting issues + various biz logic issues All 581 regression tests are now passing! --- .../system-configuration-controller.js | 5 +- app/repository/relationships-repository.js | 20 +--- app/services/attack-objects-service.js | 4 +- app/services/identities-service.js | 53 +--------- app/services/index.js | 29 ++++++ app/services/relationships-service.js | 96 +++++++++++-------- app/services/system-configuration-service.js | 26 +++-- 7 files changed, 114 insertions(+), 119 deletions(-) create mode 100644 app/services/index.js diff --git a/app/controllers/system-configuration-controller.js b/app/controllers/system-configuration-controller.js index a2ec5c83..5902ac09 100644 --- a/app/controllers/system-configuration-controller.js +++ b/app/controllers/system-configuration-controller.js @@ -1,11 +1,12 @@ 'use strict'; const systemConfigurationService = require('../services/system-configuration-service'); +const { SystemConfigurationService } = require('../services/system-configuration-service'); const logger = require('../lib/logger'); exports.retrieveSystemVersion = function (req, res) { try { - const systemVersionInfo = systemConfigurationService.retrieveSystemVersion(); + const systemVersionInfo = SystemConfigurationService.retrieveSystemVersion(); logger.debug( `Success: Retrieved system version, version: ${systemVersionInfo.version}, attackSpecVersion: ${systemVersionInfo.attackSpecVersion}`, ); @@ -57,7 +58,7 @@ exports.setOrganizationIdentity = async function (req, res) { exports.retrieveAuthenticationConfig = function (req, res) { try { - const authenticationConfig = systemConfigurationService.retrieveAuthenticationConfig(); + const authenticationConfig = SystemConfigurationService.retrieveAuthenticationConfig(); logger.debug('Success: Retrieved authentication configuration.'); return res.status(200).send(authenticationConfig); } catch (err) { diff --git a/app/repository/relationships-repository.js b/app/repository/relationships-repository.js index 3edbf069..d597da92 100644 --- a/app/repository/relationships-repository.js +++ b/app/repository/relationships-repository.js @@ -2,6 +2,8 @@ const BaseRepository = require('./_base.repository'); const Relationship = require('../models/relationship-model'); +const { lastUpdatedByQueryHelper } = require('../lib/request-parameter-helper'); +const { DatabaseError } = require('../exceptions'); class RelationshipsRepository extends BaseRepository { async retrieveAll(options) { @@ -73,24 +75,6 @@ class RelationshipsRepository extends BaseRepository { }); } - const facet = { - $facet: { - totalCount: [{ $count: 'totalCount' }], - documents: [], - }, - }; - - if (options.offset) { - facet.$facet.documents.push({ $skip: options.offset }); - } else { - facet.$facet.documents.push({ $skip: 0 }); - } - if (options.limit) { - facet.$facet.documents.push({ $limit: options.limit }); - } - - aggregation.push(facet); - return await this.model.aggregate(aggregation).exec(); } catch (err) { throw new DatabaseError(err); diff --git a/app/services/attack-objects-service.js b/app/services/attack-objects-service.js index cbd58239..749266a8 100644 --- a/app/services/attack-objects-service.js +++ b/app/services/attack-objects-service.js @@ -115,10 +115,12 @@ class AttackObjectsService extends BaseService { * Override of base class create() because: * 1. create() requires a STIX `type` -- this service does not define a type */ - async create(data, options, callback) { + create(data, options, callback) { throw new NotImplementedError(this.constructor.name, 'create'); } } +module.exports.AttackObjectsService = AttackObjectsService; + // Export an instance of the service module.exports = new AttackObjectsService(null, attackObjectsRepository); diff --git a/app/services/identities-service.js b/app/services/identities-service.js index f797ab51..a9df91a1 100644 --- a/app/services/identities-service.js +++ b/app/services/identities-service.js @@ -5,7 +5,7 @@ const config = require('../config/config'); const identitiesRepository = require('../repository/identities-repository'); const BaseService = require('./_base.service'); const { InvalidTypeError } = require('../exceptions'); -const { Identity: IdentityType } = require('../lib/stix-types'); +const { Identity: IdentityType } = require('../lib/types'); class IdentitiesService extends BaseService { /** @@ -42,55 +42,12 @@ class IdentitiesService extends BaseService { } // Save the document in the database - try { - return await this.repository.save(data); - } catch (err) { - th - } + return await this.repository.save(data); } - - /** - * @public - * CRUD Operation: Read - * Inherited from BaseService - * retrieveAll(options) - */ - - /** - * @public - * CRUD Operation: Read - * Inherited from BaseService - * retrieveById(stixId, options) - */ - - /** - * @public - * CRUD Operation: Read - * Inherited from BaseService - * retrieveVersionById(stixId, modified) - */ - - /** - * @public - * CRUD Operation: Update - * Inherited from BaseService - * updateFull(stixId, modified, data) - */ - - /** - * @public - * CRUD Operation: Delete - * Inherited from BaseService - * deleteVersionById(stixId, modified) - */ - - /** - * @public - * CRUD Operation: Delete - * Inherited from BaseService - * deleteById(stixId) - */ } +//Default export +module.exports.IdentitiesService = IdentitiesService; + // Export an instance of the service module.exports = new IdentitiesService(IdentityType, identitiesRepository); diff --git a/app/services/index.js b/app/services/index.js new file mode 100644 index 00000000..ba4b67d3 --- /dev/null +++ b/app/services/index.js @@ -0,0 +1,29 @@ +'use strict'; + +//** import repositories */ +const attackObjectsRepository = require('../repository/attack-objects-repository'); +const identitiesRepository = require('../repository/identities-repository'); +const relationshipsRepository = require('../repository/relationships-repository'); + +//** imports services */ +const AttackObjectsService = require('./attack-objects-service'); +const { IdentitiesService } = require('./identities-service'); +const RelationshipsService = require('./relationships-service'); + +//** import types */ +const { Identity: IdentityType, Relationship: RelationshipType } = require('../lib/types'); + +// ** initialize services */ +const identitiesService = new IdentitiesService(IdentityType, identitiesRepository); +const relationshipsService = new RelationshipsService(RelationshipType, relationshipsRepository); +const attackObjectsService = new AttackObjectsService( + attackObjectsRepository, + identitiesService, + relationshipsService, +); + +module.exports = { + identitiesService, + relationshipsService, + attackObjectsService, +}; diff --git a/app/services/relationships-service.js b/app/services/relationships-service.js index 84096183..84052af5 100644 --- a/app/services/relationships-service.js +++ b/app/services/relationships-service.js @@ -20,71 +20,87 @@ const objectTypeMap = new Map([ class RelationshipsService extends BaseService { async retrieveAll(options) { - let results = await super.retrieveAll(options); + let results = await this.repository.retrieveAll(options); - if (options.lookupRefs) { - results = Array.isArray(results) ? results : results.data; - - // Filter by source type if specified - if (options.sourceType) { - results = results.filter((document) => { - if (document.source_objects?.length === 0) { - return false; - } + // Filter out relationships that don't reference the source type + if (options.sourceType) { + results = results.filter((document) => { + if (document.source_objects.length === 0) { + return false; + } else { document.source_objects.sort((a, b) => b.stix.modified - a.stix.modified); return objectTypeMap.get(document.source_objects[0].stix.type) === options.sourceType; - }); - } + } + }); + } - // Filter by target type if specified - if (options.targetType) { - results = results.filter((document) => { - if (document.target_objects?.length === 0) { - return false; - } + // Filter out relationships that don't reference the target type + if (options.targetType) { + results = results.filter((document) => { + if (document.target_objects.length === 0) { + return false; + } else { document.target_objects.sort((a, b) => b.stix.modified - a.stix.modified); return objectTypeMap.get(document.target_objects[0].stix.type) === options.targetType; - }); + } + }); + } + + const prePaginationTotal = results.length; + + // Apply pagination parameters + if (options.offset || options.limit) { + const start = options.offset || 0; + if (options.limit) { + const end = start + options.limit; + results = results.slice(start, end); + } else { + results = results.slice(start); } + } - // Move latest source and target objects to non-array properties - for (const document of results) { - if (Array.isArray(document.source_objects)) { - if (document.source_objects.length === 0) { - document.source_objects = undefined; - } else { - document.source_object = document.source_objects[0]; - document.source_objects = undefined; - } + // Move latest source and target objects to a non-array property, then remove array of source and target objects + for (const document of results) { + if (Array.isArray(document.source_objects)) { + if (document.source_objects.length === 0) { + document.source_objects = undefined; + } else { + document.source_object = document.source_objects[0]; + document.source_objects = undefined; } + } - if (Array.isArray(document.target_objects)) { - if (document.target_objects.length === 0) { - document.target_objects = undefined; - } else { - document.target_object = document.target_objects[0]; - document.target_objects = undefined; - } + if (Array.isArray(document.target_objects)) { + if (document.target_objects.length === 0) { + document.target_objects = undefined; + } else { + document.target_object = document.target_objects[0]; + document.target_objects = undefined; } } } - // this does not work: - // return RelationshipsService.paginate(options, results); + if (options.includeIdentities) { + await this.addCreatedByAndModifiedByIdentitiesToAll(results); + } if (options.includePagination) { return { pagination: { - total: results.length, + total: prePaginationTotal, offset: options.offset, limit: options.limit, }, data: results, }; + } else { + return results; } - - return results; } } +// Default export +module.exports.RelationshipsService = RelationshipsService; + +// Default export - export an instance of the service module.exports = new RelationshipsService(RelationshipType, relationshipsRepository); diff --git a/app/services/system-configuration-service.js b/app/services/system-configuration-service.js index ba25d07d..e2177a6e 100644 --- a/app/services/system-configuration-service.js +++ b/app/services/system-configuration-service.js @@ -14,17 +14,21 @@ const { DefaultMarkingDefinitionsNotFoundError, AnonymousUserAccountNotSetError, AnonymousUserAccountNotFoundError, + NotImplementedError, } = require('../exceptions'); -let allowedValues; - class SystemConfigurationService extends BaseService { + constructor() { + super(null, systemConfigurationRepository); + this._allowedValues = null; + } + /** * @public - * CRUD Operation: Read + * (CRUD Operation: Read) * Returns the system version information */ - retrieveSystemVersion() { + static retrieveSystemVersion() { return { version: config.app.version, attackSpecVersion: config.app.attackSpecVersion, @@ -37,12 +41,12 @@ class SystemConfigurationService extends BaseService { * Returns allowed values for system configuration */ async retrieveAllowedValues() { - if (allowedValues) { - return allowedValues; + if (this._allowedValues) { + return this._allowedValues; } const data = await fs.promises.readFile(config.configurationFiles.allowedValues); - allowedValues = JSON.parse(data); - return allowedValues; + this._allowedValues = JSON.parse(data); + return this._allowedValues; } /** @@ -207,7 +211,7 @@ class SystemConfigurationService extends BaseService { * CRUD Operation: Read * Returns the authentication configuration */ - retrieveAuthenticationConfig() { + static retrieveAuthenticationConfig() { return { mechanisms: [{ authnType: config.userAuthn.mechanism }], }; @@ -248,7 +252,7 @@ class SystemConfigurationService extends BaseService { * Override of base class create() because: * 1. create() requires a STIX `type` -- this service does not define a type */ - async create(data, options, callback) { + create(data, options, callback) { throw new NotImplementedError(this.constructor.name, 'create'); } } @@ -256,3 +260,5 @@ class SystemConfigurationService extends BaseService { // Export an instance of the service // Pass null for type since SystemConfiguration isn't a STIX type module.exports = new SystemConfigurationService(null, systemConfigurationRepository); + +module.exports.SystemConfigurationService = SystemConfigurationService; From 6c3f3aa7057c20c73feadb0324fa4f88cb2e1c77 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 9 Jan 2025 14:54:49 -0500 Subject: [PATCH 26/26] Override 'class-methods-use-this' linting rule for two BaseService methods Converting these two methods to static would require changes throughout the service layer. Rather than exhaust time refactoring them to call the method statically, we should investigate design changes that will organically resolve the issue. --- app/services/_base.service.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/services/_base.service.js b/app/services/_base.service.js index bfcb1926..a1146496 100644 --- a/app/services/_base.service.js +++ b/app/services/_base.service.js @@ -59,6 +59,8 @@ class BaseService extends AbstractService { // System Configuration Methods // ============================ + // TODO resolve this linting issue at some point - it was less impactful to just bypass it at the time + // eslint-disable-next-line class-methods-use-this async retrieveOrganizationIdentityRef() { const systemConfig = await systemConfigurationRepository.retrieveOne(); @@ -69,6 +71,8 @@ class BaseService extends AbstractService { } } + // TODO resolve this linting issue at some point - it was less impactful to just bypass it at the time + // eslint-disable-next-line class-methods-use-this async setDefaultMarkingDefinitionsForObject(attackObject) { const systemConfig = await systemConfigurationRepository.retrieveOne({ lean: true }); if (!systemConfig) return;