From 50648ba3dc2e3a6687e9f4d2a3f75099f470548d Mon Sep 17 00:00:00 2001 From: Sun Date: Wed, 4 Oct 2023 17:12:14 -0400 Subject: [PATCH 01/36] starting to refactor groups service with service and repository --- app/repository/groups-repository.js | 14 ++ app/services/groups-service.js | 352 +--------------------------- 2 files changed, 19 insertions(+), 347 deletions(-) create mode 100644 app/repository/groups-repository.js diff --git a/app/repository/groups-repository.js b/app/repository/groups-repository.js new file mode 100644 index 00000000..616fa592 --- /dev/null +++ b/app/repository/groups-repository.js @@ -0,0 +1,14 @@ +'use strict'; + +const BaseRepository = require('./_base.repository'); +const Group = require('../models/group-model'); + +class GroupsRepository extends BaseRepository { + + constructor() { + super(Group); + + } +} + +module.exports = new GroupsRepository(); \ No newline at end of file diff --git a/app/services/groups-service.js b/app/services/groups-service.js index 8d307c86..ceb02222 100644 --- a/app/services/groups-service.js +++ b/app/services/groups-service.js @@ -19,354 +19,12 @@ const errors = { }; exports.errors = errors; -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 (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 - Group.aggregate(aggregation, function(err, results) { - if (err) { - return callback(err); - } - else { - identitiesService.addCreatedByAndModifiedByIdentitiesToAll(results[0].documents) - .then(function() { - 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: results[0].documents - }; - return callback(null, returnValue); - } - else { - return callback(null, results[0].documents); - } - }); - } - }); -}; - -exports.retrieveById = function(stixId, options, callback) { - // versions=all Retrieve all groups with the stixId - // versions=latest Retrieve the groups 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') { - Group.find({'stix.id': stixId}) - .lean() - .exec(function (err, groups) { - 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(groups) - .then(() => callback(null, groups)); - } - }); - } - else if (options.versions === 'latest') { - Group.findOne({ 'stix.id': stixId }) - .sort('-stix.modified') - .lean() - .exec(function(err, group) { - 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 (group) { - identitiesService.addCreatedByAndModifiedByIdentities(group) - .then(() => callback(null, [ group ])); - } - 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 versions of the group 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); - } - - Group.findOne({ 'stix.id': stixId, 'stix.modified': modified }, function(err, group) { - 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 (group) { - identitiesService.addCreatedByAndModifiedByIdentities(group) - .then(() => callback(null, group)); - } - else { - return callback(); - } - } - }); -}; +class GroupsService extends BaseService { -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. + constructor() { + super(matrixRepository, Group); - if (data.stix.type !== 'intrusion-set') { - throw new Error(errors.invalidType); } +} - // Create the document - const group = new Group(data); - - options = options || {}; - if (!options.import) { - // Set the ATT&CK Spec Version - group.stix.x_mitre_attack_spec_version = group.stix.x_mitre_attack_spec_version ?? config.app.attackSpecVersion; - - // Record the user account that created the object - if (options.userAccountId) { - group.workspace.workflow.created_by_user_account = options.userAccountId; - } - - // Set the default marking definitions - await attackObjectsService.setDefaultMarkingDefinitions(group); - - // Get the organization identity - const organizationIdentityRef = await systemConfigurationService.retrieveOrganizationIdentityRef(); - - // Check for an existing object - let existingObject; - if (group.stix.id) { - existingObject = await Group.findOne({ 'stix.id': group.stix.id }); - } - - if (existingObject) { - // New version of an existing object - // Only set the x_mitre_modified_by_ref property - group.stix.x_mitre_modified_by_ref = organizationIdentityRef; - } - else { - // New object - // Assign a new STIX id if not already provided - group.stix.id = group.stix.id || `intrusion-set--${uuid.v4()}`; - - // Set the created_by_ref and x_mitre_modified_by_ref properties - group.stix.created_by_ref = organizationIdentityRef; - group.stix.x_mitre_modified_by_ref = organizationIdentityRef; - } - } - - // Save the document in the database - try { - const savedGroup = await group.save(); - return savedGroup; - } - 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); - } - - Group.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); - } - - Group.findOneAndRemove({ 'stix.id': stixId, 'stix.modified': stixModified }, function (err, group) { - if (err) { - return callback(err); - } else { - //Note: group is null if not found - return callback(null, group); - } - }); -}; - -exports.deleteById = function (stixId, callback) { - if (!stixId) { - const error = new Error(errors.missingParameter); - error.parameterName = 'stixId'; - return callback(error); - } - - Group.deleteMany({ 'stix.id': stixId }, function (err, group) { - if (err) { - return callback(err); - } else { - //Note: group is null if not found - return callback(null, group); - } - }); -}; +module.exports = new GroupsService; \ No newline at end of file From 1e07ea34473ee72f84d36f1d890567430eaca70c Mon Sep 17 00:00:00 2001 From: Sun Date: Wed, 4 Oct 2023 17:55:27 -0400 Subject: [PATCH 02/36] refactoring tests part 1 --- .../groups/groups-input-validation.spec.js | 83 ++++--------------- 1 file changed, 14 insertions(+), 69 deletions(-) diff --git a/app/tests/api/groups/groups-input-validation.spec.js b/app/tests/api/groups/groups-input-validation.spec.js index ad7dbbf3..e0413aba 100644 --- a/app/tests/api/groups/groups-input-validation.spec.js +++ b/app/tests/api/groups/groups-input-validation.spec.js @@ -36,7 +36,7 @@ const initialObjectData = { function executeTests(getApp, propertyName, options) { options = options ?? {}; if (options.required) { - it(`POST /api/groups does not create a group when ${propertyName} is missing`, function (done) { + 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; @@ -47,18 +47,11 @@ function executeTests(getApp, propertyName, options) { .post('/api/groups') .send(body) .set('Accept', 'application/json') - .expect(400) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); + .expect(400); }); } - it(`POST /api/groups does not create a group when ${ propertyName } is a number`, function (done) { + 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; @@ -69,18 +62,10 @@ function executeTests(getApp, propertyName, options) { .post('/api/groups') .send(body) .set('Accept', 'application/json') - .expect(400) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); + .expect(400); }); - it(`POST /api/groups does not create a group when ${ propertyName } is an object`, function (done) { + 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; @@ -91,15 +76,7 @@ function executeTests(getApp, propertyName, options) { .post('/api/groups') .send(body) .set('Accept', 'application/json') - .expect(400) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); + .expect(400); }); } @@ -125,25 +102,17 @@ describe('Groups API Input Validation', function () { passportCookie = await login.loginAnonymous(app); }); - it('POST /api/groups does not create an empty group', function (done) { + it('POST /api/groups does not create an empty group', async function () { const body = { }; request(app) .post('/api/groups') .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); }); - it('POST /api/groups does not create a group when an unknown query parameter is provided', function (done) { + 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; @@ -154,18 +123,10 @@ describe('Groups API Input Validation', function () { .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); }); - it('POST /api/groups does not create a group when an invalid type is provided', function (done) { + it('POST /api/groups does not create a group when an invalid type is provided', function () { const groupData = _.cloneDeep(initialObjectData); const timestamp = new Date().toISOString(); groupData.stix.created = timestamp; @@ -177,18 +138,10 @@ describe('Groups API Input Validation', function () { .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); }); - it('POST /api/groups does not create a group when an incorrect type is provided', function (done) { + it('POST /api/groups does not create a group when an incorrect type is provided', function () { const groupData = _.cloneDeep(initialObjectData); const timestamp = new Date().toISOString(); groupData.stix.created = timestamp; @@ -200,15 +153,7 @@ describe('Groups API Input Validation', function () { .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); }); executeTests(() => app, 'stix.type', { required: true }); From 030e9a1522c2d97568b55b88728890e0acdd7f07 Mon Sep 17 00:00:00 2001 From: Sun Date: Thu, 5 Oct 2023 12:40:04 -0400 Subject: [PATCH 03/36] refactoring groups tests part 2 --- app/tests/api/groups/groups.spec.js | 196 +++++++++++----------------- 1 file changed, 76 insertions(+), 120 deletions(-) diff --git a/app/tests/api/groups/groups.spec.js b/app/tests/api/groups/groups.spec.js index c41ac34d..1d265723 100644 --- a/app/tests/api/groups/groups.spec.js +++ b/app/tests/api/groups/groups.spec.js @@ -88,157 +88,113 @@ describe('Groups API', function () { defaultMarkingDefinition1 = await addDefaultMarkingDefinition(markingDefinitionData); }); - it('GET /api/groups returns an empty array of groups', function (done) { - request(app) + 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/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // 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); - done(); - } - }); + .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', function (done) { + it('POST /api/groups does not create an empty group', async function () { const body = { }; - request(app) + const res = await request(app) .post('/api/groups') .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 group1; - it('POST /api/groups creates a group', function (done) { + 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; - request(app) + 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/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // 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); + .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); - done(); - } - }); }); - it('GET /api/groups returns the added group', function (done) { - request(app) + 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/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // 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); - done(); - } - }); + .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', function (done) { - request(app) + it('GET /api/groups/:id should not return a group when the id cannot be found', async function () { + const res = await request(app) .get('/api/groups/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/groups/:id returns the added group', function (done) { - request(app) + 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/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // 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); + .expect('Content-Type', /json/); - 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); + // 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); - done(); - } - }); + 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', function (done) { + it('PUT /api/groups updates a group', async function () { const originalModified = group1.stix.modified; const timestamp = new Date().toISOString(); group1.stix.modified = timestamp; @@ -266,7 +222,7 @@ describe('Groups API', function () { }); }); - it('POST /api/groups does not create a group with the same id and modified date', function (done) { + it('POST /api/groups does not create a group with the same id and modified date', async function () { const body = group1; request(app) .post('/api/groups') @@ -312,7 +268,7 @@ describe('Groups API', function () { expect(group).toBeDefined(); }); - it('GET /api/groups returns the latest added group', function (done) { + it('GET /api/groups returns the latest added group', async function () { request(app) .get('/api/groups/' + group2.stix.id) .set('Accept', 'application/json') @@ -345,7 +301,7 @@ describe('Groups API', function () { }); }); - it('GET /api/groups returns all added groups', function (done) { + it('GET /api/groups returns all added groups', async function () { request(app) .get('/api/groups/' + group1.stix.id + '?versions=all') .set('Accept', 'application/json') @@ -367,7 +323,7 @@ describe('Groups API', function () { }); }); - it('GET /api/groups/:id/modified/:modified returns the first added group', function (done) { + it('GET /api/groups/:id/modified/:modified returns the first added group', async function () { request(app) .get('/api/groups/' + group1.stix.id + '/modified/' + group1.stix.modified) .set('Accept', 'application/json') @@ -390,7 +346,7 @@ describe('Groups API', function () { }); }); - it('GET /api/groups/:id/modified/:modified returns the second added group', function (done) { + it('GET /api/groups/:id/modified/:modified returns the second added group', async function () { request(app) .get('/api/groups/' + group2.stix.id + '/modified/' + group2.stix.modified) .set('Accept', 'application/json') @@ -414,7 +370,7 @@ describe('Groups API', function () { }); let group3; - it('POST /api/groups should create a new group with a different stix.id', function (done) { + 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; @@ -470,7 +426,7 @@ describe('Groups API', function () { expect(group).toBeDefined(); }); - it('GET /api/groups uses the search parameter to return the latest version of the group', function (done) { + it('GET /api/groups uses the search parameter to return the latest version of the group', async function () { request(app) .get('/api/groups?search=yellow') .set('Accept', 'application/json') @@ -499,7 +455,7 @@ describe('Groups API', function () { }); }); - it('GET /api/groups should not get the first version of the group when using the search parameter', function (done) { + it('GET /api/groups should not get the first version of the group when using the search parameter', async function () { request(app) .get('/api/groups?search=blue') .set('Accept', 'application/json') @@ -521,7 +477,7 @@ describe('Groups API', function () { }); }); - it('GET /api/groups uses the search parameter to return the group using the name property', function (done) { + it('GET /api/groups uses the search parameter to return the group using the name property', async function () { request(app) .get('/api/groups?search=brown') .set('Accept', 'application/json') @@ -550,7 +506,7 @@ describe('Groups API', function () { }); }); - it('DELETE /api/groups/:id should not delete a group when the id cannot be found', function (done) { + it('DELETE /api/groups/:id should not delete a group when the id cannot be found', async function () { request(app) .delete('/api/groups/not-an-id') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) @@ -565,7 +521,7 @@ describe('Groups API', function () { }); }); - it('DELETE /api/groups/:id/modified/:modified deletes a group', function (done) { + it('DELETE /api/groups/:id/modified/:modified deletes a group', async function () { request(app) .delete('/api/groups/' + group1.stix.id + '/modified/' + group1.stix.modified) .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) @@ -580,7 +536,7 @@ describe('Groups API', function () { }); }); - it('DELETE /api/groups/:id should delete all the groups with the same stix id', function (done) { + it('DELETE /api/groups/:id should delete all the groups with the same stix id', async function () { request(app) .delete('/api/groups/' + group2.stix.id) .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) @@ -595,7 +551,7 @@ describe('Groups API', function () { }); }); - it('DELETE /api/groups/:id/modified/:modified should delete the third group', function (done) { + it('DELETE /api/groups/:id/modified/:modified should delete the third group', async function () { request(app) .delete('/api/groups/' + group3.stix.id + '/modified/' + group3.stix.modified) .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) @@ -610,7 +566,7 @@ describe('Groups API', function () { }); }); - it('GET /api/groups returns an empty array of groups', function (done) { + it('GET /api/groups returns an empty array of groups', async function () { request(app) .get('/api/groups') .set('Accept', 'application/json') From def641b97a9ae2afce4b4ec228b0439d05d0594c Mon Sep 17 00:00:00 2001 From: Sun Date: Thu, 5 Oct 2023 12:57:40 -0400 Subject: [PATCH 04/36] finished refactoring tests --- app/tests/api/groups/groups.spec.js | 345 ++++++++++------------------ 1 file changed, 121 insertions(+), 224 deletions(-) diff --git a/app/tests/api/groups/groups.spec.js b/app/tests/api/groups/groups.spec.js index 1d265723..2bdfccea 100644 --- a/app/tests/api/groups/groups.spec.js +++ b/app/tests/api/groups/groups.spec.js @@ -206,38 +206,23 @@ describe('Groups API', function () { .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 group - const group = res.body; - expect(group).toBeDefined(); - expect(group.stix.id).toBe(group1.stix.id); - expect(group.stix.modified).toBe(group1.stix.modified); - done(); - } - }); + .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; - request(app) + await request(app) .post('/api/groups') .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 group2; @@ -269,104 +254,78 @@ describe('Groups API', function () { }); it('GET /api/groups returns the latest added group', async function () { - request(app) + 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/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // 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); - - done(); - } - }); + .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 () { - request(app) + 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/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // 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); - done(); - } - }); + .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 () { - request(app) + 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/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // 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); - done(); - } - }); + .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 () { - request(app) + 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/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // 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); - done(); - } - }); + .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; @@ -382,24 +341,18 @@ describe('Groups API', function () { group.stix.name = 'Mr. Brown'; group.stix.description = 'This is a new group. Red.'; const body = group; - request(app) + 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/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the created group - group3 = res.body; - expect(group3).toBeDefined(); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get the created group + group3 = res.body; + expect(group3).toBeDefined(); + }); let group4; @@ -427,165 +380,109 @@ describe('Groups API', function () { }); it('GET /api/groups uses the search parameter to return the latest version of the group', async function () { - request(app) + 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/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // 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); - done(); - } - }); + .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 () { - request(app) + await request(app) .get('/api/groups?search=blue') .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 groups in an array - const groups = res.body; - expect(groups).toBeDefined(); - expect(Array.isArray(groups)).toBe(true); - expect(groups.length).toBe(0); - done(); - } - }); + .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 () { - request(app) + await request(app) .get('/api/groups?search=brown') .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 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); - done(); - } - }); + .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 () { - request(app) + await request(app) .delete('/api/groups/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/groups/:id/modified/:modified deletes a group', async function () { - request(app) + const res = await request(app) .delete('/api/groups/' + group1.stix.id + '/modified/' + group1.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/groups/:id should delete all the groups with the same stix id', async function () { - request(app) + const res = await request(app) .delete('/api/groups/' + group2.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/groups/:id/modified/:modified should delete the third group', async function () { - request(app) + const res = await request(app) .delete('/api/groups/' + group3.stix.id + '/modified/' + group3.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/groups returns an empty array of groups', async function () { - request(app) + const res = await request(app) .get('/api/groups') .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 groups = res.body; - expect(groups).toBeDefined(); - expect(Array.isArray(groups)).toBe(true); - expect(groups.length).toBe(0); - done(); - } - }); + .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() { From d771542dccaf0c8461c78846d18ef3729716d7bd Mon Sep 17 00:00:00 2001 From: Sun Date: Thu, 5 Oct 2023 13:46:05 -0400 Subject: [PATCH 05/36] finished refactoring tests for queries --- app/tests/api/groups/groups.query.spec.js | 286 +++++++++------------- 1 file changed, 110 insertions(+), 176 deletions(-) diff --git a/app/tests/api/groups/groups.query.spec.js b/app/tests/api/groups/groups.query.spec.js index 15aa424e..92074c94 100644 --- a/app/tests/api/groups/groups.query.spec.js +++ b/app/tests/api/groups/groups.query.spec.js @@ -176,240 +176,174 @@ describe('Groups API Queries', function () { await loadGroups(groups); }); - it('GET /api/groups should return 3 of the preloaded groups', function (done) { - request(app) + 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/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // 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); - done(); - } - }); + .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)', function (done) { - request(app) + 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/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // 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); - done(); - } - }); + .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', function (done) { - request(app) + 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/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // 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); - done(); - } - }); + .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)', function (done) { - request(app) + 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/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // 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); - done(); - } - }); + .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', function (done) { - request(app) + 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/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // 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); - done(); - } - }); + .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', function (done) { - request(app) + 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/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // 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'); - - done(); - } - }); + .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', function (done) { - request(app) + 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/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the latest group with the correct ATT&CK ID - 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.attack_id).toEqual('G0001'); - - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get the latest group with the correct ATT&CK ID + 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.attack_id).toEqual('G0001'); + }); - it('GET /api/groups should return groups created by userAccount1', function (done) { - request(app) + 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/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // 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); - - done(); - } - }); + .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', function (done) { - request(app) + 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/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // 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); - - done(); - } - }); + .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', function (done) { - request(app) + 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/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // 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); - - done(); - } - }); + .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() { From 496b7c6f5f26a628cb0155a15751cc474823f84e Mon Sep 17 00:00:00 2001 From: Sun Date: Thu, 5 Oct 2023 14:38:37 -0400 Subject: [PATCH 06/36] refactored groups controller --- app/controllers/groups-controller.js | 194 +++++++++++++-------------- 1 file changed, 97 insertions(+), 97 deletions(-) diff --git a/app/controllers/groups-controller.js b/app/controllers/groups-controller.js index b9642af1..1c74d1d4 100644 --- a/app/controllers/groups-controller.js +++ b/app/controllers/groups-controller.js @@ -3,7 +3,7 @@ const groupsService = require('../services/groups-service'); const logger = require('../lib/logger'); -exports.retrieveAll = function(req, res) { +exports.retrieveAll = async function(req, res) { const options = { offset: req.query.offset || 0, limit: req.query.limit || 0, @@ -14,13 +14,9 @@ exports.retrieveAll = function(req, res) { lastUpdatedBy: req.query.lastUpdatedBy, includePagination: req.query.includePagination } + try { + const res = await groupsService.retrieveAll(options); - groupsService.retrieveAll(options, function(err, results) { - if (err) { - logger.error('Failed with error: ' + err); - return res.status(500).send('Unable to get groups. Server error.'); - } - else { if (options.includePagination) { logger.debug(`Success: Retrieved ${ results.data.length } of ${ results.pagination.total } total group(s)`); } @@ -28,75 +24,80 @@ exports.retrieveAll = function(req, res) { 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.'); } - }); + }; -exports.retrieveById = function(req, res) { +exports.retrieveById = async function(req, res) { const options = { versions: req.query.versions || 'latest' } + try { + const groups = await groupsService.retrieveById(req.params.stixId, options); - groupsService.retrieveById(req.params.stixId, options, function (err, groups) { - if (err) { - if (err.message === groupsService.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 === groupsService.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 groups. Server error.'); - } + + if (groups.length === 0) { + return res.status(404).send('Group not found.'); } else { - 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); - } + logger.debug(`Success: Retrieved ${ groups.length } group(s) with id ${ req.params.stixId }`); + return res.status(200).send(groups); } - }); + + } catch (err) { + if (err.message === groupsService.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 === groupsService.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 groups. Server error.'); + } + } + }; -exports.retrieveVersionById = function(req, res) { - groupsService.retrieveVersionById(req.params.stixId, req.params.modified, function (err, group) { - if (err) { - if (err.message === groupsService.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 group. Server error.'); - } - } else { - 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 = 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.message === groupsService.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 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 options = { - import: false, - userAccountId: req.user?.userAccountId - }; const group = await groupsService.create(groupData, options); logger.debug('Success: Created group with id ' + group.stix.id); @@ -118,58 +119,57 @@ exports.create = async function(req, res) { } }; -exports.updateFull = function(req, res) { +exports.updateFull = async function(req, res) { // Get the data from the request const groupData = req.body; - // Create the group - groupsService.updateFull(req.params.stixId, req.params.modified, groupData, function(err, group) { - if (err) { - logger.error("Failed with error: " + err); - return res.status(500).send("Unable to update group. Server error."); - } - else { - 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); - } + 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 = function(req, res) { - groupsService.deleteVersionById(req.params.stixId, req.params.modified, function (err, group) { - if (err) { - logger.error('Delete group failed. ' + err); - return res.status(500).send('Unable to delete group. Server error.'); - } - else { - 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(); - } +exports.deleteVersionById = async function(req, res) { + + try { + const group = 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 = function(req, res) { - groupsService.deleteById(req.params.stixId, function (err, groups) { - if (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 = groupsService.deleteById(req.params.stixId); + if (groups.deletedCount === 0) { + return res.status(404).send('Group not found.'); } else { - 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(); - } + 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.'); + } + }; From 28412f2fa59001d8ae523b8992369575e04ea7f4 Mon Sep 17 00:00:00 2001 From: Sun Date: Thu, 5 Oct 2023 14:41:50 -0400 Subject: [PATCH 07/36] fixing some pieces --- app/services/groups-service.js | 5 ++++- app/tests/api/groups/groups.spec.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/services/groups-service.js b/app/services/groups-service.js index ceb02222..64aa9525 100644 --- a/app/services/groups-service.js +++ b/app/services/groups-service.js @@ -19,10 +19,13 @@ const errors = { }; exports.errors = errors; +const BaseService = require('./_base.service'); +const groupsRepository = require('../repository/groups-repository'); + class GroupsService extends BaseService { constructor() { - super(matrixRepository, Group); + super(groupsRepository, Group); } } diff --git a/app/tests/api/groups/groups.spec.js b/app/tests/api/groups/groups.spec.js index 2bdfccea..4d243145 100644 --- a/app/tests/api/groups/groups.spec.js +++ b/app/tests/api/groups/groups.spec.js @@ -200,7 +200,7 @@ describe('Groups API', function () { group1.stix.modified = timestamp; group1.stix.description = 'This is an updated group. Blue.' const body = group1; - request(app) + const res = await request(app) .put('/api/groups/' + group1.stix.id + '/modified/' + originalModified) .send(body) .set('Accept', 'application/json') From a4fbc5d924d9a7ca0b9b6850300fa45bf162ae5b Mon Sep 17 00:00:00 2001 From: Sun Date: Thu, 5 Oct 2023 14:48:03 -0400 Subject: [PATCH 08/36] added groups exception --- app/controllers/groups-controller.js | 13 +++++++------ app/exceptions/index.js | 7 +++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/controllers/groups-controller.js b/app/controllers/groups-controller.js index 1c74d1d4..cbd9c959 100644 --- a/app/controllers/groups-controller.js +++ b/app/controllers/groups-controller.js @@ -2,6 +2,7 @@ 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 = { @@ -48,11 +49,11 @@ exports.retrieveById = async function(req, res) { } } catch (err) { - if (err.message === groupsService.errors.badlyFormattedParameter) { + 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 === groupsService.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.'); } @@ -67,7 +68,7 @@ exports.retrieveById = async function(req, res) { exports.retrieveVersionById = async function(req, res) { try { - const group = groupsService.retrieveVersionById(req.params.stixId, req.params.modified); + const group = await groupsService.retrieveVersionById(req.params.stixId, req.params.modified); if (!group) { return res.status(404).send('Group not found.'); } @@ -76,7 +77,7 @@ exports.retrieveVersionById = async function(req, res) { return res.status(200).send(group); } } catch (err) { - if (err.message === groupsService.errors.badlyFormattedParameter) { + if (err instanceof BadlyFormattedParameterError) { logger.warn('Badly formatted stix id: ' + req.params.stixId); return res.status(400).send('Stix id is badly formatted.'); } @@ -104,11 +105,11 @@ exports.create = async function(req, res) { return res.status(201).send(group); } catch(err) { - if (err.message === groupsService.errors.duplicateId) { + 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.message === groupsService.errors.invalidType) { + 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'); } diff --git a/app/exceptions/index.js b/app/exceptions/index.js index d940e82d..7d0dab3e 100644 --- a/app/exceptions/index.js +++ b/app/exceptions/index.js @@ -81,6 +81,12 @@ class NotImplementedError extends CustomError { } } +class InvalidTypeError extends CustomError { + constructor(options) { + super('Invalid stix.type', options); + } +} + module.exports = { //** General errors */ @@ -101,4 +107,5 @@ module.exports = { IdentityServiceError, TechniquesServiceError, TacticsServiceError, + InvalidTypeError }; From c93c41f70ef763b9f35ce2ad46600c0fd9868188 Mon Sep 17 00:00:00 2001 From: Sun Date: Thu, 5 Oct 2023 15:28:51 -0400 Subject: [PATCH 09/36] added groups exceptions to group service --- app/services/groups-service.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/app/services/groups-service.js b/app/services/groups-service.js index 64aa9525..3600bc8b 100644 --- a/app/services/groups-service.js +++ b/app/services/groups-service.js @@ -9,15 +9,7 @@ const config = require('../config/config'); const regexValidator = require('../lib/regex'); 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', - invalidType: 'Invalid stix.type' -}; -exports.errors = errors; +const { DuplicateIdError, BadlyFormattedParameterError, InvalidTypeError, InvalidQueryStringParameterError } = require('../exceptions'); const BaseService = require('./_base.service'); const groupsRepository = require('../repository/groups-repository'); From 148a27e8a7a31236cf5d1dcf41707657fe8f878b Mon Sep 17 00:00:00 2001 From: Sun Date: Fri, 6 Oct 2023 11:29:36 -0400 Subject: [PATCH 10/36] fixing one test error --- app/controllers/groups-controller.js | 2 +- app/tests/api/groups/groups.spec.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/groups-controller.js b/app/controllers/groups-controller.js index cbd9c959..9eb6dd74 100644 --- a/app/controllers/groups-controller.js +++ b/app/controllers/groups-controller.js @@ -16,7 +16,7 @@ exports.retrieveAll = async function(req, res) { includePagination: req.query.includePagination } try { - const res = await groupsService.retrieveAll(options); + const results = await groupsService.retrieveAll(options); if (options.includePagination) { logger.debug(`Success: Retrieved ${ results.data.length } of ${ results.pagination.total } total group(s)`); diff --git a/app/tests/api/groups/groups.spec.js b/app/tests/api/groups/groups.spec.js index 4d243145..f54127a8 100644 --- a/app/tests/api/groups/groups.spec.js +++ b/app/tests/api/groups/groups.spec.js @@ -217,7 +217,7 @@ describe('Groups API', function () { it('POST /api/groups does not create a group with the same id and modified date', async function () { const body = group1; - await request(app) + const res = await request(app) .post('/api/groups') .send(body) .set('Accept', 'application/json') From af6dee3369361a22a5b48a679f4514d583698e3e Mon Sep 17 00:00:00 2001 From: Sun Date: Fri, 6 Oct 2023 13:35:02 -0400 Subject: [PATCH 11/36] trying to fix errors --- app/services/groups-service.js | 66 +++++++++++++++++++++++++++++ app/tests/api/groups/groups.spec.js | 4 +- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/app/services/groups-service.js b/app/services/groups-service.js index 3600bc8b..e6a8a265 100644 --- a/app/services/groups-service.js +++ b/app/services/groups-service.js @@ -20,6 +20,72 @@ class GroupsService extends BaseService { super(groupsRepository, Group); } + 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 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. + + if (data.stix.type !== 'intrusion-set') { + throw new InvalidTypeError; + } + + // Create the document + const group = new Group(data); + + options = options || {}; + if (!options.import) { + // Set the ATT&CK Spec Version + group.stix.x_mitre_attack_spec_version = group.stix.x_mitre_attack_spec_version ?? config.app.attackSpecVersion; + + // Record the user account that created the object + if (options.userAccountId) { + group.workspace.workflow.created_by_user_account = options.userAccountId; + } + + // Set the default marking definitions + await attackObjectsService.setDefaultMarkingDefinitions(group); + + // Get the organization identity + const organizationIdentityRef = await systemConfigurationService.retrieveOrganizationIdentityRef(); + + // Check for an existing object + let existingObject; + if (group.stix.id) { + existingObject = await Group.findOne({ 'stix.id': group.stix.id }); + } + + if (existingObject) { + // New version of an existing object + // Only set the x_mitre_modified_by_ref property + group.stix.x_mitre_modified_by_ref = organizationIdentityRef; + } + else { + // New object + // Assign a new STIX id if not already provided + group.stix.id = group.stix.id || `intrusion-set--${uuid.v4()}`; + + // Set the created_by_ref and x_mitre_modified_by_ref properties + group.stix.created_by_ref = organizationIdentityRef; + group.stix.x_mitre_modified_by_ref = organizationIdentityRef; + } + } + + // Save the document in the database + try { + const savedGroup = await group.save(); + return savedGroup; + } + catch (err) { + if (err.name === 'MongoServerError' && err.code === 11000) { + throw new DuplicateIdError; + } + else { + throw err; + } + } + }; } module.exports = new GroupsService; \ No newline at end of file diff --git a/app/tests/api/groups/groups.spec.js b/app/tests/api/groups/groups.spec.js index f54127a8..c4a7171b 100644 --- a/app/tests/api/groups/groups.spec.js +++ b/app/tests/api/groups/groups.spec.js @@ -403,7 +403,7 @@ describe('Groups API', function () { }); it('GET /api/groups should not get the first version of the group when using the search parameter', async function () { - await request(app) + const res = await request(app) .get('/api/groups?search=blue') .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) @@ -419,7 +419,7 @@ describe('Groups API', function () { }); it('GET /api/groups uses the search parameter to return the group using the name property', async function () { - await request(app) + const res = await request(app) .get('/api/groups?search=brown') .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) From a2f09695efb3c3ce9ccb54e1cb91e9ab47740a46 Mon Sep 17 00:00:00 2001 From: Sun Date: Fri, 6 Oct 2023 13:46:18 -0400 Subject: [PATCH 12/36] all tests pass --- app/controllers/groups-controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/groups-controller.js b/app/controllers/groups-controller.js index 9eb6dd74..99251314 100644 --- a/app/controllers/groups-controller.js +++ b/app/controllers/groups-controller.js @@ -143,7 +143,7 @@ exports.updateFull = async function(req, res) { exports.deleteVersionById = async function(req, res) { try { - const group = groupsService.deleteVersionById(req.params.stixId, req.params.modified); + const group = await groupsService.deleteVersionById(req.params.stixId, req.params.modified); if (!group) { return res.status(404).send('Group not found.'); } else { @@ -160,7 +160,7 @@ exports.deleteVersionById = async function(req, res) { exports.deleteById = async function(req, res) { try { - const groups = groupsService.deleteById(req.params.stixId); + const groups = await groupsService.deleteById(req.params.stixId); if (groups.deletedCount === 0) { return res.status(404).send('Group not found.'); } From f561bd99a15330f99486abbf5a0bc031e6364647 Mon Sep 17 00:00:00 2001 From: Sun Date: Thu, 12 Oct 2023 15:20:09 -0400 Subject: [PATCH 13/36] fixing lint issues --- app/services/groups-service.js | 9 ++++----- .../api/groups/groups-input-validation.spec.js | 18 +++++++++--------- app/tests/api/groups/groups.spec.js | 12 ++++++------ 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/app/services/groups-service.js b/app/services/groups-service.js index e6a8a265..056e7eaf 100644 --- a/app/services/groups-service.js +++ b/app/services/groups-service.js @@ -3,13 +3,10 @@ const uuid = require('uuid'); const Group = require('../models/group-model'); const systemConfigurationService = require('./system-configuration-service'); -const identitiesService = require('./identities-service'); const attackObjectsService = require('./attack-objects-service'); const config = require('../config/config'); -const regexValidator = require('../lib/regex'); -const {lastUpdatedByQueryHelper} = require('../lib/request-parameter-helper'); -const { DuplicateIdError, BadlyFormattedParameterError, InvalidTypeError, InvalidQueryStringParameterError } = require('../exceptions'); +const { DuplicateIdError, InvalidTypeError} = require('../exceptions'); const BaseService = require('./_base.service'); const groupsRepository = require('../repository/groups-repository'); @@ -20,7 +17,9 @@ class GroupsService extends BaseService { super(groupsRepository, Group); } + 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 both stix.created_by_ref and stix.x_mitre_modified_by_ref to the organization identity. @@ -85,7 +84,7 @@ class GroupsService extends BaseService { throw err; } } - }; + } } module.exports = new GroupsService; \ No newline at end of file diff --git a/app/tests/api/groups/groups-input-validation.spec.js b/app/tests/api/groups/groups-input-validation.spec.js index e0413aba..96374ae0 100644 --- a/app/tests/api/groups/groups-input-validation.spec.js +++ b/app/tests/api/groups/groups-input-validation.spec.js @@ -43,7 +43,7 @@ function executeTests(getApp, propertyName, options) { groupData.stix.modified = timestamp; _.set(groupData, propertyName, undefined); const body = groupData; - request(getApp()) + await request(getApp()) .post('/api/groups') .send(body) .set('Accept', 'application/json') @@ -58,7 +58,7 @@ function executeTests(getApp, propertyName, options) { groupData.stix.modified = timestamp; _.set(groupData, propertyName, 9); const body = groupData; - request(getApp()) + await request(getApp()) .post('/api/groups') .send(body) .set('Accept', 'application/json') @@ -72,7 +72,7 @@ function executeTests(getApp, propertyName, options) { groupData.stix.modified = timestamp; _.set(groupData, propertyName, { value: 'group-name' }); const body = groupData; - request(getApp()) + await request(getApp()) .post('/api/groups') .send(body) .set('Accept', 'application/json') @@ -104,7 +104,7 @@ describe('Groups API Input Validation', function () { it('POST /api/groups does not create an empty group', async function () { const body = { }; - request(app) + await request(app) .post('/api/groups') .send(body) .set('Accept', 'application/json') @@ -118,7 +118,7 @@ describe('Groups API Input Validation', function () { groupData.stix.created = timestamp; groupData.stix.modified = timestamp; const body = groupData; - request(app) + await request(app) .post('/api/groups?not-a-parameter=unexpectedvalue') .send(body) .set('Accept', 'application/json') @@ -126,14 +126,14 @@ describe('Groups API Input Validation', function () { .expect(400); }); - it('POST /api/groups does not create a group when an invalid type is provided', function () { + 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; - request(app) + await request(app) .post('/api/groups') .send(body) .set('Accept', 'application/json') @@ -141,14 +141,14 @@ describe('Groups API Input Validation', function () { .expect(400); }); - it('POST /api/groups does not create a group when an incorrect type is provided', function () { + 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; - request(app) + await request(app) .post('/api/groups') .send(body) .set('Accept', 'application/json') diff --git a/app/tests/api/groups/groups.spec.js b/app/tests/api/groups/groups.spec.js index c4a7171b..8e7bbdf4 100644 --- a/app/tests/api/groups/groups.spec.js +++ b/app/tests/api/groups/groups.spec.js @@ -106,7 +106,7 @@ describe('Groups API', function () { it('POST /api/groups does not create an empty group', async function () { const body = { }; - const res = await request(app) + await request(app) .post('/api/groups') .send(body) .set('Accept', 'application/json') @@ -160,7 +160,7 @@ describe('Groups API', function () { }); it('GET /api/groups/:id should not return a group when the id cannot be found', async function () { - const res = await request(app) + await request(app) .get('/api/groups/not-an-id') .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) @@ -217,7 +217,7 @@ describe('Groups API', function () { it('POST /api/groups does not create a group with the same id and modified date', async function () { const body = group1; - const res = await request(app) + await request(app) .post('/api/groups') .send(body) .set('Accept', 'application/json') @@ -449,21 +449,21 @@ describe('Groups API', function () { }); it('DELETE /api/groups/:id/modified/:modified deletes a group', async function () { - const res = await request(app) + 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 () { - const res = await request(app) + 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 () { - const res = await request(app) + await request(app) .delete('/api/groups/' + group3.stix.id + '/modified/' + group3.stix.modified) .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(204); From 0bc5596e96672b461d21895d85019c2731abea07 Mon Sep 17 00:00:00 2001 From: Sun Date: Mon, 16 Oct 2023 13:23:50 -0400 Subject: [PATCH 14/36] making create method static --- app/services/groups-service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/groups-service.js b/app/services/groups-service.js index 056e7eaf..9fc7d378 100644 --- a/app/services/groups-service.js +++ b/app/services/groups-service.js @@ -18,7 +18,7 @@ class GroupsService extends BaseService { } - async create(data, options) { + static 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 From 51a27b2caac5498ed4074e8a85425dc4a52da138 Mon Sep 17 00:00:00 2001 From: Sun Date: Mon, 16 Oct 2023 13:28:29 -0400 Subject: [PATCH 15/36] changing group to this.model --- app/services/groups-service.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/services/groups-service.js b/app/services/groups-service.js index 9fc7d378..f0f31a10 100644 --- a/app/services/groups-service.js +++ b/app/services/groups-service.js @@ -18,7 +18,7 @@ class GroupsService extends BaseService { } - static async create(data, options) { + 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 @@ -31,7 +31,7 @@ class GroupsService extends BaseService { } // Create the document - const group = new Group(data); + const group = new this.model(data); options = options || {}; if (!options.import) { @@ -52,7 +52,7 @@ class GroupsService extends BaseService { // Check for an existing object let existingObject; if (group.stix.id) { - existingObject = await Group.findOne({ 'stix.id': group.stix.id }); + existingObject = await this.model.findOne({ 'stix.id': group.stix.id }); } if (existingObject) { From c024f6040c14d8bace2a035930760d8669b3cb60 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Wed, 1 Nov 2023 16:46:15 -0400 Subject: [PATCH 16/36] Merge branch 'project-orion' of https://github.com/center-for-threat-informed-defense/attack-workbench-rest-api into 300-refactor-groups-service --- app/controllers/assets-controller.js | 13 +- .../{attack-prefix-ids.js => model-names.js} | 1 + app/models/asset-model.js | 3 +- app/models/campaign-model.js | 2 +- app/models/collection-model.js | 2 +- app/models/data-component-model.js | 2 +- app/models/data-source-model.js | 2 +- app/models/group-model.js | 2 +- app/models/identity-model.js | 2 +- app/models/marking-definition-model.js | 2 +- app/models/matrix-model.js | 2 +- app/models/note-model.js | 2 +- app/models/relationship-model.js | 2 +- app/models/software-model.js | 2 +- app/models/tactic-model.js | 2 +- app/models/technique-model.js | 2 +- app/repository/assets-repository.js | 13 + app/services/assets-service.js | 345 +----------- app/tests/api/assets/assets.spec.js | 503 +++++++----------- 19 files changed, 224 insertions(+), 680 deletions(-) rename app/lib/{attack-prefix-ids.js => model-names.js} (96%) create mode 100644 app/repository/assets-repository.js diff --git a/app/controllers/assets-controller.js b/app/controllers/assets-controller.js index f4794cad..06d4d45c 100644 --- a/app/controllers/assets-controller.js +++ b/app/controllers/assets-controller.js @@ -2,6 +2,7 @@ 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 = { @@ -50,15 +51,13 @@ exports.retrieveById = async function(req, res) { } } catch(err) { - if (err.message === assetsService.errors.badlyFormattedParameter) { + 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 === assetsService.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.'); - } - else { + } else { logger.error('Failed with error: ' + err); return res.status(500).send('Unable to get assets. Server error.'); } @@ -82,7 +81,7 @@ exports.retrieveVersionById = async function(req, res) { } } catch(err) { - if (err.message === assetsService.errors.badlyFormattedParameter) { + if (err instanceof BadlyFormattedParameterError) { logger.warn('Badly formatted stix id: ' + req.params.stixId); return res.status(400).send('Stix id is badly formatted.'); } @@ -108,7 +107,7 @@ exports.create = async function(req, res) { return res.status(201).send(asset); } catch(err) { - if (err.message === assetsService.errors.duplicateId) { + 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.'); } diff --git a/app/lib/attack-prefix-ids.js b/app/lib/model-names.js similarity index 96% rename from app/lib/attack-prefix-ids.js rename to app/lib/model-names.js index 4f14a337..adf65d7f 100644 --- a/app/lib/attack-prefix-ids.js +++ b/app/lib/model-names.js @@ -6,6 +6,7 @@ * @enum {string} */ exports.ModelName = { + Asset: "Asset", Campaign: "Campaign", Collection: "Collection", DataComponent: "Data-Component", diff --git a/app/models/asset-model.js b/app/models/asset-model.js index 5f0339d0..d2e24124 100644 --- a/app/models/asset-model.js +++ b/app/models/asset-model.js @@ -2,6 +2,7 @@ const mongoose = require('mongoose'); const AttackObject = require('./attack-object-model'); +const { ModelName } = require('../lib/model-names'); const relatedAsset = { name: { type: String, required: true }, @@ -39,6 +40,6 @@ const assetDefinition = { const assetSchema = new mongoose.Schema(assetDefinition); // Create the model -const AssetModel = AttackObject.discriminator('Asset', assetSchema); +const AssetModel = AttackObject.discriminator(ModelName.Asset, assetSchema); module.exports = AssetModel; diff --git a/app/models/campaign-model.js b/app/models/campaign-model.js index a376a1e6..680735af 100644 --- a/app/models/campaign-model.js +++ b/app/models/campaign-model.js @@ -2,7 +2,7 @@ const mongoose = require('mongoose'); const AttackObject = require('./attack-object-model'); -const { ModelName } = require('../lib/attack-prefix-ids'); +const { ModelName } = require('../lib/model-names'); const stixCampaign = { // STIX campaign specific properties diff --git a/app/models/collection-model.js b/app/models/collection-model.js index bc74bf0e..7ee3fa17 100644 --- a/app/models/collection-model.js +++ b/app/models/collection-model.js @@ -3,7 +3,7 @@ const mongoose = require('mongoose'); const AttackObject = require('./attack-object-model'); const workspaceDefinitions = require('./subschemas/workspace'); -const { ModelName } = require('../lib/attack-prefix-ids'); +const { ModelName } = require('../lib/model-names'); const xMitreContent = { object_ref: { type: String, required: true }, diff --git a/app/models/data-component-model.js b/app/models/data-component-model.js index 7e07516c..51e84cbc 100644 --- a/app/models/data-component-model.js +++ b/app/models/data-component-model.js @@ -2,7 +2,7 @@ const mongoose = require('mongoose'); const AttackObject = require('./attack-object-model'); -const { ModelName } = require('../lib/attack-prefix-ids'); +const { ModelName } = require('../lib/model-names'); const stixDataComponent = { // STIX x-mitre-data-component specific properties diff --git a/app/models/data-source-model.js b/app/models/data-source-model.js index fde1f7a5..3f6fa713 100644 --- a/app/models/data-source-model.js +++ b/app/models/data-source-model.js @@ -2,7 +2,7 @@ const mongoose = require('mongoose'); const AttackObject = require('./attack-object-model'); -const { ModelName } = require('../lib/attack-prefix-ids'); +const { ModelName } = require('../lib/model-names'); const stixDataSource = { // STIX x-mitre-data-source specific properties diff --git a/app/models/group-model.js b/app/models/group-model.js index e2a2e218..692d05b1 100644 --- a/app/models/group-model.js +++ b/app/models/group-model.js @@ -2,7 +2,7 @@ const mongoose = require('mongoose'); const AttackObject = require('./attack-object-model'); -const { ModelName } = require('../lib/attack-prefix-ids'); +const { ModelName } = require('../lib/model-names'); const stixIntrusionSet = { // STIX intrusion-set specific properties diff --git a/app/models/identity-model.js b/app/models/identity-model.js index 882bbcd5..743efb31 100644 --- a/app/models/identity-model.js +++ b/app/models/identity-model.js @@ -2,7 +2,7 @@ const mongoose = require('mongoose'); const AttackObject = require('./attack-object-model'); -const { ModelName } = require('../lib/attack-prefix-ids'); +const { ModelName } = require('../lib/model-names'); const identityProperties = { // identity specific properties diff --git a/app/models/marking-definition-model.js b/app/models/marking-definition-model.js index 4fca1f34..4699b18c 100644 --- a/app/models/marking-definition-model.js +++ b/app/models/marking-definition-model.js @@ -2,7 +2,7 @@ const mongoose = require('mongoose'); const AttackObject = require('./attack-object-model'); -const { ModelName } = require('../lib/attack-prefix-ids'); +const { ModelName } = require('../lib/model-names'); const markingObject = { statement: String, diff --git a/app/models/matrix-model.js b/app/models/matrix-model.js index 6bf697ca..c9556583 100644 --- a/app/models/matrix-model.js +++ b/app/models/matrix-model.js @@ -2,7 +2,7 @@ const mongoose = require('mongoose'); const AttackObject = require('./attack-object-model'); -const { ModelName } = require('../lib/attack-prefix-ids'); +const { ModelName } = require('../lib/model-names'); const matrixProperties = { // x-mitre-matrix specific properties diff --git a/app/models/note-model.js b/app/models/note-model.js index f8e1414c..bfbde1e9 100644 --- a/app/models/note-model.js +++ b/app/models/note-model.js @@ -2,7 +2,7 @@ const mongoose = require('mongoose'); const AttackObject = require('./attack-object-model'); -const { ModelName } = require('../lib/attack-prefix-ids'); +const { ModelName } = require('../lib/model-names'); const noteProperties = { // note specific properties diff --git a/app/models/relationship-model.js b/app/models/relationship-model.js index 668d5e5e..d21abef9 100644 --- a/app/models/relationship-model.js +++ b/app/models/relationship-model.js @@ -3,7 +3,7 @@ const mongoose = require('mongoose'); const workspaceDefinitions = require('./subschemas/workspace'); const stixCoreDefinitions = require('./subschemas/stix-core'); -const { ModelName } = require('../lib/attack-prefix-ids'); +const { ModelName } = require('../lib/model-names'); const relationshipProperties = { // relationship specific properties diff --git a/app/models/software-model.js b/app/models/software-model.js index 5324e991..1c1ce411 100644 --- a/app/models/software-model.js +++ b/app/models/software-model.js @@ -2,7 +2,7 @@ const mongoose = require('mongoose'); const AttackObject = require('./attack-object-model'); -const { ModelName } = require('../lib/attack-prefix-ids'); +const { ModelName } = require('../lib/model-names'); const stixMalware = { // STIX malware and tool specific properties diff --git a/app/models/tactic-model.js b/app/models/tactic-model.js index 24545f95..2a7071ba 100644 --- a/app/models/tactic-model.js +++ b/app/models/tactic-model.js @@ -2,7 +2,7 @@ const mongoose = require('mongoose'); const AttackObject = require('./attack-object-model'); -const { ModelName } = require('../lib/attack-prefix-ids'); +const { ModelName } = require('../lib/model-names'); const stixTactic = { // STIX x-mitre-tactic specific properties diff --git a/app/models/technique-model.js b/app/models/technique-model.js index 04ce8e67..d35bdc28 100644 --- a/app/models/technique-model.js +++ b/app/models/technique-model.js @@ -3,7 +3,7 @@ const mongoose = require('mongoose'); const AttackObject = require('./attack-object-model'); const attackPatternDefinitions = require('./subschemas/attack-pattern'); -const { ModelName } = require('../lib/attack-prefix-ids'); +const { ModelName } = require('../lib/model-names'); // Create the definition const techniqueDefinition = { diff --git a/app/repository/assets-repository.js b/app/repository/assets-repository.js new file mode 100644 index 00000000..c6e58305 --- /dev/null +++ b/app/repository/assets-repository.js @@ -0,0 +1,13 @@ +'use strict'; + +const BaseRepository = require('./_base.repository'); +const Asset = require('../models/asset-model'); + +class AssetsRepository extends BaseRepository { + + constructor() { + super(Asset); + } +} + +module.exports = new AssetsRepository(); \ No newline at end of file diff --git a/app/services/assets-service.js b/app/services/assets-service.js index ab0f3782..49bdb277 100644 --- a/app/services/assets-service.js +++ b/app/services/assets-service.js @@ -1,346 +1,15 @@ 'use strict'; -const uuid = require('uuid'); - const Asset = require('../models/asset-model'); +const assetsRepository = require('../repository/assets-repository'); -const attackObjectsService = require('./attack-objects-service'); -const identitiesService = require('./identities-service'); -const systemConfigurationService = require('./system-configuration-service'); - -const config = require('../config/config'); -const regexValidator = require('../lib/regex'); -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; - -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.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 - const results = await Asset.aggregate(aggregation); - - await identitiesService.addCreatedByAndModifiedByIdentitiesToAll(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: results[0].documents - }; - return returnValue; - } - else { - return results[0].documents; - } -}; - -exports.retrieveById = async function(stixId, options) { - // versions=all Retrieve all versions of the asset with the stixId - // versions=latest Retrieve the asset with the latest modified date for this stixId - - if (!stixId) { - const error = new Error(errors.missingParameter); - error.parameterName = 'stixId'; - throw error; - } - - if (options.versions === 'all') { - try { - const assets = await Asset.find({'stix.id': stixId}).lean(); - return assets; - } - catch(err) { - if (err.name === 'CastError') { - const error = new Error(errors.badlyFormattedParameter); - error.parameterName = 'stixId'; - throw error; - } else { - throw err; - } - } - } - else if (options.versions === 'latest') { - try { - const asset = await Asset.findOne({'stix.id': stixId}) - .sort('-stix.modified') - .lean(); - if (asset) { - await identitiesService.addCreatedByAndModifiedByIdentities(asset); - return [ asset ]; - } - else { - return []; - } - } - catch(err) { - if (err.name === 'CastError') { - const error = new Error(errors.badlyFormattedParameter); - error.parameterName = 'stixId'; - throw error; - } - else { - throw err; - } - } - } - else { - const error = new Error(errors.invalidQueryStringParameter); - error.parameterName = 'versions'; - throw error; - } -}; - -exports.retrieveVersionById = async function(stixId, modified, options) { - // Retrieve the version of the asset with the matching stixId and modified date - - if (!stixId) { - const error = new Error(errors.missingParameter); - error.parameterName = 'stixId'; - throw error; - } - - if (!modified) { - const error = new Error(errors.missingParameter); - error.parameterName = 'modified'; - throw error; - } - - try { - const asset = await Asset.findOne({ 'stix.id': stixId, 'stix.modified': modified }); - return asset; - } - catch(err) { - if (err.name === 'CastError') { - const error = new Error(errors.badlyFormattedParameter); - error.parameterName = 'stixId'; - throw error; - } - else { - throw err; - } - - } -}; - -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. +const BaseService = require('./_base.service'); - // Create the document - const asset = new Asset(data); - - options = options || {}; - if (!options.import) { - // Set the ATT&CK Spec Version - asset.stix.x_mitre_attack_spec_version = asset.stix.x_mitre_attack_spec_version ?? config.app.attackSpecVersion; - - // Record the user account that created the object - if (options.userAccountId) { - asset.workspace.workflow.created_by_user_account = options.userAccountId; - } - - // Set the default marking definitions - await attackObjectsService.setDefaultMarkingDefinitions(asset); - - // Get the organization identity - const organizationIdentityRef = await systemConfigurationService.retrieveOrganizationIdentityRef(); - - // Check for an existing object - let existingObject; - if (asset.stix.id) { - existingObject = await Asset.findOne({ 'stix.id': asset.stix.id }); - } - - if (existingObject) { - // New version of an existing object - // Only set the x_mitre_modified_by_ref property - asset.stix.x_mitre_modified_by_ref = organizationIdentityRef; - } - else { - // New object - // Assign a new STIX id if not already provided - asset.stix.id = asset.stix.id || `x-mitre-asset--${uuid.v4()}`; - - // Set the created_by_ref and x_mitre_modified_by_ref properties - asset.stix.created_by_ref = organizationIdentityRef; - asset.stix.x_mitre_modified_by_ref = organizationIdentityRef; - } - } - - // Save the document in the database - try { - const savedAsset = await asset.save(); - return savedAsset; - } - catch (err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - // 11000 = Duplicate index - throw new Error(errors.duplicateId); - } - else { - throw err; - } - } -}; - -exports.updateFull = async function(stixId, stixModified, data) { - if (!stixId) { - const error = new Error(errors.missingParameter); - error.parameterName = 'stixId'; - throw error; - } - - if (!stixModified) { - const error = new Error(errors.missingParameter); - error.parameterName = 'modified'; - throw error; - } - - try { - const asset = await Asset.findOne({'stix.id': stixId, 'stix.modified': stixModified}); - if (!asset) { - // asset not found - return null; - } - else { - // Copy data to found document and save - Object.assign(asset, data); - const savedAsset = await asset.save(); - return savedAsset; - } - } - catch(err) { - if (err.name === 'CastError') { - const error = new Error(errors.badlyFormattedParameter); - error.parameterName = 'stixId'; - throw error; - } - else if (err.name === 'MongoServerError' && err.code === 11000) { - // 11000 = Duplicate index - throw new Error(errors.duplicateId); - } - else { - throw err; - } - } -}; - -exports.deleteById = async function (stixId) { - if (!stixId) { - const error = new Error(errors.missingParameter); - error.parameterName = 'stixId'; - throw error; - } - - const assets = await Asset.deleteMany({ 'stix.id': stixId }); - return assets; -}; - -exports.deleteVersionById = async function (stixId, stixModified) { - if (!stixId) { - const error = new Error(errors.missingParameter); - error.parameterName = 'stixId'; - throw error; - } +class AssetsService extends BaseService { - if (!stixModified) { - const error = new Error(errors.missingParameter); - error.parameterName = 'modified'; - throw error; + constructor() { + super(assetsRepository, Asset); } +} - const asset = await Asset.findOneAndRemove({ 'stix.id': stixId, 'stix.modified': stixModified }); - return asset; -}; +module.exports = new AssetsService(); \ No newline at end of file diff --git a/app/tests/api/assets/assets.spec.js b/app/tests/api/assets/assets.spec.js index 2d2e8f30..69cd231b 100644 --- a/app/tests/api/assets/assets.spec.js +++ b/app/tests/api/assets/assets.spec.js @@ -65,230 +65,164 @@ describe('Assets API', function () { passportCookie = await login.loginAnonymous(app); }); - it('GET /api/assets returns an empty array of assets', function (done) { - request(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 }`) + .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 assets = res.body; - expect(assets).toBeDefined(); - expect(Array.isArray(assets)).toBe(true); - expect(assets.length).toBe(0); - done(); - } - }); + .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', function (done) { + it('POST /api/assets does not create an empty asset', async function () { const body = { }; - request(app) + await request(app) .post('/api/assets') .send(body) .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(400); + }); let asset1; - it('POST /api/assets creates an asset', function (done) { + 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; - request(app) + const res = await request(app) .post('/api/assets') .send(body) .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) + .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 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); - - done(); - } - }); + .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', function (done) { - request(app) + 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 }`) + .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 asset in an array - const assets = res.body; - expect(assets).toBeDefined(); - expect(Array.isArray(assets)).toBe(true); - expect(assets.length).toBe(1); - done(); - } - }); + .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', function (done) { - request(app) + 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) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); + .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', function (done) { - request(app) + 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) - .end(function (err, res) { - if (err) { - done(err); - } else { - done(); - } - }); + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); }); - it('GET /api/assets/:id returns the added asset', function (done) { - request(app) + 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 }`) + .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 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); - - done(); - } - }); + .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', function (done) { + 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; - request(app) + 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 }`) + .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 asset - const asset = res.body; - expect(asset).toBeDefined(); - expect(asset.stix.id).toBe(asset1.stix.id); - expect(asset.stix.modified).toBe(asset1.stix.modified); - done(); - } - }); + .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', function (done) { + it('POST /api/assets does not create an asset with the same id and modified date', async function () { const body = asset1; - request(app) + await request(app) .post('/api/assets') .send(body) .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(409) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); + .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', function (done) { + 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; @@ -296,28 +230,21 @@ describe('Assets API', function () { const timestamp = new Date().toISOString(); asset2.stix.modified = timestamp; const body = asset2; - request(app) + const res = await request(app) .post('/api/assets') .send(body) .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) + .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 asset - const asset = res.body; - expect(asset).toBeDefined(); - done(); - } - }); + .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', function (done) { + 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; @@ -325,184 +252,118 @@ describe('Assets API', function () { const timestamp = new Date().toISOString(); asset3.stix.modified = timestamp; const body = asset3; - request(app) + const res = await request(app) .post('/api/assets') .send(body) .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) + .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 asset - const asset = res.body; - expect(asset).toBeDefined(); - done(); - } - }); + .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', function (done) { - request(app) + 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 }`) + .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 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); - done(); - } - }); + .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', function (done) { - request(app) + 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 }`) + .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 assets in an array - const assets = res.body; - expect(assets).toBeDefined(); - expect(Array.isArray(assets)).toBe(true); - expect(assets.length).toBe(3); - done(); - } - }); + .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', function (done) { - request(app) + 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 }`) + .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 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); - done(); - } - }); + .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', function (done) { - request(app) + 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 }`) + .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 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); - done(); - } - }); + .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', function (done) { - request(app) + 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) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(404); }); - it('DELETE /api/assets/:id/modified/:modified deletes an asset', function (done) { - request(app) + 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) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); }); - it('DELETE /api/assets/:id should delete all the assets with the same stix id', function (done) { - request(app) + 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) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); + .set('Cookie', `${login.passportCookieName}=${passportCookie.value}`) + .expect(204); }); - it('GET /api/assets returns an empty array of assets', function (done) { - request(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 }`) + .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 assets = res.body; - expect(assets).toBeDefined(); - expect(Array.isArray(assets)).toBe(true); - expect(assets.length).toBe(0); - done(); - } - }); + .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() { From 8b9bf305b8aaa9a4c1ac3744017b7ede38166e22 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:50:45 -0400 Subject: [PATCH 17/36] Remove useless constructor from AssetsService; Pass repository and model at point of instantiation rather than in constructor --- app/services/assets-service.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/services/assets-service.js b/app/services/assets-service.js index 49bdb277..3bd0a1c9 100644 --- a/app/services/assets-service.js +++ b/app/services/assets-service.js @@ -1,15 +1,12 @@ 'use strict'; const Asset = require('../models/asset-model'); + const assetsRepository = require('../repository/assets-repository'); const BaseService = require('./_base.service'); -class AssetsService extends BaseService { - constructor() { - super(assetsRepository, Asset); - } -} +class AssetsService extends BaseService { } -module.exports = new AssetsService(); \ No newline at end of file +module.exports = new AssetsService(assetsRepository, Asset); \ No newline at end of file From 51568041e92e5217f830c0c4fc2c5b156d51d10a Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:51:33 -0400 Subject: [PATCH 18/36] Restore missing search match against workspace.attack_id in the $or array in BaseRepository --- app/repository/_base.repository.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/repository/_base.repository.js b/app/repository/_base.repository.js index a1a12062..7bb57139 100644 --- a/app/repository/_base.repository.js +++ b/app/repository/_base.repository.js @@ -53,7 +53,8 @@ class BaseRepository extends AbstractRepository { $match: { $or: [ { 'stix.name': { '$regex': options.search, '$options': 'i' } }, - { 'stix.description': { '$regex': options.search, '$options': 'i' } } + { 'stix.description': { '$regex': options.search, '$options': 'i' } }, + { 'workspace.attack_id': { '$regex': options.search, '$options': 'i' } } ] } }; From ba0ab4b0d47d7ef579d91bc1c0d2803d7a9ae775 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:51:56 -0400 Subject: [PATCH 19/36] Remove useless constructor from AssetsRepository; Pass model at point of instantiation rather than in constructor --- app/repository/assets-repository.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/repository/assets-repository.js b/app/repository/assets-repository.js index c6e58305..e565ce24 100644 --- a/app/repository/assets-repository.js +++ b/app/repository/assets-repository.js @@ -3,11 +3,6 @@ const BaseRepository = require('./_base.repository'); const Asset = require('../models/asset-model'); -class AssetsRepository extends BaseRepository { +class AssetsRepository extends BaseRepository {} - constructor() { - super(Asset); - } -} - -module.exports = new AssetsRepository(); \ No newline at end of file +module.exports = new AssetsRepository(Asset); \ No newline at end of file From 3ab41cc0687dce0261504083567a1b3f339d5074 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:52:15 -0400 Subject: [PATCH 20/36] Pre-stage new CollectionBundlesRepository --- app/repository/collection-bundles.repository.js | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 app/repository/collection-bundles.repository.js diff --git a/app/repository/collection-bundles.repository.js b/app/repository/collection-bundles.repository.js new file mode 100644 index 00000000..b6a91445 --- /dev/null +++ b/app/repository/collection-bundles.repository.js @@ -0,0 +1,7 @@ +const BaseRepository = require("./_base.repository"); + +const Collection = require('../models/collection-model'); + +class CollectionBundlesRepository extends BaseRepository { } + +module.exports = new CollectionBundlesRepository(Collection); \ No newline at end of file From aef8e31afce4502676b1655bfcccde89e8973775 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:52:35 -0400 Subject: [PATCH 21/36] Remove useless constructor from GroupsRepository; Pass model at point of instantiation rather than in constructor --- app/repository/groups-repository.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/repository/groups-repository.js b/app/repository/groups-repository.js index 616fa592..5ff58238 100644 --- a/app/repository/groups-repository.js +++ b/app/repository/groups-repository.js @@ -3,12 +3,6 @@ const BaseRepository = require('./_base.repository'); const Group = require('../models/group-model'); -class GroupsRepository extends BaseRepository { +class GroupsRepository extends BaseRepository { } - constructor() { - super(Group); - - } -} - -module.exports = new GroupsRepository(); \ No newline at end of file +module.exports = new GroupsRepository(Group); \ No newline at end of file From aa7c5f0ba3af005041c7cf9b1132bec36ef01d24 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:52:59 -0400 Subject: [PATCH 22/36] Remove useless constructor from MatrixRepository; Pass model at point of instantiation rather than in constructor --- app/repository/matrix-repository.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/repository/matrix-repository.js b/app/repository/matrix-repository.js index 20e37a62..db14f2fb 100644 --- a/app/repository/matrix-repository.js +++ b/app/repository/matrix-repository.js @@ -3,12 +3,6 @@ const BaseRepository = require('./_base.repository'); const Matrix = require('../models/matrix-model'); -class MatrixRepository extends BaseRepository { +class MatrixRepository extends BaseRepository { } - constructor() { - super(Matrix); - // this.model = Matrix; - } -} - -module.exports = new MatrixRepository(); \ No newline at end of file +module.exports = new MatrixRepository(Matrix); \ No newline at end of file From 1fe09e95887f7b12b5d6db286ce7165cc2b44aab Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:53:12 -0400 Subject: [PATCH 23/36] Pre-stage new NotesRepository --- app/repository/notes.repository.js | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 app/repository/notes.repository.js diff --git a/app/repository/notes.repository.js b/app/repository/notes.repository.js new file mode 100644 index 00000000..b4e35a82 --- /dev/null +++ b/app/repository/notes.repository.js @@ -0,0 +1,7 @@ +const BaseRepository = require('./_base.repository'); + +const Note = require('../models/note-model'); + +class NotesRepository extends BaseRepository { } + +module.exports = new NotesRepository(Note); \ No newline at end of file From 4cee4be4c7a8c0bfccc745160c818ad8fd29db25 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:53:26 -0400 Subject: [PATCH 24/36] Pre-stage new ReferencesRepository --- app/repository/references.repository.js | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 app/repository/references.repository.js diff --git a/app/repository/references.repository.js b/app/repository/references.repository.js new file mode 100644 index 00000000..ef6714c7 --- /dev/null +++ b/app/repository/references.repository.js @@ -0,0 +1,8 @@ +'use strict'; + +const BaseRepository = require('./_base.repository'); +const Reference = require('../models/reference-model'); + +class ReferencesRepository extends BaseRepository { } + +module.exports = new ReferencesRepository(Reference); \ No newline at end of file From 70adbf7578d42128ac6464770489dc0cc67426d0 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:53:56 -0400 Subject: [PATCH 25/36] Remove useless constructor from TeamsRepository; Pass model at point of instantiation rather than in constructor --- app/repository/teams-repository.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/repository/teams-repository.js b/app/repository/teams-repository.js index aad17884..c558e49a 100644 --- a/app/repository/teams-repository.js +++ b/app/repository/teams-repository.js @@ -4,11 +4,6 @@ const BaseRepository = require('./_base.repository'); class TeamsRepository extends BaseRepository { - constructor() { - super(); - this.model = Team; - } - // TODO decouple DB logic; migrate DB logic to DAO/repository class findTeamsByUserId(userAccountId, options) { const aggregation = [ @@ -25,9 +20,9 @@ class TeamsRepository extends BaseRepository { } ]; - return Team.aggregate(aggregation).exec(); + return this.model.aggregate(aggregation).exec(); } } -module.exports = new TeamsRepository(); +module.exports = new TeamsRepository(Team); From 8fa8d05c8e8f6cb83bc72e47a3e2f34f98cf5b58 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:54:20 -0400 Subject: [PATCH 26/36] Remove useless constructor from UserAccountsRepository; Pass model at point of instantiation rather than in constructor --- app/repository/user-accounts-repository.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/repository/user-accounts-repository.js b/app/repository/user-accounts-repository.js index 960f8ac0..3822b015 100644 --- a/app/repository/user-accounts-repository.js +++ b/app/repository/user-accounts-repository.js @@ -4,10 +4,6 @@ const { DatabaseError, DuplicateIdError } = require('../exceptions'); class UserAccountsRepository extends BaseRepository { - constructor() { - super(UserAccount); - } - async retrieveOneByEmail(email) { try { return await this.model.findOne({ 'email': email }).lean().exec(); @@ -58,4 +54,4 @@ class UserAccountsRepository extends BaseRepository { } -module.exports = new UserAccountsRepository(); \ No newline at end of file +module.exports = new UserAccountsRepository(UserAccount); \ No newline at end of file From 7cf5466fa993fef2a9363a9948aefbb3d30d7797 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:55:04 -0400 Subject: [PATCH 27/36] Stop passing Asset model to AssetsService constructor (only need to pass repository) --- app/services/assets-service.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/services/assets-service.js b/app/services/assets-service.js index 3bd0a1c9..6e30ed58 100644 --- a/app/services/assets-service.js +++ b/app/services/assets-service.js @@ -1,7 +1,6 @@ 'use strict'; -const Asset = require('../models/asset-model'); - +// const Asset = require('../models/asset-model'); const assetsRepository = require('../repository/assets-repository'); const BaseService = require('./_base.service'); @@ -9,4 +8,4 @@ const BaseService = require('./_base.service'); class AssetsService extends BaseService { } -module.exports = new AssetsService(assetsRepository, Asset); \ No newline at end of file +module.exports = new AssetsService(assetsRepository); \ No newline at end of file From b421a130bb07b15e53865e41a538e3964c885ba5 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:56:37 -0400 Subject: [PATCH 28/36] Cleanup GroupsService; only override create with new createGroup wrapper to ensure InvalidTypeError thrown when data.stix.type is not intrusion-set --- app/controllers/groups-controller.js | 2 +- app/services/groups-service.js | 84 +++-------------------- app/tests/api/groups/groups.query.spec.js | 5 +- 3 files changed, 13 insertions(+), 78 deletions(-) diff --git a/app/controllers/groups-controller.js b/app/controllers/groups-controller.js index 99251314..b093d3dd 100644 --- a/app/controllers/groups-controller.js +++ b/app/controllers/groups-controller.js @@ -100,7 +100,7 @@ exports.create = async function(req, res) { // Create the group try { - const group = await groupsService.create(groupData, options); + const group = await groupsService.createGroup(groupData, options); logger.debug('Success: Created group with id ' + group.stix.id); return res.status(201).send(group); } diff --git a/app/services/groups-service.js b/app/services/groups-service.js index f0f31a10..6a06a6c5 100644 --- a/app/services/groups-service.js +++ b/app/services/groups-service.js @@ -1,90 +1,22 @@ 'use strict'; -const uuid = require('uuid'); -const Group = require('../models/group-model'); -const systemConfigurationService = require('./system-configuration-service'); -const attackObjectsService = require('./attack-objects-service'); -const config = require('../config/config'); - -const { DuplicateIdError, InvalidTypeError} = require('../exceptions'); - const BaseService = require('./_base.service'); const groupsRepository = require('../repository/groups-repository'); +const { InvalidTypeError } = require('../exceptions'); class GroupsService extends BaseService { - constructor() { - super(groupsRepository, Group); - - } + createGroup(data, options) { - async create(data, options) { + // Overrides the base method for groups to inject an additional + // logic check to verify that the creation request is not for an + // intrusion-set, which is an unsupported/invalid use case. - // 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. - if (data.stix.type !== 'intrusion-set') { - throw new InvalidTypeError; - } - - // Create the document - const group = new this.model(data); - - options = options || {}; - if (!options.import) { - // Set the ATT&CK Spec Version - group.stix.x_mitre_attack_spec_version = group.stix.x_mitre_attack_spec_version ?? config.app.attackSpecVersion; - - // Record the user account that created the object - if (options.userAccountId) { - group.workspace.workflow.created_by_user_account = options.userAccountId; - } - - // Set the default marking definitions - await attackObjectsService.setDefaultMarkingDefinitions(group); - - // Get the organization identity - const organizationIdentityRef = await systemConfigurationService.retrieveOrganizationIdentityRef(); - - // Check for an existing object - let existingObject; - if (group.stix.id) { - existingObject = await this.model.findOne({ 'stix.id': group.stix.id }); - } - - if (existingObject) { - // New version of an existing object - // Only set the x_mitre_modified_by_ref property - group.stix.x_mitre_modified_by_ref = organizationIdentityRef; - } - else { - // New object - // Assign a new STIX id if not already provided - group.stix.id = group.stix.id || `intrusion-set--${uuid.v4()}`; - - // Set the created_by_ref and x_mitre_modified_by_ref properties - group.stix.created_by_ref = organizationIdentityRef; - group.stix.x_mitre_modified_by_ref = organizationIdentityRef; - } - } - - // Save the document in the database - try { - const savedGroup = await group.save(); - return savedGroup; - } - catch (err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - throw new DuplicateIdError; - } - else { - throw err; - } + throw new InvalidTypeError(); } + return this.create(data, options); } } -module.exports = new GroupsService; \ No newline at end of file +module.exports = new GroupsService(groupsRepository); \ No newline at end of file diff --git a/app/tests/api/groups/groups.query.spec.js b/app/tests/api/groups/groups.query.spec.js index 92074c94..6d06c5d3 100644 --- a/app/tests/api/groups/groups.query.spec.js +++ b/app/tests/api/groups/groups.query.spec.js @@ -127,7 +127,7 @@ async function loadGroups(groups) { } // eslint-disable-next-line no-await-in-loop - await groupsService.create(group, { import: false, userAccountId: group.userAccountId }); + await groupsService.createGroup(group, { import: false, userAccountId: group.userAccountId }); } } @@ -285,11 +285,14 @@ describe('Groups API Queries', function () { // 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'); }); From f21b3c1284c56d5cf63d0886dce9e1aab283f772 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:58:18 -0400 Subject: [PATCH 29/36] Refactor to support backwards compatibility/dual support for callbacks & promises --- app/services/_base.service.js | 207 ++++++++++++++++++++++++++----- app/services/matrices-service.js | 42 +++++-- 2 files changed, 209 insertions(+), 40 deletions(-) diff --git a/app/services/_base.service.js b/app/services/_base.service.js index 6c45aa5d..05c16a86 100644 --- a/app/services/_base.service.js +++ b/app/services/_base.service.js @@ -16,7 +16,10 @@ class BaseService extends AbstractService { constructor(repository, model) { super(); this.repository = repository; - this.model = model; + + // Helper function to determine if the last argument is a callback + static isCallback(arg) { + return typeof arg === 'function'; } static paginate(options, results) { @@ -38,32 +41,57 @@ class BaseService extends AbstractService { } } - async retrieveAll(options) { + async retrieveAll(options, callback) { + if (BaseService.isCallback(arguments[arguments.length - 1])) { + callback = arguments[arguments.length - 1]; + } + let results; try { results = await this.repository.retrieveAll(options); } catch (err) { - throw new DatabaseError(err); // Let the DatabaseError bubble up + const databaseError = new DatabaseError(err); // Let the DatabaseError buddle up + if (callback) { + return callback(databaseError); + } + throw databaseError; } try { await identitiesService.addCreatedByAndModifiedByIdentitiesToAll(results[0].documents); } catch (err) { - throw new IdentityServiceError({ + const identityError = new IdentityServiceError({ details: err.message, cause: err }); + if (callback) { + return callback(identityError); + } + throw identityError; + + } + + const paginatedResults = BaseService.paginate(options, results); + if (callback) { + return callback(null, paginatedResults); } + return paginatedResults; - return BaseService.paginate(options, results); } - async retrieveById(stixId, options) { + async retrieveById(stixId, options, callback) { + if (BaseService.isCallback(arguments[arguments.length - 1])) { + callback = arguments[arguments.length - 1]; + } + if (!stixId) { - throw new MissingParameterError({ parameterName: 'stixId' }); + const err = new MissingParameterError({ parameterName: 'stixId' }); + if (callback) { + return callback(err); + } + throw err; } - // eslint-disable-next-line no-useless-catch try { if (options.versions === 'all') { const documents = await this.repository.retrieveAllById(stixId); @@ -71,12 +99,18 @@ class BaseService extends AbstractService { try { await identitiesService.addCreatedByAndModifiedByIdentitiesToAll(documents); } catch (err) { - throw new IdentityServiceError({ + 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') { @@ -86,32 +120,59 @@ class BaseService extends AbstractService { try { await identitiesService.addCreatedByAndModifiedByIdentities(document); } catch (err) { - throw new IdentityServiceError({ + 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 { - throw new InvalidQueryStringParameterError({ parameterName: 'versions' }); + const err = new InvalidQueryStringParameterError({ parameterName: 'versions' }); + if (callback) { + return callback(err); + } + throw err; } } catch (err) { + if (callback) { + return callback(err); + } throw err; // Let the DatabaseError bubble up } } - async retrieveVersionById(stixId, modified) { + async retrieveVersionById(stixId, modified, callback) { + if (BaseService.isCallback(arguments[arguments.length - 1])) { + callback = arguments[arguments.length - 1]; + } if (!stixId) { - throw new MissingParameterError({ parameterName: 'stixId' }); + const err = new MissingParameterError({ parameterName: 'stixId' }); + if (callback) { + return callback(err); + } + throw err; } if (!modified) { - throw new MissingParameterError({ parameterName: 'modified' }); + const err = new MissingParameterError({ parameterName: 'modified' }); + if (callback) { + return callback(err); + } + throw err; } // eslint-disable-next-line no-useless-catch @@ -120,24 +181,40 @@ class BaseService extends AbstractService { if (!document) { console.log('** NOT FOUND'); + if (callback) { + return callback(null, null); + } return null; } else { try { await identitiesService.addCreatedByAndModifiedByIdentities(document); } catch (err) { - throw new IdentityServiceError({ + 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 create(data, options) { + async create(data, options, callback) { + if (BaseService.isCallback(arguments[arguments.length - 1])) { + callback = arguments[arguments.length - 1]; + } // eslint-disable-next-line no-useless-catch try { // This function handles two use cases: @@ -186,20 +263,37 @@ class BaseService extends AbstractService { data.stix.x_mitre_modified_by_ref = organizationIdentityRef; } } - return await this.repository.save(data); + 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) { - + async updateFull(stixId, stixModified, data, callback) { + if (BaseService.isCallback(arguments[arguments.length - 1])) { + callback = arguments[arguments.length - 1]; + } if (!stixId) { - throw new MissingParameterError({ parameterName: 'stixId' }); + const err = new MissingParameterError({ parameterName: 'stixId' }); + if (callback) { + return callback(err); + } + throw err; } if (!stixModified) { - throw new MissingParameterError({ parameterName: 'modified' }); + const err = new MissingParameterError({ parameterName: 'modified' }); + if (callback) { + return callback(err); + } + throw err; } let document; @@ -207,37 +301,64 @@ class BaseService extends AbstractService { try { document = await this.repository.retrieveOneByVersion(stixId, stixModified); } catch (err) { + if (callback) { + return callback(err); + } throw err; } if (!document) { + if (callback) { + return callback(null, null); + } return null; } - // eslint-disable-next-line no-useless-catch try { const newDocument = await this.repository.updateAndSave(document, data); if (newDocument === document) { // Document successfully saved + if (callback) { + return callback(null, newDocument); + } return newDocument; } else { - throw new DatabaseError({ + 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; } } - async deleteVersionById(stixId, stixModified) { + async deleteVersionById(stixId, stixModified, callback) { + if (BaseService.isCallback(arguments[arguments.length - 1])) { + callback = arguments[arguments.length - 1]; + } if (!stixId) { - throw new MissingParameterError({ parameterName: 'stixId' }); + const err = new MissingParameterError({ parameterName: 'stixId' }); + if (callback) { + return callback(err); + } + throw err; } + if (!stixModified) { - throw new MissingParameterError({ parameterName: 'modified' }); + const err = new MissingParameterError({ parameterName: 'modified' }); + if (callback) { + return callback(err); + } + throw err; } // eslint-disable-next-line no-useless-catch try { @@ -245,27 +366,49 @@ class BaseService extends AbstractService { 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) { + async deleteById(stixId, callback) { + if (BaseService.isCallback(arguments[arguments.length - 1])) { + callback = arguments[arguments.length - 1]; + } if (!stixId) { - return new MissingParameterError({ parameterName: 'stixId' }); + const err = new MissingParameterError({ parameterName: 'stixId' }); + if (callback) { + return callback(err); + } + throw err; } // eslint-disable-next-line no-useless-catch try { - return await this.repository.deleteMany(stixId); + 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 diff --git a/app/services/matrices-service.js b/app/services/matrices-service.js index 1b2435b4..c525a37c 100644 --- a/app/services/matrices-service.js +++ b/app/services/matrices-service.js @@ -3,7 +3,7 @@ const util = require('util'); const { GenericServiceError, MissingParameterError } = require('../exceptions'); -const Matrix = require('../models/matrix-model'); +// const Matrix = require('../models/matrix-model'); const matrixRepository = require('../repository/matrix-repository'); const BaseService = require('./_base.service'); @@ -11,7 +11,7 @@ const BaseService = require('./_base.service'); class MatrixService extends BaseService { constructor() { - super(matrixRepository, Matrix); + super(matrixRepository); this.retrieveTacticById = null; this.retrieveTechniquesForTactic = null; @@ -19,7 +19,12 @@ class MatrixService extends BaseService { // Custom methods specific to MatrixService should be specified below - async retrieveTechniquesForMatrix(stixId, modified) { + async retrieveTechniquesForMatrix(stixId, modified, callback) { + + if (BaseService.isCallback(arguments[arguments.length - 1])) { + callback = arguments[arguments.length - 1]; + } + // Lazy loading of services if (!this.retrieveTacticById || !this.retrieveTechniquesForTactic) { const tacticsService = require('./tactics-service'); @@ -28,10 +33,19 @@ class MatrixService extends BaseService { } if (!stixId) { - throw new MissingParameterError({ parameterName: 'stixId' }); + const err = new MissingParameterError({ parameterName: 'stixId' }); + if (callback) { + return callback(err); + } + throw err; } + if (!modified) { - throw new MissingParameterError({ parameterName: 'modified' }); + const err = new MissingParameterError({ parameterName: 'modified' }); + if (callback) { + return callback(err); + } + throw err; } let matrix; @@ -39,10 +53,16 @@ class MatrixService extends BaseService { try { matrix = await matrixRepository.retrieveOneByVersion(stixId, modified); } catch (err) { + if (callback) { + return callback(err); + } throw err; // Let the DatabaseError bubble up } if (!matrix) { + if (callback) { + return callback(null, null); + } return null; } @@ -57,7 +77,11 @@ class MatrixService extends BaseService { techniques = await this.retrieveTechniquesForTactic(tacticId, tactics[0].stix.modified, options); } } catch (err) { - throw new GenericServiceError(err); // TODO it's probably better to throw TechniquesServiceError or TacticsServiceError + 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) { @@ -85,9 +109,11 @@ class MatrixService extends BaseService { tacticsTechniques[tactic.stix.name] = tactic; } } - + if (callback) { + return callback(null, tacticsTechniques); + } return tacticsTechniques; } } -module.exports = new MatrixService(); \ No newline at end of file +module.exports = new MatrixService(matrixRepository); \ No newline at end of file From b4ebdda1ab21c1e6b54c59e1b147d2f1d569ee4c Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:58:54 -0400 Subject: [PATCH 30/36] Remove model from BaseService constructor args --- app/services/_base.service.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/services/_base.service.js b/app/services/_base.service.js index 05c16a86..44814f0e 100644 --- a/app/services/_base.service.js +++ b/app/services/_base.service.js @@ -13,9 +13,10 @@ const AbstractService = require('./_abstract.service'); class BaseService extends AbstractService { - constructor(repository, model) { + constructor(repository) { super(); this.repository = repository; + } // Helper function to determine if the last argument is a callback static isCallback(arg) { From 6548b6e057f4e990260b9a1c0cdc26a44aa34c34 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 2 Nov 2023 11:59:42 -0400 Subject: [PATCH 31/36] Fix bug in BaseService.create where retrieveAllById was being called instead of retrieveOneById --- app/services/_base.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/_base.service.js b/app/services/_base.service.js index 44814f0e..1e599bca 100644 --- a/app/services/_base.service.js +++ b/app/services/_base.service.js @@ -243,7 +243,7 @@ class BaseService extends AbstractService { // Check for an existing object let existingObject; if (data.stix.id) { - existingObject = await this.repository.retrieveAllById(data.stix.id); + existingObject = await this.repository.retrieveOneById(data.stix.id); } if (existingObject) { From d47ef2baffb5690f052e74ded4b11ea3c48185ad Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 2 Nov 2023 12:02:20 -0400 Subject: [PATCH 32/36] Temporarily cease passing options in AssetsController call to assetsService.retrieveVersionById because it appears to be unused/useless --- app/controllers/assets-controller.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/controllers/assets-controller.js b/app/controllers/assets-controller.js index 06d4d45c..c2fc14e7 100644 --- a/app/controllers/assets-controller.js +++ b/app/controllers/assets-controller.js @@ -66,12 +66,15 @@ exports.retrieveById = async function(req, res) { }; exports.retrieveVersionById = async function(req, res) { - const options = { - retrieveDataComponents: req.query.retrieveDataComponents - } + + // 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, options); + const asset = await assetsService.retrieveVersionById(req.params.stixId, req.params.modified); if (!asset) { return res.status(404).send('Asset not found.'); } From 79857333093e3c807f204d365d7263d4e11ab29a Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 2 Nov 2023 12:03:15 -0400 Subject: [PATCH 33/36] Add VS Code launch configuration that runs the regression tests to make debugging tests easier --- .vscode/launch.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index f8518282..90b62f3c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,6 +14,23 @@ "program": "${workspaceFolder}/bin/www", "outputCapture": "std", "envFile": "${workspaceFolder}/.env" + }, + { + "type": "node", + "request": "launch", + "name": "Run Regression Tests", + "skipFiles": [ + "/**" + ], + "runtimeExecutable": "npm", + "args": [ + "run", + "test" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "outputCapture": "std", + "envFile": "${workspaceFolder}/.env" } ] } \ No newline at end of file From aacfee4896c0305196757e6032b3f84aac2208a3 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Thu, 2 Nov 2023 12:15:50 -0400 Subject: [PATCH 34/36] Pass repository to MatrixService via constructor rather than by injecting via global module import --- app/services/matrices-service.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/services/matrices-service.js b/app/services/matrices-service.js index c525a37c..07020f42 100644 --- a/app/services/matrices-service.js +++ b/app/services/matrices-service.js @@ -3,15 +3,14 @@ const util = require('util'); const { GenericServiceError, MissingParameterError } = require('../exceptions'); -// const Matrix = require('../models/matrix-model'); const matrixRepository = require('../repository/matrix-repository'); const BaseService = require('./_base.service'); class MatrixService extends BaseService { - constructor() { - super(matrixRepository); + constructor(repository) { + super(repository); this.retrieveTacticById = null; this.retrieveTechniquesForTactic = null; From a5fc4e7029ba48c6d55deb94916e228c6c72f9e2 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Mon, 6 Nov 2023 15:23:19 -0500 Subject: [PATCH 35/36] Add 'type' member variable to BaseService and integrate object STIX type validation check to 'create' method --- app/controllers/groups-controller.js | 2 +- app/services/_base.service.js | 11 +++++++++-- app/services/assets-service.js | 3 +-- app/services/groups-service.js | 18 ++---------------- app/services/matrices-service.js | 6 +++--- app/tests/api/groups/groups.query.spec.js | 2 +- 6 files changed, 17 insertions(+), 25 deletions(-) diff --git a/app/controllers/groups-controller.js b/app/controllers/groups-controller.js index b093d3dd..99251314 100644 --- a/app/controllers/groups-controller.js +++ b/app/controllers/groups-controller.js @@ -100,7 +100,7 @@ exports.create = async function(req, res) { // Create the group try { - const group = await groupsService.createGroup(groupData, options); + const group = await groupsService.create(groupData, options); logger.debug('Success: Created group with id ' + group.stix.id); return res.status(201).send(group); } diff --git a/app/services/_base.service.js b/app/services/_base.service.js index 1e599bca..41a907fb 100644 --- a/app/services/_base.service.js +++ b/app/services/_base.service.js @@ -8,13 +8,15 @@ const config = require('../config/config'); const { DatabaseError, IdentityServiceError, MissingParameterError, - InvalidQueryStringParameterError } = require('../exceptions'); + InvalidQueryStringParameterError, + InvalidTypeError } = require('../exceptions'); const AbstractService = require('./_abstract.service'); class BaseService extends AbstractService { - constructor(repository) { + constructor(type, repository) { super(); + this.type = type; this.repository = repository; } @@ -216,6 +218,11 @@ class BaseService extends AbstractService { if (BaseService.isCallback(arguments[arguments.length - 1])) { callback = arguments[arguments.length - 1]; } + + if (data?.stix?.type !== this.type) { + throw new InvalidTypeError(); + } + // eslint-disable-next-line no-useless-catch try { // This function handles two use cases: diff --git a/app/services/assets-service.js b/app/services/assets-service.js index 6e30ed58..d9f6c52a 100644 --- a/app/services/assets-service.js +++ b/app/services/assets-service.js @@ -5,7 +5,6 @@ const assetsRepository = require('../repository/assets-repository'); const BaseService = require('./_base.service'); - class AssetsService extends BaseService { } -module.exports = new AssetsService(assetsRepository); \ No newline at end of file +module.exports = new AssetsService('x-mitre-asset', assetsRepository); \ No newline at end of file diff --git a/app/services/groups-service.js b/app/services/groups-service.js index 6a06a6c5..0a8b098e 100644 --- a/app/services/groups-service.js +++ b/app/services/groups-service.js @@ -2,21 +2,7 @@ const BaseService = require('./_base.service'); const groupsRepository = require('../repository/groups-repository'); -const { InvalidTypeError } = require('../exceptions'); -class GroupsService extends BaseService { +class GroupsService extends BaseService { } - createGroup(data, options) { - - // Overrides the base method for groups to inject an additional - // logic check to verify that the creation request is not for an - // intrusion-set, which is an unsupported/invalid use case. - - if (data.stix.type !== 'intrusion-set') { - throw new InvalidTypeError(); - } - return this.create(data, options); - } -} - -module.exports = new GroupsService(groupsRepository); \ No newline at end of file +module.exports = new GroupsService('intrusion-set', groupsRepository); \ No newline at end of file diff --git a/app/services/matrices-service.js b/app/services/matrices-service.js index 07020f42..858a2e18 100644 --- a/app/services/matrices-service.js +++ b/app/services/matrices-service.js @@ -9,8 +9,8 @@ const BaseService = require('./_base.service'); class MatrixService extends BaseService { - constructor(repository) { - super(repository); + constructor(type, repository) { + super(type, repository); this.retrieveTacticById = null; this.retrieveTechniquesForTactic = null; @@ -115,4 +115,4 @@ class MatrixService extends BaseService { } } -module.exports = new MatrixService(matrixRepository); \ No newline at end of file +module.exports = new MatrixService('x-mitre-matrix', matrixRepository); \ No newline at end of file diff --git a/app/tests/api/groups/groups.query.spec.js b/app/tests/api/groups/groups.query.spec.js index 6d06c5d3..7d6210b8 100644 --- a/app/tests/api/groups/groups.query.spec.js +++ b/app/tests/api/groups/groups.query.spec.js @@ -127,7 +127,7 @@ async function loadGroups(groups) { } // eslint-disable-next-line no-await-in-loop - await groupsService.createGroup(group, { import: false, userAccountId: group.userAccountId }); + await groupsService.create(group, { import: false, userAccountId: group.userAccountId }); } } From 3fa882a2efb8314c2a6c9364b63be2a33cbdbe98 Mon Sep 17 00:00:00 2001 From: Sean Sica <23294618+seansica@users.noreply.github.com> Date: Mon, 6 Nov 2023 15:25:22 -0500 Subject: [PATCH 36/36] Ensure the 'import' option is set to true when callbacks are passed to service.create in the importBundle function within collection-bundles.service --- app/services/collection-bundles-service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/collection-bundles-service.js b/app/services/collection-bundles-service.js index d533c105..771f5bee 100644 --- a/app/services/collection-bundles-service.js +++ b/app/services/collection-bundles-service.js @@ -464,7 +464,7 @@ exports.importBundle = function(collection, data, options, callback) { }); } else { - service.create(newObject, function (err, savedObject) { + service.create(newObject, { import: true }, function (err, savedObject) { if (err) { if (err.message === service.errors.duplicateId) { // We've checked for this already, so this shouldn't occur