From 9a9fee2f2fff8426eea3fef5cbba92f3a2f4a5bc Mon Sep 17 00:00:00 2001 From: adpare Date: Fri, 21 Aug 2026 15:57:04 -0400 Subject: [PATCH] feat: add excluding ids functionality for better filtering --- .../definitions/paths/attack-objects-paths.yml | 11 +++++++++++ app/controllers/attack-objects-controller.js | 1 + app/repository/attack-objects-repository.js | 5 +++++ .../api/attack-objects/attack-objects.spec.js | 18 ++++++++++++++++++ 4 files changed, 35 insertions(+) diff --git a/app/api/definitions/paths/attack-objects-paths.yml b/app/api/definitions/paths/attack-objects-paths.yml index ffb1e0e0..6c150a7a 100644 --- a/app/api/definitions/paths/attack-objects-paths.yml +++ b/app/api/definitions/paths/attack-objects-paths.yml @@ -22,6 +22,17 @@ paths: items: type: string example: 'T9999' + - name: excludeID + in: query + description: | + Exclude objects with the specified STIX IDs before sorting and pagination. + This parameter may be set multiple times. + schema: + oneOf: + - type: string + - type: array + items: + type: string - name: limit in: query description: | diff --git a/app/controllers/attack-objects-controller.js b/app/controllers/attack-objects-controller.js index d86bf5dc..25da8479 100644 --- a/app/controllers/attack-objects-controller.js +++ b/app/controllers/attack-objects-controller.js @@ -6,6 +6,7 @@ const logger = require('../lib/logger'); exports.retrieveAll = async function (req, res) { const options = { attackId: req.query.attackId, + excludeID: req.query.excludeID, offset: req.query.offset || 0, limit: req.query.limit || 0, state: req.query.state, diff --git a/app/repository/attack-objects-repository.js b/app/repository/attack-objects-repository.js index 9eeb4a3e..82dc82cf 100644 --- a/app/repository/attack-objects-repository.js +++ b/app/repository/attack-objects-repository.js @@ -22,6 +22,11 @@ class AttackObjectsRepository extends BaseRepository { query['workspace.attack_id'] = options.attackId; } } + if (typeof options.excludeID !== 'undefined') { + query['stix.id'] = { + $nin: Array.isArray(options.excludeID) ? options.excludeID : [options.excludeID], + }; + } if (!options.includeRevoked) { query['stix.revoked'] = { $in: [null, false] }; } diff --git a/app/tests/api/attack-objects/attack-objects.spec.js b/app/tests/api/attack-objects/attack-objects.spec.js index bd4a9184..bb801706 100644 --- a/app/tests/api/attack-objects/attack-objects.spec.js +++ b/app/tests/api/attack-objects/attack-objects.spec.js @@ -222,6 +222,24 @@ describe('ATT&CK Objects API', function () { expect(attackObjects.length).toBe(3); }); + it('GET /api/attack-objects excludes IDs before paginating results', async function () { + const excludedGroupId = 'intrusion-set--925216d2-dd4c-4487-8d19-f96e81dabd5d'; + const excludedSoftwareId = 'malware--9c5ab575-f015-462c-92a0-f887277d8519'; + const includedTechniqueId = 'attack-pattern--757471d4-d931-4109-82dd-cdd50c04744e'; + const res = await request(app) + .get( + `/api/attack-objects?attackId=G9001&attackId=S9001&attackId=T9001&excludeID=${excludedGroupId}&excludeID=${excludedSoftwareId}&offset=0&limit=1&includePagination=true`, + ) + .set('Accept', 'application/json') + .set('Cookie', `${passportCookie.name}=${passportCookie.value}`) + .expect(200) + .expect('Content-Type', /json/); + + expect(res.body.pagination).toEqual({ total: 1, offset: 0, limit: 1 }); + expect(res.body.data).toHaveLength(1); + expect(res.body.data[0].stix.id).toBe(includedTechniqueId); + }); + it('GET /api/attack-objects uses the search parameter to return the tactic objects', async function () { const res = await request(app) .get('/api/attack-objects?search=nabu')