Skip to content

Commit

Permalink
Fix/recent activity (#279)
Browse files Browse the repository at this point in the history
* separate recent activity endpoint

* recent activity service
  • Loading branch information
clemiller authored Aug 24, 2023
1 parent 54e3165 commit c867272
Show file tree
Hide file tree
Showing 5 changed files with 262 additions and 1 deletion.
6 changes: 5 additions & 1 deletion app/api/definitions/openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -298,4 +298,8 @@ paths:
$ref: 'paths/teams-paths.yml#/paths/~1api~1teams~1{id}'

/api/teams/{id}/users:
$ref: 'paths/teams-paths.yml#/paths/~1api~1teams~1{id}~1users'
$ref: 'paths/teams-paths.yml#/paths/~1api~1teams~1{id}~1users'

# Recent Activity
/api/recent-activity:
$ref: 'paths/recent-activity-paths.yml#/paths/~1api~1recent-activity'
78 changes: 78 additions & 0 deletions app/api/definitions/paths/recent-activity-paths.yml
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'
32 changes: 32 additions & 0 deletions app/controllers/recent-activity-controller.js
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.');
}
};
18 changes: 18 additions & 0 deletions app/routes/recent-activity-routes.js
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;
129 changes: 129 additions & 0 deletions app/services/recent-activity-service.js
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;
}
};

0 comments on commit c867272

Please sign in to comment.