-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* separate recent activity endpoint * recent activity service
- Loading branch information
Showing
5 changed files
with
262 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
paths: | ||
/api/recent-activity: | ||
get: | ||
summary: 'Get a list of all recent activity' | ||
operationId: 'get-all-recent-activity' | ||
description: | | ||
This endpoint gets a list of all ATT&CK objects and relationships by their modified date. | ||
tags: | ||
- 'Recent Activity' | ||
parameters: | ||
- name: limit | ||
in: query | ||
description: | | ||
The number of objects to retrieve. | ||
The default (0) will retrieve all objects. | ||
schema: | ||
type: number | ||
default: 0 | ||
- name: offset | ||
in: query | ||
description: | | ||
The number of objects to skip. | ||
The default (0) will start with the first object. | ||
schema: | ||
type: number | ||
default: 0 | ||
- name: includeRevoked | ||
in: query | ||
description: | | ||
Whether to include objects that have the `revoked` property set to true. | ||
schema: | ||
type: boolean | ||
default: true | ||
- name: includeDeprecated | ||
in: query | ||
description: | | ||
Whether to include objects that have the `x_mitre_deprecated` property set to true. | ||
schema: | ||
type: boolean | ||
default: true | ||
- name: lastUpdatedBy | ||
in: query | ||
description: | | ||
The STIX ID of the user who last modified the object | ||
schema: | ||
oneOf: | ||
- type: string | ||
- type: array | ||
items: | ||
type: string | ||
example: 'identity--f568ad89-69bc-48e7-877b-43755f1d376d' | ||
- name: includePagination | ||
in: query | ||
description: | | ||
Whether to include pagination data in the returned value. | ||
Wraps returned objects in a larger object. | ||
schema: | ||
type: boolean | ||
default: false | ||
responses: | ||
'200': | ||
description: 'A list of ATT&CK Objects and Relationships.' | ||
content: | ||
application/json: | ||
schema: | ||
type: array | ||
items: | ||
anyOf: | ||
- $ref: '../components/collections.yml#/components/schemas/collection' | ||
- $ref: '../components/groups.yml#/components/schemas/group' | ||
- $ref: '../components/identities.yml#/components/schemas/identity' | ||
- $ref: '../components/marking-definitions.yml#/components/schemas/marking-definition' | ||
- $ref: '../components/matrices.yml#/components/schemas/matrix' | ||
- $ref: '../components/mitigations.yml#/components/schemas/mitigation' | ||
- $ref: '../components/relationships.yml#/components/schemas/relationship' | ||
- $ref: '../components/software.yml#/components/schemas/software' | ||
- $ref: '../components/tactics.yml#/components/schemas/tactic' | ||
- $ref: '../components/techniques.yml#/components/schemas/technique' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
'use strict'; | ||
|
||
const recentActivityService = require('../services/recent-activity-service'); | ||
const logger = require('../lib/logger'); | ||
|
||
exports.retrieveAll = async function(req, res) { | ||
const options = { | ||
offset: req.query.offset || 0, | ||
limit: req.query.limit || 0, | ||
includeRevoked: req.query.includeRevoked, | ||
includeDeprecated: req.query.includeDeprecated, | ||
lastUpdatedBy: req.query.lastUpdatedBy, | ||
includePagination: req.query.includePagination, | ||
} | ||
|
||
try { | ||
const results = await recentActivityService.retrieveAll(options); | ||
|
||
if (options.includePagination) { | ||
logger.debug(`Success: Retrieved ${results.data.length} of ${results.pagination.total} total ATT&CK object(s)`); | ||
} | ||
else { | ||
logger.debug(`Success: Retrieved ${results.length} ATT&CK object(s)`); | ||
} | ||
|
||
return res.status(200).send(results); | ||
} | ||
catch (err) { | ||
logger.error('Failed with error: ' + err); | ||
return res.status(500).send('Unable to get ATT&CK objects. Server error.'); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
'use strict'; | ||
|
||
const express = require('express'); | ||
|
||
const recentActivityController = require('../controllers/recent-activity-controller'); | ||
const authn = require('../lib/authn-middleware'); | ||
const authz = require('../lib/authz-middleware'); | ||
|
||
const router = express.Router(); | ||
|
||
router.route('/recent-activity') | ||
.get( | ||
authn.authenticate, | ||
authz.requireRole(authz.visitorOrHigher, authz.readOnlyService), | ||
recentActivityController.retrieveAll | ||
); | ||
|
||
module.exports = router; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
'use strict'; | ||
|
||
const AttackObject = require('../models/attack-object-model'); | ||
const Relationship = require('../models/relationship-model'); | ||
const identitiesService = require('./identities-service'); | ||
|
||
const logger = require('../lib/logger'); | ||
|
||
const { lastUpdatedByQueryHelper } = require('../lib/request-parameter-helper'); | ||
|
||
const errors = { | ||
missingParameter: 'Missing required parameter', | ||
badlyFormattedParameter: 'Badly formatted parameter', | ||
duplicateId: 'Duplicate id', | ||
notFound: 'Document not found', | ||
invalidQueryStringParameter: 'Invalid query string parameter', | ||
duplicateCollection: 'Duplicate collection' | ||
}; | ||
exports.errors = errors; | ||
|
||
exports.retrieveAll = async function(options) { | ||
// Build the query | ||
const query = {}; | ||
if (!options.includeRevoked) { | ||
query['stix.revoked'] = { $in: [null, false] }; | ||
} | ||
if (!options.includeDeprecated) { | ||
query['stix.x_mitre_deprecated'] = { $in: [null, false] }; | ||
} | ||
if (typeof options.lastUpdatedBy !== 'undefined') { | ||
query['workspace.workflow.created_by_user_account'] = lastUpdatedByQueryHelper(options.lastUpdatedBy); | ||
} | ||
|
||
// Filter out objects without modified dates (incl. Marking Definitions & Identities) | ||
query['stix.modified'] = { $exists: true }; | ||
|
||
// Build the aggregation | ||
const aggregation = []; | ||
|
||
// Sort objects by last modified | ||
aggregation.push({ $sort: { 'stix.modified': -1 } }); | ||
|
||
// Limit documents to prevent memory issues | ||
const limit = options.limit ?? 0; | ||
if (limit) { | ||
aggregation.push({ $limit: limit }); | ||
} | ||
|
||
// Then apply query, skip and limit options | ||
aggregation.push({ $match: query }); | ||
|
||
// Retrieve the documents | ||
let objectDocuments = await AttackObject.aggregate(aggregation); | ||
|
||
// Lookup source/target refs for relationships | ||
aggregation.push({ | ||
$lookup: { | ||
from: 'attackObjects', | ||
localField: 'stix.source_ref', | ||
foreignField: 'stix.id', | ||
as: 'source_objects' | ||
} | ||
}); | ||
aggregation.push({ | ||
$lookup: { | ||
from: 'attackObjects', | ||
localField: 'stix.target_ref', | ||
foreignField: 'stix.id', | ||
as: 'target_objects' | ||
} | ||
}); | ||
let relationshipDocuments = await Relationship.aggregate(aggregation); | ||
let documents = objectDocuments.concat(relationshipDocuments); | ||
|
||
// Sort by most recent | ||
documents.sort((a, b) => b.stix.modified - a.stix.modified); | ||
|
||
// Move latest source and target objects to a non-array property, then remove array of source and target objects | ||
for (const document of documents) { | ||
if (Array.isArray(document.source_objects)) { | ||
if (document.source_objects.length === 0) { | ||
document.source_objects = undefined; | ||
} | ||
else { | ||
document.source_object = document.source_objects[0]; | ||
document.source_objects = undefined; | ||
} | ||
} | ||
|
||
if (Array.isArray(document.target_objects)) { | ||
if (document.target_objects.length === 0) { | ||
document.target_objects = undefined; | ||
} | ||
else { | ||
document.target_object = document.target_objects[0]; | ||
document.target_objects = undefined; | ||
} | ||
} | ||
} | ||
|
||
// Apply pagination | ||
const offset = options.offset ?? 0; | ||
let paginatedDocuments; | ||
if (limit > 0) { | ||
paginatedDocuments = documents.slice(offset, offset + limit); | ||
} | ||
else { | ||
paginatedDocuments = documents.slice(offset); | ||
} | ||
|
||
// Add identities | ||
await identitiesService.addCreatedByAndModifiedByIdentitiesToAll(paginatedDocuments); | ||
|
||
// Prepare the return value | ||
if (options.includePagination) { | ||
const returnValue = { | ||
pagination: { | ||
total: documents.length, | ||
offset: options.offset, | ||
limit: options.limit | ||
}, | ||
data: paginatedDocuments | ||
}; | ||
return returnValue; | ||
} | ||
else { | ||
return paginatedDocuments; | ||
} | ||
}; |