@mitre-attack/attack-data-model
Version:
A TypeScript API for the MITRE ATT&CK data model
357 lines (353 loc) • 10.4 kB
JavaScript
import {
AttackDataModel
} from "./chunk-TJML7IMQ.js";
import {
campaignSchema,
extensibleStixBundleSchema,
groupSchema,
malwareSchema,
relationshipSchema,
techniqueSchema,
toolSchema
} from "./chunk-AGVLYSTT.js";
import {
markingDefinitionSchema
} from "./chunk-NGT3NXS6.js";
import {
matrixSchema
} from "./chunk-I3N3UE6S.js";
import {
mitigationSchema
} from "./chunk-XQK5DHA7.js";
import {
tacticSchema
} from "./chunk-OBBW2YZ2.js";
import {
collectionSchema
} from "./chunk-SWPQKEG3.js";
import {
dataComponentSchema
} from "./chunk-LUZFW5GQ.js";
import {
dataSourceSchema
} from "./chunk-WKPLIQD4.js";
import {
detectionStrategySchema
} from "./chunk-QCHCLAHE.js";
import {
identitySchema
} from "./chunk-IATBMSJK.js";
import {
logSourceSchema
} from "./chunk-OG5WXWVB.js";
import {
analyticSchema
} from "./chunk-BCTQGLCT.js";
import {
assetSchema
} from "./chunk-H47PCQWS.js";
import {
attackDomainSchema
} from "./chunk-Z7F5EWOT.js";
import {
fetchAttackVersions
} from "./chunk-MFM4HCY4.js";
// src/main.ts
import axios from "axios";
import { v4 as uuidv4 } from "uuid";
var readFile = async (path) => {
if (typeof window !== "undefined") {
const response = await fetch(path);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.text();
} else {
const fs = await import("fs");
const { promisify } = await import("util");
const nodeReadFile = promisify(fs.readFile);
return nodeReadFile(path, "utf8");
}
};
var GITHUB_BASE_URL = process.env.GITHUB_BASE_URL || "https://raw.githubusercontent.com/mitre-attack/attack-stix-data/master";
var dataSources = {};
async function registerDataSource(registration) {
const { source, parsingMode = "strict" } = registration.options;
let rawData;
const uniqueId = uuidv4();
switch (source) {
case "attack": {
const { domain, version } = registration.options;
rawData = await fetchAttackDataFromGitHub(domain, version);
break;
}
case "file": {
const { path } = registration.options;
rawData = await fetchDataFromFile(path);
break;
}
case "url":
case "taxii": {
const { url } = registration.options;
rawData = await fetchDataFromUrl(url);
break;
}
default:
throw new Error(`Unsupported source type: ${source}`);
}
console.log("Retrieved data");
const parsedAttackObjects = parseStixBundle(rawData, parsingMode);
console.log("Parsed data.");
console.log(parsedAttackObjects.length);
const model = new AttackDataModel(uniqueId, parsedAttackObjects);
console.log("Initialized data model.");
dataSources[uniqueId] = { id: uniqueId, model };
return uniqueId;
}
async function fetchAttackDataFromGitHub(domain, version) {
let url = `${GITHUB_BASE_URL}/${domain}/`;
const normalizedVersion = version ? version.replace(/^v/, "") : version;
url += normalizedVersion ? `${domain}-${normalizedVersion}.json` : `${domain}.json`;
try {
const response = await axios.get(url, {
headers: {
"Content-Type": "application/json"
},
timeout: 1e4
// 10-second timeout
});
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(`Failed to fetch data from ${url}: ${error.message}`);
} else {
throw new Error(`Unexpected error: ${error}`);
}
}
}
async function fetchDataFromUrl(url) {
try {
const response = await axios.get(url, {
headers: {
"Content-Type": "application/json"
},
timeout: 1e4
// Set a timeout of 10 seconds
});
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
throw new Error(`Failed to fetch data from ${url}: ${error.message}`);
} else {
throw new Error(`Unexpected error: ${error}`);
}
}
}
async function fetchDataFromFile(filePath) {
try {
const data = await readFile(filePath);
return JSON.parse(data);
} catch (error) {
if (error instanceof Error) {
throw new Error(`Failed to read or parse file at ${filePath}: ${error.message}`);
} else {
throw new Error(`Failed to read or parse file at ${filePath}: ${String(error)}`);
}
}
}
function parseStixBundle(rawData, parsingMode) {
const errors = [];
const validObjects = [];
const baseBundleValidationResults = extensibleStixBundleSchema.pick({
id: true,
type: true,
spec_version: true
}).safeParse(rawData);
if (!baseBundleValidationResults.success) {
baseBundleValidationResults.error.issues.forEach((issue) => {
errors.push(`Error: Path - ${issue.path.join(".")}, Message - ${issue.message}`);
});
if (parsingMode === "strict") {
throw new Error(`Bundle validation failed:
${errors.join("\n")}`);
} else {
console.warn(`Bundle validation errors:
${errors.join("\n")}`);
}
return [];
}
const objects = rawData.objects;
for (let index = 0; index < objects.length; index++) {
const obj = objects[index];
let objParseResult;
switch (obj.type) {
case "x-mitre-asset":
objParseResult = assetSchema.safeParse(obj);
break;
case "campaign":
objParseResult = campaignSchema.safeParse(obj);
break;
case "x-mitre-collection":
objParseResult = collectionSchema.safeParse(obj);
break;
case "x-mitre-data-component":
objParseResult = dataComponentSchema.safeParse(obj);
break;
case "x-mitre-data-source":
objParseResult = dataSourceSchema.safeParse(obj);
break;
case "intrusion-set":
objParseResult = groupSchema.safeParse(obj);
break;
case "identity":
objParseResult = identitySchema.safeParse(obj);
break;
case "malware":
objParseResult = malwareSchema.safeParse(obj);
break;
case "x-mitre-matrix":
objParseResult = matrixSchema.safeParse(obj);
break;
case "course-of-action":
objParseResult = mitigationSchema.safeParse(obj);
break;
case "x-mitre-tactic":
objParseResult = tacticSchema.safeParse(obj);
break;
case "attack-pattern":
objParseResult = techniqueSchema.safeParse(obj);
break;
case "tool":
objParseResult = toolSchema.safeParse(obj);
break;
case "marking-definition":
objParseResult = markingDefinitionSchema.safeParse(obj);
break;
case "relationship":
objParseResult = relationshipSchema.safeParse(obj);
break;
case "x-mitre-log-source":
objParseResult = logSourceSchema.safeParse(obj);
break;
case "x-mitre-detection-strategy":
objParseResult = detectionStrategySchema.safeParse(obj);
break;
case "x-mitre-analytic":
objParseResult = analyticSchema.safeParse(obj);
break;
default:
errors.push(`Unknown object type at index ${index}: ${obj.type}`);
objParseResult = null;
break;
}
if (objParseResult && objParseResult.success) {
validObjects.push(objParseResult.data);
} else {
if (objParseResult && objParseResult.error) {
objParseResult.error.issues.forEach((issue) => {
errors.push(
`Error: Path - objects.${index}.${issue.path.join(".")}, Message - ${issue.message}`
);
});
} else {
errors.push(`Failed to parse object at index ${index}`);
}
if (parsingMode === "relaxed") {
validObjects.push(obj);
}
}
}
if (errors.length > 0) {
if (parsingMode === "strict") {
throw new Error(`Validation errors:
${errors.join("\n")}`);
} else {
console.warn(`Validation errors:
${errors.join("\n")}`);
}
}
return validObjects;
}
function loadDataModel(id) {
const dataSource = dataSources[id];
if (!dataSource) {
throw new Error(`Data source with ID ${id} not found.`);
}
return dataSource.model;
}
// src/data-sources/data-source-registration.ts
var DataSourceRegistration2 = class {
/**
* Creates a new DataSourceRegistration instance.
* @param options - The data source options to register.
*/
constructor(options) {
this.options = options;
this.validateOptions();
}
/**
* Validates the data source options to ensure the correct fields are provided for each source type.
* @throws An error if validation fails.
*/
async validateOptions() {
const { source, parsingMode } = this.options;
if (parsingMode && !["strict", "relaxed"].includes(parsingMode)) {
throw new Error(`Invalid parsingMode: ${parsingMode}. Expected 'strict' or 'relaxed'.`);
}
switch (source) {
case "attack": {
await this.validateAttackOptions();
break;
}
case "file": {
this.validateFileOptions();
break;
}
case "url":
case "taxii": {
throw new Error(`The ${source} source is not implemented yet.`);
}
default: {
throw new Error(`Unsupported data source type: ${source}`);
}
}
}
/**
* Validates options specific to the 'attack' source type.
* @throws An error if validation fails.
*/
async validateAttackOptions() {
const { domain, version } = this.options;
if (!domain || !Object.values(attackDomainSchema.enum).includes(domain)) {
throw new Error(
`Invalid domain provided for 'attack' source. Expected one of: ${Object.values(
attackDomainSchema.enum
).join(", ")}`
);
}
if (version) {
const supportedVersions = await fetchAttackVersions();
const normalizedVersion = version.replace(/^v/, "");
if (!supportedVersions.includes(normalizedVersion)) {
throw new Error(
`Invalid version: ${version}. Supported versions are: ${supportedVersions.join(", ")}`
);
}
}
}
/**
* Validates options specific to the 'file' source type.
* @throws An error if validation fails.
*/
validateFileOptions() {
const { path } = this.options;
if (!path) {
throw new Error("The 'file' source requires a 'path' field to specify the file location.");
}
}
};
export {
DataSourceRegistration2 as DataSourceRegistration,
registerDataSource,
loadDataModel
};