daemon-sec-lotl

DÆMONBins: GTFOBins × LOLBAS × WADComs × LOOBins in one filterable catalog
git clone https://git.daemon-sec.xyz/daemon-sec-lotl.git
Log | Files | Refs | Submodules | README | LICENSE

techniques.ts (6156B)


      1 // Shared Technique model + pure filter/serialize logic. Imported by the Astro
      2 // pages at build time and by the client-side catalog island — the functions
      3 // have no framework or DOM dependency, so they run in both.
      4 
      5 import { SOURCE_ROUTE, NS_SOURCE } from './taxonomy';
      6 import type { Capability, Platform, SourceId } from './taxonomy';
      7 
      8 // Route helpers — a tool lives at /<deckRoute>/<toolSlug>. Both the deck and
      9 // the slug come from the colon-namespaced toolId ("gtfo:vim" -> gtfobins/vim,
     10 // "wadcoms:Certipy" -> wadcoms/certipy), never from a per-technique source, so
     11 // a mixed upstream+daemon family always resolves to one page. Lowercased so the
     12 // path is case-stable on GitHub Pages.
     13 export function toolSlug(toolId: string): string {
     14   return toolId.split(':').slice(1).join('-').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
     15 }
     16 export function toolNamespace(toolId: string): string {
     17   return toolId.split(':')[0];
     18 }
     19 export function routeSourceOf(toolId: string): SourceId {
     20   return NS_SOURCE[toolNamespace(toolId)] ?? 'DAEMON';
     21 }
     22 export function toolRoute(toolId: string): string {
     23   return `${SOURCE_ROUTE[routeSourceOf(toolId)]}/${toolSlug(toolId)}`;
     24 }
     25 
     26 export interface Detection { type: string; value: string }
     27 
     28 export interface Technique {
     29   id: string;
     30   toolId: string;
     31   toolName: string;
     32   name?: string;
     33   source: SourceId;
     34   platform: Platform[];
     35   capability: Capability[];
     36   nativeCategory: string[];
     37   command: string;
     38   description?: string;
     39   usecase?: string;
     40   mitre: string[];
     41   privilege?: string;
     42   context?: string;
     43   requires?: string[];
     44   services?: string[];
     45   fullPath?: string[];
     46   toolType?: string;
     47   detection?: Detection[];
     48   references: string[];
     49   added?: boolean;
     50   verifyNote?: string;
     51   environment?: string[];
     52   aliases?: string[];
     53   availability?: string;
     54   verification?: string;
     55   reviewedAt?: string;
     56   compatibility?: string;
     57   expected?: string;
     58   troubleshooting?: string;
     59   sideEffects?: string;
     60   restore?: string;
     61   template?: { command: string; shell: 'posix' | 'powershell'; variables: { key: string; label: string; default: string }[] };
     62 }
     63 
     64 export interface Tool {
     65   id: string;
     66   name: string;
     67   source: SourceId;
     68   platform: Platform[];
     69   toolType?: string;
     70   aliases?: string[];
     71   fullPath?: string[];
     72   author?: string;
     73   created?: string;
     74   contributors?: string[];
     75   references: string[];
     76   count: number;
     77 }
     78 
     79 export interface CatalogFilters {
     80   platform: string[];
     81   capability: string[];
     82   source: string[];
     83   query: string;
     84   addedOnly: boolean;
     85   context?: string[];
     86   requires?: string[];
     87   services?: string[];
     88   environment?: string[];
     89   availability?: string[];
     90   verification?: string[];
     91 }
     92 
     93 export const EMPTY_FILTERS: CatalogFilters = {
     94   platform: [], capability: [], source: [], query: '', addedOnly: false,
     95 };
     96 
     97 // OR within a facet (any ticked value present on the technique), AND across
     98 // facets — the conventional faceted-search behaviour.
     99 const inFacet = (values: string[], selected: string[]) =>
    100   selected.length === 0 || selected.some((s) => values.includes(s));
    101 
    102 export function searchText(t: Technique): string {
    103   return [t.toolName, t.name, t.command, t.description, t.usecase, t.context, t.privilege,
    104     ...(t.aliases || []), ...(t.requires || []), ...(t.services || []), ...(t.environment || []), ...(t.nativeCategory || []), ...(t.mitre || [])]
    105     .filter(Boolean)
    106     .join(' ')
    107     .toLowerCase();
    108 }
    109 
    110 export function matchesQuery(t: Technique, q: string): boolean {
    111   const hay = searchText(t);
    112   return q.toLowerCase().trim().split(/\s+/).every(word => hay.includes(word));
    113 }
    114 
    115 export const FACET_KEYS = ['platform', 'capability', 'source', 'context', 'requires', 'services', 'environment', 'availability', 'verification'] as const;
    116 export type FacetKey = (typeof FACET_KEYS)[number];
    117 export function facetValues(t: Technique, key: FacetKey): string[] {
    118   const v = key === 'context' ? (t.context || t.privilege) : t[key];
    119   return Array.isArray(v) ? v : v ? [v] : [];
    120 }
    121 
    122 export function matches(t: Technique, f: CatalogFilters): boolean {
    123   if (f.addedOnly && !t.added) return false;
    124   if (!inFacet(t.platform, f.platform)) return false;
    125   if (!inFacet(t.capability, f.capability)) return false;
    126   if (!inFacet([t.source], f.source)) return false;
    127   for (const key of FACET_KEYS.slice(3)) if (!inFacet(facetValues(t, key), f[key] || [])) return false;
    128   if (!matchesQuery(t, f.query)) return false;
    129   return true;
    130 }
    131 
    132 export function filterTechniques(techniques: Technique[], f: CatalogFilters): Technique[] {
    133   return techniques.filter((t) => matches(t, f));
    134 }
    135 
    136 export function countActive(f: CatalogFilters): number {
    137   return FACET_KEYS.reduce((n, k) => n + (f[k]?.length || 0), 0);
    138 }
    139 
    140 export function isEmpty(f: CatalogFilters): boolean {
    141   return countActive(f) === 0 && !f.query && !f.addedOnly;
    142 }
    143 
    144 // URL <-> filters. Comma-joined multi-values so a narrowed view is shareable.
    145 const PARAM_KEYS = { platform: 'p', capability: 'c', source: 's', context: 'ctx', requires: 'access', services: 'svc', environment: 'env', availability: 'available', verification: 'verified' } as const;
    146 
    147 export function filtersToSearch(f: CatalogFilters): string {
    148   const params = new URLSearchParams();
    149   for (const [key, param] of Object.entries(PARAM_KEYS) as [keyof typeof PARAM_KEYS, string][]) {
    150     const vals = f[key];
    151     if (vals?.length) params.set(param, vals.join(','));
    152   }
    153   if (f.addedOnly) params.set('new', '1');
    154   const q = f.query.trim();
    155   if (q) params.set('q', q);
    156   return params.toString();
    157 }
    158 
    159 export function filtersFromSearch(search: string): CatalogFilters {
    160   const params = new URLSearchParams(search.startsWith('?') ? search.slice(1) : search);
    161   const list = (k: string) => (params.get(k) || '').split(',').map((s) => s.trim()).filter(Boolean);
    162   return {
    163     ...Object.fromEntries(Object.entries(PARAM_KEYS).slice(3).filter(([, param]) => list(param).length).map(([key, param]) => [key, list(param)])),
    164     platform: list(PARAM_KEYS.platform),
    165     capability: list(PARAM_KEYS.capability),
    166     source: list(PARAM_KEYS.source),
    167     query: params.get('q') || '',
    168     addedOnly: params.get('new') === '1',
    169   };
    170 }