catalog-workspace.test.mjs (4219B)
1 import { test } from 'node:test'; 2 import assert from 'node:assert/strict'; 3 import { readFileSync } from 'node:fs'; 4 import { loadTs } from './_loadts.mjs'; 5 import { normalizeTechnique, stableId } from '../scripts/enrich-data.mjs'; 6 import { LIST_FIELDS as indexFields } from '../scripts/build-index.mjs'; 7 const { configureCommand, quoteArgument, groupTechniques, contextualCounts, sortTechniques } = await loadTs('src/lib/catalog-workspace.ts'); 8 const { matches, EMPTY_FILTERS, filtersToSearch, filtersFromSearch } = await loadTs('src/lib/techniques.ts'); 9 const { LIST_FIELDS, renderRow } = await loadTs('src/lib/render-row.ts'); 10 const row = (over = {}) => ({ id:'a', toolId:'gtfo:vim', toolName:'vim', source:'GTFOBins', command:'vim', platform:['Linux'], capability:['Execution'], nativeCategory:[], mitre:[], references:[], ...over }); 11 12 test('context/access/service/environment filters compose and round-trip', () => { 13 const t = row({ context:'sudo', requires:['Shell'], services:['SMB'], environment:['Local host'], aliases:['vi'] }); 14 const f = { ...EMPTY_FILTERS, context:['sudo'], requires:['Shell'], services:['SMB'], environment:['Local host'], query:'vi sudo' }; 15 assert.equal(matches(t,f),true); assert.deepEqual(filtersFromSearch(filtersToSearch(f)),f); 16 assert.equal(matches(t,{ ...f, context:['suid'] }),false); 17 assert.equal(matches(t,{ ...f, query:'vi missing' }),false); 18 }); 19 test('facet counts exclude their own facet but retain the other filters', () => { 20 const rows = [row({context:'sudo'}),row({id:'b',context:'suid'}),row({id:'c',context:'suid',platform:['Windows']})]; 21 const counts = contextualCounts(rows,{...EMPTY_FILTERS,platform:['Linux'],context:['sudo']}); 22 assert.deepEqual(counts.context,{sudo:1,suid:1}); assert.deepEqual(counts.platform,{Linux:1}); 23 }); 24 test('grouping retains variants and exact tool matches sort first', () => { 25 const rows = [row(),row({id:'b',context:'sudo'}),row({id:'c',toolId:'daemon:aws',toolName:'aws'})]; 26 assert.equal(groupTechniques(rows).length,2); assert.equal(groupTechniques(rows)[0].length,2); 27 assert.equal(sortTechniques(rows,'tool','vim')[0].toolName,'vim'); 28 }); 29 test('template variables are quoted as arguments and cannot introduce new lines', () => { 30 const tpl = {shell:'posix',command:'tool --host {{host}}',variables:[{key:'host',label:'Host',default:'example.test'}]}; 31 assert.equal(configureCommand(tpl,{}),"tool --host 'example.test'"); 32 assert.equal(configureCommand(tpl,{host:"x'; echo bad; '"}),"tool --host 'x'\"'\"'; echo bad; '\"'\"''"); 33 assert.equal(quoteArgument("a'b",'powershell'),"'a''b'"); 34 assert.throws(()=>configureCommand(tpl,{host:'x\ncommand'})); 35 assert.throws(()=>configureCommand({...tpl,command:'tool {{missing}}'},{})); 36 }); 37 test('list payload retains the fields required by filters and configuration', () => { 38 assert.deepEqual(LIST_FIELDS,indexFields); 39 for (const key of ['context','requires','services','environment','template','references']) assert.ok(indexFields.includes(key)); 40 }); 41 test('row markup keeps navigation links outside interactive buttons', () => { 42 const html = renderRow(row()); 43 for (const button of html.matchAll(/<button\b[^>]*>([\s\S]*?)<\/button>/g)) assert.doesNotMatch(button[1],/<a\b/); 44 }); 45 test('normalization removes unverified macOS tags and category pollution', () => { 46 const t = normalizeTechnique(row({platform:['Linux','macOS'],requires:['Exploitation','powershell','PowerShell','Hash'],services:['Enumeration','SMB']})); 47 assert.deepEqual(t.platform,['Linux']); assert.deepEqual(t.requires,['PowerShell','NTLM hash']); assert.deepEqual(t.services,['SMB']); 48 assert.equal(stableId('tool','command'),stableId('tool','command')); 49 }); 50 test('committed additions and imported examples carry explicit evidence', () => { 51 const rows = JSON.parse(readFileSync('src/data/techniques.json')); 52 const mac = rows.filter(t=>t.source==='LOOBins'); assert.equal(mac.length,183); 53 assert.ok(mac.every(t=>t.verification==='Upstream reference' && t.references.some(r=>r.includes('/blob/')))); 54 assert.ok(!rows.some(t=>t.source==='GTFOBins' && t.platform.includes('macOS'))); 55 assert.equal(JSON.parse(readFileSync('src/data/sources/wadcoms-additions.json')).length,134); 56 });