Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AF-10: Autocomplete #95

Merged
merged 20 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
48ec70a
Update FocusBox, FocusBox usage, Text and Enum Fields
mikecarenzo Aug 1, 2023
3bf9eba
Add support for StringProperty text suggestions.
mikecarenzo Aug 1, 2023
d4d63c2
create intel file
mikecarenzo Aug 3, 2023
33bce88
autosuggest tactics and techniques from intel file
mikecarenzo Aug 3, 2023
d65fae7
update ScrollBox construct
mikecarenzo Aug 3, 2023
3f8ece3
separated intel configuration from source file
mikecarenzo Aug 3, 2023
c02831f
Add matrix information to ATT&CK intelligence file
mikecarenzo Aug 16, 2023
90cba64
add `id` field to `Property`
mikecarenzo Aug 17, 2023
cc8f1fa
expose `Command` constructor fields, extend `Property` Commands from …
mikecarenzo Aug 18, 2023
f86443d
add processor construct
mikecarenzo Aug 18, 2023
f642780
add autofill processor
mikecarenzo Aug 18, 2023
6de7bd4
prevent cut command from breaking when nothing is selected
mikecarenzo Aug 18, 2023
80d906d
Merge branch 'main' into AF-10_autocomplete
mikecarenzo Aug 18, 2023
7d84e9d
fix bug that kept `ListProperty` from enumerating options
mikecarenzo Aug 22, 2023
de178e3
allow "Escape" key to stop `TextField` suggestions
mikecarenzo Aug 22, 2023
795dd87
limit tactic and technique suggestions to ID only
mikecarenzo Aug 22, 2023
be0afd8
update developer documentation
mikecarenzo Aug 22, 2023
56c5587
Merge branch 'main' into AF-10_autocomplete
mikecarenzo Aug 24, 2023
bf3c960
correct `hash` to `hashes` in validator
mikecarenzo Aug 24, 2023
328c3e8
add additional ATT&CK info to suggestions
mikecarenzo Aug 24, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading