UNPKG

@mitre-attack/attack-data-model

Version:

A TypeScript API for the MITRE ATT&CK data model

357 lines (353 loc) 10.4 kB
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 };