daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

commit da75f457d3d99669e05cccdd85b4ef2160f3e56c
parent 8ce68e70d359451b37def46c69061e23238de382
Author: DAEMON <zer0sec.xp@icloud.com>
Date:   Mon, 28 Sep 2026 04:05:59 +0100

Add ired.team provenance plumbing: references, figure credit, fetch helper

ired.team publishes no licence, so nothing from it can be mirrored. This
lays the groundwork for sheets that draw on it without copying it:

- sheets schema gains an optional `references[]` array, because the flat
  `upstream*` fields hold one source and a merged sheet has two (the AD
  cheatsheet must credit S1ckB0y1337 under MIT *and* ired.team under no
  licence). SheetReferences renders them at the foot of the article.
- `figure.shot` styles a screenshot that belongs to someone else, with
  per-image credit in the caption rather than one blanket line per page.
- scripts/fetch-ired.py is a read-only reading aid, deliberately not
  named sync-* so it is never mistaken for a content generator. It
  translates GitBook macros and emits percent-encoded, SHA-pinned image
  URLs, since upstream asset names contain spaces.
- /credits states plainly that no licence is published, that nothing is
  mirrored, that screenshots are hotlinked not redistributed, and that
  the material comes down if the author asks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Diffstat:
Adocs/superpowers/specs/2026-09-28-ired-team-integration-design.md | 367+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ascripts/fetch-ired.py | 235+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/SheetReferences.astro | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/content.config.ts | 21+++++++++++++++++++++
Asrc/data/ired-source.json | 10++++++++++
Msrc/pages/credits.astro | 32++++++++++++++++++++++++++++++++
Msrc/pages/sheets/[...slug].astro | 2++
Msrc/styles/prose.css | 46++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 834 insertions(+), 0 deletions(-)

