UNPKG

@cyclonedx/cdxgen

Version:

Creates CycloneDX Software Bill of Materials (SBOM) from source or container image

324 lines (313 loc) 10.1 kB
import { readFileSync } from "node:fs"; import { basename } from "node:path"; import { fileURLToPath } from "node:url"; const GTFOBINS_INDEX_FILE = fileURLToPath( new URL("../../data/gtfobins-index.json", import.meta.url), ); const GTFOBINS_REFERENCE_PREFIX = "https://gtfobins.github.io/gtfobins/"; const PRIVILEGED_CONTEXTS = ["sudo", "suid", "capabilities"]; const MATCH_FIELDS = [ "name", "path", "cmdline", "parent_cmdline", "action", "program", "executable", "description", ]; const UNIX_PATH_PATTERN = /(?:^|[\s'"`=;|&(])((?:\.{0,2}\/|\/)[^\s'"`|;&()]+)/g; const STANDALONE_COMMAND_PATTERN = /(?:^|[\s;|&(])([a-z_][a-z0-9+._-]{1,})(?=$|[\s'"`|;&()])/gi; const CONTAINER_ESCAPE_HELPERS = new Set([ "chroot", "ctr", "docker", "kubectl", "mount", "nsenter", "tar", "unshare", ]); const DIRECT_ALIASES = new Map([["nodejs", "node"]]); const VERSIONED_ALIASES = [ { pattern: /^python(?:\d+(?:\.\d+)*)?$/i, target: "python" }, { pattern: /^perl(?:\d+(?:\.\d+)*)?$/i, target: "perl" }, { pattern: /^ruby(?:\d+(?:\.\d+)*)?$/i, target: "ruby" }, { pattern: /^php(?:\d+(?:\.\d+)*)?$/i, target: "php" }, { pattern: /^lua(?:\d+(?:\.\d+)*)?$/i, target: "lua" }, { pattern: /^node(?:\d+(?:\.\d+)*)?$/i, target: "node" }, ]; const GTFOBINS_INDEX = loadGtfoBinsIndex(); function loadGtfoBinsIndex() { try { return JSON.parse(readFileSync(GTFOBINS_INDEX_FILE, "utf8")); } catch { return { entries: {}, source: GTFOBINS_REFERENCE_PREFIX, sourceRef: "" }; } } function uniqueSortedStrings(values) { return [...new Set((values || []).filter(Boolean))].sort(); } function collectValueCandidates(value) { if (!value || typeof value !== "string") { return []; } const candidates = new Set(); const trimmedValue = value.trim(); if (trimmedValue) { candidates.add(trimmedValue); } for (const match of value.matchAll(UNIX_PATH_PATTERN)) { if (match[1]) { candidates.add(match[1]); } } for (const match of value.matchAll(STANDALONE_COMMAND_PATTERN)) { if (match[1]) { candidates.add(match[1]); } } return Array.from(candidates); } function resolveCandidateName(candidate) { if (!candidate || typeof candidate !== "string") { return undefined; } const trimmed = basename(candidate.trim()); if (!trimmed) { return undefined; } const normalized = trimmed.toLowerCase(); if (GTFOBINS_INDEX.entries?.[trimmed]) { return { canonicalName: trimmed, matchSource: "basename" }; } if (GTFOBINS_INDEX.entries?.[normalized]) { return { canonicalName: normalized, matchSource: "basename" }; } const directAlias = DIRECT_ALIASES.get(normalized); if (directAlias && GTFOBINS_INDEX.entries?.[directAlias]) { return { canonicalName: directAlias, matchSource: "alias" }; } for (const aliasRule of VERSIONED_ALIASES) { if ( aliasRule.pattern.test(normalized) && GTFOBINS_INDEX.entries?.[aliasRule.target] ) { return { canonicalName: aliasRule.target, matchSource: "alias" }; } } return undefined; } function deriveRiskTags(entry, canonicalName) { const functions = new Set(entry?.functions || []); const contexts = new Set(entry?.contexts || []); const riskTags = new Set(); const hasExecPrimitive = functions.has("shell") || functions.has("command") || functions.has("reverse-shell") || functions.has("bind-shell"); const hasNetworkPrimitive = functions.has("upload") || functions.has("download") || functions.has("reverse-shell") || functions.has("bind-shell"); if (functions.has("privilege-escalation")) { riskTags.add("privilege-escalation"); } if ( contexts.has("sudo") || contexts.has("suid") || contexts.has("capabilities") ) { riskTags.add("privilege-escalation"); } if (hasExecPrimitive && hasNetworkPrimitive) { riskTags.add("lateral-movement"); } if (functions.has("upload") || functions.has("file-read")) { riskTags.add("data-exfiltration"); } if (functions.has("file-write") || functions.has("library-load")) { riskTags.add("persistence"); } if ( CONTAINER_ESCAPE_HELPERS.has(canonicalName) && (hasExecPrimitive || functions.has("privilege-escalation") || functions.has("library-load")) ) { riskTags.add("container-escape"); } return Array.from(riskTags).sort(); } export function getGtfoBinsMetadata(name, linkedName) { const directMatch = resolveCandidateName(name); if (directMatch) { const entry = GTFOBINS_INDEX.entries[directMatch.canonicalName]; return { canonicalName: directMatch.canonicalName, contexts: entry.contexts, functions: entry.functions, matchSource: directMatch.matchSource, mitreTechniques: entry.mitreTechniques, privilegedContexts: entry?.contexts?.filter((context) => PRIVILEGED_CONTEXTS.includes(context), ), reference: `${GTFOBINS_REFERENCE_PREFIX}${encodeURIComponent(directMatch.canonicalName)}/`, riskTags: deriveRiskTags(entry, directMatch.canonicalName), source: GTFOBINS_INDEX.source, sourceRef: GTFOBINS_INDEX.sourceRef, }; } const linkedMatch = resolveCandidateName(linkedName); if (!linkedMatch) { return undefined; } const entry = GTFOBINS_INDEX.entries[linkedMatch.canonicalName]; return { canonicalName: linkedMatch.canonicalName, contexts: entry.contexts, functions: entry.functions, matchSource: "symlink", mitreTechniques: entry.mitreTechniques, privilegedContexts: entry.contexts.filter((context) => PRIVILEGED_CONTEXTS.includes(context), ), reference: `${GTFOBINS_REFERENCE_PREFIX}${encodeURIComponent(linkedMatch.canonicalName)}/`, riskTags: deriveRiskTags(entry, linkedMatch.canonicalName), source: GTFOBINS_INDEX.source, sourceRef: GTFOBINS_INDEX.sourceRef, }; } export function createGtfoBinsProperties(name, linkedName) { const metadata = getGtfoBinsMetadata(name, linkedName); if (!metadata) { return []; } const properties = [ { name: "cdx:gtfobins:matched", value: "true" }, { name: "cdx:gtfobins:name", value: metadata.canonicalName }, { name: "cdx:gtfobins:matchSource", value: metadata.matchSource }, { name: "cdx:gtfobins:functions", value: metadata.functions.join(",") }, { name: "cdx:gtfobins:contexts", value: metadata.contexts.join(",") }, { name: "cdx:gtfobins:reference", value: metadata.reference }, { name: "cdx:gtfobins:sourceRef", value: metadata.sourceRef || "" }, ]; if (metadata.mitreTechniques.length) { properties.push({ name: "cdx:gtfobins:mitreTechniques", value: metadata.mitreTechniques.join(","), }); } if (metadata.privilegedContexts.length) { properties.push({ name: "cdx:gtfobins:privilegedContexts", value: metadata.privilegedContexts.join(","), }); } if (metadata.riskTags.length) { properties.push({ name: "cdx:gtfobins:riskTags", value: metadata.riskTags.join(","), }); } return properties; } /** * Resolve GTFOBins properties for a live Linux osquery row. * * @param {string} queryCategory Osquery query category * @param {object} row Osquery row * @returns {Array<object>} CycloneDX custom properties */ export function createGtfoBinsPropertiesFromRow(queryCategory, row) { const matches = new Map(); for (const field of MATCH_FIELDS) { const fieldValue = row?.[field]; if (!fieldValue) { continue; } for (const candidate of collectValueCandidates(String(fieldValue))) { const metadata = getGtfoBinsMetadata(candidate); if (!metadata) { continue; } const existing = matches.get(metadata.canonicalName) || { fields: new Set(), metadata, }; existing.fields.add(field); matches.set(metadata.canonicalName, existing); } } if (!matches.size) { return []; } const names = uniqueSortedStrings(Array.from(matches.keys())); const matchFields = uniqueSortedStrings( Array.from(matches.values()).flatMap((match) => Array.from(match.fields)), ); const functions = uniqueSortedStrings( Array.from(matches.values()).flatMap((match) => match.metadata.functions), ); const contexts = uniqueSortedStrings( Array.from(matches.values()).flatMap((match) => match.metadata.contexts), ); const privilegedContexts = uniqueSortedStrings( Array.from(matches.values()).flatMap( (match) => match.metadata.privilegedContexts, ), ); const references = uniqueSortedStrings( Array.from(matches.values()).map((match) => match.metadata.reference), ); const riskTags = uniqueSortedStrings( Array.from(matches.values()).flatMap((match) => match.metadata.riskTags), ); const mitreTechniques = uniqueSortedStrings( Array.from(matches.values()).flatMap( (match) => match.metadata.mitreTechniques, ), ); const properties = [ { name: "cdx:gtfobins:matched", value: "true" }, { name: "cdx:gtfobins:names", value: names.join(",") }, { name: "cdx:gtfobins:matchFields", value: matchFields.join(",") }, { name: "cdx:gtfobins:queryCategory", value: queryCategory }, { name: "cdx:gtfobins:reference", value: references.join(",") }, { name: "cdx:gtfobins:sourceRef", value: GTFOBINS_INDEX.sourceRef || "" }, ]; if (functions.length) { properties.push({ name: "cdx:gtfobins:functions", value: functions.join(","), }); } if (contexts.length) { properties.push({ name: "cdx:gtfobins:contexts", value: contexts.join(","), }); } if (privilegedContexts.length) { properties.push({ name: "cdx:gtfobins:privilegedContexts", value: privilegedContexts.join(","), }); } if (riskTags.length) { properties.push({ name: "cdx:gtfobins:riskTags", value: riskTags.join(","), }); } if (mitreTechniques.length) { properties.push({ name: "cdx:gtfobins:mitreTechniques", value: mitreTechniques.join(","), }); } return properties; }