Skip to content

Commit

Permalink
Merge pull request #95 from center-for-threat-informed-defense/AF-10_…
Browse files Browse the repository at this point in the history
…autocomplete

AF-10: Autocomplete
  • Loading branch information
mikecarenzo committed Aug 24, 2023
2 parents ecb1175 + 328c3e8 commit 6f5bb26
Show file tree
Hide file tree
Showing 56 changed files with 13,254 additions and 1,480 deletions.
26 changes: 26 additions & 0 deletions docs/developers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,32 @@ If this starts up successfully, then you can access the application at
http://localhost:8080/. As you edit source code and save, the server will automatically
rebuild the application and you can refresh the browser to run it again.

Update Intelligence File
~~~~~~~~~~~~~~~~~~~~~~~~

The *Intelligence File* (`builder.config.intel.ts`) drives the application's autocomplete features.
This file is generated automatically by a set of scripts which download and organize relevant ATT&CK
information into a format the application can leverage.

To update the Intelligence File, simply invoke:

.. code:: shell
$ npm run update-intel
> [email protected] update-intel
> node ./attack/update_attack_intel.js
→ Downloading ATT\&CK Data...
→ .../attack-stix-data/master/enterprise-attack/enterprise-attack-13.0.json
→ ...m/mitre-attack/attack-stix-data/master/ics-attack/ics-attack-13.0.json
→ ...e-attack/attack-stix-data/master/mobile-attack/mobile-attack-13.0.json
→ Generating Application Intel File...
Intelligence updated successfully.
The configured list of sources can be modified at any time from `download_sources.js`.

Preload a Flow
~~~~~~~~~~~~~~

Expand Down
170 changes: 170 additions & 0 deletions src/attack_flow_builder/attack/download_attack.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
const https = require("https");

/**
* @typedef {Object} AttackObject
* An ATT&CK Object.
* @property {string} id
* The object's ATT&CK id.
* @property {string} name
* The object's ATT&CK name.
* @property {string} type
* The object's type.
* @property {string} url
* The object's ATT&CK url.
* @property {string} stixId
* The object's STIX id.
* @property {boolean} deprecated
* True if the ATT&CK object has been deprecated, false otherwise.
* @property {string} matrix
* The ATT&CK object's matrix.
*/

/**
* A map that relates STIX types to ATT&CK types.
*/
const STIX_TO_ATTACK = {
"campaign" : "campaign",
"course-of-action" : "mitigation",
"intrusion-set" : "group",
"malware" : "software",
"tool" : "software",
"x-mitre-data-source" : "data_source",
"x-mitre-tactic" : "tactic",
"attack-pattern" : "technique"
}

/**
* MITRE's source identifiers.
*/
const MITRE_SOURCES = new Set([
"mitre-attack",
"mitre-ics-attack",
"mitre-mobile-attack"
])

/**
* Fetches JSON data from a url.
* @param {string} url
* The url.
* @param {Object} options
* The request's options.
* @returns {Promise<Object>}
* A Promise that resolves with the JSON data.
*/
function fetchJson(url, options = {}) {
return new Promise((resolve, reject) => {
https.get(url, options, res => {
let json = "";
res.on("data", chunk => {
json += chunk;
});
res.on("end", () => {
try {
resolve(JSON.parse(json));
} catch(err) {
reject(err)
}
})
}).on("error", (err) => {
reject(err);
});
})
}

/**
* Parses an ATT&CK object from a STIX object.
* @param {Object} obj
* The STIX object.
* @param {string} matrix
* The STIX object's matrix.
* @returns {AttackObject}
* The parsed ATT&CK object.
*/
function parseStixToAttackObject(obj, matrix) {

// Parse STIX id, name, and type directly
let parse = {
stixId : obj.id,
name : obj.name,
type : STIX_TO_ATTACK[obj.type],
matrix : matrix
}

// Parse MITRE reference information
let mitreRef = obj.external_references.find(
o => MITRE_SOURCES.has(o.source_name)
);
if(!mitreRef) {
throw new Error("Missing MITRE reference information.")
}
parse.id = mitreRef.external_id;
parse.url = mitreRef.url;

// Parse deprecation status
parse.deprecated = (obj.x_mitre_deprecated || obj.revoked) ?? false;

// Return
return parse;
}

/**
* Parses a set of ATT&CK objects from a STIX manifest.
* @param {Object} data
* The STIX manifest.
* @returns {AttackObject[]}
* The parsed ATT&CK objects.
*/
function parseAttackObjectsFromManifest(data) {
// Parse matrix
let collection = data.objects.find(o => o.type === "x-mitre-collection");
if(!collection) {
throw new Error("STIX collection information missing.");
}
// Parse objects
let objs = []
for(let obj of data.objects) {
if(!(obj.type in STIX_TO_ATTACK)) {
continue;
}
objs.push(parseStixToAttackObject(obj, collection.name));
}
return objs;
}

