technique-schema.mjs (3569B)
1 /** 2 * The unified Technique vocabulary + Zod schema — the single source of truth 3 * shared by the dataset builder (build-dataset.mjs), the standalone validator 4 * (validate-data.mjs, run in CI over the committed JSON) and the one-shot 5 * migrations. Keep it dependency-free beyond zod so it loads anywhere. 6 * 7 * Mirrors src/lib/taxonomy.ts; the TS side is the UI vocabulary, this is the 8 * data contract. They must agree — validate-data.mjs cross-checks them. 9 */ 10 import { z } from 'zod'; 11 12 export const PLATFORMS = ['Linux', 'Windows', 'macOS', 'ActiveDirectory']; 13 export const SOURCES = ['GTFOBins', 'LOLBAS', 'WADComs', 'LOOBins', 'DAEMON']; 14 export const CAPABILITIES = [ 15 'Execution', 'Reverse/Bind Shell', 16 'File Download', 'File Upload', 'File Read', 'File Write', 'File Copy', 17 'Library Load', 'Compile', 18 'Privilege Escalation', 'UAC Bypass', 'AWL / Policy Bypass', 19 'Defense Evasion', 'Credential Access', 20 'Discovery', 'Enumeration', 'Persistence', 'Lateral Movement', 'Collection', 21 ]; 22 23 // Only absolute http(s) URLs may become <a href> — a stray `javascript:` or 24 // relative reference from an upstream merge must fail validation, not render. 25 const HTTP_URL = /^https?:\/\/\S+$/; 26 export const HttpUrl = z.string().regex(HTTP_URL, 'must be an absolute http(s) URL'); 27 28 export const TechniqueSchema = z.object({ 29 id: z.string().min(1), 30 toolId: z.string().min(1), 31 toolName: z.string().min(1), 32 name: z.string().optional(), 33 source: z.enum(SOURCES), 34 platform: z.array(z.enum(PLATFORMS)).min(1), 35 capability: z.array(z.enum(CAPABILITIES)), 36 nativeCategory: z.array(z.string()), 37 command: z.string().min(1), 38 description: z.string().optional(), 39 usecase: z.string().optional(), 40 mitre: z.array(z.string().regex(/^T\d{4}(\.\d{3})?$/, 'must be a MITRE ATT&CK technique id')), 41 privilege: z.string().optional(), 42 context: z.string().optional(), 43 requires: z.array(z.string()).optional(), 44 services: z.array(z.string()).optional(), 45 fullPath: z.array(z.string()).optional(), 46 toolType: z.string().optional(), 47 detection: z.array(z.object({ type: z.string(), value: z.string() })).optional(), 48 references: z.array(HttpUrl), 49 added: z.boolean().optional(), 50 verifyNote: z.string().optional(), 51 environment: z.array(z.string()).optional(), 52 aliases: z.array(z.string()).optional(), 53 availability: z.string().optional(), 54 verification: z.enum(['Upstream reference', 'Documentation checked', 'Lab tested']).optional(), 55 reviewedAt: z.string().optional(), 56 compatibility: z.string().optional(), 57 expected: z.string().optional(), 58 troubleshooting: z.string().optional(), 59 sideEffects: z.string().optional(), 60 restore: z.string().optional(), 61 template: z.object({ 62 command: z.string(), shell: z.enum(['posix', 'powershell']), 63 variables: z.array(z.object({ key: z.string().regex(/^[a-z][a-z0-9_]*$/), label: z.string(), default: z.string() })), 64 }).optional(), 65 }); 66 67 /** 68 * Validate a whole Technique[]: schema per record + unique ids. Returns a list 69 * of human-readable problems (empty = valid). Pure; callers decide how to fail. 70 */ 71 export function validateTechniques(techniques) { 72 const problems = []; 73 const ids = new Set(); 74 for (const t of techniques) { 75 const r = TechniqueSchema.safeParse(t); 76 if (!r.success) { 77 const why = r.error.issues.map((i) => `${i.path.join('.')} ${i.message}`).join('; '); 78 problems.push(`invalid ${t && t.id ? t.id : '<no id>'}: ${why}`); 79 continue; 80 } 81 if (ids.has(t.id)) problems.push(`duplicate id ${t.id}`); 82 ids.add(t.id); 83 } 84 return problems; 85 }