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

jsonld.ts (5499B)


      1 // Structured data (schema.org JSON-LD) builders. Pure: every function takes
      2 // what it needs and returns a plain object; Base.astro serialises whatever a
      3 // page hands it. Kept small on purpose — the goal is a correct WebSite /
      4 // TechArticle / BreadcrumbList / Dataset graph, not every property Google lists.
      5 
      6 import { SOURCE_META, SOURCES } from './taxonomy';
      7 import type { SourceId } from './taxonomy';
      8 import type { Technique, Tool } from './techniques';
      9 
     10 export type JsonLd = Record<string, unknown>;
     11 
     12 export const SITE_NAME = 'DÆMONBins';
     13 export const SITE_FALLBACK = 'https://lotl.daemon-sec.xyz';
     14 const GPL = 'https://www.gnu.org/licenses/gpl-3.0.html';
     15 
     16 const abs = (site: URL | undefined, path: string) => new URL(path, site ?? SITE_FALLBACK).href;
     17 
     18 const publisher = {
     19   '@type': 'Organization',
     20   name: 'DÆMON',
     21   url: 'https://daemon-sec.xyz',
     22 };
     23 
     24 /** Home: the site + its search box (SearchAction → /catalog?q=). */
     25 export function website(site: URL | undefined): JsonLd {
     26   const root = abs(site, '/');
     27   return {
     28     '@context': 'https://schema.org',
     29     '@type': 'WebSite',
     30     '@id': `${root}#website`,
     31     name: SITE_NAME,
     32     alternateName: 'the Off-the-Land Almanac',
     33     url: root,
     34     publisher,
     35     license: GPL,
     36     potentialAction: {
     37       '@type': 'SearchAction',
     38       target: { '@type': 'EntryPoint', urlTemplate: `${abs(site, '/catalog')}?q={search_term_string}` },
     39       'query-input': 'required name=search_term_string',
     40     },
     41   };
     42 }
     43 
     44 /** The merged dataset itself — on /credits and the catalog. */
     45 export function dataset(
     46   site: URL | undefined,
     47   counts: { techniques: number; tools: number; bySource?: Record<string, number> },
     48   commits?: Record<string, string | null | undefined>,
     49 ): JsonLd {
     50   const upstream: SourceId[] = SOURCES.filter((s) => s !== 'DAEMON');
     51   return {
     52     '@context': 'https://schema.org',
     53     '@type': 'Dataset',
     54     '@id': `${abs(site, '/')}#dataset`,
     55     name: `${SITE_NAME} technique index`,
     56     description: `${counts.techniques} command references across ${counts.tools} tools, from GTFOBins, LOLBAS, WADComs and LOOBins with DÆMON-authored additions. ATT&CK mappings are included where available.`,
     57     url: abs(site, '/catalog'),
     58     license: GPL,
     59     isAccessibleForFree: true,
     60     creator: publisher,
     61     keywords: ['living off the land', 'LOLBins', 'GTFOBins', 'LOLBAS', 'WADComs', 'MITRE ATT&CK', 'privilege escalation'],
     62     isBasedOn: upstream.map((id) => {
     63       const s = SOURCE_META[id];
     64       const sha = commits?.[id.toLowerCase()];
     65       return {
     66         '@type': 'Dataset',
     67         name: s.label,
     68         url: s.homepage,
     69         sameAs: s.repo,
     70         license: GPL,
     71         ...(sha ? { version: sha } : {}),
     72       };
     73     }),
     74     distribution: [{
     75       '@type': 'DataDownload',
     76       encodingFormat: 'application/json',
     77       contentUrl: abs(site, '/data/techniques.json'),
     78     }],
     79   };
     80 }
     81 
     82 /** /catalog and the deck indexes. */
     83 export function collectionPage(site: URL | undefined, path: string, name: string, description: string): JsonLd {
     84   return {
     85     '@context': 'https://schema.org',
     86     '@type': 'CollectionPage',
     87     name,
     88     description,
     89     url: abs(site, path),
     90     isPartOf: { '@id': `${abs(site, '/')}#website` },
     91     about: { '@id': `${abs(site, '/')}#dataset` },
     92   };
     93 }
     94 
     95 /** Catalog → Source → Tool. */
     96 export function breadcrumbs(site: URL | undefined, items: { name: string; path: string }[]): JsonLd {
     97   return {
     98     '@context': 'https://schema.org',
     99     '@type': 'BreadcrumbList',
    100     itemListElement: items.map((it, i) => ({
    101       '@type': 'ListItem',
    102       position: i + 1,
    103       name: it.name,
    104       item: abs(site, it.path),
    105     })),
    106   };
    107 }
    108 
    109 /** One tool page: the binary and its techniques as a TechArticle. */
    110 export function techArticle(
    111   site: URL | undefined,
    112   path: string,
    113   tool: Tool,
    114   techniques: Technique[],
    115   source: SourceId,
    116 ): JsonLd {
    117   const s = SOURCE_META[source];
    118   const caps = [...new Set(techniques.flatMap((t) => t.capability))];
    119   const mitre = [...new Set(techniques.flatMap((t) => t.mitre || []))];
    120   const platforms = [...new Set(techniques.flatMap((t) => t.platform))];
    121   const upstream = tool.references?.find((r) => /gtfobins|lolbas|wadcoms/.test(r)) || s.homepage;
    122   return {
    123     '@context': 'https://schema.org',
    124     '@type': 'TechArticle',
    125     headline: `${tool.name} — ${s.label}`,
    126     name: tool.name,
    127     description: `${tool.name}: ${techniques.length} living-off-the-land technique${techniques.length === 1 ? '' : 's'} from ${s.label}. Commands, MITRE ATT&CK mapping, detection and references.`,
    128     url: abs(site, path),
    129     mainEntityOfPage: abs(site, path),
    130     inLanguage: 'en',
    131     isPartOf: { '@id': `${abs(site, '/')}#website` },
    132     articleSection: s.label,
    133     keywords: [tool.name, ...caps, ...platforms, ...(tool.aliases || [])],
    134     about: mitre.map((id) => ({
    135       '@type': 'Thing',
    136       name: `MITRE ATT&CK ${id}`,
    137       url: `https://attack.mitre.org/techniques/${id.replace('.', '/')}/`,
    138     })),
    139     isBasedOn: upstream,
    140     license: GPL,
    141     author: { '@type': 'Organization', name: s.author.replace(/\s*&\s*contributors$/, ''), url: s.homepage },
    142     publisher,
    143     ...(tool.created ? { dateCreated: tool.created } : {}),
    144   };
    145 }
    146 
    147 /**
    148  * Serialise for an inline <script type="application/ld+json">. `<` is escaped
    149  * so upstream text (tool descriptions, names) can never close the script tag.
    150  */
    151 export function serialize(ld: JsonLd | JsonLd[]): string {
    152   return JSON.stringify(ld).replace(/</g, '\\u003c');
    153 }