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:
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:
+# 
+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 & 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 & 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 & 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;
+}