/**
* Fetches ATT&CK data from a set of STIX manifests.
* @param {...string} urls
* A list of STIX manifests specified by url.
* @returns {Promise<Map<string, AttackObject>>}
* A Promise that resolves with the parsed ATT&CK data.
*/
async function fetchAttackData(...urls) {
console.log("→ Downloading ATT&CK Data...");

// Parse objects
let catalog = new Map();
for(let url of urls) {
console.log(` → ${ url.length > 70 ? '...' : '' }${ url.substr(url.length - 70) }`);
let objs = parseAttackObjectsFromManifest(await fetchJson(url));
for(let obj of objs) {
catalog.set(obj.stixId, obj);
}
}

// Categorize catalog
let types = new Map(
Object.values(STIX_TO_ATTACK).map(v => [v, []])
);
for(let obj of catalog.values()) {
types.get(obj.type).push(obj);
}

// Return
return types;

}

/**
* Define exports.
*/
module.exports = { fetchAttackData }
20 changes: 20 additions & 0 deletions src/attack_flow_builder/attack/download_sources.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* The base URL for the ATT&CK repository.
*/
const BASE_URL = "https://raw.githubusercontent.com/mitre-attack/attack-stix-data/master";

/**
* The STIX sources.
*/
const STIX_SOURCES = [
`${BASE_URL}/enterprise-attack/enterprise-attack-13.0.json`,
`${BASE_URL}/ics-attack/ics-attack-13.0.json`,
`${BASE_URL}/mobile-attack/mobile-attack-13.0.json`
]

/**
* Export
*/
module.exports = {
STIX_SOURCES
};
61 changes: 61 additions & 0 deletions src/attack_flow_builder/attack/update_attack_intel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const { resolve } = require("path");
const { writeFileSync } = require("fs");
const { fetchAttackData } = require("./download_attack");
const { STIX_SOURCES } = require("./download_sources");

/**
* The intel file's export key.
*/
const EXPORT_KEY = "intel";

/**
* The intel file's path.
*/
const INTEL_FILE_PATH = "../src/assets/builder.config.intel.ts";

/**
* JavaScript variable regex.
*/
const JS_VAR_REGEX = /^[a-z_$][a-z0-9_$]*$/i;

/**
* Updates the specified intel file.
* @param {string} path
* The intel file's path.
* @param {...string} urls
* A list of STIX manifests specified by url.
*/
async function updateApplicationAttackIntel(path, ...urls) {
path = resolve(__dirname, path);

// Validate export key
if(!JS_VAR_REGEX.test(EXPORT_KEY)) {
throw new Error(`Export key '${ EXPORT_KEY }' is not a valid variable name.`);
}

// Collect intel
let types = await fetchAttackData(...urls);
console.log("→ Generating Application Intel File...");
let intel = {
tactics : types.get("tactic"),
tactic_recs : types.get("tactic").map(o => `${o.id} (${o.matrix.split(/\s+/)[0]} / ${o.name})`).sort(),
technique : types.get("technique"),
technique_recs : types.get("technique").map(o => `${o.id} (${o.name})`).sort()
};

// Generate intel file
let file = "";
file += `export const ${ EXPORT_KEY } = `;
file += JSON.stringify(intel, null, 4);
file += `;\n\nexport default ${ EXPORT_KEY };\n`
writeFileSync(path, file);

// Done
console.log("\nIntelligence updated successfully.\n");

}

/**
* Main
*/
updateApplicationAttackIntel(INTEL_FILE_PATH, ...STIX_SOURCES);
65 changes: 33 additions & 32 deletions src/attack_flow_builder/package.json
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
{
"name": "attack-flow-builder",
"version": "2.0.1",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"release": "standard-version"
},
"author": "mcarenzo",
"dependencies": {
"vue": "^3.0.0",
"vuex": "^4.0.0-0"
},
"devDependencies": {
"@types/d3": "^7.4.0",
"@types/node": "^20.4.5",
"@types/resize-observer-browser": "^0.1.7",
"@vue/cli-plugin-typescript": "~4.5.15",
"@vue/cli-plugin-vuex": "~4.5.15",
"@vue/cli-service": "~4.5.15",
"@vue/compiler-sfc": "^3.0.0",
"d3": "^7.4.4",
"standard-version": "^9.3.2",
"typescript": "^4.1.5"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
{
"name": "attack-flow-builder",
"version": "2.0.1",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"release": "standard-version",
"update-intel": "node ./attack/update_attack_intel.js"
},
"author": "mcarenzo",
"dependencies": {
"vue": "^3.0.0",
"vuex": "^4.0.0-0"
},
"devDependencies": {
"@types/d3": "^7.4.0",
"@types/node": "^20.4.5",
"@types/resize-observer-browser": "^0.1.7",
"@vue/cli-plugin-typescript": "~4.5.15",
"@vue/cli-plugin-vuex": "~4.5.15",
"@vue/cli-service": "~4.5.15",
"@vue/compiler-sfc": "^3.0.0",
"d3": "^7.4.4",
"standard-version": "^9.3.2",
"typescript": "^4.1.5"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
}
Loading

0 comments on commit 6f5bb26

Please sign in to comment.