diff --git a/docs/superpowers/specs/2026-09-28-ired-team-integration-design.md b/docs/superpowers/specs/2026-09-28-ired-team-integration-design.md @@ -0,0 +1,367 @@ +# ired.team Integration — Design + +**Date:** 2026-09-28 +**Upstream:** [`mantvydasb/RedTeaming-Tactics-and-Techniques`](https://github.com/mantvydasb/RedTeaming-Tactics-and-Techniques) — the source repository behind [ired.team](https://ired.team) +**Author:** Mantvydas Baranauskas ([@mantvydasb](https://github.com/mantvydasb)) +**Pinned at:** `8cdbdd60eb4a8997e689649f3911f7c893e59ed9` (2026-06-13) +**Licence:** **none published.** The GitHub API returns `"license": null`; there is no `LICENSE` +file and `/license` returns 404. + +--- + +## 1. Intent + +Bring the technique coverage of ired.team onto DÆMON//SEC **without giving it a section of its +own**. ired.team material is either folded into the sheet that already covers the topic, or becomes +a new sheet filed under an existing category. No entry is added to `taxonomy.ts`; no `/ired` route +exists; the site's 13 tabs stay 13 tabs. + +Success looks like: an operator searching the site for "process hollowing" or "AMSI bypass" finds a +DÆMON//SEC sheet in the house voice, with the screenshots that make the technique legible, and a +credit line that sends them to ired.team for the full lab write-up. + +### What the user asked for, verbatim + +> "so hacktricks has been added can you add this as well https://www.ired.team dont make it have its +> own sections just read the guides and add them to the site or add to the owns that it goes over." + +> "use the images as well and credit the images" + +### Decisions taken by the user during brainstorming + +| Question | Decision | +|---|---| +| Placement | **Hybrid** — merge into sheets that already cover the topic; new sheets under existing categories where nothing does | +| Text fidelity | **Rewritten** in the DÆMON//SEC sheet voice, credited as `derived` | +| First-pass scope | **Curated, ~40–50 topics**, delivered in reviewable batches | +| Images | **Hotlinked** at the pinned SHA, per-image credit, nothing copied into the repo | + +### Assumptions + +- "Don't make it have its own sections" governs site navigation, so `subcategory` frontmatter is the + organising tool rather than a new category. Existing category pages already render subcategories + (Active Directory uses 11 of them), so this needs no template work. +- The curated ~45 are chosen for *capability the site does not currently have*, not for upstream + page length. + +--- + +## 2. Licence position + +This is the constraint that shapes everything else, and the repo has already ruled on the +identical situation twice. + +`src/content.config.ts` on the `upstreamLicense` enum: + +> `none` means the upstream publishes no licence at all — that grants no right to copy, so such a +> sheet must be a link-only stub, not a mirror. + +`docs/provenance-audit.md` on `enumeration/awesome-nmap-grep.md`, an unlicensed upstream: + +> No grant of rights exists, so republication is not permitted by any licence. […] Adding a credit +> line alone does **not** make this compliant. + +Therefore, for ired.team: + +| | Handling | +|---|---| +| Prose | **Not copied.** Written fresh in the house voice. ired.team's guides are first-person lab narrative ("This lab is my attempt to…", shout-outs to collaborators); a command-first cheatsheet is a different work, not a reformat of that one. | +| Commands, API sequences, struct layouts, registry paths | Reproduced as the technical facts they are. These are not the upstream's creative expression, and most are not the upstream's inventions either — the README says so: "Most of these techniques are discovered by other security researchers and I do not claim their ownership." | +| Screenshots | **Hotlinked** at the pinned SHA. Referenced, never reproduced on our domain. 3–6 per sheet, each captioned and credited. | +| Credit | Mandatory on every sheet that draws on ired.team, plus a provenance plate on `/credits`. | + +The HackTricks approach — adapted mirror under CC BY-NC — is **not available here**, because +HackTricks publishes a licence and ired.team does not. + +### What this does not claim + +Hotlinking is chosen because it avoids reproduction, not because it is a licence. If Mantvydas +Baranauskas asks for the screenshots or the derived sheets to come down, they come down. The +`/credits` plate names him and links the upstream so that request is easy to make. + +--- + +## 3. Provenance model + +### 3.1 Problem + +`SheetCredit` renders one upstream per sheet, driven by six flat frontmatter fields. That is +sufficient for a new sheet derived from one ired.team guide, but it cannot express a *merged* sheet: +`active-directory/active-directory-cheat-sheet.md` must credit S1ckB0y1337 under MIT (per the +provenance audit) **and** ired.team with no licence. One slot, two upstreams. + +### 3.2 Schema change + +Add an optional `references` array to the `sheets` collection. Generic by design — merging any +future third-party material reuses it. + +```ts +references: z + .array( + z.object({ + name: z.string(), + url: z.string().url(), + author: z.string().optional(), + license: z + .enum(['MIT', 'BSD-2-Clause', 'BSD-3-Clause', 'Apache-2.0', 'CC-BY-NC-4.0', 'none', 'proprietary']) + .optional(), + relation: z.enum(['verbatim', 'derived', 'inspired', 'link-only']).optional(), + // What this source contributed, so the credit is specific rather than decorative. + note: z.string().optional(), + }), + ) + .default([]), +``` + +No existing field changes and no existing sheet is touched by the schema edit, so the 264 sheets on +disk keep validating. + +### 3.3 Which mechanism a sheet uses + +| Sheet kind | Credit mechanism | +|---|---| +| New sheet, substance from one ired.team guide | Existing flat fields: `upstreamName: "ired.team"`, `upstreamUrl`, `upstreamAuthor: "Mantvydas Baranauskas"`, `upstreamLicense: none`, `upstreamRelation: derived`. `SheetCredit` already renders this as "Derived from ired.team by **Mantvydas Baranauskas** · no licence published · credits". | +| New sheet drawing on several guides | Flat fields naming ired.team, plus `references` entries for the specific guides. | +| Existing sheet gaining an ired.team-derived section | `references` entry only. Flat fields stay as they are, so prior attribution is preserved. | + +### 3.4 New component — `src/components/SheetReferences.astro` + +Renders a **Sources & further reading** plate at the foot of the article when `references` is +non-empty. Reuses the `.patt-credit` visual language already shared by `SheetCredit`, +`PayloadCredit` and `HackTricksCredit`: square plate, corner brackets, left accent bar, JetBrains +Mono micro-labels, cream-on-night inversion. One list row per reference — relation badge, linked +name, author, licence label, `note`. + +Licence wording follows `SheetCredit` exactly, including `none` → "no licence published", so the two +panels cannot disagree about the same upstream. + +Mounted in `src/pages/sheets/[...slug].astro` after `<Content />`, inside the `<article>`. + +--- + +## 4. Images + +### 4.1 URL form + +``` +https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/<percent-encoded filename> +``` + +Pinning to the SHA rather than `master` means an upstream reorganisation cannot silently swap or +break the image a caption describes. Asset filenames routinely contain spaces and parentheses +(`Screenshot from 2019-04-28 16-28-59.png`), so every path segment is percent-encoded; +`fetch-ired.py` emits the finished URL so this is never done by hand. + +### 4.2 Markup + +Sheets are plain `.md`, so an Astro component is not available inline. Raw HTML passes through +Markdown, so each screenshot is a `<figure>`: + +```html +<figure class="shot"> + <img src="https://raw.githubusercontent.com/mantvydasb/RedTeaming-Tactics-and-Techniques/8cdbdd60eb4a8997e689649f3911f7c893e59ed9/.gitbook/assets/Screenshot%20from%202019-04-28%2016-36-33.png" + alt="WinDBG showing the host process PEB at 0100e000 with ImageBaseAddress eight bytes in" + loading="lazy" referrerpolicy="no-referrer"> + <figcaption>Reading <code>ImageBaseAddress</code> from the host PEB in WinDBG. + <span class="shot-credit">ired.team · Mantvydas Baranauskas</span> + </figcaption> +</figure> +``` + +Rules: +- `alt` describes what the shot *shows*, for screen readers and for the case where the hotlink dies. +- `loading="lazy"` — a sheet with six remote screenshots must not block first paint. +- `referrerpolicy="no-referrer"` — don't leak reader browsing to GitHub. +- `.shot-credit` on **every** figure. Credit is per-image, as requested, not one blanket line. + +### 4.3 CSS + +Add `.shot`, `.shot img`, `.shot figcaption`, `.shot-credit` to `src/styles/prose.css`, matching the +existing `figure.code-pane` treatment: square corners, 1px rule, caption in the terminal font at +micro-label size, `.shot-credit` in `--fg-faint`. Both themes, since `prose.css` already carries the +cream/night pairs. + +### 4.4 Selection + +3–6 screenshots per sheet — the ones that carry information the text cannot (debugger state, a +detection artefact in Process Hacker, a SysInternals view). Upstream pages with 30+ near-identical +shots get the decisive few. This keeps pages light and keeps the use clearly illustrative. + +--- + +## 5. Placement map + +No new `taxonomy.ts` entries. `subcategory` does the grouping. + +| ired.team cluster | Guides | Category | Subcategory | Mode | +|---|---|---|---|---| +| `code-injection-process-injection` | 41 | `exploitation` | Process Injection | new sheets | +| `defense-evasion` | 32 | `exploitation` | Defense Evasion | new sheets | +| `persistence` | 25 | `exploitation` | Persistence | new sheets | +| `code-execution` | 21 | `exploitation` | Code Execution | new sheets | +| `initial-access` | 16 | `exploitation` | Initial Access | new sheets | +| `credential-access-and-credential-dumping` | 22 | `password-attacks` | Credential Dumping | new sheets + merge into the two credential-hunting sheets | +| `active-directory-kerberos-abuse` | 29 | `active-directory` | Kerberos & Delegation *(existing)* | merge — AD is already 136 sheets deep here | +| `lateral-movement` | 22 | `active-directory` | Lateral Movement *(existing)* | new sheets + merge | +| `privilege-escalation` | 10 | `privilege-escalation` | — | merge into the two existing Windows/Linux sheets | +| `red-team-infrastructure` | 8 | `tunneling-pivoting` | Red Team Infrastructure | new sheets | +| `enumeration-and-discovery` | 10 | `enumeration` | — | merge into `windows-enumeration.md` | +| `windows-kernel-internals`, `windows-kernel`, ETW, PEB, `get-injectedthread` | 42 | `dfir` | Windows Internals | new sheets — detection-side framing | + +`exploitation` grows from 4 sheets to roughly 30 across batches 1–3, which is the single biggest gain: the category's +own blurb promises "injection, upload, and shell delivery" and currently ships four sheets, none of +which is about injection. + +### Why not a new category + +A "Post-Exploitation" or "Red Team" tab would be the natural taxonomy for injection + evasion + +persistence. The user ruled it out explicitly. `subcategory` gives the same reading experience on +the category page without adding a tab, so the constraint costs nothing structurally. Noting it here +because if the user later changes their mind, the move is a frontmatter rename, not a rewrite. + +--- + +## 6. Tooling + +### 6.1 `scripts/fetch-ired.py` + +Read-only research helper. **It never writes to `src/`.** Content is written by hand; this script +only makes reading the upstream cheap and makes image URLs correct. + +``` +usage: fetch-ired.py [--list] [--tree] [--get PATH ...] [--images PATH] + [--out DIR] [--refresh-sha] +``` + +- `--list` / `--tree` — the 287 guide paths, grouped by cluster. +- `--get PATH` — fetch a guide at the pinned SHA into `--out` (default: the session scratchpad), + with GitBook macros translated so the text is readable: `{% hint style=X %}` → a blockquote, + `{% content-ref %}` → a plain link, `{% code %}` / `{% endcode %}` stripped, `{% embed %}` → link. +- `--images PATH` — print every image referenced by that guide as a finished, percent-encoded, + SHA-pinned URL ready to paste into a `<figure>`. +- Pins from `src/data/ired-source.json`; `--refresh-sha` re-pins and rewrites that file. + +Placed in `scripts/` beside `sync-hacktricks.py`, but deliberately *not* named `sync-*`: the +`sync-*` scripts generate content collections, and this one must never be mistaken for one. + +### 6.2 `src/data/ired-source.json` + +```json +{ + "repo": "mantvydasb/RedTeaming-Tactics-and-Techniques", + "site": "https://ired.team", + "author": "Mantvydas Baranauskas", + "sha": "8cdbdd60eb4a8997e689649f3911f7c893e59ed9", + "fetched": "2026-09-28", + "license": null +} +``` + +One place to bump the pin. `credits.astro` reads it so the displayed commit cannot drift from the +commit the image URLs use. + +### 6.3 `src/pages/credits.astro` + +New ired.team section, placed after HackTricks, following the existing `.slab.corners.provenance` +pattern. It must state plainly: + +- Upstream repo and site, author named. +- **Licence: none published.** No rights are granted to copy, so nothing is mirrored. +- Prose on DÆMON//SEC is original; commands and technique facts are reproduced as facts. +- Screenshots load from the upstream repository at a pinned commit and are **not** redistributed. +- A takedown sentence: if the author objects, the material is removed. + +### 6.4 `docs/provenance-audit.md` + +Append an ired.team row so the audit stays the single record of third-party content. It currently +says 75 sheets never entered triage; adding ~45 sheets without recording them would make that +honesty statement wrong. + +--- + +## 7. Sheet anatomy + +```markdown +--- +title: "Process Hollowing" +description: "Carving a suspended process's image out of memory and running a replacement PE in its place, plus the relocation fixups that make it work." +category: exploitation +subcategory: "Process Injection" +tags: [process-injection, evasion, windows, maldev] +tools: [windbg, visual-studio, process-hacker] +difficulty: advanced +updated: 2026-09-28 +upstreamName: "ired.team" +upstreamUrl: "https://ired.team/offensive-security/code-injection-process-injection/process-hollowing-and-pe-image-relocations" +upstreamAuthor: "Mantvydas Baranauskas" +upstreamLicense: none +upstreamRelation: derived +references: + - name: "Process-Hollowing" + url: "https://github.com/m0n0ph1/Process-Hollowing" + author: "m0n0ph1" + relation: inspired + note: "The reference implementation ired.team's lab works from." +--- + +## What it does +## Prerequisites +## Walkthrough ← commands / API sequence + the 3–6 credited figures +## Detection ← what defenders see; DÆMON//SEC's own addition +## References +``` + +House-style requirements: +- Every code fence carries a language. `validate-content.py` warns on bare fences and the baseline + already has 100+ such warnings; this work must not add to them. +- A **Detection** section on every offensive sheet. ired.team is attacker-side notes; the detection + framing is original DÆMON//SEC value and is also what makes these derived works rather than + restatements. +- Internal cross-links use `/sheets/<category>/<slug>` — `validate-content.py` resolves these + against files on disk and fails on a broken or self-referential one. +- No `[[wikilinks]]`, no `![[embeds]]`, no `%%comments%%`. + +--- + +## 8. Verification + +| Gate | Command | Pass condition | +|---|---|---| +| Content validator | `python3 scripts/validate-content.py` | **No new `ISSUES`** vs. the recorded baseline. Baseline on `main` is already non-zero: `MISSING: exploitation/shell-stabilization` plus 201 `EXTRA` and ~113 fence warnings. New sheets appear under `EXTRA`, which is informational (`ok = not missing and not issues`). | +| Build + tests | `npm run test` (`astro build && node --test`) | Green, including `test/internal-links.test.mjs`. | +| Image spot check | `curl -sIL <figure src>` on a sample from each batch | `200` and an `image/*` content type. | +| Rendered page | Build, then read one new sheet and one merged sheet | Credit panel, references plate and figures all render; captions carry credit; both themes. | + +The pre-existing baseline failure is recorded rather than fixed: `content-manifest.json` is stale by +200 sheets and repairing it is unrelated to this work. It is called out so a reviewer does not read +a red validator as regression. + +--- + +## 9. Delivery + +~45 topics in batches of 8–10. Each batch is one jj change on its own bookmark off `main` — per the +user's standing rule, nothing is pushed to `main` directly. + +| Batch | Content | Why this order | +|---|---|---| +| 0 | Schema `references`, `SheetReferences.astro`, `prose.css` figure styles, `fetch-ired.py`, `ired-source.json`, `credits.astro` plate | Plumbing first, so batch 1 proves the whole pipeline end to end | +| 1 | Process injection — ~9 sheets | Biggest gap on the site; exercises figures hardest (debugger-heavy) | +| 2 | Defense evasion — ~9 sheets | AMSI, ETW, unhooking | +| 3 | Persistence + code execution — ~10 sheets | | +| 4 | Credential dumping + privesc — ~9 sheets, includes the first merges | Merge path and `references`-only credit get exercised | +| 5 | Lateral movement, red team infra, Windows internals, enumeration merges — ~9 sheets | | + +Batch 0 ships with batch 1 so there is something to look at. + +--- + +## 10. Out of scope + +- The other ~240 upstream guides. The tooling and credit plumbing handle them; only the writing + remains, and it can continue in later batches. +- Repairing the stale `content-manifest.json` or the ~113 pre-existing bare-fence warnings. +- The four open items in `docs/provenance-audit.md` §2 (awesome-nmap-grep, the two HTB sheets, + the S1ckB0y1337 MIT notice). Unrelated to ired.team and each needs a decision from the user. +- The OSINT section requested mid-session (Bellingcat toolkit + tools.osintnewsletter.com). That one + *does* want its own tab, so it gets its own design pass after this work lands. diff --git a/scripts/fetch-ired.py b/scripts/fetch-ired.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +"""Read-only research helper for ired.team (RedTeaming-Tactics-and-Techniques). + +This is deliberately NOT a `sync-*` script. The sync scripts generate content +collections; this one writes nothing into `src/`. ired.team publishes no licence +(the GitHub API returns `"license": null`, there is no LICENSE file, `/license` +404s), so there is no grant of rights to copy its prose — the same situation +`docs/provenance-audit.md` adjudicated for awesome-nmap-grep, where it recorded +that credit alone does not cure a missing licence. + +So this script only makes the upstream cheap to *read*, and makes image URLs +correct. Sheets are then written by hand in the house voice, and their +screenshots hotlink to the pinned commit rather than being redistributed. + + ./scripts/fetch-ired.py --tree + ./scripts/fetch-ired.py --get offensive-security/persistence/t1015-sethc.md + ./scripts/fetch-ired.py --images offensive-security/persistence/t1015-sethc.md + ./scripts/fetch-ired.py --refresh-sha + +Guide paths may be given with or without the `.md` suffix. +""" +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +import urllib.error +import urllib.request +from urllib.parse import quote + +ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +SOURCE_JSON = os.path.join(ROOT, "src", "data", "ired-source.json") +DEFAULT_OUT = os.environ.get("CLAUDE_SCRATCHPAD") or os.path.join(ROOT, ".ired-cache") + + +def load_source() -> dict: + with open(SOURCE_JSON, encoding="utf-8") as fh: + return json.load(fh) + + +def get(url: str) -> bytes: + req = urllib.request.Request(url, headers={"User-Agent": "daemon-sec-cheatsheet/fetch-ired"}) + with urllib.request.urlopen(req, timeout=30) as resp: + return resp.read() + + +def raw_url(src: dict, path: str) -> str: + """Pinned raw URL for an upstream path. + + Every segment is percent-encoded. Upstream asset names are GitBook exports + like `Screenshot from 2019-04-28 16-28-59.png` and routinely carry spaces, + parentheses and the odd `+`, so hand-writing these URLs reliably produces + a silently broken image. + """ + encoded = "/".join(quote(seg, safe="") for seg in path.split("/")) + return f"https://raw.githubusercontent.com/{src['repo']}/{src['sha']}/{encoded}" + + +def site_url(src: dict, path: str) -> str: + """The human-facing ired.team URL for a guide, for the credit line.""" + slug = re.sub(r"\.md$", "", path) + slug = re.sub(r"/README$", "", slug) + return f"{src['site']}/{slug}" + + +def normalise(path: str) -> str: + return path if path.endswith(".md") else path + ".md" + + +# ---- tree ---------------------------------------------------------------- + +def fetch_tree(src: dict) -> list[str]: + url = f"https://api.github.com/repos/{src['repo']}/git/trees/{src['sha']}?recursive=1" + data = json.loads(get(url)) + if data.get("truncated"): + print("warning: upstream tree response was truncated", file=sys.stderr) + return [t["path"] for t in data["tree"] if t["path"].endswith(".md")] + + +def cmd_tree(src: dict, grouped: bool) -> None: + paths = sorted(fetch_tree(src)) + if not grouped: + print("\n".join(paths)) + return + clusters: dict[str, list[str]] = {} + for p in paths: + parts = p.split("/") + key = "/".join(parts[:2]) if len(parts) > 2 else (parts[0] if len(parts) > 1 else "(root)") + clusters.setdefault(key, []).append(p) + for key in sorted(clusters): + print(f"\n## {key} ({len(clusters[key])})") + for p in clusters[key]: + print(f" {p}") + + +# ---- guide text ---------------------------------------------------------- + +GITBOOK_HINT = re.compile(r"\{%\s*hint\s+style=\"?(\w+)\"?\s*%\}(.*?)\{%\s*endhint\s*%\}", re.S) +GITBOOK_CONTENT_REF = re.compile(r"\{%\s*content-ref\s+url=\"([^\"]+)\"\s*%\}(.*?)\{%\s*endcontent-ref\s*%\}", re.S) +GITBOOK_EMBED = re.compile(r"\{%\s*embed\s+url=\"([^\"]+)\"\s*%\}(?:\s*\{%\s*endembed\s*%\})?", re.S) +GITBOOK_CODE = re.compile(r"\{%\s*(?:end)?code[^%]*%\}") +GITBOOK_TABS = re.compile(r"\{%\s*(?:end)?tabs?[^%]*%\}") +GITBOOK_FILE = re.compile(r"\{%\s*file\s+src=\"([^\"]+)\"\s*%\}") +GITBOOK_LEFTOVER = re.compile(r"\{%.*?%\}", re.S) + + +def degitbook(text: str) -> str: + """Translate GitBook macros into plain markdown so the guide is readable. + + Nothing here is about producing publishable text — it is about not having + to mentally parse `{% hint %}` blocks while reading 40 guides. + """ + text = GITBOOK_HINT.sub(lambda m: f"\n> **{m.group(1).upper()}:** {m.group(2).strip()}\n", text) + text = GITBOOK_CONTENT_REF.sub(lambda m: f"\n→ see `{m.group(1)}`\n", text) + text = GITBOOK_EMBED.sub(lambda m: f"\n→ {m.group(1)}\n", text) + text = GITBOOK_FILE.sub(lambda m: f"\n→ attached file: `{m.group(1)}`\n", text) + text = GITBOOK_CODE.sub("", text) + text = GITBOOK_TABS.sub("", text) + text = GITBOOK_LEFTOVER.sub("", text) + # GitBook escapes underscores in prose; unescape so identifiers read right. + text = text.replace("\\_", "_") + return re.sub(r"\n{3,}", "\n\n", text) + + +def cmd_get(src: dict, paths: list[str], out_dir: str) -> None: + os.makedirs(out_dir, exist_ok=True) + for path in paths: + path = normalise(path) + try: + body = get(raw_url(src, path)).decode("utf-8", "replace") + except urllib.error.HTTPError as exc: + print(f"!! {path}: HTTP {exc.code}", file=sys.stderr) + continue + dest = os.path.join(out_dir, path.replace("/", "__")) + header = ( + f"<!-- upstream: {path}\n" + f" site: {site_url(src, path)}\n" + f" commit: {src['sha']}\n" + f" licence: NONE PUBLISHED — do not copy this prose. Rewrite. -->\n\n" + ) + with open(dest, "w", encoding="utf-8") as fh: + fh.write(header + degitbook(body)) + print(dest) + + +# ---- images -------------------------------------------------------------- + +# Markdown images, including GitBook's angle-bracket form for paths with spaces: +# ![](<../../.gitbook/assets/Screenshot from 2019-04-28 16-28-59.png>) +IMG = re.compile(r"!\[([^\]]*)\]\(\s*<?([^>)\s]+(?:\s[^>)]*)?)>?\s*\)") + + +def resolve(guide_path: str, target: str) -> str: + """Resolve a guide-relative image target to a repo-root-relative path.""" + if target.startswith(("http://", "https://")): + return target + base = os.path.dirname(guide_path) + return os.path.normpath(os.path.join(base, target)).replace(os.sep, "/") + + +def cmd_images(src: dict, paths: list[str]) -> None: + for path in paths: + path = normalise(path) + try: + body = get(raw_url(src, path)).decode("utf-8", "replace") + except urllib.error.HTTPError as exc: + print(f"!! {path}: HTTP {exc.code}", file=sys.stderr) + continue + matches = IMG.findall(body) + print(f"\n## {path} ({len(matches)} images)") + print(f" credit: ired.team · {src['author']}") + print(f" guide: {site_url(src, path)}") + seen: set[str] = set() + for alt, target in matches: + resolved = resolve(path, target.strip()) + if resolved in seen: + continue + seen.add(resolved) + url = resolved if resolved.startswith("http") else raw_url(src, resolved) + print(f"\n alt-hint: {alt.strip() or '(none upstream — write one)'}") + print(f" {url}") + + +# ---- sha ----------------------------------------------------------------- + +def cmd_refresh_sha(src: dict) -> None: + url = f"https://api.github.com/repos/{src['repo']}/commits/{src.get('default_branch', 'master')}" + head = json.loads(get(url)) + old, new = src["sha"], head["sha"] + if old == new: + print(f"already pinned at {new}") + return + print(f"{old}\n -> {new} ({head['commit']['committer']['date']})") + print( + "\nNOTE: every <figure class=\"shot\"> URL already in src/content/sheets still\n" + " points at the old commit. Re-pin those too, or the captions and the\n" + " images can drift apart.", + file=sys.stderr, + ) + src["sha"] = new + with open(SOURCE_JSON, "w", encoding="utf-8") as fh: + json.dump(src, fh, indent=2) + fh.write("\n") + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--tree", action="store_true", help="list guide paths grouped by cluster") + ap.add_argument("--list", action="store_true", help="list guide paths, one per line") + ap.add_argument("--get", nargs="+", metavar="PATH", help="fetch guides as readable markdown") + ap.add_argument("--images", nargs="+", metavar="PATH", help="print pinned, encoded image URLs") + ap.add_argument("--out", default=DEFAULT_OUT, metavar="DIR", help=f"output dir for --get (default: {DEFAULT_OUT})") + ap.add_argument("--refresh-sha", action="store_true", help="re-pin to upstream HEAD") + args = ap.parse_args() + + src = load_source() + if not any([args.tree, args.list, args.get, args.images, args.refresh_sha]): + ap.print_help() + return 1 + if args.refresh_sha: + cmd_refresh_sha(src) + src = load_source() + if args.tree or args.list: + cmd_tree(src, grouped=bool(args.tree)) + if args.get: + cmd_get(src, args.get, args.out) + if args.images: + cmd_images(src, args.images) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/components/SheetReferences.astro b/src/components/SheetReferences.astro @@ -0,0 +1,121 @@ +--- +/** + * "Sources & further reading" plate for a sheet with secondary provenance. + * + * SheetCredit carries the one upstream a sheet *is* derived from, and sits + * above the article as a header banner. This carries the sources a sheet + * merely draws on — one row each — and sits at the foot, where a reader who + * has finished the page is looking for where to go next. + * + * Licence wording is deliberately the same mapping SheetCredit uses. Two + * panels describing the same upstream in different words is how a site ends + * up implying a grant that does not exist, so `none` reads "no licence + * published" in both places or in neither. + */ +import Icon from './Icon.astro'; +import { url } from '../lib/url'; + +interface Reference { + name: string; + url: string; + author?: string; + license?: string; + relation?: string; + note?: string; +} +interface Props { + references?: Reference[]; +} +const { references = [] } = Astro.props; + +const BADGE: Record<string, string> = { + verbatim: 'mirror', + derived: 'derived', + inspired: 'inspired', + 'link-only': 'link', +}; + +function licenceLabel(license?: string): string | null { + if (!license) return null; + if (license === 'none') return 'no licence published'; + if (license === 'proprietary') return 'proprietary — all rights reserved'; + return license; +} +--- +{references.length > 0 && ( + <aside class="refs plate corners" aria-labelledby="refs-heading"> + <p class="micro refs__label" id="refs-heading">// sources &amp; further reading</p> + <ul class="refs__list"> + {references.map((ref) => ( + <li class="refs__item"> + <span class="patt-badge refs__badge">{BADGE[ref.relation ?? ''] ?? 'credit'}</span> + <span class="refs__body"> + <a class="refs__name" href={ref.url} target="_blank" rel="noopener">{ref.name}</a> + {ref.author && <span class="refs__author"> by <strong>{ref.author}</strong></span>} + {ref.note && <span class="refs__note">{ref.note}</span>} + {licenceLabel(ref.license) && ( + <span class="refs__lic">{licenceLabel(ref.license)}</span> + )} + </span> + </li> + ))} + </ul> + <p class="refs__foot"> + <Icon name="github" style="width:13px;height:13px;" /> + Upstream repositories remain canonical — corrections belong there. + <a href={url('credits')}>Full credits &amp; licences</a> + </p> + </aside> +)} + +<style> + /* Same object as the credit banner above the article (SheetCredit) and the + provenance slabs on /credits: square plate, corner brackets, left accent + bar, provenance metadata set in the terminal font at micro-label size. + Unlike SheetCredit this one does NOT pin its palette — it reads as part + of the page it closes, not as a foreign notice pasted on top. */ + .refs { + margin: 2.5rem 0 0; + padding: 1rem 1.2rem 1.1rem; + border-left: 2px solid var(--iris); + --bracket: var(--iris); + } + .refs__label { color: var(--fg-faint); margin: 0 0 0.75rem; } + .refs__list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.7rem; } + .refs__item { display: flex; align-items: flex-start; gap: 0.7rem; margin: 0; } + .refs__badge { flex: none; margin-top: 0.1rem; color: var(--iris); border-color: color-mix(in oklab, var(--iris) 50%, transparent); } + .refs__body { display: block; min-width: 0; } + .refs__name { font-weight: 600; } + .refs__author { color: var(--fg-dim); } + /* Note and licence each take their own line so a long `note` cannot shove + the licence off the end of a phone-width row, where it is the one part + that must not be missed. */ + .refs__note, + .refs__lic { + display: block; + color: var(--fg-dim); + font-size: var(--step--1, 0.9em); + } + .refs__lic { + color: var(--fg-faint); + font-family: var(--font-term, 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, monospace); + font-size: var(--step-micro); + letter-spacing: var(--track-micro); + text-transform: uppercase; + margin-top: 0.2rem; + } + .refs__foot { + display: flex; + align-items: center; + gap: 0.45rem; + flex-wrap: wrap; + margin: 0.95rem 0 0; + padding-top: 0.7rem; + border-top: 1px solid var(--rule); + color: var(--fg-faint); + font-family: var(--font-term, 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, monospace); + font-size: var(--step-micro); + letter-spacing: var(--track-micro); + text-transform: uppercase; + } +</style> diff --git a/src/content.config.ts b/src/content.config.ts @@ -37,6 +37,27 @@ const sheets = defineCollection({ upstreamRelation: z.enum(['verbatim', 'derived', 'inspired', 'link-only']).optional(), // The notice MIT/BSD/Apache require to travel with a copy. upstreamCopyright: z.string().optional(), + // Secondary provenance, for a sheet that draws on more than one upstream. + // The flat `upstream*` fields above hold a single source, which is enough + // for a sheet that *is* a copy or a rewrite of one thing; it cannot express + // a sheet that reproduces an MIT cheatsheet and also folds in a section + // derived from somewhere else. Each entry credits one source and says in + // `note` what it actually contributed, so the credit is specific rather + // than a wall of links. Rendered by SheetReferences at the foot of the page. + references: z + .array( + z.object({ + name: z.string(), + url: z.string().url(), + author: z.string().optional(), + license: z + .enum(['MIT', 'BSD-2-Clause', 'BSD-3-Clause', 'Apache-2.0', 'CC-BY-NC-4.0', 'none', 'proprietary']) + .optional(), + relation: z.enum(['verbatim', 'derived', 'inspired', 'link-only']).optional(), + note: z.string().optional(), + }), + ) + .default([]), // When set, the sheet embeds/links a PDF that lives in public/pdfs/<pdf>. pdf: z.string().optional(), draft: z.boolean().default(false), diff --git a/src/data/ired-source.json b/src/data/ired-source.json @@ -0,0 +1,10 @@ +{ + "repo": "mantvydasb/RedTeaming-Tactics-and-Techniques", + "site": "https://ired.team", + "author": "Mantvydas Baranauskas", + "authorUrl": "https://github.com/mantvydasb", + "sha": "8cdbdd60eb4a8997e689649f3911f7c893e59ed9", + "fetched": "2026-09-28", + "license": null, + "note": "No licence is published upstream, so nothing is mirrored. Sheet prose is original; screenshots are hotlinked from this pinned commit, never redistributed." +} diff --git a/src/pages/credits.astro b/src/pages/credits.astro @@ -4,6 +4,7 @@ import { payloadsByTopic } from '../lib/payloads'; import { internalBySection, internalRootPages } from '../lib/internal'; import { hackTricksBySection } from '../lib/hacktricks'; import { url } from '../lib/url'; +import iredSource from '../data/ired-source.json'; const groups = await payloadsByTopic(); const totalPages = groups.reduce((n, g) => n + g.count, 0); @@ -129,6 +130,37 @@ SOFTWARE.`; this site can then regenerate its adapted copy from a newer commit. </p> + <h2>ired.team</h2> + <p> + <a href="https://ired.team" target="_blank" rel="noopener">ired.team</a> — the red teaming notes of + <strong>Mantvydas Baranauskas</strong> (<a href={iredSource.authorUrl} target="_blank" rel="noopener">@mantvydasb</a>) — + is the reference behind a number of sheets in Exploitation, Password Attacks, Privilege Escalation, + Tunneling &amp; Pivoting and DFIR. His write-ups on process injection, defense evasion and persistence are + some of the most careful hands-on documentation of those techniques anywhere. + </p> + <p> + <strong>ired.team publishes no licence.</strong> That grants no right to copy it, so nothing from it is + mirrored here. Sheets that draw on it are written from scratch for this site; commands, API sequences and + technique facts are reproduced as facts; and the screenshots load from the upstream repository at a pinned + commit rather than being copied onto this domain. + </p> + <div class="slab corners provenance"> + <p class="micro provenance__label">// provenance · ired.team</p> + <ul> + <li><strong>Upstream:</strong> <a href={`https://github.com/${iredSource.repo}`} target="_blank" rel="noopener">github.com/{iredSource.repo}</a></li> + <li><strong>Author:</strong> {iredSource.author}. Per the upstream README, most techniques documented there were discovered by other researchers and he does not claim their ownership.</li> + <li><strong>License:</strong> <strong>none published</strong> — no <code>LICENSE</code> file, the repository <code>license</code> field is <code>null</code>, and the licence API returns 404.</li> + <li><strong>Pinned at commit:</strong> <code>{iredSource.sha}</code> (read {iredSource.fetched})</li> + <li><strong>Fidelity:</strong> <em>not a mirror.</em> No upstream prose is reproduced. Sheet text is original; each sheet is marked <code>derived</code> and links the guide it draws on.</li> + <li><strong>Images:</strong> screenshots remain the author's. They are hotlinked from the upstream repository at the commit above, credited individually in each caption, and are <strong>not</strong> redistributed from this site.</li> + <li><strong>On request:</strong> if the author would prefer this material not be referenced here, it will be removed — open an issue on the site repository or contact the maintainer.</li> + </ul> + </div> + <p class="muted"> + ired.team remains canonical and considerably deeper than any summary of it. Every sheet that draws on it + links the original guide; read it there. + </p> + <h2>The DÆMON//SEC cheatsheets</h2> <p> The hand-curated <a href={url('')}>cheatsheet vault</a> is compiled from public tooling documentation and diff --git a/src/pages/sheets/[...slug].astro b/src/pages/sheets/[...slug].astro @@ -3,6 +3,7 @@ import { render } from 'astro:content'; import Base from '../../layouts/Base.astro'; import Icon from '../../components/Icon.astro'; import SheetCredit from '../../components/SheetCredit.astro'; +import SheetReferences from '../../components/SheetReferences.astro'; import { categoryOf } from '../../lib/taxonomy'; import { sheetsByCategory, prevNext, sheetHref, tagSlug, type Sheet } from '../../lib/sheets'; import { url } from '../../lib/url'; @@ -78,6 +79,7 @@ const pdfHref = d.pdf ? url(`pdfs/${d.pdf}`) : null; </figure> )} <Content /> + <SheetReferences references={d.references} /> </article> {/* One TOC serves both layouts. Below 1040px the grid puts it above the diff --git a/src/styles/prose.css b/src/styles/prose.css @@ -297,3 +297,49 @@ /* First heading of the doc is the page title (rendered separately) */ .prose > h1:first-child { display: none; } + +/* ---- Credited screenshot (`<figure class="shot">`) ---------------------- + A screenshot that belongs to someone else. The frame is the diagram plate's + (square, ruled, mono caption) but the caption carries a second line: who the + image belongs to. That credit is per-image rather than one blanket line per + page because an unlicensed upstream is exactly the case where "which of + these pictures is yours?" must have an answer on the image itself. + + The `<img>` is remote by design — pinned to an upstream commit, never + copied into this repo — so the box has to survive the hotlink failing: + a min-height and the alt text keep the caption meaningful on a broken + image instead of collapsing the figure to a caption floating in space. */ +.prose figure.shot { + margin: 1.6rem 0; + border: 1px solid var(--rule); + background: var(--surface); +} +.prose figure.shot img { + display: block; + width: 100%; + height: auto; + min-height: 2.5rem; + margin: 0; + border: 0; + border-bottom: 1px solid var(--rule); + /* Screenshots are mostly debugger and console captures on a light chrome; + letterbox rather than crop so register values stay readable. */ + object-fit: contain; + background: #faf4ed; +} +.prose figure.shot figcaption { + padding: 0.6rem 0.8rem; + color: var(--fg-dim); + font-size: var(--step--1, 0.9em); + line-height: 1.5; +} +.prose figure.shot figcaption code { font-size: 0.95em; } +.prose figure.shot .shot-credit { + display: block; + margin-top: 0.3rem; + color: var(--fg-faint); + font-family: var(--font-term, 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, monospace); + font-size: var(--step-micro); + letter-spacing: var(--track-micro); + text-transform: uppercase; +}