guardlink

Threat Model
Assets 18
Open 14
Controls 13
Coverage 87%
/
Filtered to feature:
Executive Summary
D
High Risk 14 unmitigated exposure(s), 3 high severity
Graded from confirmed findings first, then open exposures by severity. Mitigated exposures do not count.
What to do next5 items
0 critical, 3 high. Each needs a @mitigates with a real control, or a human @accepts with a reason.
guardlink review .
Show
The code beneath them changed since they were verified; 26 of them are mitigations or acceptances that may no longer hold. Re-check the code, then re-lock.
guardlink verify --stale
Show
The introducing commit credits an AI tool. Worth a closer review, and worth knowing which tool and model.
#agent-launcher, #mcp, #tui, #llm-client and 4 more carry open exposures but no @owns, so nobody is accountable for closing them. Name the team in the definitions file.
Each @audit marks a risk with no control yet. Review it and either add a control or record a decision.
Threat mitigation coverage
88% 101 of 115 exposures mitigated

An exposure counts as mitigated when a @mitigates on the same asset and threat covers it. Accepted risks are not mitigations.

Severity breakdownall exposures
Critical
2
High
35
Medium
35
Low
43
Worst open exposures6 of 14
high
#agent-launcher → #prompt-injection
User prompt concatenated into agent instruction text
src/agents/prompts.ts:6
high
#mcp → #cmd-injection
Accepts tool calls from external MCP clients
src/mcp/index.ts:6
high
#tui → #cmd-injection
/annotate and /threat-report spawn child processes
src/tui/commands.ts:11
medium
#agent-launcher → #prompt-injection
User prompt passed to agent CLI as argument
src/agents/launcher.ts:13
medium
#agent-launcher → #config-tamper
Translate prompt may read CXG reference paths from environment overrides
src/agents/prompts.ts:10
medium
#llm-client → #prompt-injection
User prompts sent to LLM API
src/analyze/llm.ts:17
See all 14 open exposures →
Who introduces exposed codeAttribution →

From git history: the author, co-authors and any AI tool credited on the commit that introduced each exposure's code. 49 of 115 exposures were introduced with AI help.

People

human:Animesh Srivastava80 introduced · 14 open
human:zippon35 introduced · 0 open
human:Jordi Murgó0 introduced · 0 open
human:jpmo0 introduced · 0 open

AI tools

claude-code Claude Fable 5.127 introduced · 0 open
claude-code Claude Sonnet 4.63 introduced · 0 open
warp0 introduced · 0 open
Model inventoryclick a tile to open its page
Analytics

Where the exposure is concentrated, who owns it, what covers it, and what nothing covers. Every cell is a link into the filtered Threats page. Pick a feature in the top bar and every grid recomputes for it.

Who owns the open risk0 teams · 8 unowned

Open exposures rolled up to the team named by @owns on the asset. An exposed asset with no owner has nobody accountable for closing it.

No @owns in the model, so no team is accountable for anything here.

Sensitive data under open exposure3 classifications

Assets recorded with @handles, and how many of them carry an open exposure. Click a class for its rows; each pill is one exposed asset.

internal 75100high
secrets 4370high
pii 6250high
Asset × threat13 assets · 14 threats

Each cell is how many exposures that asset carries for that threat. Red means at least one is open (or confirmed), green means all are mitigated, blue all accepted; darker is more. Click a cell for those rows, a row for the asset, a column for the threat.

Severity × status115 exposures

Open critical and high cells are the ones to close first; a large mitigated column with a small open one is a model in good shape.

Threats by frequencyexposures per threat class

How often each threat class appears across the model; the hint says how many of those are still open.

#arbitrary-write
24 all covered
#path-traversal
24 all covered
#dos
16 1 open
#cmd-injection
9 2 open
#api-key-exposure
7 all covered
#data-exposure
7 5 open
#redos
7 all covered
#insecure-deser
6 all covered
#xss
3 all covered
#config-tamper
2 1 open
#info-disclosure
2 all covered
Control coverage12 used · 1 unused

What each declared control actually mitigates. A control declared in the definitions that no @mitigates names is doing nothing for the model — either it is missing annotations or it is not real.

#path-validation
41 2 threats · 12 assets
#output-encoding
20 2 threats · 2 assets
#resource-limits
14 3 threats · 7 assets
#regex-anchoring
10 2 threats · 6 assets
#config-validation
8 4 threats · 4 assets
#input-sanitize
6 4 threats · 4 assets
#key-redaction
6 1 threat · 5 assets
#param-commands
6 2 threats · 4 assets
#glob-filtering
4 1 threat · 3 assets
#prefix-ownership
2 1 threat · 2 assets
#identity-redaction
1 1 threat · 1 asset
#yaml-validation
1 1 threat · 1 asset
1 unused #process-sandbox
Files with the most open exposurestop 10

Where the open exposure concentrates in the tree. Click to see that file's open rows.

People × timetop 2 introducers · by quarter

Exposures each person's commits introduced, per quarter. A row that stays dark is someone who keeps introducing exposed code; a row that fades is improvement. Click a name for their claims.

person \ quarter2026-Q12026-Q3
human:Animesh Srivastava6218
human:zippon332
AI tool × severity3 tools

Exposures whose introducing commit credited each AI tool, by severity. Credit is declared by the commit's trailers, never detected.

Threat Reports1 saved

Reports written by guardlink threat-report (STRIDE, DREAD, PASTA, attacker, rapid, general or a custom prompt). Pick one to read it here; copy or download it to share.

Write another:

guardlink threat-report strideguardlink threat-report dreadguardlink threat-report pastaguardlink threat-report attackerguardlink threat-report rapidguardlink threat-report general guardlink threat-report general --custom "focus on auth"
Threats & Exposures115 exposures

Every @exposes in the model, with whether a control covers it, whether the claim is still verified against the code beneath it, and who introduced that code. Click a row for detail and actions; click a column to sort; type / to search (every word must match, so #api sqli narrows to one pair).

Status Severity Claim
Exposures14 open · 101 mitigated · 0 accepted

One table, every exposure, open first. Open rows have no covering control; use the chips above to narrow, or click a column header to sort.

Description
Openstale
high
#agent-launcher#prompt-injection
User prompt concatenated into agent instruction text human:Animesh Srivastava
Openverified
high
#mcp#cmd-injection
Accepts tool calls from external MCP clients human:Animesh Srivastava
Openstale
high
#tui#cmd-injection
/annotate and /threat-report spawn child processes human:Animesh Srivastava
Openverified
medium
#agent-launcher#prompt-injection
User prompt passed to agent CLI as argument human:Animesh Srivastava
Openstale
medium
#agent-launcher#config-tamper
Translate prompt may read CXG reference paths from environment overrides human:Animesh Srivastava
Openverified
medium
#llm-client#prompt-injection
User prompts sent to LLM API human:Animesh Srivastava
Openstale
medium
#mcp#prompt-injection
annotate and threat_report tools pass user prompts to LLM human:Animesh Srivastava
Openstale
medium
#mcp#data-exposure
Resources expose full threat model to MCP clients human:Animesh Srivastava
Openstale
medium
#tui#prompt-injection
Freeform chat sends user text to LLM human:Animesh Srivastava
Openstale
low
#llm-client#data-exposure
Serializes full threat model and code snippets for LLM human:Animesh Srivastava
Openstale
low
#sarif#data-exposure
Exposes threat model findings to SARIF consumers human:Animesh Srivastava
Openstale
low
#init#data-exposure
Writes API key config to .guardlink/config.json human:Animesh Srivastava
Openverified
low
#suggest#dos
Large files loaded into memory for pattern scanning human:Animesh Srivastava
Openverified
low
#parser#data-exposure
Reads every annotated source file into memory human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
Mitigatedverified
critical
#agent-launcher#child-proc-injection
spawn/spawnSync execute external binaries human:Animesh Srivastava
Mitigatedstale
critical
#cli#cmd-injection
Agent launcher spawns child processes human:Animesh Srivastava
Mitigatedverified
high
#agent-launcher#api-key-exposure
API keys loaded from env vars, files; stored in config.json human:Animesh Srivastava
Mitigatedverified
high
#llm-client#api-key-exposure
API keys passed in Authorization headers human:Animesh Srivastava
Mitigatedstale
high
#cli#path-traversal
User-supplied dir argument resolved via path.resolve human:Animesh Srivastava
Mitigatedstale
high
#cli#arbitrary-write
init/report/sarif/dashboard write files to user-specified paths human:Animesh Srivastava
Mitigatedstale
high
#cli#api-key-exposure
API keys handled in config set/show commands human:Animesh Srivastava
Mitigatedunverified
high
#dashboard#xss
buildExploreData carries asset ids, threat names, file paths and annotation descriptions from the model into strings the Explore page renders human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
Mitigatedunverified
high
#dashboard#xss
generateDashboardHTML() interpolates model descriptions, asset names, git identities and commit trailers into the page markup and the embedded JSON constants human:Animesh Srivastava
Mitigatedstale
high
#dashboard#xss
Generates HTML with threat model data human:Animesh Srivastava
Mitigatedverified
high
#diff#cmd-injection
execSync runs git commands with ref argument human:Animesh Srivastava
Mitigatedverified
high
#diff#cmd-injection
ref is interpolated into an execSync git command human:zippon
Mitigatedverified
high
#diff#cmd-injection
git.ts uses execSync with ref argument human:Animesh Srivastava
Mitigatedstale
high
#init#arbitrary-write
Creates/modifies files: .guardlink/, CLAUDE.md, .cursorrules, etc. human:Animesh Srivastava
Mitigatedverified
high
#init#arbitrary-write
Creates directories for agent-file writes human:Animesh Srivastava
Mitigatedverified
high
#init#arbitrary-write
Decides whether init may overwrite an existing definitions/config file human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
Mitigatedstale
high
#mcp#path-traversal
Tool arguments include 'root' directory path from external client human:Animesh Srivastava
Mitigatedstale
high
#mcp#arbitrary-write
report, dashboard, sarif tools write files human:Animesh Srivastava
Mitigatedverified
high
#suggest#path-traversal
File path from MCP client joined with root human:Animesh Srivastava
Mitigatedverified
high
#parser#arbitrary-write
Writes annotation sidecars from tool input human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
Mitigatedverified
high
#parser#arbitrary-write
Writes modified content back to discovered files human:Animesh Srivastava
Mitigatedverified
high
#parser#path-traversal
Glob patterns determine which files are modified human:Animesh Srivastava
Mitigatedverified
high
#parser#arbitrary-write
Rewrites source files and creates sidecars across the project human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
Mitigatedstale
high
#parser#path-traversal
File path from caller read via readFile; no validation here human:Animesh Srivastava
Mitigatedstale
high
#parser#path-traversal
Glob patterns could escape root directory human:Animesh Srivastava
Mitigatedverified
high
#cli#arbitrary-write
Accepting a proposal writes an @entitles line into a source file named by the proposal human:zippon
Mitigatedverified
high
#cli#arbitrary-write
Agent-supplied rationale and human decision notes are interpolated into annotation text, where a newline would forge a second annotation human:zippon
Mitigatedunverified
high
#cli#arbitrary-write
Reviewer-supplied justification and author name are interpolated into annotation text, where a newline would forge a second annotation human:Animesh Srivastava
Mitigatedverified
high
#cli#arbitrary-write
Writes annotation lines into a caller-supplied file path human:Animesh Srivastava
Mitigatedstale
high
#tui#path-traversal
File paths from user args in /view, /sarif -o human:Animesh Srivastava
Mitigatedstale
high
#tui#arbitrary-write
/report, /sarif, /dashboard write files human:Animesh Srivastava
Mitigatedstale
high
#tui#api-key-exposure
/model handles API key input and storage human:Animesh Srivastava
Mitigatedverified
high
#tui#api-key-exposure
API keys loaded from and saved to config files human:Animesh Srivastava
Mitigatedstale
high
#tui#path-traversal
User-supplied dir argument resolved via path.resolve human:Animesh Srivastava
Mitigatedverified
medium
#agent-launcher#path-traversal
Config paths resolved from root and homedir human:Animesh Srivastava
Mitigatedverified
medium
#agent-launcher#arbitrary-write
saveProjectConfig writes to .guardlink/config.json human:Animesh Srivastava
Mitigatedstale
medium
#agent-launcher#path-traversal
Reads reference docs from root-relative paths human:Animesh Srivastava
Mitigatedstale
medium
#llm-client#path-traversal
buildProjectContext reads files from root-relative paths human:Animesh Srivastava
Mitigatedstale
medium
#llm-client#arbitrary-write
writeFileSync saves threat reports to .guardlink/ human:Animesh Srivastava
Mitigatedverified
medium
#llm-client#ssrf
fetch() calls external LLM API endpoints human:Animesh Srivastava
Mitigatedverified
medium
#llm-client#ssrf
lookupCve fetches from NVD API with user-controlled CVE ID human:zippon
claude-code (Claude Sonnet 4.6)
Mitigatedverified
medium
#llm-client#path-traversal
searchCodebase reads files from project root human:zippon
claude-code (Claude Sonnet 4.6)
Mitigatedstale
medium
#dashboard#arbitrary-write
Writes .mmd, model.json and MANIFEST.json under a caller-supplied root human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
Mitigatedunverified
medium
#blame#data-exposure
identityFor() turns author.name and author.email from git history into identity strings that buildBlamePayload() writes into reports, dashboards and MCP responses
human:zippon
claude-code (Claude Fable 5.1)
Mitigatedunverified
medium
#dashboard#path-traversal
readFileSync reads the file each annotation's location names, for the code context human:zippon
claude-code (Claude Fable 5.1)
Mitigatedstale
medium
#dashboard#path-traversal
readFileSync reads code files for annotation context human:Animesh Srivastava
Mitigatedverified
medium
#diff#arbitrary-write
writeFileSync creates files in temp directory human:Animesh Srivastava
Mitigatedverified
medium
#diff#path-traversal
git show extracts files based on ls-tree output human:Animesh Srivastava
Mitigatedunverified
medium
#gate#arbitrary-write
stripViolations rewrites source files named by the model's locations human:zippon
claude-code (Claude Fable 5.1)
Mitigatedunverified
medium
#cli#arbitrary-write
writeConfirmedLine() edits the source file a claim's location names; importScan() reads the scan path the user passed human:zippon
claude-code (Claude Fable 5.1)
Mitigatedstale
medium
#init#path-traversal
Reads/writes files based on root argument human:Animesh Srivastava
Mitigatedverified
medium
#mcp#path-traversal
Caller-supplied file path is resolved against the project root human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
Mitigatedstale
medium
#mcp#api-key-exposure
threat_report tool uses API keys from environment human:Animesh Srivastava
Mitigatedstale
medium
#mcp#arbitrary-write
guardlink_entitlement_propose writes a client-supplied claim into .guardlink/entitlement-proposals.json human:Animesh Srivastava
Mitigatedverified
medium
#suggest#redos
Complex regex patterns applied to source code human:Animesh Srivastava
Mitigatedunverified
medium
#parser#config-tamper
readAcceptancePolicy reads .guardlink/config.json to decide how strict this project's acceptance rule is, so anyone who can edit that file can lower the bar their own acceptances have to clear
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
Mitigatedunverified
medium
#parser#redos
Marker and decoration matching runs on every line of every scanned file, including attacker-supplied source human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
Mitigatedstale
medium
#parser#dos
Large files loaded entirely into memory human:Animesh Srivastava
Mitigatedstale
medium
#parser#redos
Complex regex patterns applied to annotation text human:Animesh Srivastava
Mitigatedstale
medium
#parser#dos
Large projects with many files could exhaust memory human:Animesh Srivastava
Mitigatedverified
medium
#cli#insecure-deser
JSON.parse of .guardlink/entitlement-proposals.json, which an agent or another repo may have written human:zippon
Mitigatedstale
medium
#cli#arbitrary-write
Writes @accepts/@audit annotations into source files human:Animesh Srivastava
Mitigatedstale
medium
#tui#api-key-exposure
API keys displayed in banner via resolveLLMConfig human:Animesh Srivastava
Mitigatedverified
low
#agent-launcher#dos
No timeout on foreground spawn; agent controls duration human:Animesh Srivastava
Mitigatedverified
low
#agent-launcher#dos
Prompt size now scales with model size: uncapped flow and exposure rows mean a large repo assembles a large prompt human:zippon
claude-code (Claude Opus 5 (1M context))
Mitigatedunverified
low
#llm-client#insecure-deser
JSON.parse over a block the model wrote human:zippon
claude-code (Claude Fable 5.1)
Mitigatedverified
low
#llm-client#dos
searchCodebase reads many files; the LLM now sets max_results itself human:zippon
claude-code (Claude Sonnet 4.6)
Mitigatedunverified
low
#blame#path-traversal
safeRelPath() receives location.file from each parsed record; a sidecar @source path is author text that can carry ../ into the argv computeBlame() hands to git human:zippon
claude-code (Claude Fable 5.1)
Mitigatedunverified
low
#blame#dos
computeBlame() spawns git blame once per annotated file, git log -L once per distinct symbol span and listCommits() once over the whole history; a large model multiplies the spawns
human:zippon
claude-code (Claude Fable 5.1)
Mitigatedunverified
low
#blame#redos
readBlameConfig() compiles blame.tools[].match from .guardlink/config.json into RegExp objects that attributeCommit() runs against every author and trailer in the history
human:zippon
claude-code (Claude Fable 5.1)
Mitigatedunverified
low
#blame#path-traversal
ignore_revs names a file git will open with --ignore-revs-file human:zippon
claude-code (Claude Fable 5.1)
Mitigatedunverified
low
#blame#cmd-injection
gitExec() builds the argv it spawns from location.file and the start/end line numbers of each parsed record human:zippon
claude-code (Claude Fable 5.1)
Mitigatedunverified
low
#blame#path-traversal
blameFile(), spanOldestCommit() and fileAddCommit() pass location.file to git blame, git log and git ls-files, which read whatever path it names human:zippon
claude-code (Claude Fable 5.1)
Mitigatedunverified
low
#blame#dos
git log -L walks history once per distinct span, blame reads every annotated file, and listCommits reads the whole history reachable from HEAD human:zippon
claude-code (Claude Fable 5.1)
Mitigatedunverified
low
#blame#dos
listCommits() runs git log over every commit reachable from HEAD and parseLogRecords() parses each trailer block; history size, not model size, sets the cost human:zippon
claude-code (Claude Fable 5.1)
Mitigatedunverified
low
#blame#redos
attributeCommit() runs every compiled ToolRule.match against each author and Co-Authored-By trailer in the history human:zippon
claude-code (Claude Fable 5.1)
Mitigatedverified
low
#cli#arbitrary-write
The one command that writes .guardlink/verified.json human:zippon
claude-code (Claude Fable 5.1)
Mitigatedunverified
low
#dashboard#path-traversal
detectRepoLinks reads <root>/.git/config and, when .git is a worktree file, follows its `gitdir:` pointer to a config elsewhere on disk; a crafted .git file can name any readable path
human:zippon
claude-code (Claude Fable 5.1)
Mitigatedunverified
low
#dashboard#data-exposure
detectRepoLinks() reads remote.origin.url from .git/config, which may embed user:token@host credentials that would land in the committed HTML human:zippon
claude-code (Claude Fable 5.1)
Mitigatedunverified
low
#dashboard#dos
Measures caller-supplied diagram text with regular expressions human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
Mitigatedunverified
low
#dashboard#cmd-injection
loadSince() passes the --since ref the user typed to parseAtRef() and into the git log and git rev-list argv human:zippon
claude-code (Claude Fable 5.1)
Mitigatedunverified
low
#dashboard#dos
measureLegibility scans caller-supplied diagram text with regular expressions, once per line human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
Mitigatedunverified
low
#cli#insecure-deser
importScan() JSON.parses a scan report human:zippon
claude-code (Claude Fable 5.1)
Mitigatedunverified
low
#cli#arbitrary-write
writeHypotheses() writes .guardlink/hypotheses.json under the root the caller resolved human:zippon
claude-code (Claude Fable 5.1)
Mitigatedverified
low
#init#path-traversal
Reads package.json, pyproject.toml, etc. from root human:Animesh Srivastava
Mitigatedverified
low
#mcp#info-disclosure
Envelope discloses the absolute project root and git SHA to the connected client human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
Mitigatedverified
low
#mcp#redos
Regex patterns applied to query strings human:Animesh Srivastava
Mitigatedstale
low
#mcp#dos
Unbounded depth on a dense graph expands the frontier human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
Mitigatedstale
low
#mcp#info-disclosure
D34: the subgraph spread carried the whole repo's unannotated_files inventory into a neighbourhood answer — 8094 paths, 654.8 KB, on a query that resolved nothing
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
Mitigatedunverified
low
#parser#insecure-deser
JSON.parse of a repository file human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
Mitigatedverified
low
#parser#insecure-deser
Annotation lines arrive as caller-supplied text human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
Mitigatedverified
low
#parser#dos
Fingerprint globs the project tree on every call human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
Mitigatedverified
low
#parser#insecure-deser
JSON.parse on a committed file under .guardlink/ human:zippon
claude-code (Claude Fable 5.1)
Mitigatedverified
low
#cli#arbitrary-write
writeLedger writes one fixed path under root human:zippon
claude-code (Claude Fable 5.1)
Mitigatedverified
low
#parser#path-traversal
Reads source files named by @source blocks human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
Mitigatedverified
low
#cli#cmd-injection
git is spawned for the verifier name and HEAD human:zippon
claude-code (Claude Fable 5.1)
Mitigatedunverified
low
#cli#redos
selectAnnotatePlaybook() runs each Playbook.triggers RegExp against the prompt text the user typed human:zippon
claude-code (Claude Fable 5.1)
Mitigatedverified
low
#parser#path-traversal
Reads the file each annotation's location names, and a .gal @source path is author-supplied text that survives normalisation with ../ intact human:zippon
claude-code (Claude Fable 5.1)
Mitigatedverified
low
#parser#dos
One structure parse per annotated file human:zippon
claude-code (Claude Fable 5.1)
Mitigatedverified
low
#parser#dos
Grammar WASM is loaded into memory per language; a pathological source file costs one parse human:zippon
claude-code (Claude Fable 5.1)
Mitigatedverified
low
#tui#dos
Rapid keystrokes could consume CPU in raw mode human:Animesh Srivastava
Explore

One question at a time, and an answer sized to be read. Each panel states what it is for, how big the drawing is against the 12-node / 16-edge legibility budget measured for this panel, and what fell just outside the frame. Diagrams here are drawn at full size and never shrunk to fit — where long labels make one wider than the panel, drag to pan or press Fit. The whole-model pictures are still on Diagrams, where they say their own size.

13 components against 14 weakness classs — 65 pairs in all. A matrix has no legibility budget to exceed: one cell per pair, however many pairs there are.

Red means at least one claim in that cell is open or confirmed, green that all are mitigated, blue accepted; darker is more. Click a cell or a row to open that component's diagrams, a column for everywhere that weakness was declared.

Exposed tothreats declared on this component, and the controls that answer them
%%{init: {"flowchart": {"nodeSpacing": 55, "rankSpacing": 150, "curve": "monotoneX", "htmlLabels": false, "padding": 24}}}%%
graph LR
  mcp["GuardLink.MCP"]
  cmd_injection{{"Command_Injection (cwe:CWE-78)"}}:::sev_crit
  path_traversal{{"Path_Traversal (cwe:CWE-22)"}}:::sev_high
  arbitrary_write{{"Arbitrary_File_Write (cwe:CWE-73)"}}:::sev_high
  api_key_exposure{{"API_Key_Exposure (cwe:CWE-798)"}}:::sev_high
  path_validation(["Path_Validation"]):::control
  key_redaction(["Key_Redaction"]):::control
  mcp -. exposes .-> cmd_injection
  mcp -. exposes .-> path_traversal
  mcp -. exposes .-> arbitrary_write
  path_validation -- mitigates --> path_traversal
  path_validation -. protects .-> mcp
  path_validation -- mitigates --> arbitrary_write
  key_redaction -- mitigates --> api_key_exposure
  key_redaction -. protects .-> mcp
  classDef threat fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.3px
  classDef control fill:#102a24,stroke:#33d49d,color:#f0f0f0,stroke-width:1.3px
  classDef sev_crit fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.4px
  classDef sev_high fill:#402019,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.2px
  classDef sev_med fill:#1f3943,stroke:#55899e,color:#f0f0f0
  classDef sev_low fill:#10263b,stroke:#0360a2,color:#f0f0f0
  classDef sev_unset fill:#223942,stroke:#3b6779,color:#f0f0f0

7 nodes / 8 edges, within the 12 / 16 legibility budget · narrowed to high and critical — 12 lower-severity claims hidden, all of them in the rows below

Talks todata flows and trust boundaries in its neighbourhood
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.MCP · Trust boundary at MCP protocol"]
    mcp["GuardLink.MCP"]
  end
  subgraph Z1["MCPClient · Trust boundary at MCP protocol"]
    mcpclient(["MCPClient"])
  end
  threatmodel["ThreatModel"]
  llm_client(["GuardLink.LLM_Client"])
  gitrepo["GitRepo"]
  configfile["ConfigFile"]
  querystring["QueryString"]
  filesystem["FileSystem"]
  mcp -.-|Trust boundary at MCP protocol| mcpclient
  threatmodel -- "serializeModel" --> llm_client
  mcpclient -- "guardlink_context" --> mcp
  threatmodel -- "fileContext" --> mcp
  threatmodel -- "buildEnvelope" --> mcp
  gitrepo -- "readFileSync" --> mcp
  mcpclient -- "stdio" --> mcp
  configfile -- "readFileSync" --> mcp
  querystring -- "lookup" --> mcp
  mcpclient -- "tool_call" --> mcp
  mcp -- "writeFile" --> filesystem
  mcp -- "generateThreatReport" --> llm_client
  mcp -- "resource" --> mcpclient
  gitrepo -- "computeBlame" --> mcp
  threatmodel -- "selectSubgraph" --> mcp

8 nodes / 15 edges, within the 12 / 16 legibility budget · 20 neighbours just outside the frame: #agent-launcher, #blame, #cli, #dashboard, #diff, #gate, #init, LLMConfig, and 12 more

Every claim on #mcp— rows · 3 open
Exposed tothreats declared on this component, and the controls that answer them
%%{init: {"flowchart": {"nodeSpacing": 55, "rankSpacing": 150, "curve": "monotoneX", "htmlLabels": false, "padding": 24}}}%%
graph LR
  agent_launcher["GuardLink.Agent_Launcher"]
  api_key_exposure{{"API_Key_Exposure (cwe:CWE-798)"}}:::sev_high
  child_proc_injection{{"Child_Process_Injection (cwe:CWE-78)"}}:::sev_crit
  prompt_injection{{"Prompt_Injection (cwe:CWE-77)"}}:::sev_high
  path_traversal{{"Path_Traversal (cwe:CWE-22)"}}:::sev_high
  arbitrary_write{{"Arbitrary_File_Write (cwe:CWE-73)"}}:::sev_high
  cmd_injection{{"Command_Injection (cwe:CWE-78)"}}:::sev_crit
  key_redaction(["Key_Redaction"]):::control
  path_validation(["Path_Validation"]):::control
  param_commands(["Parameterized_Commands"]):::control
  agent_launcher -. exposes .-> api_key_exposure
  agent_launcher -. exposes .-> child_proc_injection
  agent_launcher -. exposes .-> prompt_injection
  key_redaction -- mitigates --> api_key_exposure
  key_redaction -. protects .-> agent_launcher
  path_validation -- mitigates --> path_traversal
  path_validation -. protects .-> agent_launcher
  path_validation -- mitigates --> arbitrary_write
  param_commands -- mitigates --> child_proc_injection
  param_commands -. protects .-> agent_launcher
  param_commands -- mitigates --> cmd_injection
  classDef threat fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.3px
  classDef control fill:#102a24,stroke:#33d49d,color:#f0f0f0,stroke-width:1.3px
  classDef sev_crit fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.4px
  classDef sev_high fill:#402019,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.2px
  classDef sev_med fill:#1f3943,stroke:#55899e,color:#f0f0f0
  classDef sev_low fill:#10263b,stroke:#0360a2,color:#f0f0f0
  classDef sev_unset fill:#223942,stroke:#3b6779,color:#f0f0f0

10 nodes / 11 edges, within the 12 / 16 legibility budget · narrowed to high and critical — 8 lower-severity claims hidden, all of them in the rows below

Talks todata flows and trust boundaries in its neighbourhood
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Agent_Launcher · Trust boundary at process spawn"]
    agent_launcher["GuardLink.Agent_Launcher"]
  end
  subgraph Z1["AgentProcess · Trust boundary at process spawn"]
    agentprocess["AgentProcess"]
  end
  envvars["EnvVars"]
  configfile["ConfigFile"]
  userprompt(["UserPrompt"])
  threatmodel["ThreatModel"]
  agentprompt["AgentPrompt"]
  tui(["GuardLink.TUI"])
  agent_launcher -.-|Trust boundary at process spawn| agentprocess
  envvars -- "process.env" --> agent_launcher
  configfile -- "readFileSync" --> agent_launcher
  agent_launcher -- "writeFileSync" --> configfile
  userprompt -- "launchAgent" --> agent_launcher
  agent_launcher -- "spawn" --> agentprocess
  agentprocess -- "stdout" --> agent_launcher
  userprompt -- "buildAnnotatePrompt" --> agent_launcher
  userprompt -- "buildTranslatePrompt" --> agent_launcher
  userprompt -- "buildAskPrompt" --> agent_launcher
  threatmodel -- "model" --> agent_launcher
  agent_launcher -- "return" --> agentprompt
  tui -- "launchAgent" --> agent_launcher
  configfile -- "loadProjectConfig" --> tui
  tui -- "saveProjectConfig" --> configfile

8 nodes / 15 edges, within the 12 / 16 legibility budget · 19 neighbours just outside the frame: #blame, #cli, Commands, #dashboard, #diff, FileSystem, #gate, GitRepo, and 11 more

Every claim on #agent-launcher— rows · 3 open
Exposed tothreats declared on this component, and the controls that answer them
%%{init: {"flowchart": {"nodeSpacing": 55, "rankSpacing": 150, "curve": "monotoneX", "htmlLabels": false, "padding": 24}}}%%
graph LR
  llm_client["GuardLink.LLM_Client"]
  api_key_exposure{{"API_Key_Exposure (cwe:CWE-798)"}}:::sev_high
  path_traversal{{"Path_Traversal (cwe:CWE-22)"}}:::sev_high
  arbitrary_write{{"Arbitrary_File_Write (cwe:CWE-73)"}}:::sev_high
  path_validation(["Path_Validation"]):::control
  key_redaction(["Key_Redaction"]):::control
  glob_filtering(["Glob_Pattern_Filtering"]):::control
  llm_client -. exposes .-> api_key_exposure
  path_validation -- mitigates --> path_traversal
  path_validation -. protects .-> llm_client
  path_validation -- mitigates --> arbitrary_write
  key_redaction -- mitigates --> api_key_exposure
  key_redaction -. protects .-> llm_client
  glob_filtering -- mitigates --> path_traversal
  glob_filtering -. protects .-> llm_client
  classDef threat fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.3px
  classDef control fill:#102a24,stroke:#33d49d,color:#f0f0f0,stroke-width:1.3px
  classDef sev_crit fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.4px
  classDef sev_high fill:#402019,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.2px
  classDef sev_med fill:#1f3943,stroke:#55899e,color:#f0f0f0
  classDef sev_low fill:#10263b,stroke:#0360a2,color:#f0f0f0
  classDef sev_unset fill:#223942,stroke:#3b6779,color:#f0f0f0

7 nodes / 8 edges, within the 12 / 16 legibility budget · narrowed to high and critical — 13 lower-severity claims hidden, all of them in the rows below

Talks todata flows and trust boundaries in its neighbourhood
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.LLM_Client · Trust boundary at external API call"]
    llm_client(["GuardLink.LLM_Client"])
  end
  subgraph Z1["LLMProvider · Trust boundary at external API call"]
    llmprovider["LLMProvider"]
  end
  subgraph Z2["NVD · Trust boundary at external API"]
    nvd["NVD"]
  end
  savedreport["SavedReport"]
  projectfiles["ProjectFiles"]
  reportfile["ReportFile"]
  pentestfindings["PentestFindings"]
  llmconfig["LLMConfig"]
  llmtoolcall["LLMToolCall"]
  mcp["GuardLink.MCP"]
  tui(["GuardLink.TUI"])
  llm_client -.-|Trust boundary at external API call| llmprovider
  llm_client -.-|Trust boundary at external API| nvd
  savedreport -- "parseFindingsBlock" --> llm_client
  projectfiles -- "readFileSync" --> llm_client
  llm_client -- "writeFileSync" --> reportfile
  pentestfindings -- "readFileSync" --> llm_client
  llmconfig -- "chatCompletion" --> llm_client
  llm_client -- "fetch" --> llmprovider
  llmprovider -- "response" --> llm_client
  llmtoolcall -- "createToolExecutor" --> llm_client
  llm_client -- "fetch" --> nvd
  mcp -- "generateThreatReport" --> llm_client
  tui -- "chatCompletion" --> llm_client

11 nodes / 13 edges, within the 12 / 16 legibility budget · 12 neighbours just outside the frame: #agent-launcher, Commands, ConfigFile, FileSystem, GitRepo, MCPClient, QueryString, RawStdin, and 4 more

Every claim on #llm-client— rows · 2 open
Exposed tothreats declared on this component, and the controls that answer them
%%{init: {"flowchart": {"nodeSpacing": 55, "rankSpacing": 150, "curve": "monotoneX", "htmlLabels": false, "padding": 24}}}%%
graph LR
  tui["GuardLink.TUI"]
  path_traversal{{"Path_Traversal (cwe:CWE-22)"}}:::sev_high
  arbitrary_write{{"Arbitrary_File_Write (cwe:CWE-73)"}}:::sev_high
  cmd_injection{{"Command_Injection (cwe:CWE-78)"}}:::sev_crit
  api_key_exposure{{"API_Key_Exposure (cwe:CWE-798)"}}:::sev_high
  prompt_injection{{"Prompt_Injection (cwe:CWE-77)"}}:::sev_med
  dos{{"Denial_of_Service (cwe:CWE-400)"}}:::sev_med
  path_validation(["Path_Validation"]):::control
  key_redaction(["Key_Redaction"]):::control
  resource_limits(["Resource_Limits"]):::control
  tui -. exposes .-> path_traversal
  tui -. exposes .-> arbitrary_write
  tui -. exposes .-> cmd_injection
  tui -. exposes .-> api_key_exposure
  tui -. exposes .-> prompt_injection
  tui -. exposes .-> dos
  path_validation -- mitigates --> path_traversal
  path_validation -. protects .-> tui
  path_validation -- mitigates --> arbitrary_write
  key_redaction -- mitigates --> api_key_exposure
  key_redaction -. protects .-> tui
  resource_limits -- mitigates --> dos
  resource_limits -. protects .-> tui
  classDef threat fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.3px
  classDef control fill:#102a24,stroke:#33d49d,color:#f0f0f0,stroke-width:1.3px
  classDef sev_crit fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.4px
  classDef sev_high fill:#402019,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.2px
  classDef sev_med fill:#1f3943,stroke:#55899e,color:#f0f0f0
  classDef sev_low fill:#10263b,stroke:#0360a2,color:#f0f0f0
  classDef sev_unset fill:#223942,stroke:#3b6779,color:#f0f0f0

10 nodes / 13 edges, within the 12 / 16 legibility budget

Talks todata flows and trust boundaries in its neighbourhood
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.TUI · Trust boundary at interactive input"]
    tui(["GuardLink.TUI"])
  end
  subgraph Z1["UserInput · Trust boundary at interactive input"]
    userinput(["UserInput"])
  end
  configfile["ConfigFile"]
  agent_launcher["GuardLink.Agent_Launcher"]
  userargs(["UserArgs"])
  filesystem["FileSystem"]
  llm_client(["GuardLink.LLM_Client"])
  gitrepo["GitRepo"]
  commands["Commands"]
  rawstdin["RawStdin"]
  terminal["Terminal"]
  tui -.-|Trust boundary at interactive input| userinput
  configfile -- "readFileSync" --> agent_launcher
  agent_launcher -- "writeFileSync" --> configfile
  userargs -- "args" --> tui
  tui -- "writeFile" --> filesystem
  tui -- "launchAgent" --> agent_launcher
  tui -- "chatCompletion" --> llm_client
  gitrepo -- "computeBlame" --> tui
  configfile -- "loadProjectConfig" --> tui
  tui -- "saveProjectConfig" --> configfile
  userinput -- "readline" --> tui
  tui -- "dispatch" --> commands
  rawstdin -- "process.stdin" --> tui
  tui -- "process.stdout" --> terminal

11 nodes / 14 edges, within the 12 / 16 legibility budget · 22 neighbours just outside the frame: AgentProcess, AgentPrompt, #blame, #cli, #dashboard, #diff, EnvVars, #init, and 14 more

Every claim on #tui— rows · 2 open
Exposed tothreats declared on this component, and the controls that answer them
%%{init: {"flowchart": {"nodeSpacing": 55, "rankSpacing": 150, "curve": "monotoneX", "htmlLabels": false, "padding": 24}}}%%
graph LR
  parser["GuardLink.Parser"]
  arbitrary_write{{"Arbitrary_File_Write (cwe:CWE-73)"}}:::sev_high
  path_traversal{{"Path_Traversal (cwe:CWE-22)"}}:::sev_high
  path_validation(["Path_Validation"]):::control
  glob_filtering(["Glob_Pattern_Filtering"]):::control
  parser -. exposes .-> arbitrary_write
  parser -. exposes .-> path_traversal
  path_validation -- mitigates --> arbitrary_write
  path_validation -. protects .-> parser
  glob_filtering -- mitigates --> path_traversal
  glob_filtering -. protects .-> parser
  path_validation -- mitigates --> path_traversal
  classDef threat fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.3px
  classDef control fill:#102a24,stroke:#33d49d,color:#f0f0f0,stroke-width:1.3px
  classDef sev_crit fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.4px
  classDef sev_high fill:#402019,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.2px
  classDef sev_med fill:#1f3943,stroke:#55899e,color:#f0f0f0
  classDef sev_low fill:#10263b,stroke:#0360a2,color:#f0f0f0
  classDef sev_unset fill:#223942,stroke:#3b6779,color:#f0f0f0

5 nodes / 7 edges, within the 12 / 16 legibility budget · narrowed to high and critical — 27 lower-severity claims hidden, all of them in the rows below

Talks todata flows and trust boundaries in its neighbourhood
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z1["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  configfile["ConfigFile"]
  mcpclient(["MCPClient"])
  projectroot["ProjectRoot"]
  sourcefiles["SourceFiles"]
  sourcefile["SourceFile"]
  ledgerfile["LedgerFile"]
  filepath["FilePath"]
  annotations["Annotations"]
  parsediagnostics["ParseDiagnostics"]
  grammarfile["GrammarFile"]
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  configfile -- "readAcceptancePolicy" --> parser
  mcpclient -- "applyAnnotations" --> parser
  parser -- "writeFileSync" --> filesystem
  projectroot -- "fast-glob" --> parser
  parser -- "writeFile" --> sourcefiles
  sourcefile -- "resolveGalPath" --> parser
  ledgerfile -- "readLedger" --> parser
  filepath -- "readFile" --> parser
  parser -- "parseString" --> annotations
  parser -- "parseString" --> parsediagnostics
  ledgerfile -- "classifyClaims" --> parser
  sourcefiles -- "attachAnchors" --> parser
  sourcefile -- "parseStructure" --> parser
  grammarfile -- "Language.load" --> parser

12 nodes / 15 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #agent-launcher, #blame, #cli, #dashboard, #gate, #init, #mcp, #suggest, and 2 more

Every claim on #parser— rows · 1 open
Exposed tothreats declared on this component, and the controls that answer them
%%{init: {"flowchart": {"nodeSpacing": 55, "rankSpacing": 150, "curve": "monotoneX", "htmlLabels": false, "padding": 24}}}%%
graph LR
  init["GuardLink.Init"]
  path_traversal{{"Path_Traversal (cwe:CWE-22)"}}:::sev_high
  arbitrary_write{{"Arbitrary_File_Write (cwe:CWE-73)"}}:::sev_high
  data_exposure{{"Sensitive_Data_Exposure (cwe:CWE-200)"}}:::sev_med
  path_validation(["Path_Validation"]):::control
  config_validation(["Config_Validation"]):::control
  init -. exposes .-> path_traversal
  init -. exposes .-> arbitrary_write
  init -. exposes .-> data_exposure
  path_validation -- mitigates --> path_traversal
  path_validation -. protects .-> init
  path_validation -- mitigates --> arbitrary_write
  config_validation -- mitigates --> arbitrary_write
  config_validation -. protects .-> init
  classDef threat fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.3px
  classDef control fill:#102a24,stroke:#33d49d,color:#f0f0f0,stroke-width:1.3px
  classDef sev_crit fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.4px
  classDef sev_high fill:#402019,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.2px
  classDef sev_med fill:#1f3943,stroke:#55899e,color:#f0f0f0
  classDef sev_low fill:#10263b,stroke:#0360a2,color:#f0f0f0
  classDef sev_unset fill:#223942,stroke:#3b6779,color:#f0f0f0

6 nodes / 8 edges, within the 12 / 16 legibility budget

Talks todata flows and trust boundaries in its neighbourhood
%%{init: {"flowchart": {"nodeSpacing": 47, "rankSpacing": 67, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  configfile["ConfigFile"]
  agent_launcher["GuardLink.Agent_Launcher"]
  blame["GuardLink.Blame"]
  projectroot["ProjectRoot"]
  init["GuardLink.Init"]
  agentfiles["AgentFiles"]
  definitionsfile["DefinitionsFile"]
  mcp["GuardLink.MCP"]
  parser["GuardLink.Parser"]
  tui(["GuardLink.TUI"])
  workspace_link["Workspace.Link"]
  configfile -- "readFileSync" --> agent_launcher
  agent_launcher -- "writeFileSync" --> configfile
  configfile -- "readBlameConfig" --> blame
  projectroot -- "detectProject" --> init
  projectroot -- "options.root" --> init
  init -- "writeFileSync" --> agentfiles
  init -- "writeFileSync" --> configfile
  definitionsfile -- "definitionsArePopulated" --> init
  configfile -- "configIsCustomised" --> init
  configfile -- "readFileSync" --> mcp
  configfile -- "readAcceptancePolicy" --> parser
  projectroot -- "fast-glob" --> parser
  tui -- "launchAgent" --> agent_launcher
  configfile -- "loadProjectConfig" --> tui
  tui -- "saveProjectConfig" --> configfile
  workspace_link -- "updateAgentWorkspaceContext" --> agentfiles

11 nodes / 16 edges, within the 12 / 16 legibility budget · 25 neighbours just outside the frame: AgentProcess, AgentPrompt, Annotations, #cli, Commands, CommitTrailers, #dashboard, EnvVars, and 17 more

Every claim on #init— rows · 1 open
Exposed tothreats declared on this component, and the controls that answer them
%%{init: {"flowchart": {"nodeSpacing": 55, "rankSpacing": 150, "curve": "monotoneX", "htmlLabels": false, "padding": 24}}}%%
graph LR
  suggest["GuardLink.Suggest"]
  path_traversal{{"Path_Traversal (cwe:CWE-22)"}}:::sev_high
  redos{{"ReDoS (cwe:CWE-1333)"}}:::sev_med
  dos{{"Denial_of_Service (cwe:CWE-400)"}}:::sev_med
  path_validation(["Path_Validation"]):::control
  regex_anchoring(["Regex_Anchoring"]):::control
  suggest -. exposes .-> path_traversal
  suggest -. exposes .-> redos
  suggest -. exposes .-> dos
  path_validation -- mitigates --> path_traversal
  path_validation -. protects .-> suggest
  regex_anchoring -- mitigates --> redos
  regex_anchoring -. protects .-> suggest
  classDef threat fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.3px
  classDef control fill:#102a24,stroke:#33d49d,color:#f0f0f0,stroke-width:1.3px
  classDef sev_crit fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.4px
  classDef sev_high fill:#402019,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.2px
  classDef sev_med fill:#1f3943,stroke:#55899e,color:#f0f0f0
  classDef sev_low fill:#10263b,stroke:#0360a2,color:#f0f0f0
  classDef sev_unset fill:#223942,stroke:#3b6779,color:#f0f0f0

6 nodes / 7 edges, within the 12 / 16 legibility budget

Talks todata flows and trust boundaries in its neighbourhood
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z1["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  filepath["FilePath"]
  suggest["GuardLink.Suggest"]
  suggestions["Suggestions"]
  configfile["ConfigFile"]
  mcpclient(["MCPClient"])
  projectroot["ProjectRoot"]
  ledgerfile["LedgerFile"]
  annotations["Annotations"]
  parsediagnostics["ParseDiagnostics"]
  grammarfile["GrammarFile"]
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  filepath -- "readFileSync" --> suggest
  suggest -- "suggestAnnotations" --> suggestions
  configfile -- "readAcceptancePolicy" --> parser
  mcpclient -- "applyAnnotations" --> parser
  parser -- "writeFileSync" --> filesystem
  projectroot -- "fast-glob" --> parser
  ledgerfile -- "readLedger" --> parser
  filepath -- "readFile" --> parser
  parser -- "parseString" --> annotations
  parser -- "parseString" --> parsediagnostics
  ledgerfile -- "classifyClaims" --> parser
  grammarfile -- "Language.load" --> parser

12 nodes / 13 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #agent-launcher, #blame, #cli, #dashboard, #init, #mcp, SourceFile, SourceFiles, and 2 more

Every claim on #suggest— rows · 1 open
Exposed tothreats declared on this component, and the controls that answer them
%%{init: {"flowchart": {"nodeSpacing": 55, "rankSpacing": 150, "curve": "monotoneX", "htmlLabels": false, "padding": 24}}}%%
graph LR
  sarif["GuardLink.SARIF"]
  data_exposure{{"Sensitive_Data_Exposure (cwe:CWE-200)"}}:::sev_med
  sarif -. exposes .-> data_exposure
  classDef threat fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.3px
  classDef control fill:#102a24,stroke:#33d49d,color:#f0f0f0,stroke-width:1.3px
  classDef sev_crit fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.4px
  classDef sev_high fill:#402019,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.2px
  classDef sev_med fill:#1f3943,stroke:#55899e,color:#f0f0f0
  classDef sev_low fill:#10263b,stroke:#0360a2,color:#f0f0f0
  classDef sev_unset fill:#223942,stroke:#3b6779,color:#f0f0f0

2 nodes / 1 edge, within the 12 / 16 legibility budget

Talks todata flows and trust boundaries in its neighbourhood
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  sarif["GuardLink.SARIF"]
  sariflog["SarifLog"]
  parser["GuardLink.Parser"]
  threatmodel -- "generateSarif" --> sarif
  sarif -- "return" --> sariflog
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser

4 nodes / 12 edges, within the 12 / 16 legibility budget · 21 neighbours just outside the frame: #agent-launcher, Annotations, #blame, #cli, ConfigFile, #dashboard, #diff, FilePath, and 13 more

Every claim on #sarif— rows · 1 open
Exposed tothreats declared on this component, and the controls that answer them
%%{init: {"flowchart": {"nodeSpacing": 55, "rankSpacing": 150, "curve": "monotoneX", "htmlLabels": false, "padding": 24}}}%%
graph LR
  cli["GuardLink.CLI"]
  path_traversal{{"Path_Traversal (cwe:CWE-22)"}}:::sev_high
  arbitrary_write{{"Arbitrary_File_Write (cwe:CWE-73, cwe:CWE-74)"}}:::sev_high
  api_key_exposure{{"API_Key_Exposure (cwe:CWE-798)"}}:::sev_high
  cmd_injection{{"Command_Injection (cwe:CWE-78)"}}:::sev_crit
  path_validation(["Path_Validation"]):::control
  key_redaction(["Key_Redaction"]):::control
  param_commands(["Parameterized_Commands"]):::control
  input_sanitize(["Input_Sanitization"]):::control
  cli -. exposes .-> path_traversal
  cli -. exposes .-> arbitrary_write
  cli -. exposes .-> api_key_exposure
  cli -. exposes .-> cmd_injection
  path_validation -- mitigates --> path_traversal
  path_validation -. protects .-> cli
  path_validation -- mitigates --> arbitrary_write
  key_redaction -- mitigates --> api_key_exposure
  key_redaction -. protects .-> cli
  param_commands -- mitigates --> cmd_injection
  param_commands -. protects .-> cli
  input_sanitize -- mitigates --> arbitrary_write
  input_sanitize -. protects .-> cli
  classDef threat fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.3px
  classDef control fill:#102a24,stroke:#33d49d,color:#f0f0f0,stroke-width:1.3px
  classDef sev_crit fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.4px
  classDef sev_high fill:#402019,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.2px
  classDef sev_med fill:#1f3943,stroke:#55899e,color:#f0f0f0
  classDef sev_low fill:#10263b,stroke:#0360a2,color:#f0f0f0
  classDef sev_unset fill:#223942,stroke:#3b6779,color:#f0f0f0

9 nodes / 13 edges, within the 12 / 16 legibility budget · narrowed to high and critical — 13 lower-severity claims hidden, all of them in the rows below

Talks todata flows and trust boundaries in its neighbourhood
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  gitrepo["GitRepo"]
  ledgerfile["LedgerFile"]
  filesystem["FileSystem"]
  gate["GuardLink.Gate"]
  agentproposal["AgentProposal"]
  gitconfig["GitConfig"]
  blame -- "formatBlameText" --> cli
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "buildBlamePayload" --> cli
  ledgerfile -- "readLedger" --> cli
  cli -- "writeFile" --> filesystem
  gitrepo -- "attachBlame" --> cli
  ledgerfile -- "readHypotheses" --> cli
  gitrepo -- "computeBlame" --> cli
  cli -- "runGate" --> gate
  gitrepo -- "loadSince" --> cli
  cli -- "writeHypotheses" --> ledgerfile
  cli -- "writeLedger" --> ledgerfile
  cli -- "applyVerification" --> ledgerfile
  agentproposal -- "proposeEntitlement" --> cli
  gitconfig -- "execFileSync" --> cli

8 nodes / 16 edges, within the 12 / 16 legibility budget · 17 neighbours just outside the frame: AgentPrompt, CommitTrailers, ConfigFile, #dashboard, #diff, #mcp, PackageJson, ParseDiagnostics, and 9 more

Every claim on #cli— rows · 0 open
Exposed tothreats declared on this component, and the controls that answer them
%%{init: {"flowchart": {"nodeSpacing": 55, "rankSpacing": 150, "curve": "monotoneX", "htmlLabels": false, "padding": 24}}}%%
graph LR
  dashboard["GuardLink.Dashboard"]
  arbitrary_write{{"Arbitrary_File_Write (cwe:CWE-73)"}}:::sev_high
  path_traversal{{"Path_Traversal (cwe:CWE-22)"}}:::sev_high
  xss{{"Cross_Site_Scripting (cwe:CWE-79)"}}:::sev_high
  data_exposure{{"Sensitive_Data_Exposure (cwe:CWE-200)"}}:::sev_med
  dos{{"Denial_of_Service (cwe:CWE-400)"}}:::sev_med
  cmd_injection{{"Command_Injection (cwe:CWE-78)"}}:::sev_crit
  path_validation(["Path_Validation"]):::control
  output_encoding(["Output_Encoding"]):::control
  regex_anchoring(["Regex_Anchoring"]):::control
  param_commands(["Parameterized_Commands"]):::control
  dashboard -. exposes .-> arbitrary_write
  dashboard -. exposes .-> path_traversal
  dashboard -. exposes .-> xss
  dashboard -. exposes .-> data_exposure
  dashboard -. exposes .-> dos
  dashboard -. exposes .-> cmd_injection
  path_validation -- mitigates --> arbitrary_write
  path_validation -. protects .-> dashboard
  path_validation -- mitigates --> path_traversal
  output_encoding -- mitigates --> xss
  output_encoding -. protects .-> dashboard
  output_encoding -- mitigates --> data_exposure
  regex_anchoring -- mitigates --> dos
  regex_anchoring -. protects .-> dashboard
  param_commands -- mitigates --> cmd_injection
  param_commands -. protects .-> dashboard
  classDef threat fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.3px
  classDef control fill:#102a24,stroke:#33d49d,color:#f0f0f0,stroke-width:1.3px
  classDef sev_crit fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.4px
  classDef sev_high fill:#402019,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.2px
  classDef sev_med fill:#1f3943,stroke:#55899e,color:#f0f0f0
  classDef sev_low fill:#10263b,stroke:#0360a2,color:#f0f0f0
  classDef sev_unset fill:#223942,stroke:#3b6779,color:#f0f0f0

11 nodes / 16 edges, within the 12 / 16 legibility budget

Talks todata flows and trust boundaries in its neighbourhood
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  dashboard["GuardLink.Dashboard"]
  filesystem["FileSystem"]
  gitrepo["GitRepo"]
  blame["GuardLink.Blame"]
  sourcefiles["SourceFiles"]
  ledgerfile["LedgerFile"]
  gitconfig["GitConfig"]
  html["HTML"]
  dashboardhtml["DashboardHTML"]
  diagramsource["DiagramSource"]
  dashboard -- "writeFileSync" --> filesystem
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "summarise" --> dashboard
  sourcefiles -- "readFileSync" --> dashboard
  ledgerfile -- "readLedger" --> dashboard
  gitrepo -- "loadSince" --> dashboard
  ledgerfile -- "computeLedgerStates" --> dashboard
  gitconfig -- "detectRepoLinks" --> dashboard
  dashboard -- "return" --> html
  ledgerfile -- "readHypotheses" --> dashboard
  gitconfig -- "readFileSync" --> dashboard
  dashboard -- "RepoLinks" --> dashboardhtml
  diagramsource -- "checkRenderBudget" --> dashboard
  gitrepo -- "parseAtRef" --> dashboard
  diagramsource -- "measureLegibility" --> dashboard

10 nodes / 16 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #cli, CommitTrailers, ConfigFile, #diff, #gate, #mcp, #parser, #report-metadata, and 2 more

Every claim on #dashboard— rows · 0 open
Exposed tothreats declared on this component, and the controls that answer them
%%{init: {"flowchart": {"nodeSpacing": 55, "rankSpacing": 150, "curve": "monotoneX", "htmlLabels": false, "padding": 24}}}%%
graph LR
  blame["GuardLink.Blame"]
  path_traversal{{"Path_Traversal (cwe:CWE-22)"}}:::sev_high
  dos{{"Denial_of_Service (cwe:CWE-400)"}}:::sev_med
  redos{{"ReDoS (cwe:CWE-1333)"}}:::sev_med
  cmd_injection{{"Command_Injection (cwe:CWE-78)"}}:::sev_crit
  data_exposure{{"Sensitive_Data_Exposure (cwe:CWE-200)"}}:::sev_med
  path_validation(["Path_Validation"]):::control
  resource_limits(["Resource_Limits"]):::control
  regex_anchoring(["Regex_Anchoring"]):::control
  param_commands(["Parameterized_Commands"]):::control
  identity_redaction(["Identity_Redaction"]):::control
  blame -. exposes .-> path_traversal
  blame -. exposes .-> dos
  blame -. exposes .-> redos
  blame -. exposes .-> cmd_injection
  blame -. exposes .-> data_exposure
  path_validation -- mitigates --> path_traversal
  path_validation -. protects .-> blame
  resource_limits -- mitigates --> dos
  resource_limits -. protects .-> blame
  regex_anchoring -- mitigates --> redos
  regex_anchoring -. protects .-> blame
  param_commands -- mitigates --> cmd_injection
  param_commands -. protects .-> blame
  identity_redaction -- mitigates --> data_exposure
  identity_redaction -. protects .-> blame
  classDef threat fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.3px
  classDef control fill:#102a24,stroke:#33d49d,color:#f0f0f0,stroke-width:1.3px
  classDef sev_crit fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.4px
  classDef sev_high fill:#402019,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.2px
  classDef sev_med fill:#1f3943,stroke:#55899e,color:#f0f0f0
  classDef sev_low fill:#10263b,stroke:#0360a2,color:#f0f0f0
  classDef sev_unset fill:#223942,stroke:#3b6779,color:#f0f0f0

11 nodes / 15 edges, within the 12 / 16 legibility budget

Talks todata flows and trust boundaries in its neighbourhood
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  configfile["ConfigFile"]
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  gitrepo["GitRepo"]
  dashboard["GuardLink.Dashboard"]
  committrailers["CommitTrailers"]
  configfile -- "readBlameConfig" --> blame
  blame -- "formatBlameText" --> cli
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "buildBlamePayload" --> cli
  blame -- "summarise" --> dashboard
  committrailers -- "parseTrailerBlock" --> blame
  gitrepo -- "attachBlame" --> cli
  gitrepo -- "computeBlame" --> cli
  gitrepo -- "loadSince" --> cli
  gitrepo -- "loadSince" --> dashboard
  gitrepo -- "parseAtRef" --> dashboard

6 nodes / 12 edges, within the 12 / 16 legibility budget · 23 neighbours just outside the frame: #agent-launcher, AgentProposal, DashboardHTML, DiagramSource, #diff, FileSystem, #gate, GitConfig, and 15 more

Every claim on #blame— rows · 0 open
Exposed tothreats declared on this component, and the controls that answer them
%%{init: {"flowchart": {"nodeSpacing": 55, "rankSpacing": 150, "curve": "monotoneX", "htmlLabels": false, "padding": 24}}}%%
graph LR
  diff["GuardLink.Diff"]
  cmd_injection{{"Command_Injection (cwe:CWE-78)"}}:::sev_crit
  arbitrary_write{{"Arbitrary_File_Write (cwe:CWE-73)"}}:::sev_high
  path_traversal{{"Path_Traversal (cwe:CWE-22)"}}:::sev_high
  input_sanitize(["Input_Sanitization"]):::control
  path_validation(["Path_Validation"]):::control
  glob_filtering(["Glob_Pattern_Filtering"]):::control
  diff -. exposes .-> cmd_injection
  diff -. exposes .-> arbitrary_write
  diff -. exposes .-> path_traversal
  input_sanitize -- mitigates --> cmd_injection
  input_sanitize -. protects .-> diff
  path_validation -- mitigates --> arbitrary_write
  path_validation -. protects .-> diff
  glob_filtering -- mitigates --> path_traversal
  glob_filtering -. protects .-> diff
  classDef threat fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.3px
  classDef control fill:#102a24,stroke:#33d49d,color:#f0f0f0,stroke-width:1.3px
  classDef sev_crit fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.4px
  classDef sev_high fill:#402019,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.2px
  classDef sev_med fill:#1f3943,stroke:#55899e,color:#f0f0f0
  classDef sev_low fill:#10263b,stroke:#0360a2,color:#f0f0f0
  classDef sev_unset fill:#223942,stroke:#3b6779,color:#f0f0f0

7 nodes / 9 edges, within the 12 / 16 legibility budget

Talks todata flows and trust boundaries in its neighbourhood
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Diff · Trust boundary at git command execution"]
    diff["GuardLink.Diff"]
  end
  subgraph Z1["GitRepo · Trust boundary at git command execution"]
    gitrepo["GitRepo"]
  end
  threatmodel["ThreatModel"]
  gitref["GitRef"]
  tempdir["TempDir"]
  changedfilelist["ChangedFileList"]
  diff -.-|Trust boundary at git command execution| gitrepo
  threatmodel -- "diffModels" --> diff
  gitref -- "execSync" --> diff
  diff -- "writeFileSync" --> tempdir
  diff -- "parseProject" --> threatmodel
  diff -- "return" --> changedfilelist
  gitref -- "parseAtRef" --> diff

6 nodes / 7 edges, within the 12 / 16 legibility budget · 12 neighbours just outside the frame: #agent-launcher, #blame, #cli, #dashboard, #gate, #llm-client, #mcp, #parser, and 4 more

Every claim on #diff— rows · 0 open
Exposed tothreats declared on this component, and the controls that answer them
%%{init: {"flowchart": {"nodeSpacing": 55, "rankSpacing": 150, "curve": "monotoneX", "htmlLabels": false, "padding": 24}}}%%
graph LR
  gate["GuardLink.Gate"]
  arbitrary_write{{"Arbitrary_File_Write (cwe:CWE-73)"}}:::sev_high
  path_validation(["Path_Validation"]):::control
  gate -. exposes .-> arbitrary_write
  path_validation -- mitigates --> arbitrary_write
  path_validation -. protects .-> gate
  classDef threat fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.3px
  classDef control fill:#102a24,stroke:#33d49d,color:#f0f0f0,stroke-width:1.3px
  classDef sev_crit fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.4px
  classDef sev_high fill:#402019,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.2px
  classDef sev_med fill:#1f3943,stroke:#55899e,color:#f0f0f0
  classDef sev_low fill:#10263b,stroke:#0360a2,color:#f0f0f0
  classDef sev_unset fill:#223942,stroke:#3b6779,color:#f0f0f0

3 nodes / 3 edges, within the 12 / 16 legibility budget

Talks todata flows and trust boundaries in its neighbourhood
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  cli["GuardLink.CLI"]
  sourcefiles["SourceFiles"]
  gate["GuardLink.Gate"]
  agentprompt["AgentPrompt"]
  threatmodel -- "runCiChecks" --> cli
  sourcefiles -- "findAnchorDrift" --> cli
  cli -- "runGate" --> gate
  threatmodel -- "findUnmitigatedPaths" --> cli
  threatmodel -- "runGate" --> gate
  gate -- "buildGateFollowUp" --> agentprompt
  gate -- "stripViolations" --> sourcefiles
  threatmodel -- "lintAnnotations" --> gate
  threatmodel -- "classifyHypotheses" --> cli
  cli -- "attachHypotheses" --> threatmodel
  cli -- "writeConfirmedLine" --> sourcefiles
  cli -- "applyProposalDecision" --> sourcefiles
  threatmodel -- "getReviewableExposures" --> cli
  cli -- "writeFile" --> sourcefiles

5 nodes / 14 edges, within the 12 / 16 legibility budget · 21 neighbours just outside the frame: #agent-launcher, AgentProposal, #blame, #dashboard, #diff, FileSystem, GitConfig, GitRepo, and 13 more

Every claim on #gate— rows · 0 open
Exposed tothreats declared on this component, and the controls that answer them
%%{init: {"flowchart": {"nodeSpacing": 55, "rankSpacing": 150, "curve": "monotoneX", "htmlLabels": false, "padding": 24}}}%%
graph LR
  merge_engine["Workspace.Merge"]
  tag_collision{{"Tag_Collision"}}:::sev_med
  prefix_ownership(["Prefix_Ownership"]):::control
  prefix_ownership -- mitigates --> tag_collision
  prefix_ownership -. protects .-> merge_engine
  classDef threat fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.3px
  classDef control fill:#102a24,stroke:#33d49d,color:#f0f0f0,stroke-width:1.3px
  classDef sev_crit fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.4px
  classDef sev_high fill:#402019,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.2px
  classDef sev_med fill:#1f3943,stroke:#55899e,color:#f0f0f0
  classDef sev_low fill:#10263b,stroke:#0360a2,color:#f0f0f0
  classDef sev_unset fill:#223942,stroke:#3b6779,color:#f0f0f0

3 nodes / 2 edges, within the 12 / 16 legibility budget

Talks todata flows and trust boundaries in its neighbourhood
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  reportjson["ReportJSON"]
  merge_engine["Workspace.Merge"]
  mergedreport["MergedReport"]
  reportjson -- "mergeReports" --> merge_engine
  merge_engine -- "mergeReports" --> mergedreport

3 nodes / 2 edges, within the 12 / 16 legibility budget

Every claim on #merge-engine— rows · 0 open
Exposed tothreats declared on this component, and the controls that answer them
%%{init: {"flowchart": {"nodeSpacing": 55, "rankSpacing": 150, "curve": "monotoneX", "htmlLabels": false, "padding": 24}}}%%
graph LR
  report["GuardLink.Report"]
  xss{{"Cross_Site_Scripting (cwe:CWE-79)"}}:::sev_high
  output_encoding(["Output_Encoding"]):::control
  output_encoding -- mitigates --> xss
  output_encoding -. protects .-> report
  classDef threat fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.3px
  classDef control fill:#102a24,stroke:#33d49d,color:#f0f0f0,stroke-width:1.3px
  classDef sev_crit fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.4px
  classDef sev_high fill:#402019,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.2px
  classDef sev_med fill:#1f3943,stroke:#55899e,color:#f0f0f0
  classDef sev_low fill:#10263b,stroke:#0360a2,color:#f0f0f0
  classDef sev_unset fill:#223942,stroke:#3b6779,color:#f0f0f0

3 nodes / 2 edges, within the 12 / 16 legibility budget

Talks todata flows and trust boundaries in its neighbourhood
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  parser["GuardLink.Parser"]
  featurename["FeatureName"]
  report["GuardLink.Report"]
  markdown["Markdown"]
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser
  featurename -- "filtered_by_features" --> report
  threatmodel -- "generateReport" --> report
  report -- "return" --> markdown
  threatmodel -- "generateSequenceDiagram" --> report

5 nodes / 14 edges, within the 12 / 16 legibility budget · 21 neighbours just outside the frame: #agent-launcher, Annotations, #blame, #cli, ConfigFile, #dashboard, #diff, FilePath, and 13 more

Every claim on #report— rows · 0 open
Exposed tothreats declared on this component, and the controls that answer them

This component declares no @exposes, @mitigates, @confirmed or @accepts.

Talks todata flows and trust boundaries in its neighbourhood
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  parser["GuardLink.Parser"]
  gitrepo["GitRepo"]
  report_metadata["Workspace.Metadata"]
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser
  gitrepo -- "execSync" --> report_metadata
  report_metadata -- "populateMetadata" --> threatmodel

4 nodes / 12 edges, within the 12 / 16 legibility budget · 22 neighbours just outside the frame: #agent-launcher, Annotations, #blame, #cli, ConfigFile, #dashboard, #diff, FilePath, and 14 more

Every claim on #report-metadata— rows · 0 open
Exposed tothreats declared on this component, and the controls that answer them
%%{init: {"flowchart": {"nodeSpacing": 55, "rankSpacing": 150, "curve": "monotoneX", "htmlLabels": false, "padding": 24}}}%%
graph LR
  workspace_config["Workspace.Config"]
  config_tamper{{"Config_Tampering (cwe:CWE-15)"}}:::sev_med
  yaml_validation(["YAML_Validation"]):::control
  yaml_validation -- mitigates --> config_tamper
  yaml_validation -. protects .-> workspace_config
  classDef threat fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.3px
  classDef control fill:#102a24,stroke:#33d49d,color:#f0f0f0,stroke-width:1.3px
  classDef sev_crit fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.4px
  classDef sev_high fill:#402019,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.2px
  classDef sev_med fill:#1f3943,stroke:#55899e,color:#f0f0f0
  classDef sev_low fill:#10263b,stroke:#0360a2,color:#f0f0f0
  classDef sev_unset fill:#223942,stroke:#3b6779,color:#f0f0f0

3 nodes / 2 edges, within the 12 / 16 legibility budget

Talks todata flows and trust boundaries in its neighbourhood

This component declares no @flows or @boundary, so it has no flow neighbourhood to draw.

Every claim on #workspace-config— rows · 0 open
Exposed tothreats declared on this component, and the controls that answer them

This component declares no @exposes, @mitigates, @confirmed or @accepts.

Talks todata flows and trust boundaries in its neighbourhood
%%{init: {"flowchart": {"nodeSpacing": 47, "rankSpacing": 67, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  userargs(["UserArgs"])
  gate["GuardLink.Gate"]
  init["GuardLink.Init"]
  agentfiles["AgentFiles"]
  agentproposal["AgentProposal"]
  tui(["GuardLink.TUI"])
  agent_launcher["GuardLink.Agent_Launcher"]
  llm_client(["GuardLink.LLM_Client"])
  commands["Commands"]
  workspace_link["Workspace.Link"]
  blame -- "formatBlameText" --> cli
  blame -- "buildBlamePayload" --> cli
  userargs -- "process.argv" --> cli
  cli -- "runGate" --> gate
  init -- "writeFileSync" --> agentfiles
  agentproposal -- "proposeEntitlement" --> cli
  userargs -- "args" --> tui
  tui -- "launchAgent" --> agent_launcher
  tui -- "chatCompletion" --> llm_client
  tui -- "dispatch" --> commands
  userargs -- "linkProject" --> workspace_link
  workspace_link -- "updateAgentWorkspaceContext" --> agentfiles

12 nodes / 12 edges, within the 12 / 16 legibility budget · 31 neighbours just outside the frame: AgentProcess, AgentPrompt, CommitTrailers, ConfigFile, #dashboard, DefinitionsFile, EnvVars, FileSystem, and 23 more

Every claim on #workspace-link— rows · 0 open
#arbitrary-write10 components · 0 still open

Drawn as a node-link graph this would be one node with 10 spokes, every spoke labelled the same word — past the 12-node budget at 13 components, and carrying nothing the rows do not. Open a component to see this weakness in its context.

#path-traversal11 components · 0 still open

Drawn as a node-link graph this would be one node with 11 spokes, every spoke labelled the same word — past the 12-node budget at 13 components, and carrying nothing the rows do not. Open a component to see this weakness in its context.

#dos8 components · 1 still open

Drawn as a node-link graph this would be one node with 8 spokes, every spoke labelled the same word — past the 12-node budget at 13 components, and carrying nothing the rows do not. Open a component to see this weakness in its context.

#cmd-injection7 components · 2 still open

Drawn as a node-link graph this would be one node with 7 spokes, every spoke labelled the same word — past the 12-node budget at 13 components, and carrying nothing the rows do not. Open a component to see this weakness in its context.

#api-key-exposure5 components · 0 still open

Drawn as a node-link graph this would be one node with 5 spokes, every spoke labelled the same word — past the 12-node budget at 13 components, and carrying nothing the rows do not. Open a component to see this weakness in its context.

#data-exposure7 components · 5 still open

Drawn as a node-link graph this would be one node with 7 spokes, every spoke labelled the same word — past the 12-node budget at 13 components, and carrying nothing the rows do not. Open a component to see this weakness in its context.

#redos5 components · 0 still open

Drawn as a node-link graph this would be one node with 5 spokes, every spoke labelled the same word — past the 12-node budget at 13 components, and carrying nothing the rows do not. Open a component to see this weakness in its context.

#insecure-deser3 components · 0 still open

Drawn as a node-link graph this would be one node with 3 spokes, every spoke labelled the same word — past the 12-node budget at 13 components, and carrying nothing the rows do not. Open a component to see this weakness in its context.

#prompt-injection4 components · 5 still open

Drawn as a node-link graph this would be one node with 4 spokes, every spoke labelled the same word — past the 12-node budget at 13 components, and carrying nothing the rows do not. Open a component to see this weakness in its context.

#xss2 components · 0 still open

Drawn as a node-link graph this would be one node with 2 spokes, every spoke labelled the same word — past the 12-node budget at 13 components, and carrying nothing the rows do not. Open a component to see this weakness in its context.

#config-tamper3 components · 1 still open

Drawn as a node-link graph this would be one node with 3 spokes, every spoke labelled the same word — past the 12-node budget at 13 components, and carrying nothing the rows do not. Open a component to see this weakness in its context.

#info-disclosure1 component · 0 still open

Drawn as a node-link graph this would be one node with 1 spoke, every spoke labelled the same word — past the 12-node budget at 13 components, and carrying nothing the rows do not. Open a component to see this weakness in its context.

#ssrf1 component · 0 still open

Drawn as a node-link graph this would be one node with 1 spoke, every spoke labelled the same word — past the 12-node budget at 13 components, and carrying nothing the rows do not. Open a component to see this weakness in its context.

#child-proc-injection1 component · 0 still open

Drawn as a node-link graph this would be one node with 1 spoke, every spoke labelled the same word — past the 12-node budget at 13 components, and carrying nothing the rows do not. Open a component to see this weakness in its context.

Trust boundary at process spawn#agent-launcher ↔ AgentProcess
Across the lineboth sides, and what flows between them
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Agent_Launcher · Trust boundary at process spawn"]
    agent_launcher["GuardLink.Agent_Launcher"]
  end
  subgraph Z1["AgentProcess · Trust boundary at process spawn"]
    agentprocess["AgentProcess"]
  end
  envvars["EnvVars"]
  configfile["ConfigFile"]
  userprompt(["UserPrompt"])
  threatmodel["ThreatModel"]
  agentprompt["AgentPrompt"]
  tui(["GuardLink.TUI"])
  agent_launcher -.-|Trust boundary at process spawn| agentprocess
  envvars -- "process.env" --> agent_launcher
  configfile -- "readFileSync" --> agent_launcher
  agent_launcher -- "writeFileSync" --> configfile
  userprompt -- "launchAgent" --> agent_launcher
  agent_launcher -- "spawn" --> agentprocess
  agentprocess -- "stdout" --> agent_launcher
  userprompt -- "buildAnnotatePrompt" --> agent_launcher
  userprompt -- "buildTranslatePrompt" --> agent_launcher
  userprompt -- "buildAskPrompt" --> agent_launcher
  threatmodel -- "model" --> agent_launcher
  agent_launcher -- "return" --> agentprompt
  tui -- "launchAgent" --> agent_launcher
  configfile -- "loadProjectConfig" --> tui
  tui -- "saveProjectConfig" --> configfile

8 nodes / 15 edges, within the 12 / 16 legibility budget · 19 neighbours just outside the frame: #blame, #cli, Commands, #dashboard, #diff, FileSystem, #gate, GitRepo, and 11 more

Declared at src/agents/launcher.ts:20.

Trust boundary at external API call#llm-client ↔ LLMProvider
Across the lineboth sides, and what flows between them
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.LLM_Client · Trust boundary at external API call"]
    llm_client(["GuardLink.LLM_Client"])
  end
  subgraph Z1["LLMProvider · Trust boundary at external API call"]
    llmprovider["LLMProvider"]
  end
  subgraph Z2["NVD · Trust boundary at external API"]
    nvd["NVD"]
  end
  savedreport["SavedReport"]
  projectfiles["ProjectFiles"]
  reportfile["ReportFile"]
  pentestfindings["PentestFindings"]
  llmconfig["LLMConfig"]
  llmtoolcall["LLMToolCall"]
  mcp["GuardLink.MCP"]
  tui(["GuardLink.TUI"])
  llm_client -.-|Trust boundary at external API call| llmprovider
  llm_client -.-|Trust boundary at external API| nvd
  savedreport -- "parseFindingsBlock" --> llm_client
  projectfiles -- "readFileSync" --> llm_client
  llm_client -- "writeFileSync" --> reportfile
  pentestfindings -- "readFileSync" --> llm_client
  llmconfig -- "chatCompletion" --> llm_client
  llm_client -- "fetch" --> llmprovider
  llmprovider -- "response" --> llm_client
  llmtoolcall -- "createToolExecutor" --> llm_client
  llm_client -- "fetch" --> nvd
  mcp -- "generateThreatReport" --> llm_client
  tui -- "chatCompletion" --> llm_client

11 nodes / 13 edges, within the 12 / 16 legibility budget · 12 neighbours just outside the frame: #agent-launcher, Commands, ConfigFile, FileSystem, GitRepo, MCPClient, QueryString, RawStdin, and 4 more

Declared at src/analyze/llm.ts:22.

Trust boundary at external API#llm-client ↔ NVD
Across the lineboth sides, and what flows between them
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.LLM_Client · Trust boundary at external API call"]
    llm_client(["GuardLink.LLM_Client"])
  end
  subgraph Z1["LLMProvider · Trust boundary at external API call"]
    llmprovider["LLMProvider"]
  end
  subgraph Z2["NVD · Trust boundary at external API"]
    nvd["NVD"]
  end
  savedreport["SavedReport"]
  projectfiles["ProjectFiles"]
  reportfile["ReportFile"]
  pentestfindings["PentestFindings"]
  llmconfig["LLMConfig"]
  llmtoolcall["LLMToolCall"]
  mcp["GuardLink.MCP"]
  tui(["GuardLink.TUI"])
  llm_client -.-|Trust boundary at external API call| llmprovider
  llm_client -.-|Trust boundary at external API| nvd
  savedreport -- "parseFindingsBlock" --> llm_client
  projectfiles -- "readFileSync" --> llm_client
  llm_client -- "writeFileSync" --> reportfile
  pentestfindings -- "readFileSync" --> llm_client
  llmconfig -- "chatCompletion" --> llm_client
  llm_client -- "fetch" --> llmprovider
  llmprovider -- "response" --> llm_client
  llmtoolcall -- "createToolExecutor" --> llm_client
  llm_client -- "fetch" --> nvd
  mcp -- "generateThreatReport" --> llm_client
  tui -- "chatCompletion" --> llm_client

11 nodes / 13 edges, within the 12 / 16 legibility budget · 12 neighbours just outside the frame: #agent-launcher, Commands, ConfigFile, FileSystem, GitRepo, MCPClient, QueryString, RawStdin, and 4 more

Declared at src/analyze/tools.ts:19.

Trust boundary at CLI argument parsing#cli ↔ UserInput
Across the lineboth sides, and what flows between them
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.CLI · Trust boundary at CLI argument parsing"]
    cli["GuardLink.CLI"]
  end
  subgraph Z1["UserInput · Trust boundary at CLI argument parsing"]
    userinput(["UserInput"])
  end
  subgraph Z2["GuardLink.TUI · Trust boundary at interactive input"]
    tui(["GuardLink.TUI"])
  end
  blame["GuardLink.Blame"]
  filesystem["FileSystem"]
  gate["GuardLink.Gate"]
  agentproposal["AgentProposal"]
  gitconfig["GitConfig"]
  cli -.-|Trust boundary at CLI argument parsing| userinput
  tui -.-|Trust boundary at interactive input| userinput
  blame -- "formatBlameText" --> cli
  blame -- "buildBlamePayload" --> cli
  cli -- "writeFile" --> filesystem
  userinput -- "verify" --> cli
  cli -- "runGate" --> gate
  agentproposal -- "proposeEntitlement" --> cli
  gitconfig -- "execFileSync" --> cli
  tui -- "writeFile" --> filesystem
  userinput -- "readline" --> tui

8 nodes / 11 edges, within the 12 / 16 legibility budget · 20 neighbours just outside the frame: #agent-launcher, AgentPrompt, Commands, CommitTrailers, ConfigFile, #dashboard, GitRepo, LedgerFile, and 12 more

Declared at src/cli/index.ts:43.

Trust boundary at git command execution#diff ↔ GitRepo
Across the lineboth sides, and what flows between them
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Diff · Trust boundary at git command execution"]
    diff["GuardLink.Diff"]
  end
  subgraph Z1["GitRepo · Trust boundary at git command execution"]
    gitrepo["GitRepo"]
  end
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  dashboard["GuardLink.Dashboard"]
  changedfilelist["ChangedFileList"]
  mcp["GuardLink.MCP"]
  tui(["GuardLink.TUI"])
  diff -.-|Trust boundary at git command execution| gitrepo
  blame -- "formatBlameText" --> cli
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "buildBlamePayload" --> cli
  blame -- "summarise" --> dashboard
  gitrepo -- "attachBlame" --> cli
  gitrepo -- "computeBlame" --> cli
  gitrepo -- "loadSince" --> cli
  gitrepo -- "loadSince" --> dashboard
  gitrepo -- "parseAtRef" --> dashboard
  diff -- "return" --> changedfilelist
  gitrepo -- "readFileSync" --> mcp
  gitrepo -- "computeBlame" --> mcp
  gitrepo -- "computeBlame" --> tui

8 nodes / 15 edges, within the 12 / 16 legibility budget · 28 neighbours just outside the frame: #agent-launcher, AgentProposal, Commands, CommitTrailers, ConfigFile, DashboardHTML, DiagramSource, FileSystem, and 20 more

Declared at src/diff/git.ts:15.

Trust boundary at MCP protocol#mcp ↔ MCPClient
Across the lineboth sides, and what flows between them
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.MCP · Trust boundary at MCP protocol"]
    mcp["GuardLink.MCP"]
  end
  subgraph Z1["MCPClient · Trust boundary at MCP protocol"]
    mcpclient(["MCPClient"])
  end
  subgraph Z2["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z3["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  gitrepo["GitRepo"]
  configfile["ConfigFile"]
  querystring["QueryString"]
  llm_client(["GuardLink.LLM_Client"])
  mcp -.-|Trust boundary at MCP protocol| mcpclient
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  mcpclient -- "guardlink_context" --> mcp
  gitrepo -- "readFileSync" --> mcp
  mcpclient -- "stdio" --> mcp
  configfile -- "readFileSync" --> mcp
  querystring -- "lookup" --> mcp
  mcpclient -- "tool_call" --> mcp
  mcp -- "writeFile" --> filesystem
  mcp -- "generateThreatReport" --> llm_client
  mcp -- "resource" --> mcpclient
  gitrepo -- "computeBlame" --> mcp
  configfile -- "readAcceptancePolicy" --> parser
  mcpclient -- "applyAnnotations" --> parser
  parser -- "writeFileSync" --> filesystem

8 nodes / 15 edges, within the 12 / 16 legibility budget · 25 neighbours just outside the frame: #agent-launcher, Annotations, #blame, #cli, #dashboard, #diff, FilePath, GrammarFile, and 17 more

Declared at src/mcp/index.ts:9.

Trust boundary at tool argument parsing#mcp ↔ MCPClient
Across the lineboth sides, and what flows between them
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.MCP · Trust boundary at MCP protocol"]
    mcp["GuardLink.MCP"]
  end
  subgraph Z1["MCPClient · Trust boundary at MCP protocol"]
    mcpclient(["MCPClient"])
  end
  subgraph Z2["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z3["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  gitrepo["GitRepo"]
  configfile["ConfigFile"]
  querystring["QueryString"]
  llm_client(["GuardLink.LLM_Client"])
  mcp -.-|Trust boundary at MCP protocol| mcpclient
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  mcpclient -- "guardlink_context" --> mcp
  gitrepo -- "readFileSync" --> mcp
  mcpclient -- "stdio" --> mcp
  configfile -- "readFileSync" --> mcp
  querystring -- "lookup" --> mcp
  mcpclient -- "tool_call" --> mcp
  mcp -- "writeFile" --> filesystem
  mcp -- "generateThreatReport" --> llm_client
  mcp -- "resource" --> mcpclient
  gitrepo -- "computeBlame" --> mcp
  configfile -- "readAcceptancePolicy" --> parser
  mcpclient -- "applyAnnotations" --> parser
  parser -- "writeFileSync" --> filesystem

8 nodes / 15 edges, within the 12 / 16 legibility budget · 25 neighbours just outside the frame: #agent-launcher, Annotations, #blame, #cli, #dashboard, #diff, FilePath, GrammarFile, and 17 more

Declared at src/mcp/server.ts:50.

Trust boundary between parser and disk I/O#parser ↔ FileSystem
Across the lineboth sides, and what flows between them
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z1["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  dashboard["GuardLink.Dashboard"]
  cli["GuardLink.CLI"]
  configfile["ConfigFile"]
  mcp["GuardLink.MCP"]
  filepath["FilePath"]
  annotations["Annotations"]
  grammarfile["GrammarFile"]
  tui(["GuardLink.TUI"])
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  dashboard -- "writeFileSync" --> filesystem
  cli -- "writeFile" --> filesystem
  configfile -- "readFileSync" --> mcp
  mcp -- "writeFile" --> filesystem
  configfile -- "readAcceptancePolicy" --> parser
  parser -- "writeFileSync" --> filesystem
  filepath -- "readFile" --> parser
  parser -- "parseString" --> annotations
  grammarfile -- "Language.load" --> parser
  tui -- "writeFile" --> filesystem
  configfile -- "loadProjectConfig" --> tui
  tui -- "saveProjectConfig" --> configfile

10 nodes / 13 edges, within the 12 / 16 legibility budget · 28 neighbours just outside the frame: #agent-launcher, AgentProposal, #blame, Commands, DashboardHTML, DiagramSource, #gate, GitConfig, and 20 more

Declared at src/parser/parse-project.ts:14.

Trust boundary at interactive input#tui ↔ UserInput
Across the lineboth sides, and what flows between them
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.CLI · Trust boundary at CLI argument parsing"]
    cli["GuardLink.CLI"]
  end
  subgraph Z1["UserInput · Trust boundary at CLI argument parsing"]
    userinput(["UserInput"])
  end
  subgraph Z2["GuardLink.TUI · Trust boundary at interactive input"]
    tui(["GuardLink.TUI"])
  end
  configfile["ConfigFile"]
  agent_launcher["GuardLink.Agent_Launcher"]
  filesystem["FileSystem"]
  llm_client(["GuardLink.LLM_Client"])
  commands["Commands"]
  cli -.-|Trust boundary at CLI argument parsing| userinput
  tui -.-|Trust boundary at interactive input| userinput
  configfile -- "readFileSync" --> agent_launcher
  agent_launcher -- "writeFileSync" --> configfile
  cli -- "writeFile" --> filesystem
  userinput -- "verify" --> cli
  tui -- "writeFile" --> filesystem
  tui -- "launchAgent" --> agent_launcher
  tui -- "chatCompletion" --> llm_client
  configfile -- "loadProjectConfig" --> tui
  tui -- "saveProjectConfig" --> configfile
  userinput -- "readline" --> tui
  tui -- "dispatch" --> commands

8 nodes / 13 edges, within the 12 / 16 legibility budget · 31 neighbours just outside the frame: AgentProcess, AgentPrompt, AgentProposal, #blame, #dashboard, EnvVars, #gate, GitConfig, and 23 more

Declared at src/tui/index.ts:15.

Entry to sink with nothing in the way1 of 268 routes
UserArgs→#workspace-link→AgentFiles
no declared boundary on this route src/workspace/link.ts:8 · src/workspace/link.ts:9

Searched between 26 entry points and 16 sinks — endpoints that appear in the flow graph without an @asset declaration, classified by degree: no inbound flow means data originates there, no outbound flow means it terminates there. 267 further routes run entry to sink and are defended; those are not findings. Derived from the annotations with no model in the loop, so none of this can cite a line that does not exist.

src/mcp/server.ts5 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.MCP · Trust boundary at MCP protocol"]
    mcp["GuardLink.MCP"]
  end
  subgraph Z1["MCPClient · Trust boundary at MCP protocol"]
    mcpclient(["MCPClient"])
  end
  subgraph Z2["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z3["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  cli["GuardLink.CLI"]
  gitrepo["GitRepo"]
  llm_client(["GuardLink.LLM_Client"])
  mcp -.-|Trust boundary at MCP protocol| mcpclient
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  cli -- "writeFile" --> filesystem
  gitrepo -- "attachBlame" --> cli
  gitrepo -- "computeBlame" --> cli
  gitrepo -- "loadSince" --> cli
  mcpclient -- "guardlink_context" --> mcp
  gitrepo -- "readFileSync" --> mcp
  mcpclient -- "stdio" --> mcp
  mcpclient -- "tool_call" --> mcp
  mcp -- "writeFile" --> filesystem
  mcp -- "generateThreatReport" --> llm_client
  mcp -- "resource" --> mcpclient
  gitrepo -- "computeBlame" --> mcp
  mcpclient -- "applyAnnotations" --> parser
  parser -- "writeFileSync" --> filesystem

7 nodes / 16 edges, within the 12 / 16 legibility budget · 32 neighbours just outside the frame: AgentProposal, Annotations, #blame, ConfigFile, #dashboard, #diff, FilePath, #gate, and 24 more

Claims written in this file— rows
src/cli/index.ts9 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z1["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  ledgerfile["LedgerFile"]
  cli["GuardLink.CLI"]
  gitrepo["GitRepo"]
  gate["GuardLink.Gate"]
  scanreport["ScanReport"]
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  ledgerfile -- "readLedger" --> cli
  cli -- "writeFile" --> filesystem
  gitrepo -- "attachBlame" --> cli
  ledgerfile -- "readHypotheses" --> cli
  gitrepo -- "computeBlame" --> cli
  cli -- "runGate" --> gate
  scanreport -- "importScan" --> cli
  gitrepo -- "loadSince" --> cli
  cli -- "writeHypotheses" --> ledgerfile
  parser -- "writeFileSync" --> filesystem
  ledgerfile -- "readLedger" --> parser
  cli -- "writeLedger" --> ledgerfile
  ledgerfile -- "classifyClaims" --> parser
  cli -- "applyVerification" --> ledgerfile

7 nodes / 15 edges, within the 12 / 16 legibility budget · 23 neighbours just outside the frame: AgentPrompt, AgentProposal, Annotations, #blame, ConfigFile, #dashboard, #diff, FilePath, and 15 more

Claims written in this file— rows
src/blame/git.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Diff · Trust boundary at git command execution"]
    diff["GuardLink.Diff"]
  end
  subgraph Z1["GitRepo · Trust boundary at git command execution"]
    gitrepo["GitRepo"]
  end
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  dashboard["GuardLink.Dashboard"]
  committrailers["CommitTrailers"]
  mcp["GuardLink.MCP"]
  tui(["GuardLink.TUI"])
  diff -.-|Trust boundary at git command execution| gitrepo
  blame -- "formatBlameText" --> cli
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "buildBlamePayload" --> cli
  blame -- "summarise" --> dashboard
  committrailers -- "parseTrailerBlock" --> blame
  gitrepo -- "attachBlame" --> cli
  gitrepo -- "computeBlame" --> cli
  gitrepo -- "loadSince" --> cli
  gitrepo -- "loadSince" --> dashboard
  gitrepo -- "parseAtRef" --> dashboard
  gitrepo -- "readFileSync" --> mcp
  gitrepo -- "computeBlame" --> mcp
  gitrepo -- "computeBlame" --> tui

8 nodes / 15 edges, within the 12 / 16 legibility budget · 28 neighbours just outside the frame: #agent-launcher, AgentProposal, ChangedFileList, Commands, ConfigFile, DashboardHTML, DiagramSource, FileSystem, and 20 more

Claims written in this file— rows
src/diff/git.ts6 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Diff · Trust boundary at git command execution"]
    diff["GuardLink.Diff"]
  end
  subgraph Z1["GitRepo · Trust boundary at git command execution"]
    gitrepo["GitRepo"]
  end
  threatmodel["ThreatModel"]
  gitref["GitRef"]
  tempdir["TempDir"]
  changedfilelist["ChangedFileList"]
  diff -.-|Trust boundary at git command execution| gitrepo
  threatmodel -- "diffModels" --> diff
  gitref -- "execSync" --> diff
  diff -- "writeFileSync" --> tempdir
  diff -- "parseProject" --> threatmodel
  diff -- "return" --> changedfilelist
  gitref -- "parseAtRef" --> diff

6 nodes / 7 edges, within the 12 / 16 legibility budget · 12 neighbours just outside the frame: #agent-launcher, #blame, #cli, #dashboard, #gate, #llm-client, #mcp, #parser, and 4 more

Claims written in this file— rows
src/review/entitlements.ts5 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  dashboard["GuardLink.Dashboard"]
  sourcefiles["SourceFiles"]
  gitconfig["GitConfig"]
  parser["GuardLink.Parser"]
  agentproposal["AgentProposal"]
  proposalledger["ProposalLedger"]
  blame -- "formatBlameText" --> cli
  blame -- "buildBlamePayload" --> cli
  blame -- "summarise" --> dashboard
  sourcefiles -- "findAnchorDrift" --> cli
  sourcefiles -- "readFileSync" --> dashboard
  gitconfig -- "detectRepoLinks" --> dashboard
  gitconfig -- "readFileSync" --> dashboard
  cli -- "writeConfirmedLine" --> sourcefiles
  parser -- "writeFile" --> sourcefiles
  agentproposal -- "proposeEntitlement" --> cli
  cli -- "writeFile" --> proposalledger
  proposalledger -- "readFile" --> cli
  cli -- "applyProposalDecision" --> sourcefiles
  gitconfig -- "execFileSync" --> cli
  cli -- "writeFile" --> sourcefiles
  sourcefiles -- "attachAnchors" --> parser

8 nodes / 16 edges, within the 12 / 16 legibility budget · 21 neighbours just outside the frame: Annotations, CommitTrailers, ConfigFile, DashboardHTML, DiagramSource, FilePath, FileSystem, #gate, and 13 more

Claims written in this file— rows
src/tui/commands.ts6 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z1["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  userargs(["UserArgs"])
  cli["GuardLink.CLI"]
  gitrepo["GitRepo"]
  mcp["GuardLink.MCP"]
  llm_client(["GuardLink.LLM_Client"])
  tui(["GuardLink.TUI"])
  agent_launcher["GuardLink.Agent_Launcher"]
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  userargs -- "process.argv" --> cli
  cli -- "writeFile" --> filesystem
  gitrepo -- "attachBlame" --> cli
  gitrepo -- "computeBlame" --> cli
  gitrepo -- "loadSince" --> cli
  gitrepo -- "readFileSync" --> mcp
  mcp -- "writeFile" --> filesystem
  mcp -- "generateThreatReport" --> llm_client
  gitrepo -- "computeBlame" --> mcp
  parser -- "writeFileSync" --> filesystem
  userargs -- "args" --> tui
  tui -- "writeFile" --> filesystem
  tui -- "launchAgent" --> agent_launcher
  tui -- "chatCompletion" --> llm_client
  gitrepo -- "computeBlame" --> tui

9 nodes / 16 edges, within the 12 / 16 legibility budget · 39 neighbours just outside the frame: AgentProcess, AgentPrompt, AgentProposal, Annotations, #blame, Commands, ConfigFile, #dashboard, and 31 more

Claims written in this file— rows
src/init/index.ts4 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 47, "rankSpacing": 67, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  configfile["ConfigFile"]
  agent_launcher["GuardLink.Agent_Launcher"]
  blame["GuardLink.Blame"]
  projectroot["ProjectRoot"]
  init["GuardLink.Init"]
  agentfiles["AgentFiles"]
  definitionsfile["DefinitionsFile"]
  mcp["GuardLink.MCP"]
  parser["GuardLink.Parser"]
  tui(["GuardLink.TUI"])
  workspace_link["Workspace.Link"]
  configfile -- "readFileSync" --> agent_launcher
  agent_launcher -- "writeFileSync" --> configfile
  configfile -- "readBlameConfig" --> blame
  projectroot -- "detectProject" --> init
  projectroot -- "options.root" --> init
  init -- "writeFileSync" --> agentfiles
  init -- "writeFileSync" --> configfile
  definitionsfile -- "definitionsArePopulated" --> init
  configfile -- "configIsCustomised" --> init
  configfile -- "readFileSync" --> mcp
  configfile -- "readAcceptancePolicy" --> parser
  projectroot -- "fast-glob" --> parser
  tui -- "launchAgent" --> agent_launcher
  configfile -- "loadProjectConfig" --> tui
  tui -- "saveProjectConfig" --> configfile
  workspace_link -- "updateAgentWorkspaceContext" --> agentfiles

11 nodes / 16 edges, within the 12 / 16 legibility budget · 25 neighbours just outside the frame: AgentProcess, AgentPrompt, Annotations, #cli, Commands, CommitTrailers, #dashboard, EnvVars, and 17 more

Claims written in this file— rows
src/agents/config.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Agent_Launcher · Trust boundary at process spawn"]
    agent_launcher["GuardLink.Agent_Launcher"]
  end
  subgraph Z1["AgentProcess · Trust boundary at process spawn"]
    agentprocess["AgentProcess"]
  end
  envvars["EnvVars"]
  configfile["ConfigFile"]
  agentprompt["AgentPrompt"]
  blame["GuardLink.Blame"]
  init["GuardLink.Init"]
  mcp["GuardLink.MCP"]
  parser["GuardLink.Parser"]
  tui(["GuardLink.TUI"])
  agent_launcher -.-|Trust boundary at process spawn| agentprocess
  envvars -- "process.env" --> agent_launcher
  configfile -- "readFileSync" --> agent_launcher
  agent_launcher -- "writeFileSync" --> configfile
  agent_launcher -- "spawn" --> agentprocess
  agentprocess -- "stdout" --> agent_launcher
  agent_launcher -- "return" --> agentprompt
  configfile -- "readBlameConfig" --> blame
  init -- "writeFileSync" --> configfile
  configfile -- "configIsCustomised" --> init
  configfile -- "readFileSync" --> mcp
  configfile -- "readAcceptancePolicy" --> parser
  tui -- "launchAgent" --> agent_launcher
  configfile -- "loadProjectConfig" --> tui
  tui -- "saveProjectConfig" --> configfile

10 nodes / 15 edges, within the 12 / 16 legibility budget · 26 neighbours just outside the frame: AgentFiles, Annotations, #cli, Commands, CommitTrailers, #dashboard, DefinitionsFile, FilePath, and 18 more

Claims written in this file— rows
src/agents/prompts.ts4 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  userprompt(["UserPrompt"])
  agent_launcher["GuardLink.Agent_Launcher"]
  threatmodel["ThreatModel"]
  agentprompt["AgentPrompt"]
  parser["GuardLink.Parser"]
  userprompt -- "launchAgent" --> agent_launcher
  userprompt -- "buildAnnotatePrompt" --> agent_launcher
  userprompt -- "buildTranslatePrompt" --> agent_launcher
  userprompt -- "buildAskPrompt" --> agent_launcher
  threatmodel -- "model" --> agent_launcher
  agent_launcher -- "return" --> agentprompt
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser

5 nodes / 16 edges, within the 12 / 16 legibility budget · 24 neighbours just outside the frame: AgentProcess, Annotations, #blame, #cli, ConfigFile, #dashboard, #diff, EnvVars, and 16 more

Claims written in this file— rows
src/analyze/index.ts5 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  llm_client(["GuardLink.LLM_Client"])
  projectfiles["ProjectFiles"]
  reportfile["ReportFile"]
  pentestfindings["PentestFindings"]
  parser["GuardLink.Parser"]
  threatmodel -- "serializeModel" --> llm_client
  projectfiles -- "readFileSync" --> llm_client
  llm_client -- "writeFileSync" --> reportfile
  pentestfindings -- "readFileSync" --> llm_client
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser

6 nodes / 14 edges, within the 12 / 16 legibility budget · 27 neighbours just outside the frame: #agent-launcher, Annotations, #blame, #cli, ConfigFile, #dashboard, #diff, FilePath, and 19 more

Claims written in this file— rows
src/analyze/tools.ts4 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.LLM_Client · Trust boundary at external API call"]
    llm_client(["GuardLink.LLM_Client"])
  end
  subgraph Z1["LLMProvider · Trust boundary at external API call"]
    llmprovider["LLMProvider"]
  end
  subgraph Z2["NVD · Trust boundary at external API"]
    nvd["NVD"]
  end
  savedreport["SavedReport"]
  projectfiles["ProjectFiles"]
  reportfile["ReportFile"]
  pentestfindings["PentestFindings"]
  llmconfig["LLMConfig"]
  llmtoolcall["LLMToolCall"]
  mcp["GuardLink.MCP"]
  tui(["GuardLink.TUI"])
  llm_client -.-|Trust boundary at external API call| llmprovider
  llm_client -.-|Trust boundary at external API| nvd
  savedreport -- "parseFindingsBlock" --> llm_client
  projectfiles -- "readFileSync" --> llm_client
  llm_client -- "writeFileSync" --> reportfile
  pentestfindings -- "readFileSync" --> llm_client
  llmconfig -- "chatCompletion" --> llm_client
  llm_client -- "fetch" --> llmprovider
  llmprovider -- "response" --> llm_client
  llmtoolcall -- "createToolExecutor" --> llm_client
  llm_client -- "fetch" --> nvd
  mcp -- "generateThreatReport" --> llm_client
  tui -- "chatCompletion" --> llm_client

11 nodes / 13 edges, within the 12 / 16 legibility budget · 12 neighbours just outside the frame: #agent-launcher, Commands, ConfigFile, FileSystem, GitRepo, MCPClient, QueryString, RawStdin, and 4 more

Claims written in this file— rows
src/review/index.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  cli["GuardLink.CLI"]
  sourcefiles["SourceFiles"]
  threatmodel -- "runCiChecks" --> cli
  sourcefiles -- "findAnchorDrift" --> cli
  threatmodel -- "findUnmitigatedPaths" --> cli
  threatmodel -- "classifyHypotheses" --> cli
  cli -- "attachHypotheses" --> threatmodel
  cli -- "writeConfirmedLine" --> sourcefiles
  cli -- "applyProposalDecision" --> sourcefiles
  threatmodel -- "getReviewableExposures" --> cli
  cli -- "writeFile" --> sourcefiles

3 nodes / 9 edges, within the 12 / 16 legibility budget · 22 neighbours just outside the frame: #agent-launcher, AgentProposal, #blame, #dashboard, #diff, FileSystem, #gate, GitConfig, and 14 more

Claims written in this file— rows
src/agents/launcher.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Agent_Launcher · Trust boundary at process spawn"]
    agent_launcher["GuardLink.Agent_Launcher"]
  end
  subgraph Z1["AgentProcess · Trust boundary at process spawn"]
    agentprocess["AgentProcess"]
  end
  envvars["EnvVars"]
  configfile["ConfigFile"]
  userprompt(["UserPrompt"])
  threatmodel["ThreatModel"]
  agentprompt["AgentPrompt"]
  tui(["GuardLink.TUI"])
  agent_launcher -.-|Trust boundary at process spawn| agentprocess
  envvars -- "process.env" --> agent_launcher
  configfile -- "readFileSync" --> agent_launcher
  agent_launcher -- "writeFileSync" --> configfile
  userprompt -- "launchAgent" --> agent_launcher
  agent_launcher -- "spawn" --> agentprocess
  agentprocess -- "stdout" --> agent_launcher
  userprompt -- "buildAnnotatePrompt" --> agent_launcher
  userprompt -- "buildTranslatePrompt" --> agent_launcher
  userprompt -- "buildAskPrompt" --> agent_launcher
  threatmodel -- "model" --> agent_launcher
  agent_launcher -- "return" --> agentprompt
  tui -- "launchAgent" --> agent_launcher
  configfile -- "loadProjectConfig" --> tui
  tui -- "saveProjectConfig" --> configfile

8 nodes / 15 edges, within the 12 / 16 legibility budget · 19 neighbours just outside the frame: #blame, #cli, Commands, #dashboard, #diff, FileSystem, #gate, GitRepo, and 11 more

Claims written in this file— rows
src/analyze/llm.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.LLM_Client · Trust boundary at external API call"]
    llm_client(["GuardLink.LLM_Client"])
  end
  subgraph Z1["LLMProvider · Trust boundary at external API call"]
    llmprovider["LLMProvider"]
  end
  subgraph Z2["NVD · Trust boundary at external API"]
    nvd["NVD"]
  end
  savedreport["SavedReport"]
  projectfiles["ProjectFiles"]
  reportfile["ReportFile"]
  pentestfindings["PentestFindings"]
  llmconfig["LLMConfig"]
  llmtoolcall["LLMToolCall"]
  mcp["GuardLink.MCP"]
  tui(["GuardLink.TUI"])
  llm_client -.-|Trust boundary at external API call| llmprovider
  llm_client -.-|Trust boundary at external API| nvd
  savedreport -- "parseFindingsBlock" --> llm_client
  projectfiles -- "readFileSync" --> llm_client
  llm_client -- "writeFileSync" --> reportfile
  pentestfindings -- "readFileSync" --> llm_client
  llmconfig -- "chatCompletion" --> llm_client
  llm_client -- "fetch" --> llmprovider
  llmprovider -- "response" --> llm_client
  llmtoolcall -- "createToolExecutor" --> llm_client
  llm_client -- "fetch" --> nvd
  mcp -- "generateThreatReport" --> llm_client
  tui -- "chatCompletion" --> llm_client

11 nodes / 13 edges, within the 12 / 16 legibility budget · 12 neighbours just outside the frame: #agent-launcher, Commands, ConfigFile, FileSystem, GitRepo, MCPClient, QueryString, RawStdin, and 4 more

Claims written in this file— rows
src/dashboard/generate.ts7 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  dashboard["GuardLink.Dashboard"]
  sourcefiles["SourceFiles"]
  ledgerfile["LedgerFile"]
  gitrepo["GitRepo"]
  gitconfig["GitConfig"]
  html["HTML"]
  threatmodel -- "emitArtifacts" --> dashboard
  sourcefiles -- "readFileSync" --> dashboard
  ledgerfile -- "readLedger" --> dashboard
  threatmodel -- "generateThreatGraph" --> dashboard
  threatmodel -- "buildExploreData" --> dashboard
  gitrepo -- "loadSince" --> dashboard
  threatmodel -- "computeStats" --> dashboard
  ledgerfile -- "computeLedgerStates" --> dashboard
  gitconfig -- "detectRepoLinks" --> dashboard
  dashboard -- "return" --> html
  ledgerfile -- "readHypotheses" --> dashboard
  threatmodel -- "generateDashboardHTML" --> dashboard
  gitconfig -- "readFileSync" --> dashboard
  threatmodel -- "buildClaims" --> dashboard
  gitrepo -- "parseAtRef" --> dashboard
  threatmodel -- "growWithinBudget" --> dashboard

7 nodes / 16 edges, within the 12 / 16 legibility budget · 15 neighbours just outside the frame: #agent-launcher, #blame, #cli, DashboardHTML, DiagramSource, #diff, FileSystem, #gate, and 7 more

Claims written in this file— rows
src/dashboard/links.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  dashboard["GuardLink.Dashboard"]
  filesystem["FileSystem"]
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  gitconfig["GitConfig"]
  dashboardhtml["DashboardHTML"]
  diagramsource["DiagramSource"]
  dashboard -- "writeFileSync" --> filesystem
  blame -- "formatBlameText" --> cli
  blame -- "buildBlamePayload" --> cli
  blame -- "summarise" --> dashboard
  cli -- "writeFile" --> filesystem
  gitconfig -- "detectRepoLinks" --> dashboard
  gitconfig -- "readFileSync" --> dashboard
  dashboard -- "RepoLinks" --> dashboardhtml
  diagramsource -- "checkRenderBudget" --> dashboard
  diagramsource -- "measureLegibility" --> dashboard
  gitconfig -- "execFileSync" --> cli

7 nodes / 11 edges, within the 12 / 16 legibility budget · 18 neighbours just outside the frame: AgentProposal, CommitTrailers, ConfigFile, #gate, GitRepo, HTML, LedgerFile, #mcp, and 10 more

Claims written in this file— rows
src/mcp/suggest.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z1["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  filepath["FilePath"]
  suggest["GuardLink.Suggest"]
  suggestions["Suggestions"]
  configfile["ConfigFile"]
  mcpclient(["MCPClient"])
  projectroot["ProjectRoot"]
  ledgerfile["LedgerFile"]
  annotations["Annotations"]
  parsediagnostics["ParseDiagnostics"]
  grammarfile["GrammarFile"]
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  filepath -- "readFileSync" --> suggest
  suggest -- "suggestAnnotations" --> suggestions
  configfile -- "readAcceptancePolicy" --> parser
  mcpclient -- "applyAnnotations" --> parser
  parser -- "writeFileSync" --> filesystem
  projectroot -- "fast-glob" --> parser
  ledgerfile -- "readLedger" --> parser
  filepath -- "readFile" --> parser
  parser -- "parseString" --> annotations
  parser -- "parseString" --> parsediagnostics
  ledgerfile -- "classifyClaims" --> parser
  grammarfile -- "Language.load" --> parser

12 nodes / 13 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #agent-launcher, #blame, #cli, #dashboard, #init, #mcp, SourceFile, SourceFiles, and 2 more

Claims written in this file— rows
src/blame/compute.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  blame["GuardLink.Blame"]
  threatmodel["ThreatModel"]
  parser["GuardLink.Parser"]
  blame -- "attachBlame" --> threatmodel
  threatmodel -- "computeBlame" --> blame
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser

3 nodes / 12 edges, within the 12 / 16 legibility budget · 23 neighbours just outside the frame: #agent-launcher, Annotations, #cli, CommitTrailers, ConfigFile, #dashboard, #diff, FilePath, and 15 more

Claims written in this file— rows
src/blame/config.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  configfile["ConfigFile"]
  agent_launcher["GuardLink.Agent_Launcher"]
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  dashboard["GuardLink.Dashboard"]
  committrailers["CommitTrailers"]
  init["GuardLink.Init"]
  mcp["GuardLink.MCP"]
  parser["GuardLink.Parser"]
  tui(["GuardLink.TUI"])
  configfile -- "readFileSync" --> agent_launcher
  agent_launcher -- "writeFileSync" --> configfile
  configfile -- "readBlameConfig" --> blame
  blame -- "formatBlameText" --> cli
  blame -- "buildBlamePayload" --> cli
  blame -- "summarise" --> dashboard
  committrailers -- "parseTrailerBlock" --> blame
  init -- "writeFileSync" --> configfile
  configfile -- "configIsCustomised" --> init
  configfile -- "readFileSync" --> mcp
  configfile -- "readAcceptancePolicy" --> parser
  tui -- "launchAgent" --> agent_launcher
  configfile -- "loadProjectConfig" --> tui
  tui -- "saveProjectConfig" --> configfile

10 nodes / 14 edges, within the 12 / 16 legibility budget · 34 neighbours just outside the frame: AgentFiles, AgentProcess, AgentPrompt, AgentProposal, Annotations, Commands, DashboardHTML, DefinitionsFile, and 26 more

Claims written in this file— rows
src/blame/trailers.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  configfile["ConfigFile"]
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  gitrepo["GitRepo"]
  dashboard["GuardLink.Dashboard"]
  committrailers["CommitTrailers"]
  configfile -- "readBlameConfig" --> blame
  blame -- "formatBlameText" --> cli
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "buildBlamePayload" --> cli
  blame -- "summarise" --> dashboard
  committrailers -- "parseTrailerBlock" --> blame
  gitrepo -- "attachBlame" --> cli
  gitrepo -- "computeBlame" --> cli
  gitrepo -- "loadSince" --> cli
  gitrepo -- "loadSince" --> dashboard
  gitrepo -- "parseAtRef" --> dashboard

6 nodes / 12 edges, within the 12 / 16 legibility budget · 23 neighbours just outside the frame: #agent-launcher, AgentProposal, DashboardHTML, DiagramSource, #diff, FileSystem, #gate, GitConfig, and 15 more

Claims written in this file— rows
src/hypothesis/commands.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  dashboard["GuardLink.Dashboard"]
  sourcefiles["SourceFiles"]
  gate["GuardLink.Gate"]
  scanreport["ScanReport"]
  parser["GuardLink.Parser"]
  agentproposal["AgentProposal"]
  blame -- "formatBlameText" --> cli
  blame -- "buildBlamePayload" --> cli
  blame -- "summarise" --> dashboard
  sourcefiles -- "findAnchorDrift" --> cli
  cli -- "runGate" --> gate
  scanreport -- "importScan" --> cli
  sourcefiles -- "readFileSync" --> dashboard
  gate -- "stripViolations" --> sourcefiles
  cli -- "writeConfirmedLine" --> sourcefiles
  parser -- "writeFile" --> sourcefiles
  agentproposal -- "proposeEntitlement" --> cli
  cli -- "applyProposalDecision" --> sourcefiles
  cli -- "writeFile" --> sourcefiles
  sourcefiles -- "attachAnchors" --> parser

8 nodes / 14 edges, within the 12 / 16 legibility budget · 22 neighbours just outside the frame: AgentPrompt, Annotations, CommitTrailers, ConfigFile, DashboardHTML, DiagramSource, FilePath, FileSystem, and 14 more

Claims written in this file— rows
src/mcp/subgraph.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  mcp["GuardLink.MCP"]
  parser["GuardLink.Parser"]
  threatmodel -- "fileContext" --> mcp
  threatmodel -- "buildEnvelope" --> mcp
  threatmodel -- "selectSubgraph" --> mcp
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser

3 nodes / 13 edges, within the 12 / 16 legibility budget · 23 neighbours just outside the frame: #agent-launcher, Annotations, #blame, #cli, ConfigFile, #dashboard, #diff, FilePath, and 15 more

Claims written in this file— rows
src/parser/acceptance.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  cli["GuardLink.CLI"]
  parser["GuardLink.Parser"]
  configfile["ConfigFile"]
  threatmodel -- "runCiChecks" --> cli
  threatmodel -- "findUnmitigatedPaths" --> cli
  threatmodel -- "classifyHypotheses" --> cli
  cli -- "attachHypotheses" --> threatmodel
  threatmodel -- "findAcceptanceDefects" --> parser
  configfile -- "readAcceptancePolicy" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser
  threatmodel -- "getReviewableExposures" --> cli

4 nodes / 16 edges, within the 12 / 16 legibility budget · 30 neighbours just outside the frame: #agent-launcher, AgentProposal, Annotations, #blame, #dashboard, #diff, FilePath, FileSystem, and 22 more

Claims written in this file— rows
src/parser/apply-annotations.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.MCP · Trust boundary at MCP protocol"]
    mcp["GuardLink.MCP"]
  end
  subgraph Z1["MCPClient · Trust boundary at MCP protocol"]
    mcpclient(["MCPClient"])
  end
  subgraph Z2["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z3["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  dashboard["GuardLink.Dashboard"]
  cli["GuardLink.CLI"]
  annotations["Annotations"]
  tui(["GuardLink.TUI"])
  mcp -.-|Trust boundary at MCP protocol| mcpclient
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  dashboard -- "writeFileSync" --> filesystem
  cli -- "writeFile" --> filesystem
  mcpclient -- "guardlink_context" --> mcp
  mcpclient -- "stdio" --> mcp
  mcpclient -- "tool_call" --> mcp
  mcp -- "writeFile" --> filesystem
  mcp -- "resource" --> mcpclient
  mcpclient -- "applyAnnotations" --> parser
  parser -- "writeFileSync" --> filesystem
  parser -- "parseString" --> annotations
  tui -- "writeFile" --> filesystem

8 nodes / 13 edges, within the 12 / 16 legibility budget · 28 neighbours just outside the frame: #agent-launcher, AgentProposal, #blame, Commands, ConfigFile, DashboardHTML, DiagramSource, FilePath, and 20 more

Claims written in this file— rows
src/parser/ledger.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  dashboard["GuardLink.Dashboard"]
  ledgerfile["LedgerFile"]
  gate["GuardLink.Gate"]
  parser["GuardLink.Parser"]
  annotations["Annotations"]
  agentproposal["AgentProposal"]
  blame -- "formatBlameText" --> cli
  blame -- "buildBlamePayload" --> cli
  blame -- "summarise" --> dashboard
  ledgerfile -- "readLedger" --> cli
  ledgerfile -- "readHypotheses" --> cli
  cli -- "runGate" --> gate
  ledgerfile -- "readLedger" --> dashboard
  ledgerfile -- "computeLedgerStates" --> dashboard
  ledgerfile -- "readHypotheses" --> dashboard
  cli -- "writeHypotheses" --> ledgerfile
  ledgerfile -- "readLedger" --> parser
  cli -- "writeLedger" --> ledgerfile
  parser -- "parseString" --> annotations
  ledgerfile -- "classifyClaims" --> parser
  cli -- "applyVerification" --> ledgerfile
  agentproposal -- "proposeEntitlement" --> cli

8 nodes / 16 edges, within the 12 / 16 legibility budget · 22 neighbours just outside the frame: AgentPrompt, CommitTrailers, ConfigFile, DashboardHTML, DiagramSource, FilePath, FileSystem, GitConfig, and 14 more

Claims written in this file— rows
src/parser/parse-project.ts4 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z1["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  threatmodel["ThreatModel"]
  projectroot["ProjectRoot"]
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  parser -- "writeFileSync" --> filesystem
  threatmodel -- "canonicalizeModelOrder" --> parser
  projectroot -- "fast-glob" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser

4 nodes / 13 edges, within the 12 / 16 legibility budget · 22 neighbours just outside the frame: #agent-launcher, Annotations, #blame, #cli, ConfigFile, #dashboard, #diff, FilePath, and 14 more

Claims written in this file— rows
src/structure/attach.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z1["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  dashboard["GuardLink.Dashboard"]
  sourcefiles["SourceFiles"]
  cli["GuardLink.CLI"]
  gate["GuardLink.Gate"]
  configfile["ConfigFile"]
  filepath["FilePath"]
  annotations["Annotations"]
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  dashboard -- "writeFileSync" --> filesystem
  sourcefiles -- "findAnchorDrift" --> cli
  cli -- "writeFile" --> filesystem
  cli -- "runGate" --> gate
  sourcefiles -- "readFileSync" --> dashboard
  gate -- "stripViolations" --> sourcefiles
  cli -- "writeConfirmedLine" --> sourcefiles
  configfile -- "readAcceptancePolicy" --> parser
  parser -- "writeFileSync" --> filesystem
  parser -- "writeFile" --> sourcefiles
  filepath -- "readFile" --> parser
  parser -- "parseString" --> annotations
  cli -- "applyProposalDecision" --> sourcefiles
  cli -- "writeFile" --> sourcefiles
  sourcefiles -- "attachAnchors" --> parser

9 nodes / 16 edges, within the 12 / 16 legibility budget · 25 neighbours just outside the frame: #agent-launcher, AgentPrompt, AgentProposal, #blame, DashboardHTML, DiagramSource, GitConfig, GitRepo, and 17 more

Claims written in this file— rows
src/parser/clear.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  sourcefiles["SourceFiles"]
  cli["GuardLink.CLI"]
  gate["GuardLink.Gate"]
  dashboard["GuardLink.Dashboard"]
  projectroot["ProjectRoot"]
  init["GuardLink.Init"]
  configfile["ConfigFile"]
  parser["GuardLink.Parser"]
  annotations["Annotations"]
  sourcefiles -- "findAnchorDrift" --> cli
  cli -- "runGate" --> gate
  sourcefiles -- "readFileSync" --> dashboard
  gate -- "stripViolations" --> sourcefiles
  cli -- "writeConfirmedLine" --> sourcefiles
  projectroot -- "detectProject" --> init
  projectroot -- "options.root" --> init
  init -- "writeFileSync" --> configfile
  configfile -- "configIsCustomised" --> init
  configfile -- "readAcceptancePolicy" --> parser
  projectroot -- "fast-glob" --> parser
  parser -- "writeFile" --> sourcefiles
  parser -- "parseString" --> annotations
  cli -- "applyProposalDecision" --> sourcefiles
  cli -- "writeFile" --> sourcefiles
  sourcefiles -- "attachAnchors" --> parser

9 nodes / 16 edges, within the 12 / 16 legibility budget · 26 neighbours just outside the frame: #agent-launcher, AgentFiles, AgentPrompt, AgentProposal, #blame, DashboardHTML, DefinitionsFile, DiagramSource, and 18 more

Claims written in this file— rows
src/parser/comment-strip.ts1 component named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z1["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  configfile["ConfigFile"]
  mcpclient(["MCPClient"])
  projectroot["ProjectRoot"]
  sourcefiles["SourceFiles"]
  sourcefile["SourceFile"]
  ledgerfile["LedgerFile"]
  filepath["FilePath"]
  annotations["Annotations"]
  parsediagnostics["ParseDiagnostics"]
  grammarfile["GrammarFile"]
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  configfile -- "readAcceptancePolicy" --> parser
  mcpclient -- "applyAnnotations" --> parser
  parser -- "writeFileSync" --> filesystem
  projectroot -- "fast-glob" --> parser
  parser -- "writeFile" --> sourcefiles
  sourcefile -- "resolveGalPath" --> parser
  ledgerfile -- "readLedger" --> parser
  filepath -- "readFile" --> parser
  parser -- "parseString" --> annotations
  parser -- "parseString" --> parsediagnostics
  ledgerfile -- "classifyClaims" --> parser
  sourcefiles -- "attachAnchors" --> parser
  sourcefile -- "parseStructure" --> parser
  grammarfile -- "Language.load" --> parser

12 nodes / 15 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #agent-launcher, #blame, #cli, #dashboard, #gate, #init, #mcp, #suggest, and 2 more

Claims written in this file— rows
src/parser/migrate-mode.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z1["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  threatmodel["ThreatModel"]
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  parser -- "writeFileSync" --> filesystem
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser

3 nodes / 12 edges, within the 12 / 16 legibility budget · 22 neighbours just outside the frame: #agent-launcher, Annotations, #blame, #cli, ConfigFile, #dashboard, #diff, FilePath, and 14 more

Claims written in this file— rows
src/tui/index.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.CLI · Trust boundary at CLI argument parsing"]
    cli["GuardLink.CLI"]
  end
  subgraph Z1["UserInput · Trust boundary at CLI argument parsing"]
    userinput(["UserInput"])
  end
  subgraph Z2["GuardLink.TUI · Trust boundary at interactive input"]
    tui(["GuardLink.TUI"])
  end
  configfile["ConfigFile"]
  agent_launcher["GuardLink.Agent_Launcher"]
  filesystem["FileSystem"]
  llm_client(["GuardLink.LLM_Client"])
  commands["Commands"]
  cli -.-|Trust boundary at CLI argument parsing| userinput
  tui -.-|Trust boundary at interactive input| userinput
  configfile -- "readFileSync" --> agent_launcher
  agent_launcher -- "writeFileSync" --> configfile
  cli -- "writeFile" --> filesystem
  userinput -- "verify" --> cli
  tui -- "writeFile" --> filesystem
  tui -- "launchAgent" --> agent_launcher
  tui -- "chatCompletion" --> llm_client
  configfile -- "loadProjectConfig" --> tui
  tui -- "saveProjectConfig" --> configfile
  userinput -- "readline" --> tui
  tui -- "dispatch" --> commands

8 nodes / 13 edges, within the 12 / 16 legibility budget · 31 neighbours just outside the frame: AgentProcess, AgentPrompt, AgentProposal, #blame, #dashboard, EnvVars, #gate, GitConfig, and 23 more

Claims written in this file— rows
src/analyze/findings.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.LLM_Client · Trust boundary at external API call"]
    llm_client(["GuardLink.LLM_Client"])
  end
  subgraph Z1["LLMProvider · Trust boundary at external API call"]
    llmprovider["LLMProvider"]
  end
  subgraph Z2["NVD · Trust boundary at external API"]
    nvd["NVD"]
  end
  savedreport["SavedReport"]
  projectfiles["ProjectFiles"]
  reportfile["ReportFile"]
  pentestfindings["PentestFindings"]
  llmconfig["LLMConfig"]
  llmtoolcall["LLMToolCall"]
  mcp["GuardLink.MCP"]
  tui(["GuardLink.TUI"])
  llm_client -.-|Trust boundary at external API call| llmprovider
  llm_client -.-|Trust boundary at external API| nvd
  savedreport -- "parseFindingsBlock" --> llm_client
  projectfiles -- "readFileSync" --> llm_client
  llm_client -- "writeFileSync" --> reportfile
  pentestfindings -- "readFileSync" --> llm_client
  llmconfig -- "chatCompletion" --> llm_client
  llm_client -- "fetch" --> llmprovider
  llmprovider -- "response" --> llm_client
  llmtoolcall -- "createToolExecutor" --> llm_client
  llm_client -- "fetch" --> nvd
  mcp -- "generateThreatReport" --> llm_client
  tui -- "chatCompletion" --> llm_client

11 nodes / 13 edges, within the 12 / 16 legibility budget · 12 neighbours just outside the frame: #agent-launcher, Commands, ConfigFile, FileSystem, GitRepo, MCPClient, QueryString, RawStdin, and 4 more

Claims written in this file— rows
src/artifacts/emit.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  dashboard["GuardLink.Dashboard"]
  filesystem["FileSystem"]
  threatmodel -- "emitArtifacts" --> dashboard
  dashboard -- "writeFileSync" --> filesystem
  threatmodel -- "generateThreatGraph" --> dashboard
  threatmodel -- "buildExploreData" --> dashboard
  threatmodel -- "computeStats" --> dashboard
  threatmodel -- "generateDashboardHTML" --> dashboard
  threatmodel -- "buildClaims" --> dashboard
  threatmodel -- "growWithinBudget" --> dashboard

3 nodes / 8 edges, within the 12 / 16 legibility budget · 19 neighbours just outside the frame: #agent-launcher, #blame, #cli, DashboardHTML, DiagramSource, #diff, #gate, GitConfig, and 11 more

Claims written in this file— rows
src/dashboard/annotations.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  dashboard["GuardLink.Dashboard"]
  sourcefiles["SourceFiles"]
  gate["GuardLink.Gate"]
  dashboardhtml["DashboardHTML"]
  diagramsource["DiagramSource"]
  parser["GuardLink.Parser"]
  blame -- "formatBlameText" --> cli
  blame -- "buildBlamePayload" --> cli
  blame -- "summarise" --> dashboard
  sourcefiles -- "findAnchorDrift" --> cli
  cli -- "runGate" --> gate
  sourcefiles -- "readFileSync" --> dashboard
  dashboard -- "RepoLinks" --> dashboardhtml
  diagramsource -- "checkRenderBudget" --> dashboard
  gate -- "stripViolations" --> sourcefiles
  diagramsource -- "measureLegibility" --> dashboard
  cli -- "writeConfirmedLine" --> sourcefiles
  parser -- "writeFile" --> sourcefiles
  cli -- "applyProposalDecision" --> sourcefiles
  cli -- "writeFile" --> sourcefiles
  sourcefiles -- "attachAnchors" --> parser

8 nodes / 15 edges, within the 12 / 16 legibility budget · 22 neighbours just outside the frame: AgentPrompt, AgentProposal, Annotations, CommitTrailers, ConfigFile, FilePath, FileSystem, GitConfig, and 14 more

Claims written in this file— rows
src/dashboard/explore.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  dashboard["GuardLink.Dashboard"]
  threatmodel -- "emitArtifacts" --> dashboard
  threatmodel -- "generateThreatGraph" --> dashboard
  threatmodel -- "buildExploreData" --> dashboard
  threatmodel -- "computeStats" --> dashboard
  threatmodel -- "generateDashboardHTML" --> dashboard
  threatmodel -- "buildClaims" --> dashboard
  threatmodel -- "growWithinBudget" --> dashboard

2 nodes / 7 edges, within the 12 / 16 legibility budget · 19 neighbours just outside the frame: #agent-launcher, #blame, #cli, DashboardHTML, DiagramSource, #diff, FileSystem, #gate, and 11 more

Claims written in this file— rows
src/dashboard/index.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  dashboard["GuardLink.Dashboard"]
  threatmodel -- "emitArtifacts" --> dashboard
  threatmodel -- "generateThreatGraph" --> dashboard
  threatmodel -- "buildExploreData" --> dashboard
  threatmodel -- "computeStats" --> dashboard
  threatmodel -- "generateDashboardHTML" --> dashboard
  threatmodel -- "buildClaims" --> dashboard
  threatmodel -- "growWithinBudget" --> dashboard

2 nodes / 7 edges, within the 12 / 16 legibility budget · 19 neighbours just outside the frame: #agent-launcher, #blame, #cli, DashboardHTML, DiagramSource, #diff, FileSystem, #gate, and 11 more

Claims written in this file— rows
src/dashboard/render-budget.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  dashboard["GuardLink.Dashboard"]
  filesystem["FileSystem"]
  gitrepo["GitRepo"]
  blame["GuardLink.Blame"]
  sourcefiles["SourceFiles"]
  ledgerfile["LedgerFile"]
  gitconfig["GitConfig"]
  html["HTML"]
  dashboardhtml["DashboardHTML"]
  diagramsource["DiagramSource"]
  dashboard -- "writeFileSync" --> filesystem
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "summarise" --> dashboard
  sourcefiles -- "readFileSync" --> dashboard
  ledgerfile -- "readLedger" --> dashboard
  gitrepo -- "loadSince" --> dashboard
  ledgerfile -- "computeLedgerStates" --> dashboard
  gitconfig -- "detectRepoLinks" --> dashboard
  dashboard -- "return" --> html
  ledgerfile -- "readHypotheses" --> dashboard
  gitconfig -- "readFileSync" --> dashboard
  dashboard -- "RepoLinks" --> dashboardhtml
  diagramsource -- "checkRenderBudget" --> dashboard
  gitrepo -- "parseAtRef" --> dashboard
  diagramsource -- "measureLegibility" --> dashboard

10 nodes / 16 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #cli, CommitTrailers, ConfigFile, #diff, #gate, #mcp, #parser, #report-metadata, and 2 more

Claims written in this file— rows
src/dashboard/since.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Diff · Trust boundary at git command execution"]
    diff["GuardLink.Diff"]
  end
  subgraph Z1["GitRepo · Trust boundary at git command execution"]
    gitrepo["GitRepo"]
  end
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  dashboard["GuardLink.Dashboard"]
  dashboardhtml["DashboardHTML"]
  mcp["GuardLink.MCP"]
  tui(["GuardLink.TUI"])
  diff -.-|Trust boundary at git command execution| gitrepo
  blame -- "formatBlameText" --> cli
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "buildBlamePayload" --> cli
  blame -- "summarise" --> dashboard
  gitrepo -- "attachBlame" --> cli
  gitrepo -- "computeBlame" --> cli
  gitrepo -- "loadSince" --> cli
  gitrepo -- "loadSince" --> dashboard
  dashboard -- "RepoLinks" --> dashboardhtml
  gitrepo -- "parseAtRef" --> dashboard
  gitrepo -- "readFileSync" --> mcp
  gitrepo -- "computeBlame" --> mcp
  gitrepo -- "computeBlame" --> tui

8 nodes / 15 edges, within the 12 / 16 legibility budget · 28 neighbours just outside the frame: #agent-launcher, AgentProposal, ChangedFileList, Commands, CommitTrailers, ConfigFile, DiagramSource, FileSystem, and 20 more

Claims written in this file— rows
src/gate/gate.ts4 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  gate["GuardLink.Gate"]
  agentprompt["AgentPrompt"]
  sourcefiles["SourceFiles"]
  parser["GuardLink.Parser"]
  threatmodel -- "runGate" --> gate
  gate -- "buildGateFollowUp" --> agentprompt
  gate -- "stripViolations" --> sourcefiles
  threatmodel -- "lintAnnotations" --> gate
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  parser -- "writeFile" --> sourcefiles
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser
  sourcefiles -- "attachAnchors" --> parser

5 nodes / 16 edges, within the 12 / 16 legibility budget · 20 neighbours just outside the frame: #agent-launcher, Annotations, #blame, #cli, ConfigFile, #dashboard, #diff, FilePath, and 12 more

Claims written in this file— rows
src/graph/legibility.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  dashboard["GuardLink.Dashboard"]
  filesystem["FileSystem"]
  gitrepo["GitRepo"]
  blame["GuardLink.Blame"]
  sourcefiles["SourceFiles"]
  ledgerfile["LedgerFile"]
  gitconfig["GitConfig"]
  html["HTML"]
  dashboardhtml["DashboardHTML"]
  diagramsource["DiagramSource"]
  dashboard -- "writeFileSync" --> filesystem
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "summarise" --> dashboard
  sourcefiles -- "readFileSync" --> dashboard
  ledgerfile -- "readLedger" --> dashboard
  gitrepo -- "loadSince" --> dashboard
  ledgerfile -- "computeLedgerStates" --> dashboard
  gitconfig -- "detectRepoLinks" --> dashboard
  dashboard -- "return" --> html
  ledgerfile -- "readHypotheses" --> dashboard
  gitconfig -- "readFileSync" --> dashboard
  dashboard -- "RepoLinks" --> dashboardhtml
  diagramsource -- "checkRenderBudget" --> dashboard
  gitrepo -- "parseAtRef" --> dashboard
  diagramsource -- "measureLegibility" --> dashboard

10 nodes / 16 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #cli, CommitTrailers, ConfigFile, #diff, #gate, #mcp, #parser, #report-metadata, and 2 more

Claims written in this file— rows
src/hypothesis/ledger.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  dashboard["GuardLink.Dashboard"]
  ledgerfile["LedgerFile"]
  gate["GuardLink.Gate"]
  parser["GuardLink.Parser"]
  agentproposal["AgentProposal"]
  blame -- "formatBlameText" --> cli
  blame -- "buildBlamePayload" --> cli
  blame -- "summarise" --> dashboard
  ledgerfile -- "readLedger" --> cli
  ledgerfile -- "readHypotheses" --> cli
  cli -- "runGate" --> gate
  ledgerfile -- "readLedger" --> dashboard
  ledgerfile -- "computeLedgerStates" --> dashboard
  ledgerfile -- "readHypotheses" --> dashboard
  cli -- "writeHypotheses" --> ledgerfile
  ledgerfile -- "readLedger" --> parser
  cli -- "writeLedger" --> ledgerfile
  ledgerfile -- "classifyClaims" --> parser
  cli -- "applyVerification" --> ledgerfile
  agentproposal -- "proposeEntitlement" --> cli

7 nodes / 15 edges, within the 12 / 16 legibility budget · 23 neighbours just outside the frame: AgentPrompt, Annotations, CommitTrailers, ConfigFile, DashboardHTML, DiagramSource, FilePath, FileSystem, and 15 more

Claims written in this file— rows
src/init/detect.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  configfile["ConfigFile"]
  agent_launcher["GuardLink.Agent_Launcher"]
  blame["GuardLink.Blame"]
  projectroot["ProjectRoot"]
  init["GuardLink.Init"]
  agentfiles["AgentFiles"]
  definitionsfile["DefinitionsFile"]
  mcp["GuardLink.MCP"]
  parser["GuardLink.Parser"]
  annotations["Annotations"]
  tui(["GuardLink.TUI"])
  configfile -- "readFileSync" --> agent_launcher
  agent_launcher -- "writeFileSync" --> configfile
  configfile -- "readBlameConfig" --> blame
  projectroot -- "detectProject" --> init
  projectroot -- "options.root" --> init
  init -- "writeFileSync" --> agentfiles
  init -- "writeFileSync" --> configfile
  definitionsfile -- "definitionsArePopulated" --> init
  configfile -- "configIsCustomised" --> init
  configfile -- "readFileSync" --> mcp
  configfile -- "readAcceptancePolicy" --> parser
  projectroot -- "fast-glob" --> parser
  parser -- "parseString" --> annotations
  tui -- "launchAgent" --> agent_launcher
  configfile -- "loadProjectConfig" --> tui
  tui -- "saveProjectConfig" --> configfile

11 nodes / 16 edges, within the 12 / 16 legibility budget · 25 neighbours just outside the frame: AgentProcess, AgentPrompt, #cli, Commands, CommitTrailers, #dashboard, EnvVars, FilePath, and 17 more

Claims written in this file— rows
src/init/preserve.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  configfile["ConfigFile"]
  agent_launcher["GuardLink.Agent_Launcher"]
  blame["GuardLink.Blame"]
  projectroot["ProjectRoot"]
  init["GuardLink.Init"]
  agentfiles["AgentFiles"]
  definitionsfile["DefinitionsFile"]
  mcp["GuardLink.MCP"]
  parser["GuardLink.Parser"]
  tui(["GuardLink.TUI"])
  configfile -- "readFileSync" --> agent_launcher
  agent_launcher -- "writeFileSync" --> configfile
  configfile -- "readBlameConfig" --> blame
  projectroot -- "detectProject" --> init
  projectroot -- "options.root" --> init
  init -- "writeFileSync" --> agentfiles
  init -- "writeFileSync" --> configfile
  definitionsfile -- "definitionsArePopulated" --> init
  configfile -- "configIsCustomised" --> init
  configfile -- "readFileSync" --> mcp
  configfile -- "readAcceptancePolicy" --> parser
  projectroot -- "fast-glob" --> parser
  tui -- "launchAgent" --> agent_launcher
  configfile -- "loadProjectConfig" --> tui
  tui -- "saveProjectConfig" --> configfile

10 nodes / 15 edges, within the 12 / 16 legibility budget · 26 neighbours just outside the frame: AgentProcess, AgentPrompt, Annotations, #cli, Commands, CommitTrailers, #dashboard, EnvVars, and 18 more

Claims written in this file— rows
src/mcp/context.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.MCP · Trust boundary at MCP protocol"]
    mcp["GuardLink.MCP"]
  end
  subgraph Z1["MCPClient · Trust boundary at MCP protocol"]
    mcpclient(["MCPClient"])
  end
  threatmodel["ThreatModel"]
  mcp -.-|Trust boundary at MCP protocol| mcpclient
  mcpclient -- "guardlink_context" --> mcp
  threatmodel -- "fileContext" --> mcp
  threatmodel -- "buildEnvelope" --> mcp
  mcpclient -- "stdio" --> mcp
  mcpclient -- "tool_call" --> mcp
  mcp -- "resource" --> mcpclient
  threatmodel -- "selectSubgraph" --> mcp

3 nodes / 8 edges, within the 12 / 16 legibility budget · 15 neighbours just outside the frame: #agent-launcher, #blame, #cli, ConfigFile, #dashboard, #diff, FileSystem, #gate, and 7 more

Claims written in this file— rows
src/mcp/lookup.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.MCP · Trust boundary at MCP protocol"]
    mcp["GuardLink.MCP"]
  end
  subgraph Z1["MCPClient · Trust boundary at MCP protocol"]
    mcpclient(["MCPClient"])
  end
  threatmodel["ThreatModel"]
  llm_client(["GuardLink.LLM_Client"])
  gitrepo["GitRepo"]
  configfile["ConfigFile"]
  querystring["QueryString"]
  filesystem["FileSystem"]
  mcp -.-|Trust boundary at MCP protocol| mcpclient
  threatmodel -- "serializeModel" --> llm_client
  mcpclient -- "guardlink_context" --> mcp
  threatmodel -- "fileContext" --> mcp
  threatmodel -- "buildEnvelope" --> mcp
  gitrepo -- "readFileSync" --> mcp
  mcpclient -- "stdio" --> mcp
  configfile -- "readFileSync" --> mcp
  querystring -- "lookup" --> mcp
  mcpclient -- "tool_call" --> mcp
  mcp -- "writeFile" --> filesystem
  mcp -- "generateThreatReport" --> llm_client
  mcp -- "resource" --> mcpclient
  gitrepo -- "computeBlame" --> mcp
  threatmodel -- "selectSubgraph" --> mcp

8 nodes / 15 edges, within the 12 / 16 legibility budget · 20 neighbours just outside the frame: #agent-launcher, #blame, #cli, #dashboard, #diff, #gate, #init, LLMConfig, and 12 more

Claims written in this file— rows
src/parser/fingerprint.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z1["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  projectroot["ProjectRoot"]
  init["GuardLink.Init"]
  configfile["ConfigFile"]
  mcpclient(["MCPClient"])
  ledgerfile["LedgerFile"]
  filepath["FilePath"]
  annotations["Annotations"]
  parsediagnostics["ParseDiagnostics"]
  grammarfile["GrammarFile"]
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  projectroot -- "detectProject" --> init
  projectroot -- "options.root" --> init
  init -- "writeFileSync" --> configfile
  configfile -- "configIsCustomised" --> init
  configfile -- "readAcceptancePolicy" --> parser
  mcpclient -- "applyAnnotations" --> parser
  parser -- "writeFileSync" --> filesystem
  projectroot -- "fast-glob" --> parser
  ledgerfile -- "readLedger" --> parser
  filepath -- "readFile" --> parser
  parser -- "parseString" --> annotations
  parser -- "parseString" --> parsediagnostics
  ledgerfile -- "classifyClaims" --> parser
  grammarfile -- "Language.load" --> parser

11 nodes / 15 edges, within the 12 / 16 legibility budget · 12 neighbours just outside the frame: #agent-launcher, AgentFiles, #blame, #cli, #dashboard, DefinitionsFile, #mcp, SourceFile, and 4 more

Claims written in this file— rows
src/parser/parse-file.ts4 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z1["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  parsediagnostics["ParseDiagnostics"]
  cli["GuardLink.CLI"]
  filepath["FilePath"]
  suggest["GuardLink.Suggest"]
  configfile["ConfigFile"]
  annotations["Annotations"]
  grammarfile["GrammarFile"]
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  parsediagnostics -- "runCiChecks" --> cli
  cli -- "writeFile" --> filesystem
  filepath -- "readFileSync" --> suggest
  configfile -- "readAcceptancePolicy" --> parser
  parser -- "writeFileSync" --> filesystem
  filepath -- "readFile" --> parser
  parser -- "parseString" --> annotations
  parser -- "parseString" --> parsediagnostics
  grammarfile -- "Language.load" --> parser

9 nodes / 10 edges, within the 12 / 16 legibility budget · 22 neighbours just outside the frame: #agent-launcher, AgentProposal, #blame, #dashboard, #gate, GitConfig, GitRepo, #init, and 14 more

Claims written in this file— rows
src/parser/parse-line.ts1 component named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z1["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  configfile["ConfigFile"]
  mcpclient(["MCPClient"])
  projectroot["ProjectRoot"]
  sourcefiles["SourceFiles"]
  sourcefile["SourceFile"]
  ledgerfile["LedgerFile"]
  filepath["FilePath"]
  annotations["Annotations"]
  parsediagnostics["ParseDiagnostics"]
  grammarfile["GrammarFile"]
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  configfile -- "readAcceptancePolicy" --> parser
  mcpclient -- "applyAnnotations" --> parser
  parser -- "writeFileSync" --> filesystem
  projectroot -- "fast-glob" --> parser
  parser -- "writeFile" --> sourcefiles
  sourcefile -- "resolveGalPath" --> parser
  ledgerfile -- "readLedger" --> parser
  filepath -- "readFile" --> parser
  parser -- "parseString" --> annotations
  parser -- "parseString" --> parsediagnostics
  ledgerfile -- "classifyClaims" --> parser
  sourcefiles -- "attachAnchors" --> parser
  sourcefile -- "parseStructure" --> parser
  grammarfile -- "Language.load" --> parser

12 nodes / 15 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #agent-launcher, #blame, #cli, #dashboard, #gate, #init, #mcp, #suggest, and 2 more

Claims written in this file— rows
src/parser/reanchor.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  cli["GuardLink.CLI"]
  parser["GuardLink.Parser"]
  threatmodel -- "runCiChecks" --> cli
  threatmodel -- "findUnmitigatedPaths" --> cli
  threatmodel -- "classifyHypotheses" --> cli
  cli -- "attachHypotheses" --> threatmodel
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser
  threatmodel -- "getReviewableExposures" --> cli

3 nodes / 15 edges, within the 12 / 16 legibility budget · 29 neighbours just outside the frame: #agent-launcher, AgentProposal, Annotations, #blame, ConfigFile, #dashboard, #diff, FilePath, and 21 more

Claims written in this file— rows
src/parser/verify.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  dashboard["GuardLink.Dashboard"]
  ledgerfile["LedgerFile"]
  gate["GuardLink.Gate"]
  parser["GuardLink.Parser"]
  agentproposal["AgentProposal"]
  blame -- "formatBlameText" --> cli
  blame -- "buildBlamePayload" --> cli
  blame -- "summarise" --> dashboard
  ledgerfile -- "readLedger" --> cli
  ledgerfile -- "readHypotheses" --> cli
  cli -- "runGate" --> gate
  ledgerfile -- "readLedger" --> dashboard
  ledgerfile -- "computeLedgerStates" --> dashboard
  ledgerfile -- "readHypotheses" --> dashboard
  cli -- "writeHypotheses" --> ledgerfile
  ledgerfile -- "readLedger" --> parser
  cli -- "writeLedger" --> ledgerfile
  ledgerfile -- "classifyClaims" --> parser
  cli -- "applyVerification" --> ledgerfile
  agentproposal -- "proposeEntitlement" --> cli

7 nodes / 15 edges, within the 12 / 16 legibility budget · 23 neighbours just outside the frame: AgentPrompt, Annotations, CommitTrailers, ConfigFile, DashboardHTML, DiagramSource, FilePath, FileSystem, and 15 more

Claims written in this file— rows
src/playbooks/select.ts1 component named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  gitrepo["GitRepo"]
  ledgerfile["LedgerFile"]
  filesystem["FileSystem"]
  gate["GuardLink.Gate"]
  agentproposal["AgentProposal"]
  gitconfig["GitConfig"]
  blame -- "formatBlameText" --> cli
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "buildBlamePayload" --> cli
  ledgerfile -- "readLedger" --> cli
  cli -- "writeFile" --> filesystem
  gitrepo -- "attachBlame" --> cli
  ledgerfile -- "readHypotheses" --> cli
  gitrepo -- "computeBlame" --> cli
  cli -- "runGate" --> gate
  gitrepo -- "loadSince" --> cli
  cli -- "writeHypotheses" --> ledgerfile
  cli -- "writeLedger" --> ledgerfile
  cli -- "applyVerification" --> ledgerfile
  agentproposal -- "proposeEntitlement" --> cli
  gitconfig -- "execFileSync" --> cli

8 nodes / 16 edges, within the 12 / 16 legibility budget · 17 neighbours just outside the frame: AgentPrompt, CommitTrailers, ConfigFile, #dashboard, #diff, #mcp, PackageJson, ParseDiagnostics, and 9 more

Claims written in this file— rows
src/structure/runtime.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z1["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  configfile["ConfigFile"]
  mcpclient(["MCPClient"])
  projectroot["ProjectRoot"]
  sourcefiles["SourceFiles"]
  sourcefile["SourceFile"]
  ledgerfile["LedgerFile"]
  filepath["FilePath"]
  annotations["Annotations"]
  parsediagnostics["ParseDiagnostics"]
  grammarfile["GrammarFile"]
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  configfile -- "readAcceptancePolicy" --> parser
  mcpclient -- "applyAnnotations" --> parser
  parser -- "writeFileSync" --> filesystem
  projectroot -- "fast-glob" --> parser
  parser -- "writeFile" --> sourcefiles
  sourcefile -- "resolveGalPath" --> parser
  ledgerfile -- "readLedger" --> parser
  filepath -- "readFile" --> parser
  parser -- "parseString" --> annotations
  parser -- "parseString" --> parsediagnostics
  ledgerfile -- "classifyClaims" --> parser
  sourcefiles -- "attachAnchors" --> parser
  sourcefile -- "parseStructure" --> parser
  grammarfile -- "Language.load" --> parser

12 nodes / 15 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #agent-launcher, #blame, #cli, #dashboard, #gate, #init, #mcp, #suggest, and 2 more

Claims written in this file— rows
src/tui/config.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  configfile["ConfigFile"]
  agent_launcher["GuardLink.Agent_Launcher"]
  blame["GuardLink.Blame"]
  init["GuardLink.Init"]
  mcp["GuardLink.MCP"]
  llm_client(["GuardLink.LLM_Client"])
  parser["GuardLink.Parser"]
  tui(["GuardLink.TUI"])
  commands["Commands"]
  configfile -- "readFileSync" --> agent_launcher
  agent_launcher -- "writeFileSync" --> configfile
  configfile -- "readBlameConfig" --> blame
  init -- "writeFileSync" --> configfile
  configfile -- "configIsCustomised" --> init
  configfile -- "readFileSync" --> mcp
  mcp -- "generateThreatReport" --> llm_client
  configfile -- "readAcceptancePolicy" --> parser
  tui -- "launchAgent" --> agent_launcher
  tui -- "chatCompletion" --> llm_client
  configfile -- "loadProjectConfig" --> tui
  tui -- "saveProjectConfig" --> configfile
  tui -- "dispatch" --> commands

9 nodes / 13 edges, within the 12 / 16 legibility budget · 34 neighbours just outside the frame: AgentFiles, AgentProcess, AgentPrompt, Annotations, #cli, CommitTrailers, #dashboard, DefinitionsFile, and 26 more

Claims written in this file— rows
src/tui/input.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.TUI · Trust boundary at interactive input"]
    tui(["GuardLink.TUI"])
  end
  subgraph Z1["UserInput · Trust boundary at interactive input"]
    userinput(["UserInput"])
  end
  configfile["ConfigFile"]
  agent_launcher["GuardLink.Agent_Launcher"]
  userargs(["UserArgs"])
  filesystem["FileSystem"]
  llm_client(["GuardLink.LLM_Client"])
  gitrepo["GitRepo"]
  commands["Commands"]
  rawstdin["RawStdin"]
  terminal["Terminal"]
  tui -.-|Trust boundary at interactive input| userinput
  configfile -- "readFileSync" --> agent_launcher
  agent_launcher -- "writeFileSync" --> configfile
  userargs -- "args" --> tui
  tui -- "writeFile" --> filesystem
  tui -- "launchAgent" --> agent_launcher
  tui -- "chatCompletion" --> llm_client
  gitrepo -- "computeBlame" --> tui
  configfile -- "loadProjectConfig" --> tui
  tui -- "saveProjectConfig" --> configfile
  userinput -- "readline" --> tui
  tui -- "dispatch" --> commands
  rawstdin -- "process.stdin" --> tui
  tui -- "process.stdout" --> terminal

11 nodes / 14 edges, within the 12 / 16 legibility budget · 22 neighbours just outside the frame: AgentProcess, AgentPrompt, #blame, #cli, #dashboard, #diff, EnvVars, #init, and 14 more

Claims written in this file— rows
src/analyzer/sarif.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  sarif["GuardLink.SARIF"]
  sariflog["SarifLog"]
  parser["GuardLink.Parser"]
  threatmodel -- "generateSarif" --> sarif
  sarif -- "return" --> sariflog
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser

4 nodes / 12 edges, within the 12 / 16 legibility budget · 21 neighbours just outside the frame: #agent-launcher, Annotations, #blame, #cli, ConfigFile, #dashboard, #diff, FilePath, and 13 more

Claims written in this file— rows
src/dashboard/client.ts1 component named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  dashboard["GuardLink.Dashboard"]
  filesystem["FileSystem"]
  gitrepo["GitRepo"]
  blame["GuardLink.Blame"]
  sourcefiles["SourceFiles"]
  ledgerfile["LedgerFile"]
  gitconfig["GitConfig"]
  html["HTML"]
  dashboardhtml["DashboardHTML"]
  diagramsource["DiagramSource"]
  dashboard -- "writeFileSync" --> filesystem
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "summarise" --> dashboard
  sourcefiles -- "readFileSync" --> dashboard
  ledgerfile -- "readLedger" --> dashboard
  gitrepo -- "loadSince" --> dashboard
  ledgerfile -- "computeLedgerStates" --> dashboard
  gitconfig -- "detectRepoLinks" --> dashboard
  dashboard -- "return" --> html
  ledgerfile -- "readHypotheses" --> dashboard
  gitconfig -- "readFileSync" --> dashboard
  dashboard -- "RepoLinks" --> dashboardhtml
  diagramsource -- "checkRenderBudget" --> dashboard
  gitrepo -- "parseAtRef" --> dashboard
  diagramsource -- "measureLegibility" --> dashboard

10 nodes / 16 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #cli, CommitTrailers, ConfigFile, #diff, #gate, #mcp, #parser, #report-metadata, and 2 more

Claims written in this file— rows
src/dashboard/diagrams.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  dashboard["GuardLink.Dashboard"]
  threatmodel -- "emitArtifacts" --> dashboard
  threatmodel -- "generateThreatGraph" --> dashboard
  threatmodel -- "buildExploreData" --> dashboard
  threatmodel -- "computeStats" --> dashboard
  threatmodel -- "generateDashboardHTML" --> dashboard
  threatmodel -- "buildClaims" --> dashboard
  threatmodel -- "growWithinBudget" --> dashboard

2 nodes / 7 edges, within the 12 / 16 legibility budget · 19 neighbours just outside the frame: #agent-launcher, #blame, #cli, DashboardHTML, DiagramSource, #diff, FileSystem, #gate, and 11 more

Claims written in this file— rows
src/dashboard/html.ts1 component named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  dashboard["GuardLink.Dashboard"]
  filesystem["FileSystem"]
  gitrepo["GitRepo"]
  blame["GuardLink.Blame"]
  sourcefiles["SourceFiles"]
  ledgerfile["LedgerFile"]
  gitconfig["GitConfig"]
  html["HTML"]
  dashboardhtml["DashboardHTML"]
  diagramsource["DiagramSource"]
  dashboard -- "writeFileSync" --> filesystem
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "summarise" --> dashboard
  sourcefiles -- "readFileSync" --> dashboard
  ledgerfile -- "readLedger" --> dashboard
  gitrepo -- "loadSince" --> dashboard
  ledgerfile -- "computeLedgerStates" --> dashboard
  gitconfig -- "detectRepoLinks" --> dashboard
  dashboard -- "return" --> html
  ledgerfile -- "readHypotheses" --> dashboard
  gitconfig -- "readFileSync" --> dashboard
  dashboard -- "RepoLinks" --> dashboardhtml
  diagramsource -- "checkRenderBudget" --> dashboard
  gitrepo -- "parseAtRef" --> dashboard
  diagramsource -- "measureLegibility" --> dashboard

10 nodes / 16 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #cli, CommitTrailers, ConfigFile, #diff, #gate, #mcp, #parser, #report-metadata, and 2 more

Claims written in this file— rows
src/dashboard/pages/analytics.ts1 component named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  dashboard["GuardLink.Dashboard"]
  filesystem["FileSystem"]
  gitrepo["GitRepo"]
  blame["GuardLink.Blame"]
  sourcefiles["SourceFiles"]
  ledgerfile["LedgerFile"]
  gitconfig["GitConfig"]
  html["HTML"]
  dashboardhtml["DashboardHTML"]
  diagramsource["DiagramSource"]
  dashboard -- "writeFileSync" --> filesystem
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "summarise" --> dashboard
  sourcefiles -- "readFileSync" --> dashboard
  ledgerfile -- "readLedger" --> dashboard
  gitrepo -- "loadSince" --> dashboard
  ledgerfile -- "computeLedgerStates" --> dashboard
  gitconfig -- "detectRepoLinks" --> dashboard
  dashboard -- "return" --> html
  ledgerfile -- "readHypotheses" --> dashboard
  gitconfig -- "readFileSync" --> dashboard
  dashboard -- "RepoLinks" --> dashboardhtml
  diagramsource -- "checkRenderBudget" --> dashboard
  gitrepo -- "parseAtRef" --> dashboard
  diagramsource -- "measureLegibility" --> dashboard

10 nodes / 16 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #cli, CommitTrailers, ConfigFile, #diff, #gate, #mcp, #parser, #report-metadata, and 2 more

Claims written in this file— rows
src/dashboard/pages/assets.ts1 component named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  dashboard["GuardLink.Dashboard"]
  filesystem["FileSystem"]
  gitrepo["GitRepo"]
  blame["GuardLink.Blame"]
  sourcefiles["SourceFiles"]
  ledgerfile["LedgerFile"]
  gitconfig["GitConfig"]
  html["HTML"]
  dashboardhtml["DashboardHTML"]
  diagramsource["DiagramSource"]
  dashboard -- "writeFileSync" --> filesystem
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "summarise" --> dashboard
  sourcefiles -- "readFileSync" --> dashboard
  ledgerfile -- "readLedger" --> dashboard
  gitrepo -- "loadSince" --> dashboard
  ledgerfile -- "computeLedgerStates" --> dashboard
  gitconfig -- "detectRepoLinks" --> dashboard
  dashboard -- "return" --> html
  ledgerfile -- "readHypotheses" --> dashboard
  gitconfig -- "readFileSync" --> dashboard
  dashboard -- "RepoLinks" --> dashboardhtml
  diagramsource -- "checkRenderBudget" --> dashboard
  gitrepo -- "parseAtRef" --> dashboard
  diagramsource -- "measureLegibility" --> dashboard

10 nodes / 16 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #cli, CommitTrailers, ConfigFile, #diff, #gate, #mcp, #parser, #report-metadata, and 2 more

Claims written in this file— rows
src/dashboard/pages/attribution.ts1 component named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  dashboard["GuardLink.Dashboard"]
  filesystem["FileSystem"]
  gitrepo["GitRepo"]
  blame["GuardLink.Blame"]
  sourcefiles["SourceFiles"]
  ledgerfile["LedgerFile"]
  gitconfig["GitConfig"]
  html["HTML"]
  dashboardhtml["DashboardHTML"]
  diagramsource["DiagramSource"]
  dashboard -- "writeFileSync" --> filesystem
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "summarise" --> dashboard
  sourcefiles -- "readFileSync" --> dashboard
  ledgerfile -- "readLedger" --> dashboard
  gitrepo -- "loadSince" --> dashboard
  ledgerfile -- "computeLedgerStates" --> dashboard
  gitconfig -- "detectRepoLinks" --> dashboard
  dashboard -- "return" --> html
  ledgerfile -- "readHypotheses" --> dashboard
  gitconfig -- "readFileSync" --> dashboard
  dashboard -- "RepoLinks" --> dashboardhtml
  diagramsource -- "checkRenderBudget" --> dashboard
  gitrepo -- "parseAtRef" --> dashboard
  diagramsource -- "measureLegibility" --> dashboard

10 nodes / 16 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #cli, CommitTrailers, ConfigFile, #diff, #gate, #mcp, #parser, #report-metadata, and 2 more

Claims written in this file— rows
src/dashboard/pages/code.ts1 component named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  dashboard["GuardLink.Dashboard"]
  filesystem["FileSystem"]
  gitrepo["GitRepo"]
  blame["GuardLink.Blame"]
  sourcefiles["SourceFiles"]
  ledgerfile["LedgerFile"]
  gitconfig["GitConfig"]
  html["HTML"]
  dashboardhtml["DashboardHTML"]
  diagramsource["DiagramSource"]
  dashboard -- "writeFileSync" --> filesystem
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "summarise" --> dashboard
  sourcefiles -- "readFileSync" --> dashboard
  ledgerfile -- "readLedger" --> dashboard
  gitrepo -- "loadSince" --> dashboard
  ledgerfile -- "computeLedgerStates" --> dashboard
  gitconfig -- "detectRepoLinks" --> dashboard
  dashboard -- "return" --> html
  ledgerfile -- "readHypotheses" --> dashboard
  gitconfig -- "readFileSync" --> dashboard
  dashboard -- "RepoLinks" --> dashboardhtml
  diagramsource -- "checkRenderBudget" --> dashboard
  gitrepo -- "parseAtRef" --> dashboard
  diagramsource -- "measureLegibility" --> dashboard

10 nodes / 16 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #cli, CommitTrailers, ConfigFile, #diff, #gate, #mcp, #parser, #report-metadata, and 2 more

Claims written in this file— rows
src/dashboard/pages/data-boundaries.ts1 component named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  dashboard["GuardLink.Dashboard"]
  filesystem["FileSystem"]
  gitrepo["GitRepo"]
  blame["GuardLink.Blame"]
  sourcefiles["SourceFiles"]
  ledgerfile["LedgerFile"]
  gitconfig["GitConfig"]
  html["HTML"]
  dashboardhtml["DashboardHTML"]
  diagramsource["DiagramSource"]
  dashboard -- "writeFileSync" --> filesystem
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "summarise" --> dashboard
  sourcefiles -- "readFileSync" --> dashboard
  ledgerfile -- "readLedger" --> dashboard
  gitrepo -- "loadSince" --> dashboard
  ledgerfile -- "computeLedgerStates" --> dashboard
  gitconfig -- "detectRepoLinks" --> dashboard
  dashboard -- "return" --> html
  ledgerfile -- "readHypotheses" --> dashboard
  gitconfig -- "readFileSync" --> dashboard
  dashboard -- "RepoLinks" --> dashboardhtml
  diagramsource -- "checkRenderBudget" --> dashboard
  gitrepo -- "parseAtRef" --> dashboard
  diagramsource -- "measureLegibility" --> dashboard

10 nodes / 16 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #cli, CommitTrailers, ConfigFile, #diff, #gate, #mcp, #parser, #report-metadata, and 2 more

Claims written in this file— rows
src/dashboard/pages/diagrams.ts1 component named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  dashboard["GuardLink.Dashboard"]
  filesystem["FileSystem"]
  gitrepo["GitRepo"]
  blame["GuardLink.Blame"]
  sourcefiles["SourceFiles"]
  ledgerfile["LedgerFile"]
  gitconfig["GitConfig"]
  html["HTML"]
  dashboardhtml["DashboardHTML"]
  diagramsource["DiagramSource"]
  dashboard -- "writeFileSync" --> filesystem
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "summarise" --> dashboard
  sourcefiles -- "readFileSync" --> dashboard
  ledgerfile -- "readLedger" --> dashboard
  gitrepo -- "loadSince" --> dashboard
  ledgerfile -- "computeLedgerStates" --> dashboard
  gitconfig -- "detectRepoLinks" --> dashboard
  dashboard -- "return" --> html
  ledgerfile -- "readHypotheses" --> dashboard
  gitconfig -- "readFileSync" --> dashboard
  dashboard -- "RepoLinks" --> dashboardhtml
  diagramsource -- "checkRenderBudget" --> dashboard
  gitrepo -- "parseAtRef" --> dashboard
  diagramsource -- "measureLegibility" --> dashboard

10 nodes / 16 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #cli, CommitTrailers, ConfigFile, #diff, #gate, #mcp, #parser, #report-metadata, and 2 more

Claims written in this file— rows
src/dashboard/pages/explore.ts1 component named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  dashboard["GuardLink.Dashboard"]
  filesystem["FileSystem"]
  gitrepo["GitRepo"]
  blame["GuardLink.Blame"]
  sourcefiles["SourceFiles"]
  ledgerfile["LedgerFile"]
  gitconfig["GitConfig"]
  html["HTML"]
  dashboardhtml["DashboardHTML"]
  diagramsource["DiagramSource"]
  dashboard -- "writeFileSync" --> filesystem
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "summarise" --> dashboard
  sourcefiles -- "readFileSync" --> dashboard
  ledgerfile -- "readLedger" --> dashboard
  gitrepo -- "loadSince" --> dashboard
  ledgerfile -- "computeLedgerStates" --> dashboard
  gitconfig -- "detectRepoLinks" --> dashboard
  dashboard -- "return" --> html
  ledgerfile -- "readHypotheses" --> dashboard
  gitconfig -- "readFileSync" --> dashboard
  dashboard -- "RepoLinks" --> dashboardhtml
  diagramsource -- "checkRenderBudget" --> dashboard
  gitrepo -- "parseAtRef" --> dashboard
  diagramsource -- "measureLegibility" --> dashboard

10 nodes / 16 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #cli, CommitTrailers, ConfigFile, #diff, #gate, #mcp, #parser, #report-metadata, and 2 more

Claims written in this file— rows
src/dashboard/pages/reports.ts1 component named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  dashboard["GuardLink.Dashboard"]
  filesystem["FileSystem"]
  gitrepo["GitRepo"]
  blame["GuardLink.Blame"]
  sourcefiles["SourceFiles"]
  ledgerfile["LedgerFile"]
  gitconfig["GitConfig"]
  html["HTML"]
  dashboardhtml["DashboardHTML"]
  diagramsource["DiagramSource"]
  dashboard -- "writeFileSync" --> filesystem
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "summarise" --> dashboard
  sourcefiles -- "readFileSync" --> dashboard
  ledgerfile -- "readLedger" --> dashboard
  gitrepo -- "loadSince" --> dashboard
  ledgerfile -- "computeLedgerStates" --> dashboard
  gitconfig -- "detectRepoLinks" --> dashboard
  dashboard -- "return" --> html
  ledgerfile -- "readHypotheses" --> dashboard
  gitconfig -- "readFileSync" --> dashboard
  dashboard -- "RepoLinks" --> dashboardhtml
  diagramsource -- "checkRenderBudget" --> dashboard
  gitrepo -- "parseAtRef" --> dashboard
  diagramsource -- "measureLegibility" --> dashboard

10 nodes / 16 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #cli, CommitTrailers, ConfigFile, #diff, #gate, #mcp, #parser, #report-metadata, and 2 more

Claims written in this file— rows
src/dashboard/pages/summary.ts1 component named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  dashboard["GuardLink.Dashboard"]
  filesystem["FileSystem"]
  gitrepo["GitRepo"]
  blame["GuardLink.Blame"]
  sourcefiles["SourceFiles"]
  ledgerfile["LedgerFile"]
  gitconfig["GitConfig"]
  html["HTML"]
  dashboardhtml["DashboardHTML"]
  diagramsource["DiagramSource"]
  dashboard -- "writeFileSync" --> filesystem
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "summarise" --> dashboard
  sourcefiles -- "readFileSync" --> dashboard
  ledgerfile -- "readLedger" --> dashboard
  gitrepo -- "loadSince" --> dashboard
  ledgerfile -- "computeLedgerStates" --> dashboard
  gitconfig -- "detectRepoLinks" --> dashboard
  dashboard -- "return" --> html
  ledgerfile -- "readHypotheses" --> dashboard
  gitconfig -- "readFileSync" --> dashboard
  dashboard -- "RepoLinks" --> dashboardhtml
  diagramsource -- "checkRenderBudget" --> dashboard
  gitrepo -- "parseAtRef" --> dashboard
  diagramsource -- "measureLegibility" --> dashboard

10 nodes / 16 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #cli, CommitTrailers, ConfigFile, #diff, #gate, #mcp, #parser, #report-metadata, and 2 more

Claims written in this file— rows
src/dashboard/pages/threats.ts1 component named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  dashboard["GuardLink.Dashboard"]
  filesystem["FileSystem"]
  gitrepo["GitRepo"]
  blame["GuardLink.Blame"]
  sourcefiles["SourceFiles"]
  ledgerfile["LedgerFile"]
  gitconfig["GitConfig"]
  html["HTML"]
  dashboardhtml["DashboardHTML"]
  diagramsource["DiagramSource"]
  dashboard -- "writeFileSync" --> filesystem
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "summarise" --> dashboard
  sourcefiles -- "readFileSync" --> dashboard
  ledgerfile -- "readLedger" --> dashboard
  gitrepo -- "loadSince" --> dashboard
  ledgerfile -- "computeLedgerStates" --> dashboard
  gitconfig -- "detectRepoLinks" --> dashboard
  dashboard -- "return" --> html
  ledgerfile -- "readHypotheses" --> dashboard
  gitconfig -- "readFileSync" --> dashboard
  dashboard -- "RepoLinks" --> dashboardhtml
  diagramsource -- "checkRenderBudget" --> dashboard
  gitrepo -- "parseAtRef" --> dashboard
  diagramsource -- "measureLegibility" --> dashboard

10 nodes / 16 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #cli, CommitTrailers, ConfigFile, #diff, #gate, #mcp, #parser, #report-metadata, and 2 more

Claims written in this file— rows
src/diff/index.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Diff · Trust boundary at git command execution"]
    diff["GuardLink.Diff"]
  end
  subgraph Z1["GitRepo · Trust boundary at git command execution"]
    gitrepo["GitRepo"]
  end
  threatmodel["ThreatModel"]
  gitref["GitRef"]
  tempdir["TempDir"]
  changedfilelist["ChangedFileList"]
  diff -.-|Trust boundary at git command execution| gitrepo
  threatmodel -- "diffModels" --> diff
  gitref -- "execSync" --> diff
  diff -- "writeFileSync" --> tempdir
  diff -- "parseProject" --> threatmodel
  diff -- "return" --> changedfilelist
  gitref -- "parseAtRef" --> diff

6 nodes / 7 edges, within the 12 / 16 legibility budget · 12 neighbours just outside the frame: #agent-launcher, #blame, #cli, #dashboard, #gate, #llm-client, #mcp, #parser, and 4 more

Claims written in this file— rows
src/mcp/freshness.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  mcp["GuardLink.MCP"]
  gitrepo["GitRepo"]
  parser["GuardLink.Parser"]
  threatmodel -- "fileContext" --> mcp
  threatmodel -- "buildEnvelope" --> mcp
  gitrepo -- "readFileSync" --> mcp
  gitrepo -- "computeBlame" --> mcp
  threatmodel -- "selectSubgraph" --> mcp
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser

4 nodes / 15 edges, within the 12 / 16 legibility budget · 23 neighbours just outside the frame: #agent-launcher, Annotations, #blame, #cli, ConfigFile, #dashboard, #diff, FilePath, and 15 more

Claims written in this file— rows
src/mcp/index.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.MCP · Trust boundary at MCP protocol"]
    mcp["GuardLink.MCP"]
  end
  subgraph Z1["MCPClient · Trust boundary at MCP protocol"]
    mcpclient(["MCPClient"])
  end
  subgraph Z2["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z3["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  gitrepo["GitRepo"]
  configfile["ConfigFile"]
  querystring["QueryString"]
  llm_client(["GuardLink.LLM_Client"])
  mcp -.-|Trust boundary at MCP protocol| mcpclient
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  mcpclient -- "guardlink_context" --> mcp
  gitrepo -- "readFileSync" --> mcp
  mcpclient -- "stdio" --> mcp
  configfile -- "readFileSync" --> mcp
  querystring -- "lookup" --> mcp
  mcpclient -- "tool_call" --> mcp
  mcp -- "writeFile" --> filesystem
  mcp -- "generateThreatReport" --> llm_client
  mcp -- "resource" --> mcpclient
  gitrepo -- "computeBlame" --> mcp
  configfile -- "readAcceptancePolicy" --> parser
  mcpclient -- "applyAnnotations" --> parser
  parser -- "writeFileSync" --> filesystem

8 nodes / 15 edges, within the 12 / 16 legibility budget · 25 neighbours just outside the frame: #agent-launcher, Annotations, #blame, #cli, #dashboard, #diff, FilePath, GrammarFile, and 17 more

Claims written in this file— rows
src/parser/citation.ts1 component named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z1["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  configfile["ConfigFile"]
  mcpclient(["MCPClient"])
  projectroot["ProjectRoot"]
  sourcefiles["SourceFiles"]
  sourcefile["SourceFile"]
  ledgerfile["LedgerFile"]
  filepath["FilePath"]
  annotations["Annotations"]
  parsediagnostics["ParseDiagnostics"]
  grammarfile["GrammarFile"]
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  configfile -- "readAcceptancePolicy" --> parser
  mcpclient -- "applyAnnotations" --> parser
  parser -- "writeFileSync" --> filesystem
  projectroot -- "fast-glob" --> parser
  parser -- "writeFile" --> sourcefiles
  sourcefile -- "resolveGalPath" --> parser
  ledgerfile -- "readLedger" --> parser
  filepath -- "readFile" --> parser
  parser -- "parseString" --> annotations
  parser -- "parseString" --> parsediagnostics
  ledgerfile -- "classifyClaims" --> parser
  sourcefiles -- "attachAnchors" --> parser
  sourcefile -- "parseStructure" --> parser
  grammarfile -- "Language.load" --> parser

12 nodes / 15 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #agent-launcher, #blame, #cli, #dashboard, #gate, #init, #mcp, #suggest, and 2 more

Claims written in this file— rows
src/parser/validate.ts1 component named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z1["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  configfile["ConfigFile"]
  mcpclient(["MCPClient"])
  projectroot["ProjectRoot"]
  sourcefiles["SourceFiles"]
  sourcefile["SourceFile"]
  ledgerfile["LedgerFile"]
  filepath["FilePath"]
  annotations["Annotations"]
  parsediagnostics["ParseDiagnostics"]
  grammarfile["GrammarFile"]
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  configfile -- "readAcceptancePolicy" --> parser
  mcpclient -- "applyAnnotations" --> parser
  parser -- "writeFileSync" --> filesystem
  projectroot -- "fast-glob" --> parser
  parser -- "writeFile" --> sourcefiles
  sourcefile -- "resolveGalPath" --> parser
  ledgerfile -- "readLedger" --> parser
  filepath -- "readFile" --> parser
  parser -- "parseString" --> annotations
  parser -- "parseString" --> parsediagnostics
  ledgerfile -- "classifyClaims" --> parser
  sourcefiles -- "attachAnchors" --> parser
  sourcefile -- "parseStructure" --> parser
  grammarfile -- "Language.load" --> parser

12 nodes / 15 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #agent-launcher, #blame, #cli, #dashboard, #gate, #init, #mcp, #suggest, and 2 more

Claims written in this file— rows
src/report/mermaid.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  parser["GuardLink.Parser"]
  featurename["FeatureName"]
  report["GuardLink.Report"]
  markdown["Markdown"]
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser
  featurename -- "filtered_by_features" --> report
  threatmodel -- "generateReport" --> report
  report -- "return" --> markdown
  threatmodel -- "generateSequenceDiagram" --> report

5 nodes / 14 edges, within the 12 / 16 legibility budget · 21 neighbours just outside the frame: #agent-launcher, Annotations, #blame, #cli, ConfigFile, #dashboard, #diff, FilePath, and 13 more

Claims written in this file— rows
src/report/sequence.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  parser["GuardLink.Parser"]
  featurename["FeatureName"]
  report["GuardLink.Report"]
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser
  featurename -- "filtered_by_features" --> report
  threatmodel -- "generateReport" --> report
  threatmodel -- "generateSequenceDiagram" --> report

4 nodes / 13 edges, within the 12 / 16 legibility budget · 22 neighbours just outside the frame: #agent-launcher, Annotations, #blame, #cli, ConfigFile, #dashboard, #diff, FilePath, and 14 more

Claims written in this file— rows
src/workspace/merge.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  reportjson["ReportJSON"]
  merge_engine["Workspace.Merge"]
  mergedreport["MergedReport"]
  reportjson -- "mergeReports" --> merge_engine
  merge_engine -- "mergeReports" --> mergedreport

3 nodes / 2 edges, within the 12 / 16 legibility budget

Claims written in this file— rows
src/workspace/types.ts1 component named here
Reachesthe components this file names, and where their data goes

The assets this file names carry no @flows.

Claims written in this file— rows
.guardlink/definitions.ts14 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Agent_Launcher · Trust boundary at process spawn"]
    agent_launcher["GuardLink.Agent_Launcher"]
  end
  subgraph Z1["AgentProcess · Trust boundary at process spawn"]
    agentprocess["AgentProcess"]
  end
  agentprompt["AgentPrompt"]
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  dashboard["GuardLink.Dashboard"]
  gate["GuardLink.Gate"]
  init["GuardLink.Init"]
  agentfiles["AgentFiles"]
  mcp["GuardLink.MCP"]
  llm_client(["GuardLink.LLM_Client"])
  tui(["GuardLink.TUI"])
  agent_launcher -.-|Trust boundary at process spawn| agentprocess
  agent_launcher -- "spawn" --> agentprocess
  agentprocess -- "stdout" --> agent_launcher
  agent_launcher -- "return" --> agentprompt
  blame -- "formatBlameText" --> cli
  blame -- "buildBlamePayload" --> cli
  blame -- "summarise" --> dashboard
  cli -- "runGate" --> gate
  gate -- "buildGateFollowUp" --> agentprompt
  init -- "writeFileSync" --> agentfiles
  mcp -- "generateThreatReport" --> llm_client
  tui -- "launchAgent" --> agent_launcher
  tui -- "chatCompletion" --> llm_client

12 nodes / 13 edges, within the 12 / 16 legibility budget · 47 neighbours just outside the frame: AgentProposal, Annotations, ChangedFileList, Commands, CommitTrailers, ConfigFile, DashboardHTML, DefinitionsFile, and 39 more

Claims written in this file— rows
src/blame/attach.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  blame["GuardLink.Blame"]
  threatmodel["ThreatModel"]
  parser["GuardLink.Parser"]
  blame -- "attachBlame" --> threatmodel
  threatmodel -- "computeBlame" --> blame
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser

3 nodes / 12 edges, within the 12 / 16 legibility budget · 23 neighbours just outside the frame: #agent-launcher, Annotations, #cli, CommitTrailers, ConfigFile, #dashboard, #diff, FilePath, and 15 more

Claims written in this file— rows
src/blame/format.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  dashboard["GuardLink.Dashboard"]
  filesystem["FileSystem"]
  configfile["ConfigFile"]
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  committrailers["CommitTrailers"]
  gate["GuardLink.Gate"]
  gitconfig["GitConfig"]
  agentproposal["AgentProposal"]
  dashboard -- "writeFileSync" --> filesystem
  configfile -- "readBlameConfig" --> blame
  blame -- "formatBlameText" --> cli
  blame -- "buildBlamePayload" --> cli
  blame -- "summarise" --> dashboard
  committrailers -- "parseTrailerBlock" --> blame
  cli -- "writeFile" --> filesystem
  cli -- "runGate" --> gate
  gitconfig -- "detectRepoLinks" --> dashboard
  gitconfig -- "readFileSync" --> dashboard
  agentproposal -- "proposeEntitlement" --> cli
  gitconfig -- "execFileSync" --> cli

9 nodes / 12 edges, within the 12 / 16 legibility budget · 19 neighbours just outside the frame: #agent-launcher, AgentPrompt, DashboardHTML, DiagramSource, GitRepo, HTML, #init, LedgerFile, and 11 more

Claims written in this file— rows
src/blame/summary.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  dashboard["GuardLink.Dashboard"]
  filesystem["FileSystem"]
  configfile["ConfigFile"]
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  committrailers["CommitTrailers"]
  gate["GuardLink.Gate"]
  gitconfig["GitConfig"]
  dashboardhtml["DashboardHTML"]
  diagramsource["DiagramSource"]
  agentproposal["AgentProposal"]
  dashboard -- "writeFileSync" --> filesystem
  configfile -- "readBlameConfig" --> blame
  blame -- "formatBlameText" --> cli
  blame -- "buildBlamePayload" --> cli
  blame -- "summarise" --> dashboard
  committrailers -- "parseTrailerBlock" --> blame
  cli -- "writeFile" --> filesystem
  cli -- "runGate" --> gate
  gitconfig -- "detectRepoLinks" --> dashboard
  gitconfig -- "readFileSync" --> dashboard
  dashboard -- "RepoLinks" --> dashboardhtml
  diagramsource -- "checkRenderBudget" --> dashboard
  diagramsource -- "measureLegibility" --> dashboard
  agentproposal -- "proposeEntitlement" --> cli
  gitconfig -- "execFileSync" --> cli

11 nodes / 15 edges, within the 12 / 16 legibility budget · 17 neighbours just outside the frame: #agent-launcher, AgentPrompt, GitRepo, HTML, #init, LedgerFile, #mcp, PackageJson, and 9 more

Claims written in this file— rows
src/ci/index.ts5 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  cli["GuardLink.CLI"]
  parsediagnostics["ParseDiagnostics"]
  sourcefiles["SourceFiles"]
  ledgerfile["LedgerFile"]
  threatmodel -- "runCiChecks" --> cli
  parsediagnostics -- "runCiChecks" --> cli
  sourcefiles -- "findAnchorDrift" --> cli
  ledgerfile -- "readLedger" --> cli
  ledgerfile -- "readHypotheses" --> cli
  threatmodel -- "findUnmitigatedPaths" --> cli
  threatmodel -- "classifyHypotheses" --> cli
  cli -- "attachHypotheses" --> threatmodel
  cli -- "writeConfirmedLine" --> sourcefiles
  cli -- "writeHypotheses" --> ledgerfile
  cli -- "writeLedger" --> ledgerfile
  cli -- "applyVerification" --> ledgerfile
  cli -- "applyProposalDecision" --> sourcefiles
  threatmodel -- "getReviewableExposures" --> cli
  cli -- "writeFile" --> sourcefiles

5 nodes / 15 edges, within the 12 / 16 legibility budget · 20 neighbours just outside the frame: #agent-launcher, AgentProposal, #blame, #dashboard, #diff, FileSystem, #gate, GitConfig, and 12 more

Claims written in this file— rows
src/dashboard/data.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  dashboard["GuardLink.Dashboard"]
  ledgerfile["LedgerFile"]
  dashboardhtml["DashboardHTML"]
  diagramsource["DiagramSource"]
  parser["GuardLink.Parser"]
  blame -- "formatBlameText" --> cli
  blame -- "buildBlamePayload" --> cli
  blame -- "summarise" --> dashboard
  ledgerfile -- "readLedger" --> cli
  ledgerfile -- "readHypotheses" --> cli
  ledgerfile -- "readLedger" --> dashboard
  ledgerfile -- "computeLedgerStates" --> dashboard
  ledgerfile -- "readHypotheses" --> dashboard
  dashboard -- "RepoLinks" --> dashboardhtml
  diagramsource -- "checkRenderBudget" --> dashboard
  diagramsource -- "measureLegibility" --> dashboard
  cli -- "writeHypotheses" --> ledgerfile
  ledgerfile -- "readLedger" --> parser
  cli -- "writeLedger" --> ledgerfile
  ledgerfile -- "classifyClaims" --> parser
  cli -- "applyVerification" --> ledgerfile

7 nodes / 16 edges, within the 12 / 16 legibility budget · 22 neighbours just outside the frame: AgentProposal, Annotations, CommitTrailers, ConfigFile, FilePath, FileSystem, #gate, GitConfig, and 14 more

Claims written in this file— rows
src/dashboard/pages/context.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  dashboard["GuardLink.Dashboard"]
  threatmodel -- "emitArtifacts" --> dashboard
  threatmodel -- "generateThreatGraph" --> dashboard
  threatmodel -- "buildExploreData" --> dashboard
  threatmodel -- "computeStats" --> dashboard
  threatmodel -- "generateDashboardHTML" --> dashboard
  threatmodel -- "buildClaims" --> dashboard
  threatmodel -- "growWithinBudget" --> dashboard

2 nodes / 7 edges, within the 12 / 16 legibility budget · 19 neighbours just outside the frame: #agent-launcher, #blame, #cli, DashboardHTML, DiagramSource, #diff, FileSystem, #gate, and 11 more

Claims written in this file— rows
src/diff/engine.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  diff["GuardLink.Diff"]
  parser["GuardLink.Parser"]
  threatmodel -- "diffModels" --> diff
  diff -- "parseProject" --> threatmodel
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser

3 nodes / 12 edges, within the 12 / 16 legibility budget · 25 neighbours just outside the frame: #agent-launcher, Annotations, #blame, ChangedFileList, #cli, ConfigFile, #dashboard, FilePath, and 17 more

Claims written in this file— rows
src/gate/lint.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  gate["GuardLink.Gate"]
  parser["GuardLink.Parser"]
  threatmodel -- "runGate" --> gate
  threatmodel -- "lintAnnotations" --> gate
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser

3 nodes / 12 edges, within the 12 / 16 legibility budget · 22 neighbours just outside the frame: #agent-launcher, AgentPrompt, Annotations, #blame, #cli, ConfigFile, #dashboard, #diff, and 14 more

Claims written in this file— rows
src/graph/views.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  dashboard["GuardLink.Dashboard"]
  threatmodel -- "emitArtifacts" --> dashboard
  threatmodel -- "generateThreatGraph" --> dashboard
  threatmodel -- "buildExploreData" --> dashboard
  threatmodel -- "computeStats" --> dashboard
  threatmodel -- "generateDashboardHTML" --> dashboard
  threatmodel -- "buildClaims" --> dashboard
  threatmodel -- "growWithinBudget" --> dashboard

2 nodes / 7 edges, within the 12 / 16 legibility budget · 19 neighbours just outside the frame: #agent-launcher, #blame, #cli, DashboardHTML, DiagramSource, #diff, FileSystem, #gate, and 11 more

Claims written in this file— rows
src/hypothesis/classify.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  cli["GuardLink.CLI"]
  parser["GuardLink.Parser"]
  threatmodel -- "runCiChecks" --> cli
  threatmodel -- "findUnmitigatedPaths" --> cli
  threatmodel -- "classifyHypotheses" --> cli
  cli -- "attachHypotheses" --> threatmodel
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser
  threatmodel -- "getReviewableExposures" --> cli

3 nodes / 15 edges, within the 12 / 16 legibility budget · 29 neighbours just outside the frame: #agent-launcher, AgentProposal, Annotations, #blame, ConfigFile, #dashboard, #diff, FilePath, and 21 more

Claims written in this file— rows
src/mcp/instructions.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z1["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  configfile["ConfigFile"]
  agent_launcher["GuardLink.Agent_Launcher"]
  blame["GuardLink.Blame"]
  init["GuardLink.Init"]
  mcp["GuardLink.MCP"]
  llm_client(["GuardLink.LLM_Client"])
  tui(["GuardLink.TUI"])
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  configfile -- "readFileSync" --> agent_launcher
  agent_launcher -- "writeFileSync" --> configfile
  configfile -- "readBlameConfig" --> blame
  init -- "writeFileSync" --> configfile
  configfile -- "configIsCustomised" --> init
  configfile -- "readFileSync" --> mcp
  mcp -- "writeFile" --> filesystem
  mcp -- "generateThreatReport" --> llm_client
  configfile -- "readAcceptancePolicy" --> parser
  parser -- "writeFileSync" --> filesystem
  tui -- "writeFile" --> filesystem
  tui -- "launchAgent" --> agent_launcher
  tui -- "chatCompletion" --> llm_client
  configfile -- "loadProjectConfig" --> tui
  tui -- "saveProjectConfig" --> configfile

9 nodes / 16 edges, within the 12 / 16 legibility budget · 34 neighbours just outside the frame: AgentFiles, AgentProcess, AgentPrompt, Annotations, #cli, Commands, CommitTrailers, #dashboard, and 26 more

Claims written in this file— rows
src/parser/annotation-hash.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  cli["GuardLink.CLI"]
  parser["GuardLink.Parser"]
  threatmodel -- "runCiChecks" --> cli
  threatmodel -- "findUnmitigatedPaths" --> cli
  threatmodel -- "classifyHypotheses" --> cli
  cli -- "attachHypotheses" --> threatmodel
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser
  threatmodel -- "getReviewableExposures" --> cli

3 nodes / 15 edges, within the 12 / 16 legibility budget · 29 neighbours just outside the frame: #agent-launcher, AgentProposal, Annotations, #blame, ConfigFile, #dashboard, #diff, FilePath, and 21 more

Claims written in this file— rows
src/parser/annotation-mode.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  cli["GuardLink.CLI"]
  parser["GuardLink.Parser"]
  threatmodel -- "runCiChecks" --> cli
  threatmodel -- "findUnmitigatedPaths" --> cli
  threatmodel -- "classifyHypotheses" --> cli
  cli -- "attachHypotheses" --> threatmodel
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser
  threatmodel -- "getReviewableExposures" --> cli

3 nodes / 15 edges, within the 12 / 16 legibility budget · 29 neighbours just outside the frame: #agent-launcher, AgentProposal, Annotations, #blame, ConfigFile, #dashboard, #diff, FilePath, and 21 more

Claims written in this file— rows
src/parser/canonical-order.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  cli["GuardLink.CLI"]
  parser["GuardLink.Parser"]
  threatmodel -- "runCiChecks" --> cli
  threatmodel -- "findUnmitigatedPaths" --> cli
  threatmodel -- "classifyHypotheses" --> cli
  cli -- "attachHypotheses" --> threatmodel
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser
  threatmodel -- "getReviewableExposures" --> cli

3 nodes / 15 edges, within the 12 / 16 legibility budget · 29 neighbours just outside the frame: #agent-launcher, AgentProposal, Annotations, #blame, ConfigFile, #dashboard, #diff, FilePath, and 21 more

Claims written in this file— rows
src/parser/coverage.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  cli["GuardLink.CLI"]
  parser["GuardLink.Parser"]
  threatmodel -- "runCiChecks" --> cli
  threatmodel -- "findUnmitigatedPaths" --> cli
  threatmodel -- "classifyHypotheses" --> cli
  cli -- "attachHypotheses" --> threatmodel
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser
  threatmodel -- "getReviewableExposures" --> cli

3 nodes / 15 edges, within the 12 / 16 legibility budget · 29 neighbours just outside the frame: #agent-launcher, AgentProposal, Annotations, #blame, ConfigFile, #dashboard, #diff, FilePath, and 21 more

Claims written in this file— rows
src/parser/gal-path.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z1["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  configfile["ConfigFile"]
  mcpclient(["MCPClient"])
  projectroot["ProjectRoot"]
  sourcefiles["SourceFiles"]
  sourcefile["SourceFile"]
  ledgerfile["LedgerFile"]
  filepath["FilePath"]
  annotations["Annotations"]
  parsediagnostics["ParseDiagnostics"]
  grammarfile["GrammarFile"]
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  configfile -- "readAcceptancePolicy" --> parser
  mcpclient -- "applyAnnotations" --> parser
  parser -- "writeFileSync" --> filesystem
  projectroot -- "fast-glob" --> parser
  parser -- "writeFile" --> sourcefiles
  sourcefile -- "resolveGalPath" --> parser
  ledgerfile -- "readLedger" --> parser
  filepath -- "readFile" --> parser
  parser -- "parseString" --> annotations
  parser -- "parseString" --> parsediagnostics
  ledgerfile -- "classifyClaims" --> parser
  sourcefiles -- "attachAnchors" --> parser
  sourcefile -- "parseStructure" --> parser
  grammarfile -- "Language.load" --> parser

12 nodes / 15 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #agent-launcher, #blame, #cli, #dashboard, #gate, #init, #mcp, #suggest, and 2 more

Claims written in this file— rows
src/parser/verification.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  parser["GuardLink.Parser"]
  ledgerfile["LedgerFile"]
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  ledgerfile -- "readLedger" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  ledgerfile -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser

3 nodes / 12 edges, within the 12 / 16 legibility budget · 21 neighbours just outside the frame: #agent-launcher, Annotations, #blame, #cli, ConfigFile, #dashboard, #diff, FilePath, and 13 more

Claims written in this file— rows
src/paths/index.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  cli["GuardLink.CLI"]
  parser["GuardLink.Parser"]
  threatmodel -- "runCiChecks" --> cli
  threatmodel -- "findUnmitigatedPaths" --> cli
  threatmodel -- "classifyHypotheses" --> cli
  cli -- "attachHypotheses" --> threatmodel
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser
  threatmodel -- "getReviewableExposures" --> cli

3 nodes / 15 edges, within the 12 / 16 legibility budget · 29 neighbours just outside the frame: #agent-launcher, AgentProposal, Annotations, #blame, ConfigFile, #dashboard, #diff, FilePath, and 21 more

Claims written in this file— rows
src/report/report.ts4 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  parser["GuardLink.Parser"]
  featurename["FeatureName"]
  report["GuardLink.Report"]
  markdown["Markdown"]
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser
  featurename -- "filtered_by_features" --> report
  threatmodel -- "generateReport" --> report
  report -- "return" --> markdown
  threatmodel -- "generateSequenceDiagram" --> report

5 nodes / 14 edges, within the 12 / 16 legibility budget · 21 neighbours just outside the frame: #agent-launcher, Annotations, #blame, #cli, ConfigFile, #dashboard, #diff, FilePath, and 13 more

Claims written in this file— rows
src/structure/index.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser"]
  end
  subgraph Z1["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  configfile["ConfigFile"]
  mcpclient(["MCPClient"])
  projectroot["ProjectRoot"]
  sourcefiles["SourceFiles"]
  sourcefile["SourceFile"]
  ledgerfile["LedgerFile"]
  filepath["FilePath"]
  annotations["Annotations"]
  parsediagnostics["ParseDiagnostics"]
  grammarfile["GrammarFile"]
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  configfile -- "readAcceptancePolicy" --> parser
  mcpclient -- "applyAnnotations" --> parser
  parser -- "writeFileSync" --> filesystem
  projectroot -- "fast-glob" --> parser
  parser -- "writeFile" --> sourcefiles
  sourcefile -- "resolveGalPath" --> parser
  ledgerfile -- "readLedger" --> parser
  filepath -- "readFile" --> parser
  parser -- "parseString" --> annotations
  parser -- "parseString" --> parsediagnostics
  ledgerfile -- "classifyClaims" --> parser
  sourcefiles -- "attachAnchors" --> parser
  sourcefile -- "parseStructure" --> parser
  grammarfile -- "Language.load" --> parser

12 nodes / 15 edges, within the 12 / 16 legibility budget · 10 neighbours just outside the frame: #agent-launcher, #blame, #cli, #dashboard, #gate, #init, #mcp, #suggest, and 2 more

Claims written in this file— rows
src/version.ts2 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  gitrepo["GitRepo"]
  filesystem["FileSystem"]
  gate["GuardLink.Gate"]
  agentproposal["AgentProposal"]
  gitconfig["GitConfig"]
  packagejson["PackageJson"]
  blame -- "formatBlameText" --> cli
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "buildBlamePayload" --> cli
  cli -- "writeFile" --> filesystem
  gitrepo -- "attachBlame" --> cli
  gitrepo -- "computeBlame" --> cli
  cli -- "runGate" --> gate
  gitrepo -- "loadSince" --> cli
  agentproposal -- "proposeEntitlement" --> cli
  gitconfig -- "execFileSync" --> cli
  packagejson -- "readFileSync" --> cli

8 nodes / 12 edges, within the 12 / 16 legibility budget · 17 neighbours just outside the frame: AgentPrompt, CommitTrailers, ConfigFile, #dashboard, #diff, LedgerFile, #mcp, ParseDiagnostics, and 9 more

Claims written in this file— rows
src/workspace/link.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 47, "rankSpacing": 67, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  blame["GuardLink.Blame"]
  cli["GuardLink.CLI"]
  userargs(["UserArgs"])
  gate["GuardLink.Gate"]
  init["GuardLink.Init"]
  agentfiles["AgentFiles"]
  agentproposal["AgentProposal"]
  tui(["GuardLink.TUI"])
  agent_launcher["GuardLink.Agent_Launcher"]
  llm_client(["GuardLink.LLM_Client"])
  commands["Commands"]
  workspace_link["Workspace.Link"]
  blame -- "formatBlameText" --> cli
  blame -- "buildBlamePayload" --> cli
  userargs -- "process.argv" --> cli
  cli -- "runGate" --> gate
  init -- "writeFileSync" --> agentfiles
  agentproposal -- "proposeEntitlement" --> cli
  userargs -- "args" --> tui
  tui -- "launchAgent" --> agent_launcher
  tui -- "chatCompletion" --> llm_client
  tui -- "dispatch" --> commands
  userargs -- "linkProject" --> workspace_link
  workspace_link -- "updateAgentWorkspaceContext" --> agentfiles

12 nodes / 12 edges, within the 12 / 16 legibility budget · 31 neighbours just outside the frame: AgentProcess, AgentPrompt, CommitTrailers, ConfigFile, #dashboard, DefinitionsFile, EnvVars, FileSystem, and 23 more

Claims written in this file— rows
src/workspace/metadata.ts3 components named here
Reachesthe components this file names, and where their data goes
%%{init: {"flowchart": {"nodeSpacing": 44, "rankSpacing": 58, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  threatmodel["ThreatModel"]
  parser["GuardLink.Parser"]
  gitrepo["GitRepo"]
  report_metadata["Workspace.Metadata"]
  threatmodel -- "findAcceptanceDefects" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  threatmodel -- "canonicalizeModelOrder" --> parser
  threatmodel -- "isCovered" --> parser
  threatmodel -- "migrateAnnotationMode" --> parser
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  threatmodel -- "findUnmitigatedPaths" --> parser
  gitrepo -- "execSync" --> report_metadata
  report_metadata -- "populateMetadata" --> threatmodel

4 nodes / 12 edges, within the 12 / 16 legibility budget · 22 neighbours just outside the frame: #agent-launcher, Annotations, #blame, #cli, ConfigFile, #dashboard, #diff, FilePath, and 14 more

Claims written in this file— rows
Open, worst first14 claims

Every open claim is here. A ranking is a list — there is no graph question in "what should I fix next".

Diagrams

These are the whole model in one picture. That is the right thing on a small repository and stops being readable at about a dozen components — each panel below says its own size, and Explore is where a question gets an answer scaled to be read. Scroll to zoom, drag to pan, double-click or press Fit to reset. Find dims everything that does not match; on the threat graph, pick an asset to see only it and its neighbours. Source copies the Mermaid text.

Threat Graph
These are whole-model diagrams, past the size anyone can read.
  • Threat Graph (high/critical) — 29 nodes / 70 edges, past the 12 / 16 legibility budget (29 nodes against 12, 70 edges against 16)
  • Threat Graph (all severities) — 43 nodes / 150 edges, past the 12 / 16 legibility budget (43 nodes against 12, 150 edges against 16)

The budget is 12 nodes and 16 edges, measured against this panel at its label size — past it the drawing grows taller than the panel and "Fit" shrinks the labels below reading size rather than fitting. It still draws, and zoom and pan still work, so it is kept: on a small model it is the right picture. For a model this size, Explore answers one question at a time and every answer is sized to be read.

%%{init: {"flowchart": {"nodeSpacing": 55, "rankSpacing": 150, "curve": "monotoneX", "htmlLabels": false, "padding": 24}}}%%
graph LR
  subgraph TZ0["Trust boundary at process spawn"]
    agent_launcher["GuardLink.Agent_Launcher [SECRETS, INTERNAL]"]
  end
  subgraph TZ1["Trust boundary at external API call"]
    llm_client["GuardLink.LLM_Client [INTERNAL, SECRETS]"]
  end
  subgraph TZ3["Trust boundary at CLI argument parsing"]
    cli["GuardLink.CLI [SECRETS, PII, INTERNAL]"]
  end
  subgraph TZ4["Trust boundary at git command execution"]
    diff["GuardLink.Diff"]
  end
  subgraph TZ5["Trust boundary at MCP protocol"]
    mcp["GuardLink.MCP [INTERNAL, PII]"]
  end
  subgraph TZ7["Trust boundary between parser and disk …"]
    parser["GuardLink.Parser [INTERNAL]"]
  end
  subgraph TZ8["Trust boundary at interactive input"]
    tui["GuardLink.TUI [SECRETS, PII]"]
  end
  dashboard["GuardLink.Dashboard [PII, INTERNAL]"]
  init["GuardLink.Init [INTERNAL]"]
  suggest["GuardLink.Suggest"]
  blame["GuardLink.Blame [PII]"]
  gate["GuardLink.Gate"]
  report["GuardLink.Report [PII]"]
  api_key_exposure{{"API_Key_Exposure (cwe:CWE-798)"}}:::sev_high
  child_proc_injection{{"Child_Process_Injection (cwe:CWE-78)"}}:::sev_crit
  prompt_injection{{"Prompt_Injection (cwe:CWE-77)"}}:::sev_high
  path_traversal{{"Path_Traversal (cwe:CWE-22)"}}:::sev_high
  arbitrary_write{{"Arbitrary_File_Write (cwe:CWE-73, cwe:CWE-74)"}}:::sev_high
  cmd_injection{{"Command_Injection (cwe:CWE-78)"}}:::sev_crit
  xss{{"Cross_Site_Scripting (cwe:CWE-79)"}}:::sev_high
  key_redaction(["Key_Redaction"]):::control
  path_validation(["Path_Validation"]):::control
  param_commands(["Parameterized_Commands"]):::control
  glob_filtering(["Glob_Pattern_Filtering"]):::control
  output_encoding(["Output_Encoding"]):::control
  input_sanitize(["Input_Sanitization"]):::control
  config_validation(["Config_Validation"]):::control
  resource_limits(["Resource_Limits"]):::control
  regex_anchoring(["Regex_Anchoring"]):::control
  agent_launcher -. exposes .-> api_key_exposure
  agent_launcher -. exposes .-> child_proc_injection
  agent_launcher -. exposes .-> prompt_injection
  llm_client -. exposes .-> api_key_exposure
  cli -. exposes .-> path_traversal
  cli -. exposes .-> arbitrary_write
  cli -. exposes .-> api_key_exposure
  cli -. exposes .-> cmd_injection
  dashboard -. exposes .-> xss
  diff -. exposes .-> cmd_injection
  init -. exposes .-> arbitrary_write
  mcp -. exposes .-> cmd_injection
  mcp -. exposes .-> path_traversal
  mcp -. exposes .-> arbitrary_write
  suggest -. exposes .-> path_traversal
  parser -. exposes .-> arbitrary_write
  parser -. exposes .-> path_traversal
  tui -. exposes .-> path_traversal
  tui -. exposes .-> arbitrary_write
  tui -. exposes .-> cmd_injection
  tui -. exposes .-> api_key_exposure
  key_redaction -- mitigates --> api_key_exposure
  key_redaction -. protects .-> agent_launcher
  path_validation -- mitigates --> path_traversal
  path_validation -. protects .-> agent_launcher
  path_validation -- mitigates --> arbitrary_write
  param_commands -- mitigates --> child_proc_injection
  param_commands -. protects .-> agent_launcher
  param_commands -- mitigates --> cmd_injection
  path_validation -. protects .-> llm_client
  key_redaction -. protects .-> llm_client
  glob_filtering -- mitigates --> path_traversal
  glob_filtering -. protects .-> llm_client
  path_validation -. protects .-> dashboard
  path_validation -. protects .-> blame
  param_commands -. protects .-> blame
  path_validation -. protects .-> cli
  key_redaction -. protects .-> cli
  output_encoding -- mitigates --> xss
  output_encoding -. protects .-> dashboard
  param_commands -. protects .-> dashboard
  input_sanitize -- mitigates --> cmd_injection
  input_sanitize -. protects .-> diff
  path_validation -. protects .-> diff
  glob_filtering -. protects .-> diff
  path_validation -. protects .-> gate
  path_validation -. protects .-> init
  config_validation -- mitigates --> arbitrary_write
  config_validation -. protects .-> init
  path_validation -. protects .-> mcp
  key_redaction -. protects .-> mcp
  path_validation -. protects .-> suggest
  path_validation -. protects .-> parser
  glob_filtering -. protects .-> parser
  param_commands -. protects .-> cli
  output_encoding -. protects .-> report
  input_sanitize -- mitigates --> arbitrary_write
  input_sanitize -. protects .-> cli
  path_validation -. protects .-> tui
  key_redaction -. protects .-> tui
  resource_limits -. validates .-> mcp
  regex_anchoring -. validates .-> parser
  input_sanitize -. validates .-> parser
  blame -- "formatBlameText" --> cli
  blame -- "buildBlamePayload" --> cli
  blame -- "summarise" --> dashboard
  cli -- "runGate" --> gate
  mcp -- "generateThreatReport" --> llm_client
  tui -- "launchAgent" --> agent_launcher
  tui -- "chatCompletion" --> llm_client
  classDef threat fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.3px
  classDef control fill:#102a24,stroke:#33d49d,color:#f0f0f0,stroke-width:1.3px
  classDef sev_crit fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.4px
  classDef sev_high fill:#402019,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.2px
  classDef sev_med fill:#1f3943,stroke:#55899e,color:#f0f0f0
  classDef sev_low fill:#10263b,stroke:#0360a2,color:#f0f0f0
  classDef sev_unset fill:#223942,stroke:#3b6779,color:#f0f0f0
Assets, threats, controls, and mitigations. Filtered to high/critical by default — click All severities to expand. asset threat control critical / high medium lowdashed box: trust zone
Data Flow
This is a whole-model diagram, past the size anyone can read.
  • Data Flow — 68 nodes / 164 edges, past the 12 / 16 legibility budget (68 nodes against 12, 164 edges against 16)

The budget is 12 nodes and 16 edges, measured against this panel at its label size — past it the drawing grows taller than the panel and "Fit" shrinks the labels below reading size rather than fitting. It still draws, and zoom and pan still work, so it is kept: on a small model it is the right picture. For a model this size, Explore answers one question at a time and every answer is sized to be read.

%%{init: {"flowchart": {"nodeSpacing": 47, "rankSpacing": 67, "curve": "basis", "htmlLabels": false}}}%%
graph LR
  subgraph Z0["GuardLink.Agent_Launcher · Trust boundary at process spawn"]
    agent_launcher["GuardLink.Agent_Launcher · secrets, internal"]
  end
  subgraph Z1["AgentProcess · Trust boundary at process spawn"]
    agentprocess["AgentProcess"]
  end
  subgraph Z2["GuardLink.LLM_Client · Trust boundary at external API call"]
    llm_client(["GuardLink.LLM_Client · internal, secrets"])
  end
  subgraph Z3["LLMProvider · Trust boundary at external API call"]
    llmprovider["LLMProvider"]
  end
  subgraph Z4["NVD · Trust boundary at external API"]
    nvd["NVD"]
  end
  subgraph Z5["GuardLink.CLI · Trust boundary at CLI argument parsing"]
    cli["GuardLink.CLI · secrets, pii, internal"]
  end
  subgraph Z6["UserInput · Trust boundary at CLI argument parsing"]
    userinput(["UserInput"])
  end
  subgraph Z7["GuardLink.Diff · Trust boundary at git command execution"]
    diff["GuardLink.Diff"]
  end
  subgraph Z8["GitRepo · Trust boundary at git command execution"]
    gitrepo["GitRepo"]
  end
  subgraph Z9["GuardLink.MCP · Trust boundary at MCP protocol"]
    mcp["GuardLink.MCP · internal, pii"]
  end
  subgraph Z10["MCPClient · Trust boundary at MCP protocol"]
    mcpclient(["MCPClient"])
  end
  subgraph Z11["GuardLink.Parser · Trust boundary between parser and disk I/O"]
    parser["GuardLink.Parser · internal"]
  end
  subgraph Z12["FileSystem · Trust boundary between parser and disk I/O"]
    filesystem["FileSystem"]
  end
  subgraph Z13["GuardLink.TUI · Trust boundary at interactive input"]
    tui(["GuardLink.TUI · secrets, pii"])
  end
  envvars["EnvVars"]
  configfile["ConfigFile"]
  userprompt(["UserPrompt"])
  threatmodel["ThreatModel"]
  agentprompt["AgentPrompt"]
  savedreport["SavedReport"]
  projectfiles["ProjectFiles"]
  reportfile["ReportFile"]
  pentestfindings["PentestFindings"]
  llmconfig["LLMConfig"]
  llmtoolcall["LLMToolCall"]
  sarif["GuardLink.SARIF"]
  sariflog["SarifLog"]
  dashboard["GuardLink.Dashboard · pii, internal"]
  blame["GuardLink.Blame · pii"]
  committrailers["CommitTrailers"]
  parsediagnostics["ParseDiagnostics"]
  sourcefiles["SourceFiles"]
  ledgerfile["LedgerFile"]
  userargs(["UserArgs"])
  gate["GuardLink.Gate"]
  scanreport["ScanReport"]
  gitconfig["GitConfig"]
  html["HTML"]
  dashboardhtml["DashboardHTML"]
  diagramsource["DiagramSource"]
  gitref["GitRef"]
  tempdir["TempDir"]
  changedfilelist["ChangedFileList"]
  projectroot["ProjectRoot"]
  init["GuardLink.Init · internal"]
  agentfiles["AgentFiles"]
  definitionsfile["DefinitionsFile"]
  querystring["QueryString"]
  filepath["FilePath"]
  suggest["GuardLink.Suggest"]
  suggestions["Suggestions"]
  sourcefile["SourceFile"]
  annotations["Annotations"]
  featurename["FeatureName"]
  report["GuardLink.Report · pii"]
  markdown["Markdown"]
  agentproposal["AgentProposal"]
  proposalledger["ProposalLedger"]
  grammarfile["GrammarFile"]
  commands["Commands"]
  rawstdin["RawStdin"]
  terminal["Terminal"]
  packagejson["PackageJson"]
  workspace_link["Workspace.Link"]
  reportjson["ReportJSON"]
  merge_engine["Workspace.Merge"]
  mergedreport["MergedReport"]
  report_metadata["Workspace.Metadata"]
  agent_launcher -.-|Trust boundary at process spawn| agentprocess
  llm_client -.-|Trust boundary at external API call| llmprovider
  llm_client -.-|Trust boundary at external API| nvd
  cli -.-|Trust boundary at CLI argument parsing| userinput
  diff -.-|Trust boundary at git command execution| gitrepo
  mcp -.-|Trust boundary at MCP protocol| mcpclient
  parser -.-|Trust boundary between parser and disk I/O| filesystem
  tui -.-|Trust boundary at interactive input| userinput
  envvars -- "process.env" --> agent_launcher
  configfile -- "readFileSync" --> agent_launcher
  agent_launcher -- "writeFileSync" --> configfile
  userprompt -- "launchAgent" --> agent_launcher
  agent_launcher -- "spawn" --> agentprocess
  agentprocess -- "stdout" --> agent_launcher
  userprompt -- "buildAnnotatePrompt" --> agent_launcher
  userprompt -- "buildTranslatePrompt" --> agent_launcher
  userprompt -- "buildAskPrompt" --> agent_launcher
  threatmodel -- "model" --> agent_launcher
  agent_launcher -- "return" --> agentprompt
  savedreport -- "parseFindingsBlock" --> llm_client
  threatmodel -- "serializeModel" --> llm_client
  projectfiles -- "readFileSync" --> llm_client
  llm_client -- "writeFileSync" --> reportfile
  pentestfindings -- "readFileSync" --> llm_client
  llmconfig -- "chatCompletion" --> llm_client
  llm_client -- "fetch" --> llmprovider
  llmprovider -- "response" --> llm_client
  llmtoolcall -- "createToolExecutor" --> llm_client
  llm_client -- "fetch" --> nvd
  threatmodel -- "generateSarif" --> sarif
  sarif -- "return" --> sariflog
  threatmodel -- "emitArtifacts" --> dashboard
  dashboard -- "writeFileSync" --> filesystem
  blame -- "attachBlame" --> threatmodel
  threatmodel -- "computeBlame" --> blame
  configfile -- "readBlameConfig" --> blame
  blame -- "formatBlameText" --> cli
  gitrepo -- "execFileSync" --> blame
  gitrepo -- "listCommits" --> blame
  blame -- "buildBlamePayload" --> cli
  blame -- "summarise" --> dashboard
  committrailers -- "parseTrailerBlock" --> blame
  threatmodel -- "runCiChecks" --> cli
  parsediagnostics -- "runCiChecks" --> cli
  sourcefiles -- "findAnchorDrift" --> cli
  ledgerfile -- "readLedger" --> cli
  userargs -- "process.argv" --> cli
  cli -- "writeFile" --> filesystem
  gitrepo -- "attachBlame" --> cli
  ledgerfile -- "readHypotheses" --> cli
  gitrepo -- "computeBlame" --> cli
  userinput -- "verify" --> cli
  cli -- "runGate" --> gate
  threatmodel -- "findUnmitigatedPaths" --> cli
  scanreport -- "importScan" --> cli
  gitrepo -- "loadSince" --> cli
  sourcefiles -- "readFileSync" --> dashboard
  ledgerfile -- "readLedger" --> dashboard
  threatmodel -- "generateThreatGraph" --> dashboard
  threatmodel -- "buildExploreData" --> dashboard
  gitrepo -- "loadSince" --> dashboard
  threatmodel -- "computeStats" --> dashboard
  ledgerfile -- "computeLedgerStates" --> dashboard
  gitconfig -- "detectRepoLinks" --> dashboard
  dashboard -- "return" --> html
  ledgerfile -- "readHypotheses" --> dashboard
  threatmodel -- "generateDashboardHTML" --> dashboard
  gitconfig -- "readFileSync" --> dashboard
  dashboard -- "RepoLinks" --> dashboardhtml
  threatmodel -- "buildClaims" --> dashboard
  diagramsource -- "checkRenderBudget" --> dashboard
  gitrepo -- "parseAtRef" --> dashboard
  threatmodel -- "diffModels" --> diff
  gitref -- "execSync" --> diff
  diff -- "writeFileSync" --> tempdir
  diff -- "parseProject" --> threatmodel
  diff -- "return" --> changedfilelist
  gitref -- "parseAtRef" --> diff
  threatmodel -- "runGate" --> gate
  gate -- "buildGateFollowUp" --> agentprompt
  gate -- "stripViolations" --> sourcefiles
  threatmodel -- "lintAnnotations" --> gate
  diagramsource -- "measureLegibility" --> dashboard
  threatmodel -- "growWithinBudget" --> dashboard
  threatmodel -- "classifyHypotheses" --> cli
  cli -- "attachHypotheses" --> threatmodel
  cli -- "writeConfirmedLine" --> sourcefiles
  cli -- "writeHypotheses" --> ledgerfile
  projectroot -- "detectProject" --> init
  projectroot -- "options.root" --> init
  init -- "writeFileSync" --> agentfiles
  init -- "writeFileSync" --> configfile
  definitionsfile -- "definitionsArePopulated" --> init
  configfile -- "configIsCustomised" --> init
  mcpclient -- "guardlink_context" --> mcp
  threatmodel -- "fileContext" --> mcp
  threatmodel -- "buildEnvelope" --> mcp
  gitrepo -- "readFileSync" --> mcp
  mcpclient -- "stdio" --> mcp
  configfile -- "readFileSync" --> mcp
  querystring -- "lookup" --> mcp
  mcpclient -- "tool_call" --> mcp
  mcp -- "writeFile" --> filesystem
  mcp -- "generateThreatReport" --> llm_client
  mcp -- "resource" --> mcpclient
  gitrepo -- "computeBlame" --> mcp
  threatmodel -- "selectSubgraph" --> mcp
  filepath -- "readFileSync" --> suggest
  suggest -- "suggestAnnotations" --> suggestions
  threatmodel -- "findAcceptanceDefects" --> parser
  configfile -- "readAcceptancePolicy" --> parser
  threatmodel -- "computeAnnotationHash" --> parser
  threatmodel -- "detectAnnotationMode" --> parser
  mcpclient -- "applyAnnotations" --> parser
  parser -- "writeFileSync" --> filesystem
  threatmodel -- "canonicalizeModelOrder" --> parser
  projectroot -- "fast-glob" --> parser
  parser -- "writeFile" --> sourcefiles
  threatmodel -- "isCovered" --> parser
  sourcefile -- "resolveGalPath" --> parser
  ledgerfile -- "readLedger" --> parser
  cli -- "writeLedger" --> ledgerfile
  threatmodel -- "migrateAnnotationMode" --> parser
  filepath -- "readFile" --> parser
  parser -- "parseString" --> annotations
  parser -- "parseString" --> parsediagnostics
  parser -- "assembleModel" --> threatmodel
  threatmodel -- "findAnchorDrift" --> parser
  threatmodel -- "classifyClaims" --> parser
  ledgerfile -- "classifyClaims" --> parser
  cli -- "applyVerification" --> ledgerfile
  threatmodel -- "findUnmitigatedPaths" --> parser
  featurename -- "filtered_by_features" --> report
  threatmodel -- "generateReport" --> report
  report -- "return" --> markdown
  threatmodel -- "generateSequenceDiagram" --> report
  agentproposal -- "proposeEntitlement" --> cli
  cli -- "writeFile" --> proposalledger
  proposalledger -- "readFile" --> cli
  cli -- "applyProposalDecision" --> sourcefiles
  gitconfig -- "execFileSync" --> cli
  threatmodel -- "getReviewableExposures" --> cli
  cli -- "writeFile" --> sourcefiles
  sourcefiles -- "attachAnchors" --> parser
  sourcefile -- "parseStructure" --> parser
  grammarfile -- "Language.load" --> parser
  userargs -- "args" --> tui
  tui -- "writeFile" --> filesystem
  tui -- "launchAgent" --> agent_launcher
  tui -- "chatCompletion" --> llm_client
  gitrepo -- "computeBlame" --> tui
  configfile -- "loadProjectConfig" --> tui
  tui -- "saveProjectConfig" --> configfile
  userinput -- "readline" --> tui
  tui -- "dispatch" --> commands
  rawstdin -- "process.stdin" --> tui
  tui -- "process.stdout" --> terminal
  packagejson -- "readFileSync" --> cli
  userargs -- "linkProject" --> workspace_link
  workspace_link -- "updateAgentWorkspaceContext" --> agentfiles
  reportjson -- "mergeReports" --> merge_engine
  merge_engine -- "mergeReports" --> mergedreport
  gitrepo -- "execSync" --> report_metadata
  report_metadata -- "populateMetadata" --> threatmodel
Data movement across trust boundaries; each boundary shows both sides of the trust line. client or user external party data store servicedashed box: trust zone
Attack Surface
This is a whole-model diagram, past the size anyone can read.
  • Attack Surface — 65 nodes / 0 edges, past the 12 / 16 legibility budget (65 nodes against 12)

The budget is 12 nodes and 16 edges, measured against this panel at its label size — past it the drawing grows taller than the panel and "Fit" shrinks the labels below reading size rather than fitting. It still draws, and zoom and pan still work, so it is kept: on a small model it is the right picture. For a model this size, Explore answers one question at a time and every answer is sized to be read.

%%{init: {"flowchart": {"nodeSpacing": 38, "rankSpacing": 48, "curve": "linear", "htmlLabels": false}}}%%
graph TB
  subgraph A_mcp["GuardLink.MCP · 3 open"]
    direction TB
    E0(["Path_Traversal ×2"]):::sev_high
    E1["Command_Injection"]:::sev_high
    E2(["Arbitrary_File_Write ×2"]):::sev_high
    E3["Prompt_Injection"]:::sev_med
    E4(["API_Key_Exposure"]):::sev_med
    E5["Sensitive_Data_Exposure"]:::sev_med
    E6(["Information_Disclosure ×2"]):::sev_low
    E7(["ReDoS"]):::sev_low
    E8(["Denial_of_Service"]):::sev_low
  end
  subgraph A_agent_launcher["GuardLink.Agent_Launcher · 2 open"]
    direction TB
    E9(["Child_Process_Injection"]):::sev_crit
    E10(["API_Key_Exposure"]):::sev_high
    E11["Prompt_Injection ×2"]:::sev_high
    E12(["Path_Traversal ×2"]):::sev_med
    E13(["Arbitrary_File_Write"]):::sev_med
    E14["Config_Tampering"]:::sev_med
    E15(["Denial_of_Service ×2"]):::sev_low
  end
  subgraph A_llm_client["GuardLink.LLM_Client · 2 open"]
    direction TB
    E16(["API_Key_Exposure"]):::sev_high
    E17(["Path_Traversal ×2"]):::sev_med
    E18(["Arbitrary_File_Write"]):::sev_med
    E19(["Server_Side_Request_Forgery ×2"]):::sev_med
    E20["Prompt_Injection"]:::sev_med
    E21(["Insecure_Deserialization"]):::sev_low
    E22["Sensitive_Data_Exposure"]:::sev_low
    E23(["Denial_of_Service"]):::sev_low
  end
  subgraph A_tui["GuardLink.TUI · 2 open"]
    direction TB
    E24(["Path_Traversal ×2"]):::sev_high
    E25(["Arbitrary_File_Write"]):::sev_high
    E26["Command_Injection"]:::sev_high
    E27(["API_Key_Exposure ×3"]):::sev_high
    E28["Prompt_Injection"]:::sev_med
    E29(["Denial_of_Service"]):::sev_low
  end
  subgraph A_init["GuardLink.Init · 1 open"]
    direction TB
    E30(["Arbitrary_File_Write ×3"]):::sev_high
    E31(["Path_Traversal ×2"]):::sev_med
    E32["Sensitive_Data_Exposure"]:::sev_low
  end
  subgraph A_parser["GuardLink.Parser · 1 open"]
    direction TB
    E33(["Arbitrary_File_Write ×3"]):::sev_high
    E34(["Path_Traversal ×5"]):::sev_high
    E35(["Config_Tampering"]):::sev_med
    E36(["ReDoS ×2"]):::sev_med
    E37(["Denial_of_Service ×5"]):::sev_med
    E38(["Insecure_Deserialization ×3"]):::sev_low
    E39["Sensitive_Data_Exposure"]:::sev_low
  end
  subgraph A_sarif["GuardLink.SARIF · 1 open"]
    direction TB
    E40["Sensitive_Data_Exposure"]:::sev_low
  end
  subgraph A_suggest["GuardLink.Suggest · 1 open"]
    direction TB
    E41(["Path_Traversal"]):::sev_high
    E42(["ReDoS"]):::sev_med
    E43["Denial_of_Service"]:::sev_low
  end
  subgraph A_blame["GuardLink.Blame · 100% covered"]
    direction TB
    E44(["Sensitive_Data_Exposure"]):::sev_med
    E45(["Path_Traversal ×3"]):::sev_low
    E46(["Denial_of_Service ×3"]):::sev_low
    E47(["ReDoS ×2"]):::sev_low
    E48(["Command_Injection"]):::sev_low
  end
  subgraph A_cli["GuardLink.CLI · 100% covered"]
    direction TB
    E49(["Command_Injection ×2"]):::sev_crit
    E50(["Path_Traversal"]):::sev_high
    E51(["Arbitrary_File_Write ×10"]):::sev_high
    E52(["API_Key_Exposure"]):::sev_high
    E53(["Insecure_Deserialization ×2"]):::sev_med
    E54(["ReDoS"]):::sev_low
  end
  subgraph A_dashboard["GuardLink.Dashboard · 100% covered"]
    direction TB
    E55(["Cross_Site_Scripting ×3"]):::sev_high
    E56(["Arbitrary_File_Write"]):::sev_med
    E57(["Path_Traversal ×3"]):::sev_med
    E58(["Sensitive_Data_Exposure"]):::sev_low
    E59(["Denial_of_Service ×2"]):::sev_low
    E60(["Command_Injection"]):::sev_low
  end
  subgraph A_diff["GuardLink.Diff · 100% covered"]
    direction TB
    E61(["Command_Injection ×3"]):::sev_high
    E62(["Arbitrary_File_Write"]):::sev_med
    E63(["Path_Traversal"]):::sev_med
  end
  subgraph A_gate["GuardLink.Gate · 100% covered"]
    direction TB
    E64(["Arbitrary_File_Write"]):::sev_med
  end
  classDef sev_crit fill:#3a1010,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.4px
  classDef sev_high fill:#402019,stroke:#ea1d1d,color:#f0f0f0,stroke-width:1.2px
  classDef sev_med fill:#1f3943,stroke:#55899e,color:#f0f0f0
  classDef sev_low fill:#10263b,stroke:#0360a2,color:#f0f0f0
  classDef sev_unset fill:#223942,stroke:#3b6779,color:#f0f0f0
Exposures per asset. confirmed open mitigated acceptedcolour is severity
Code & Annotations130 files

Every file with GuardLink annotations. Click a file to expand it and any annotation to see details; type / to search by path, kind or asset.

src/agents/prompts.ts 2 openhigh4 stale shield 20flow 5exposes 4comment 3 open 37
L6 exposes #agent-launcher → #prompt-injection
User prompt concatenated into agent instruction text
1 │ /** 2 │ * GuardLink Agents — Prompt builders for annotation and analysis. 3 │ * 4 │ * Extracted from tui/commands.ts for shared use across CLI, TUI, MCP. 5 │ * 6 │ * @exposes #agent-launcher to #prompt-injection [high] cwe:CWE-77 -- "User prompt concatenated into agent instruction text" 7 │ * @audit #agent-launcher -- "Prompt injection mitigated by agent's own safety measures; GuardLink prompt is read-only context" 8 │ * @exposes #agent-launcher to #path-traversal [medium] cwe:CWE-22 -- "Reads reference docs from root-relative paths" 9 │ * @mitigates #agent-launcher against #path-traversal using #path-validation -- "resolve() with root constrains file access" 10 │ * @exposes #agent-launcher to #config-tamper [medium] cwe:CWE-15 -- "Translate prompt may read CXG reference paths from environment overrides" 11 │ * @audit #agent-launcher -- "Environment override paths are optional convenience; verify trusted local paths in CI"
L7 audit Audit: #agent-launcher
Prompt injection mitigated by agent's own safety measures; GuardLink prompt is read-only context
2 │ * GuardLink Agents — Prompt builders for annotation and analysis. 3 │ * 4 │ * Extracted from tui/commands.ts for shared use across CLI, TUI, MCP. 5 │ * 6 │ * @exposes #agent-launcher to #prompt-injection [high] cwe:CWE-77 -- "User prompt concatenated into agent instruction text" 7 │ * @audit #agent-launcher -- "Prompt injection mitigated by agent's own safety measures; GuardLink prompt is read-only context" 8 │ * @exposes #agent-launcher to #path-traversal [medium] cwe:CWE-22 -- "Reads reference docs from root-relative paths" 9 │ * @mitigates #agent-launcher against #path-traversal using #path-validation -- "resolve() with root constrains file access" 10 │ * @exposes #agent-launcher to #config-tamper [medium] cwe:CWE-15 -- "Translate prompt may read CXG reference paths from environment overrides" 11 │ * @audit #agent-launcher -- "Environment override paths are optional convenience; verify trusted local paths in CI" 12 │ * @flows UserPrompt -> #agent-launcher via buildAnnotatePrompt -- "User instruction input"
L8 exposes #agent-launcher → #path-traversal
Reads reference docs from root-relative paths
3 │ * 4 │ * Extracted from tui/commands.ts for shared use across CLI, TUI, MCP. 5 │ * 6 │ * @exposes #agent-launcher to #prompt-injection [high] cwe:CWE-77 -- "User prompt concatenated into agent instruction text" 7 │ * @audit #agent-launcher -- "Prompt injection mitigated by agent's own safety measures; GuardLink prompt is read-only context" 8 │ * @exposes #agent-launcher to #path-traversal [medium] cwe:CWE-22 -- "Reads reference docs from root-relative paths" 9 │ * @mitigates #agent-launcher against #path-traversal using #path-validation -- "resolve() with root constrains file access" 10 │ * @exposes #agent-launcher to #config-tamper [medium] cwe:CWE-15 -- "Translate prompt may read CXG reference paths from environment overrides" 11 │ * @audit #agent-launcher -- "Environment override paths are optional convenience; verify trusted local paths in CI" 12 │ * @flows UserPrompt -> #agent-launcher via buildAnnotatePrompt -- "User instruction input" 13 │ * @flows UserPrompt -> #agent-launcher via buildTranslatePrompt -- "Template translation instruction input"
L9 mitigates #path-validation mitigates #path-traversal
resolve() with root constrains file access
4 │ * Extracted from tui/commands.ts for shared use across CLI, TUI, MCP. 5 │ * 6 │ * @exposes #agent-launcher to #prompt-injection [high] cwe:CWE-77 -- "User prompt concatenated into agent instruction text" 7 │ * @audit #agent-launcher -- "Prompt injection mitigated by agent's own safety measures; GuardLink prompt is read-only context" 8 │ * @exposes #agent-launcher to #path-traversal [medium] cwe:CWE-22 -- "Reads reference docs from root-relative paths" 9 │ * @mitigates #agent-launcher against #path-traversal using #path-validation -- "resolve() with root constrains file access" 10 │ * @exposes #agent-launcher to #config-tamper [medium] cwe:CWE-15 -- "Translate prompt may read CXG reference paths from environment overrides" 11 │ * @audit #agent-launcher -- "Environment override paths are optional convenience; verify trusted local paths in CI" 12 │ * @flows UserPrompt -> #agent-launcher via buildAnnotatePrompt -- "User instruction input" 13 │ * @flows UserPrompt -> #agent-launcher via buildTranslatePrompt -- "Template translation instruction input" 14 │ * @flows UserPrompt -> #agent-launcher via buildAskPrompt -- "Threat model question input"
L10 exposes #agent-launcher → #config-tamper
Translate prompt may read CXG reference paths from environment overrides
5 │ * 6 │ * @exposes #agent-launcher to #prompt-injection [high] cwe:CWE-77 -- "User prompt concatenated into agent instruction text" 7 │ * @audit #agent-launcher -- "Prompt injection mitigated by agent's own safety measures; GuardLink prompt is read-only context" 8 │ * @exposes #agent-launcher to #path-traversal [medium] cwe:CWE-22 -- "Reads reference docs from root-relative paths" 9 │ * @mitigates #agent-launcher against #path-traversal using #path-validation -- "resolve() with root constrains file access" 10 │ * @exposes #agent-launcher to #config-tamper [medium] cwe:CWE-15 -- "Translate prompt may read CXG reference paths from environment overrides" 11 │ * @audit #agent-launcher -- "Environment override paths are optional convenience; verify trusted local paths in CI" 12 │ * @flows UserPrompt -> #agent-launcher via buildAnnotatePrompt -- "User instruction input" 13 │ * @flows UserPrompt -> #agent-launcher via buildTranslatePrompt -- "Template translation instruction input" 14 │ * @flows UserPrompt -> #agent-launcher via buildAskPrompt -- "Threat model question input" 15 │ * @flows ThreatModel -> #agent-launcher via model -- "Model context injection"
L11 audit Audit: #agent-launcher
Environment override paths are optional convenience; verify trusted local paths in CI
6 │ * @exposes #agent-launcher to #prompt-injection [high] cwe:CWE-77 -- "User prompt concatenated into agent instruction text" 7 │ * @audit #agent-launcher -- "Prompt injection mitigated by agent's own safety measures; GuardLink prompt is read-only context" 8 │ * @exposes #agent-launcher to #path-traversal [medium] cwe:CWE-22 -- "Reads reference docs from root-relative paths" 9 │ * @mitigates #agent-launcher against #path-traversal using #path-validation -- "resolve() with root constrains file access" 10 │ * @exposes #agent-launcher to #config-tamper [medium] cwe:CWE-15 -- "Translate prompt may read CXG reference paths from environment overrides" 11 │ * @audit #agent-launcher -- "Environment override paths are optional convenience; verify trusted local paths in CI" 12 │ * @flows UserPrompt -> #agent-launcher via buildAnnotatePrompt -- "User instruction input" 13 │ * @flows UserPrompt -> #agent-launcher via buildTranslatePrompt -- "Template translation instruction input" 14 │ * @flows UserPrompt -> #agent-launcher via buildAskPrompt -- "Threat model question input" 15 │ * @flows ThreatModel -> #agent-launcher via model -- "Model context injection" 16 │ * @flows #agent-launcher -> AgentPrompt via return -- "Assembled prompt output"
L12 flow UserPrompt → #agent-launcher
User instruction input
7 │ * @audit #agent-launcher -- "Prompt injection mitigated by agent's own safety measures; GuardLink prompt is read-only context" 8 │ * @exposes #agent-launcher to #path-traversal [medium] cwe:CWE-22 -- "Reads reference docs from root-relative paths" 9 │ * @mitigates #agent-launcher against #path-traversal using #path-validation -- "resolve() with root constrains file access" 10 │ * @exposes #agent-launcher to #config-tamper [medium] cwe:CWE-15 -- "Translate prompt may read CXG reference paths from environment overrides" 11 │ * @audit #agent-launcher -- "Environment override paths are optional convenience; verify trusted local paths in CI" 12 │ * @flows UserPrompt -> #agent-launcher via buildAnnotatePrompt -- "User instruction input" 13 │ * @flows UserPrompt -> #agent-launcher via buildTranslatePrompt -- "Template translation instruction input" 14 │ * @flows UserPrompt -> #agent-launcher via buildAskPrompt -- "Threat model question input" 15 │ * @flows ThreatModel -> #agent-launcher via model -- "Model context injection" 16 │ * @flows #agent-launcher -> AgentPrompt via return -- "Assembled prompt output" 17 │ * @handles internal on #agent-launcher -- "Serializes threat model IDs and flows into prompt"
L13 flow UserPrompt → #agent-launcher
Template translation instruction input
8 │ * @exposes #agent-launcher to #path-traversal [medium] cwe:CWE-22 -- "Reads reference docs from root-relative paths" 9 │ * @mitigates #agent-launcher against #path-traversal using #path-validation -- "resolve() with root constrains file access" 10 │ * @exposes #agent-launcher to #config-tamper [medium] cwe:CWE-15 -- "Translate prompt may read CXG reference paths from environment overrides" 11 │ * @audit #agent-launcher -- "Environment override paths are optional convenience; verify trusted local paths in CI" 12 │ * @flows UserPrompt -> #agent-launcher via buildAnnotatePrompt -- "User instruction input" 13 │ * @flows UserPrompt -> #agent-launcher via buildTranslatePrompt -- "Template translation instruction input" 14 │ * @flows UserPrompt -> #agent-launcher via buildAskPrompt -- "Threat model question input" 15 │ * @flows ThreatModel -> #agent-launcher via model -- "Model context injection" 16 │ * @flows #agent-launcher -> AgentPrompt via return -- "Assembled prompt output" 17 │ * @handles internal on #agent-launcher -- "Serializes threat model IDs and flows into prompt" 18 │ * @comment -- "The @entitles section instructs agents to file a proposal rather than write the annotation: the rule itself is enforced in src/review/entitlements.ts (an @entitles with no accepted proposal is a validation error), because a prompt is guidance and this claim needs a gate (actor-entitlement design §3.6)"
L14 flow UserPrompt → #agent-launcher
Threat model question input
9 │ * @mitigates #agent-launcher against #path-traversal using #path-validation -- "resolve() with root constrains file access" 10 │ * @exposes #agent-launcher to #config-tamper [medium] cwe:CWE-15 -- "Translate prompt may read CXG reference paths from environment overrides" 11 │ * @audit #agent-launcher -- "Environment override paths are optional convenience; verify trusted local paths in CI" 12 │ * @flows UserPrompt -> #agent-launcher via buildAnnotatePrompt -- "User instruction input" 13 │ * @flows UserPrompt -> #agent-launcher via buildTranslatePrompt -- "Template translation instruction input" 14 │ * @flows UserPrompt -> #agent-launcher via buildAskPrompt -- "Threat model question input" 15 │ * @flows ThreatModel -> #agent-launcher via model -- "Model context injection" 16 │ * @flows #agent-launcher -> AgentPrompt via return -- "Assembled prompt output" 17 │ * @handles internal on #agent-launcher -- "Serializes threat model IDs and flows into prompt" 18 │ * @comment -- "The @entitles section instructs agents to file a proposal rather than write the annotation: the rule itself is enforced in src/review/entitlements.ts (an @entitles with no accepted proposal is a validation error), because a prompt is guidance and this claim needs a gate (actor-entitlement design §3.6)" 19 │ */
L15 flow ThreatModel → #agent-launcher
Model context injection
10 │ * @exposes #agent-launcher to #config-tamper [medium] cwe:CWE-15 -- "Translate prompt may read CXG reference paths from environment overrides" 11 │ * @audit #agent-launcher -- "Environment override paths are optional convenience; verify trusted local paths in CI" 12 │ * @flows UserPrompt -> #agent-launcher via buildAnnotatePrompt -- "User instruction input" 13 │ * @flows UserPrompt -> #agent-launcher via buildTranslatePrompt -- "Template translation instruction input" 14 │ * @flows UserPrompt -> #agent-launcher via buildAskPrompt -- "Threat model question input" 15 │ * @flows ThreatModel -> #agent-launcher via model -- "Model context injection" 16 │ * @flows #agent-launcher -> AgentPrompt via return -- "Assembled prompt output" 17 │ * @handles internal on #agent-launcher -- "Serializes threat model IDs and flows into prompt" 18 │ * @comment -- "The @entitles section instructs agents to file a proposal rather than write the annotation: the rule itself is enforced in src/review/entitlements.ts (an @entitles with no accepted proposal is a validation error), because a prompt is guidance and this claim needs a gate (actor-entitlement design §3.6)" 19 │ */ 20 │
L16 flow #agent-launcher → AgentPrompt
Assembled prompt output
11 │ * @audit #agent-launcher -- "Environment override paths are optional convenience; verify trusted local paths in CI" 12 │ * @flows UserPrompt -> #agent-launcher via buildAnnotatePrompt -- "User instruction input" 13 │ * @flows UserPrompt -> #agent-launcher via buildTranslatePrompt -- "Template translation instruction input" 14 │ * @flows UserPrompt -> #agent-launcher via buildAskPrompt -- "Threat model question input" 15 │ * @flows ThreatModel -> #agent-launcher via model -- "Model context injection" 16 │ * @flows #agent-launcher -> AgentPrompt via return -- "Assembled prompt output" 17 │ * @handles internal on #agent-launcher -- "Serializes threat model IDs and flows into prompt" 18 │ * @comment -- "The @entitles section instructs agents to file a proposal rather than write the annotation: the rule itself is enforced in src/review/entitlements.ts (an @entitles with no accepted proposal is a validation error), because a prompt is guidance and this claim needs a gate (actor-entitlement design §3.6)" 19 │ */ 20 │ 21 │ import { existsSync, readFileSync } from 'node:fs';
L17 handles #agent-launcher: internal
Serializes threat model IDs and flows into prompt
12 │ * @flows UserPrompt -> #agent-launcher via buildAnnotatePrompt -- "User instruction input" 13 │ * @flows UserPrompt -> #agent-launcher via buildTranslatePrompt -- "Template translation instruction input" 14 │ * @flows UserPrompt -> #agent-launcher via buildAskPrompt -- "Threat model question input" 15 │ * @flows ThreatModel -> #agent-launcher via model -- "Model context injection" 16 │ * @flows #agent-launcher -> AgentPrompt via return -- "Assembled prompt output" 17 │ * @handles internal on #agent-launcher -- "Serializes threat model IDs and flows into prompt" 18 │ * @comment -- "The @entitles section instructs agents to file a proposal rather than write the annotation: the rule itself is enforced in src/review/entitlements.ts (an @entitles with no accepted proposal is a validation error), because a prompt is guidance and this claim needs a gate (actor-entitlement design §3.6)" 19 │ */ 20 │ 21 │ import { existsSync, readFileSync } from 'node:fs'; 22 │ import { resolve } from 'node:path';
L18 comment The @entitles section instructs agents to file a proposal rather than write the annotation: the rule itself is enforced in src/review/entitlements.ts (an @entitles with no accepted proposal is a validation error), because a prompt is guidance and this claim needs a gate (actor-entitlement design §3.6)
The @entitles section instructs agents to file a proposal rather than write the annotation: the rule itself is enforced in src/review/entitlements.ts (an @entitles with no accepted proposal is a validation error), because a prompt is guidance and this claim needs a gate (actor-entitlement design §3.6)
13 │ * @flows UserPrompt -> #agent-launcher via buildTranslatePrompt -- "Template translation instruction input" 14 │ * @flows UserPrompt -> #agent-launcher via buildAskPrompt -- "Threat model question input" 15 │ * @flows ThreatModel -> #agent-launcher via model -- "Model context injection" 16 │ * @flows #agent-launcher -> AgentPrompt via return -- "Assembled prompt output" 17 │ * @handles internal on #agent-launcher -- "Serializes threat model IDs and flows into prompt" 18 │ * @comment -- "The @entitles section instructs agents to file a proposal rather than write the annotation: the rule itself is enforced in src/review/entitlements.ts (an @entitles with no accepted proposal is a validation error), because a prompt is guidance and this claim needs a gate (actor-entitlement design §3.6)" 19 │ */ 20 │ 21 │ import { existsSync, readFileSync } from 'node:fs'; 22 │ import { resolve } from 'node:path'; 23 │ import { homedir } from 'node:os';
L65 exposes #agent-launcher → #dos
Prompt size now scales with model size: uncapped flow and exposure rows mean a large repo assembles a large prompt
60 │ * arbitrary row count. On a repo large enough for that budget to bite, the fix 61 │ * is to scope the run (`--feature`, or a subgraph) rather than to hand the agent 62 │ * an arbitrary prefix of the graph and let it believe it saw everything. 63 │ */ 64 │ /** 65 │ * @exposes #agent-launcher to #dos [low] cwe:CWE-400 -- "Prompt size now scales with model size: uncapped flow and exposure rows mean a large repo assembles a large prompt" 66 │ * @mitigates #agent-launcher against #dos using #resource-limits -- "renderRows caps the assembled context at PROMPT_CONTEXT_BUDGET_CHARS and reports how many rows it dropped" 67 │ * @comment -- "Row counts are uncapped on purpose and the byte budget is the only ceiling: a truncated flow graph made the agent duplicate edges it was never shown, and a missing hop reads as findPath's 'no directed path' — a false clean rather than a coverage gap" 68 │ */ 69 │ const PROMPT_CONTEXT_BUDGET_CHARS = 120_000; 70 │
L66 mitigates #resource-limits mitigates #dos
renderRows caps the assembled context at PROMPT_CONTEXT_BUDGET_CHARS and reports how many rows it dropped
61 │ * is to scope the run (`--feature`, or a subgraph) rather than to hand the agent 62 │ * an arbitrary prefix of the graph and let it believe it saw everything. 63 │ */ 64 │ /** 65 │ * @exposes #agent-launcher to #dos [low] cwe:CWE-400 -- "Prompt size now scales with model size: uncapped flow and exposure rows mean a large repo assembles a large prompt" 66 │ * @mitigates #agent-launcher against #dos using #resource-limits -- "renderRows caps the assembled context at PROMPT_CONTEXT_BUDGET_CHARS and reports how many rows it dropped" 67 │ * @comment -- "Row counts are uncapped on purpose and the byte budget is the only ceiling: a truncated flow graph made the agent duplicate edges it was never shown, and a missing hop reads as findPath's 'no directed path' — a false clean rather than a coverage gap" 68 │ */ 69 │ const PROMPT_CONTEXT_BUDGET_CHARS = 120_000; 70 │ 71 │ /** Render rows, keeping every one that fits the budget and saying so when some do not. */
L67 comment Row counts are uncapped on purpose and the byte budget is the only ceiling: a truncated flow graph made the agent duplicate edges it was never shown, and a missing hop reads as findPath's 'no directed path' — a false clean rather than a coverage gap
Row counts are uncapped on purpose and the byte budget is the only ceiling: a truncated flow graph made the agent duplicate edges it was never shown, and a missing hop reads as findPath's 'no directed path' — a false clean rather than a coverage gap
62 │ * an arbitrary prefix of the graph and let it believe it saw everything. 63 │ */ 64 │ /** 65 │ * @exposes #agent-launcher to #dos [low] cwe:CWE-400 -- "Prompt size now scales with model size: uncapped flow and exposure rows mean a large repo assembles a large prompt" 66 │ * @mitigates #agent-launcher against #dos using #resource-limits -- "renderRows caps the assembled context at PROMPT_CONTEXT_BUDGET_CHARS and reports how many rows it dropped" 67 │ * @comment -- "Row counts are uncapped on purpose and the byte budget is the only ceiling: a truncated flow graph made the agent duplicate edges it was never shown, and a missing hop reads as findPath's 'no directed path' — a false clean rather than a coverage gap" 68 │ */ 69 │ const PROMPT_CONTEXT_BUDGET_CHARS = 120_000; 70 │ 71 │ /** Render rows, keeping every one that fits the budget and saying so when some do not. */ 72 │ function renderRows(rows: string[], budget = PROMPT_CONTEXT_BUDGET_CHARS): { text: string; omitted: number } {
L121 comment The method sits above the generic rules on purpose: the agent reads it first, and the evidence bar it carries is what the gate later checks
The method sits above the generic rules on purpose: the agent reads it first, and the evidence bar it carries is what the gate later checks
116 │ * the user's text supplies scope and intent. When no playbook is given one is 117 │ * selected from the text by rule, so a vague prompt still runs a full method. 118 │ * Then the reference doc, the current model, the placement mode and the 119 │ * syntax rules. 120 │ * 121 │ * @comment -- "The method sits above the generic rules on purpose: the agent reads it first, and the evidence bar it carries is what the gate later checks" 122 │ */ 123 │ export function buildAnnotatePrompt( 124 │ userPrompt: string, 125 │ root: string, 126 │ model: ThreatModel | null,
L268 shield Example annotation block for reference, excluded from parsing
263 │ 264 │ ### Always Couple Annotations Together 265 │ A file's doc-block should paint the full security picture of that module. Group annotations logically: 266 │ 267 │ \`\`\` 268 │ // @shield:begin -- "Example annotation block for reference, excluded from parsing" 269 │ // 270 │ // GOOD — Complete story at a single code location: 271 │ // @exposes #auth-api to #sqli [P1] cwe:CWE-89 -- "User-supplied email passed to findUser() query builder" 272 │ // @mitigates #auth-api against #sqli using #input-validation -- "Zod schema validates email format before query" 273 │ // @flows User_Input -> #auth-api via POST./login -- "Login form submits credentials"
L281 shield Shielded region
276 │ // @comment -- "Password comparison uses bcrypt.compare with timing-safe equality" 277 │ // 278 │ // BAD — Isolated annotation with no context: 279 │ // @exposes #auth-api to #sqli -- "SQL injection possible" 280 │ // 281 │ // @shield:end 282 │ \`\`\` 283 │ 284 │ ### Description Style — Reference Actual Code 285 │ Descriptions must reference the real code: function names, variable names, libraries, mechanisms. 286 │
L288 shield Description examples, excluded from parsing
283 │ 284 │ ### Description Style — Reference Actual Code 285 │ Descriptions must reference the real code: function names, variable names, libraries, mechanisms. 286 │ 287 │ \`\`\` 288 │ // @shield:begin -- "Description examples, excluded from parsing" 289 │ // 290 │ // GOOD: -- "req.body.token passed to jwt.verify() without audience check" 291 │ // GOOD: -- "bcrypt rounds set to 12 via BCRYPT_COST env var" 292 │ // GOOD: -- "Rate limiter uses express-rate-limit at 100req/15min on /api/*" 293 │ //
L298 shield Shielded region
293 │ // 294 │ // BAD: -- "Input not validated" (too vague — WHICH input? WHERE?) 295 │ // BAD: -- "Uses encryption" (WHAT encryption? On WHAT data?) 296 │ // BAD: -- "Security vulnerability exists" (meaningless — be specific) 297 │ // 298 │ // @shield:end 299 │ \`\`\` 300 │ 301 │ ### \`@flows\` — Stitch the Complete Data Path 302 │ @flows is the backbone of the threat model. Trace data movement accurately: 303 │
L305 shield Flow examples, excluded from parsing
300 │ 301 │ ### \`@flows\` — Stitch the Complete Data Path 302 │ @flows is the backbone of the threat model. Trace data movement accurately: 303 │ 304 │ \`\`\` 305 │ // @shield:begin -- "Flow examples, excluded from parsing" 306 │ // 307 │ // Trace a request through the full stack: 308 │ // @flows User_Browser -> #api-gateway via HTTPS -- "Client sends auth request" 309 │ // @flows #api-gateway -> #auth-service via internal.gRPC -- "Gateway forwards to auth microservice" 310 │ // @flows #auth-service -> #user-db via pg.query -- "Looks up user record by email"
L314 shield Shielded region
309 │ // @flows #api-gateway -> #auth-service via internal.gRPC -- "Gateway forwards to auth microservice" 310 │ // @flows #auth-service -> #user-db via pg.query -- "Looks up user record by email" 311 │ // @flows #auth-service -> #session-store via redis.set -- "Stores session token with TTL" 312 │ // @flows #auth-service -> User_Browser via Set-Cookie -- "Returns session cookie to client" 313 │ // 314 │ // @shield:end 315 │ \`\`\` 316 │ 317 │ ### \`@boundary\` — Mark Every Trust Zone Crossing 318 │ Place @boundary annotations where trust level changes between two components: 319 │
L321 shield Boundary examples, excluded from parsing
316 │ 317 │ ### \`@boundary\` — Mark Every Trust Zone Crossing 318 │ Place @boundary annotations where trust level changes between two components: 319 │ 320 │ \`\`\` 321 │ // @shield:begin -- "Boundary examples, excluded from parsing" 322 │ // 323 │ // @boundary between #api-gateway and External_Internet (#public-boundary) -- "TLS termination, rate limiting at edge" 324 │ // @boundary between #backend and #database (#data-boundary) -- "Application to persistence layer, connection pooling via pgBouncer" 325 │ // @boundary between #app and #payment-provider (#vendor-boundary) -- "PCI-DSS scope boundary, tokenized card data only" 326 │ //
L327 shield Shielded region
322 │ // 323 │ // @boundary between #api-gateway and External_Internet (#public-boundary) -- "TLS termination, rate limiting at edge" 324 │ // @boundary between #backend and #database (#data-boundary) -- "Application to persistence layer, connection pooling via pgBouncer" 325 │ // @boundary between #app and #payment-provider (#vendor-boundary) -- "PCI-DSS scope boundary, tokenized card data only" 326 │ // 327 │ // @shield:end 328 │ \`\`\` 329 │ 330 │ ### Where to Place Annotations 331 │ ${annotationMode === 'external' 332 │ ? 'Annotations go in associated `.gal` files, grouped by `@source` blocks that point at the real code location:'
L388 shield Actor example, excluded from parsing
383 │ 4. **Under-grant, don't over-grant.** Naming too narrow an actor costs noise; naming too broad an actor can close a real privilege escalation as by-design. When unsure which role the code actually requires, write @comment describing what you saw and let a human decide. 384 │ 5. **Declare the actor first.** \`@actor <Name> (#id)\` belongs in the definitions file next to @asset/@threat/@control; an @entitles naming an undeclared actor is a validation error. 385 │ 386 │ \`\`\` 387 │ // In the definitions file — declaring the principal is yours to do: 388 │ // @shield:begin -- "Actor example, excluded from parsing" 389 │ // @actor Namespace_Admin (#ns-admin) -- "Administers one namespace's configuration" 390 │ // @shield:end 391 │ \`\`\` 392 │ 393 │ Then propose the entitlement instead of writing it:
L390 shield Shielded region
385 │ 386 │ \`\`\` 387 │ // In the definitions file — declaring the principal is yours to do: 388 │ // @shield:begin -- "Actor example, excluded from parsing" 389 │ // @actor Namespace_Admin (#ns-admin) -- "Administers one namespace's configuration" 390 │ // @shield:end 391 │ \`\`\` 392 │ 393 │ Then propose the entitlement instead of writing it: 394 │ 395 │ \`\`\`
L407 shield Entitlement example, excluded from parsing
402 │ \`\`\` 403 │ 404 │ Report what you proposed at the end of your run, and tell the user to review it with \`guardlink entitle\`. If they accept, the annotation below is what lands — written by the accept step, with their name on it: 405 │ 406 │ \`\`\` 407 │ // @shield:begin -- "Entitlement example, excluded from parsing" 408 │ // @entitles #ns-admin to configure-archival-destination on #archival-fs 409 │ // -- "By design: the archival URI is namespace configuration. Authz: ScopeCluster/AccessAdmin at common/api/metadata.go:189" 410 │ // @shield:end 411 │ \`\`\` 412 │
L410 shield Shielded region
405 │ 406 │ \`\`\` 407 │ // @shield:begin -- "Entitlement example, excluded from parsing" 408 │ // @entitles #ns-admin to configure-archival-destination on #archival-fs 409 │ // -- "By design: the archival URI is namespace configuration. Authz: ScopeCluster/AccessAdmin at common/api/metadata.go:189" 410 │ // @shield:end 411 │ \`\`\` 412 │ 413 │ ### \`@accepts\` — NEVER USE (Human-Only Decision) 414 │ @accepts marks a risk as intentionally unmitigated. This is a **human-only governance decision** — it requires conscious risk ownership by a person or team. 415 │ As an AI agent, you MUST NEVER write @accepts annotations. You cannot accept risk on behalf of humans.
L425 shield @accepts alternative examples, excluded from parsing
420 │ 3. Add @comment explaining what controls COULD be added 421 │ 4. Optionally add @assumes to document any assumptions the code makes 422 │ 423 │ Example — what to do when no mitigation exists: 424 │ \`\`\` 425 │ // @shield:begin -- "@accepts alternative examples, excluded from parsing" 426 │ // 427 │ // WRONG (AI rubber-stamping risk): 428 │ // @accepts #prompt-injection on #ai-endpoint -- "Relying on model safety filters" 429 │ // 430 │ // RIGHT (flag for human review):
L435 shield Shielded region
430 │ // RIGHT (flag for human review): 431 │ // @exposes #ai-endpoint to #prompt-injection [P1] cwe:CWE-77 -- "User prompt passed directly to LLM API without sanitization" 432 │ // @audit #ai-endpoint -- "No prompt sanitization — needs human review to decide: add input filter or accept risk" 433 │ // @comment -- "Potential controls: #prompt-filter (input sanitization), #output-validator (response filtering)" 434 │ // 435 │ // @shield:end 436 │ \`\`\` 437 │ 438 │ Leaving exposures unmitigated is HONEST. The dashboard and reports will surface them as open risks for humans to triage. 439 │ 440 │ ### Pentest-Confirmable vs Governance-Only Gaps
L462 shield Definition syntax examples, excluded from parsing
457 │ 458 │ Definitions go in .guardlink/definitions.{ts,js,py,rs}. Relationship annotations can live in source comments or standalone .gal files. 459 │ 460 │ ### Definitions (in .guardlink/definitions file) 461 │ \`\`\` 462 │ // @shield:begin -- "Definition syntax examples, excluded from parsing" 463 │ // @asset Server.Auth (#auth) -- "Authentication service handling login and session management" 464 │ // @threat SQL_Injection (#sqli) [P0] cwe:CWE-89 -- "Unsanitized input reaches SQL query builder" 465 │ // @control Prepared_Statements (#prepared-stmts) -- "Parameterized queries via ORM or driver placeholders" 466 │ // @shield:end 467 │ \`\`\`
L466 shield Shielded region
461 │ \`\`\` 462 │ // @shield:begin -- "Definition syntax examples, excluded from parsing" 463 │ // @asset Server.Auth (#auth) -- "Authentication service handling login and session management" 464 │ // @threat SQL_Injection (#sqli) [P0] cwe:CWE-89 -- "Unsanitized input reaches SQL query builder" 465 │ // @control Prepared_Statements (#prepared-stmts) -- "Parameterized queries via ORM or driver placeholders" 466 │ // @shield:end 467 │ \`\`\` 468 │ 469 │ ### Relationships (in source files) 470 │ \`\`\` 471 │ // @shield:begin -- "Relationship syntax examples, excluded from parsing"
L471 shield Relationship syntax examples, excluded from parsing
466 │ // @shield:end 467 │ \`\`\` 468 │ 469 │ ### Relationships (in source files) 470 │ \`\`\` 471 │ // @shield:begin -- "Relationship syntax examples, excluded from parsing" 472 │ // @exposes #auth to #sqli [P0] cwe:CWE-89 owasp:A03:2021 -- "User input concatenated into query" 473 │ // @confirmed #sqli on #auth [critical] cwe:CWE-89 -- "Pentest 2026-04: time-based blind SQLi on /login confirmed" 474 │ // @mitigates #auth against #sqli using #prepared-stmts -- "Uses parameterized queries via sqlx" 475 │ // @audit #auth -- "Timing attack risk — needs human review to decide if bcrypt constant-time comparison is sufficient" 476 │ // @transfers #ddos from #api to #cdn -- "Cloudflare handles L7 DDoS mitigation"
L487 shield Shielded region
482 │ // @assumes #auth -- "Upstream API gateway has already validated TLS and rate-limited requests" 483 │ // @owns security-team for #auth -- "Security team reviews all auth PRs" 484 │ // @entitles #tenant-admin to rotate-signing-key on #auth -- "By design: key rotation is tenant admin scope. Authz: src/authz/scopes.ts:88" 485 │ // @feature "SSO Login" -- "Single sign-on authentication flow" 486 │ // @comment -- "Password hashing uses bcrypt with cost factor 12, migration from SHA256 completed in v2.1" 487 │ // @shield:end 488 │ \`\`\` 489 │ 490 │ ### Relationships (in standalone .gal files) 491 │ \`\`\` 492 │ // @shield:begin -- "Externalized relationship examples, excluded from parsing"
L492 shield Externalized relationship examples, excluded from parsing
487 │ // @shield:end 488 │ \`\`\` 489 │ 490 │ ### Relationships (in standalone .gal files) 491 │ \`\`\` 492 │ // @shield:begin -- "Externalized relationship examples, excluded from parsing" 493 │ @source file:src/auth/login.ts line:42 symbol:authenticate 494 │ @exposes #auth to #sqli [P0] cwe:CWE-89 owasp:A03:2021 -- "User input concatenated into query" 495 │ @mitigates #auth against #sqli using #prepared-stmts -- "Uses parameterized queries via sqlx" 496 │ @audit #auth -- "Timing attack risk — needs human review" 497 │ // @shield:end
L497 shield Shielded region
492 │ // @shield:begin -- "Externalized relationship examples, excluded from parsing" 493 │ @source file:src/auth/login.ts line:42 symbol:authenticate 494 │ @exposes #auth to #sqli [P0] cwe:CWE-89 owasp:A03:2021 -- "User input concatenated into query" 495 │ @mitigates #auth against #sqli using #prepared-stmts -- "Uses parameterized queries via sqlx" 496 │ @audit #auth -- "Timing attack risk — needs human review" 497 │ // @shield:end 498 │ \`\`\` 499 │ 500 │ ## CRITICAL SYNTAX RULES (violations cause parse errors) 501 │ 502 │ 1. **@boundary requires TWO assets**: \`@boundary between #A and #B\` or \`@boundary #A | #B\`.
src/tui/commands.ts 2 openhigh8 stale exposes 5flow 5mitigates 3audit 2 open 19
L7 exposes #tui → #path-traversal
File paths from user args in /view, /sarif -o
2 │ * GuardLink TUI — Command implementations. 3 │ * 4 │ * Each command function takes (args, ctx) and prints output directly. 5 │ * Returns void. Throws on fatal errors. 6 │ * 7 │ * @exposes #tui to #path-traversal [high] cwe:CWE-22 -- "File paths from user args in /view, /sarif -o" 8 │ * @mitigates #tui against #path-traversal using #path-validation -- "resolve() with ctx.root constrains file access" 9 │ * @exposes #tui to #arbitrary-write [high] cwe:CWE-73 -- "/report, /sarif, /dashboard write files" 10 │ * @mitigates #tui against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root" 11 │ * @exposes #tui to #cmd-injection [high] cwe:CWE-78 -- "/annotate and /threat-report spawn child processes" 12 │ * @audit #tui -- "Child process spawning delegated to agents/launcher.ts"
L8 mitigates #path-validation mitigates #path-traversal
resolve() with ctx.root constrains file access
3 │ * 4 │ * Each command function takes (args, ctx) and prints output directly. 5 │ * Returns void. Throws on fatal errors. 6 │ * 7 │ * @exposes #tui to #path-traversal [high] cwe:CWE-22 -- "File paths from user args in /view, /sarif -o" 8 │ * @mitigates #tui against #path-traversal using #path-validation -- "resolve() with ctx.root constrains file access" 9 │ * @exposes #tui to #arbitrary-write [high] cwe:CWE-73 -- "/report, /sarif, /dashboard write files" 10 │ * @mitigates #tui against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root" 11 │ * @exposes #tui to #cmd-injection [high] cwe:CWE-78 -- "/annotate and /threat-report spawn child processes" 12 │ * @audit #tui -- "Child process spawning delegated to agents/launcher.ts" 13 │ * @exposes #tui to #api-key-exposure [high] cwe:CWE-798 -- "/model handles API key input and storage"
L9 exposes #tui → #arbitrary-write
/report, /sarif, /dashboard write files
4 │ * Each command function takes (args, ctx) and prints output directly. 5 │ * Returns void. Throws on fatal errors. 6 │ * 7 │ * @exposes #tui to #path-traversal [high] cwe:CWE-22 -- "File paths from user args in /view, /sarif -o" 8 │ * @mitigates #tui against #path-traversal using #path-validation -- "resolve() with ctx.root constrains file access" 9 │ * @exposes #tui to #arbitrary-write [high] cwe:CWE-73 -- "/report, /sarif, /dashboard write files" 10 │ * @mitigates #tui against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root" 11 │ * @exposes #tui to #cmd-injection [high] cwe:CWE-78 -- "/annotate and /threat-report spawn child processes" 12 │ * @audit #tui -- "Child process spawning delegated to agents/launcher.ts" 13 │ * @exposes #tui to #api-key-exposure [high] cwe:CWE-798 -- "/model handles API key input and storage" 14 │ * @mitigates #tui against #api-key-exposure using #key-redaction -- "API keys masked in /model show output"
L10 mitigates #path-validation mitigates #arbitrary-write
Output paths resolved relative to project root
5 │ * Returns void. Throws on fatal errors. 6 │ * 7 │ * @exposes #tui to #path-traversal [high] cwe:CWE-22 -- "File paths from user args in /view, /sarif -o" 8 │ * @mitigates #tui against #path-traversal using #path-validation -- "resolve() with ctx.root constrains file access" 9 │ * @exposes #tui to #arbitrary-write [high] cwe:CWE-73 -- "/report, /sarif, /dashboard write files" 10 │ * @mitigates #tui against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root" 11 │ * @exposes #tui to #cmd-injection [high] cwe:CWE-78 -- "/annotate and /threat-report spawn child processes" 12 │ * @audit #tui -- "Child process spawning delegated to agents/launcher.ts" 13 │ * @exposes #tui to #api-key-exposure [high] cwe:CWE-798 -- "/model handles API key input and storage" 14 │ * @mitigates #tui against #api-key-exposure using #key-redaction -- "API keys masked in /model show output" 15 │ * @exposes #tui to #prompt-injection [medium] cwe:CWE-77 -- "Freeform chat sends user text to LLM"
L11 exposes #tui → #cmd-injection
/annotate and /threat-report spawn child processes
6 │ * 7 │ * @exposes #tui to #path-traversal [high] cwe:CWE-22 -- "File paths from user args in /view, /sarif -o" 8 │ * @mitigates #tui against #path-traversal using #path-validation -- "resolve() with ctx.root constrains file access" 9 │ * @exposes #tui to #arbitrary-write [high] cwe:CWE-73 -- "/report, /sarif, /dashboard write files" 10 │ * @mitigates #tui against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root" 11 │ * @exposes #tui to #cmd-injection [high] cwe:CWE-78 -- "/annotate and /threat-report spawn child processes" 12 │ * @audit #tui -- "Child process spawning delegated to agents/launcher.ts" 13 │ * @exposes #tui to #api-key-exposure [high] cwe:CWE-798 -- "/model handles API key input and storage" 14 │ * @mitigates #tui against #api-key-exposure using #key-redaction -- "API keys masked in /model show output" 15 │ * @exposes #tui to #prompt-injection [medium] cwe:CWE-77 -- "Freeform chat sends user text to LLM" 16 │ * @audit #tui -- "User freeform text passed to LLM via cmdChat; model context is read-only"
L12 audit Audit: #tui
Child process spawning delegated to agents/launcher.ts
7 │ * @exposes #tui to #path-traversal [high] cwe:CWE-22 -- "File paths from user args in /view, /sarif -o" 8 │ * @mitigates #tui against #path-traversal using #path-validation -- "resolve() with ctx.root constrains file access" 9 │ * @exposes #tui to #arbitrary-write [high] cwe:CWE-73 -- "/report, /sarif, /dashboard write files" 10 │ * @mitigates #tui against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root" 11 │ * @exposes #tui to #cmd-injection [high] cwe:CWE-78 -- "/annotate and /threat-report spawn child processes" 12 │ * @audit #tui -- "Child process spawning delegated to agents/launcher.ts" 13 │ * @exposes #tui to #api-key-exposure [high] cwe:CWE-798 -- "/model handles API key input and storage" 14 │ * @mitigates #tui against #api-key-exposure using #key-redaction -- "API keys masked in /model show output" 15 │ * @exposes #tui to #prompt-injection [medium] cwe:CWE-77 -- "Freeform chat sends user text to LLM" 16 │ * @audit #tui -- "User freeform text passed to LLM via cmdChat; model context is read-only" 17 │ * @flows UserArgs -> #tui via args -- "Command argument input"
L13 exposes #tui → #api-key-exposure
/model handles API key input and storage
8 │ * @mitigates #tui against #path-traversal using #path-validation -- "resolve() with ctx.root constrains file access" 9 │ * @exposes #tui to #arbitrary-write [high] cwe:CWE-73 -- "/report, /sarif, /dashboard write files" 10 │ * @mitigates #tui against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root" 11 │ * @exposes #tui to #cmd-injection [high] cwe:CWE-78 -- "/annotate and /threat-report spawn child processes" 12 │ * @audit #tui -- "Child process spawning delegated to agents/launcher.ts" 13 │ * @exposes #tui to #api-key-exposure [high] cwe:CWE-798 -- "/model handles API key input and storage" 14 │ * @mitigates #tui against #api-key-exposure using #key-redaction -- "API keys masked in /model show output" 15 │ * @exposes #tui to #prompt-injection [medium] cwe:CWE-77 -- "Freeform chat sends user text to LLM" 16 │ * @audit #tui -- "User freeform text passed to LLM via cmdChat; model context is read-only" 17 │ * @flows UserArgs -> #tui via args -- "Command argument input" 18 │ * @flows #tui -> FileSystem via writeFile -- "Report/config output"
L14 mitigates #key-redaction mitigates #api-key-exposure
API keys masked in /model show output
9 │ * @exposes #tui to #arbitrary-write [high] cwe:CWE-73 -- "/report, /sarif, /dashboard write files" 10 │ * @mitigates #tui against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root" 11 │ * @exposes #tui to #cmd-injection [high] cwe:CWE-78 -- "/annotate and /threat-report spawn child processes" 12 │ * @audit #tui -- "Child process spawning delegated to agents/launcher.ts" 13 │ * @exposes #tui to #api-key-exposure [high] cwe:CWE-798 -- "/model handles API key input and storage" 14 │ * @mitigates #tui against #api-key-exposure using #key-redaction -- "API keys masked in /model show output" 15 │ * @exposes #tui to #prompt-injection [medium] cwe:CWE-77 -- "Freeform chat sends user text to LLM" 16 │ * @audit #tui -- "User freeform text passed to LLM via cmdChat; model context is read-only" 17 │ * @flows UserArgs -> #tui via args -- "Command argument input" 18 │ * @flows #tui -> FileSystem via writeFile -- "Report/config output" 19 │ * @flows #tui -> #agent-launcher via launchAgent -- "Agent spawn path"
L15 exposes #tui → #prompt-injection
Freeform chat sends user text to LLM
10 │ * @mitigates #tui against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root" 11 │ * @exposes #tui to #cmd-injection [high] cwe:CWE-78 -- "/annotate and /threat-report spawn child processes" 12 │ * @audit #tui -- "Child process spawning delegated to agents/launcher.ts" 13 │ * @exposes #tui to #api-key-exposure [high] cwe:CWE-798 -- "/model handles API key input and storage" 14 │ * @mitigates #tui against #api-key-exposure using #key-redaction -- "API keys masked in /model show output" 15 │ * @exposes #tui to #prompt-injection [medium] cwe:CWE-77 -- "Freeform chat sends user text to LLM" 16 │ * @audit #tui -- "User freeform text passed to LLM via cmdChat; model context is read-only" 17 │ * @flows UserArgs -> #tui via args -- "Command argument input" 18 │ * @flows #tui -> FileSystem via writeFile -- "Report/config output" 19 │ * @flows #tui -> #agent-launcher via launchAgent -- "Agent spawn path" 20 │ * @flows #tui -> #llm-client via chatCompletion -- "LLM API call path"
L16 audit Audit: #tui
User freeform text passed to LLM via cmdChat; model context is read-only
11 │ * @exposes #tui to #cmd-injection [high] cwe:CWE-78 -- "/annotate and /threat-report spawn child processes" 12 │ * @audit #tui -- "Child process spawning delegated to agents/launcher.ts" 13 │ * @exposes #tui to #api-key-exposure [high] cwe:CWE-798 -- "/model handles API key input and storage" 14 │ * @mitigates #tui against #api-key-exposure using #key-redaction -- "API keys masked in /model show output" 15 │ * @exposes #tui to #prompt-injection [medium] cwe:CWE-77 -- "Freeform chat sends user text to LLM" 16 │ * @audit #tui -- "User freeform text passed to LLM via cmdChat; model context is read-only" 17 │ * @flows UserArgs -> #tui via args -- "Command argument input" 18 │ * @flows #tui -> FileSystem via writeFile -- "Report/config output" 19 │ * @flows #tui -> #agent-launcher via launchAgent -- "Agent spawn path" 20 │ * @flows #tui -> #llm-client via chatCompletion -- "LLM API call path" 21 │ * @handles secrets on #tui -- "Processes and stores API keys via /model"
L17 flow UserArgs → #tui
Command argument input
12 │ * @audit #tui -- "Child process spawning delegated to agents/launcher.ts" 13 │ * @exposes #tui to #api-key-exposure [high] cwe:CWE-798 -- "/model handles API key input and storage" 14 │ * @mitigates #tui against #api-key-exposure using #key-redaction -- "API keys masked in /model show output" 15 │ * @exposes #tui to #prompt-injection [medium] cwe:CWE-77 -- "Freeform chat sends user text to LLM" 16 │ * @audit #tui -- "User freeform text passed to LLM via cmdChat; model context is read-only" 17 │ * @flows UserArgs -> #tui via args -- "Command argument input" 18 │ * @flows #tui -> FileSystem via writeFile -- "Report/config output" 19 │ * @flows #tui -> #agent-launcher via launchAgent -- "Agent spawn path" 20 │ * @flows #tui -> #llm-client via chatCompletion -- "LLM API call path" 21 │ * @handles secrets on #tui -- "Processes and stores API keys via /model" 22 │ */
L18 flow #tui → FileSystem
Report/config output
13 │ * @exposes #tui to #api-key-exposure [high] cwe:CWE-798 -- "/model handles API key input and storage" 14 │ * @mitigates #tui against #api-key-exposure using #key-redaction -- "API keys masked in /model show output" 15 │ * @exposes #tui to #prompt-injection [medium] cwe:CWE-77 -- "Freeform chat sends user text to LLM" 16 │ * @audit #tui -- "User freeform text passed to LLM via cmdChat; model context is read-only" 17 │ * @flows UserArgs -> #tui via args -- "Command argument input" 18 │ * @flows #tui -> FileSystem via writeFile -- "Report/config output" 19 │ * @flows #tui -> #agent-launcher via launchAgent -- "Agent spawn path" 20 │ * @flows #tui -> #llm-client via chatCompletion -- "LLM API call path" 21 │ * @handles secrets on #tui -- "Processes and stores API keys via /model" 22 │ */ 23 │
L19 flow #tui → #agent-launcher
Agent spawn path
14 │ * @mitigates #tui against #api-key-exposure using #key-redaction -- "API keys masked in /model show output" 15 │ * @exposes #tui to #prompt-injection [medium] cwe:CWE-77 -- "Freeform chat sends user text to LLM" 16 │ * @audit #tui -- "User freeform text passed to LLM via cmdChat; model context is read-only" 17 │ * @flows UserArgs -> #tui via args -- "Command argument input" 18 │ * @flows #tui -> FileSystem via writeFile -- "Report/config output" 19 │ * @flows #tui -> #agent-launcher via launchAgent -- "Agent spawn path" 20 │ * @flows #tui -> #llm-client via chatCompletion -- "LLM API call path" 21 │ * @handles secrets on #tui -- "Processes and stores API keys via /model" 22 │ */ 23 │ 24 │ import { resolve, basename } from 'node:path';
L20 flow #tui → #llm-client
LLM API call path
15 │ * @exposes #tui to #prompt-injection [medium] cwe:CWE-77 -- "Freeform chat sends user text to LLM" 16 │ * @audit #tui -- "User freeform text passed to LLM via cmdChat; model context is read-only" 17 │ * @flows UserArgs -> #tui via args -- "Command argument input" 18 │ * @flows #tui -> FileSystem via writeFile -- "Report/config output" 19 │ * @flows #tui -> #agent-launcher via launchAgent -- "Agent spawn path" 20 │ * @flows #tui -> #llm-client via chatCompletion -- "LLM API call path" 21 │ * @handles secrets on #tui -- "Processes and stores API keys via /model" 22 │ */ 23 │ 24 │ import { resolve, basename } from 'node:path'; 25 │ import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';
L21 handles #tui: secrets
Processes and stores API keys via /model
16 │ * @audit #tui -- "User freeform text passed to LLM via cmdChat; model context is read-only" 17 │ * @flows UserArgs -> #tui via args -- "Command argument input" 18 │ * @flows #tui -> FileSystem via writeFile -- "Report/config output" 19 │ * @flows #tui -> #agent-launcher via launchAgent -- "Agent spawn path" 20 │ * @flows #tui -> #llm-client via chatCompletion -- "LLM API call path" 21 │ * @handles secrets on #tui -- "Processes and stores API keys via /model" 22 │ */ 23 │ 24 │ import { resolve, basename } from 'node:path'; 25 │ import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs'; 26 │ import { parseProject, findDanglingRefs, findUnmitigatedExposures, findAcceptedWithoutAudit, findAcceptedExposures, findUndeclaredActors, findInertEntitlements, findImpreciseEntitlements, clearAnnotations, listFeatures, filterByFeature, getFeatureSummaries } from '../parser/index.js';
L995 flow GitRepo → #tui
Attribution read from blame, log -L and commit trailers
990 │ 991 │ /** 992 │ * Attribution for the current model, read from git. Prints the same text as 993 │ * `guardlink blame`, indented. 994 │ * 995 │ * @flows GitRepo -> #tui via computeBlame -- "Attribution read from blame, log -L and commit trailers" 996 │ * @handles pii on #tui -- "Author identities printed to the terminal in the configured identity mode" 997 │ * @comment -- "Non-mutating on purpose: ctx.model is reused by every later command, and /blame must not make /report carry attribution unasked" 998 │ */ 999 │ export async function cmdBlame(args: string, ctx: TuiContext): Promise<void> {1000 │ if (!ctx.model) await refreshModel(ctx);
L996 handles #tui: pii
Author identities printed to the terminal in the configured identity mode
991 │ /** 992 │ * Attribution for the current model, read from git. Prints the same text as 993 │ * `guardlink blame`, indented. 994 │ * 995 │ * @flows GitRepo -> #tui via computeBlame -- "Attribution read from blame, log -L and commit trailers" 996 │ * @handles pii on #tui -- "Author identities printed to the terminal in the configured identity mode" 997 │ * @comment -- "Non-mutating on purpose: ctx.model is reused by every later command, and /blame must not make /report carry attribution unasked" 998 │ */ 999 │ export async function cmdBlame(args: string, ctx: TuiContext): Promise<void> {1000 │ if (!ctx.model) await refreshModel(ctx);1001 │ const file = args.trim() || undefined;
L997 comment Non-mutating on purpose: ctx.model is reused by every later command, and /blame must not make /report carry attribution unasked
Non-mutating on purpose: ctx.model is reused by every later command, and /blame must not make /report carry attribution unasked
992 │ * Attribution for the current model, read from git. Prints the same text as 993 │ * `guardlink blame`, indented. 994 │ * 995 │ * @flows GitRepo -> #tui via computeBlame -- "Attribution read from blame, log -L and commit trailers" 996 │ * @handles pii on #tui -- "Author identities printed to the terminal in the configured identity mode" 997 │ * @comment -- "Non-mutating on purpose: ctx.model is reused by every later command, and /blame must not make /report carry attribution unasked" 998 │ */ 999 │ export async function cmdBlame(args: string, ctx: TuiContext): Promise<void> {1000 │ if (!ctx.model) await refreshModel(ctx);1001 │ const file = args.trim() || undefined;1002 │ console.log(C.dim(` Reading git history${file ? ` for ${file}` : ''}...`));
L1069 comment Derived query over @flows and @mitigates; every hop it prints is an existing annotation location, so a finding here cannot cite a line that was never written
Derived query over @flows and @mitigates; every hop it prints is an existing annotation location, so a finding here cannot cite a line that was never written
1064 │ *1065 │ * Reads the parsed model only — no file writes, no agent, no network — which is1066 │ * why this handler has no @exposes of its own while its neighbours in this file1067 │ * do. `--all` widens the result to routes a control already covers.1068 │ *1069 │ * @comment -- "Derived query over @flows and @mitigates; every hop it prints is an existing annotation location, so a finding here cannot cite a line that was never written"1070 │ */1071 │ export async function cmdPaths(args: string, ctx: TuiContext): Promise<void> {1072 │ const includeMitigated = /(?:^|\s)--all(?=\s|$)/.test(args);1073 │ const boundaryOnly = /(?:^|\s)--boundary-only(?=\s|$)/.test(args);1074 │
src/mcp/index.ts 1 openhigh exposes 1audit 1flow 1boundary 1 open 5
L6 exposes #mcp → #cmd-injection
Accepts tool calls from external MCP clients
1 │ #!/usr/bin/env node 2 │ 3 │ /** 4 │ * GuardLink MCP Server — exports and stdio entry point. 5 │ * 6 │ * @exposes #mcp to #cmd-injection [high] cwe:CWE-78 -- "Accepts tool calls from external MCP clients" 7 │ * @audit #mcp -- "All tool calls validated by server.ts before execution" 8 │ * @flows MCPClient -> #mcp via stdio -- "MCP protocol transport" 9 │ * @boundary #mcp and MCPClient (#mcp-boundary) -- "Trust boundary at MCP protocol" 10 │ * @comment -- "D35: the `guardlink-mcp` bin executes this module directly, so it carries a shebang and a main guard. Startup errors go to stderr — stdout is the JSON-RPC channel and a stray line there corrupts the transport." 11 │ */
L7 audit Audit: #mcp
All tool calls validated by server.ts before execution
2 │ 3 │ /** 4 │ * GuardLink MCP Server — exports and stdio entry point. 5 │ * 6 │ * @exposes #mcp to #cmd-injection [high] cwe:CWE-78 -- "Accepts tool calls from external MCP clients" 7 │ * @audit #mcp -- "All tool calls validated by server.ts before execution" 8 │ * @flows MCPClient -> #mcp via stdio -- "MCP protocol transport" 9 │ * @boundary #mcp and MCPClient (#mcp-boundary) -- "Trust boundary at MCP protocol" 10 │ * @comment -- "D35: the `guardlink-mcp` bin executes this module directly, so it carries a shebang and a main guard. Startup errors go to stderr — stdout is the JSON-RPC channel and a stray line there corrupts the transport." 11 │ */ 12 │
L8 flow MCPClient → #mcp
MCP protocol transport
3 │ /** 4 │ * GuardLink MCP Server — exports and stdio entry point. 5 │ * 6 │ * @exposes #mcp to #cmd-injection [high] cwe:CWE-78 -- "Accepts tool calls from external MCP clients" 7 │ * @audit #mcp -- "All tool calls validated by server.ts before execution" 8 │ * @flows MCPClient -> #mcp via stdio -- "MCP protocol transport" 9 │ * @boundary #mcp and MCPClient (#mcp-boundary) -- "Trust boundary at MCP protocol" 10 │ * @comment -- "D35: the `guardlink-mcp` bin executes this module directly, so it carries a shebang and a main guard. Startup errors go to stderr — stdout is the JSON-RPC channel and a stray line there corrupts the transport." 11 │ */ 12 │ 13 │ export { createServer } from './server.js';
L9 boundary #mcp ↔ MCPClient
Trust boundary at MCP protocol
4 │ * GuardLink MCP Server — exports and stdio entry point. 5 │ * 6 │ * @exposes #mcp to #cmd-injection [high] cwe:CWE-78 -- "Accepts tool calls from external MCP clients" 7 │ * @audit #mcp -- "All tool calls validated by server.ts before execution" 8 │ * @flows MCPClient -> #mcp via stdio -- "MCP protocol transport" 9 │ * @boundary #mcp and MCPClient (#mcp-boundary) -- "Trust boundary at MCP protocol" 10 │ * @comment -- "D35: the `guardlink-mcp` bin executes this module directly, so it carries a shebang and a main guard. Startup errors go to stderr — stdout is the JSON-RPC channel and a stray line there corrupts the transport." 11 │ */ 12 │ 13 │ export { createServer } from './server.js'; 14 │ export { lookup, type LookupResult } from './lookup.js';
L10 comment D35: the `guardlink-mcp` bin executes this module directly, so it carries a shebang and a main guard. Startup errors go to stderr — stdout is the JSON-RPC channel and a stray line there corrupts the transport.
D35: the `guardlink-mcp` bin executes this module directly, so it carries a shebang and a main guard. Startup errors go to stderr — stdout is the JSON-RPC channel and a stray line there corrupts the transport.
5 │ * 6 │ * @exposes #mcp to #cmd-injection [high] cwe:CWE-78 -- "Accepts tool calls from external MCP clients" 7 │ * @audit #mcp -- "All tool calls validated by server.ts before execution" 8 │ * @flows MCPClient -> #mcp via stdio -- "MCP protocol transport" 9 │ * @boundary #mcp and MCPClient (#mcp-boundary) -- "Trust boundary at MCP protocol" 10 │ * @comment -- "D35: the `guardlink-mcp` bin executes this module directly, so it carries a shebang and a main guard. Startup errors go to stderr — stdout is the JSON-RPC channel and a stray line there corrupts the transport." 11 │ */ 12 │ 13 │ export { createServer } from './server.js'; 14 │ export { lookup, type LookupResult } from './lookup.js'; 15 │ export { suggestAnnotations, type Suggestion, type SuggestOptions } from './suggest.js';
src/mcp/server.ts 2 openmedium10 stale exposes 6flow 5mitigates 4comment 3 open 24
L33 exposes #mcp → #path-traversal
Tool arguments include 'root' directory path from external client
28 │ * guardlink://definitions — Assets, threats, controls 29 │ * guardlink://unmitigated — Unmitigated exposures list 30 │ * 31 │ * Transport: stdio (for Claude Code .mcp.json, Cursor, etc.) 32 │ * 33 │ * @exposes #mcp to #path-traversal [high] cwe:CWE-22 -- "Tool arguments include 'root' directory path from external client" 34 │ * @mitigates #mcp against #path-traversal using #path-validation -- "Zod schema validates root; resolve() canonicalizes" 35 │ * @exposes #mcp to #arbitrary-write [high] cwe:CWE-73 -- "report, dashboard, sarif tools write files" 36 │ * @mitigates #mcp against #arbitrary-write using #path-validation -- "Output paths resolved relative to validated root" 37 │ * @exposes #mcp to #prompt-injection [medium] cwe:CWE-77 -- "annotate and threat_report tools pass user prompts to LLM" 38 │ * @audit #mcp -- "User prompts passed to LLM; model context is read-only"
L34 mitigates #path-validation mitigates #path-traversal
Zod schema validates root; resolve() canonicalizes
29 │ * guardlink://unmitigated — Unmitigated exposures list 30 │ * 31 │ * Transport: stdio (for Claude Code .mcp.json, Cursor, etc.) 32 │ * 33 │ * @exposes #mcp to #path-traversal [high] cwe:CWE-22 -- "Tool arguments include 'root' directory path from external client" 34 │ * @mitigates #mcp against #path-traversal using #path-validation -- "Zod schema validates root; resolve() canonicalizes" 35 │ * @exposes #mcp to #arbitrary-write [high] cwe:CWE-73 -- "report, dashboard, sarif tools write files" 36 │ * @mitigates #mcp against #arbitrary-write using #path-validation -- "Output paths resolved relative to validated root" 37 │ * @exposes #mcp to #prompt-injection [medium] cwe:CWE-77 -- "annotate and threat_report tools pass user prompts to LLM" 38 │ * @audit #mcp -- "User prompts passed to LLM; model context is read-only" 39 │ * @exposes #mcp to #api-key-exposure [medium] cwe:CWE-798 -- "threat_report tool uses API keys from environment"
L35 exposes #mcp → #arbitrary-write
report, dashboard, sarif tools write files
30 │ * 31 │ * Transport: stdio (for Claude Code .mcp.json, Cursor, etc.) 32 │ * 33 │ * @exposes #mcp to #path-traversal [high] cwe:CWE-22 -- "Tool arguments include 'root' directory path from external client" 34 │ * @mitigates #mcp against #path-traversal using #path-validation -- "Zod schema validates root; resolve() canonicalizes" 35 │ * @exposes #mcp to #arbitrary-write [high] cwe:CWE-73 -- "report, dashboard, sarif tools write files" 36 │ * @mitigates #mcp against #arbitrary-write using #path-validation -- "Output paths resolved relative to validated root" 37 │ * @exposes #mcp to #prompt-injection [medium] cwe:CWE-77 -- "annotate and threat_report tools pass user prompts to LLM" 38 │ * @audit #mcp -- "User prompts passed to LLM; model context is read-only" 39 │ * @exposes #mcp to #api-key-exposure [medium] cwe:CWE-798 -- "threat_report tool uses API keys from environment" 40 │ * @mitigates #mcp against #api-key-exposure using #key-redaction -- "Keys from env only; never logged or returned"
L36 mitigates #path-validation mitigates #arbitrary-write
Output paths resolved relative to validated root
31 │ * Transport: stdio (for Claude Code .mcp.json, Cursor, etc.) 32 │ * 33 │ * @exposes #mcp to #path-traversal [high] cwe:CWE-22 -- "Tool arguments include 'root' directory path from external client" 34 │ * @mitigates #mcp against #path-traversal using #path-validation -- "Zod schema validates root; resolve() canonicalizes" 35 │ * @exposes #mcp to #arbitrary-write [high] cwe:CWE-73 -- "report, dashboard, sarif tools write files" 36 │ * @mitigates #mcp against #arbitrary-write using #path-validation -- "Output paths resolved relative to validated root" 37 │ * @exposes #mcp to #prompt-injection [medium] cwe:CWE-77 -- "annotate and threat_report tools pass user prompts to LLM" 38 │ * @audit #mcp -- "User prompts passed to LLM; model context is read-only" 39 │ * @exposes #mcp to #api-key-exposure [medium] cwe:CWE-798 -- "threat_report tool uses API keys from environment" 40 │ * @mitigates #mcp against #api-key-exposure using #key-redaction -- "Keys from env only; never logged or returned" 41 │ * @exposes #mcp to #data-exposure [medium] cwe:CWE-200 -- "Resources expose full threat model to MCP clients"
L37 exposes #mcp → #prompt-injection
annotate and threat_report tools pass user prompts to LLM
32 │ * 33 │ * @exposes #mcp to #path-traversal [high] cwe:CWE-22 -- "Tool arguments include 'root' directory path from external client" 34 │ * @mitigates #mcp against #path-traversal using #path-validation -- "Zod schema validates root; resolve() canonicalizes" 35 │ * @exposes #mcp to #arbitrary-write [high] cwe:CWE-73 -- "report, dashboard, sarif tools write files" 36 │ * @mitigates #mcp against #arbitrary-write using #path-validation -- "Output paths resolved relative to validated root" 37 │ * @exposes #mcp to #prompt-injection [medium] cwe:CWE-77 -- "annotate and threat_report tools pass user prompts to LLM" 38 │ * @audit #mcp -- "User prompts passed to LLM; model context is read-only" 39 │ * @exposes #mcp to #api-key-exposure [medium] cwe:CWE-798 -- "threat_report tool uses API keys from environment" 40 │ * @mitigates #mcp against #api-key-exposure using #key-redaction -- "Keys from env only; never logged or returned" 41 │ * @exposes #mcp to #data-exposure [medium] cwe:CWE-200 -- "Resources expose full threat model to MCP clients" 42 │ * @audit #mcp -- "Threat model data intentionally exposed to connected agents"
L38 audit Audit: #mcp
User prompts passed to LLM; model context is read-only
33 │ * @exposes #mcp to #path-traversal [high] cwe:CWE-22 -- "Tool arguments include 'root' directory path from external client" 34 │ * @mitigates #mcp against #path-traversal using #path-validation -- "Zod schema validates root; resolve() canonicalizes" 35 │ * @exposes #mcp to #arbitrary-write [high] cwe:CWE-73 -- "report, dashboard, sarif tools write files" 36 │ * @mitigates #mcp against #arbitrary-write using #path-validation -- "Output paths resolved relative to validated root" 37 │ * @exposes #mcp to #prompt-injection [medium] cwe:CWE-77 -- "annotate and threat_report tools pass user prompts to LLM" 38 │ * @audit #mcp -- "User prompts passed to LLM; model context is read-only" 39 │ * @exposes #mcp to #api-key-exposure [medium] cwe:CWE-798 -- "threat_report tool uses API keys from environment" 40 │ * @mitigates #mcp against #api-key-exposure using #key-redaction -- "Keys from env only; never logged or returned" 41 │ * @exposes #mcp to #data-exposure [medium] cwe:CWE-200 -- "Resources expose full threat model to MCP clients" 42 │ * @audit #mcp -- "Threat model data intentionally exposed to connected agents" 43 │ * @exposes #mcp to #arbitrary-write [medium] cwe:CWE-73 -- "guardlink_entitlement_propose writes a client-supplied claim into .guardlink/entitlement-proposals.json"
L39 exposes #mcp → #api-key-exposure
threat_report tool uses API keys from environment
34 │ * @mitigates #mcp against #path-traversal using #path-validation -- "Zod schema validates root; resolve() canonicalizes" 35 │ * @exposes #mcp to #arbitrary-write [high] cwe:CWE-73 -- "report, dashboard, sarif tools write files" 36 │ * @mitigates #mcp against #arbitrary-write using #path-validation -- "Output paths resolved relative to validated root" 37 │ * @exposes #mcp to #prompt-injection [medium] cwe:CWE-77 -- "annotate and threat_report tools pass user prompts to LLM" 38 │ * @audit #mcp -- "User prompts passed to LLM; model context is read-only" 39 │ * @exposes #mcp to #api-key-exposure [medium] cwe:CWE-798 -- "threat_report tool uses API keys from environment" 40 │ * @mitigates #mcp against #api-key-exposure using #key-redaction -- "Keys from env only; never logged or returned" 41 │ * @exposes #mcp to #data-exposure [medium] cwe:CWE-200 -- "Resources expose full threat model to MCP clients" 42 │ * @audit #mcp -- "Threat model data intentionally exposed to connected agents" 43 │ * @exposes #mcp to #arbitrary-write [medium] cwe:CWE-73 -- "guardlink_entitlement_propose writes a client-supplied claim into .guardlink/entitlement-proposals.json" 44 │ * @mitigates #mcp against #arbitrary-write using #path-validation -- "The proposal artifact path is fixed, and its target file must resolve inside the project root — a proposal never writes to source"
L40 mitigates #key-redaction mitigates #api-key-exposure
Keys from env only; never logged or returned
35 │ * @exposes #mcp to #arbitrary-write [high] cwe:CWE-73 -- "report, dashboard, sarif tools write files" 36 │ * @mitigates #mcp against #arbitrary-write using #path-validation -- "Output paths resolved relative to validated root" 37 │ * @exposes #mcp to #prompt-injection [medium] cwe:CWE-77 -- "annotate and threat_report tools pass user prompts to LLM" 38 │ * @audit #mcp -- "User prompts passed to LLM; model context is read-only" 39 │ * @exposes #mcp to #api-key-exposure [medium] cwe:CWE-798 -- "threat_report tool uses API keys from environment" 40 │ * @mitigates #mcp against #api-key-exposure using #key-redaction -- "Keys from env only; never logged or returned" 41 │ * @exposes #mcp to #data-exposure [medium] cwe:CWE-200 -- "Resources expose full threat model to MCP clients" 42 │ * @audit #mcp -- "Threat model data intentionally exposed to connected agents" 43 │ * @exposes #mcp to #arbitrary-write [medium] cwe:CWE-73 -- "guardlink_entitlement_propose writes a client-supplied claim into .guardlink/entitlement-proposals.json" 44 │ * @mitigates #mcp against #arbitrary-write using #path-validation -- "The proposal artifact path is fixed, and its target file must resolve inside the project root — a proposal never writes to source" 45 │ * @comment -- "No guardlink_entitlement_accept tool exists on purpose: proposing is an agent's job, granting authority is not (actor-entitlement design §3.6)"
L41 exposes #mcp → #data-exposure
Resources expose full threat model to MCP clients
36 │ * @mitigates #mcp against #arbitrary-write using #path-validation -- "Output paths resolved relative to validated root" 37 │ * @exposes #mcp to #prompt-injection [medium] cwe:CWE-77 -- "annotate and threat_report tools pass user prompts to LLM" 38 │ * @audit #mcp -- "User prompts passed to LLM; model context is read-only" 39 │ * @exposes #mcp to #api-key-exposure [medium] cwe:CWE-798 -- "threat_report tool uses API keys from environment" 40 │ * @mitigates #mcp against #api-key-exposure using #key-redaction -- "Keys from env only; never logged or returned" 41 │ * @exposes #mcp to #data-exposure [medium] cwe:CWE-200 -- "Resources expose full threat model to MCP clients" 42 │ * @audit #mcp -- "Threat model data intentionally exposed to connected agents" 43 │ * @exposes #mcp to #arbitrary-write [medium] cwe:CWE-73 -- "guardlink_entitlement_propose writes a client-supplied claim into .guardlink/entitlement-proposals.json" 44 │ * @mitigates #mcp against #arbitrary-write using #path-validation -- "The proposal artifact path is fixed, and its target file must resolve inside the project root — a proposal never writes to source" 45 │ * @comment -- "No guardlink_entitlement_accept tool exists on purpose: proposing is an agent's job, granting authority is not (actor-entitlement design §3.6)" 46 │ * @flows MCPClient -> #mcp via tool_call -- "Tool invocation input"
L42 audit Audit: #mcp
Threat model data intentionally exposed to connected agents
37 │ * @exposes #mcp to #prompt-injection [medium] cwe:CWE-77 -- "annotate and threat_report tools pass user prompts to LLM" 38 │ * @audit #mcp -- "User prompts passed to LLM; model context is read-only" 39 │ * @exposes #mcp to #api-key-exposure [medium] cwe:CWE-798 -- "threat_report tool uses API keys from environment" 40 │ * @mitigates #mcp against #api-key-exposure using #key-redaction -- "Keys from env only; never logged or returned" 41 │ * @exposes #mcp to #data-exposure [medium] cwe:CWE-200 -- "Resources expose full threat model to MCP clients" 42 │ * @audit #mcp -- "Threat model data intentionally exposed to connected agents" 43 │ * @exposes #mcp to #arbitrary-write [medium] cwe:CWE-73 -- "guardlink_entitlement_propose writes a client-supplied claim into .guardlink/entitlement-proposals.json" 44 │ * @mitigates #mcp against #arbitrary-write using #path-validation -- "The proposal artifact path is fixed, and its target file must resolve inside the project root — a proposal never writes to source" 45 │ * @comment -- "No guardlink_entitlement_accept tool exists on purpose: proposing is an agent's job, granting authority is not (actor-entitlement design §3.6)" 46 │ * @flows MCPClient -> #mcp via tool_call -- "Tool invocation input" 47 │ * @flows #mcp -> FileSystem via writeFile -- "Report/dashboard output"
L43 exposes #mcp → #arbitrary-write
guardlink_entitlement_propose writes a client-supplied claim into .guardlink/entitlement-proposals.json
38 │ * @audit #mcp -- "User prompts passed to LLM; model context is read-only" 39 │ * @exposes #mcp to #api-key-exposure [medium] cwe:CWE-798 -- "threat_report tool uses API keys from environment" 40 │ * @mitigates #mcp against #api-key-exposure using #key-redaction -- "Keys from env only; never logged or returned" 41 │ * @exposes #mcp to #data-exposure [medium] cwe:CWE-200 -- "Resources expose full threat model to MCP clients" 42 │ * @audit #mcp -- "Threat model data intentionally exposed to connected agents" 43 │ * @exposes #mcp to #arbitrary-write [medium] cwe:CWE-73 -- "guardlink_entitlement_propose writes a client-supplied claim into .guardlink/entitlement-proposals.json" 44 │ * @mitigates #mcp against #arbitrary-write using #path-validation -- "The proposal artifact path is fixed, and its target file must resolve inside the project root — a proposal never writes to source" 45 │ * @comment -- "No guardlink_entitlement_accept tool exists on purpose: proposing is an agent's job, granting authority is not (actor-entitlement design §3.6)" 46 │ * @flows MCPClient -> #mcp via tool_call -- "Tool invocation input" 47 │ * @flows #mcp -> FileSystem via writeFile -- "Report/dashboard output" 48 │ * @flows #mcp -> #llm-client via generateThreatReport -- "LLM API call path"
L44 mitigates #path-validation mitigates #arbitrary-write
The proposal artifact path is fixed, and its target file must resolve inside the project root — a proposal never writes to source
39 │ * @exposes #mcp to #api-key-exposure [medium] cwe:CWE-798 -- "threat_report tool uses API keys from environment" 40 │ * @mitigates #mcp against #api-key-exposure using #key-redaction -- "Keys from env only; never logged or returned" 41 │ * @exposes #mcp to #data-exposure [medium] cwe:CWE-200 -- "Resources expose full threat model to MCP clients" 42 │ * @audit #mcp -- "Threat model data intentionally exposed to connected agents" 43 │ * @exposes #mcp to #arbitrary-write [medium] cwe:CWE-73 -- "guardlink_entitlement_propose writes a client-supplied claim into .guardlink/entitlement-proposals.json" 44 │ * @mitigates #mcp against #arbitrary-write using #path-validation -- "The proposal artifact path is fixed, and its target file must resolve inside the project root — a proposal never writes to source" 45 │ * @comment -- "No guardlink_entitlement_accept tool exists on purpose: proposing is an agent's job, granting authority is not (actor-entitlement design §3.6)" 46 │ * @flows MCPClient -> #mcp via tool_call -- "Tool invocation input" 47 │ * @flows #mcp -> FileSystem via writeFile -- "Report/dashboard output" 48 │ * @flows #mcp -> #llm-client via generateThreatReport -- "LLM API call path" 49 │ * @flows #mcp -> MCPClient via resource -- "Threat model data output"
L45 comment No guardlink_entitlement_accept tool exists on purpose: proposing is an agent's job, granting authority is not (actor-entitlement design §3.6)
No guardlink_entitlement_accept tool exists on purpose: proposing is an agent's job, granting authority is not (actor-entitlement design §3.6)
40 │ * @mitigates #mcp against #api-key-exposure using #key-redaction -- "Keys from env only; never logged or returned" 41 │ * @exposes #mcp to #data-exposure [medium] cwe:CWE-200 -- "Resources expose full threat model to MCP clients" 42 │ * @audit #mcp -- "Threat model data intentionally exposed to connected agents" 43 │ * @exposes #mcp to #arbitrary-write [medium] cwe:CWE-73 -- "guardlink_entitlement_propose writes a client-supplied claim into .guardlink/entitlement-proposals.json" 44 │ * @mitigates #mcp against #arbitrary-write using #path-validation -- "The proposal artifact path is fixed, and its target file must resolve inside the project root — a proposal never writes to source" 45 │ * @comment -- "No guardlink_entitlement_accept tool exists on purpose: proposing is an agent's job, granting authority is not (actor-entitlement design §3.6)" 46 │ * @flows MCPClient -> #mcp via tool_call -- "Tool invocation input" 47 │ * @flows #mcp -> FileSystem via writeFile -- "Report/dashboard output" 48 │ * @flows #mcp -> #llm-client via generateThreatReport -- "LLM API call path" 49 │ * @flows #mcp -> MCPClient via resource -- "Threat model data output" 50 │ * @boundary #mcp and MCPClient (#mcp-tool-boundary) -- "Trust boundary at tool argument parsing"
L46 flow MCPClient → #mcp
Tool invocation input
41 │ * @exposes #mcp to #data-exposure [medium] cwe:CWE-200 -- "Resources expose full threat model to MCP clients" 42 │ * @audit #mcp -- "Threat model data intentionally exposed to connected agents" 43 │ * @exposes #mcp to #arbitrary-write [medium] cwe:CWE-73 -- "guardlink_entitlement_propose writes a client-supplied claim into .guardlink/entitlement-proposals.json" 44 │ * @mitigates #mcp against #arbitrary-write using #path-validation -- "The proposal artifact path is fixed, and its target file must resolve inside the project root — a proposal never writes to source" 45 │ * @comment -- "No guardlink_entitlement_accept tool exists on purpose: proposing is an agent's job, granting authority is not (actor-entitlement design §3.6)" 46 │ * @flows MCPClient -> #mcp via tool_call -- "Tool invocation input" 47 │ * @flows #mcp -> FileSystem via writeFile -- "Report/dashboard output" 48 │ * @flows #mcp -> #llm-client via generateThreatReport -- "LLM API call path" 49 │ * @flows #mcp -> MCPClient via resource -- "Threat model data output" 50 │ * @boundary #mcp and MCPClient (#mcp-tool-boundary) -- "Trust boundary at tool argument parsing" 51 │ * @handles internal on #mcp -- "Processes project annotations and threat model data"
L47 flow #mcp → FileSystem
Report/dashboard output
42 │ * @audit #mcp -- "Threat model data intentionally exposed to connected agents" 43 │ * @exposes #mcp to #arbitrary-write [medium] cwe:CWE-73 -- "guardlink_entitlement_propose writes a client-supplied claim into .guardlink/entitlement-proposals.json" 44 │ * @mitigates #mcp against #arbitrary-write using #path-validation -- "The proposal artifact path is fixed, and its target file must resolve inside the project root — a proposal never writes to source" 45 │ * @comment -- "No guardlink_entitlement_accept tool exists on purpose: proposing is an agent's job, granting authority is not (actor-entitlement design §3.6)" 46 │ * @flows MCPClient -> #mcp via tool_call -- "Tool invocation input" 47 │ * @flows #mcp -> FileSystem via writeFile -- "Report/dashboard output" 48 │ * @flows #mcp -> #llm-client via generateThreatReport -- "LLM API call path" 49 │ * @flows #mcp -> MCPClient via resource -- "Threat model data output" 50 │ * @boundary #mcp and MCPClient (#mcp-tool-boundary) -- "Trust boundary at tool argument parsing" 51 │ * @handles internal on #mcp -- "Processes project annotations and threat model data" 52 │ * @feature "MCP Integration" -- "Model Context Protocol server for AI agent tooling"
L48 flow #mcp → #llm-client
LLM API call path
43 │ * @exposes #mcp to #arbitrary-write [medium] cwe:CWE-73 -- "guardlink_entitlement_propose writes a client-supplied claim into .guardlink/entitlement-proposals.json" 44 │ * @mitigates #mcp against #arbitrary-write using #path-validation -- "The proposal artifact path is fixed, and its target file must resolve inside the project root — a proposal never writes to source" 45 │ * @comment -- "No guardlink_entitlement_accept tool exists on purpose: proposing is an agent's job, granting authority is not (actor-entitlement design §3.6)" 46 │ * @flows MCPClient -> #mcp via tool_call -- "Tool invocation input" 47 │ * @flows #mcp -> FileSystem via writeFile -- "Report/dashboard output" 48 │ * @flows #mcp -> #llm-client via generateThreatReport -- "LLM API call path" 49 │ * @flows #mcp -> MCPClient via resource -- "Threat model data output" 50 │ * @boundary #mcp and MCPClient (#mcp-tool-boundary) -- "Trust boundary at tool argument parsing" 51 │ * @handles internal on #mcp -- "Processes project annotations and threat model data" 52 │ * @feature "MCP Integration" -- "Model Context Protocol server for AI agent tooling" 53 │ * @entitles #mcp-agent to read-threat-model on #mcp against #data-exposure -- "By design: guardlink mcp exists to hand a connected coding agent the threat model — that disclosure is the product, not a leak. No privilege gain either: the agent already reads the annotated source these records are parsed from, so the assembled model tells it nothing it could not derive itself. Authorization is the channel: the server is stdio-only with no network listener, so the only client is the process the operator launched, at src/mcp/index.ts:23"
L49 flow #mcp → MCPClient
Threat model data output
44 │ * @mitigates #mcp against #arbitrary-write using #path-validation -- "The proposal artifact path is fixed, and its target file must resolve inside the project root — a proposal never writes to source" 45 │ * @comment -- "No guardlink_entitlement_accept tool exists on purpose: proposing is an agent's job, granting authority is not (actor-entitlement design §3.6)" 46 │ * @flows MCPClient -> #mcp via tool_call -- "Tool invocation input" 47 │ * @flows #mcp -> FileSystem via writeFile -- "Report/dashboard output" 48 │ * @flows #mcp -> #llm-client via generateThreatReport -- "LLM API call path" 49 │ * @flows #mcp -> MCPClient via resource -- "Threat model data output" 50 │ * @boundary #mcp and MCPClient (#mcp-tool-boundary) -- "Trust boundary at tool argument parsing" 51 │ * @handles internal on #mcp -- "Processes project annotations and threat model data" 52 │ * @feature "MCP Integration" -- "Model Context Protocol server for AI agent tooling" 53 │ * @entitles #mcp-agent to read-threat-model on #mcp against #data-exposure -- "By design: guardlink mcp exists to hand a connected coding agent the threat model — that disclosure is the product, not a leak. No privilege gain either: the agent already reads the annotated source these records are parsed from, so the assembled model tells it nothing it could not derive itself. Authorization is the channel: the server is stdio-only with no network listener, so the only client is the process the operator launched, at src/mcp/index.ts:23" 54 │ * @comment -- "Entitlement accepted by zippon on 2026-08-10 via guardlink entitle (proposal ent-mcp_agent.mcp.data_exposure)."
L50 boundary #mcp ↔ MCPClient
Trust boundary at tool argument parsing
45 │ * @comment -- "No guardlink_entitlement_accept tool exists on purpose: proposing is an agent's job, granting authority is not (actor-entitlement design §3.6)" 46 │ * @flows MCPClient -> #mcp via tool_call -- "Tool invocation input" 47 │ * @flows #mcp -> FileSystem via writeFile -- "Report/dashboard output" 48 │ * @flows #mcp -> #llm-client via generateThreatReport -- "LLM API call path" 49 │ * @flows #mcp -> MCPClient via resource -- "Threat model data output" 50 │ * @boundary #mcp and MCPClient (#mcp-tool-boundary) -- "Trust boundary at tool argument parsing" 51 │ * @handles internal on #mcp -- "Processes project annotations and threat model data" 52 │ * @feature "MCP Integration" -- "Model Context Protocol server for AI agent tooling" 53 │ * @entitles #mcp-agent to read-threat-model on #mcp against #data-exposure -- "By design: guardlink mcp exists to hand a connected coding agent the threat model — that disclosure is the product, not a leak. No privilege gain either: the agent already reads the annotated source these records are parsed from, so the assembled model tells it nothing it could not derive itself. Authorization is the channel: the server is stdio-only with no network listener, so the only client is the process the operator launched, at src/mcp/index.ts:23" 54 │ * @comment -- "Entitlement accepted by zippon on 2026-08-10 via guardlink entitle (proposal ent-mcp_agent.mcp.data_exposure)." 55 │ */
L51 handles #mcp: internal
Processes project annotations and threat model data
46 │ * @flows MCPClient -> #mcp via tool_call -- "Tool invocation input" 47 │ * @flows #mcp -> FileSystem via writeFile -- "Report/dashboard output" 48 │ * @flows #mcp -> #llm-client via generateThreatReport -- "LLM API call path" 49 │ * @flows #mcp -> MCPClient via resource -- "Threat model data output" 50 │ * @boundary #mcp and MCPClient (#mcp-tool-boundary) -- "Trust boundary at tool argument parsing" 51 │ * @handles internal on #mcp -- "Processes project annotations and threat model data" 52 │ * @feature "MCP Integration" -- "Model Context Protocol server for AI agent tooling" 53 │ * @entitles #mcp-agent to read-threat-model on #mcp against #data-exposure -- "By design: guardlink mcp exists to hand a connected coding agent the threat model — that disclosure is the product, not a leak. No privilege gain either: the agent already reads the annotated source these records are parsed from, so the assembled model tells it nothing it could not derive itself. Authorization is the channel: the server is stdio-only with no network listener, so the only client is the process the operator launched, at src/mcp/index.ts:23" 54 │ * @comment -- "Entitlement accepted by zippon on 2026-08-10 via guardlink entitle (proposal ent-mcp_agent.mcp.data_exposure)." 55 │ */ 56 │
L53 entitles #mcp-agent entitled to read-threat-model
By design: guardlink mcp exists to hand a connected coding agent the threat model — that disclosure is the product, not a leak. No privilege gain either: the agent already reads the annotated source these records are parsed from, so the assembled model tells it nothing it could not derive itself. Authorization is the channel: the server is stdio-only with no network listener, so the only client is the process the operator launched, at src/mcp/index.ts:23
48 │ * @flows #mcp -> #llm-client via generateThreatReport -- "LLM API call path" 49 │ * @flows #mcp -> MCPClient via resource -- "Threat model data output" 50 │ * @boundary #mcp and MCPClient (#mcp-tool-boundary) -- "Trust boundary at tool argument parsing" 51 │ * @handles internal on #mcp -- "Processes project annotations and threat model data" 52 │ * @feature "MCP Integration" -- "Model Context Protocol server for AI agent tooling" 53 │ * @entitles #mcp-agent to read-threat-model on #mcp against #data-exposure -- "By design: guardlink mcp exists to hand a connected coding agent the threat model — that disclosure is the product, not a leak. No privilege gain either: the agent already reads the annotated source these records are parsed from, so the assembled model tells it nothing it could not derive itself. Authorization is the channel: the server is stdio-only with no network listener, so the only client is the process the operator launched, at src/mcp/index.ts:23" 54 │ * @comment -- "Entitlement accepted by zippon on 2026-08-10 via guardlink entitle (proposal ent-mcp_agent.mcp.data_exposure)." 55 │ */ 56 │ 57 │ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 58 │ import { z } from 'zod';
L54 comment Entitlement accepted by zippon on 2026-08-10 via guardlink entitle (proposal ent-mcp_agent.mcp.data_exposure).
Entitlement accepted by zippon on 2026-08-10 via guardlink entitle (proposal ent-mcp_agent.mcp.data_exposure).
49 │ * @flows #mcp -> MCPClient via resource -- "Threat model data output" 50 │ * @boundary #mcp and MCPClient (#mcp-tool-boundary) -- "Trust boundary at tool argument parsing" 51 │ * @handles internal on #mcp -- "Processes project annotations and threat model data" 52 │ * @feature "MCP Integration" -- "Model Context Protocol server for AI agent tooling" 53 │ * @entitles #mcp-agent to read-threat-model on #mcp against #data-exposure -- "By design: guardlink mcp exists to hand a connected coding agent the threat model — that disclosure is the product, not a leak. No privilege gain either: the agent already reads the annotated source these records are parsed from, so the assembled model tells it nothing it could not derive itself. Authorization is the channel: the server is stdio-only with no network listener, so the only client is the process the operator launched, at src/mcp/index.ts:23" 54 │ * @comment -- "Entitlement accepted by zippon on 2026-08-10 via guardlink entitle (proposal ent-mcp_agent.mcp.data_exposure)." 55 │ */ 56 │ 57 │ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; 58 │ import { z } from 'zod'; 59 │ // MERGE: main added the entitlement validators and the proposal module; ours
L1113 flow GitRepo → #mcp
Attribution read from blame, log -L and commit trailers for the connected agent
1108 │ },1109 │ );1110 │ 1111 │ // ── Tool: guardlink_blame ──1112 │ /**1113 │ * @flows GitRepo -> #mcp via computeBlame -- "Attribution read from blame, log -L and commit trailers for the connected agent"1114 │ * @handles pii on #mcp -- "Author identities, in the configured identity mode (blame.identity in config.json), reach the MCP client"1115 │ * @comment -- "Non-mutating: computeBlame leaves the cached model untouched, so a later guardlink_parse in the same session never carries blame unasked. Nothing is written to the repository"1116 │ */1117 │ registerTool(1118 │ server, cache,
L1114 handles #mcp: pii
Author identities, in the configured identity mode (blame.identity in config.json), reach the MCP client
1109 │ );1110 │ 1111 │ // ── Tool: guardlink_blame ──1112 │ /**1113 │ * @flows GitRepo -> #mcp via computeBlame -- "Attribution read from blame, log -L and commit trailers for the connected agent"1114 │ * @handles pii on #mcp -- "Author identities, in the configured identity mode (blame.identity in config.json), reach the MCP client"1115 │ * @comment -- "Non-mutating: computeBlame leaves the cached model untouched, so a later guardlink_parse in the same session never carries blame unasked. Nothing is written to the repository"1116 │ */1117 │ registerTool(1118 │ server, cache,1119 │ 'guardlink_blame',
L1115 comment Non-mutating: computeBlame leaves the cached model untouched, so a later guardlink_parse in the same session never carries blame unasked. Nothing is written to the repository
Non-mutating: computeBlame leaves the cached model untouched, so a later guardlink_parse in the same session never carries blame unasked. Nothing is written to the repository
1110 │ 1111 │ // ── Tool: guardlink_blame ──1112 │ /**1113 │ * @flows GitRepo -> #mcp via computeBlame -- "Attribution read from blame, log -L and commit trailers for the connected agent"1114 │ * @handles pii on #mcp -- "Author identities, in the configured identity mode (blame.identity in config.json), reach the MCP client"1115 │ * @comment -- "Non-mutating: computeBlame leaves the cached model untouched, so a later guardlink_parse in the same session never carries blame unasked. Nothing is written to the repository"1116 │ */1117 │ registerTool(1118 │ server, cache,1119 │ 'guardlink_blame',1120 │ 'Who introduced the code beneath each claim, who declared it, who declared its fix, and which AI tool co-authored those commits — read from git (blame, log -L, Co-Authored-By / Assisted-by trailers); nothing is written. AI credit is DECLARED by commit trailers, never detected from code: a commit with no trailer stays human. Pass `file` to narrow to one file. Entries carry the ledger claim key, so they join to .guardlink/verified.json. Outside a git checkout every entry is status no-git; uncommitted means the line has changes git has not seen; shallow means every introduction is a lower bound.',
src/agents/launcher.ts 1 openmedium exposes 3flow 3mitigates 2audit 2 open 11
L10 exposes #agent-launcher → #child-proc-injection
spawn/spawnSync execute external binaries
5 │ * 1. Foreground spawn (CLI + TUI): takes over terminal, returns on exit 6 │ * 2. IDE launch: opens GUI app with project directory 7 │ * 8 │ * Clipboard copy is always performed first regardless of agent type. 9 │ * 10 │ * @exposes #agent-launcher to #child-proc-injection [critical] cwe:CWE-78 -- "spawn/spawnSync execute external binaries" 11 │ * @mitigates #agent-launcher against #child-proc-injection using #param-commands -- "Binary names from hardcoded AGENTS registry; no shell interpolation" 12 │ * @mitigates #agent-launcher against #cmd-injection using #param-commands -- "Arguments passed as array, not shell string" 13 │ * @exposes #agent-launcher to #prompt-injection [medium] cwe:CWE-77 -- "User prompt passed to agent CLI as argument" 14 │ * @audit #agent-launcher -- "Prompt content is opaque to agent binary; injection risk depends on agent implementation" 15 │ * @exposes #agent-launcher to #dos [low] cwe:CWE-400 -- "No timeout on foreground spawn; agent controls duration"
L11 mitigates #param-commands mitigates #child-proc-injection
Binary names from hardcoded AGENTS registry; no shell interpolation
6 │ * 2. IDE launch: opens GUI app with project directory 7 │ * 8 │ * Clipboard copy is always performed first regardless of agent type. 9 │ * 10 │ * @exposes #agent-launcher to #child-proc-injection [critical] cwe:CWE-78 -- "spawn/spawnSync execute external binaries" 11 │ * @mitigates #agent-launcher against #child-proc-injection using #param-commands -- "Binary names from hardcoded AGENTS registry; no shell interpolation" 12 │ * @mitigates #agent-launcher against #cmd-injection using #param-commands -- "Arguments passed as array, not shell string" 13 │ * @exposes #agent-launcher to #prompt-injection [medium] cwe:CWE-77 -- "User prompt passed to agent CLI as argument" 14 │ * @audit #agent-launcher -- "Prompt content is opaque to agent binary; injection risk depends on agent implementation" 15 │ * @exposes #agent-launcher to #dos [low] cwe:CWE-400 -- "No timeout on foreground spawn; agent controls duration" 16 │ * @audit #agent-launcher -- "Timeout intentionally omitted for interactive sessions; inline mode has implicit control"
L12 mitigates #param-commands mitigates #cmd-injection
Arguments passed as array, not shell string
7 │ * 8 │ * Clipboard copy is always performed first regardless of agent type. 9 │ * 10 │ * @exposes #agent-launcher to #child-proc-injection [critical] cwe:CWE-78 -- "spawn/spawnSync execute external binaries" 11 │ * @mitigates #agent-launcher against #child-proc-injection using #param-commands -- "Binary names from hardcoded AGENTS registry; no shell interpolation" 12 │ * @mitigates #agent-launcher against #cmd-injection using #param-commands -- "Arguments passed as array, not shell string" 13 │ * @exposes #agent-launcher to #prompt-injection [medium] cwe:CWE-77 -- "User prompt passed to agent CLI as argument" 14 │ * @audit #agent-launcher -- "Prompt content is opaque to agent binary; injection risk depends on agent implementation" 15 │ * @exposes #agent-launcher to #dos [low] cwe:CWE-400 -- "No timeout on foreground spawn; agent controls duration" 16 │ * @audit #agent-launcher -- "Timeout intentionally omitted for interactive sessions; inline mode has implicit control" 17 │ * @flows UserPrompt -> #agent-launcher via launchAgent -- "Prompt input path"
L13 exposes #agent-launcher → #prompt-injection
User prompt passed to agent CLI as argument
8 │ * Clipboard copy is always performed first regardless of agent type. 9 │ * 10 │ * @exposes #agent-launcher to #child-proc-injection [critical] cwe:CWE-78 -- "spawn/spawnSync execute external binaries" 11 │ * @mitigates #agent-launcher against #child-proc-injection using #param-commands -- "Binary names from hardcoded AGENTS registry; no shell interpolation" 12 │ * @mitigates #agent-launcher against #cmd-injection using #param-commands -- "Arguments passed as array, not shell string" 13 │ * @exposes #agent-launcher to #prompt-injection [medium] cwe:CWE-77 -- "User prompt passed to agent CLI as argument" 14 │ * @audit #agent-launcher -- "Prompt content is opaque to agent binary; injection risk depends on agent implementation" 15 │ * @exposes #agent-launcher to #dos [low] cwe:CWE-400 -- "No timeout on foreground spawn; agent controls duration" 16 │ * @audit #agent-launcher -- "Timeout intentionally omitted for interactive sessions; inline mode has implicit control" 17 │ * @flows UserPrompt -> #agent-launcher via launchAgent -- "Prompt input path" 18 │ * @flows #agent-launcher -> AgentProcess via spawn -- "Process spawn path"
L14 audit Audit: #agent-launcher
Prompt content is opaque to agent binary; injection risk depends on agent implementation
9 │ * 10 │ * @exposes #agent-launcher to #child-proc-injection [critical] cwe:CWE-78 -- "spawn/spawnSync execute external binaries" 11 │ * @mitigates #agent-launcher against #child-proc-injection using #param-commands -- "Binary names from hardcoded AGENTS registry; no shell interpolation" 12 │ * @mitigates #agent-launcher against #cmd-injection using #param-commands -- "Arguments passed as array, not shell string" 13 │ * @exposes #agent-launcher to #prompt-injection [medium] cwe:CWE-77 -- "User prompt passed to agent CLI as argument" 14 │ * @audit #agent-launcher -- "Prompt content is opaque to agent binary; injection risk depends on agent implementation" 15 │ * @exposes #agent-launcher to #dos [low] cwe:CWE-400 -- "No timeout on foreground spawn; agent controls duration" 16 │ * @audit #agent-launcher -- "Timeout intentionally omitted for interactive sessions; inline mode has implicit control" 17 │ * @flows UserPrompt -> #agent-launcher via launchAgent -- "Prompt input path" 18 │ * @flows #agent-launcher -> AgentProcess via spawn -- "Process spawn path" 19 │ * @flows AgentProcess -> #agent-launcher via stdout -- "Agent output capture"
L15 exposes #agent-launcher → #dos
No timeout on foreground spawn; agent controls duration
10 │ * @exposes #agent-launcher to #child-proc-injection [critical] cwe:CWE-78 -- "spawn/spawnSync execute external binaries" 11 │ * @mitigates #agent-launcher against #child-proc-injection using #param-commands -- "Binary names from hardcoded AGENTS registry; no shell interpolation" 12 │ * @mitigates #agent-launcher against #cmd-injection using #param-commands -- "Arguments passed as array, not shell string" 13 │ * @exposes #agent-launcher to #prompt-injection [medium] cwe:CWE-77 -- "User prompt passed to agent CLI as argument" 14 │ * @audit #agent-launcher -- "Prompt content is opaque to agent binary; injection risk depends on agent implementation" 15 │ * @exposes #agent-launcher to #dos [low] cwe:CWE-400 -- "No timeout on foreground spawn; agent controls duration" 16 │ * @audit #agent-launcher -- "Timeout intentionally omitted for interactive sessions; inline mode has implicit control" 17 │ * @flows UserPrompt -> #agent-launcher via launchAgent -- "Prompt input path" 18 │ * @flows #agent-launcher -> AgentProcess via spawn -- "Process spawn path" 19 │ * @flows AgentProcess -> #agent-launcher via stdout -- "Agent output capture" 20 │ * @boundary #agent-launcher and AgentProcess (#agent-boundary) -- "Trust boundary at process spawn"
L16 audit Audit: #agent-launcher
Timeout intentionally omitted for interactive sessions; inline mode has implicit control
11 │ * @mitigates #agent-launcher against #child-proc-injection using #param-commands -- "Binary names from hardcoded AGENTS registry; no shell interpolation" 12 │ * @mitigates #agent-launcher against #cmd-injection using #param-commands -- "Arguments passed as array, not shell string" 13 │ * @exposes #agent-launcher to #prompt-injection [medium] cwe:CWE-77 -- "User prompt passed to agent CLI as argument" 14 │ * @audit #agent-launcher -- "Prompt content is opaque to agent binary; injection risk depends on agent implementation" 15 │ * @exposes #agent-launcher to #dos [low] cwe:CWE-400 -- "No timeout on foreground spawn; agent controls duration" 16 │ * @audit #agent-launcher -- "Timeout intentionally omitted for interactive sessions; inline mode has implicit control" 17 │ * @flows UserPrompt -> #agent-launcher via launchAgent -- "Prompt input path" 18 │ * @flows #agent-launcher -> AgentProcess via spawn -- "Process spawn path" 19 │ * @flows AgentProcess -> #agent-launcher via stdout -- "Agent output capture" 20 │ * @boundary #agent-launcher and AgentProcess (#agent-boundary) -- "Trust boundary at process spawn" 21 │ */
L17 flow UserPrompt → #agent-launcher
Prompt input path
12 │ * @mitigates #agent-launcher against #cmd-injection using #param-commands -- "Arguments passed as array, not shell string" 13 │ * @exposes #agent-launcher to #prompt-injection [medium] cwe:CWE-77 -- "User prompt passed to agent CLI as argument" 14 │ * @audit #agent-launcher -- "Prompt content is opaque to agent binary; injection risk depends on agent implementation" 15 │ * @exposes #agent-launcher to #dos [low] cwe:CWE-400 -- "No timeout on foreground spawn; agent controls duration" 16 │ * @audit #agent-launcher -- "Timeout intentionally omitted for interactive sessions; inline mode has implicit control" 17 │ * @flows UserPrompt -> #agent-launcher via launchAgent -- "Prompt input path" 18 │ * @flows #agent-launcher -> AgentProcess via spawn -- "Process spawn path" 19 │ * @flows AgentProcess -> #agent-launcher via stdout -- "Agent output capture" 20 │ * @boundary #agent-launcher and AgentProcess (#agent-boundary) -- "Trust boundary at process spawn" 21 │ */ 22 │
L18 flow #agent-launcher → AgentProcess
Process spawn path
13 │ * @exposes #agent-launcher to #prompt-injection [medium] cwe:CWE-77 -- "User prompt passed to agent CLI as argument" 14 │ * @audit #agent-launcher -- "Prompt content is opaque to agent binary; injection risk depends on agent implementation" 15 │ * @exposes #agent-launcher to #dos [low] cwe:CWE-400 -- "No timeout on foreground spawn; agent controls duration" 16 │ * @audit #agent-launcher -- "Timeout intentionally omitted for interactive sessions; inline mode has implicit control" 17 │ * @flows UserPrompt -> #agent-launcher via launchAgent -- "Prompt input path" 18 │ * @flows #agent-launcher -> AgentProcess via spawn -- "Process spawn path" 19 │ * @flows AgentProcess -> #agent-launcher via stdout -- "Agent output capture" 20 │ * @boundary #agent-launcher and AgentProcess (#agent-boundary) -- "Trust boundary at process spawn" 21 │ */ 22 │ 23 │ import { spawnSync, spawn } from 'node:child_process';
L19 flow AgentProcess → #agent-launcher
Agent output capture
14 │ * @audit #agent-launcher -- "Prompt content is opaque to agent binary; injection risk depends on agent implementation" 15 │ * @exposes #agent-launcher to #dos [low] cwe:CWE-400 -- "No timeout on foreground spawn; agent controls duration" 16 │ * @audit #agent-launcher -- "Timeout intentionally omitted for interactive sessions; inline mode has implicit control" 17 │ * @flows UserPrompt -> #agent-launcher via launchAgent -- "Prompt input path" 18 │ * @flows #agent-launcher -> AgentProcess via spawn -- "Process spawn path" 19 │ * @flows AgentProcess -> #agent-launcher via stdout -- "Agent output capture" 20 │ * @boundary #agent-launcher and AgentProcess (#agent-boundary) -- "Trust boundary at process spawn" 21 │ */ 22 │ 23 │ import { spawnSync, spawn } from 'node:child_process'; 24 │ import { platform } from 'node:os';
L20 boundary #agent-launcher ↔ AgentProcess
Trust boundary at process spawn
15 │ * @exposes #agent-launcher to #dos [low] cwe:CWE-400 -- "No timeout on foreground spawn; agent controls duration" 16 │ * @audit #agent-launcher -- "Timeout intentionally omitted for interactive sessions; inline mode has implicit control" 17 │ * @flows UserPrompt -> #agent-launcher via launchAgent -- "Prompt input path" 18 │ * @flows #agent-launcher -> AgentProcess via spawn -- "Process spawn path" 19 │ * @flows AgentProcess -> #agent-launcher via stdout -- "Agent output capture" 20 │ * @boundary #agent-launcher and AgentProcess (#agent-boundary) -- "Trust boundary at process spawn" 21 │ */ 22 │ 23 │ import { spawnSync, spawn } from 'node:child_process'; 24 │ import { platform } from 'node:os'; 25 │ import { mkdtempSync, readFileSync, unlinkSync, existsSync } from 'node:fs';
src/analyze/llm.ts 1 openmedium exposes 3flow 3mitigates 2audit 1 open 11
L13 exposes #llm-client → #ssrf
fetch() calls external LLM API endpoints
8 │ * - OpenAI-compatible Chat Completions (DeepSeek, OpenRouter, Ollama) 9 │ * - DeepSeek reasoning mode (deepseek-reasoner) 10 │ * 11 │ * Zero dependencies — uses Node 20+ built-in fetch. 12 │ * 13 │ * @exposes #llm-client to #ssrf [medium] cwe:CWE-918 -- "fetch() calls external LLM API endpoints" 14 │ * @mitigates #llm-client against #ssrf using #config-validation -- "BASE_URLS are hardcoded; baseUrl override is optional config" 15 │ * @exposes #llm-client to #api-key-exposure [high] cwe:CWE-798 -- "API keys passed in Authorization headers" 16 │ * @mitigates #llm-client against #api-key-exposure using #key-redaction -- "Keys never logged; passed directly to API" 17 │ * @exposes #llm-client to #prompt-injection [medium] cwe:CWE-77 -- "User prompts sent to LLM API" 18 │ * @audit #llm-client -- "Prompt injection mitigated by LLM provider safety; local code is read-only"
L14 mitigates #config-validation mitigates #ssrf
BASE_URLS are hardcoded; baseUrl override is optional config
9 │ * - DeepSeek reasoning mode (deepseek-reasoner) 10 │ * 11 │ * Zero dependencies — uses Node 20+ built-in fetch. 12 │ * 13 │ * @exposes #llm-client to #ssrf [medium] cwe:CWE-918 -- "fetch() calls external LLM API endpoints" 14 │ * @mitigates #llm-client against #ssrf using #config-validation -- "BASE_URLS are hardcoded; baseUrl override is optional config" 15 │ * @exposes #llm-client to #api-key-exposure [high] cwe:CWE-798 -- "API keys passed in Authorization headers" 16 │ * @mitigates #llm-client against #api-key-exposure using #key-redaction -- "Keys never logged; passed directly to API" 17 │ * @exposes #llm-client to #prompt-injection [medium] cwe:CWE-77 -- "User prompts sent to LLM API" 18 │ * @audit #llm-client -- "Prompt injection mitigated by LLM provider safety; local code is read-only" 19 │ * @flows LLMConfig -> #llm-client via chatCompletion -- "Config and prompt input"
L15 exposes #llm-client → #api-key-exposure
API keys passed in Authorization headers
10 │ * 11 │ * Zero dependencies — uses Node 20+ built-in fetch. 12 │ * 13 │ * @exposes #llm-client to #ssrf [medium] cwe:CWE-918 -- "fetch() calls external LLM API endpoints" 14 │ * @mitigates #llm-client against #ssrf using #config-validation -- "BASE_URLS are hardcoded; baseUrl override is optional config" 15 │ * @exposes #llm-client to #api-key-exposure [high] cwe:CWE-798 -- "API keys passed in Authorization headers" 16 │ * @mitigates #llm-client against #api-key-exposure using #key-redaction -- "Keys never logged; passed directly to API" 17 │ * @exposes #llm-client to #prompt-injection [medium] cwe:CWE-77 -- "User prompts sent to LLM API" 18 │ * @audit #llm-client -- "Prompt injection mitigated by LLM provider safety; local code is read-only" 19 │ * @flows LLMConfig -> #llm-client via chatCompletion -- "Config and prompt input" 20 │ * @flows #llm-client -> LLMProvider via fetch -- "API request output"
L16 mitigates #key-redaction mitigates #api-key-exposure
Keys never logged; passed directly to API
11 │ * Zero dependencies — uses Node 20+ built-in fetch. 12 │ * 13 │ * @exposes #llm-client to #ssrf [medium] cwe:CWE-918 -- "fetch() calls external LLM API endpoints" 14 │ * @mitigates #llm-client against #ssrf using #config-validation -- "BASE_URLS are hardcoded; baseUrl override is optional config" 15 │ * @exposes #llm-client to #api-key-exposure [high] cwe:CWE-798 -- "API keys passed in Authorization headers" 16 │ * @mitigates #llm-client against #api-key-exposure using #key-redaction -- "Keys never logged; passed directly to API" 17 │ * @exposes #llm-client to #prompt-injection [medium] cwe:CWE-77 -- "User prompts sent to LLM API" 18 │ * @audit #llm-client -- "Prompt injection mitigated by LLM provider safety; local code is read-only" 19 │ * @flows LLMConfig -> #llm-client via chatCompletion -- "Config and prompt input" 20 │ * @flows #llm-client -> LLMProvider via fetch -- "API request output" 21 │ * @flows LLMProvider -> #llm-client via response -- "API response input"
L17 exposes #llm-client → #prompt-injection
User prompts sent to LLM API
12 │ * 13 │ * @exposes #llm-client to #ssrf [medium] cwe:CWE-918 -- "fetch() calls external LLM API endpoints" 14 │ * @mitigates #llm-client against #ssrf using #config-validation -- "BASE_URLS are hardcoded; baseUrl override is optional config" 15 │ * @exposes #llm-client to #api-key-exposure [high] cwe:CWE-798 -- "API keys passed in Authorization headers" 16 │ * @mitigates #llm-client against #api-key-exposure using #key-redaction -- "Keys never logged; passed directly to API" 17 │ * @exposes #llm-client to #prompt-injection [medium] cwe:CWE-77 -- "User prompts sent to LLM API" 18 │ * @audit #llm-client -- "Prompt injection mitigated by LLM provider safety; local code is read-only" 19 │ * @flows LLMConfig -> #llm-client via chatCompletion -- "Config and prompt input" 20 │ * @flows #llm-client -> LLMProvider via fetch -- "API request output" 21 │ * @flows LLMProvider -> #llm-client via response -- "API response input" 22 │ * @boundary #llm-client and LLMProvider (#llm-api-boundary) -- "Trust boundary at external API call"
L18 audit Audit: #llm-client
Prompt injection mitigated by LLM provider safety; local code is read-only
13 │ * @exposes #llm-client to #ssrf [medium] cwe:CWE-918 -- "fetch() calls external LLM API endpoints" 14 │ * @mitigates #llm-client against #ssrf using #config-validation -- "BASE_URLS are hardcoded; baseUrl override is optional config" 15 │ * @exposes #llm-client to #api-key-exposure [high] cwe:CWE-798 -- "API keys passed in Authorization headers" 16 │ * @mitigates #llm-client against #api-key-exposure using #key-redaction -- "Keys never logged; passed directly to API" 17 │ * @exposes #llm-client to #prompt-injection [medium] cwe:CWE-77 -- "User prompts sent to LLM API" 18 │ * @audit #llm-client -- "Prompt injection mitigated by LLM provider safety; local code is read-only" 19 │ * @flows LLMConfig -> #llm-client via chatCompletion -- "Config and prompt input" 20 │ * @flows #llm-client -> LLMProvider via fetch -- "API request output" 21 │ * @flows LLMProvider -> #llm-client via response -- "API response input" 22 │ * @boundary #llm-client and LLMProvider (#llm-api-boundary) -- "Trust boundary at external API call" 23 │ * @handles secrets on #llm-client -- "Processes API keys for authentication"
L19 flow LLMConfig → #llm-client
Config and prompt input
14 │ * @mitigates #llm-client against #ssrf using #config-validation -- "BASE_URLS are hardcoded; baseUrl override is optional config" 15 │ * @exposes #llm-client to #api-key-exposure [high] cwe:CWE-798 -- "API keys passed in Authorization headers" 16 │ * @mitigates #llm-client against #api-key-exposure using #key-redaction -- "Keys never logged; passed directly to API" 17 │ * @exposes #llm-client to #prompt-injection [medium] cwe:CWE-77 -- "User prompts sent to LLM API" 18 │ * @audit #llm-client -- "Prompt injection mitigated by LLM provider safety; local code is read-only" 19 │ * @flows LLMConfig -> #llm-client via chatCompletion -- "Config and prompt input" 20 │ * @flows #llm-client -> LLMProvider via fetch -- "API request output" 21 │ * @flows LLMProvider -> #llm-client via response -- "API response input" 22 │ * @boundary #llm-client and LLMProvider (#llm-api-boundary) -- "Trust boundary at external API call" 23 │ * @handles secrets on #llm-client -- "Processes API keys for authentication" 24 │ */
L20 flow #llm-client → LLMProvider
API request output
15 │ * @exposes #llm-client to #api-key-exposure [high] cwe:CWE-798 -- "API keys passed in Authorization headers" 16 │ * @mitigates #llm-client against #api-key-exposure using #key-redaction -- "Keys never logged; passed directly to API" 17 │ * @exposes #llm-client to #prompt-injection [medium] cwe:CWE-77 -- "User prompts sent to LLM API" 18 │ * @audit #llm-client -- "Prompt injection mitigated by LLM provider safety; local code is read-only" 19 │ * @flows LLMConfig -> #llm-client via chatCompletion -- "Config and prompt input" 20 │ * @flows #llm-client -> LLMProvider via fetch -- "API request output" 21 │ * @flows LLMProvider -> #llm-client via response -- "API response input" 22 │ * @boundary #llm-client and LLMProvider (#llm-api-boundary) -- "Trust boundary at external API call" 23 │ * @handles secrets on #llm-client -- "Processes API keys for authentication" 24 │ */ 25 │
L21 flow LLMProvider → #llm-client
API response input
16 │ * @mitigates #llm-client against #api-key-exposure using #key-redaction -- "Keys never logged; passed directly to API" 17 │ * @exposes #llm-client to #prompt-injection [medium] cwe:CWE-77 -- "User prompts sent to LLM API" 18 │ * @audit #llm-client -- "Prompt injection mitigated by LLM provider safety; local code is read-only" 19 │ * @flows LLMConfig -> #llm-client via chatCompletion -- "Config and prompt input" 20 │ * @flows #llm-client -> LLMProvider via fetch -- "API request output" 21 │ * @flows LLMProvider -> #llm-client via response -- "API response input" 22 │ * @boundary #llm-client and LLMProvider (#llm-api-boundary) -- "Trust boundary at external API call" 23 │ * @handles secrets on #llm-client -- "Processes API keys for authentication" 24 │ */ 25 │ 26 │ export type LLMProvider = 'anthropic' | 'openai' | 'google' | 'openrouter' | 'deepseek' | 'ollama';
L22 boundary #llm-client ↔ LLMProvider
Trust boundary at external API call
17 │ * @exposes #llm-client to #prompt-injection [medium] cwe:CWE-77 -- "User prompts sent to LLM API" 18 │ * @audit #llm-client -- "Prompt injection mitigated by LLM provider safety; local code is read-only" 19 │ * @flows LLMConfig -> #llm-client via chatCompletion -- "Config and prompt input" 20 │ * @flows #llm-client -> LLMProvider via fetch -- "API request output" 21 │ * @flows LLMProvider -> #llm-client via response -- "API response input" 22 │ * @boundary #llm-client and LLMProvider (#llm-api-boundary) -- "Trust boundary at external API call" 23 │ * @handles secrets on #llm-client -- "Processes API keys for authentication" 24 │ */ 25 │ 26 │ export type LLMProvider = 'anthropic' | 'openai' | 'google' | 'openrouter' | 'deepseek' | 'ollama'; 27 │
L23 handles #llm-client: secrets
Processes API keys for authentication
18 │ * @audit #llm-client -- "Prompt injection mitigated by LLM provider safety; local code is read-only" 19 │ * @flows LLMConfig -> #llm-client via chatCompletion -- "Config and prompt input" 20 │ * @flows #llm-client -> LLMProvider via fetch -- "API request output" 21 │ * @flows LLMProvider -> #llm-client via response -- "API response input" 22 │ * @boundary #llm-client and LLMProvider (#llm-api-boundary) -- "Trust boundary at external API call" 23 │ * @handles secrets on #llm-client -- "Processes API keys for authentication" 24 │ */ 25 │ 26 │ export type LLMProvider = 'anthropic' | 'openai' | 'google' | 'openrouter' | 'deepseek' | 'ollama'; 27 │ 28 │ // ─── Tool definitions ────────────────────────────────────────────────
src/analyze/index.ts 1 openlow5 stale flow 4exposes 3mitigates 3handles 2 open 13
L8 exposes #llm-client → #path-traversal
buildProjectContext reads files from root-relative paths
3 │ * 4 │ * Serializes the threat model, sends it to an LLM with a framework- 5 │ * specific prompt, streams the response, and saves timestamped results 6 │ * to .guardlink/threat-reports/. 7 │ * 8 │ * @exposes #llm-client to #path-traversal [medium] cwe:CWE-22 -- "buildProjectContext reads files from root-relative paths" 9 │ * @mitigates #llm-client against #path-traversal using #path-validation -- "join() with root constrains file access" 10 │ * @exposes #llm-client to #arbitrary-write [medium] cwe:CWE-73 -- "writeFileSync saves threat reports to .guardlink/" 11 │ * @mitigates #llm-client against #arbitrary-write using #path-validation -- "Output path is fixed to .guardlink/threat-reports/" 12 │ * @exposes #llm-client to #data-exposure [low] cwe:CWE-200 -- "Serializes full threat model and code snippets for LLM" 13 │ * @audit #llm-client -- "Threat model data intentionally sent to LLM for analysis"
L9 mitigates #path-validation mitigates #path-traversal
join() with root constrains file access
4 │ * Serializes the threat model, sends it to an LLM with a framework- 5 │ * specific prompt, streams the response, and saves timestamped results 6 │ * to .guardlink/threat-reports/. 7 │ * 8 │ * @exposes #llm-client to #path-traversal [medium] cwe:CWE-22 -- "buildProjectContext reads files from root-relative paths" 9 │ * @mitigates #llm-client against #path-traversal using #path-validation -- "join() with root constrains file access" 10 │ * @exposes #llm-client to #arbitrary-write [medium] cwe:CWE-73 -- "writeFileSync saves threat reports to .guardlink/" 11 │ * @mitigates #llm-client against #arbitrary-write using #path-validation -- "Output path is fixed to .guardlink/threat-reports/" 12 │ * @exposes #llm-client to #data-exposure [low] cwe:CWE-200 -- "Serializes full threat model and code snippets for LLM" 13 │ * @audit #llm-client -- "Threat model data intentionally sent to LLM for analysis" 14 │ * @flows ThreatModel -> #llm-client via serializeModel -- "Model serialization input"
L10 exposes #llm-client → #arbitrary-write
writeFileSync saves threat reports to .guardlink/
5 │ * specific prompt, streams the response, and saves timestamped results 6 │ * to .guardlink/threat-reports/. 7 │ * 8 │ * @exposes #llm-client to #path-traversal [medium] cwe:CWE-22 -- "buildProjectContext reads files from root-relative paths" 9 │ * @mitigates #llm-client against #path-traversal using #path-validation -- "join() with root constrains file access" 10 │ * @exposes #llm-client to #arbitrary-write [medium] cwe:CWE-73 -- "writeFileSync saves threat reports to .guardlink/" 11 │ * @mitigates #llm-client against #arbitrary-write using #path-validation -- "Output path is fixed to .guardlink/threat-reports/" 12 │ * @exposes #llm-client to #data-exposure [low] cwe:CWE-200 -- "Serializes full threat model and code snippets for LLM" 13 │ * @audit #llm-client -- "Threat model data intentionally sent to LLM for analysis" 14 │ * @flows ThreatModel -> #llm-client via serializeModel -- "Model serialization input" 15 │ * @flows ProjectFiles -> #llm-client via readFileSync -- "Project context read"
L11 mitigates #path-validation mitigates #arbitrary-write
Output path is fixed to .guardlink/threat-reports/
6 │ * to .guardlink/threat-reports/. 7 │ * 8 │ * @exposes #llm-client to #path-traversal [medium] cwe:CWE-22 -- "buildProjectContext reads files from root-relative paths" 9 │ * @mitigates #llm-client against #path-traversal using #path-validation -- "join() with root constrains file access" 10 │ * @exposes #llm-client to #arbitrary-write [medium] cwe:CWE-73 -- "writeFileSync saves threat reports to .guardlink/" 11 │ * @mitigates #llm-client against #arbitrary-write using #path-validation -- "Output path is fixed to .guardlink/threat-reports/" 12 │ * @exposes #llm-client to #data-exposure [low] cwe:CWE-200 -- "Serializes full threat model and code snippets for LLM" 13 │ * @audit #llm-client -- "Threat model data intentionally sent to LLM for analysis" 14 │ * @flows ThreatModel -> #llm-client via serializeModel -- "Model serialization input" 15 │ * @flows ProjectFiles -> #llm-client via readFileSync -- "Project context read" 16 │ * @flows #llm-client -> ReportFile via writeFileSync -- "Report output"
L12 exposes #llm-client → #data-exposure
Serializes full threat model and code snippets for LLM
7 │ * 8 │ * @exposes #llm-client to #path-traversal [medium] cwe:CWE-22 -- "buildProjectContext reads files from root-relative paths" 9 │ * @mitigates #llm-client against #path-traversal using #path-validation -- "join() with root constrains file access" 10 │ * @exposes #llm-client to #arbitrary-write [medium] cwe:CWE-73 -- "writeFileSync saves threat reports to .guardlink/" 11 │ * @mitigates #llm-client against #arbitrary-write using #path-validation -- "Output path is fixed to .guardlink/threat-reports/" 12 │ * @exposes #llm-client to #data-exposure [low] cwe:CWE-200 -- "Serializes full threat model and code snippets for LLM" 13 │ * @audit #llm-client -- "Threat model data intentionally sent to LLM for analysis" 14 │ * @flows ThreatModel -> #llm-client via serializeModel -- "Model serialization input" 15 │ * @flows ProjectFiles -> #llm-client via readFileSync -- "Project context read" 16 │ * @flows #llm-client -> ReportFile via writeFileSync -- "Report output" 17 │ * @handles internal on #llm-client -- "Processes project dependencies, env examples, code snippets"
L13 audit Audit: #llm-client
Threat model data intentionally sent to LLM for analysis
8 │ * @exposes #llm-client to #path-traversal [medium] cwe:CWE-22 -- "buildProjectContext reads files from root-relative paths" 9 │ * @mitigates #llm-client against #path-traversal using #path-validation -- "join() with root constrains file access" 10 │ * @exposes #llm-client to #arbitrary-write [medium] cwe:CWE-73 -- "writeFileSync saves threat reports to .guardlink/" 11 │ * @mitigates #llm-client against #arbitrary-write using #path-validation -- "Output path is fixed to .guardlink/threat-reports/" 12 │ * @exposes #llm-client to #data-exposure [low] cwe:CWE-200 -- "Serializes full threat model and code snippets for LLM" 13 │ * @audit #llm-client -- "Threat model data intentionally sent to LLM for analysis" 14 │ * @flows ThreatModel -> #llm-client via serializeModel -- "Model serialization input" 15 │ * @flows ProjectFiles -> #llm-client via readFileSync -- "Project context read" 16 │ * @flows #llm-client -> ReportFile via writeFileSync -- "Report output" 17 │ * @handles internal on #llm-client -- "Processes project dependencies, env examples, code snippets" 18 │ */
L14 flow ThreatModel → #llm-client
Model serialization input
9 │ * @mitigates #llm-client against #path-traversal using #path-validation -- "join() with root constrains file access" 10 │ * @exposes #llm-client to #arbitrary-write [medium] cwe:CWE-73 -- "writeFileSync saves threat reports to .guardlink/" 11 │ * @mitigates #llm-client against #arbitrary-write using #path-validation -- "Output path is fixed to .guardlink/threat-reports/" 12 │ * @exposes #llm-client to #data-exposure [low] cwe:CWE-200 -- "Serializes full threat model and code snippets for LLM" 13 │ * @audit #llm-client -- "Threat model data intentionally sent to LLM for analysis" 14 │ * @flows ThreatModel -> #llm-client via serializeModel -- "Model serialization input" 15 │ * @flows ProjectFiles -> #llm-client via readFileSync -- "Project context read" 16 │ * @flows #llm-client -> ReportFile via writeFileSync -- "Report output" 17 │ * @handles internal on #llm-client -- "Processes project dependencies, env examples, code snippets" 18 │ */ 19 │
L15 flow ProjectFiles → #llm-client
Project context read
10 │ * @exposes #llm-client to #arbitrary-write [medium] cwe:CWE-73 -- "writeFileSync saves threat reports to .guardlink/" 11 │ * @mitigates #llm-client against #arbitrary-write using #path-validation -- "Output path is fixed to .guardlink/threat-reports/" 12 │ * @exposes #llm-client to #data-exposure [low] cwe:CWE-200 -- "Serializes full threat model and code snippets for LLM" 13 │ * @audit #llm-client -- "Threat model data intentionally sent to LLM for analysis" 14 │ * @flows ThreatModel -> #llm-client via serializeModel -- "Model serialization input" 15 │ * @flows ProjectFiles -> #llm-client via readFileSync -- "Project context read" 16 │ * @flows #llm-client -> ReportFile via writeFileSync -- "Report output" 17 │ * @handles internal on #llm-client -- "Processes project dependencies, env examples, code snippets" 18 │ */ 19 │ 20 │ import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync } from 'node:fs';
L16 flow #llm-client → ReportFile
Report output
11 │ * @mitigates #llm-client against #arbitrary-write using #path-validation -- "Output path is fixed to .guardlink/threat-reports/" 12 │ * @exposes #llm-client to #data-exposure [low] cwe:CWE-200 -- "Serializes full threat model and code snippets for LLM" 13 │ * @audit #llm-client -- "Threat model data intentionally sent to LLM for analysis" 14 │ * @flows ThreatModel -> #llm-client via serializeModel -- "Model serialization input" 15 │ * @flows ProjectFiles -> #llm-client via readFileSync -- "Project context read" 16 │ * @flows #llm-client -> ReportFile via writeFileSync -- "Report output" 17 │ * @handles internal on #llm-client -- "Processes project dependencies, env examples, code snippets" 18 │ */ 19 │ 20 │ import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync } from 'node:fs'; 21 │ import { join, relative } from 'node:path';
L17 handles #llm-client: internal
Processes project dependencies, env examples, code snippets
12 │ * @exposes #llm-client to #data-exposure [low] cwe:CWE-200 -- "Serializes full threat model and code snippets for LLM" 13 │ * @audit #llm-client -- "Threat model data intentionally sent to LLM for analysis" 14 │ * @flows ThreatModel -> #llm-client via serializeModel -- "Model serialization input" 15 │ * @flows ProjectFiles -> #llm-client via readFileSync -- "Project context read" 16 │ * @flows #llm-client -> ReportFile via writeFileSync -- "Report output" 17 │ * @handles internal on #llm-client -- "Processes project dependencies, env examples, code snippets" 18 │ */ 19 │ 20 │ import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync } from 'node:fs'; 21 │ import { join, relative } from 'node:path'; 22 │ import type { ThreatModel } from '../types/index.js';
L726 flow PentestFindings → #llm-client
Reads CXG scan results for dashboard and report context
721 │ } 722 │ 723 │ // ─── Pentest findings loader ───────────────────────────────────────── 724 │ 725 │ /** 726 │ * @flows PentestFindings -> #llm-client via readFileSync -- "Reads CXG scan results for dashboard and report context" 727 │ * @handles internal on #llm-client -- "Processes pentest scan output (JSON/SARIF)" 728 │ */ 729 │ 730 │ export interface PentestFinding { 731 │ id: string;
L727 handles #llm-client: internal
Processes pentest scan output (JSON/SARIF)
722 │ 723 │ // ─── Pentest findings loader ───────────────────────────────────────── 724 │ 725 │ /** 726 │ * @flows PentestFindings -> #llm-client via readFileSync -- "Reads CXG scan results for dashboard and report context" 727 │ * @handles internal on #llm-client -- "Processes pentest scan output (JSON/SARIF)" 728 │ */ 729 │ 730 │ export interface PentestFinding { 731 │ id: string; 732 │ target: string;
L799 mitigates #path-validation mitigates #path-traversal
join() constrains reads to .guardlink/
794 │ 795 │ /** 796 │ * Load pentest findings from .guardlink/pentest-findings/ and template 797 │ * metadata from .guardlink/cxg-templates/. 798 │ * 799 │ * @mitigates #llm-client against #path-traversal using #path-validation -- "join() constrains reads to .guardlink/" 800 │ */ 801 │ export function loadPentestData(root: string): PentestData { 802 │ const data: PentestData = { scans: [], templates: [], totalFindings: 0, findingsBySeverity: {} }; 803 │ 804 │ // Load scan results (JSON files)
src/analyzer/sarif.ts 1 openlow1 stale comment 4flow 2exposes 1audit 1 open 8
L34 exposes #sarif → #data-exposure
Exposes threat model findings to SARIF consumers
29 │ * been rewritten as unchanged, which is the silent all-clear §2 exists to 30 │ * prevent — the same reason the annotation hash was taught about @entitles in 31 │ * v2. `results` and `tool` remain byte-identical, and the test asserts exactly 32 │ * that rather than document equality. 33 │ * 34 │ * @exposes #sarif to #data-exposure [low] cwe:CWE-200 -- "Exposes threat model findings to SARIF consumers" 35 │ * @audit #sarif -- "SARIF output intentionally reveals security findings for CI/CD integration" 36 │ * @comment -- "Pure function: transforms ThreatModel to SARIF JSON; no I/O" 37 │ * @comment -- "runs[0].properties.annotation_hash stamps the export with the annotations it was cut from (R10), so a hygiene gate can tell a current SARIF from one built three commits ago — this file is the pentest surface, and a stale one decides which exposures get tested" 38 │ * @comment -- "@entitles has no export semantics by design: SARIF for a model with entitlements is byte-identical to one without, so an entitlement can never hide an exposure from the pentest export (actor-entitlement design §3.2)" 39 │ * @comment -- "Exposure and confirmed results carry codegraph_reachability{http_method,http_path} derived from the asset's inbound @flows route so downstream HTTP consumers (e.g. cert-x-gen) can target the endpoint; emitted verbatim from the annotation, no base path assumed"
L35 audit Audit: #sarif
SARIF output intentionally reveals security findings for CI/CD integration
30 │ * prevent — the same reason the annotation hash was taught about @entitles in 31 │ * v2. `results` and `tool` remain byte-identical, and the test asserts exactly 32 │ * that rather than document equality. 33 │ * 34 │ * @exposes #sarif to #data-exposure [low] cwe:CWE-200 -- "Exposes threat model findings to SARIF consumers" 35 │ * @audit #sarif -- "SARIF output intentionally reveals security findings for CI/CD integration" 36 │ * @comment -- "Pure function: transforms ThreatModel to SARIF JSON; no I/O" 37 │ * @comment -- "runs[0].properties.annotation_hash stamps the export with the annotations it was cut from (R10), so a hygiene gate can tell a current SARIF from one built three commits ago — this file is the pentest surface, and a stale one decides which exposures get tested" 38 │ * @comment -- "@entitles has no export semantics by design: SARIF for a model with entitlements is byte-identical to one without, so an entitlement can never hide an exposure from the pentest export (actor-entitlement design §3.2)" 39 │ * @comment -- "Exposure and confirmed results carry codegraph_reachability{http_method,http_path} derived from the asset's inbound @flows route so downstream HTTP consumers (e.g. cert-x-gen) can target the endpoint; emitted verbatim from the annotation, no base path assumed" 40 │ * @flows ThreatModel -> #sarif via generateSarif -- "Model input"
L36 comment Pure function: transforms ThreatModel to SARIF JSON; no I/O
Pure function: transforms ThreatModel to SARIF JSON; no I/O
31 │ * v2. `results` and `tool` remain byte-identical, and the test asserts exactly 32 │ * that rather than document equality. 33 │ * 34 │ * @exposes #sarif to #data-exposure [low] cwe:CWE-200 -- "Exposes threat model findings to SARIF consumers" 35 │ * @audit #sarif -- "SARIF output intentionally reveals security findings for CI/CD integration" 36 │ * @comment -- "Pure function: transforms ThreatModel to SARIF JSON; no I/O" 37 │ * @comment -- "runs[0].properties.annotation_hash stamps the export with the annotations it was cut from (R10), so a hygiene gate can tell a current SARIF from one built three commits ago — this file is the pentest surface, and a stale one decides which exposures get tested" 38 │ * @comment -- "@entitles has no export semantics by design: SARIF for a model with entitlements is byte-identical to one without, so an entitlement can never hide an exposure from the pentest export (actor-entitlement design §3.2)" 39 │ * @comment -- "Exposure and confirmed results carry codegraph_reachability{http_method,http_path} derived from the asset's inbound @flows route so downstream HTTP consumers (e.g. cert-x-gen) can target the endpoint; emitted verbatim from the annotation, no base path assumed" 40 │ * @flows ThreatModel -> #sarif via generateSarif -- "Model input" 41 │ * @flows #sarif -> SarifLog via return -- "SARIF output"
L37 comment runs[0].properties.annotation_hash stamps the export with the annotations it was cut from (R10), so a hygiene gate can tell a current SARIF from one built three commits ago — this file is the pentest surface, and a stale one decides which exposures get tested
runs[0].properties.annotation_hash stamps the export with the annotations it was cut from (R10), so a hygiene gate can tell a current SARIF from one built three commits ago — this file is the pentest surface, and a stale one decides which exposures get tested
32 │ * that rather than document equality. 33 │ * 34 │ * @exposes #sarif to #data-exposure [low] cwe:CWE-200 -- "Exposes threat model findings to SARIF consumers" 35 │ * @audit #sarif -- "SARIF output intentionally reveals security findings for CI/CD integration" 36 │ * @comment -- "Pure function: transforms ThreatModel to SARIF JSON; no I/O" 37 │ * @comment -- "runs[0].properties.annotation_hash stamps the export with the annotations it was cut from (R10), so a hygiene gate can tell a current SARIF from one built three commits ago — this file is the pentest surface, and a stale one decides which exposures get tested" 38 │ * @comment -- "@entitles has no export semantics by design: SARIF for a model with entitlements is byte-identical to one without, so an entitlement can never hide an exposure from the pentest export (actor-entitlement design §3.2)" 39 │ * @comment -- "Exposure and confirmed results carry codegraph_reachability{http_method,http_path} derived from the asset's inbound @flows route so downstream HTTP consumers (e.g. cert-x-gen) can target the endpoint; emitted verbatim from the annotation, no base path assumed" 40 │ * @flows ThreatModel -> #sarif via generateSarif -- "Model input" 41 │ * @flows #sarif -> SarifLog via return -- "SARIF output" 42 │ */
L38 comment @entitles has no export semantics by design: SARIF for a model with entitlements is byte-identical to one without, so an entitlement can never hide an exposure from the pentest export (actor-entitlement design §3.2)
@entitles has no export semantics by design: SARIF for a model with entitlements is byte-identical to one without, so an entitlement can never hide an exposure from the pentest export (actor-entitlement design §3.2)
33 │ * 34 │ * @exposes #sarif to #data-exposure [low] cwe:CWE-200 -- "Exposes threat model findings to SARIF consumers" 35 │ * @audit #sarif -- "SARIF output intentionally reveals security findings for CI/CD integration" 36 │ * @comment -- "Pure function: transforms ThreatModel to SARIF JSON; no I/O" 37 │ * @comment -- "runs[0].properties.annotation_hash stamps the export with the annotations it was cut from (R10), so a hygiene gate can tell a current SARIF from one built three commits ago — this file is the pentest surface, and a stale one decides which exposures get tested" 38 │ * @comment -- "@entitles has no export semantics by design: SARIF for a model with entitlements is byte-identical to one without, so an entitlement can never hide an exposure from the pentest export (actor-entitlement design §3.2)" 39 │ * @comment -- "Exposure and confirmed results carry codegraph_reachability{http_method,http_path} derived from the asset's inbound @flows route so downstream HTTP consumers (e.g. cert-x-gen) can target the endpoint; emitted verbatim from the annotation, no base path assumed" 40 │ * @flows ThreatModel -> #sarif via generateSarif -- "Model input" 41 │ * @flows #sarif -> SarifLog via return -- "SARIF output" 42 │ */ 43 │
L39 comment Exposure and confirmed results carry codegraph_reachability{http_method,http_path} derived from the asset's inbound @flows route so downstream HTTP consumers (e.g. cert-x-gen) can target the endpoint; emitted verbatim from the annotation, no base path assumed
Exposure and confirmed results carry codegraph_reachability{http_method,http_path} derived from the asset's inbound @flows route so downstream HTTP consumers (e.g. cert-x-gen) can target the endpoint; emitted verbatim from the annotation, no base path assumed
34 │ * @exposes #sarif to #data-exposure [low] cwe:CWE-200 -- "Exposes threat model findings to SARIF consumers" 35 │ * @audit #sarif -- "SARIF output intentionally reveals security findings for CI/CD integration" 36 │ * @comment -- "Pure function: transforms ThreatModel to SARIF JSON; no I/O" 37 │ * @comment -- "runs[0].properties.annotation_hash stamps the export with the annotations it was cut from (R10), so a hygiene gate can tell a current SARIF from one built three commits ago — this file is the pentest surface, and a stale one decides which exposures get tested" 38 │ * @comment -- "@entitles has no export semantics by design: SARIF for a model with entitlements is byte-identical to one without, so an entitlement can never hide an exposure from the pentest export (actor-entitlement design §3.2)" 39 │ * @comment -- "Exposure and confirmed results carry codegraph_reachability{http_method,http_path} derived from the asset's inbound @flows route so downstream HTTP consumers (e.g. cert-x-gen) can target the endpoint; emitted verbatim from the annotation, no base path assumed" 40 │ * @flows ThreatModel -> #sarif via generateSarif -- "Model input" 41 │ * @flows #sarif -> SarifLog via return -- "SARIF output" 42 │ */ 43 │ 44 │ import { createHash } from 'node:crypto';
L40 flow ThreatModel → #sarif
Model input
35 │ * @audit #sarif -- "SARIF output intentionally reveals security findings for CI/CD integration" 36 │ * @comment -- "Pure function: transforms ThreatModel to SARIF JSON; no I/O" 37 │ * @comment -- "runs[0].properties.annotation_hash stamps the export with the annotations it was cut from (R10), so a hygiene gate can tell a current SARIF from one built three commits ago — this file is the pentest surface, and a stale one decides which exposures get tested" 38 │ * @comment -- "@entitles has no export semantics by design: SARIF for a model with entitlements is byte-identical to one without, so an entitlement can never hide an exposure from the pentest export (actor-entitlement design §3.2)" 39 │ * @comment -- "Exposure and confirmed results carry codegraph_reachability{http_method,http_path} derived from the asset's inbound @flows route so downstream HTTP consumers (e.g. cert-x-gen) can target the endpoint; emitted verbatim from the annotation, no base path assumed" 40 │ * @flows ThreatModel -> #sarif via generateSarif -- "Model input" 41 │ * @flows #sarif -> SarifLog via return -- "SARIF output" 42 │ */ 43 │ 44 │ import { createHash } from 'node:crypto'; 45 │
L41 flow #sarif → SarifLog
SARIF output
36 │ * @comment -- "Pure function: transforms ThreatModel to SARIF JSON; no I/O" 37 │ * @comment -- "runs[0].properties.annotation_hash stamps the export with the annotations it was cut from (R10), so a hygiene gate can tell a current SARIF from one built three commits ago — this file is the pentest surface, and a stale one decides which exposures get tested" 38 │ * @comment -- "@entitles has no export semantics by design: SARIF for a model with entitlements is byte-identical to one without, so an entitlement can never hide an exposure from the pentest export (actor-entitlement design §3.2)" 39 │ * @comment -- "Exposure and confirmed results carry codegraph_reachability{http_method,http_path} derived from the asset's inbound @flows route so downstream HTTP consumers (e.g. cert-x-gen) can target the endpoint; emitted verbatim from the annotation, no base path assumed" 40 │ * @flows ThreatModel -> #sarif via generateSarif -- "Model input" 41 │ * @flows #sarif -> SarifLog via return -- "SARIF output" 42 │ */ 43 │ 44 │ import { createHash } from 'node:crypto'; 45 │ 46 │ import type { ThreatModel, ParseDiagnostic, Severity } from '../types/index.js';
src/init/index.ts 1 openlow5 stale exposes 4mitigates 3flow 3audit 1 open 12
L8 exposes #init → #arbitrary-write
Creates/modifies files: .guardlink/, CLAUDE.md, .cursorrules, etc.
3 │ * 4 │ * Detects project language and existing agent files, creates .guardlink/ 5 │ * directory with shared definitions, and injects GuardLink instructions 6 │ * into agent instruction files (CLAUDE.md, .cursorrules, etc.). 7 │ * 8 │ * @exposes #init to #arbitrary-write [high] cwe:CWE-73 -- "Creates/modifies files: .guardlink/, CLAUDE.md, .cursorrules, etc." 9 │ * @mitigates #init against #arbitrary-write using #path-validation -- "All paths are relative to root; join() constrains" 10 │ * @exposes #init to #path-traversal [medium] cwe:CWE-22 -- "Reads/writes files based on root argument" 11 │ * @mitigates #init against #path-traversal using #path-validation -- "join() with explicit root constrains file access" 12 │ * @exposes #init to #data-exposure [low] cwe:CWE-200 -- "Writes API key config to .guardlink/config.json" 13 │ * @audit #init -- "Config file may contain API keys; .gitignore entry added automatically"
L9 mitigates #path-validation mitigates #arbitrary-write
All paths are relative to root; join() constrains
4 │ * Detects project language and existing agent files, creates .guardlink/ 5 │ * directory with shared definitions, and injects GuardLink instructions 6 │ * into agent instruction files (CLAUDE.md, .cursorrules, etc.). 7 │ * 8 │ * @exposes #init to #arbitrary-write [high] cwe:CWE-73 -- "Creates/modifies files: .guardlink/, CLAUDE.md, .cursorrules, etc." 9 │ * @mitigates #init against #arbitrary-write using #path-validation -- "All paths are relative to root; join() constrains" 10 │ * @exposes #init to #path-traversal [medium] cwe:CWE-22 -- "Reads/writes files based on root argument" 11 │ * @mitigates #init against #path-traversal using #path-validation -- "join() with explicit root constrains file access" 12 │ * @exposes #init to #data-exposure [low] cwe:CWE-200 -- "Writes API key config to .guardlink/config.json" 13 │ * @audit #init -- "Config file may contain API keys; .gitignore entry added automatically" 14 │ * @flows ProjectRoot -> #init via options.root -- "Project root input"
L10 exposes #init → #path-traversal
Reads/writes files based on root argument
5 │ * directory with shared definitions, and injects GuardLink instructions 6 │ * into agent instruction files (CLAUDE.md, .cursorrules, etc.). 7 │ * 8 │ * @exposes #init to #arbitrary-write [high] cwe:CWE-73 -- "Creates/modifies files: .guardlink/, CLAUDE.md, .cursorrules, etc." 9 │ * @mitigates #init against #arbitrary-write using #path-validation -- "All paths are relative to root; join() constrains" 10 │ * @exposes #init to #path-traversal [medium] cwe:CWE-22 -- "Reads/writes files based on root argument" 11 │ * @mitigates #init against #path-traversal using #path-validation -- "join() with explicit root constrains file access" 12 │ * @exposes #init to #data-exposure [low] cwe:CWE-200 -- "Writes API key config to .guardlink/config.json" 13 │ * @audit #init -- "Config file may contain API keys; .gitignore entry added automatically" 14 │ * @flows ProjectRoot -> #init via options.root -- "Project root input" 15 │ * @flows #init -> AgentFiles via writeFileSync -- "Agent instruction file writes"
L11 mitigates #path-validation mitigates #path-traversal
join() with explicit root constrains file access
6 │ * into agent instruction files (CLAUDE.md, .cursorrules, etc.). 7 │ * 8 │ * @exposes #init to #arbitrary-write [high] cwe:CWE-73 -- "Creates/modifies files: .guardlink/, CLAUDE.md, .cursorrules, etc." 9 │ * @mitigates #init against #arbitrary-write using #path-validation -- "All paths are relative to root; join() constrains" 10 │ * @exposes #init to #path-traversal [medium] cwe:CWE-22 -- "Reads/writes files based on root argument" 11 │ * @mitigates #init against #path-traversal using #path-validation -- "join() with explicit root constrains file access" 12 │ * @exposes #init to #data-exposure [low] cwe:CWE-200 -- "Writes API key config to .guardlink/config.json" 13 │ * @audit #init -- "Config file may contain API keys; .gitignore entry added automatically" 14 │ * @flows ProjectRoot -> #init via options.root -- "Project root input" 15 │ * @flows #init -> AgentFiles via writeFileSync -- "Agent instruction file writes" 16 │ * @flows #init -> ConfigFile via writeFileSync -- "Config file write"
L12 exposes #init → #data-exposure
Writes API key config to .guardlink/config.json
7 │ * 8 │ * @exposes #init to #arbitrary-write [high] cwe:CWE-73 -- "Creates/modifies files: .guardlink/, CLAUDE.md, .cursorrules, etc." 9 │ * @mitigates #init against #arbitrary-write using #path-validation -- "All paths are relative to root; join() constrains" 10 │ * @exposes #init to #path-traversal [medium] cwe:CWE-22 -- "Reads/writes files based on root argument" 11 │ * @mitigates #init against #path-traversal using #path-validation -- "join() with explicit root constrains file access" 12 │ * @exposes #init to #data-exposure [low] cwe:CWE-200 -- "Writes API key config to .guardlink/config.json" 13 │ * @audit #init -- "Config file may contain API keys; .gitignore entry added automatically" 14 │ * @flows ProjectRoot -> #init via options.root -- "Project root input" 15 │ * @flows #init -> AgentFiles via writeFileSync -- "Agent instruction file writes" 16 │ * @flows #init -> ConfigFile via writeFileSync -- "Config file write" 17 │ * @handles internal on #init -- "Generates definitions and agent instruction content"
L13 audit Audit: #init
Config file may contain API keys; .gitignore entry added automatically
8 │ * @exposes #init to #arbitrary-write [high] cwe:CWE-73 -- "Creates/modifies files: .guardlink/, CLAUDE.md, .cursorrules, etc." 9 │ * @mitigates #init against #arbitrary-write using #path-validation -- "All paths are relative to root; join() constrains" 10 │ * @exposes #init to #path-traversal [medium] cwe:CWE-22 -- "Reads/writes files based on root argument" 11 │ * @mitigates #init against #path-traversal using #path-validation -- "join() with explicit root constrains file access" 12 │ * @exposes #init to #data-exposure [low] cwe:CWE-200 -- "Writes API key config to .guardlink/config.json" 13 │ * @audit #init -- "Config file may contain API keys; .gitignore entry added automatically" 14 │ * @flows ProjectRoot -> #init via options.root -- "Project root input" 15 │ * @flows #init -> AgentFiles via writeFileSync -- "Agent instruction file writes" 16 │ * @flows #init -> ConfigFile via writeFileSync -- "Config file write" 17 │ * @handles internal on #init -- "Generates definitions and agent instruction content" 18 │ */
L14 flow ProjectRoot → #init
Project root input
9 │ * @mitigates #init against #arbitrary-write using #path-validation -- "All paths are relative to root; join() constrains" 10 │ * @exposes #init to #path-traversal [medium] cwe:CWE-22 -- "Reads/writes files based on root argument" 11 │ * @mitigates #init against #path-traversal using #path-validation -- "join() with explicit root constrains file access" 12 │ * @exposes #init to #data-exposure [low] cwe:CWE-200 -- "Writes API key config to .guardlink/config.json" 13 │ * @audit #init -- "Config file may contain API keys; .gitignore entry added automatically" 14 │ * @flows ProjectRoot -> #init via options.root -- "Project root input" 15 │ * @flows #init -> AgentFiles via writeFileSync -- "Agent instruction file writes" 16 │ * @flows #init -> ConfigFile via writeFileSync -- "Config file write" 17 │ * @handles internal on #init -- "Generates definitions and agent instruction content" 18 │ */ 19 │
L15 flow #init → AgentFiles
Agent instruction file writes
10 │ * @exposes #init to #path-traversal [medium] cwe:CWE-22 -- "Reads/writes files based on root argument" 11 │ * @mitigates #init against #path-traversal using #path-validation -- "join() with explicit root constrains file access" 12 │ * @exposes #init to #data-exposure [low] cwe:CWE-200 -- "Writes API key config to .guardlink/config.json" 13 │ * @audit #init -- "Config file may contain API keys; .gitignore entry added automatically" 14 │ * @flows ProjectRoot -> #init via options.root -- "Project root input" 15 │ * @flows #init -> AgentFiles via writeFileSync -- "Agent instruction file writes" 16 │ * @flows #init -> ConfigFile via writeFileSync -- "Config file write" 17 │ * @handles internal on #init -- "Generates definitions and agent instruction content" 18 │ */ 19 │ 20 │ import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync, statSync } from 'node:fs';
L16 flow #init → ConfigFile
Config file write
11 │ * @mitigates #init against #path-traversal using #path-validation -- "join() with explicit root constrains file access" 12 │ * @exposes #init to #data-exposure [low] cwe:CWE-200 -- "Writes API key config to .guardlink/config.json" 13 │ * @audit #init -- "Config file may contain API keys; .gitignore entry added automatically" 14 │ * @flows ProjectRoot -> #init via options.root -- "Project root input" 15 │ * @flows #init -> AgentFiles via writeFileSync -- "Agent instruction file writes" 16 │ * @flows #init -> ConfigFile via writeFileSync -- "Config file write" 17 │ * @handles internal on #init -- "Generates definitions and agent instruction content" 18 │ */ 19 │ 20 │ import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync, statSync } from 'node:fs'; 21 │ import { join, dirname } from 'node:path';
L17 handles #init: internal
Generates definitions and agent instruction content
12 │ * @exposes #init to #data-exposure [low] cwe:CWE-200 -- "Writes API key config to .guardlink/config.json" 13 │ * @audit #init -- "Config file may contain API keys; .gitignore entry added automatically" 14 │ * @flows ProjectRoot -> #init via options.root -- "Project root input" 15 │ * @flows #init -> AgentFiles via writeFileSync -- "Agent instruction file writes" 16 │ * @flows #init -> ConfigFile via writeFileSync -- "Config file write" 17 │ * @handles internal on #init -- "Generates definitions and agent instruction content" 18 │ */ 19 │ 20 │ import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync, statSync } from 'node:fs'; 21 │ import { join, dirname } from 'node:path'; 22 │ import { detectProject, type ProjectInfo } from './detect.js';
L511 exposes #init → #arbitrary-write
Creates directories for agent-file writes
506 │ } 507 │ 508 │ /** 509 │ * Ensure a directory exists, creating it if needed. 510 │ * 511 │ * @exposes #init to #arbitrary-write [high] cwe:CWE-73 -- "Creates directories for agent-file writes" 512 │ * @mitigates #init against #arbitrary-write using #path-validation -- "callers pass join(root, ...) constrained paths" 513 │ * 514 │ * Throws GuardLinkPathConflictError if the path already exists but is a FILE, not a 515 │ * directory. This happens when a project ships an older single-file agent config (e.g. 516 │ * a `.cursor/rules` file) where GuardLink expects the newer directory layout
L512 mitigates #path-validation mitigates #arbitrary-write
callers pass join(root, ...) constrained paths
507 │ 508 │ /** 509 │ * Ensure a directory exists, creating it if needed. 510 │ * 511 │ * @exposes #init to #arbitrary-write [high] cwe:CWE-73 -- "Creates directories for agent-file writes" 512 │ * @mitigates #init against #arbitrary-write using #path-validation -- "callers pass join(root, ...) constrained paths" 513 │ * 514 │ * Throws GuardLinkPathConflictError if the path already exists but is a FILE, not a 515 │ * directory. This happens when a project ships an older single-file agent config (e.g. 516 │ * a `.cursor/rules` file) where GuardLink expects the newer directory layout 517 │ * (`.cursor/rules/`). Without this guard, mkdirSync no-ops on the existing file and the
src/mcp/suggest.ts 1 openlow exposes 3mitigates 2flow 2audit 1 open 9
L12 exposes #suggest → #path-traversal
File path from MCP client joined with root
7 │ * - HTTP handlers, auth checks, input parsing 8 │ * - Missing annotations on files that handle sensitive data 9 │ * 10 │ * Designed for both file-based and diff-based analysis (§8.2). 11 │ * 12 │ * @exposes #suggest to #path-traversal [high] cwe:CWE-22 -- "File path from MCP client joined with root" 13 │ * @mitigates #suggest against #path-traversal using #path-validation -- "join() with validated root constrains access" 14 │ * @exposes #suggest to #redos [medium] cwe:CWE-1333 -- "Complex regex patterns applied to source code" 15 │ * @mitigates #suggest against #redos using #regex-anchoring -- "Patterns designed with bounded quantifiers" 16 │ * @exposes #suggest to #dos [low] cwe:CWE-400 -- "Large files loaded into memory for pattern scanning" 17 │ * @audit #suggest -- "File size is bounded by project scope; production use involves reasonable file sizes"
L13 mitigates #path-validation mitigates #path-traversal
join() with validated root constrains access
8 │ * - Missing annotations on files that handle sensitive data 9 │ * 10 │ * Designed for both file-based and diff-based analysis (§8.2). 11 │ * 12 │ * @exposes #suggest to #path-traversal [high] cwe:CWE-22 -- "File path from MCP client joined with root" 13 │ * @mitigates #suggest against #path-traversal using #path-validation -- "join() with validated root constrains access" 14 │ * @exposes #suggest to #redos [medium] cwe:CWE-1333 -- "Complex regex patterns applied to source code" 15 │ * @mitigates #suggest against #redos using #regex-anchoring -- "Patterns designed with bounded quantifiers" 16 │ * @exposes #suggest to #dos [low] cwe:CWE-400 -- "Large files loaded into memory for pattern scanning" 17 │ * @audit #suggest -- "File size is bounded by project scope; production use involves reasonable file sizes" 18 │ * @flows FilePath -> #suggest via readFileSync -- "File read path"
L14 exposes #suggest → #redos
Complex regex patterns applied to source code
9 │ * 10 │ * Designed for both file-based and diff-based analysis (§8.2). 11 │ * 12 │ * @exposes #suggest to #path-traversal [high] cwe:CWE-22 -- "File path from MCP client joined with root" 13 │ * @mitigates #suggest against #path-traversal using #path-validation -- "join() with validated root constrains access" 14 │ * @exposes #suggest to #redos [medium] cwe:CWE-1333 -- "Complex regex patterns applied to source code" 15 │ * @mitigates #suggest against #redos using #regex-anchoring -- "Patterns designed with bounded quantifiers" 16 │ * @exposes #suggest to #dos [low] cwe:CWE-400 -- "Large files loaded into memory for pattern scanning" 17 │ * @audit #suggest -- "File size is bounded by project scope; production use involves reasonable file sizes" 18 │ * @flows FilePath -> #suggest via readFileSync -- "File read path" 19 │ * @flows #suggest -> Suggestions via suggestAnnotations -- "Suggestion output"
L15 mitigates #regex-anchoring mitigates #redos
Patterns designed with bounded quantifiers
10 │ * Designed for both file-based and diff-based analysis (§8.2). 11 │ * 12 │ * @exposes #suggest to #path-traversal [high] cwe:CWE-22 -- "File path from MCP client joined with root" 13 │ * @mitigates #suggest against #path-traversal using #path-validation -- "join() with validated root constrains access" 14 │ * @exposes #suggest to #redos [medium] cwe:CWE-1333 -- "Complex regex patterns applied to source code" 15 │ * @mitigates #suggest against #redos using #regex-anchoring -- "Patterns designed with bounded quantifiers" 16 │ * @exposes #suggest to #dos [low] cwe:CWE-400 -- "Large files loaded into memory for pattern scanning" 17 │ * @audit #suggest -- "File size is bounded by project scope; production use involves reasonable file sizes" 18 │ * @flows FilePath -> #suggest via readFileSync -- "File read path" 19 │ * @flows #suggest -> Suggestions via suggestAnnotations -- "Suggestion output" 20 │ * @comment -- "Skips node_modules and .guardlink directories"
L16 exposes #suggest → #dos
Large files loaded into memory for pattern scanning
11 │ * 12 │ * @exposes #suggest to #path-traversal [high] cwe:CWE-22 -- "File path from MCP client joined with root" 13 │ * @mitigates #suggest against #path-traversal using #path-validation -- "join() with validated root constrains access" 14 │ * @exposes #suggest to #redos [medium] cwe:CWE-1333 -- "Complex regex patterns applied to source code" 15 │ * @mitigates #suggest against #redos using #regex-anchoring -- "Patterns designed with bounded quantifiers" 16 │ * @exposes #suggest to #dos [low] cwe:CWE-400 -- "Large files loaded into memory for pattern scanning" 17 │ * @audit #suggest -- "File size is bounded by project scope; production use involves reasonable file sizes" 18 │ * @flows FilePath -> #suggest via readFileSync -- "File read path" 19 │ * @flows #suggest -> Suggestions via suggestAnnotations -- "Suggestion output" 20 │ * @comment -- "Skips node_modules and .guardlink directories" 21 │ */
L17 audit Audit: #suggest
File size is bounded by project scope; production use involves reasonable file sizes
12 │ * @exposes #suggest to #path-traversal [high] cwe:CWE-22 -- "File path from MCP client joined with root" 13 │ * @mitigates #suggest against #path-traversal using #path-validation -- "join() with validated root constrains access" 14 │ * @exposes #suggest to #redos [medium] cwe:CWE-1333 -- "Complex regex patterns applied to source code" 15 │ * @mitigates #suggest against #redos using #regex-anchoring -- "Patterns designed with bounded quantifiers" 16 │ * @exposes #suggest to #dos [low] cwe:CWE-400 -- "Large files loaded into memory for pattern scanning" 17 │ * @audit #suggest -- "File size is bounded by project scope; production use involves reasonable file sizes" 18 │ * @flows FilePath -> #suggest via readFileSync -- "File read path" 19 │ * @flows #suggest -> Suggestions via suggestAnnotations -- "Suggestion output" 20 │ * @comment -- "Skips node_modules and .guardlink directories" 21 │ */ 22 │
L18 flow FilePath → #suggest
File read path
13 │ * @mitigates #suggest against #path-traversal using #path-validation -- "join() with validated root constrains access" 14 │ * @exposes #suggest to #redos [medium] cwe:CWE-1333 -- "Complex regex patterns applied to source code" 15 │ * @mitigates #suggest against #redos using #regex-anchoring -- "Patterns designed with bounded quantifiers" 16 │ * @exposes #suggest to #dos [low] cwe:CWE-400 -- "Large files loaded into memory for pattern scanning" 17 │ * @audit #suggest -- "File size is bounded by project scope; production use involves reasonable file sizes" 18 │ * @flows FilePath -> #suggest via readFileSync -- "File read path" 19 │ * @flows #suggest -> Suggestions via suggestAnnotations -- "Suggestion output" 20 │ * @comment -- "Skips node_modules and .guardlink directories" 21 │ */ 22 │ 23 │ import { readFileSync, existsSync } from 'node:fs';
L19 flow #suggest → Suggestions
Suggestion output
14 │ * @exposes #suggest to #redos [medium] cwe:CWE-1333 -- "Complex regex patterns applied to source code" 15 │ * @mitigates #suggest against #redos using #regex-anchoring -- "Patterns designed with bounded quantifiers" 16 │ * @exposes #suggest to #dos [low] cwe:CWE-400 -- "Large files loaded into memory for pattern scanning" 17 │ * @audit #suggest -- "File size is bounded by project scope; production use involves reasonable file sizes" 18 │ * @flows FilePath -> #suggest via readFileSync -- "File read path" 19 │ * @flows #suggest -> Suggestions via suggestAnnotations -- "Suggestion output" 20 │ * @comment -- "Skips node_modules and .guardlink directories" 21 │ */ 22 │ 23 │ import { readFileSync, existsSync } from 'node:fs'; 24 │ import { join } from 'node:path';
L20 comment Skips node_modules and .guardlink directories
Skips node_modules and .guardlink directories
15 │ * @mitigates #suggest against #redos using #regex-anchoring -- "Patterns designed with bounded quantifiers" 16 │ * @exposes #suggest to #dos [low] cwe:CWE-400 -- "Large files loaded into memory for pattern scanning" 17 │ * @audit #suggest -- "File size is bounded by project scope; production use involves reasonable file sizes" 18 │ * @flows FilePath -> #suggest via readFileSync -- "File read path" 19 │ * @flows #suggest -> Suggestions via suggestAnnotations -- "Suggestion output" 20 │ * @comment -- "Skips node_modules and .guardlink directories" 21 │ */ 22 │ 23 │ import { readFileSync, existsSync } from 'node:fs'; 24 │ import { join } from 'node:path'; 25 │ import type { ThreatModel } from '../types/index.js';
src/parser/migrate-mode.ts 1 openlow exposes 2flow 2mitigates 1audit 1 open 7
L24 exposes #parser → #arbitrary-write
Rewrites source files and creates sidecars across the project
19 │ * 20 │ * Only annotation lines are removed from source; surrounding comment structure 21 │ * is left exactly as it was. That is what makes the round trip reproduce the 22 │ * original file rather than an equivalent one. 23 │ * 24 │ * @exposes #parser to #arbitrary-write [high] cwe:CWE-73 -- "Rewrites source files and creates sidecars across the project" 25 │ * @mitigates #parser against #arbitrary-write using #path-validation -- "Only files already in the parsed model are touched; sidecar targets come from resolveGalPath, never from input" 26 │ * @exposes #parser to #data-exposure [low] -- "Reads every annotated source file into memory" 27 │ * @flows ThreatModel -> #parser via migrateAnnotationMode -- "Model drives which files are rewritten" 28 │ * @flows #parser -> FileSystem via writeFileSync -- "Source rewrite and sidecar creation" 29 │ * @audit #parser -- "Migration is destructive by nature — the dry-run path and the hash check are the safety net, and both should be exercised before this is trusted on a repo without clean version control"
L25 mitigates #path-validation mitigates #arbitrary-write
Only files already in the parsed model are touched; sidecar targets come from resolveGalPath, never from input
20 │ * Only annotation lines are removed from source; surrounding comment structure 21 │ * is left exactly as it was. That is what makes the round trip reproduce the 22 │ * original file rather than an equivalent one. 23 │ * 24 │ * @exposes #parser to #arbitrary-write [high] cwe:CWE-73 -- "Rewrites source files and creates sidecars across the project" 25 │ * @mitigates #parser against #arbitrary-write using #path-validation -- "Only files already in the parsed model are touched; sidecar targets come from resolveGalPath, never from input" 26 │ * @exposes #parser to #data-exposure [low] -- "Reads every annotated source file into memory" 27 │ * @flows ThreatModel -> #parser via migrateAnnotationMode -- "Model drives which files are rewritten" 28 │ * @flows #parser -> FileSystem via writeFileSync -- "Source rewrite and sidecar creation" 29 │ * @audit #parser -- "Migration is destructive by nature — the dry-run path and the hash check are the safety net, and both should be exercised before this is trusted on a repo without clean version control" 30 │ * @comment -- "Restoring a prefix is inferred from context (block comment vs line comment) rather than recorded in the .gal — recording it would put presentation data in the threat model"
L26 exposes #parser → #data-exposure
Reads every annotated source file into memory
21 │ * is left exactly as it was. That is what makes the round trip reproduce the 22 │ * original file rather than an equivalent one. 23 │ * 24 │ * @exposes #parser to #arbitrary-write [high] cwe:CWE-73 -- "Rewrites source files and creates sidecars across the project" 25 │ * @mitigates #parser against #arbitrary-write using #path-validation -- "Only files already in the parsed model are touched; sidecar targets come from resolveGalPath, never from input" 26 │ * @exposes #parser to #data-exposure [low] -- "Reads every annotated source file into memory" 27 │ * @flows ThreatModel -> #parser via migrateAnnotationMode -- "Model drives which files are rewritten" 28 │ * @flows #parser -> FileSystem via writeFileSync -- "Source rewrite and sidecar creation" 29 │ * @audit #parser -- "Migration is destructive by nature — the dry-run path and the hash check are the safety net, and both should be exercised before this is trusted on a repo without clean version control" 30 │ * @comment -- "Restoring a prefix is inferred from context (block comment vs line comment) rather than recorded in the .gal — recording it would put presentation data in the threat model" 31 │ */
L27 flow ThreatModel → #parser
Model drives which files are rewritten
22 │ * original file rather than an equivalent one. 23 │ * 24 │ * @exposes #parser to #arbitrary-write [high] cwe:CWE-73 -- "Rewrites source files and creates sidecars across the project" 25 │ * @mitigates #parser against #arbitrary-write using #path-validation -- "Only files already in the parsed model are touched; sidecar targets come from resolveGalPath, never from input" 26 │ * @exposes #parser to #data-exposure [low] -- "Reads every annotated source file into memory" 27 │ * @flows ThreatModel -> #parser via migrateAnnotationMode -- "Model drives which files are rewritten" 28 │ * @flows #parser -> FileSystem via writeFileSync -- "Source rewrite and sidecar creation" 29 │ * @audit #parser -- "Migration is destructive by nature — the dry-run path and the hash check are the safety net, and both should be exercised before this is trusted on a repo without clean version control" 30 │ * @comment -- "Restoring a prefix is inferred from context (block comment vs line comment) rather than recorded in the .gal — recording it would put presentation data in the threat model" 31 │ */ 32 │
L28 flow #parser → FileSystem
Source rewrite and sidecar creation
23 │ * 24 │ * @exposes #parser to #arbitrary-write [high] cwe:CWE-73 -- "Rewrites source files and creates sidecars across the project" 25 │ * @mitigates #parser against #arbitrary-write using #path-validation -- "Only files already in the parsed model are touched; sidecar targets come from resolveGalPath, never from input" 26 │ * @exposes #parser to #data-exposure [low] -- "Reads every annotated source file into memory" 27 │ * @flows ThreatModel -> #parser via migrateAnnotationMode -- "Model drives which files are rewritten" 28 │ * @flows #parser -> FileSystem via writeFileSync -- "Source rewrite and sidecar creation" 29 │ * @audit #parser -- "Migration is destructive by nature — the dry-run path and the hash check are the safety net, and both should be exercised before this is trusted on a repo without clean version control" 30 │ * @comment -- "Restoring a prefix is inferred from context (block comment vs line comment) rather than recorded in the .gal — recording it would put presentation data in the threat model" 31 │ */ 32 │ 33 │ import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync, rmdirSync, statSync } from 'node:fs';
L29 audit Audit: #parser
Migration is destructive by nature — the dry-run path and the hash check are the safety net, and both should be exercised before this is trusted on a repo without clean version control
24 │ * @exposes #parser to #arbitrary-write [high] cwe:CWE-73 -- "Rewrites source files and creates sidecars across the project" 25 │ * @mitigates #parser against #arbitrary-write using #path-validation -- "Only files already in the parsed model are touched; sidecar targets come from resolveGalPath, never from input" 26 │ * @exposes #parser to #data-exposure [low] -- "Reads every annotated source file into memory" 27 │ * @flows ThreatModel -> #parser via migrateAnnotationMode -- "Model drives which files are rewritten" 28 │ * @flows #parser -> FileSystem via writeFileSync -- "Source rewrite and sidecar creation" 29 │ * @audit #parser -- "Migration is destructive by nature — the dry-run path and the hash check are the safety net, and both should be exercised before this is trusted on a repo without clean version control" 30 │ * @comment -- "Restoring a prefix is inferred from context (block comment vs line comment) rather than recorded in the .gal — recording it would put presentation data in the threat model" 31 │ */ 32 │ 33 │ import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync, rmdirSync, statSync } from 'node:fs'; 34 │ import { dirname, extname, join } from 'node:path';
L30 comment Restoring a prefix is inferred from context (block comment vs line comment) rather than recorded in the .gal — recording it would put presentation data in the threat model
Restoring a prefix is inferred from context (block comment vs line comment) rather than recorded in the .gal — recording it would put presentation data in the threat model
25 │ * @mitigates #parser against #arbitrary-write using #path-validation -- "Only files already in the parsed model are touched; sidecar targets come from resolveGalPath, never from input" 26 │ * @exposes #parser to #data-exposure [low] -- "Reads every annotated source file into memory" 27 │ * @flows ThreatModel -> #parser via migrateAnnotationMode -- "Model drives which files are rewritten" 28 │ * @flows #parser -> FileSystem via writeFileSync -- "Source rewrite and sidecar creation" 29 │ * @audit #parser -- "Migration is destructive by nature — the dry-run path and the hash check are the safety net, and both should be exercised before this is trusted on a repo without clean version control" 30 │ * @comment -- "Restoring a prefix is inferred from context (block comment vs line comment) rather than recorded in the .gal — recording it would put presentation data in the threat model" 31 │ */ 32 │ 33 │ import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync, rmdirSync, statSync } from 'node:fs'; 34 │ import { dirname, extname, join } from 'node:path'; 35 │ import { stripCommentPrefix, commentStyleForExt } from './comment-strip.js';
.guardlink/definitions.ts asset 14threat 13control 13actor 3 open 43
L15 asset GuardLink.Parser
Reads source files from disk, extracts security annotations using regex patterns
10 │ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 11 │ 12 │ // ─── ASSETS ─────────────────────────────────────────────────────────── 13 │ // Components that process data, handle user input, or interact with external systems 14 │ 15 │ // @asset GuardLink.Parser (#parser) -- "Reads source files from disk, extracts security annotations using regex patterns" 16 │ // @asset GuardLink.CLI (#cli) -- "Command-line interface, handles user arguments, invokes subcommands" 17 │ // @asset GuardLink.TUI (#tui) -- "Interactive terminal interface with readline input and command dispatch" 18 │ // @asset GuardLink.MCP (#mcp) -- "Model Context Protocol server, accepts tool calls from AI agents over stdio" 19 │ // @asset GuardLink.LLM_Client (#llm-client) -- "Makes HTTP requests to external AI providers (Anthropic, OpenAI, DeepSeek, OpenRouter)" 20 │ // @asset GuardLink.Dashboard (#dashboard) -- "Generates interactive HTML threat model dashboard from ThreatModel data"
L16 asset GuardLink.CLI
Command-line interface, handles user arguments, invokes subcommands
11 │ 12 │ // ─── ASSETS ─────────────────────────────────────────────────────────── 13 │ // Components that process data, handle user input, or interact with external systems 14 │ 15 │ // @asset GuardLink.Parser (#parser) -- "Reads source files from disk, extracts security annotations using regex patterns" 16 │ // @asset GuardLink.CLI (#cli) -- "Command-line interface, handles user arguments, invokes subcommands" 17 │ // @asset GuardLink.TUI (#tui) -- "Interactive terminal interface with readline input and command dispatch" 18 │ // @asset GuardLink.MCP (#mcp) -- "Model Context Protocol server, accepts tool calls from AI agents over stdio" 19 │ // @asset GuardLink.LLM_Client (#llm-client) -- "Makes HTTP requests to external AI providers (Anthropic, OpenAI, DeepSeek, OpenRouter)" 20 │ // @asset GuardLink.Dashboard (#dashboard) -- "Generates interactive HTML threat model dashboard from ThreatModel data" 21 │ // @asset GuardLink.Init (#init) -- "Initializes projects, writes config files and agent instruction files to disk"
L17 asset GuardLink.TUI
Interactive terminal interface with readline input and command dispatch
12 │ // ─── ASSETS ─────────────────────────────────────────────────────────── 13 │ // Components that process data, handle user input, or interact with external systems 14 │ 15 │ // @asset GuardLink.Parser (#parser) -- "Reads source files from disk, extracts security annotations using regex patterns" 16 │ // @asset GuardLink.CLI (#cli) -- "Command-line interface, handles user arguments, invokes subcommands" 17 │ // @asset GuardLink.TUI (#tui) -- "Interactive terminal interface with readline input and command dispatch" 18 │ // @asset GuardLink.MCP (#mcp) -- "Model Context Protocol server, accepts tool calls from AI agents over stdio" 19 │ // @asset GuardLink.LLM_Client (#llm-client) -- "Makes HTTP requests to external AI providers (Anthropic, OpenAI, DeepSeek, OpenRouter)" 20 │ // @asset GuardLink.Dashboard (#dashboard) -- "Generates interactive HTML threat model dashboard from ThreatModel data" 21 │ // @asset GuardLink.Init (#init) -- "Initializes projects, writes config files and agent instruction files to disk" 22 │ // @asset GuardLink.Agent_Launcher (#agent-launcher) -- "Spawns child processes for AI coding agents (Claude Code, Cursor, Codex)"
L18 asset GuardLink.MCP
Model Context Protocol server, accepts tool calls from AI agents over stdio
13 │ // Components that process data, handle user input, or interact with external systems 14 │ 15 │ // @asset GuardLink.Parser (#parser) -- "Reads source files from disk, extracts security annotations using regex patterns" 16 │ // @asset GuardLink.CLI (#cli) -- "Command-line interface, handles user arguments, invokes subcommands" 17 │ // @asset GuardLink.TUI (#tui) -- "Interactive terminal interface with readline input and command dispatch" 18 │ // @asset GuardLink.MCP (#mcp) -- "Model Context Protocol server, accepts tool calls from AI agents over stdio" 19 │ // @asset GuardLink.LLM_Client (#llm-client) -- "Makes HTTP requests to external AI providers (Anthropic, OpenAI, DeepSeek, OpenRouter)" 20 │ // @asset GuardLink.Dashboard (#dashboard) -- "Generates interactive HTML threat model dashboard from ThreatModel data" 21 │ // @asset GuardLink.Init (#init) -- "Initializes projects, writes config files and agent instruction files to disk" 22 │ // @asset GuardLink.Agent_Launcher (#agent-launcher) -- "Spawns child processes for AI coding agents (Claude Code, Cursor, Codex)" 23 │ // @asset GuardLink.Diff (#diff) -- "Compares threat models across git commits, invokes git commands"
L19 asset GuardLink.LLM_Client
Makes HTTP requests to external AI providers (Anthropic, OpenAI, DeepSeek, OpenRouter)
14 │ 15 │ // @asset GuardLink.Parser (#parser) -- "Reads source files from disk, extracts security annotations using regex patterns" 16 │ // @asset GuardLink.CLI (#cli) -- "Command-line interface, handles user arguments, invokes subcommands" 17 │ // @asset GuardLink.TUI (#tui) -- "Interactive terminal interface with readline input and command dispatch" 18 │ // @asset GuardLink.MCP (#mcp) -- "Model Context Protocol server, accepts tool calls from AI agents over stdio" 19 │ // @asset GuardLink.LLM_Client (#llm-client) -- "Makes HTTP requests to external AI providers (Anthropic, OpenAI, DeepSeek, OpenRouter)" 20 │ // @asset GuardLink.Dashboard (#dashboard) -- "Generates interactive HTML threat model dashboard from ThreatModel data" 21 │ // @asset GuardLink.Init (#init) -- "Initializes projects, writes config files and agent instruction files to disk" 22 │ // @asset GuardLink.Agent_Launcher (#agent-launcher) -- "Spawns child processes for AI coding agents (Claude Code, Cursor, Codex)" 23 │ // @asset GuardLink.Diff (#diff) -- "Compares threat models across git commits, invokes git commands" 24 │ // @asset GuardLink.Report (#report) -- "Generates markdown threat model reports with Mermaid diagrams"
L20 asset GuardLink.Dashboard
Generates interactive HTML threat model dashboard from ThreatModel data
15 │ // @asset GuardLink.Parser (#parser) -- "Reads source files from disk, extracts security annotations using regex patterns" 16 │ // @asset GuardLink.CLI (#cli) -- "Command-line interface, handles user arguments, invokes subcommands" 17 │ // @asset GuardLink.TUI (#tui) -- "Interactive terminal interface with readline input and command dispatch" 18 │ // @asset GuardLink.MCP (#mcp) -- "Model Context Protocol server, accepts tool calls from AI agents over stdio" 19 │ // @asset GuardLink.LLM_Client (#llm-client) -- "Makes HTTP requests to external AI providers (Anthropic, OpenAI, DeepSeek, OpenRouter)" 20 │ // @asset GuardLink.Dashboard (#dashboard) -- "Generates interactive HTML threat model dashboard from ThreatModel data" 21 │ // @asset GuardLink.Init (#init) -- "Initializes projects, writes config files and agent instruction files to disk" 22 │ // @asset GuardLink.Agent_Launcher (#agent-launcher) -- "Spawns child processes for AI coding agents (Claude Code, Cursor, Codex)" 23 │ // @asset GuardLink.Diff (#diff) -- "Compares threat models across git commits, invokes git commands" 24 │ // @asset GuardLink.Report (#report) -- "Generates markdown threat model reports with Mermaid diagrams" 25 │ // @asset GuardLink.SARIF (#sarif) -- "Exports findings as SARIF 2.1.0 JSON for security tooling"
L21 asset GuardLink.Init
Initializes projects, writes config files and agent instruction files to disk
16 │ // @asset GuardLink.CLI (#cli) -- "Command-line interface, handles user arguments, invokes subcommands" 17 │ // @asset GuardLink.TUI (#tui) -- "Interactive terminal interface with readline input and command dispatch" 18 │ // @asset GuardLink.MCP (#mcp) -- "Model Context Protocol server, accepts tool calls from AI agents over stdio" 19 │ // @asset GuardLink.LLM_Client (#llm-client) -- "Makes HTTP requests to external AI providers (Anthropic, OpenAI, DeepSeek, OpenRouter)" 20 │ // @asset GuardLink.Dashboard (#dashboard) -- "Generates interactive HTML threat model dashboard from ThreatModel data" 21 │ // @asset GuardLink.Init (#init) -- "Initializes projects, writes config files and agent instruction files to disk" 22 │ // @asset GuardLink.Agent_Launcher (#agent-launcher) -- "Spawns child processes for AI coding agents (Claude Code, Cursor, Codex)" 23 │ // @asset GuardLink.Diff (#diff) -- "Compares threat models across git commits, invokes git commands" 24 │ // @asset GuardLink.Report (#report) -- "Generates markdown threat model reports with Mermaid diagrams" 25 │ // @asset GuardLink.SARIF (#sarif) -- "Exports findings as SARIF 2.1.0 JSON for security tooling" 26 │ // @asset GuardLink.Suggest (#suggest) -- "Analyzes code patterns to suggest appropriate security annotations"
L22 asset GuardLink.Agent_Launcher
Spawns child processes for AI coding agents (Claude Code, Cursor, Codex)
17 │ // @asset GuardLink.TUI (#tui) -- "Interactive terminal interface with readline input and command dispatch" 18 │ // @asset GuardLink.MCP (#mcp) -- "Model Context Protocol server, accepts tool calls from AI agents over stdio" 19 │ // @asset GuardLink.LLM_Client (#llm-client) -- "Makes HTTP requests to external AI providers (Anthropic, OpenAI, DeepSeek, OpenRouter)" 20 │ // @asset GuardLink.Dashboard (#dashboard) -- "Generates interactive HTML threat model dashboard from ThreatModel data" 21 │ // @asset GuardLink.Init (#init) -- "Initializes projects, writes config files and agent instruction files to disk" 22 │ // @asset GuardLink.Agent_Launcher (#agent-launcher) -- "Spawns child processes for AI coding agents (Claude Code, Cursor, Codex)" 23 │ // @asset GuardLink.Diff (#diff) -- "Compares threat models across git commits, invokes git commands" 24 │ // @asset GuardLink.Report (#report) -- "Generates markdown threat model reports with Mermaid diagrams" 25 │ // @asset GuardLink.SARIF (#sarif) -- "Exports findings as SARIF 2.1.0 JSON for security tooling" 26 │ // @asset GuardLink.Suggest (#suggest) -- "Analyzes code patterns to suggest appropriate security annotations" 27 │ // @asset GuardLink.Blame (#blame) -- "Maps annotated code spans to git history: authors, commits, AI co-author trailers"
L23 asset GuardLink.Diff
Compares threat models across git commits, invokes git commands
18 │ // @asset GuardLink.MCP (#mcp) -- "Model Context Protocol server, accepts tool calls from AI agents over stdio" 19 │ // @asset GuardLink.LLM_Client (#llm-client) -- "Makes HTTP requests to external AI providers (Anthropic, OpenAI, DeepSeek, OpenRouter)" 20 │ // @asset GuardLink.Dashboard (#dashboard) -- "Generates interactive HTML threat model dashboard from ThreatModel data" 21 │ // @asset GuardLink.Init (#init) -- "Initializes projects, writes config files and agent instruction files to disk" 22 │ // @asset GuardLink.Agent_Launcher (#agent-launcher) -- "Spawns child processes for AI coding agents (Claude Code, Cursor, Codex)" 23 │ // @asset GuardLink.Diff (#diff) -- "Compares threat models across git commits, invokes git commands" 24 │ // @asset GuardLink.Report (#report) -- "Generates markdown threat model reports with Mermaid diagrams" 25 │ // @asset GuardLink.SARIF (#sarif) -- "Exports findings as SARIF 2.1.0 JSON for security tooling" 26 │ // @asset GuardLink.Suggest (#suggest) -- "Analyzes code patterns to suggest appropriate security annotations" 27 │ // @asset GuardLink.Blame (#blame) -- "Maps annotated code spans to git history: authors, commits, AI co-author trailers" 28 │
L24 asset GuardLink.Report
Generates markdown threat model reports with Mermaid diagrams
19 │ // @asset GuardLink.LLM_Client (#llm-client) -- "Makes HTTP requests to external AI providers (Anthropic, OpenAI, DeepSeek, OpenRouter)" 20 │ // @asset GuardLink.Dashboard (#dashboard) -- "Generates interactive HTML threat model dashboard from ThreatModel data" 21 │ // @asset GuardLink.Init (#init) -- "Initializes projects, writes config files and agent instruction files to disk" 22 │ // @asset GuardLink.Agent_Launcher (#agent-launcher) -- "Spawns child processes for AI coding agents (Claude Code, Cursor, Codex)" 23 │ // @asset GuardLink.Diff (#diff) -- "Compares threat models across git commits, invokes git commands" 24 │ // @asset GuardLink.Report (#report) -- "Generates markdown threat model reports with Mermaid diagrams" 25 │ // @asset GuardLink.SARIF (#sarif) -- "Exports findings as SARIF 2.1.0 JSON for security tooling" 26 │ // @asset GuardLink.Suggest (#suggest) -- "Analyzes code patterns to suggest appropriate security annotations" 27 │ // @asset GuardLink.Blame (#blame) -- "Maps annotated code spans to git history: authors, commits, AI co-author trailers" 28 │ 29 │ // ─── THREATS ──────────────────────────────────────────────────────────
L25 asset GuardLink.SARIF
Exports findings as SARIF 2.1.0 JSON for security tooling
20 │ // @asset GuardLink.Dashboard (#dashboard) -- "Generates interactive HTML threat model dashboard from ThreatModel data" 21 │ // @asset GuardLink.Init (#init) -- "Initializes projects, writes config files and agent instruction files to disk" 22 │ // @asset GuardLink.Agent_Launcher (#agent-launcher) -- "Spawns child processes for AI coding agents (Claude Code, Cursor, Codex)" 23 │ // @asset GuardLink.Diff (#diff) -- "Compares threat models across git commits, invokes git commands" 24 │ // @asset GuardLink.Report (#report) -- "Generates markdown threat model reports with Mermaid diagrams" 25 │ // @asset GuardLink.SARIF (#sarif) -- "Exports findings as SARIF 2.1.0 JSON for security tooling" 26 │ // @asset GuardLink.Suggest (#suggest) -- "Analyzes code patterns to suggest appropriate security annotations" 27 │ // @asset GuardLink.Blame (#blame) -- "Maps annotated code spans to git history: authors, commits, AI co-author trailers" 28 │ 29 │ // ─── THREATS ────────────────────────────────────────────────────────── 30 │ // Security threats that can impact the application
L26 asset GuardLink.Suggest
Analyzes code patterns to suggest appropriate security annotations
21 │ // @asset GuardLink.Init (#init) -- "Initializes projects, writes config files and agent instruction files to disk" 22 │ // @asset GuardLink.Agent_Launcher (#agent-launcher) -- "Spawns child processes for AI coding agents (Claude Code, Cursor, Codex)" 23 │ // @asset GuardLink.Diff (#diff) -- "Compares threat models across git commits, invokes git commands" 24 │ // @asset GuardLink.Report (#report) -- "Generates markdown threat model reports with Mermaid diagrams" 25 │ // @asset GuardLink.SARIF (#sarif) -- "Exports findings as SARIF 2.1.0 JSON for security tooling" 26 │ // @asset GuardLink.Suggest (#suggest) -- "Analyzes code patterns to suggest appropriate security annotations" 27 │ // @asset GuardLink.Blame (#blame) -- "Maps annotated code spans to git history: authors, commits, AI co-author trailers" 28 │ 29 │ // ─── THREATS ────────────────────────────────────────────────────────── 30 │ // Security threats that can impact the application 31 │
L27 asset GuardLink.Blame
Maps annotated code spans to git history: authors, commits, AI co-author trailers
22 │ // @asset GuardLink.Agent_Launcher (#agent-launcher) -- "Spawns child processes for AI coding agents (Claude Code, Cursor, Codex)" 23 │ // @asset GuardLink.Diff (#diff) -- "Compares threat models across git commits, invokes git commands" 24 │ // @asset GuardLink.Report (#report) -- "Generates markdown threat model reports with Mermaid diagrams" 25 │ // @asset GuardLink.SARIF (#sarif) -- "Exports findings as SARIF 2.1.0 JSON for security tooling" 26 │ // @asset GuardLink.Suggest (#suggest) -- "Analyzes code patterns to suggest appropriate security annotations" 27 │ // @asset GuardLink.Blame (#blame) -- "Maps annotated code spans to git history: authors, commits, AI co-author trailers" 28 │ 29 │ // ─── THREATS ────────────────────────────────────────────────────────── 30 │ // Security threats that can impact the application 31 │ 32 │ // @threat Path_Traversal (#path-traversal) [high] cwe:CWE-22 -- "File read/write operations outside intended project directory via ../ sequences or absolute paths"
L32 threat Path_Traversal
File read/write operations outside intended project directory via ../ sequences or absolute paths
27 │ // @asset GuardLink.Blame (#blame) -- "Maps annotated code spans to git history: authors, commits, AI co-author trailers" 28 │ 29 │ // ─── THREATS ────────────────────────────────────────────────────────── 30 │ // Security threats that can impact the application 31 │ 32 │ // @threat Path_Traversal (#path-traversal) [high] cwe:CWE-22 -- "File read/write operations outside intended project directory via ../ sequences or absolute paths" 33 │ // @threat Command_Injection (#cmd-injection) [critical] cwe:CWE-78 -- "Shell command execution with unsanitized user input" 34 │ // @threat Cross_Site_Scripting (#xss) [high] cwe:CWE-79 -- "Injection of malicious scripts into generated HTML output" 35 │ // @threat API_Key_Exposure (#api-key-exposure) [high] cwe:CWE-798 -- "API keys leaked in logs, error messages, or unintended output" 36 │ // @threat Server_Side_Request_Forgery (#ssrf) [medium] cwe:CWE-918 -- "LLM API requests to attacker-controlled URLs via config override" 37 │ // @threat ReDoS (#redos) [medium] cwe:CWE-1333 -- "Regular expression denial of service from crafted annotation content"
L33 threat Command_Injection
Shell command execution with unsanitized user input
28 │ 29 │ // ─── THREATS ────────────────────────────────────────────────────────── 30 │ // Security threats that can impact the application 31 │ 32 │ // @threat Path_Traversal (#path-traversal) [high] cwe:CWE-22 -- "File read/write operations outside intended project directory via ../ sequences or absolute paths" 33 │ // @threat Command_Injection (#cmd-injection) [critical] cwe:CWE-78 -- "Shell command execution with unsanitized user input" 34 │ // @threat Cross_Site_Scripting (#xss) [high] cwe:CWE-79 -- "Injection of malicious scripts into generated HTML output" 35 │ // @threat API_Key_Exposure (#api-key-exposure) [high] cwe:CWE-798 -- "API keys leaked in logs, error messages, or unintended output" 36 │ // @threat Server_Side_Request_Forgery (#ssrf) [medium] cwe:CWE-918 -- "LLM API requests to attacker-controlled URLs via config override" 37 │ // @threat ReDoS (#redos) [medium] cwe:CWE-1333 -- "Regular expression denial of service from crafted annotation content" 38 │ // @threat Arbitrary_File_Write (#arbitrary-write) [high] cwe:CWE-73 -- "Writing files to attacker-controlled paths outside project"
L34 threat Cross_Site_Scripting
Injection of malicious scripts into generated HTML output
29 │ // ─── THREATS ────────────────────────────────────────────────────────── 30 │ // Security threats that can impact the application 31 │ 32 │ // @threat Path_Traversal (#path-traversal) [high] cwe:CWE-22 -- "File read/write operations outside intended project directory via ../ sequences or absolute paths" 33 │ // @threat Command_Injection (#cmd-injection) [critical] cwe:CWE-78 -- "Shell command execution with unsanitized user input" 34 │ // @threat Cross_Site_Scripting (#xss) [high] cwe:CWE-79 -- "Injection of malicious scripts into generated HTML output" 35 │ // @threat API_Key_Exposure (#api-key-exposure) [high] cwe:CWE-798 -- "API keys leaked in logs, error messages, or unintended output" 36 │ // @threat Server_Side_Request_Forgery (#ssrf) [medium] cwe:CWE-918 -- "LLM API requests to attacker-controlled URLs via config override" 37 │ // @threat ReDoS (#redos) [medium] cwe:CWE-1333 -- "Regular expression denial of service from crafted annotation content" 38 │ // @threat Arbitrary_File_Write (#arbitrary-write) [high] cwe:CWE-73 -- "Writing files to attacker-controlled paths outside project" 39 │ // @threat Prompt_Injection (#prompt-injection) [medium] cwe:CWE-77 -- "Malicious content in annotations injected into LLM prompts"
L35 threat API_Key_Exposure
API keys leaked in logs, error messages, or unintended output
30 │ // Security threats that can impact the application 31 │ 32 │ // @threat Path_Traversal (#path-traversal) [high] cwe:CWE-22 -- "File read/write operations outside intended project directory via ../ sequences or absolute paths" 33 │ // @threat Command_Injection (#cmd-injection) [critical] cwe:CWE-78 -- "Shell command execution with unsanitized user input" 34 │ // @threat Cross_Site_Scripting (#xss) [high] cwe:CWE-79 -- "Injection of malicious scripts into generated HTML output" 35 │ // @threat API_Key_Exposure (#api-key-exposure) [high] cwe:CWE-798 -- "API keys leaked in logs, error messages, or unintended output" 36 │ // @threat Server_Side_Request_Forgery (#ssrf) [medium] cwe:CWE-918 -- "LLM API requests to attacker-controlled URLs via config override" 37 │ // @threat ReDoS (#redos) [medium] cwe:CWE-1333 -- "Regular expression denial of service from crafted annotation content" 38 │ // @threat Arbitrary_File_Write (#arbitrary-write) [high] cwe:CWE-73 -- "Writing files to attacker-controlled paths outside project" 39 │ // @threat Prompt_Injection (#prompt-injection) [medium] cwe:CWE-77 -- "Malicious content in annotations injected into LLM prompts" 40 │ // @threat Denial_of_Service (#dos) [medium] cwe:CWE-400 -- "Resource exhaustion from processing large files or deep directory trees"
L36 threat Server_Side_Request_Forgery
LLM API requests to attacker-controlled URLs via config override
31 │ 32 │ // @threat Path_Traversal (#path-traversal) [high] cwe:CWE-22 -- "File read/write operations outside intended project directory via ../ sequences or absolute paths" 33 │ // @threat Command_Injection (#cmd-injection) [critical] cwe:CWE-78 -- "Shell command execution with unsanitized user input" 34 │ // @threat Cross_Site_Scripting (#xss) [high] cwe:CWE-79 -- "Injection of malicious scripts into generated HTML output" 35 │ // @threat API_Key_Exposure (#api-key-exposure) [high] cwe:CWE-798 -- "API keys leaked in logs, error messages, or unintended output" 36 │ // @threat Server_Side_Request_Forgery (#ssrf) [medium] cwe:CWE-918 -- "LLM API requests to attacker-controlled URLs via config override" 37 │ // @threat ReDoS (#redos) [medium] cwe:CWE-1333 -- "Regular expression denial of service from crafted annotation content" 38 │ // @threat Arbitrary_File_Write (#arbitrary-write) [high] cwe:CWE-73 -- "Writing files to attacker-controlled paths outside project" 39 │ // @threat Prompt_Injection (#prompt-injection) [medium] cwe:CWE-77 -- "Malicious content in annotations injected into LLM prompts" 40 │ // @threat Denial_of_Service (#dos) [medium] cwe:CWE-400 -- "Resource exhaustion from processing large files or deep directory trees" 41 │ // @threat Sensitive_Data_Exposure (#data-exposure) [medium] cwe:CWE-200 -- "Threat model details exposed to unauthorized parties"
L37 threat ReDoS
Regular expression denial of service from crafted annotation content
32 │ // @threat Path_Traversal (#path-traversal) [high] cwe:CWE-22 -- "File read/write operations outside intended project directory via ../ sequences or absolute paths" 33 │ // @threat Command_Injection (#cmd-injection) [critical] cwe:CWE-78 -- "Shell command execution with unsanitized user input" 34 │ // @threat Cross_Site_Scripting (#xss) [high] cwe:CWE-79 -- "Injection of malicious scripts into generated HTML output" 35 │ // @threat API_Key_Exposure (#api-key-exposure) [high] cwe:CWE-798 -- "API keys leaked in logs, error messages, or unintended output" 36 │ // @threat Server_Side_Request_Forgery (#ssrf) [medium] cwe:CWE-918 -- "LLM API requests to attacker-controlled URLs via config override" 37 │ // @threat ReDoS (#redos) [medium] cwe:CWE-1333 -- "Regular expression denial of service from crafted annotation content" 38 │ // @threat Arbitrary_File_Write (#arbitrary-write) [high] cwe:CWE-73 -- "Writing files to attacker-controlled paths outside project" 39 │ // @threat Prompt_Injection (#prompt-injection) [medium] cwe:CWE-77 -- "Malicious content in annotations injected into LLM prompts" 40 │ // @threat Denial_of_Service (#dos) [medium] cwe:CWE-400 -- "Resource exhaustion from processing large files or deep directory trees" 41 │ // @threat Sensitive_Data_Exposure (#data-exposure) [medium] cwe:CWE-200 -- "Threat model details exposed to unauthorized parties" 42 │ // @threat Insecure_Deserialization (#insecure-deser) [medium] cwe:CWE-502 -- "Unsafe parsing of JSON/YAML configuration files"
L38 threat Arbitrary_File_Write
Writing files to attacker-controlled paths outside project
33 │ // @threat Command_Injection (#cmd-injection) [critical] cwe:CWE-78 -- "Shell command execution with unsanitized user input" 34 │ // @threat Cross_Site_Scripting (#xss) [high] cwe:CWE-79 -- "Injection of malicious scripts into generated HTML output" 35 │ // @threat API_Key_Exposure (#api-key-exposure) [high] cwe:CWE-798 -- "API keys leaked in logs, error messages, or unintended output" 36 │ // @threat Server_Side_Request_Forgery (#ssrf) [medium] cwe:CWE-918 -- "LLM API requests to attacker-controlled URLs via config override" 37 │ // @threat ReDoS (#redos) [medium] cwe:CWE-1333 -- "Regular expression denial of service from crafted annotation content" 38 │ // @threat Arbitrary_File_Write (#arbitrary-write) [high] cwe:CWE-73 -- "Writing files to attacker-controlled paths outside project" 39 │ // @threat Prompt_Injection (#prompt-injection) [medium] cwe:CWE-77 -- "Malicious content in annotations injected into LLM prompts" 40 │ // @threat Denial_of_Service (#dos) [medium] cwe:CWE-400 -- "Resource exhaustion from processing large files or deep directory trees" 41 │ // @threat Sensitive_Data_Exposure (#data-exposure) [medium] cwe:CWE-200 -- "Threat model details exposed to unauthorized parties" 42 │ // @threat Insecure_Deserialization (#insecure-deser) [medium] cwe:CWE-502 -- "Unsafe parsing of JSON/YAML configuration files" 43 │ // @threat Child_Process_Injection (#child-proc-injection) [high] cwe:CWE-78 -- "Agent launcher executing attacker-controlled commands via process spawn"
L39 threat Prompt_Injection
Malicious content in annotations injected into LLM prompts
34 │ // @threat Cross_Site_Scripting (#xss) [high] cwe:CWE-79 -- "Injection of malicious scripts into generated HTML output" 35 │ // @threat API_Key_Exposure (#api-key-exposure) [high] cwe:CWE-798 -- "API keys leaked in logs, error messages, or unintended output" 36 │ // @threat Server_Side_Request_Forgery (#ssrf) [medium] cwe:CWE-918 -- "LLM API requests to attacker-controlled URLs via config override" 37 │ // @threat ReDoS (#redos) [medium] cwe:CWE-1333 -- "Regular expression denial of service from crafted annotation content" 38 │ // @threat Arbitrary_File_Write (#arbitrary-write) [high] cwe:CWE-73 -- "Writing files to attacker-controlled paths outside project" 39 │ // @threat Prompt_Injection (#prompt-injection) [medium] cwe:CWE-77 -- "Malicious content in annotations injected into LLM prompts" 40 │ // @threat Denial_of_Service (#dos) [medium] cwe:CWE-400 -- "Resource exhaustion from processing large files or deep directory trees" 41 │ // @threat Sensitive_Data_Exposure (#data-exposure) [medium] cwe:CWE-200 -- "Threat model details exposed to unauthorized parties" 42 │ // @threat Insecure_Deserialization (#insecure-deser) [medium] cwe:CWE-502 -- "Unsafe parsing of JSON/YAML configuration files" 43 │ // @threat Child_Process_Injection (#child-proc-injection) [high] cwe:CWE-78 -- "Agent launcher executing attacker-controlled commands via process spawn" 44 │ // @threat Information_Disclosure (#info-disclosure) [low] cwe:CWE-200 -- "Unintended exposure of internal paths, structure, or implementation details"
L40 threat Denial_of_Service
Resource exhaustion from processing large files or deep directory trees
35 │ // @threat API_Key_Exposure (#api-key-exposure) [high] cwe:CWE-798 -- "API keys leaked in logs, error messages, or unintended output" 36 │ // @threat Server_Side_Request_Forgery (#ssrf) [medium] cwe:CWE-918 -- "LLM API requests to attacker-controlled URLs via config override" 37 │ // @threat ReDoS (#redos) [medium] cwe:CWE-1333 -- "Regular expression denial of service from crafted annotation content" 38 │ // @threat Arbitrary_File_Write (#arbitrary-write) [high] cwe:CWE-73 -- "Writing files to attacker-controlled paths outside project" 39 │ // @threat Prompt_Injection (#prompt-injection) [medium] cwe:CWE-77 -- "Malicious content in annotations injected into LLM prompts" 40 │ // @threat Denial_of_Service (#dos) [medium] cwe:CWE-400 -- "Resource exhaustion from processing large files or deep directory trees" 41 │ // @threat Sensitive_Data_Exposure (#data-exposure) [medium] cwe:CWE-200 -- "Threat model details exposed to unauthorized parties" 42 │ // @threat Insecure_Deserialization (#insecure-deser) [medium] cwe:CWE-502 -- "Unsafe parsing of JSON/YAML configuration files" 43 │ // @threat Child_Process_Injection (#child-proc-injection) [high] cwe:CWE-78 -- "Agent launcher executing attacker-controlled commands via process spawn" 44 │ // @threat Information_Disclosure (#info-disclosure) [low] cwe:CWE-200 -- "Unintended exposure of internal paths, structure, or implementation details" 45 │
L41 threat Sensitive_Data_Exposure
Threat model details exposed to unauthorized parties
36 │ // @threat Server_Side_Request_Forgery (#ssrf) [medium] cwe:CWE-918 -- "LLM API requests to attacker-controlled URLs via config override" 37 │ // @threat ReDoS (#redos) [medium] cwe:CWE-1333 -- "Regular expression denial of service from crafted annotation content" 38 │ // @threat Arbitrary_File_Write (#arbitrary-write) [high] cwe:CWE-73 -- "Writing files to attacker-controlled paths outside project" 39 │ // @threat Prompt_Injection (#prompt-injection) [medium] cwe:CWE-77 -- "Malicious content in annotations injected into LLM prompts" 40 │ // @threat Denial_of_Service (#dos) [medium] cwe:CWE-400 -- "Resource exhaustion from processing large files or deep directory trees" 41 │ // @threat Sensitive_Data_Exposure (#data-exposure) [medium] cwe:CWE-200 -- "Threat model details exposed to unauthorized parties" 42 │ // @threat Insecure_Deserialization (#insecure-deser) [medium] cwe:CWE-502 -- "Unsafe parsing of JSON/YAML configuration files" 43 │ // @threat Child_Process_Injection (#child-proc-injection) [high] cwe:CWE-78 -- "Agent launcher executing attacker-controlled commands via process spawn" 44 │ // @threat Information_Disclosure (#info-disclosure) [low] cwe:CWE-200 -- "Unintended exposure of internal paths, structure, or implementation details" 45 │ 46 │ // ─── CONTROLS ─────────────────────────────────────────────────────────
L42 threat Insecure_Deserialization
Unsafe parsing of JSON/YAML configuration files
37 │ // @threat ReDoS (#redos) [medium] cwe:CWE-1333 -- "Regular expression denial of service from crafted annotation content" 38 │ // @threat Arbitrary_File_Write (#arbitrary-write) [high] cwe:CWE-73 -- "Writing files to attacker-controlled paths outside project" 39 │ // @threat Prompt_Injection (#prompt-injection) [medium] cwe:CWE-77 -- "Malicious content in annotations injected into LLM prompts" 40 │ // @threat Denial_of_Service (#dos) [medium] cwe:CWE-400 -- "Resource exhaustion from processing large files or deep directory trees" 41 │ // @threat Sensitive_Data_Exposure (#data-exposure) [medium] cwe:CWE-200 -- "Threat model details exposed to unauthorized parties" 42 │ // @threat Insecure_Deserialization (#insecure-deser) [medium] cwe:CWE-502 -- "Unsafe parsing of JSON/YAML configuration files" 43 │ // @threat Child_Process_Injection (#child-proc-injection) [high] cwe:CWE-78 -- "Agent launcher executing attacker-controlled commands via process spawn" 44 │ // @threat Information_Disclosure (#info-disclosure) [low] cwe:CWE-200 -- "Unintended exposure of internal paths, structure, or implementation details" 45 │ 46 │ // ─── CONTROLS ───────────────────────────────────────────────────────── 47 │ // Security controls that mitigate threats
L43 threat Child_Process_Injection
Agent launcher executing attacker-controlled commands via process spawn
38 │ // @threat Arbitrary_File_Write (#arbitrary-write) [high] cwe:CWE-73 -- "Writing files to attacker-controlled paths outside project" 39 │ // @threat Prompt_Injection (#prompt-injection) [medium] cwe:CWE-77 -- "Malicious content in annotations injected into LLM prompts" 40 │ // @threat Denial_of_Service (#dos) [medium] cwe:CWE-400 -- "Resource exhaustion from processing large files or deep directory trees" 41 │ // @threat Sensitive_Data_Exposure (#data-exposure) [medium] cwe:CWE-200 -- "Threat model details exposed to unauthorized parties" 42 │ // @threat Insecure_Deserialization (#insecure-deser) [medium] cwe:CWE-502 -- "Unsafe parsing of JSON/YAML configuration files" 43 │ // @threat Child_Process_Injection (#child-proc-injection) [high] cwe:CWE-78 -- "Agent launcher executing attacker-controlled commands via process spawn" 44 │ // @threat Information_Disclosure (#info-disclosure) [low] cwe:CWE-200 -- "Unintended exposure of internal paths, structure, or implementation details" 45 │ 46 │ // ─── CONTROLS ───────────────────────────────────────────────────────── 47 │ // Security controls that mitigate threats 48 │
L44 threat Information_Disclosure
Unintended exposure of internal paths, structure, or implementation details
39 │ // @threat Prompt_Injection (#prompt-injection) [medium] cwe:CWE-77 -- "Malicious content in annotations injected into LLM prompts" 40 │ // @threat Denial_of_Service (#dos) [medium] cwe:CWE-400 -- "Resource exhaustion from processing large files or deep directory trees" 41 │ // @threat Sensitive_Data_Exposure (#data-exposure) [medium] cwe:CWE-200 -- "Threat model details exposed to unauthorized parties" 42 │ // @threat Insecure_Deserialization (#insecure-deser) [medium] cwe:CWE-502 -- "Unsafe parsing of JSON/YAML configuration files" 43 │ // @threat Child_Process_Injection (#child-proc-injection) [high] cwe:CWE-78 -- "Agent launcher executing attacker-controlled commands via process spawn" 44 │ // @threat Information_Disclosure (#info-disclosure) [low] cwe:CWE-200 -- "Unintended exposure of internal paths, structure, or implementation details" 45 │ 46 │ // ─── CONTROLS ───────────────────────────────────────────────────────── 47 │ // Security controls that mitigate threats 48 │ 49 │ // @control Path_Validation (#path-validation) -- "Validates file paths using resolve() + startsWith() to ensure access within allowed directories"
L49 control Path_Validation
Validates file paths using resolve() + startsWith() to ensure access within allowed directories
44 │ // @threat Information_Disclosure (#info-disclosure) [low] cwe:CWE-200 -- "Unintended exposure of internal paths, structure, or implementation details" 45 │ 46 │ // ─── CONTROLS ───────────────────────────────────────────────────────── 47 │ // Security controls that mitigate threats 48 │ 49 │ // @control Path_Validation (#path-validation) -- "Validates file paths using resolve() + startsWith() to ensure access within allowed directories" 50 │ // @control Input_Sanitization (#input-sanitize) -- "Input validation with anchored regex patterns and length limits" 51 │ // @control Output_Encoding (#output-encoding) -- "HTML entity encoding for untrusted data in generated output" 52 │ // @control Key_Redaction (#key-redaction) -- "Masking API keys in logs and error messages" 53 │ // @control Process_Sandboxing (#process-sandbox) -- "Controlled child process spawning with explicit args array, no shell" 54 │ // @control Config_Validation (#config-validation) -- "Schema validation for configuration files before use"
L50 control Input_Sanitization
Input validation with anchored regex patterns and length limits
45 │ 46 │ // ─── CONTROLS ───────────────────────────────────────────────────────── 47 │ // Security controls that mitigate threats 48 │ 49 │ // @control Path_Validation (#path-validation) -- "Validates file paths using resolve() + startsWith() to ensure access within allowed directories" 50 │ // @control Input_Sanitization (#input-sanitize) -- "Input validation with anchored regex patterns and length limits" 51 │ // @control Output_Encoding (#output-encoding) -- "HTML entity encoding for untrusted data in generated output" 52 │ // @control Key_Redaction (#key-redaction) -- "Masking API keys in logs and error messages" 53 │ // @control Process_Sandboxing (#process-sandbox) -- "Controlled child process spawning with explicit args array, no shell" 54 │ // @control Config_Validation (#config-validation) -- "Schema validation for configuration files before use" 55 │ // @control Resource_Limits (#resource-limits) -- "File size limits, recursion depth limits, timeout constraints"
L51 control Output_Encoding
HTML entity encoding for untrusted data in generated output
46 │ // ─── CONTROLS ───────────────────────────────────────────────────────── 47 │ // Security controls that mitigate threats 48 │ 49 │ // @control Path_Validation (#path-validation) -- "Validates file paths using resolve() + startsWith() to ensure access within allowed directories" 50 │ // @control Input_Sanitization (#input-sanitize) -- "Input validation with anchored regex patterns and length limits" 51 │ // @control Output_Encoding (#output-encoding) -- "HTML entity encoding for untrusted data in generated output" 52 │ // @control Key_Redaction (#key-redaction) -- "Masking API keys in logs and error messages" 53 │ // @control Process_Sandboxing (#process-sandbox) -- "Controlled child process spawning with explicit args array, no shell" 54 │ // @control Config_Validation (#config-validation) -- "Schema validation for configuration files before use" 55 │ // @control Resource_Limits (#resource-limits) -- "File size limits, recursion depth limits, timeout constraints" 56 │ // @control Parameterized_Commands (#param-commands) -- "Using spawn with args array instead of shell string interpolation"
L52 control Key_Redaction
Masking API keys in logs and error messages
47 │ // Security controls that mitigate threats 48 │ 49 │ // @control Path_Validation (#path-validation) -- "Validates file paths using resolve() + startsWith() to ensure access within allowed directories" 50 │ // @control Input_Sanitization (#input-sanitize) -- "Input validation with anchored regex patterns and length limits" 51 │ // @control Output_Encoding (#output-encoding) -- "HTML entity encoding for untrusted data in generated output" 52 │ // @control Key_Redaction (#key-redaction) -- "Masking API keys in logs and error messages" 53 │ // @control Process_Sandboxing (#process-sandbox) -- "Controlled child process spawning with explicit args array, no shell" 54 │ // @control Config_Validation (#config-validation) -- "Schema validation for configuration files before use" 55 │ // @control Resource_Limits (#resource-limits) -- "File size limits, recursion depth limits, timeout constraints" 56 │ // @control Parameterized_Commands (#param-commands) -- "Using spawn with args array instead of shell string interpolation" 57 │ // @control Glob_Pattern_Filtering (#glob-filtering) -- "Filtering files using glob patterns with explicit excludes"
L53 control Process_Sandboxing
Controlled child process spawning with explicit args array, no shell
48 │ 49 │ // @control Path_Validation (#path-validation) -- "Validates file paths using resolve() + startsWith() to ensure access within allowed directories" 50 │ // @control Input_Sanitization (#input-sanitize) -- "Input validation with anchored regex patterns and length limits" 51 │ // @control Output_Encoding (#output-encoding) -- "HTML entity encoding for untrusted data in generated output" 52 │ // @control Key_Redaction (#key-redaction) -- "Masking API keys in logs and error messages" 53 │ // @control Process_Sandboxing (#process-sandbox) -- "Controlled child process spawning with explicit args array, no shell" 54 │ // @control Config_Validation (#config-validation) -- "Schema validation for configuration files before use" 55 │ // @control Resource_Limits (#resource-limits) -- "File size limits, recursion depth limits, timeout constraints" 56 │ // @control Parameterized_Commands (#param-commands) -- "Using spawn with args array instead of shell string interpolation" 57 │ // @control Glob_Pattern_Filtering (#glob-filtering) -- "Filtering files using glob patterns with explicit excludes" 58 │ // @control Regex_Anchoring (#regex-anchoring) -- "Using anchored regex patterns (^...$) to prevent backtracking"
L54 control Config_Validation
Schema validation for configuration files before use
49 │ // @control Path_Validation (#path-validation) -- "Validates file paths using resolve() + startsWith() to ensure access within allowed directories" 50 │ // @control Input_Sanitization (#input-sanitize) -- "Input validation with anchored regex patterns and length limits" 51 │ // @control Output_Encoding (#output-encoding) -- "HTML entity encoding for untrusted data in generated output" 52 │ // @control Key_Redaction (#key-redaction) -- "Masking API keys in logs and error messages" 53 │ // @control Process_Sandboxing (#process-sandbox) -- "Controlled child process spawning with explicit args array, no shell" 54 │ // @control Config_Validation (#config-validation) -- "Schema validation for configuration files before use" 55 │ // @control Resource_Limits (#resource-limits) -- "File size limits, recursion depth limits, timeout constraints" 56 │ // @control Parameterized_Commands (#param-commands) -- "Using spawn with args array instead of shell string interpolation" 57 │ // @control Glob_Pattern_Filtering (#glob-filtering) -- "Filtering files using glob patterns with explicit excludes" 58 │ // @control Regex_Anchoring (#regex-anchoring) -- "Using anchored regex patterns (^...$) to prevent backtracking" 59 │ // @control Prefix_Ownership (#prefix-ownership) -- "Tag prefix determines owning repo, preventing cross-repo tag collisions"
L55 control Resource_Limits
File size limits, recursion depth limits, timeout constraints
50 │ // @control Input_Sanitization (#input-sanitize) -- "Input validation with anchored regex patterns and length limits" 51 │ // @control Output_Encoding (#output-encoding) -- "HTML entity encoding for untrusted data in generated output" 52 │ // @control Key_Redaction (#key-redaction) -- "Masking API keys in logs and error messages" 53 │ // @control Process_Sandboxing (#process-sandbox) -- "Controlled child process spawning with explicit args array, no shell" 54 │ // @control Config_Validation (#config-validation) -- "Schema validation for configuration files before use" 55 │ // @control Resource_Limits (#resource-limits) -- "File size limits, recursion depth limits, timeout constraints" 56 │ // @control Parameterized_Commands (#param-commands) -- "Using spawn with args array instead of shell string interpolation" 57 │ // @control Glob_Pattern_Filtering (#glob-filtering) -- "Filtering files using glob patterns with explicit excludes" 58 │ // @control Regex_Anchoring (#regex-anchoring) -- "Using anchored regex patterns (^...$) to prevent backtracking" 59 │ // @control Prefix_Ownership (#prefix-ownership) -- "Tag prefix determines owning repo, preventing cross-repo tag collisions" 60 │ // @control YAML_Validation (#yaml-validation) -- "Schema validation for workspace.yaml configuration files"
L56 control Parameterized_Commands
Using spawn with args array instead of shell string interpolation
51 │ // @control Output_Encoding (#output-encoding) -- "HTML entity encoding for untrusted data in generated output" 52 │ // @control Key_Redaction (#key-redaction) -- "Masking API keys in logs and error messages" 53 │ // @control Process_Sandboxing (#process-sandbox) -- "Controlled child process spawning with explicit args array, no shell" 54 │ // @control Config_Validation (#config-validation) -- "Schema validation for configuration files before use" 55 │ // @control Resource_Limits (#resource-limits) -- "File size limits, recursion depth limits, timeout constraints" 56 │ // @control Parameterized_Commands (#param-commands) -- "Using spawn with args array instead of shell string interpolation" 57 │ // @control Glob_Pattern_Filtering (#glob-filtering) -- "Filtering files using glob patterns with explicit excludes" 58 │ // @control Regex_Anchoring (#regex-anchoring) -- "Using anchored regex patterns (^...$) to prevent backtracking" 59 │ // @control Prefix_Ownership (#prefix-ownership) -- "Tag prefix determines owning repo, preventing cross-repo tag collisions" 60 │ // @control YAML_Validation (#yaml-validation) -- "Schema validation for workspace.yaml configuration files" 61 │ // @control Identity_Redaction (#identity-redaction) -- "Hashes or drops author emails before attribution leaves the machine"
L57 control Glob_Pattern_Filtering
Filtering files using glob patterns with explicit excludes
52 │ // @control Key_Redaction (#key-redaction) -- "Masking API keys in logs and error messages" 53 │ // @control Process_Sandboxing (#process-sandbox) -- "Controlled child process spawning with explicit args array, no shell" 54 │ // @control Config_Validation (#config-validation) -- "Schema validation for configuration files before use" 55 │ // @control Resource_Limits (#resource-limits) -- "File size limits, recursion depth limits, timeout constraints" 56 │ // @control Parameterized_Commands (#param-commands) -- "Using spawn with args array instead of shell string interpolation" 57 │ // @control Glob_Pattern_Filtering (#glob-filtering) -- "Filtering files using glob patterns with explicit excludes" 58 │ // @control Regex_Anchoring (#regex-anchoring) -- "Using anchored regex patterns (^...$) to prevent backtracking" 59 │ // @control Prefix_Ownership (#prefix-ownership) -- "Tag prefix determines owning repo, preventing cross-repo tag collisions" 60 │ // @control YAML_Validation (#yaml-validation) -- "Schema validation for workspace.yaml configuration files" 61 │ // @control Identity_Redaction (#identity-redaction) -- "Hashes or drops author emails before attribution leaves the machine" 62 │ // @asset GuardLink.Gate (#gate) -- "Checks what an annotating agent added against the evidence bar: lints, re-prompts, strips what still fails"
L58 control Regex_Anchoring
Using anchored regex patterns (^...$) to prevent backtracking
53 │ // @control Process_Sandboxing (#process-sandbox) -- "Controlled child process spawning with explicit args array, no shell" 54 │ // @control Config_Validation (#config-validation) -- "Schema validation for configuration files before use" 55 │ // @control Resource_Limits (#resource-limits) -- "File size limits, recursion depth limits, timeout constraints" 56 │ // @control Parameterized_Commands (#param-commands) -- "Using spawn with args array instead of shell string interpolation" 57 │ // @control Glob_Pattern_Filtering (#glob-filtering) -- "Filtering files using glob patterns with explicit excludes" 58 │ // @control Regex_Anchoring (#regex-anchoring) -- "Using anchored regex patterns (^...$) to prevent backtracking" 59 │ // @control Prefix_Ownership (#prefix-ownership) -- "Tag prefix determines owning repo, preventing cross-repo tag collisions" 60 │ // @control YAML_Validation (#yaml-validation) -- "Schema validation for workspace.yaml configuration files" 61 │ // @control Identity_Redaction (#identity-redaction) -- "Hashes or drops author emails before attribution leaves the machine" 62 │ // @asset GuardLink.Gate (#gate) -- "Checks what an annotating agent added against the evidence bar: lints, re-prompts, strips what still fails" 63 │
L59 control Prefix_Ownership
Tag prefix determines owning repo, preventing cross-repo tag collisions
54 │ // @control Config_Validation (#config-validation) -- "Schema validation for configuration files before use" 55 │ // @control Resource_Limits (#resource-limits) -- "File size limits, recursion depth limits, timeout constraints" 56 │ // @control Parameterized_Commands (#param-commands) -- "Using spawn with args array instead of shell string interpolation" 57 │ // @control Glob_Pattern_Filtering (#glob-filtering) -- "Filtering files using glob patterns with explicit excludes" 58 │ // @control Regex_Anchoring (#regex-anchoring) -- "Using anchored regex patterns (^...$) to prevent backtracking" 59 │ // @control Prefix_Ownership (#prefix-ownership) -- "Tag prefix determines owning repo, preventing cross-repo tag collisions" 60 │ // @control YAML_Validation (#yaml-validation) -- "Schema validation for workspace.yaml configuration files" 61 │ // @control Identity_Redaction (#identity-redaction) -- "Hashes or drops author emails before attribution leaves the machine" 62 │ // @asset GuardLink.Gate (#gate) -- "Checks what an annotating agent added against the evidence bar: lints, re-prompts, strips what still fails" 63 │ 64 │ // ─── ACTORS ───────────────────────────────────────────────────────────
L60 control YAML_Validation
Schema validation for workspace.yaml configuration files
55 │ // @control Resource_Limits (#resource-limits) -- "File size limits, recursion depth limits, timeout constraints" 56 │ // @control Parameterized_Commands (#param-commands) -- "Using spawn with args array instead of shell string interpolation" 57 │ // @control Glob_Pattern_Filtering (#glob-filtering) -- "Filtering files using glob patterns with explicit excludes" 58 │ // @control Regex_Anchoring (#regex-anchoring) -- "Using anchored regex patterns (^...$) to prevent backtracking" 59 │ // @control Prefix_Ownership (#prefix-ownership) -- "Tag prefix determines owning repo, preventing cross-repo tag collisions" 60 │ // @control YAML_Validation (#yaml-validation) -- "Schema validation for workspace.yaml configuration files" 61 │ // @control Identity_Redaction (#identity-redaction) -- "Hashes or drops author emails before attribution leaves the machine" 62 │ // @asset GuardLink.Gate (#gate) -- "Checks what an annotating agent added against the evidence bar: lints, re-prompts, strips what still fails" 63 │ 64 │ // ─── ACTORS ─────────────────────────────────────────────────────────── 65 │ // Principals in guardlink's own authorization model — roles, not people.
L61 control Identity_Redaction
Hashes or drops author emails before attribution leaves the machine
56 │ // @control Parameterized_Commands (#param-commands) -- "Using spawn with args array instead of shell string interpolation" 57 │ // @control Glob_Pattern_Filtering (#glob-filtering) -- "Filtering files using glob patterns with explicit excludes" 58 │ // @control Regex_Anchoring (#regex-anchoring) -- "Using anchored regex patterns (^...$) to prevent backtracking" 59 │ // @control Prefix_Ownership (#prefix-ownership) -- "Tag prefix determines owning repo, preventing cross-repo tag collisions" 60 │ // @control YAML_Validation (#yaml-validation) -- "Schema validation for workspace.yaml configuration files" 61 │ // @control Identity_Redaction (#identity-redaction) -- "Hashes or drops author emails before attribution leaves the machine" 62 │ // @asset GuardLink.Gate (#gate) -- "Checks what an annotating agent added against the evidence bar: lints, re-prompts, strips what still fails" 63 │ 64 │ // ─── ACTORS ─────────────────────────────────────────────────────────── 65 │ // Principals in guardlink's own authorization model — roles, not people. 66 │ // The actor verb is a definition (docs/prd/actor-entitlement-design.md §3.1), so
L62 asset GuardLink.Gate
Checks what an annotating agent added against the evidence bar: lints, re-prompts, strips what still fails
57 │ // @control Glob_Pattern_Filtering (#glob-filtering) -- "Filtering files using glob patterns with explicit excludes" 58 │ // @control Regex_Anchoring (#regex-anchoring) -- "Using anchored regex patterns (^...$) to prevent backtracking" 59 │ // @control Prefix_Ownership (#prefix-ownership) -- "Tag prefix determines owning repo, preventing cross-repo tag collisions" 60 │ // @control YAML_Validation (#yaml-validation) -- "Schema validation for workspace.yaml configuration files" 61 │ // @control Identity_Redaction (#identity-redaction) -- "Hashes or drops author emails before attribution leaves the machine" 62 │ // @asset GuardLink.Gate (#gate) -- "Checks what an annotating agent added against the evidence bar: lints, re-prompts, strips what still fails" 63 │ 64 │ // ─── ACTORS ─────────────────────────────────────────────────────────── 65 │ // Principals in guardlink's own authorization model — roles, not people. 66 │ // The actor verb is a definition (docs/prd/actor-entitlement-design.md §3.1), so 67 │ // it lives here beside @asset/@threat/@control. Declaring an actor grants nothing;
L72 actor Actor: Local_Developer
Runs the guardlink binary from a shell on the project; already holds write access to the same working tree guardlink edits, and has an interactive TTY that can answer a confirmation prompt
67 │ // it lives here beside @asset/@threat/@control. Declaring an actor grants nothing; 68 │ // only an entitlement in a source file grants, and only when it cites authz code 69 │ // (§3.4). NOTE: never begin a comment line with @actor or @entitles followed by 70 │ // prose — the parser reads it as a real annotation and reports it malformed. 71 │ 72 │ // @actor Local_Developer (#local-dev) -- "Runs the guardlink binary from a shell on the project; already holds write access to the same working tree guardlink edits, and has an interactive TTY that can answer a confirmation prompt" 73 │ // @actor MCP_Agent (#mcp-agent) -- "AI coding agent calling guardlink tools over stdio through the MCP server; acts only with the tool set src/mcp/server.ts exposes, not with the operator's shell" 74 │ // @actor CI_Runner (#ci-runner) -- "GitHub Actions job invoking the built CLI (see .github/workflows/ci.yml); has no TTY, so it cannot answer a confirmation prompt, and is deliberately granted no entitlement" 75 │
L73 actor Actor: MCP_Agent
AI coding agent calling guardlink tools over stdio through the MCP server; acts only with the tool set src/mcp/server.ts exposes, not with the operator's shell
68 │ // only an entitlement in a source file grants, and only when it cites authz code 69 │ // (§3.4). NOTE: never begin a comment line with @actor or @entitles followed by 70 │ // prose — the parser reads it as a real annotation and reports it malformed. 71 │ 72 │ // @actor Local_Developer (#local-dev) -- "Runs the guardlink binary from a shell on the project; already holds write access to the same working tree guardlink edits, and has an interactive TTY that can answer a confirmation prompt" 73 │ // @actor MCP_Agent (#mcp-agent) -- "AI coding agent calling guardlink tools over stdio through the MCP server; acts only with the tool set src/mcp/server.ts exposes, not with the operator's shell" 74 │ // @actor CI_Runner (#ci-runner) -- "GitHub Actions job invoking the built CLI (see .github/workflows/ci.yml); has no TTY, so it cannot answer a confirmation prompt, and is deliberately granted no entitlement" 75 │
L74 actor Actor: CI_Runner
GitHub Actions job invoking the built CLI (see .github/workflows/ci.yml); has no TTY, so it cannot answer a confirmation prompt, and is deliberately granted no entitlement
69 │ // (§3.4). NOTE: never begin a comment line with @actor or @entitles followed by 70 │ // prose — the parser reads it as a real annotation and reports it malformed. 71 │ 72 │ // @actor Local_Developer (#local-dev) -- "Runs the guardlink binary from a shell on the project; already holds write access to the same working tree guardlink edits, and has an interactive TTY that can answer a confirmation prompt" 73 │ // @actor MCP_Agent (#mcp-agent) -- "AI coding agent calling guardlink tools over stdio through the MCP server; acts only with the tool set src/mcp/server.ts exposes, not with the operator's shell" 74 │ // @actor CI_Runner (#ci-runner) -- "GitHub Actions job invoking the built CLI (see .github/workflows/ci.yml); has no TTY, so it cannot answer a confirmation prompt, and is deliberately granted no entitlement" 75 │
src/agents/config.ts exposes 3mitigates 3flow 3handles 1 open 10
L13 exposes #agent-launcher → #api-key-exposure
API keys loaded from env vars, files; stored in config.json
8 │ * 4. Project config: .guardlink/config.json 9 │ * 5. Global config: ~/.config/guardlink/config.json 10 │ * 11 │ * Replaces the fragmented tui-config.json / CLI flag / env var resolution. 12 │ * 13 │ * @exposes #agent-launcher to #api-key-exposure [high] cwe:CWE-798 -- "API keys loaded from env vars, files; stored in config.json" 14 │ * @mitigates #agent-launcher against #api-key-exposure using #key-redaction -- "maskKey() redacts keys for display; keys never logged" 15 │ * @exposes #agent-launcher to #path-traversal [medium] cwe:CWE-22 -- "Config paths resolved from root and homedir" 16 │ * @mitigates #agent-launcher against #path-traversal using #path-validation -- "join() with known base dirs constrains paths" 17 │ * @exposes #agent-launcher to #arbitrary-write [medium] cwe:CWE-73 -- "saveProjectConfig writes to .guardlink/config.json" 18 │ * @mitigates #agent-launcher against #arbitrary-write using #path-validation -- "Output path is fixed relative to project root"
L14 mitigates #key-redaction mitigates #api-key-exposure
maskKey() redacts keys for display; keys never logged
9 │ * 5. Global config: ~/.config/guardlink/config.json 10 │ * 11 │ * Replaces the fragmented tui-config.json / CLI flag / env var resolution. 12 │ * 13 │ * @exposes #agent-launcher to #api-key-exposure [high] cwe:CWE-798 -- "API keys loaded from env vars, files; stored in config.json" 14 │ * @mitigates #agent-launcher against #api-key-exposure using #key-redaction -- "maskKey() redacts keys for display; keys never logged" 15 │ * @exposes #agent-launcher to #path-traversal [medium] cwe:CWE-22 -- "Config paths resolved from root and homedir" 16 │ * @mitigates #agent-launcher against #path-traversal using #path-validation -- "join() with known base dirs constrains paths" 17 │ * @exposes #agent-launcher to #arbitrary-write [medium] cwe:CWE-73 -- "saveProjectConfig writes to .guardlink/config.json" 18 │ * @mitigates #agent-launcher against #arbitrary-write using #path-validation -- "Output path is fixed relative to project root" 19 │ * @flows EnvVars -> #agent-launcher via process.env -- "Environment variable input"
L15 exposes #agent-launcher → #path-traversal
Config paths resolved from root and homedir
10 │ * 11 │ * Replaces the fragmented tui-config.json / CLI flag / env var resolution. 12 │ * 13 │ * @exposes #agent-launcher to #api-key-exposure [high] cwe:CWE-798 -- "API keys loaded from env vars, files; stored in config.json" 14 │ * @mitigates #agent-launcher against #api-key-exposure using #key-redaction -- "maskKey() redacts keys for display; keys never logged" 15 │ * @exposes #agent-launcher to #path-traversal [medium] cwe:CWE-22 -- "Config paths resolved from root and homedir" 16 │ * @mitigates #agent-launcher against #path-traversal using #path-validation -- "join() with known base dirs constrains paths" 17 │ * @exposes #agent-launcher to #arbitrary-write [medium] cwe:CWE-73 -- "saveProjectConfig writes to .guardlink/config.json" 18 │ * @mitigates #agent-launcher against #arbitrary-write using #path-validation -- "Output path is fixed relative to project root" 19 │ * @flows EnvVars -> #agent-launcher via process.env -- "Environment variable input" 20 │ * @flows ConfigFile -> #agent-launcher via readFileSync -- "Config file read"
L16 mitigates #path-validation mitigates #path-traversal
join() with known base dirs constrains paths
11 │ * Replaces the fragmented tui-config.json / CLI flag / env var resolution. 12 │ * 13 │ * @exposes #agent-launcher to #api-key-exposure [high] cwe:CWE-798 -- "API keys loaded from env vars, files; stored in config.json" 14 │ * @mitigates #agent-launcher against #api-key-exposure using #key-redaction -- "maskKey() redacts keys for display; keys never logged" 15 │ * @exposes #agent-launcher to #path-traversal [medium] cwe:CWE-22 -- "Config paths resolved from root and homedir" 16 │ * @mitigates #agent-launcher against #path-traversal using #path-validation -- "join() with known base dirs constrains paths" 17 │ * @exposes #agent-launcher to #arbitrary-write [medium] cwe:CWE-73 -- "saveProjectConfig writes to .guardlink/config.json" 18 │ * @mitigates #agent-launcher against #arbitrary-write using #path-validation -- "Output path is fixed relative to project root" 19 │ * @flows EnvVars -> #agent-launcher via process.env -- "Environment variable input" 20 │ * @flows ConfigFile -> #agent-launcher via readFileSync -- "Config file read" 21 │ * @flows #agent-launcher -> ConfigFile via writeFileSync -- "Config file write"
L17 exposes #agent-launcher → #arbitrary-write
saveProjectConfig writes to .guardlink/config.json
12 │ * 13 │ * @exposes #agent-launcher to #api-key-exposure [high] cwe:CWE-798 -- "API keys loaded from env vars, files; stored in config.json" 14 │ * @mitigates #agent-launcher against #api-key-exposure using #key-redaction -- "maskKey() redacts keys for display; keys never logged" 15 │ * @exposes #agent-launcher to #path-traversal [medium] cwe:CWE-22 -- "Config paths resolved from root and homedir" 16 │ * @mitigates #agent-launcher against #path-traversal using #path-validation -- "join() with known base dirs constrains paths" 17 │ * @exposes #agent-launcher to #arbitrary-write [medium] cwe:CWE-73 -- "saveProjectConfig writes to .guardlink/config.json" 18 │ * @mitigates #agent-launcher against #arbitrary-write using #path-validation -- "Output path is fixed relative to project root" 19 │ * @flows EnvVars -> #agent-launcher via process.env -- "Environment variable input" 20 │ * @flows ConfigFile -> #agent-launcher via readFileSync -- "Config file read" 21 │ * @flows #agent-launcher -> ConfigFile via writeFileSync -- "Config file write" 22 │ * @handles secrets on #agent-launcher -- "Processes and stores LLM API keys"
L18 mitigates #path-validation mitigates #arbitrary-write
Output path is fixed relative to project root
13 │ * @exposes #agent-launcher to #api-key-exposure [high] cwe:CWE-798 -- "API keys loaded from env vars, files; stored in config.json" 14 │ * @mitigates #agent-launcher against #api-key-exposure using #key-redaction -- "maskKey() redacts keys for display; keys never logged" 15 │ * @exposes #agent-launcher to #path-traversal [medium] cwe:CWE-22 -- "Config paths resolved from root and homedir" 16 │ * @mitigates #agent-launcher against #path-traversal using #path-validation -- "join() with known base dirs constrains paths" 17 │ * @exposes #agent-launcher to #arbitrary-write [medium] cwe:CWE-73 -- "saveProjectConfig writes to .guardlink/config.json" 18 │ * @mitigates #agent-launcher against #arbitrary-write using #path-validation -- "Output path is fixed relative to project root" 19 │ * @flows EnvVars -> #agent-launcher via process.env -- "Environment variable input" 20 │ * @flows ConfigFile -> #agent-launcher via readFileSync -- "Config file read" 21 │ * @flows #agent-launcher -> ConfigFile via writeFileSync -- "Config file write" 22 │ * @handles secrets on #agent-launcher -- "Processes and stores LLM API keys" 23 │ */
L19 flow EnvVars → #agent-launcher
Environment variable input
14 │ * @mitigates #agent-launcher against #api-key-exposure using #key-redaction -- "maskKey() redacts keys for display; keys never logged" 15 │ * @exposes #agent-launcher to #path-traversal [medium] cwe:CWE-22 -- "Config paths resolved from root and homedir" 16 │ * @mitigates #agent-launcher against #path-traversal using #path-validation -- "join() with known base dirs constrains paths" 17 │ * @exposes #agent-launcher to #arbitrary-write [medium] cwe:CWE-73 -- "saveProjectConfig writes to .guardlink/config.json" 18 │ * @mitigates #agent-launcher against #arbitrary-write using #path-validation -- "Output path is fixed relative to project root" 19 │ * @flows EnvVars -> #agent-launcher via process.env -- "Environment variable input" 20 │ * @flows ConfigFile -> #agent-launcher via readFileSync -- "Config file read" 21 │ * @flows #agent-launcher -> ConfigFile via writeFileSync -- "Config file write" 22 │ * @handles secrets on #agent-launcher -- "Processes and stores LLM API keys" 23 │ */ 24 │
L20 flow ConfigFile → #agent-launcher
Config file read
15 │ * @exposes #agent-launcher to #path-traversal [medium] cwe:CWE-22 -- "Config paths resolved from root and homedir" 16 │ * @mitigates #agent-launcher against #path-traversal using #path-validation -- "join() with known base dirs constrains paths" 17 │ * @exposes #agent-launcher to #arbitrary-write [medium] cwe:CWE-73 -- "saveProjectConfig writes to .guardlink/config.json" 18 │ * @mitigates #agent-launcher against #arbitrary-write using #path-validation -- "Output path is fixed relative to project root" 19 │ * @flows EnvVars -> #agent-launcher via process.env -- "Environment variable input" 20 │ * @flows ConfigFile -> #agent-launcher via readFileSync -- "Config file read" 21 │ * @flows #agent-launcher -> ConfigFile via writeFileSync -- "Config file write" 22 │ * @handles secrets on #agent-launcher -- "Processes and stores LLM API keys" 23 │ */ 24 │ 25 │ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
L21 flow #agent-launcher → ConfigFile
Config file write
16 │ * @mitigates #agent-launcher against #path-traversal using #path-validation -- "join() with known base dirs constrains paths" 17 │ * @exposes #agent-launcher to #arbitrary-write [medium] cwe:CWE-73 -- "saveProjectConfig writes to .guardlink/config.json" 18 │ * @mitigates #agent-launcher against #arbitrary-write using #path-validation -- "Output path is fixed relative to project root" 19 │ * @flows EnvVars -> #agent-launcher via process.env -- "Environment variable input" 20 │ * @flows ConfigFile -> #agent-launcher via readFileSync -- "Config file read" 21 │ * @flows #agent-launcher -> ConfigFile via writeFileSync -- "Config file write" 22 │ * @handles secrets on #agent-launcher -- "Processes and stores LLM API keys" 23 │ */ 24 │ 25 │ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; 26 │ import { join } from 'node:path';
L22 handles #agent-launcher: secrets
Processes and stores LLM API keys
17 │ * @exposes #agent-launcher to #arbitrary-write [medium] cwe:CWE-73 -- "saveProjectConfig writes to .guardlink/config.json" 18 │ * @mitigates #agent-launcher against #arbitrary-write using #path-validation -- "Output path is fixed relative to project root" 19 │ * @flows EnvVars -> #agent-launcher via process.env -- "Environment variable input" 20 │ * @flows ConfigFile -> #agent-launcher via readFileSync -- "Config file read" 21 │ * @flows #agent-launcher -> ConfigFile via writeFileSync -- "Config file write" 22 │ * @handles secrets on #agent-launcher -- "Processes and stores LLM API keys" 23 │ */ 24 │ 25 │ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; 26 │ import { join } from 'node:path'; 27 │ import { homedir } from 'node:os';
src/agents/index.ts comment 2 open 2
L7 comment Agent binaries are hardcoded; no user-controlled binary names
Agent binaries are hardcoded; no user-controlled binary names
2 │ * GuardLink Agents — Shared agent registry. 3 │ * 4 │ * Used by CLI, TUI, and MCP to identify and resolve coding agents 5 │ * (Claude Code, Codex, Cursor, Windsurf, Gemini, clipboard). 6 │ * 7 │ * @comment -- "Agent binaries are hardcoded; no user-controlled binary names" 8 │ * @comment -- "parseAgentFlag extracts flags from args; no injection risk" 9 │ */ 10 │ 11 │ // ─── Agent registry ────────────────────────────────────────────────── 12 │
L8 comment parseAgentFlag extracts flags from args; no injection risk
parseAgentFlag extracts flags from args; no injection risk
3 │ * 4 │ * Used by CLI, TUI, and MCP to identify and resolve coding agents 5 │ * (Claude Code, Codex, Cursor, Windsurf, Gemini, clipboard). 6 │ * 7 │ * @comment -- "Agent binaries are hardcoded; no user-controlled binary names" 8 │ * @comment -- "parseAgentFlag extracts flags from args; no injection risk" 9 │ */ 10 │ 11 │ // ─── Agent registry ────────────────────────────────────────────────── 12 │ 13 │ export interface AgentEntry {
src/analyze/findings.ts exposes 1mitigates 1flow 1comment 1 open 4
L8 exposes #llm-client → #insecure-deser
JSON.parse over a block the model wrote
3 │ * 4 │ * Every threat report ends with a fenced `guardlink-findings` JSON block. This 5 │ * module parses it out of the markdown, validates the ids it names against the 6 │ * model, renders a table from it, and strips it from the prose for display. 7 │ * 8 │ * @exposes #llm-client to #insecure-deser [low] cwe:CWE-502 -- "JSON.parse over a block the model wrote" 9 │ * @mitigates #llm-client against #insecure-deser using #config-validation -- "Only plain data is read out of the parsed object; every field is coerced to a string, a number or one of a fixed set, and anything else is dropped" 10 │ * @flows SavedReport -> #llm-client via parseFindingsBlock -- "Findings out of a saved report" 11 │ * @comment -- "The last block wins when a report carries more than one, because a model that restated its findings restated them last" 12 │ */ 13 │ import type { ThreatModel } from '../types/index.js';
L9 mitigates #config-validation mitigates #insecure-deser
Only plain data is read out of the parsed object; every field is coerced to a string, a number or one of a fixed set, and anything else is dropped
4 │ * Every threat report ends with a fenced `guardlink-findings` JSON block. This 5 │ * module parses it out of the markdown, validates the ids it names against the 6 │ * model, renders a table from it, and strips it from the prose for display. 7 │ * 8 │ * @exposes #llm-client to #insecure-deser [low] cwe:CWE-502 -- "JSON.parse over a block the model wrote" 9 │ * @mitigates #llm-client against #insecure-deser using #config-validation -- "Only plain data is read out of the parsed object; every field is coerced to a string, a number or one of a fixed set, and anything else is dropped" 10 │ * @flows SavedReport -> #llm-client via parseFindingsBlock -- "Findings out of a saved report" 11 │ * @comment -- "The last block wins when a report carries more than one, because a model that restated its findings restated them last" 12 │ */ 13 │ import type { ThreatModel } from '../types/index.js'; 14 │
L10 flow SavedReport → #llm-client
Findings out of a saved report
5 │ * module parses it out of the markdown, validates the ids it names against the 6 │ * model, renders a table from it, and strips it from the prose for display. 7 │ * 8 │ * @exposes #llm-client to #insecure-deser [low] cwe:CWE-502 -- "JSON.parse over a block the model wrote" 9 │ * @mitigates #llm-client against #insecure-deser using #config-validation -- "Only plain data is read out of the parsed object; every field is coerced to a string, a number or one of a fixed set, and anything else is dropped" 10 │ * @flows SavedReport -> #llm-client via parseFindingsBlock -- "Findings out of a saved report" 11 │ * @comment -- "The last block wins when a report carries more than one, because a model that restated its findings restated them last" 12 │ */ 13 │ import type { ThreatModel } from '../types/index.js'; 14 │ 15 │ export const FINDINGS_SCHEMA = 'guardlink.findings/v1';
L11 comment The last block wins when a report carries more than one, because a model that restated its findings restated them last
The last block wins when a report carries more than one, because a model that restated its findings restated them last
6 │ * model, renders a table from it, and strips it from the prose for display. 7 │ * 8 │ * @exposes #llm-client to #insecure-deser [low] cwe:CWE-502 -- "JSON.parse over a block the model wrote" 9 │ * @mitigates #llm-client against #insecure-deser using #config-validation -- "Only plain data is read out of the parsed object; every field is coerced to a string, a number or one of a fixed set, and anything else is dropped" 10 │ * @flows SavedReport -> #llm-client via parseFindingsBlock -- "Findings out of a saved report" 11 │ * @comment -- "The last block wins when a report carries more than one, because a model that restated its findings restated them last" 12 │ */ 13 │ import type { ThreatModel } from '../types/index.js'; 14 │ 15 │ export const FINDINGS_SCHEMA = 'guardlink.findings/v1'; 16 │
src/analyze/prompts.ts comment 2 open 2
L7 comment Prompt templates are static; no user input interpolation in system prompts
Prompt templates are static; no user input interpolation in system prompts
2 │ * GuardLink Threat Reports — Framework-specific analysis prompts. 3 │ * 4 │ * Each framework produces a structured security analysis from the 5 │ * serialized threat model. The LLM acts as a senior security architect. 6 │ * 7 │ * @comment -- "Prompt templates are static; no user input interpolation in system prompts" 8 │ * @comment -- "customPrompt is appended to user message, not system prompt — bounded injection risk" 9 │ */ 10 │ 11 │ import { getPlaybook, type ReportShapeId } from '../playbooks/index.js'; 12 │
L8 comment customPrompt is appended to user message, not system prompt — bounded injection risk
customPrompt is appended to user message, not system prompt — bounded injection risk
3 │ * 4 │ * Each framework produces a structured security analysis from the 5 │ * serialized threat model. The LLM acts as a senior security architect. 6 │ * 7 │ * @comment -- "Prompt templates are static; no user input interpolation in system prompts" 8 │ * @comment -- "customPrompt is appended to user message, not system prompt — bounded injection risk" 9 │ */ 10 │ 11 │ import { getPlaybook, type ReportShapeId } from '../playbooks/index.js'; 12 │ 13 │ export type AnalysisFramework = 'stride' | 'dread' | 'pasta' | 'attacker' | 'rapid' | 'general';
src/analyze/tools.ts exposes 3mitigates 3flow 3comment 1 open 11
L9 exposes #llm-client → #ssrf
lookupCve fetches from NVD API with user-controlled CVE ID
4 │ * Defines tools that the LLM can invoke during threat analysis: 5 │ * - lookup_cve: Search for CVE details (via web fetch) 6 │ * - validate_finding: Cross-reference a finding against the parsed model 7 │ * - search_codebase: Search project files for patterns 8 │ * 9 │ * @exposes #llm-client to #ssrf [medium] cwe:CWE-918 -- "lookupCve fetches from NVD API with user-controlled CVE ID" 10 │ * @mitigates #llm-client against #ssrf using #input-sanitize -- "CVE ID validated with strict regex; URL hardcoded to NVD" 11 │ * @exposes #llm-client to #path-traversal [medium] cwe:CWE-22 -- "searchCodebase reads files from project root" 12 │ * @mitigates #llm-client against #path-traversal using #glob-filtering -- "skipDirs excludes sensitive directories; relative() bounds output" 13 │ * @exposes #llm-client to #dos [low] cwe:CWE-400 -- "searchCodebase reads many files; the LLM now sets max_results itself" 14 │ * @mitigates #llm-client against #dos using #resource-limits -- "clampMaxResults bounds the caller's limit to [1, HARD_MAX_RESULTS] and turns a non-numeric argument into the default rather than NaN; stat.size < 500KB filter"
L10 mitigates #input-sanitize mitigates #ssrf
CVE ID validated with strict regex; URL hardcoded to NVD
5 │ * - lookup_cve: Search for CVE details (via web fetch) 6 │ * - validate_finding: Cross-reference a finding against the parsed model 7 │ * - search_codebase: Search project files for patterns 8 │ * 9 │ * @exposes #llm-client to #ssrf [medium] cwe:CWE-918 -- "lookupCve fetches from NVD API with user-controlled CVE ID" 10 │ * @mitigates #llm-client against #ssrf using #input-sanitize -- "CVE ID validated with strict regex; URL hardcoded to NVD" 11 │ * @exposes #llm-client to #path-traversal [medium] cwe:CWE-22 -- "searchCodebase reads files from project root" 12 │ * @mitigates #llm-client against #path-traversal using #glob-filtering -- "skipDirs excludes sensitive directories; relative() bounds output" 13 │ * @exposes #llm-client to #dos [low] cwe:CWE-400 -- "searchCodebase reads many files; the LLM now sets max_results itself" 14 │ * @mitigates #llm-client against #dos using #resource-limits -- "clampMaxResults bounds the caller's limit to [1, HARD_MAX_RESULTS] and turns a non-numeric argument into the default rather than NaN; stat.size < 500KB filter" 15 │ * @comment -- "The old `parseInt(args.max_results || '20', 10)` yielded NaN on a malformed argument, and `results.length >= NaN` is false forever — so the bound vanished and the walk covered the whole tree. Clamping is what makes the limit a limit"
L11 exposes #llm-client → #path-traversal
searchCodebase reads files from project root
6 │ * - validate_finding: Cross-reference a finding against the parsed model 7 │ * - search_codebase: Search project files for patterns 8 │ * 9 │ * @exposes #llm-client to #ssrf [medium] cwe:CWE-918 -- "lookupCve fetches from NVD API with user-controlled CVE ID" 10 │ * @mitigates #llm-client against #ssrf using #input-sanitize -- "CVE ID validated with strict regex; URL hardcoded to NVD" 11 │ * @exposes #llm-client to #path-traversal [medium] cwe:CWE-22 -- "searchCodebase reads files from project root" 12 │ * @mitigates #llm-client against #path-traversal using #glob-filtering -- "skipDirs excludes sensitive directories; relative() bounds output" 13 │ * @exposes #llm-client to #dos [low] cwe:CWE-400 -- "searchCodebase reads many files; the LLM now sets max_results itself" 14 │ * @mitigates #llm-client against #dos using #resource-limits -- "clampMaxResults bounds the caller's limit to [1, HARD_MAX_RESULTS] and turns a non-numeric argument into the default rather than NaN; stat.size < 500KB filter" 15 │ * @comment -- "The old `parseInt(args.max_results || '20', 10)` yielded NaN on a malformed argument, and `results.length >= NaN` is false forever — so the bound vanished and the walk covered the whole tree. Clamping is what makes the limit a limit" 16 │ * @flows LLMToolCall -> #llm-client via createToolExecutor -- "Tool invocation input"
L12 mitigates #glob-filtering mitigates #path-traversal
skipDirs excludes sensitive directories; relative() bounds output
7 │ * - search_codebase: Search project files for patterns 8 │ * 9 │ * @exposes #llm-client to #ssrf [medium] cwe:CWE-918 -- "lookupCve fetches from NVD API with user-controlled CVE ID" 10 │ * @mitigates #llm-client against #ssrf using #input-sanitize -- "CVE ID validated with strict regex; URL hardcoded to NVD" 11 │ * @exposes #llm-client to #path-traversal [medium] cwe:CWE-22 -- "searchCodebase reads files from project root" 12 │ * @mitigates #llm-client against #path-traversal using #glob-filtering -- "skipDirs excludes sensitive directories; relative() bounds output" 13 │ * @exposes #llm-client to #dos [low] cwe:CWE-400 -- "searchCodebase reads many files; the LLM now sets max_results itself" 14 │ * @mitigates #llm-client against #dos using #resource-limits -- "clampMaxResults bounds the caller's limit to [1, HARD_MAX_RESULTS] and turns a non-numeric argument into the default rather than NaN; stat.size < 500KB filter" 15 │ * @comment -- "The old `parseInt(args.max_results || '20', 10)` yielded NaN on a malformed argument, and `results.length >= NaN` is false forever — so the bound vanished and the walk covered the whole tree. Clamping is what makes the limit a limit" 16 │ * @flows LLMToolCall -> #llm-client via createToolExecutor -- "Tool invocation input" 17 │ * @flows #llm-client -> NVD via fetch -- "CVE lookup API call"
L13 exposes #llm-client → #dos
searchCodebase reads many files; the LLM now sets max_results itself
8 │ * 9 │ * @exposes #llm-client to #ssrf [medium] cwe:CWE-918 -- "lookupCve fetches from NVD API with user-controlled CVE ID" 10 │ * @mitigates #llm-client against #ssrf using #input-sanitize -- "CVE ID validated with strict regex; URL hardcoded to NVD" 11 │ * @exposes #llm-client to #path-traversal [medium] cwe:CWE-22 -- "searchCodebase reads files from project root" 12 │ * @mitigates #llm-client against #path-traversal using #glob-filtering -- "skipDirs excludes sensitive directories; relative() bounds output" 13 │ * @exposes #llm-client to #dos [low] cwe:CWE-400 -- "searchCodebase reads many files; the LLM now sets max_results itself" 14 │ * @mitigates #llm-client against #dos using #resource-limits -- "clampMaxResults bounds the caller's limit to [1, HARD_MAX_RESULTS] and turns a non-numeric argument into the default rather than NaN; stat.size < 500KB filter" 15 │ * @comment -- "The old `parseInt(args.max_results || '20', 10)` yielded NaN on a malformed argument, and `results.length >= NaN` is false forever — so the bound vanished and the walk covered the whole tree. Clamping is what makes the limit a limit" 16 │ * @flows LLMToolCall -> #llm-client via createToolExecutor -- "Tool invocation input" 17 │ * @flows #llm-client -> NVD via fetch -- "CVE lookup API call" 18 │ * @flows ProjectFiles -> #llm-client via readFileSync -- "Codebase search reads"
L14 mitigates #resource-limits mitigates #dos
clampMaxResults bounds the caller's limit to [1, HARD_MAX_RESULTS] and turns a non-numeric argument into the default rather than NaN; stat.size < 500KB filter
9 │ * @exposes #llm-client to #ssrf [medium] cwe:CWE-918 -- "lookupCve fetches from NVD API with user-controlled CVE ID" 10 │ * @mitigates #llm-client against #ssrf using #input-sanitize -- "CVE ID validated with strict regex; URL hardcoded to NVD" 11 │ * @exposes #llm-client to #path-traversal [medium] cwe:CWE-22 -- "searchCodebase reads files from project root" 12 │ * @mitigates #llm-client against #path-traversal using #glob-filtering -- "skipDirs excludes sensitive directories; relative() bounds output" 13 │ * @exposes #llm-client to #dos [low] cwe:CWE-400 -- "searchCodebase reads many files; the LLM now sets max_results itself" 14 │ * @mitigates #llm-client against #dos using #resource-limits -- "clampMaxResults bounds the caller's limit to [1, HARD_MAX_RESULTS] and turns a non-numeric argument into the default rather than NaN; stat.size < 500KB filter" 15 │ * @comment -- "The old `parseInt(args.max_results || '20', 10)` yielded NaN on a malformed argument, and `results.length >= NaN` is false forever — so the bound vanished and the walk covered the whole tree. Clamping is what makes the limit a limit" 16 │ * @flows LLMToolCall -> #llm-client via createToolExecutor -- "Tool invocation input" 17 │ * @flows #llm-client -> NVD via fetch -- "CVE lookup API call" 18 │ * @flows ProjectFiles -> #llm-client via readFileSync -- "Codebase search reads" 19 │ * @boundary #llm-client and NVD (#nvd-api-boundary) -- "Trust boundary at external API"
L15 comment The old `parseInt(args.max_results || '20', 10)` yielded NaN on a malformed argument, and `results.length >= NaN` is false forever — so the bound vanished and the walk covered the whole tree. Clamping is what makes the limit a limit
The old `parseInt(args.max_results || '20', 10)` yielded NaN on a malformed argument, and `results.length >= NaN` is false forever — so the bound vanished and the walk covered the whole tree. Clamping is what makes the limit a limit
10 │ * @mitigates #llm-client against #ssrf using #input-sanitize -- "CVE ID validated with strict regex; URL hardcoded to NVD" 11 │ * @exposes #llm-client to #path-traversal [medium] cwe:CWE-22 -- "searchCodebase reads files from project root" 12 │ * @mitigates #llm-client against #path-traversal using #glob-filtering -- "skipDirs excludes sensitive directories; relative() bounds output" 13 │ * @exposes #llm-client to #dos [low] cwe:CWE-400 -- "searchCodebase reads many files; the LLM now sets max_results itself" 14 │ * @mitigates #llm-client against #dos using #resource-limits -- "clampMaxResults bounds the caller's limit to [1, HARD_MAX_RESULTS] and turns a non-numeric argument into the default rather than NaN; stat.size < 500KB filter" 15 │ * @comment -- "The old `parseInt(args.max_results || '20', 10)` yielded NaN on a malformed argument, and `results.length >= NaN` is false forever — so the bound vanished and the walk covered the whole tree. Clamping is what makes the limit a limit" 16 │ * @flows LLMToolCall -> #llm-client via createToolExecutor -- "Tool invocation input" 17 │ * @flows #llm-client -> NVD via fetch -- "CVE lookup API call" 18 │ * @flows ProjectFiles -> #llm-client via readFileSync -- "Codebase search reads" 19 │ * @boundary #llm-client and NVD (#nvd-api-boundary) -- "Trust boundary at external API" 20 │ */
L16 flow LLMToolCall → #llm-client
Tool invocation input
11 │ * @exposes #llm-client to #path-traversal [medium] cwe:CWE-22 -- "searchCodebase reads files from project root" 12 │ * @mitigates #llm-client against #path-traversal using #glob-filtering -- "skipDirs excludes sensitive directories; relative() bounds output" 13 │ * @exposes #llm-client to #dos [low] cwe:CWE-400 -- "searchCodebase reads many files; the LLM now sets max_results itself" 14 │ * @mitigates #llm-client against #dos using #resource-limits -- "clampMaxResults bounds the caller's limit to [1, HARD_MAX_RESULTS] and turns a non-numeric argument into the default rather than NaN; stat.size < 500KB filter" 15 │ * @comment -- "The old `parseInt(args.max_results || '20', 10)` yielded NaN on a malformed argument, and `results.length >= NaN` is false forever — so the bound vanished and the walk covered the whole tree. Clamping is what makes the limit a limit" 16 │ * @flows LLMToolCall -> #llm-client via createToolExecutor -- "Tool invocation input" 17 │ * @flows #llm-client -> NVD via fetch -- "CVE lookup API call" 18 │ * @flows ProjectFiles -> #llm-client via readFileSync -- "Codebase search reads" 19 │ * @boundary #llm-client and NVD (#nvd-api-boundary) -- "Trust boundary at external API" 20 │ */ 21 │
L17 flow #llm-client → NVD
CVE lookup API call
12 │ * @mitigates #llm-client against #path-traversal using #glob-filtering -- "skipDirs excludes sensitive directories; relative() bounds output" 13 │ * @exposes #llm-client to #dos [low] cwe:CWE-400 -- "searchCodebase reads many files; the LLM now sets max_results itself" 14 │ * @mitigates #llm-client against #dos using #resource-limits -- "clampMaxResults bounds the caller's limit to [1, HARD_MAX_RESULTS] and turns a non-numeric argument into the default rather than NaN; stat.size < 500KB filter" 15 │ * @comment -- "The old `parseInt(args.max_results || '20', 10)` yielded NaN on a malformed argument, and `results.length >= NaN` is false forever — so the bound vanished and the walk covered the whole tree. Clamping is what makes the limit a limit" 16 │ * @flows LLMToolCall -> #llm-client via createToolExecutor -- "Tool invocation input" 17 │ * @flows #llm-client -> NVD via fetch -- "CVE lookup API call" 18 │ * @flows ProjectFiles -> #llm-client via readFileSync -- "Codebase search reads" 19 │ * @boundary #llm-client and NVD (#nvd-api-boundary) -- "Trust boundary at external API" 20 │ */ 21 │ 22 │ import { readFileSync, readdirSync, statSync } from 'node:fs';
L18 flow ProjectFiles → #llm-client
Codebase search reads
13 │ * @exposes #llm-client to #dos [low] cwe:CWE-400 -- "searchCodebase reads many files; the LLM now sets max_results itself" 14 │ * @mitigates #llm-client against #dos using #resource-limits -- "clampMaxResults bounds the caller's limit to [1, HARD_MAX_RESULTS] and turns a non-numeric argument into the default rather than NaN; stat.size < 500KB filter" 15 │ * @comment -- "The old `parseInt(args.max_results || '20', 10)` yielded NaN on a malformed argument, and `results.length >= NaN` is false forever — so the bound vanished and the walk covered the whole tree. Clamping is what makes the limit a limit" 16 │ * @flows LLMToolCall -> #llm-client via createToolExecutor -- "Tool invocation input" 17 │ * @flows #llm-client -> NVD via fetch -- "CVE lookup API call" 18 │ * @flows ProjectFiles -> #llm-client via readFileSync -- "Codebase search reads" 19 │ * @boundary #llm-client and NVD (#nvd-api-boundary) -- "Trust boundary at external API" 20 │ */ 21 │ 22 │ import { readFileSync, readdirSync, statSync } from 'node:fs'; 23 │ import { join, relative } from 'node:path';
L19 boundary #llm-client ↔ NVD
Trust boundary at external API
14 │ * @mitigates #llm-client against #dos using #resource-limits -- "clampMaxResults bounds the caller's limit to [1, HARD_MAX_RESULTS] and turns a non-numeric argument into the default rather than NaN; stat.size < 500KB filter" 15 │ * @comment -- "The old `parseInt(args.max_results || '20', 10)` yielded NaN on a malformed argument, and `results.length >= NaN` is false forever — so the bound vanished and the walk covered the whole tree. Clamping is what makes the limit a limit" 16 │ * @flows LLMToolCall -> #llm-client via createToolExecutor -- "Tool invocation input" 17 │ * @flows #llm-client -> NVD via fetch -- "CVE lookup API call" 18 │ * @flows ProjectFiles -> #llm-client via readFileSync -- "Codebase search reads" 19 │ * @boundary #llm-client and NVD (#nvd-api-boundary) -- "Trust boundary at external API" 20 │ */ 21 │ 22 │ import { readFileSync, readdirSync, statSync } from 'node:fs'; 23 │ import { join, relative } from 'node:path'; 24 │ import type { ToolDefinition, ToolExecutor } from './llm.js';
src/analyzer/index.ts comment 2 open 2
L4 comment SARIF generation is pure transformation; no I/O in this module
SARIF generation is pure transformation; no I/O in this module
1 │ /** 2 │ * GuardLink Analyzer — exports. 3 │ * 4 │ * @comment -- "SARIF generation is pure transformation; no I/O in this module" 5 │ * @comment -- "File writes handled by CLI/MCP callers" 6 │ */ 7 │ 8 │ export { generateSarif, type SarifOptions } from './sarif.js'; 9 │
L5 comment File writes handled by CLI/MCP callers
File writes handled by CLI/MCP callers
1 │ /** 2 │ * GuardLink Analyzer — exports. 3 │ * 4 │ * @comment -- "SARIF generation is pure transformation; no I/O in this module" 5 │ * @comment -- "File writes handled by CLI/MCP callers" 6 │ */ 7 │ 8 │ export { generateSarif, type SarifOptions } from './sarif.js'; 9 │
src/artifacts/emit.ts 2 stale shield 4comment 3flow 2exposes 1 open 11
L27 exposes #dashboard → #arbitrary-write
Writes .mmd, model.json and MANIFEST.json under a caller-supplied root
22 │ * across processes (fast-glob completion order, D23), so without it every 23 │ * regeneration would be a diff and a real change would be indistinguishable from 24 │ * a re-run — which would make the staleness signal useless in exactly the files 25 │ * that exist to carry it. 26 │ * 27 │ * @exposes #dashboard to #arbitrary-write [medium] cwe:CWE-73 -- "Writes .mmd, model.json and MANIFEST.json under a caller-supplied root" 28 │ * @mitigates #dashboard against #arbitrary-write using #path-validation -- "Every write is join(root, '.guardlink', …); feature names are slugified before use as filenames" 29 │ * @flows ThreatModel -> #dashboard via emitArtifacts -- "Model rendered to disk artifacts" 30 │ * @flows #dashboard -> FileSystem via writeFileSync -- "Artifact write path" 31 │ * @comment -- "Artifacts carry a provenance header so a stale one is detectable rather than merely wrong" 32 │ * @comment -- "by-feature/*.mmd declare themselves partial in the header: a narrowed diagram that does not say it is narrowed reads as a complete one that is missing things"
L28 mitigates #path-validation mitigates #arbitrary-write
Every write is join(root, '.guardlink', …); feature names are slugified before use as filenames
23 │ * regeneration would be a diff and a real change would be indistinguishable from 24 │ * a re-run — which would make the staleness signal useless in exactly the files 25 │ * that exist to carry it. 26 │ * 27 │ * @exposes #dashboard to #arbitrary-write [medium] cwe:CWE-73 -- "Writes .mmd, model.json and MANIFEST.json under a caller-supplied root" 28 │ * @mitigates #dashboard against #arbitrary-write using #path-validation -- "Every write is join(root, '.guardlink', …); feature names are slugified before use as filenames" 29 │ * @flows ThreatModel -> #dashboard via emitArtifacts -- "Model rendered to disk artifacts" 30 │ * @flows #dashboard -> FileSystem via writeFileSync -- "Artifact write path" 31 │ * @comment -- "Artifacts carry a provenance header so a stale one is detectable rather than merely wrong" 32 │ * @comment -- "by-feature/*.mmd declare themselves partial in the header: a narrowed diagram that does not say it is narrowed reads as a complete one that is missing things" 33 │ * @comment -- "annotation_hash stays project-wide on per-feature files — it records the state the view was cut from, which is what keeps `validate --artifacts` a single comparison for every artifact"
L29 flow ThreatModel → #dashboard
Model rendered to disk artifacts
24 │ * a re-run — which would make the staleness signal useless in exactly the files 25 │ * that exist to carry it. 26 │ * 27 │ * @exposes #dashboard to #arbitrary-write [medium] cwe:CWE-73 -- "Writes .mmd, model.json and MANIFEST.json under a caller-supplied root" 28 │ * @mitigates #dashboard against #arbitrary-write using #path-validation -- "Every write is join(root, '.guardlink', …); feature names are slugified before use as filenames" 29 │ * @flows ThreatModel -> #dashboard via emitArtifacts -- "Model rendered to disk artifacts" 30 │ * @flows #dashboard -> FileSystem via writeFileSync -- "Artifact write path" 31 │ * @comment -- "Artifacts carry a provenance header so a stale one is detectable rather than merely wrong" 32 │ * @comment -- "by-feature/*.mmd declare themselves partial in the header: a narrowed diagram that does not say it is narrowed reads as a complete one that is missing things" 33 │ * @comment -- "annotation_hash stays project-wide on per-feature files — it records the state the view was cut from, which is what keeps `validate --artifacts` a single comparison for every artifact" 34 │ */
L30 flow #dashboard → FileSystem
Artifact write path
25 │ * that exist to carry it. 26 │ * 27 │ * @exposes #dashboard to #arbitrary-write [medium] cwe:CWE-73 -- "Writes .mmd, model.json and MANIFEST.json under a caller-supplied root" 28 │ * @mitigates #dashboard against #arbitrary-write using #path-validation -- "Every write is join(root, '.guardlink', …); feature names are slugified before use as filenames" 29 │ * @flows ThreatModel -> #dashboard via emitArtifacts -- "Model rendered to disk artifacts" 30 │ * @flows #dashboard -> FileSystem via writeFileSync -- "Artifact write path" 31 │ * @comment -- "Artifacts carry a provenance header so a stale one is detectable rather than merely wrong" 32 │ * @comment -- "by-feature/*.mmd declare themselves partial in the header: a narrowed diagram that does not say it is narrowed reads as a complete one that is missing things" 33 │ * @comment -- "annotation_hash stays project-wide on per-feature files — it records the state the view was cut from, which is what keeps `validate --artifacts` a single comparison for every artifact" 34 │ */ 35 │
L31 comment Artifacts carry a provenance header so a stale one is detectable rather than merely wrong
Artifacts carry a provenance header so a stale one is detectable rather than merely wrong
26 │ * 27 │ * @exposes #dashboard to #arbitrary-write [medium] cwe:CWE-73 -- "Writes .mmd, model.json and MANIFEST.json under a caller-supplied root" 28 │ * @mitigates #dashboard against #arbitrary-write using #path-validation -- "Every write is join(root, '.guardlink', …); feature names are slugified before use as filenames" 29 │ * @flows ThreatModel -> #dashboard via emitArtifacts -- "Model rendered to disk artifacts" 30 │ * @flows #dashboard -> FileSystem via writeFileSync -- "Artifact write path" 31 │ * @comment -- "Artifacts carry a provenance header so a stale one is detectable rather than merely wrong" 32 │ * @comment -- "by-feature/*.mmd declare themselves partial in the header: a narrowed diagram that does not say it is narrowed reads as a complete one that is missing things" 33 │ * @comment -- "annotation_hash stays project-wide on per-feature files — it records the state the view was cut from, which is what keeps `validate --artifacts` a single comparison for every artifact" 34 │ */ 35 │ 36 │ import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, existsSync } from 'node:fs';
L32 comment by-feature/*.mmd declare themselves partial in the header: a narrowed diagram that does not say it is narrowed reads as a complete one that is missing things
by-feature/*.mmd declare themselves partial in the header: a narrowed diagram that does not say it is narrowed reads as a complete one that is missing things
27 │ * @exposes #dashboard to #arbitrary-write [medium] cwe:CWE-73 -- "Writes .mmd, model.json and MANIFEST.json under a caller-supplied root" 28 │ * @mitigates #dashboard against #arbitrary-write using #path-validation -- "Every write is join(root, '.guardlink', …); feature names are slugified before use as filenames" 29 │ * @flows ThreatModel -> #dashboard via emitArtifacts -- "Model rendered to disk artifacts" 30 │ * @flows #dashboard -> FileSystem via writeFileSync -- "Artifact write path" 31 │ * @comment -- "Artifacts carry a provenance header so a stale one is detectable rather than merely wrong" 32 │ * @comment -- "by-feature/*.mmd declare themselves partial in the header: a narrowed diagram that does not say it is narrowed reads as a complete one that is missing things" 33 │ * @comment -- "annotation_hash stays project-wide on per-feature files — it records the state the view was cut from, which is what keeps `validate --artifacts` a single comparison for every artifact" 34 │ */ 35 │ 36 │ import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, existsSync } from 'node:fs'; 37 │ import { join } from 'node:path';
L33 comment annotation_hash stays project-wide on per-feature files — it records the state the view was cut from, which is what keeps `validate --artifacts` a single comparison for every artifact
annotation_hash stays project-wide on per-feature files — it records the state the view was cut from, which is what keeps `validate --artifacts` a single comparison for every artifact
28 │ * @mitigates #dashboard against #arbitrary-write using #path-validation -- "Every write is join(root, '.guardlink', …); feature names are slugified before use as filenames" 29 │ * @flows ThreatModel -> #dashboard via emitArtifacts -- "Model rendered to disk artifacts" 30 │ * @flows #dashboard -> FileSystem via writeFileSync -- "Artifact write path" 31 │ * @comment -- "Artifacts carry a provenance header so a stale one is detectable rather than merely wrong" 32 │ * @comment -- "by-feature/*.mmd declare themselves partial in the header: a narrowed diagram that does not say it is narrowed reads as a complete one that is missing things" 33 │ * @comment -- "annotation_hash stays project-wide on per-feature files — it records the state the view was cut from, which is what keeps `validate --artifacts` a single comparison for every artifact" 34 │ */ 35 │ 36 │ import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync, existsSync } from 'node:fs'; 37 │ import { join } from 'node:path'; 38 │ import { generateThreatGraph, generateDataFlowDiagram, generateAttackSurface } from '../dashboard/index.js';
L863 shield graph/index example content, excluded from parsing
858 │ '.guardlink/README.md', 859 │ ]; 860 │ 861 │ // ─── graph/index.md ────────────────────────────────────────────────── 862 │ 863 │ // @shield:begin -- "graph/index example content, excluded from parsing" 864 │ /** 865 │ * A table of slices and the question each one answers. 866 │ * 867 │ * This is the file to open first, and it is deliberately Markdown rather than a 868 │ * diagram. It renders natively on GitHub and on a docs site at any model size,
L969 shield Shielded region
964 │ Generated by ${p.generator} from annotation hash \`${p.annotation_hash}\`. If that 965 │ differs from what \`guardlink status .\` reports, everything here is stale — 966 │ regenerate rather than trusting it. 967 │ `; 968 │ } 969 │ // @shield:end 970 │ 971 │ // ─── graph/README.md ───────────────────────────────────────────────── 972 │ 973 │ // @shield:begin -- "graph/README example content, excluded from parsing" 974 │ function graphReadme(p: ArtifactProvenance, features: string[]): string {
L973 shield graph/README example content, excluded from parsing
968 │ } 969 │ // @shield:end 970 │ 971 │ // ─── graph/README.md ───────────────────────────────────────────────── 972 │ 973 │ // @shield:begin -- "graph/README example content, excluded from parsing" 974 │ function graphReadme(p: ArtifactProvenance, features: string[]): string { 975 │ return `# .guardlink/graph/ — generated diagrams 976 │ 977 │ **Every file here is generated. Do not edit any of them.** Regenerate with: 978 │
L1065 shield Shielded region
1060 │ 1061 │ Generated by ${p.generator}. For when, ask git — a timestamp here would1062 │ rewrite this file on every commit and tell you less than \`git log\` does.1063 │ `;1064 │ }1065 │ // @shield:end1066 │
src/blame/attach.ts flow 1comment 1 open 2
L8 flow #blame → ThreatModel
record.blame on exposures, confirmed and mitigations
3 │ * 4 │ * The one place `record.blame` is set. Only the CLI `--blame` paths call this; 5 │ * the MCP server and the TUI use `computeBlame` and keep their cached model 6 │ * clean, so `--blame` stays opt-in even inside a long-lived process. 7 │ * 8 │ * @flows #blame -> ThreatModel via attachBlame -- "record.blame on exposures, confirmed and mitigations" 9 │ * @comment -- "The field is invisible to the annotation hash (a whitelist of claim fields) and stripped from the committed model.json, so attaching it changes no tracked artifact; the same goes for blame_context, the commit counts the dashboard's rates need" 10 │ */ 11 │ import type { ThreatModel } from '../types/index.js'; 12 │ import { computeBlame, type ComputeBlameOptions } from './compute.js'; 13 │ import type { BlameComputation, RecordBlame } from './types.js';
L9 comment The field is invisible to the annotation hash (a whitelist of claim fields) and stripped from the committed model.json, so attaching it changes no tracked artifact; the same goes for blame_context, the commit counts the dashboard's rates need
The field is invisible to the annotation hash (a whitelist of claim fields) and stripped from the committed model.json, so attaching it changes no tracked artifact; the same goes for blame_context, the commit counts the dashboard's rates need
4 │ * The one place `record.blame` is set. Only the CLI `--blame` paths call this; 5 │ * the MCP server and the TUI use `computeBlame` and keep their cached model 6 │ * clean, so `--blame` stays opt-in even inside a long-lived process. 7 │ * 8 │ * @flows #blame -> ThreatModel via attachBlame -- "record.blame on exposures, confirmed and mitigations" 9 │ * @comment -- "The field is invisible to the annotation hash (a whitelist of claim fields) and stripped from the committed model.json, so attaching it changes no tracked artifact; the same goes for blame_context, the commit counts the dashboard's rates need" 10 │ */ 11 │ import type { ThreatModel } from '../types/index.js'; 12 │ import { computeBlame, type ComputeBlameOptions } from './compute.js'; 13 │ import type { BlameComputation, RecordBlame } from './types.js'; 14 │
src/blame/compute.ts comment 3exposes 2mitigates 2flow 2 open 11
L23 exposes #blame → #path-traversal
safeRelPath() receives location.file from each parsed record; a sidecar @source path is author text that can carry ../ into the argv computeBlame() hands to git
18 │ * 19 │ * Every degradation is per record and explicit in `status`; nothing here throws 20 │ * for one bad file, because one pathological file must not take down `blame` 21 │ * for the whole repository. 22 │ * 23 │ * @exposes #blame to #path-traversal [low] cwe:CWE-22 -- "safeRelPath() receives location.file from each parsed record; a sidecar @source path is author text that can carry ../ into the argv computeBlame() hands to git" 24 │ * @mitigates #blame against #path-traversal using #path-validation -- "safeRelPath resolves each path against root and keeps it only when it is root or lies under root + sep; anything else yields status error and git never sees it" 25 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "computeBlame() spawns git blame once per annotated file, git log -L once per distinct symbol span and listCommits() once over the whole history; a large model multiplies the spawns" 26 │ * @mitigates #blame against #dos using #resource-limits -- "Files are blamed once and memoised; -L is memoised per span and skipped for dirty files and file-wide anchors; commits resolve in one batched log call; the history walk is a single call and history: false skips it" 27 │ * @flows ThreatModel -> #blame via computeBlame -- "Record locations and anchors" 28 │ * @flows #blame -> ThreatModel via attachBlame -- "record.blame, only on the CLI --blame paths"
L24 mitigates #path-validation mitigates #path-traversal
safeRelPath resolves each path against root and keeps it only when it is root or lies under root + sep; anything else yields status error and git never sees it
19 │ * Every degradation is per record and explicit in `status`; nothing here throws 20 │ * for one bad file, because one pathological file must not take down `blame` 21 │ * for the whole repository. 22 │ * 23 │ * @exposes #blame to #path-traversal [low] cwe:CWE-22 -- "safeRelPath() receives location.file from each parsed record; a sidecar @source path is author text that can carry ../ into the argv computeBlame() hands to git" 24 │ * @mitigates #blame against #path-traversal using #path-validation -- "safeRelPath resolves each path against root and keeps it only when it is root or lies under root + sep; anything else yields status error and git never sees it" 25 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "computeBlame() spawns git blame once per annotated file, git log -L once per distinct symbol span and listCommits() once over the whole history; a large model multiplies the spawns" 26 │ * @mitigates #blame against #dos using #resource-limits -- "Files are blamed once and memoised; -L is memoised per span and skipped for dirty files and file-wide anchors; commits resolve in one batched log call; the history walk is a single call and history: false skips it" 27 │ * @flows ThreatModel -> #blame via computeBlame -- "Record locations and anchors" 28 │ * @flows #blame -> ThreatModel via attachBlame -- "record.blame, only on the CLI --blame paths" 29 │ * @handles pii on #blame -- "Author identities attached to each record, and every identity in the history counted for the per-100-commits rates"
L25 exposes #blame → #dos
computeBlame() spawns git blame once per annotated file, git log -L once per distinct symbol span and listCommits() once over the whole history; a large model multiplies the spawns
20 │ * for one bad file, because one pathological file must not take down `blame` 21 │ * for the whole repository. 22 │ * 23 │ * @exposes #blame to #path-traversal [low] cwe:CWE-22 -- "safeRelPath() receives location.file from each parsed record; a sidecar @source path is author text that can carry ../ into the argv computeBlame() hands to git" 24 │ * @mitigates #blame against #path-traversal using #path-validation -- "safeRelPath resolves each path against root and keeps it only when it is root or lies under root + sep; anything else yields status error and git never sees it" 25 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "computeBlame() spawns git blame once per annotated file, git log -L once per distinct symbol span and listCommits() once over the whole history; a large model multiplies the spawns" 26 │ * @mitigates #blame against #dos using #resource-limits -- "Files are blamed once and memoised; -L is memoised per span and skipped for dirty files and file-wide anchors; commits resolve in one batched log call; the history walk is a single call and history: false skips it" 27 │ * @flows ThreatModel -> #blame via computeBlame -- "Record locations and anchors" 28 │ * @flows #blame -> ThreatModel via attachBlame -- "record.blame, only on the CLI --blame paths" 29 │ * @handles pii on #blame -- "Author identities attached to each record, and every identity in the history counted for the per-100-commits rates" 30 │ * @comment -- "Opt-in and non-mutating by default: computeBlame returns a Map; the model is byte-identical afterwards. Attribution of an AI is only ever what a commit declared"
L26 mitigates #resource-limits mitigates #dos
Files are blamed once and memoised; -L is memoised per span and skipped for dirty files and file-wide anchors; commits resolve in one batched log call; the history walk is a single call and history: false skips it
21 │ * for the whole repository. 22 │ * 23 │ * @exposes #blame to #path-traversal [low] cwe:CWE-22 -- "safeRelPath() receives location.file from each parsed record; a sidecar @source path is author text that can carry ../ into the argv computeBlame() hands to git" 24 │ * @mitigates #blame against #path-traversal using #path-validation -- "safeRelPath resolves each path against root and keeps it only when it is root or lies under root + sep; anything else yields status error and git never sees it" 25 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "computeBlame() spawns git blame once per annotated file, git log -L once per distinct symbol span and listCommits() once over the whole history; a large model multiplies the spawns" 26 │ * @mitigates #blame against #dos using #resource-limits -- "Files are blamed once and memoised; -L is memoised per span and skipped for dirty files and file-wide anchors; commits resolve in one batched log call; the history walk is a single call and history: false skips it" 27 │ * @flows ThreatModel -> #blame via computeBlame -- "Record locations and anchors" 28 │ * @flows #blame -> ThreatModel via attachBlame -- "record.blame, only on the CLI --blame paths" 29 │ * @handles pii on #blame -- "Author identities attached to each record, and every identity in the history counted for the per-100-commits rates" 30 │ * @comment -- "Opt-in and non-mutating by default: computeBlame returns a Map; the model is byte-identical afterwards. Attribution of an AI is only ever what a commit declared" 31 │ * @comment -- "as_of is the HEAD commit's author date, never the wall clock, so every age in the summary is a function of the checkout alone"
L27 flow ThreatModel → #blame
Record locations and anchors
22 │ * 23 │ * @exposes #blame to #path-traversal [low] cwe:CWE-22 -- "safeRelPath() receives location.file from each parsed record; a sidecar @source path is author text that can carry ../ into the argv computeBlame() hands to git" 24 │ * @mitigates #blame against #path-traversal using #path-validation -- "safeRelPath resolves each path against root and keeps it only when it is root or lies under root + sep; anything else yields status error and git never sees it" 25 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "computeBlame() spawns git blame once per annotated file, git log -L once per distinct symbol span and listCommits() once over the whole history; a large model multiplies the spawns" 26 │ * @mitigates #blame against #dos using #resource-limits -- "Files are blamed once and memoised; -L is memoised per span and skipped for dirty files and file-wide anchors; commits resolve in one batched log call; the history walk is a single call and history: false skips it" 27 │ * @flows ThreatModel -> #blame via computeBlame -- "Record locations and anchors" 28 │ * @flows #blame -> ThreatModel via attachBlame -- "record.blame, only on the CLI --blame paths" 29 │ * @handles pii on #blame -- "Author identities attached to each record, and every identity in the history counted for the per-100-commits rates" 30 │ * @comment -- "Opt-in and non-mutating by default: computeBlame returns a Map; the model is byte-identical afterwards. Attribution of an AI is only ever what a commit declared" 31 │ * @comment -- "as_of is the HEAD commit's author date, never the wall clock, so every age in the summary is a function of the checkout alone" 32 │ */
L28 flow #blame → ThreatModel
record.blame, only on the CLI --blame paths
23 │ * @exposes #blame to #path-traversal [low] cwe:CWE-22 -- "safeRelPath() receives location.file from each parsed record; a sidecar @source path is author text that can carry ../ into the argv computeBlame() hands to git" 24 │ * @mitigates #blame against #path-traversal using #path-validation -- "safeRelPath resolves each path against root and keeps it only when it is root or lies under root + sep; anything else yields status error and git never sees it" 25 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "computeBlame() spawns git blame once per annotated file, git log -L once per distinct symbol span and listCommits() once over the whole history; a large model multiplies the spawns" 26 │ * @mitigates #blame against #dos using #resource-limits -- "Files are blamed once and memoised; -L is memoised per span and skipped for dirty files and file-wide anchors; commits resolve in one batched log call; the history walk is a single call and history: false skips it" 27 │ * @flows ThreatModel -> #blame via computeBlame -- "Record locations and anchors" 28 │ * @flows #blame -> ThreatModel via attachBlame -- "record.blame, only on the CLI --blame paths" 29 │ * @handles pii on #blame -- "Author identities attached to each record, and every identity in the history counted for the per-100-commits rates" 30 │ * @comment -- "Opt-in and non-mutating by default: computeBlame returns a Map; the model is byte-identical afterwards. Attribution of an AI is only ever what a commit declared" 31 │ * @comment -- "as_of is the HEAD commit's author date, never the wall clock, so every age in the summary is a function of the checkout alone" 32 │ */ 33 │ import { existsSync } from 'node:fs';
L29 handles #blame: pii
Author identities attached to each record, and every identity in the history counted for the per-100-commits rates
24 │ * @mitigates #blame against #path-traversal using #path-validation -- "safeRelPath resolves each path against root and keeps it only when it is root or lies under root + sep; anything else yields status error and git never sees it" 25 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "computeBlame() spawns git blame once per annotated file, git log -L once per distinct symbol span and listCommits() once over the whole history; a large model multiplies the spawns" 26 │ * @mitigates #blame against #dos using #resource-limits -- "Files are blamed once and memoised; -L is memoised per span and skipped for dirty files and file-wide anchors; commits resolve in one batched log call; the history walk is a single call and history: false skips it" 27 │ * @flows ThreatModel -> #blame via computeBlame -- "Record locations and anchors" 28 │ * @flows #blame -> ThreatModel via attachBlame -- "record.blame, only on the CLI --blame paths" 29 │ * @handles pii on #blame -- "Author identities attached to each record, and every identity in the history counted for the per-100-commits rates" 30 │ * @comment -- "Opt-in and non-mutating by default: computeBlame returns a Map; the model is byte-identical afterwards. Attribution of an AI is only ever what a commit declared" 31 │ * @comment -- "as_of is the HEAD commit's author date, never the wall clock, so every age in the summary is a function of the checkout alone" 32 │ */ 33 │ import { existsSync } from 'node:fs'; 34 │ import { join, resolve, sep } from 'node:path';
L30 comment Opt-in and non-mutating by default: computeBlame returns a Map; the model is byte-identical afterwards. Attribution of an AI is only ever what a commit declared
Opt-in and non-mutating by default: computeBlame returns a Map; the model is byte-identical afterwards. Attribution of an AI is only ever what a commit declared
25 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "computeBlame() spawns git blame once per annotated file, git log -L once per distinct symbol span and listCommits() once over the whole history; a large model multiplies the spawns" 26 │ * @mitigates #blame against #dos using #resource-limits -- "Files are blamed once and memoised; -L is memoised per span and skipped for dirty files and file-wide anchors; commits resolve in one batched log call; the history walk is a single call and history: false skips it" 27 │ * @flows ThreatModel -> #blame via computeBlame -- "Record locations and anchors" 28 │ * @flows #blame -> ThreatModel via attachBlame -- "record.blame, only on the CLI --blame paths" 29 │ * @handles pii on #blame -- "Author identities attached to each record, and every identity in the history counted for the per-100-commits rates" 30 │ * @comment -- "Opt-in and non-mutating by default: computeBlame returns a Map; the model is byte-identical afterwards. Attribution of an AI is only ever what a commit declared" 31 │ * @comment -- "as_of is the HEAD commit's author date, never the wall clock, so every age in the summary is a function of the checkout alone" 32 │ */ 33 │ import { existsSync } from 'node:fs'; 34 │ import { join, resolve, sep } from 'node:path'; 35 │ import type {
L31 comment as_of is the HEAD commit's author date, never the wall clock, so every age in the summary is a function of the checkout alone
as_of is the HEAD commit's author date, never the wall clock, so every age in the summary is a function of the checkout alone
26 │ * @mitigates #blame against #dos using #resource-limits -- "Files are blamed once and memoised; -L is memoised per span and skipped for dirty files and file-wide anchors; commits resolve in one batched log call; the history walk is a single call and history: false skips it" 27 │ * @flows ThreatModel -> #blame via computeBlame -- "Record locations and anchors" 28 │ * @flows #blame -> ThreatModel via attachBlame -- "record.blame, only on the CLI --blame paths" 29 │ * @handles pii on #blame -- "Author identities attached to each record, and every identity in the history counted for the per-100-commits rates" 30 │ * @comment -- "Opt-in and non-mutating by default: computeBlame returns a Map; the model is byte-identical afterwards. Attribution of an AI is only ever what a commit declared" 31 │ * @comment -- "as_of is the HEAD commit's author date, never the wall clock, so every age in the summary is a function of the checkout alone" 32 │ */ 33 │ import { existsSync } from 'node:fs'; 34 │ import { join, resolve, sep } from 'node:path'; 35 │ import type { 36 │ SourceLocation, ThreatModel, ThreatModelConfirmed, ThreatModelExposure, ThreatModelMitigation,
L264 handles #blame: pii
Every author and human co-author identity in the history, counted per person
259 │ * mode as the records so a key here is the string a summary row carries. On a 260 │ * shallow clone the history is truncated and every count is a lower bound, 261 │ * which `status: 'shallow'` already says. Keys are sorted so the JSON is the 262 │ * same bytes on every run. 263 │ * 264 │ * @handles pii on #blame -- "Every author and human co-author identity in the history, counted per person" 265 │ * @comment -- "A commit credits its author and each human co-author once; a bot-authored commit with no human co-author has an agent: author and credits no person. An AI tool is credited once per commit however many trailers name it" 266 │ */ 267 │ function countCommits(history: RawCommit[], rules: readonly CompiledRule[], mode: IdentityMode): CommitCounts { 268 │ const byHuman = new Map<string, number>(); 269 │ const byAgent = new Map<string, { tool: string; model: string | null; commits: number }>();
L265 comment A commit credits its author and each human co-author once; a bot-authored commit with no human co-author has an agent: author and credits no person. An AI tool is credited once per commit however many trailers name it
A commit credits its author and each human co-author once; a bot-authored commit with no human co-author has an agent: author and credits no person. An AI tool is credited once per commit however many trailers name it
260 │ * shallow clone the history is truncated and every count is a lower bound, 261 │ * which `status: 'shallow'` already says. Keys are sorted so the JSON is the 262 │ * same bytes on every run. 263 │ * 264 │ * @handles pii on #blame -- "Every author and human co-author identity in the history, counted per person" 265 │ * @comment -- "A commit credits its author and each human co-author once; a bot-authored commit with no human co-author has an agent: author and credits no person. An AI tool is credited once per commit however many trailers name it" 266 │ */ 267 │ function countCommits(history: RawCommit[], rules: readonly CompiledRule[], mode: IdentityMode): CommitCounts { 268 │ const byHuman = new Map<string, number>(); 269 │ const byAgent = new Map<string, { tool: string; model: string | null; commits: number }>(); 270 │ let ai_assisted = 0;
src/blame/config.ts exposes 2mitigates 2flow 1comment 1 open 6
L22 exposes #blame → #redos
readBlameConfig() compiles blame.tools[].match from .guardlink/config.json into RegExp objects that attributeCommit() runs against every author and trailer in the history
17 │ * name its own bot or override how a vendor's trailer is read without a code 18 │ * change. Same shape as `readDisabledDiagnostics` in the parser: absent or 19 │ * unreadable config returns the defaults, deliberately — a broken config must 20 │ * not silently change who gets attributed. 21 │ * 22 │ * @exposes #blame to #redos [low] cwe:CWE-1333 -- "readBlameConfig() compiles blame.tools[].match from .guardlink/config.json into RegExp objects that attributeCommit() runs against every author and trailer in the history" 23 │ * @mitigates #blame against #redos using #regex-anchoring -- "Every pattern is anchored (^…$), capped at 256 characters and compiled once; a pattern that fails to compile drops its whole rule rather than half-matching" 24 │ * @exposes #blame to #path-traversal [low] cwe:CWE-22 -- "ignore_revs names a file git will open with --ignore-revs-file" 25 │ * @mitigates #blame against #path-traversal using #path-validation -- "The value is resolved against root and kept only when it is root or lies under root + sep; anything else becomes null and git is never told about it" 26 │ * @flows ConfigFile -> #blame via readBlameConfig -- "Attribution settings" 27 │ * @comment -- "The default ignore_revs is .git-blame-ignore-revs, the file GitHub already honours; git.ts passes it only when it exists"
L23 mitigates #regex-anchoring mitigates #redos
Every pattern is anchored (^…$), capped at 256 characters and compiled once; a pattern that fails to compile drops its whole rule rather than half-matching
18 │ * change. Same shape as `readDisabledDiagnostics` in the parser: absent or 19 │ * unreadable config returns the defaults, deliberately — a broken config must 20 │ * not silently change who gets attributed. 21 │ * 22 │ * @exposes #blame to #redos [low] cwe:CWE-1333 -- "readBlameConfig() compiles blame.tools[].match from .guardlink/config.json into RegExp objects that attributeCommit() runs against every author and trailer in the history" 23 │ * @mitigates #blame against #redos using #regex-anchoring -- "Every pattern is anchored (^…$), capped at 256 characters and compiled once; a pattern that fails to compile drops its whole rule rather than half-matching" 24 │ * @exposes #blame to #path-traversal [low] cwe:CWE-22 -- "ignore_revs names a file git will open with --ignore-revs-file" 25 │ * @mitigates #blame against #path-traversal using #path-validation -- "The value is resolved against root and kept only when it is root or lies under root + sep; anything else becomes null and git is never told about it" 26 │ * @flows ConfigFile -> #blame via readBlameConfig -- "Attribution settings" 27 │ * @comment -- "The default ignore_revs is .git-blame-ignore-revs, the file GitHub already honours; git.ts passes it only when it exists" 28 │ */
L24 exposes #blame → #path-traversal
ignore_revs names a file git will open with --ignore-revs-file
19 │ * unreadable config returns the defaults, deliberately — a broken config must 20 │ * not silently change who gets attributed. 21 │ * 22 │ * @exposes #blame to #redos [low] cwe:CWE-1333 -- "readBlameConfig() compiles blame.tools[].match from .guardlink/config.json into RegExp objects that attributeCommit() runs against every author and trailer in the history" 23 │ * @mitigates #blame against #redos using #regex-anchoring -- "Every pattern is anchored (^…$), capped at 256 characters and compiled once; a pattern that fails to compile drops its whole rule rather than half-matching" 24 │ * @exposes #blame to #path-traversal [low] cwe:CWE-22 -- "ignore_revs names a file git will open with --ignore-revs-file" 25 │ * @mitigates #blame against #path-traversal using #path-validation -- "The value is resolved against root and kept only when it is root or lies under root + sep; anything else becomes null and git is never told about it" 26 │ * @flows ConfigFile -> #blame via readBlameConfig -- "Attribution settings" 27 │ * @comment -- "The default ignore_revs is .git-blame-ignore-revs, the file GitHub already honours; git.ts passes it only when it exists" 28 │ */ 29 │ import { readFileSync } from 'node:fs';
L25 mitigates #path-validation mitigates #path-traversal
The value is resolved against root and kept only when it is root or lies under root + sep; anything else becomes null and git is never told about it
20 │ * not silently change who gets attributed. 21 │ * 22 │ * @exposes #blame to #redos [low] cwe:CWE-1333 -- "readBlameConfig() compiles blame.tools[].match from .guardlink/config.json into RegExp objects that attributeCommit() runs against every author and trailer in the history" 23 │ * @mitigates #blame against #redos using #regex-anchoring -- "Every pattern is anchored (^…$), capped at 256 characters and compiled once; a pattern that fails to compile drops its whole rule rather than half-matching" 24 │ * @exposes #blame to #path-traversal [low] cwe:CWE-22 -- "ignore_revs names a file git will open with --ignore-revs-file" 25 │ * @mitigates #blame against #path-traversal using #path-validation -- "The value is resolved against root and kept only when it is root or lies under root + sep; anything else becomes null and git is never told about it" 26 │ * @flows ConfigFile -> #blame via readBlameConfig -- "Attribution settings" 27 │ * @comment -- "The default ignore_revs is .git-blame-ignore-revs, the file GitHub already honours; git.ts passes it only when it exists" 28 │ */ 29 │ import { readFileSync } from 'node:fs'; 30 │ import { join, resolve, sep } from 'node:path';
L26 flow ConfigFile → #blame
Attribution settings
21 │ * 22 │ * @exposes #blame to #redos [low] cwe:CWE-1333 -- "readBlameConfig() compiles blame.tools[].match from .guardlink/config.json into RegExp objects that attributeCommit() runs against every author and trailer in the history" 23 │ * @mitigates #blame against #redos using #regex-anchoring -- "Every pattern is anchored (^…$), capped at 256 characters and compiled once; a pattern that fails to compile drops its whole rule rather than half-matching" 24 │ * @exposes #blame to #path-traversal [low] cwe:CWE-22 -- "ignore_revs names a file git will open with --ignore-revs-file" 25 │ * @mitigates #blame against #path-traversal using #path-validation -- "The value is resolved against root and kept only when it is root or lies under root + sep; anything else becomes null and git is never told about it" 26 │ * @flows ConfigFile -> #blame via readBlameConfig -- "Attribution settings" 27 │ * @comment -- "The default ignore_revs is .git-blame-ignore-revs, the file GitHub already honours; git.ts passes it only when it exists" 28 │ */ 29 │ import { readFileSync } from 'node:fs'; 30 │ import { join, resolve, sep } from 'node:path'; 31 │ import type { BlameConfig, CompiledRule, IdentityMode, ModelRule, ToolRule } from './types.js';
L27 comment The default ignore_revs is .git-blame-ignore-revs, the file GitHub already honours; git.ts passes it only when it exists
The default ignore_revs is .git-blame-ignore-revs, the file GitHub already honours; git.ts passes it only when it exists
22 │ * @exposes #blame to #redos [low] cwe:CWE-1333 -- "readBlameConfig() compiles blame.tools[].match from .guardlink/config.json into RegExp objects that attributeCommit() runs against every author and trailer in the history" 23 │ * @mitigates #blame against #redos using #regex-anchoring -- "Every pattern is anchored (^…$), capped at 256 characters and compiled once; a pattern that fails to compile drops its whole rule rather than half-matching" 24 │ * @exposes #blame to #path-traversal [low] cwe:CWE-22 -- "ignore_revs names a file git will open with --ignore-revs-file" 25 │ * @mitigates #blame against #path-traversal using #path-validation -- "The value is resolved against root and kept only when it is root or lies under root + sep; anything else becomes null and git is never told about it" 26 │ * @flows ConfigFile -> #blame via readBlameConfig -- "Attribution settings" 27 │ * @comment -- "The default ignore_revs is .git-blame-ignore-revs, the file GitHub already honours; git.ts passes it only when it exists" 28 │ */ 29 │ import { readFileSync } from 'node:fs'; 30 │ import { join, resolve, sep } from 'node:path'; 31 │ import type { BlameConfig, CompiledRule, IdentityMode, ModelRule, ToolRule } from './types.js'; 32 │
src/blame/format.ts comment 2handles 1flow 1 open 4
L8 handles #blame: pii
Identity strings printed to the terminal
3 │ * 4 │ * One group per file, one block per claim, then the two summary tables and 5 │ * the quarterly trend. Plain `padEnd` columns and no colour, so the output 6 │ * pipes cleanly and the TUI can indent it. 7 │ * 8 │ * @handles pii on #blame -- "Identity strings printed to the terminal" 9 │ * @flows #blame -> #cli via formatBlameText -- "Human-readable attribution" 10 │ * @comment -- "Every degraded status is printed next to the claim it degrades, so a reader never mistakes an unattributed claim for a clean one" 11 │ * @comment -- "A rate with no denominator (no commit counts, or an identity with no commits) prints as a dash, never as 0: an absent number must not read as a good one" 12 │ */ 13 │ import type { AgentSummaryRow, BlameEntry, BlamePayload, CommitRef, HumanSummaryRow, TrendBucket } from './types.js';
L9 flow #blame → #cli
Human-readable attribution
4 │ * One group per file, one block per claim, then the two summary tables and 5 │ * the quarterly trend. Plain `padEnd` columns and no colour, so the output 6 │ * pipes cleanly and the TUI can indent it. 7 │ * 8 │ * @handles pii on #blame -- "Identity strings printed to the terminal" 9 │ * @flows #blame -> #cli via formatBlameText -- "Human-readable attribution" 10 │ * @comment -- "Every degraded status is printed next to the claim it degrades, so a reader never mistakes an unattributed claim for a clean one" 11 │ * @comment -- "A rate with no denominator (no commit counts, or an identity with no commits) prints as a dash, never as 0: an absent number must not read as a good one" 12 │ */ 13 │ import type { AgentSummaryRow, BlameEntry, BlamePayload, CommitRef, HumanSummaryRow, TrendBucket } from './types.js'; 14 │
L10 comment Every degraded status is printed next to the claim it degrades, so a reader never mistakes an unattributed claim for a clean one
Every degraded status is printed next to the claim it degrades, so a reader never mistakes an unattributed claim for a clean one
5 │ * the quarterly trend. Plain `padEnd` columns and no colour, so the output 6 │ * pipes cleanly and the TUI can indent it. 7 │ * 8 │ * @handles pii on #blame -- "Identity strings printed to the terminal" 9 │ * @flows #blame -> #cli via formatBlameText -- "Human-readable attribution" 10 │ * @comment -- "Every degraded status is printed next to the claim it degrades, so a reader never mistakes an unattributed claim for a clean one" 11 │ * @comment -- "A rate with no denominator (no commit counts, or an identity with no commits) prints as a dash, never as 0: an absent number must not read as a good one" 12 │ */ 13 │ import type { AgentSummaryRow, BlameEntry, BlamePayload, CommitRef, HumanSummaryRow, TrendBucket } from './types.js'; 14 │ 15 │ function who(ref: CommitRef | null): string {
L11 comment A rate with no denominator (no commit counts, or an identity with no commits) prints as a dash, never as 0: an absent number must not read as a good one
A rate with no denominator (no commit counts, or an identity with no commits) prints as a dash, never as 0: an absent number must not read as a good one
6 │ * pipes cleanly and the TUI can indent it. 7 │ * 8 │ * @handles pii on #blame -- "Identity strings printed to the terminal" 9 │ * @flows #blame -> #cli via formatBlameText -- "Human-readable attribution" 10 │ * @comment -- "Every degraded status is printed next to the claim it degrades, so a reader never mistakes an unattributed claim for a clean one" 11 │ * @comment -- "A rate with no denominator (no commit counts, or an identity with no commits) prints as a dash, never as 0: an absent number must not read as a good one" 12 │ */ 13 │ import type { AgentSummaryRow, BlameEntry, BlamePayload, CommitRef, HumanSummaryRow, TrendBucket } from './types.js'; 14 │ 15 │ function who(ref: CommitRef | null): string { 16 │ if (!ref) return '—';
src/blame/git.ts exposes 4mitigates 4flow 2handles 2 open 14
L13 exposes #blame → #cmd-injection
gitExec() builds the argv it spawns from location.file and the start/end line numbers of each parsed record
8 │ * 9 │ * The parsers (`parseBlamePorcelain`, `parseLogRecords`) are pure and exported 10 │ * so they can be pinned on captured output; the commands take an injectable 11 │ * `exec` so batching can be asserted without spawning. 12 │ * 13 │ * @exposes #blame to #cmd-injection [low] cwe:CWE-78 -- "gitExec() builds the argv it spawns from location.file and the start/end line numbers of each parsed record" 14 │ * @mitigates #blame against #cmd-injection using #param-commands -- "execFileSync with an argv array and no shell; paths follow a literal -- and line ranges are integers formatted here, never caller strings" 15 │ * @exposes #blame to #path-traversal [low] cwe:CWE-22 -- "blameFile(), spanOldestCommit() and fileAddCommit() pass location.file to git blame, git log and git ls-files, which read whatever path it names" 16 │ * @mitigates #blame against #path-traversal using #path-validation -- "compute.ts resolves every path against root and drops any that escapes it before this module sees it, and git itself refuses paths outside the work tree" 17 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "git log -L walks history once per distinct span, blame reads every annotated file, and listCommits reads the whole history reachable from HEAD" 18 │ * @mitigates #blame against #dos using #resource-limits -- "30 s timeout and 64 MiB output cap per call; one blame per file; sha resolution and path queries batched; -L only for symbol/block spans on clean files; the history walk is one call a caller can skip"
L14 mitigates #param-commands mitigates #cmd-injection
execFileSync with an argv array and no shell; paths follow a literal -- and line ranges are integers formatted here, never caller strings
9 │ * The parsers (`parseBlamePorcelain`, `parseLogRecords`) are pure and exported 10 │ * so they can be pinned on captured output; the commands take an injectable 11 │ * `exec` so batching can be asserted without spawning. 12 │ * 13 │ * @exposes #blame to #cmd-injection [low] cwe:CWE-78 -- "gitExec() builds the argv it spawns from location.file and the start/end line numbers of each parsed record" 14 │ * @mitigates #blame against #cmd-injection using #param-commands -- "execFileSync with an argv array and no shell; paths follow a literal -- and line ranges are integers formatted here, never caller strings" 15 │ * @exposes #blame to #path-traversal [low] cwe:CWE-22 -- "blameFile(), spanOldestCommit() and fileAddCommit() pass location.file to git blame, git log and git ls-files, which read whatever path it names" 16 │ * @mitigates #blame against #path-traversal using #path-validation -- "compute.ts resolves every path against root and drops any that escapes it before this module sees it, and git itself refuses paths outside the work tree" 17 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "git log -L walks history once per distinct span, blame reads every annotated file, and listCommits reads the whole history reachable from HEAD" 18 │ * @mitigates #blame against #dos using #resource-limits -- "30 s timeout and 64 MiB output cap per call; one blame per file; sha resolution and path queries batched; -L only for symbol/block spans on clean files; the history walk is one call a caller can skip" 19 │ * @flows GitRepo -> #blame via execFileSync -- "blame, log, ls-files, status and rev-parse output"
L15 exposes #blame → #path-traversal
blameFile(), spanOldestCommit() and fileAddCommit() pass location.file to git blame, git log and git ls-files, which read whatever path it names
10 │ * so they can be pinned on captured output; the commands take an injectable 11 │ * `exec` so batching can be asserted without spawning. 12 │ * 13 │ * @exposes #blame to #cmd-injection [low] cwe:CWE-78 -- "gitExec() builds the argv it spawns from location.file and the start/end line numbers of each parsed record" 14 │ * @mitigates #blame against #cmd-injection using #param-commands -- "execFileSync with an argv array and no shell; paths follow a literal -- and line ranges are integers formatted here, never caller strings" 15 │ * @exposes #blame to #path-traversal [low] cwe:CWE-22 -- "blameFile(), spanOldestCommit() and fileAddCommit() pass location.file to git blame, git log and git ls-files, which read whatever path it names" 16 │ * @mitigates #blame against #path-traversal using #path-validation -- "compute.ts resolves every path against root and drops any that escapes it before this module sees it, and git itself refuses paths outside the work tree" 17 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "git log -L walks history once per distinct span, blame reads every annotated file, and listCommits reads the whole history reachable from HEAD" 18 │ * @mitigates #blame against #dos using #resource-limits -- "30 s timeout and 64 MiB output cap per call; one blame per file; sha resolution and path queries batched; -L only for symbol/block spans on clean files; the history walk is one call a caller can skip" 19 │ * @flows GitRepo -> #blame via execFileSync -- "blame, log, ls-files, status and rev-parse output" 20 │ * @handles pii on #blame -- "Author names, emails and dates from git log"
L16 mitigates #path-validation mitigates #path-traversal
compute.ts resolves every path against root and drops any that escapes it before this module sees it, and git itself refuses paths outside the work tree
11 │ * `exec` so batching can be asserted without spawning. 12 │ * 13 │ * @exposes #blame to #cmd-injection [low] cwe:CWE-78 -- "gitExec() builds the argv it spawns from location.file and the start/end line numbers of each parsed record" 14 │ * @mitigates #blame against #cmd-injection using #param-commands -- "execFileSync with an argv array and no shell; paths follow a literal -- and line ranges are integers formatted here, never caller strings" 15 │ * @exposes #blame to #path-traversal [low] cwe:CWE-22 -- "blameFile(), spanOldestCommit() and fileAddCommit() pass location.file to git blame, git log and git ls-files, which read whatever path it names" 16 │ * @mitigates #blame against #path-traversal using #path-validation -- "compute.ts resolves every path against root and drops any that escapes it before this module sees it, and git itself refuses paths outside the work tree" 17 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "git log -L walks history once per distinct span, blame reads every annotated file, and listCommits reads the whole history reachable from HEAD" 18 │ * @mitigates #blame against #dos using #resource-limits -- "30 s timeout and 64 MiB output cap per call; one blame per file; sha resolution and path queries batched; -L only for symbol/block spans on clean files; the history walk is one call a caller can skip" 19 │ * @flows GitRepo -> #blame via execFileSync -- "blame, log, ls-files, status and rev-parse output" 20 │ * @handles pii on #blame -- "Author names, emails and dates from git log" 21 │ * @comment -- "Read-only by construction: no command here writes to the repository, and optional index refreshes are disabled"
L17 exposes #blame → #dos
git log -L walks history once per distinct span, blame reads every annotated file, and listCommits reads the whole history reachable from HEAD
12 │ * 13 │ * @exposes #blame to #cmd-injection [low] cwe:CWE-78 -- "gitExec() builds the argv it spawns from location.file and the start/end line numbers of each parsed record" 14 │ * @mitigates #blame against #cmd-injection using #param-commands -- "execFileSync with an argv array and no shell; paths follow a literal -- and line ranges are integers formatted here, never caller strings" 15 │ * @exposes #blame to #path-traversal [low] cwe:CWE-22 -- "blameFile(), spanOldestCommit() and fileAddCommit() pass location.file to git blame, git log and git ls-files, which read whatever path it names" 16 │ * @mitigates #blame against #path-traversal using #path-validation -- "compute.ts resolves every path against root and drops any that escapes it before this module sees it, and git itself refuses paths outside the work tree" 17 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "git log -L walks history once per distinct span, blame reads every annotated file, and listCommits reads the whole history reachable from HEAD" 18 │ * @mitigates #blame against #dos using #resource-limits -- "30 s timeout and 64 MiB output cap per call; one blame per file; sha resolution and path queries batched; -L only for symbol/block spans on clean files; the history walk is one call a caller can skip" 19 │ * @flows GitRepo -> #blame via execFileSync -- "blame, log, ls-files, status and rev-parse output" 20 │ * @handles pii on #blame -- "Author names, emails and dates from git log" 21 │ * @comment -- "Read-only by construction: no command here writes to the repository, and optional index refreshes are disabled" 22 │ */
L18 mitigates #resource-limits mitigates #dos
30 s timeout and 64 MiB output cap per call; one blame per file; sha resolution and path queries batched; -L only for symbol/block spans on clean files; the history walk is one call a caller can skip
13 │ * @exposes #blame to #cmd-injection [low] cwe:CWE-78 -- "gitExec() builds the argv it spawns from location.file and the start/end line numbers of each parsed record" 14 │ * @mitigates #blame against #cmd-injection using #param-commands -- "execFileSync with an argv array and no shell; paths follow a literal -- and line ranges are integers formatted here, never caller strings" 15 │ * @exposes #blame to #path-traversal [low] cwe:CWE-22 -- "blameFile(), spanOldestCommit() and fileAddCommit() pass location.file to git blame, git log and git ls-files, which read whatever path it names" 16 │ * @mitigates #blame against #path-traversal using #path-validation -- "compute.ts resolves every path against root and drops any that escapes it before this module sees it, and git itself refuses paths outside the work tree" 17 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "git log -L walks history once per distinct span, blame reads every annotated file, and listCommits reads the whole history reachable from HEAD" 18 │ * @mitigates #blame against #dos using #resource-limits -- "30 s timeout and 64 MiB output cap per call; one blame per file; sha resolution and path queries batched; -L only for symbol/block spans on clean files; the history walk is one call a caller can skip" 19 │ * @flows GitRepo -> #blame via execFileSync -- "blame, log, ls-files, status and rev-parse output" 20 │ * @handles pii on #blame -- "Author names, emails and dates from git log" 21 │ * @comment -- "Read-only by construction: no command here writes to the repository, and optional index refreshes are disabled" 22 │ */ 23 │ import { execFileSync } from 'node:child_process';
L19 flow GitRepo → #blame
blame, log, ls-files, status and rev-parse output
14 │ * @mitigates #blame against #cmd-injection using #param-commands -- "execFileSync with an argv array and no shell; paths follow a literal -- and line ranges are integers formatted here, never caller strings" 15 │ * @exposes #blame to #path-traversal [low] cwe:CWE-22 -- "blameFile(), spanOldestCommit() and fileAddCommit() pass location.file to git blame, git log and git ls-files, which read whatever path it names" 16 │ * @mitigates #blame against #path-traversal using #path-validation -- "compute.ts resolves every path against root and drops any that escapes it before this module sees it, and git itself refuses paths outside the work tree" 17 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "git log -L walks history once per distinct span, blame reads every annotated file, and listCommits reads the whole history reachable from HEAD" 18 │ * @mitigates #blame against #dos using #resource-limits -- "30 s timeout and 64 MiB output cap per call; one blame per file; sha resolution and path queries batched; -L only for symbol/block spans on clean files; the history walk is one call a caller can skip" 19 │ * @flows GitRepo -> #blame via execFileSync -- "blame, log, ls-files, status and rev-parse output" 20 │ * @handles pii on #blame -- "Author names, emails and dates from git log" 21 │ * @comment -- "Read-only by construction: no command here writes to the repository, and optional index refreshes are disabled" 22 │ */ 23 │ import { execFileSync } from 'node:child_process'; 24 │ import { existsSync } from 'node:fs';
L20 handles #blame: pii
Author names, emails and dates from git log
15 │ * @exposes #blame to #path-traversal [low] cwe:CWE-22 -- "blameFile(), spanOldestCommit() and fileAddCommit() pass location.file to git blame, git log and git ls-files, which read whatever path it names" 16 │ * @mitigates #blame against #path-traversal using #path-validation -- "compute.ts resolves every path against root and drops any that escapes it before this module sees it, and git itself refuses paths outside the work tree" 17 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "git log -L walks history once per distinct span, blame reads every annotated file, and listCommits reads the whole history reachable from HEAD" 18 │ * @mitigates #blame against #dos using #resource-limits -- "30 s timeout and 64 MiB output cap per call; one blame per file; sha resolution and path queries batched; -L only for symbol/block spans on clean files; the history walk is one call a caller can skip" 19 │ * @flows GitRepo -> #blame via execFileSync -- "blame, log, ls-files, status and rev-parse output" 20 │ * @handles pii on #blame -- "Author names, emails and dates from git log" 21 │ * @comment -- "Read-only by construction: no command here writes to the repository, and optional index refreshes are disabled" 22 │ */ 23 │ import { execFileSync } from 'node:child_process'; 24 │ import { existsSync } from 'node:fs'; 25 │ import { join } from 'node:path';
L21 comment Read-only by construction: no command here writes to the repository, and optional index refreshes are disabled
Read-only by construction: no command here writes to the repository, and optional index refreshes are disabled
16 │ * @mitigates #blame against #path-traversal using #path-validation -- "compute.ts resolves every path against root and drops any that escapes it before this module sees it, and git itself refuses paths outside the work tree" 17 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "git log -L walks history once per distinct span, blame reads every annotated file, and listCommits reads the whole history reachable from HEAD" 18 │ * @mitigates #blame against #dos using #resource-limits -- "30 s timeout and 64 MiB output cap per call; one blame per file; sha resolution and path queries batched; -L only for symbol/block spans on clean files; the history walk is one call a caller can skip" 19 │ * @flows GitRepo -> #blame via execFileSync -- "blame, log, ls-files, status and rev-parse output" 20 │ * @handles pii on #blame -- "Author names, emails and dates from git log" 21 │ * @comment -- "Read-only by construction: no command here writes to the repository, and optional index refreshes are disabled" 22 │ */ 23 │ import { execFileSync } from 'node:child_process'; 24 │ import { existsSync } from 'node:fs'; 25 │ import { join } from 'node:path'; 26 │ import type { RawCommit } from './types.js';
L201 exposes #blame → #dos
listCommits() runs git log over every commit reachable from HEAD and parseLogRecords() parses each trailer block; history size, not model size, sets the cost
196 │ * `resolveCommits` reads — the denominator of every "per 100 commits" rate 197 │ * and the source of `as_of`. `HEAD --` pins the argument as a revision even 198 │ * when a file is named HEAD. An empty repository (no HEAD yet) or a plain 199 │ * directory is an empty history, not an error. 200 │ * 201 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "listCommits() runs git log over every commit reachable from HEAD and parseLogRecords() parses each trailer block; history size, not model size, sets the cost" 202 │ * @mitigates #blame against #dos using #resource-limits -- "A single log call under the same 30 s timeout and 64 MiB output cap; nothing is spawned per commit, and compute.ts lets a caller skip the walk with history: false" 203 │ * @handles pii on #blame -- "Author names, emails and co-author trailers of every commit in the history" 204 │ * @flows GitRepo -> #blame via listCommits -- "The reachable history, for commit counts and the HEAD author date" 205 │ * @comment -- "Reachable from HEAD only, never --all: what other branches carry is not this checkout's history" 206 │ */
L202 mitigates #resource-limits mitigates #dos
A single log call under the same 30 s timeout and 64 MiB output cap; nothing is spawned per commit, and compute.ts lets a caller skip the walk with history: false
197 │ * and the source of `as_of`. `HEAD --` pins the argument as a revision even 198 │ * when a file is named HEAD. An empty repository (no HEAD yet) or a plain 199 │ * directory is an empty history, not an error. 200 │ * 201 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "listCommits() runs git log over every commit reachable from HEAD and parseLogRecords() parses each trailer block; history size, not model size, sets the cost" 202 │ * @mitigates #blame against #dos using #resource-limits -- "A single log call under the same 30 s timeout and 64 MiB output cap; nothing is spawned per commit, and compute.ts lets a caller skip the walk with history: false" 203 │ * @handles pii on #blame -- "Author names, emails and co-author trailers of every commit in the history" 204 │ * @flows GitRepo -> #blame via listCommits -- "The reachable history, for commit counts and the HEAD author date" 205 │ * @comment -- "Reachable from HEAD only, never --all: what other branches carry is not this checkout's history" 206 │ */ 207 │ export function listCommits(root: string, exec: GitExec = gitExec): RawCommit[] {
L203 handles #blame: pii
Author names, emails and co-author trailers of every commit in the history
198 │ * when a file is named HEAD. An empty repository (no HEAD yet) or a plain 199 │ * directory is an empty history, not an error. 200 │ * 201 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "listCommits() runs git log over every commit reachable from HEAD and parseLogRecords() parses each trailer block; history size, not model size, sets the cost" 202 │ * @mitigates #blame against #dos using #resource-limits -- "A single log call under the same 30 s timeout and 64 MiB output cap; nothing is spawned per commit, and compute.ts lets a caller skip the walk with history: false" 203 │ * @handles pii on #blame -- "Author names, emails and co-author trailers of every commit in the history" 204 │ * @flows GitRepo -> #blame via listCommits -- "The reachable history, for commit counts and the HEAD author date" 205 │ * @comment -- "Reachable from HEAD only, never --all: what other branches carry is not this checkout's history" 206 │ */ 207 │ export function listCommits(root: string, exec: GitExec = gitExec): RawCommit[] { 208 │ const out = tryExec(root, ['log', LOG_FORMAT, 'HEAD', '--'], exec);
L204 flow GitRepo → #blame
The reachable history, for commit counts and the HEAD author date
199 │ * directory is an empty history, not an error. 200 │ * 201 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "listCommits() runs git log over every commit reachable from HEAD and parseLogRecords() parses each trailer block; history size, not model size, sets the cost" 202 │ * @mitigates #blame against #dos using #resource-limits -- "A single log call under the same 30 s timeout and 64 MiB output cap; nothing is spawned per commit, and compute.ts lets a caller skip the walk with history: false" 203 │ * @handles pii on #blame -- "Author names, emails and co-author trailers of every commit in the history" 204 │ * @flows GitRepo -> #blame via listCommits -- "The reachable history, for commit counts and the HEAD author date" 205 │ * @comment -- "Reachable from HEAD only, never --all: what other branches carry is not this checkout's history" 206 │ */ 207 │ export function listCommits(root: string, exec: GitExec = gitExec): RawCommit[] { 208 │ const out = tryExec(root, ['log', LOG_FORMAT, 'HEAD', '--'], exec); 209 │ return out === null ? [] : parseLogRecords(out);
L205 comment Reachable from HEAD only, never --all: what other branches carry is not this checkout's history
Reachable from HEAD only, never --all: what other branches carry is not this checkout's history
200 │ * 201 │ * @exposes #blame to #dos [low] cwe:CWE-400 -- "listCommits() runs git log over every commit reachable from HEAD and parseLogRecords() parses each trailer block; history size, not model size, sets the cost" 202 │ * @mitigates #blame against #dos using #resource-limits -- "A single log call under the same 30 s timeout and 64 MiB output cap; nothing is spawned per commit, and compute.ts lets a caller skip the walk with history: false" 203 │ * @handles pii on #blame -- "Author names, emails and co-author trailers of every commit in the history" 204 │ * @flows GitRepo -> #blame via listCommits -- "The reachable history, for commit counts and the HEAD author date" 205 │ * @comment -- "Reachable from HEAD only, never --all: what other branches carry is not this checkout's history" 206 │ */ 207 │ export function listCommits(root: string, exec: GitExec = gitExec): RawCommit[] { 208 │ const out = tryExec(root, ['log', LOG_FORMAT, 'HEAD', '--'], exec); 209 │ return out === null ? [] : parseLogRecords(out); 210 │ }
src/blame/index.ts comment 1 open 1
L9 comment Public surface of src/blame: computeBlame (non-mutating), attachBlame (CLI --blame), the payload builders, the config reader and listCommits, the one history walk behind the commit counts
Public surface of src/blame: computeBlame (non-mutating), attachBlame (CLI --blame), the payload builders, the config reader and listCommits, the one history walk behind the commit counts
4 │ * Who introduced the code beneath an annotation, who declared it, who declared 5 │ * the fix, and which AI tool a commit credited — computed from git at run 6 │ * time, never written into source. See `docs/GUARDLINK_REFERENCE.md`, 7 │ * "Attribution". 8 │ * 9 │ * @comment -- "Public surface of src/blame: computeBlame (non-mutating), attachBlame (CLI --blame), the payload builders, the config reader and listCommits, the one history walk behind the commit counts" 10 │ */ 11 │ export * from './types.js'; 12 │ export { listCommits } from './git.js'; 13 │ export { readBlameConfig, compileRules, DEFAULT_TOOL_RULES, DEFAULT_IGNORE_REVS } from './config.js'; 14 │ export { attributeCommit, parseTrailerBlock, parseAssistedBy, splitPerson, identityFor, classifyPerson } from './trailers.js';
src/blame/summary.ts flow 2comment 2handles 1 open 5
L23 handles #blame: pii
Identity strings aggregated per person
18 │ * the hot files — are the same fold over the same entries plus the commit 19 │ * counts `compute.ts` read from history. "Now" is never the wall clock: every 20 │ * age is measured against `as_of`, the HEAD commit's author date, so two runs 21 │ * on the same HEAD print the same bytes. 22 │ * 23 │ * @handles pii on #blame -- "Identity strings aggregated per person" 24 │ * @flows #blame -> #cli via buildBlamePayload -- "guardlink.blame/v1 payload for --json, the MCP tool and the TUI" 25 │ * @flows #blame -> #dashboard via summarise -- "Per-person and per-tool rows, the quarterly trend, the cohort comparison and the hot files the Attribution page renders" 26 │ * @comment -- "Every list is sorted (rows by introduced desc, then identity; hot files by open desc; quarters contiguous and ascending) so two runs on the same HEAD print the same bytes — the dashboard and report determinism tests depend on it" 27 │ * @comment -- "Pure over BlameEntry[] and CommitCounts: no I/O, no clock. The trend axis is bounded by the date range of the entries, everything else by their count" 28 │ */
L24 flow #blame → #cli
guardlink.blame/v1 payload for --json, the MCP tool and the TUI
19 │ * counts `compute.ts` read from history. "Now" is never the wall clock: every 20 │ * age is measured against `as_of`, the HEAD commit's author date, so two runs 21 │ * on the same HEAD print the same bytes. 22 │ * 23 │ * @handles pii on #blame -- "Identity strings aggregated per person" 24 │ * @flows #blame -> #cli via buildBlamePayload -- "guardlink.blame/v1 payload for --json, the MCP tool and the TUI" 25 │ * @flows #blame -> #dashboard via summarise -- "Per-person and per-tool rows, the quarterly trend, the cohort comparison and the hot files the Attribution page renders" 26 │ * @comment -- "Every list is sorted (rows by introduced desc, then identity; hot files by open desc; quarters contiguous and ascending) so two runs on the same HEAD print the same bytes — the dashboard and report determinism tests depend on it" 27 │ * @comment -- "Pure over BlameEntry[] and CommitCounts: no I/O, no clock. The trend axis is bounded by the date range of the entries, everything else by their count" 28 │ */ 29 │ import { relationRecords } from '../parser/claim-key.js';
L25 flow #blame → #dashboard
Per-person and per-tool rows, the quarterly trend, the cohort comparison and the hot files the Attribution page renders
20 │ * age is measured against `as_of`, the HEAD commit's author date, so two runs 21 │ * on the same HEAD print the same bytes. 22 │ * 23 │ * @handles pii on #blame -- "Identity strings aggregated per person" 24 │ * @flows #blame -> #cli via buildBlamePayload -- "guardlink.blame/v1 payload for --json, the MCP tool and the TUI" 25 │ * @flows #blame -> #dashboard via summarise -- "Per-person and per-tool rows, the quarterly trend, the cohort comparison and the hot files the Attribution page renders" 26 │ * @comment -- "Every list is sorted (rows by introduced desc, then identity; hot files by open desc; quarters contiguous and ascending) so two runs on the same HEAD print the same bytes — the dashboard and report determinism tests depend on it" 27 │ * @comment -- "Pure over BlameEntry[] and CommitCounts: no I/O, no clock. The trend axis is bounded by the date range of the entries, everything else by their count" 28 │ */ 29 │ import { relationRecords } from '../parser/claim-key.js'; 30 │ import type { ThreatModel } from '../types/index.js';
L26 comment Every list is sorted (rows by introduced desc, then identity; hot files by open desc; quarters contiguous and ascending) so two runs on the same HEAD print the same bytes — the dashboard and report determinism tests depend on it
Every list is sorted (rows by introduced desc, then identity; hot files by open desc; quarters contiguous and ascending) so two runs on the same HEAD print the same bytes — the dashboard and report determinism tests depend on it
21 │ * on the same HEAD print the same bytes. 22 │ * 23 │ * @handles pii on #blame -- "Identity strings aggregated per person" 24 │ * @flows #blame -> #cli via buildBlamePayload -- "guardlink.blame/v1 payload for --json, the MCP tool and the TUI" 25 │ * @flows #blame -> #dashboard via summarise -- "Per-person and per-tool rows, the quarterly trend, the cohort comparison and the hot files the Attribution page renders" 26 │ * @comment -- "Every list is sorted (rows by introduced desc, then identity; hot files by open desc; quarters contiguous and ascending) so two runs on the same HEAD print the same bytes — the dashboard and report determinism tests depend on it" 27 │ * @comment -- "Pure over BlameEntry[] and CommitCounts: no I/O, no clock. The trend axis is bounded by the date range of the entries, everything else by their count" 28 │ */ 29 │ import { relationRecords } from '../parser/claim-key.js'; 30 │ import type { ThreatModel } from '../types/index.js'; 31 │ import {
L27 comment Pure over BlameEntry[] and CommitCounts: no I/O, no clock. The trend axis is bounded by the date range of the entries, everything else by their count
Pure over BlameEntry[] and CommitCounts: no I/O, no clock. The trend axis is bounded by the date range of the entries, everything else by their count
22 │ * 23 │ * @handles pii on #blame -- "Identity strings aggregated per person" 24 │ * @flows #blame -> #cli via buildBlamePayload -- "guardlink.blame/v1 payload for --json, the MCP tool and the TUI" 25 │ * @flows #blame -> #dashboard via summarise -- "Per-person and per-tool rows, the quarterly trend, the cohort comparison and the hot files the Attribution page renders" 26 │ * @comment -- "Every list is sorted (rows by introduced desc, then identity; hot files by open desc; quarters contiguous and ascending) so two runs on the same HEAD print the same bytes — the dashboard and report determinism tests depend on it" 27 │ * @comment -- "Pure over BlameEntry[] and CommitCounts: no I/O, no clock. The trend axis is bounded by the date range of the entries, everything else by their count" 28 │ */ 29 │ import { relationRecords } from '../parser/claim-key.js'; 30 │ import type { ThreatModel } from '../types/index.js'; 31 │ import { 32 │ BLAME_SCHEMA,
src/blame/trailers.ts exposes 2mitigates 2handles 1flow 1 open 7
L19 exposes #blame → #data-exposure
identityFor() turns author.name and author.email from git history into identity strings that buildBlamePayload() writes into reports, dashboards and MCP responses
14 │ * Bot-as-author (GitHub Copilot's coding agent) is handled by swapping: the 15 │ * first human co-author becomes the author and the bot lands in 16 │ * `assisted_by`. With no human co-author the author is `agent:<tool>` — the 17 │ * honest answer; nothing here invents a person. 18 │ * 19 │ * @exposes #blame to #data-exposure [medium] cwe:CWE-200 -- "identityFor() turns author.name and author.email from git history into identity strings that buildBlamePayload() writes into reports, dashboards and MCP responses" 20 │ * @mitigates #blame against #data-exposure using #identity-redaction -- "identityFor renders the configured mode: name by default, and hash replaces the email with 12 hex of its sha256 so a shared artifact carries no address" 21 │ * @exposes #blame to #redos [low] cwe:CWE-1333 -- "attributeCommit() runs every compiled ToolRule.match against each author and Co-Authored-By trailer in the history" 22 │ * @mitigates #blame against #redos using #regex-anchoring -- "Rules arrive compiled, anchored and length-capped from config.ts; the fixed regexes here are linear in the input" 23 │ * @handles pii on #blame -- "Git author names and email addresses" 24 │ * @flows CommitTrailers -> #blame via parseTrailerBlock -- "Co-authored-by and Assisted-by values"
L20 mitigates #identity-redaction mitigates #data-exposure
identityFor renders the configured mode: name by default, and hash replaces the email with 12 hex of its sha256 so a shared artifact carries no address
15 │ * first human co-author becomes the author and the bot lands in 16 │ * `assisted_by`. With no human co-author the author is `agent:<tool>` — the 17 │ * honest answer; nothing here invents a person. 18 │ * 19 │ * @exposes #blame to #data-exposure [medium] cwe:CWE-200 -- "identityFor() turns author.name and author.email from git history into identity strings that buildBlamePayload() writes into reports, dashboards and MCP responses" 20 │ * @mitigates #blame against #data-exposure using #identity-redaction -- "identityFor renders the configured mode: name by default, and hash replaces the email with 12 hex of its sha256 so a shared artifact carries no address" 21 │ * @exposes #blame to #redos [low] cwe:CWE-1333 -- "attributeCommit() runs every compiled ToolRule.match against each author and Co-Authored-By trailer in the history" 22 │ * @mitigates #blame against #redos using #regex-anchoring -- "Rules arrive compiled, anchored and length-capped from config.ts; the fixed regexes here are linear in the input" 23 │ * @handles pii on #blame -- "Git author names and email addresses" 24 │ * @flows CommitTrailers -> #blame via parseTrailerBlock -- "Co-authored-by and Assisted-by values" 25 │ * @comment -- "Declared, not detected: a commit is credited to an AI only through a matching author identity or trailer. Nothing here inspects code or guesses"
L21 exposes #blame → #redos
attributeCommit() runs every compiled ToolRule.match against each author and Co-Authored-By trailer in the history
16 │ * `assisted_by`. With no human co-author the author is `agent:<tool>` — the 17 │ * honest answer; nothing here invents a person. 18 │ * 19 │ * @exposes #blame to #data-exposure [medium] cwe:CWE-200 -- "identityFor() turns author.name and author.email from git history into identity strings that buildBlamePayload() writes into reports, dashboards and MCP responses" 20 │ * @mitigates #blame against #data-exposure using #identity-redaction -- "identityFor renders the configured mode: name by default, and hash replaces the email with 12 hex of its sha256 so a shared artifact carries no address" 21 │ * @exposes #blame to #redos [low] cwe:CWE-1333 -- "attributeCommit() runs every compiled ToolRule.match against each author and Co-Authored-By trailer in the history" 22 │ * @mitigates #blame against #redos using #regex-anchoring -- "Rules arrive compiled, anchored and length-capped from config.ts; the fixed regexes here are linear in the input" 23 │ * @handles pii on #blame -- "Git author names and email addresses" 24 │ * @flows CommitTrailers -> #blame via parseTrailerBlock -- "Co-authored-by and Assisted-by values" 25 │ * @comment -- "Declared, not detected: a commit is credited to an AI only through a matching author identity or trailer. Nothing here inspects code or guesses" 26 │ */
L22 mitigates #regex-anchoring mitigates #redos
Rules arrive compiled, anchored and length-capped from config.ts; the fixed regexes here are linear in the input
17 │ * honest answer; nothing here invents a person. 18 │ * 19 │ * @exposes #blame to #data-exposure [medium] cwe:CWE-200 -- "identityFor() turns author.name and author.email from git history into identity strings that buildBlamePayload() writes into reports, dashboards and MCP responses" 20 │ * @mitigates #blame against #data-exposure using #identity-redaction -- "identityFor renders the configured mode: name by default, and hash replaces the email with 12 hex of its sha256 so a shared artifact carries no address" 21 │ * @exposes #blame to #redos [low] cwe:CWE-1333 -- "attributeCommit() runs every compiled ToolRule.match against each author and Co-Authored-By trailer in the history" 22 │ * @mitigates #blame against #redos using #regex-anchoring -- "Rules arrive compiled, anchored and length-capped from config.ts; the fixed regexes here are linear in the input" 23 │ * @handles pii on #blame -- "Git author names and email addresses" 24 │ * @flows CommitTrailers -> #blame via parseTrailerBlock -- "Co-authored-by and Assisted-by values" 25 │ * @comment -- "Declared, not detected: a commit is credited to an AI only through a matching author identity or trailer. Nothing here inspects code or guesses" 26 │ */ 27 │ import { createHash } from 'node:crypto';
L23 handles #blame: pii
Git author names and email addresses
18 │ * 19 │ * @exposes #blame to #data-exposure [medium] cwe:CWE-200 -- "identityFor() turns author.name and author.email from git history into identity strings that buildBlamePayload() writes into reports, dashboards and MCP responses" 20 │ * @mitigates #blame against #data-exposure using #identity-redaction -- "identityFor renders the configured mode: name by default, and hash replaces the email with 12 hex of its sha256 so a shared artifact carries no address" 21 │ * @exposes #blame to #redos [low] cwe:CWE-1333 -- "attributeCommit() runs every compiled ToolRule.match against each author and Co-Authored-By trailer in the history" 22 │ * @mitigates #blame against #redos using #regex-anchoring -- "Rules arrive compiled, anchored and length-capped from config.ts; the fixed regexes here are linear in the input" 23 │ * @handles pii on #blame -- "Git author names and email addresses" 24 │ * @flows CommitTrailers -> #blame via parseTrailerBlock -- "Co-authored-by and Assisted-by values" 25 │ * @comment -- "Declared, not detected: a commit is credited to an AI only through a matching author identity or trailer. Nothing here inspects code or guesses" 26 │ */ 27 │ import { createHash } from 'node:crypto'; 28 │ import type { AiAttribution, CommitRef, CompiledRule, IdentityMode, RawCommit } from './types.js';
L24 flow CommitTrailers → #blame
Co-authored-by and Assisted-by values
19 │ * @exposes #blame to #data-exposure [medium] cwe:CWE-200 -- "identityFor() turns author.name and author.email from git history into identity strings that buildBlamePayload() writes into reports, dashboards and MCP responses" 20 │ * @mitigates #blame against #data-exposure using #identity-redaction -- "identityFor renders the configured mode: name by default, and hash replaces the email with 12 hex of its sha256 so a shared artifact carries no address" 21 │ * @exposes #blame to #redos [low] cwe:CWE-1333 -- "attributeCommit() runs every compiled ToolRule.match against each author and Co-Authored-By trailer in the history" 22 │ * @mitigates #blame against #redos using #regex-anchoring -- "Rules arrive compiled, anchored and length-capped from config.ts; the fixed regexes here are linear in the input" 23 │ * @handles pii on #blame -- "Git author names and email addresses" 24 │ * @flows CommitTrailers -> #blame via parseTrailerBlock -- "Co-authored-by and Assisted-by values" 25 │ * @comment -- "Declared, not detected: a commit is credited to an AI only through a matching author identity or trailer. Nothing here inspects code or guesses" 26 │ */ 27 │ import { createHash } from 'node:crypto'; 28 │ import type { AiAttribution, CommitRef, CompiledRule, IdentityMode, RawCommit } from './types.js'; 29 │
L25 comment Declared, not detected: a commit is credited to an AI only through a matching author identity or trailer. Nothing here inspects code or guesses
Declared, not detected: a commit is credited to an AI only through a matching author identity or trailer. Nothing here inspects code or guesses
20 │ * @mitigates #blame against #data-exposure using #identity-redaction -- "identityFor renders the configured mode: name by default, and hash replaces the email with 12 hex of its sha256 so a shared artifact carries no address" 21 │ * @exposes #blame to #redos [low] cwe:CWE-1333 -- "attributeCommit() runs every compiled ToolRule.match against each author and Co-Authored-By trailer in the history" 22 │ * @mitigates #blame against #redos using #regex-anchoring -- "Rules arrive compiled, anchored and length-capped from config.ts; the fixed regexes here are linear in the input" 23 │ * @handles pii on #blame -- "Git author names and email addresses" 24 │ * @flows CommitTrailers -> #blame via parseTrailerBlock -- "Co-authored-by and Assisted-by values" 25 │ * @comment -- "Declared, not detected: a commit is credited to an AI only through a matching author identity or trailer. Nothing here inspects code or guesses" 26 │ */ 27 │ import { createHash } from 'node:crypto'; 28 │ import type { AiAttribution, CommitRef, CompiledRule, IdentityMode, RawCommit } from './types.js'; 29 │ 30 │ export interface RawPerson {
src/blame/types.ts comment 1 open 1
L14 comment Pure type declarations for the blame module; no I/O. The optional `blame` field these describe is invisible to the annotation hash and stripped from the committed model.json, exactly like `anchor`
Pure type declarations for the blame module; no I/O. The optional `blame` field these describe is invisible to the annotation hash and stripped from the committed model.json, exactly like `anchor`
9 │ * - `CommitRef.author` is an identity string in the ledger's scheme — 10 │ * `human:<name>` or `agent:<tool>` — so a gate can key on the prefix. 11 │ * - `assisted_by[]` is structured (`tool`, `model`) because "which model" 12 │ * is the question the dashboard groups by. 13 │ * 14 │ * @comment -- "Pure type declarations for the blame module; no I/O. The optional `blame` field these describe is invisible to the annotation hash and stripped from the committed model.json, exactly like `anchor`" 15 │ */ 16 │ 17 │ export const BLAME_SCHEMA = 'guardlink.blame/v1'; 18 │ 19 │ /** What an identity string shows for a human: git name, email, or a 12-hex hash of the email. */
src/ci/index.ts comment 5flow 4 open 9
L92 flow ThreatModel → #cli
Parsed model checked for uncovered exposures, confirmed exploits and unqualified acceptances
87 │ * 88 │ * Read-only. `applyReanchor` is deliberately not called from here: rewriting an 89 │ * anchor inside a CI run would move an annotation onto code nobody chose for 90 │ * it, on a machine where nobody is watching. 91 │ * 92 │ * @flows ThreatModel -> #cli via runCiChecks -- "Parsed model checked for uncovered exposures, confirmed exploits and unqualified acceptances" 93 │ * @flows ParseDiagnostics -> #cli via runCiChecks -- "Diagnostics from the parse are reported, never recomputed" 94 │ * @flows SourceFiles -> #cli via findAnchorDrift -- "Recorded anchors compared against current source" 95 │ * @flows LedgerFile -> #cli via readLedger -- "Recorded claim hashes, read only" 96 │ * @comment -- "Exit code is a pure function of (strict, exposures, confirmed, drift, demotable stale, parse errors, unqualified acceptances) and lives in the summary, so JSON consumers see the same verdict the shell got" 97 │ * @comment -- "Exposures and drift are serialized as the types the parser already produces — no renamed fields, so guardlink.ci/v1 cannot drift from the model it reports"
L93 flow ParseDiagnostics → #cli
Diagnostics from the parse are reported, never recomputed
88 │ * Read-only. `applyReanchor` is deliberately not called from here: rewriting an 89 │ * anchor inside a CI run would move an annotation onto code nobody chose for 90 │ * it, on a machine where nobody is watching. 91 │ * 92 │ * @flows ThreatModel -> #cli via runCiChecks -- "Parsed model checked for uncovered exposures, confirmed exploits and unqualified acceptances" 93 │ * @flows ParseDiagnostics -> #cli via runCiChecks -- "Diagnostics from the parse are reported, never recomputed" 94 │ * @flows SourceFiles -> #cli via findAnchorDrift -- "Recorded anchors compared against current source" 95 │ * @flows LedgerFile -> #cli via readLedger -- "Recorded claim hashes, read only" 96 │ * @comment -- "Exit code is a pure function of (strict, exposures, confirmed, drift, demotable stale, parse errors, unqualified acceptances) and lives in the summary, so JSON consumers see the same verdict the shell got" 97 │ * @comment -- "Exposures and drift are serialized as the types the parser already produces — no renamed fields, so guardlink.ci/v1 cannot drift from the model it reports" 98 │ * @comment -- "The third check reads .guardlink/verified.json and never writes it; a corrupt ledger is reported once and treated as absent"
L94 flow SourceFiles → #cli
Recorded anchors compared against current source
89 │ * anchor inside a CI run would move an annotation onto code nobody chose for 90 │ * it, on a machine where nobody is watching. 91 │ * 92 │ * @flows ThreatModel -> #cli via runCiChecks -- "Parsed model checked for uncovered exposures, confirmed exploits and unqualified acceptances" 93 │ * @flows ParseDiagnostics -> #cli via runCiChecks -- "Diagnostics from the parse are reported, never recomputed" 94 │ * @flows SourceFiles -> #cli via findAnchorDrift -- "Recorded anchors compared against current source" 95 │ * @flows LedgerFile -> #cli via readLedger -- "Recorded claim hashes, read only" 96 │ * @comment -- "Exit code is a pure function of (strict, exposures, confirmed, drift, demotable stale, parse errors, unqualified acceptances) and lives in the summary, so JSON consumers see the same verdict the shell got" 97 │ * @comment -- "Exposures and drift are serialized as the types the parser already produces — no renamed fields, so guardlink.ci/v1 cannot drift from the model it reports" 98 │ * @comment -- "The third check reads .guardlink/verified.json and never writes it; a corrupt ledger is reported once and treated as absent" 99 │ * @comment -- "@confirmed is not filtered by acceptance state at all: @accepts does not silence a reproduced exploit anywhere else in the product and must not do so here"
L95 flow LedgerFile → #cli
Recorded claim hashes, read only
90 │ * it, on a machine where nobody is watching. 91 │ * 92 │ * @flows ThreatModel -> #cli via runCiChecks -- "Parsed model checked for uncovered exposures, confirmed exploits and unqualified acceptances" 93 │ * @flows ParseDiagnostics -> #cli via runCiChecks -- "Diagnostics from the parse are reported, never recomputed" 94 │ * @flows SourceFiles -> #cli via findAnchorDrift -- "Recorded anchors compared against current source" 95 │ * @flows LedgerFile -> #cli via readLedger -- "Recorded claim hashes, read only" 96 │ * @comment -- "Exit code is a pure function of (strict, exposures, confirmed, drift, demotable stale, parse errors, unqualified acceptances) and lives in the summary, so JSON consumers see the same verdict the shell got" 97 │ * @comment -- "Exposures and drift are serialized as the types the parser already produces — no renamed fields, so guardlink.ci/v1 cannot drift from the model it reports" 98 │ * @comment -- "The third check reads .guardlink/verified.json and never writes it; a corrupt ledger is reported once and treated as absent" 99 │ * @comment -- "@confirmed is not filtered by acceptance state at all: @accepts does not silence a reproduced exploit anywhere else in the product and must not do so here" 100 │ * @comment -- "--severity narrows risk findings only; drift, parse errors, stale claims and unqualified acceptances carry no severity and a severity threshold says nothing about them"
L96 comment Exit code is a pure function of (strict, exposures, confirmed, drift, demotable stale, parse errors, unqualified acceptances) and lives in the summary, so JSON consumers see the same verdict the shell got
Exit code is a pure function of (strict, exposures, confirmed, drift, demotable stale, parse errors, unqualified acceptances) and lives in the summary, so JSON consumers see the same verdict the shell got
91 │ * 92 │ * @flows ThreatModel -> #cli via runCiChecks -- "Parsed model checked for uncovered exposures, confirmed exploits and unqualified acceptances" 93 │ * @flows ParseDiagnostics -> #cli via runCiChecks -- "Diagnostics from the parse are reported, never recomputed" 94 │ * @flows SourceFiles -> #cli via findAnchorDrift -- "Recorded anchors compared against current source" 95 │ * @flows LedgerFile -> #cli via readLedger -- "Recorded claim hashes, read only" 96 │ * @comment -- "Exit code is a pure function of (strict, exposures, confirmed, drift, demotable stale, parse errors, unqualified acceptances) and lives in the summary, so JSON consumers see the same verdict the shell got" 97 │ * @comment -- "Exposures and drift are serialized as the types the parser already produces — no renamed fields, so guardlink.ci/v1 cannot drift from the model it reports" 98 │ * @comment -- "The third check reads .guardlink/verified.json and never writes it; a corrupt ledger is reported once and treated as absent" 99 │ * @comment -- "@confirmed is not filtered by acceptance state at all: @accepts does not silence a reproduced exploit anywhere else in the product and must not do so here" 100 │ * @comment -- "--severity narrows risk findings only; drift, parse errors, stale claims and unqualified acceptances carry no severity and a severity threshold says nothing about them" 101 │ */
L97 comment Exposures and drift are serialized as the types the parser already produces — no renamed fields, so guardlink.ci/v1 cannot drift from the model it reports
Exposures and drift are serialized as the types the parser already produces — no renamed fields, so guardlink.ci/v1 cannot drift from the model it reports
92 │ * @flows ThreatModel -> #cli via runCiChecks -- "Parsed model checked for uncovered exposures, confirmed exploits and unqualified acceptances" 93 │ * @flows ParseDiagnostics -> #cli via runCiChecks -- "Diagnostics from the parse are reported, never recomputed" 94 │ * @flows SourceFiles -> #cli via findAnchorDrift -- "Recorded anchors compared against current source" 95 │ * @flows LedgerFile -> #cli via readLedger -- "Recorded claim hashes, read only" 96 │ * @comment -- "Exit code is a pure function of (strict, exposures, confirmed, drift, demotable stale, parse errors, unqualified acceptances) and lives in the summary, so JSON consumers see the same verdict the shell got" 97 │ * @comment -- "Exposures and drift are serialized as the types the parser already produces — no renamed fields, so guardlink.ci/v1 cannot drift from the model it reports" 98 │ * @comment -- "The third check reads .guardlink/verified.json and never writes it; a corrupt ledger is reported once and treated as absent" 99 │ * @comment -- "@confirmed is not filtered by acceptance state at all: @accepts does not silence a reproduced exploit anywhere else in the product and must not do so here" 100 │ * @comment -- "--severity narrows risk findings only; drift, parse errors, stale claims and unqualified acceptances carry no severity and a severity threshold says nothing about them" 101 │ */ 102 │
L98 comment The third check reads .guardlink/verified.json and never writes it; a corrupt ledger is reported once and treated as absent
The third check reads .guardlink/verified.json and never writes it; a corrupt ledger is reported once and treated as absent
93 │ * @flows ParseDiagnostics -> #cli via runCiChecks -- "Diagnostics from the parse are reported, never recomputed" 94 │ * @flows SourceFiles -> #cli via findAnchorDrift -- "Recorded anchors compared against current source" 95 │ * @flows LedgerFile -> #cli via readLedger -- "Recorded claim hashes, read only" 96 │ * @comment -- "Exit code is a pure function of (strict, exposures, confirmed, drift, demotable stale, parse errors, unqualified acceptances) and lives in the summary, so JSON consumers see the same verdict the shell got" 97 │ * @comment -- "Exposures and drift are serialized as the types the parser already produces — no renamed fields, so guardlink.ci/v1 cannot drift from the model it reports" 98 │ * @comment -- "The third check reads .guardlink/verified.json and never writes it; a corrupt ledger is reported once and treated as absent" 99 │ * @comment -- "@confirmed is not filtered by acceptance state at all: @accepts does not silence a reproduced exploit anywhere else in the product and must not do so here" 100 │ * @comment -- "--severity narrows risk findings only; drift, parse errors, stale claims and unqualified acceptances carry no severity and a severity threshold says nothing about them" 101 │ */ 102 │ 103 │ import type {
L99 comment @confirmed is not filtered by acceptance state at all: @accepts does not silence a reproduced exploit anywhere else in the product and must not do so here
@confirmed is not filtered by acceptance state at all: @accepts does not silence a reproduced exploit anywhere else in the product and must not do so here
94 │ * @flows SourceFiles -> #cli via findAnchorDrift -- "Recorded anchors compared against current source" 95 │ * @flows LedgerFile -> #cli via readLedger -- "Recorded claim hashes, read only" 96 │ * @comment -- "Exit code is a pure function of (strict, exposures, confirmed, drift, demotable stale, parse errors, unqualified acceptances) and lives in the summary, so JSON consumers see the same verdict the shell got" 97 │ * @comment -- "Exposures and drift are serialized as the types the parser already produces — no renamed fields, so guardlink.ci/v1 cannot drift from the model it reports" 98 │ * @comment -- "The third check reads .guardlink/verified.json and never writes it; a corrupt ledger is reported once and treated as absent" 99 │ * @comment -- "@confirmed is not filtered by acceptance state at all: @accepts does not silence a reproduced exploit anywhere else in the product and must not do so here" 100 │ * @comment -- "--severity narrows risk findings only; drift, parse errors, stale claims and unqualified acceptances carry no severity and a severity threshold says nothing about them" 101 │ */ 102 │ 103 │ import type { 104 │ ThreatModel, ThreatModelExposure, ThreatModelConfirmed, Severity, ParseDiagnostic, DiagnosticCode,
L100 comment --severity narrows risk findings only; drift, parse errors, stale claims and unqualified acceptances carry no severity and a severity threshold says nothing about them
--severity narrows risk findings only; drift, parse errors, stale claims and unqualified acceptances carry no severity and a severity threshold says nothing about them
95 │ * @flows LedgerFile -> #cli via readLedger -- "Recorded claim hashes, read only" 96 │ * @comment -- "Exit code is a pure function of (strict, exposures, confirmed, drift, demotable stale, parse errors, unqualified acceptances) and lives in the summary, so JSON consumers see the same verdict the shell got" 97 │ * @comment -- "Exposures and drift are serialized as the types the parser already produces — no renamed fields, so guardlink.ci/v1 cannot drift from the model it reports" 98 │ * @comment -- "The third check reads .guardlink/verified.json and never writes it; a corrupt ledger is reported once and treated as absent" 99 │ * @comment -- "@confirmed is not filtered by acceptance state at all: @accepts does not silence a reproduced exploit anywhere else in the product and must not do so here" 100 │ * @comment -- "--severity narrows risk findings only; drift, parse errors, stale claims and unqualified acceptances carry no severity and a severity threshold says nothing about them" 101 │ */ 102 │ 103 │ import type { 104 │ ThreatModel, ThreatModelExposure, ThreatModelConfirmed, Severity, ParseDiagnostic, DiagnosticCode, 105 │ } from '../types/index.js';
src/cli/index.ts 7 stale flow 15exposes 5comment 5mitigates 4 open 34
L33 exposes #cli → #path-traversal
User-supplied dir argument resolved via path.resolve
28 │ * guardlink tui [dir] Interactive TUI with slash commands + AI chat 29 │ * guardlink gal Display GAL annotation language quick reference 30 │ * guardlink link-project <repos...> Link repos into a shared workspace 31 │ * guardlink merge <files...> Merge repo reports into unified dashboard 32 │ * 33 │ * @exposes #cli to #path-traversal [high] cwe:CWE-22 -- "User-supplied dir argument resolved via path.resolve" 34 │ * @mitigates #cli against #path-traversal using #path-validation -- "resolve() canonicalizes paths; cwd-relative by design" 35 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "init/report/sarif/dashboard write files to user-specified paths" 36 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root" 37 │ * @exposes #cli to #api-key-exposure [high] cwe:CWE-798 -- "API keys handled in config set/show commands" 38 │ * @mitigates #cli against #api-key-exposure using #key-redaction -- "maskKey() redacts keys in show output"
L34 mitigates #path-validation mitigates #path-traversal
resolve() canonicalizes paths; cwd-relative by design
29 │ * guardlink gal Display GAL annotation language quick reference 30 │ * guardlink link-project <repos...> Link repos into a shared workspace 31 │ * guardlink merge <files...> Merge repo reports into unified dashboard 32 │ * 33 │ * @exposes #cli to #path-traversal [high] cwe:CWE-22 -- "User-supplied dir argument resolved via path.resolve" 34 │ * @mitigates #cli against #path-traversal using #path-validation -- "resolve() canonicalizes paths; cwd-relative by design" 35 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "init/report/sarif/dashboard write files to user-specified paths" 36 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root" 37 │ * @exposes #cli to #api-key-exposure [high] cwe:CWE-798 -- "API keys handled in config set/show commands" 38 │ * @mitigates #cli against #api-key-exposure using #key-redaction -- "maskKey() redacts keys in show output" 39 │ * @exposes #cli to #cmd-injection [critical] cwe:CWE-78 -- "Agent launcher spawns child processes"
L35 exposes #cli → #arbitrary-write
init/report/sarif/dashboard write files to user-specified paths
30 │ * guardlink link-project <repos...> Link repos into a shared workspace 31 │ * guardlink merge <files...> Merge repo reports into unified dashboard 32 │ * 33 │ * @exposes #cli to #path-traversal [high] cwe:CWE-22 -- "User-supplied dir argument resolved via path.resolve" 34 │ * @mitigates #cli against #path-traversal using #path-validation -- "resolve() canonicalizes paths; cwd-relative by design" 35 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "init/report/sarif/dashboard write files to user-specified paths" 36 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root" 37 │ * @exposes #cli to #api-key-exposure [high] cwe:CWE-798 -- "API keys handled in config set/show commands" 38 │ * @mitigates #cli against #api-key-exposure using #key-redaction -- "maskKey() redacts keys in show output" 39 │ * @exposes #cli to #cmd-injection [critical] cwe:CWE-78 -- "Agent launcher spawns child processes" 40 │ * @audit #cli -- "Child process spawning delegated to agents/launcher.ts with explicit args"
L36 mitigates #path-validation mitigates #arbitrary-write
Output paths resolved relative to project root
31 │ * guardlink merge <files...> Merge repo reports into unified dashboard 32 │ * 33 │ * @exposes #cli to #path-traversal [high] cwe:CWE-22 -- "User-supplied dir argument resolved via path.resolve" 34 │ * @mitigates #cli against #path-traversal using #path-validation -- "resolve() canonicalizes paths; cwd-relative by design" 35 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "init/report/sarif/dashboard write files to user-specified paths" 36 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root" 37 │ * @exposes #cli to #api-key-exposure [high] cwe:CWE-798 -- "API keys handled in config set/show commands" 38 │ * @mitigates #cli against #api-key-exposure using #key-redaction -- "maskKey() redacts keys in show output" 39 │ * @exposes #cli to #cmd-injection [critical] cwe:CWE-78 -- "Agent launcher spawns child processes" 40 │ * @audit #cli -- "Child process spawning delegated to agents/launcher.ts with explicit args" 41 │ * @flows UserArgs -> #cli via process.argv -- "CLI argument input path"
L37 exposes #cli → #api-key-exposure
API keys handled in config set/show commands
32 │ * 33 │ * @exposes #cli to #path-traversal [high] cwe:CWE-22 -- "User-supplied dir argument resolved via path.resolve" 34 │ * @mitigates #cli against #path-traversal using #path-validation -- "resolve() canonicalizes paths; cwd-relative by design" 35 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "init/report/sarif/dashboard write files to user-specified paths" 36 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root" 37 │ * @exposes #cli to #api-key-exposure [high] cwe:CWE-798 -- "API keys handled in config set/show commands" 38 │ * @mitigates #cli against #api-key-exposure using #key-redaction -- "maskKey() redacts keys in show output" 39 │ * @exposes #cli to #cmd-injection [critical] cwe:CWE-78 -- "Agent launcher spawns child processes" 40 │ * @audit #cli -- "Child process spawning delegated to agents/launcher.ts with explicit args" 41 │ * @flows UserArgs -> #cli via process.argv -- "CLI argument input path" 42 │ * @flows #cli -> FileSystem via writeFile -- "Report/config output path"
L38 mitigates #key-redaction mitigates #api-key-exposure
maskKey() redacts keys in show output
33 │ * @exposes #cli to #path-traversal [high] cwe:CWE-22 -- "User-supplied dir argument resolved via path.resolve" 34 │ * @mitigates #cli against #path-traversal using #path-validation -- "resolve() canonicalizes paths; cwd-relative by design" 35 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "init/report/sarif/dashboard write files to user-specified paths" 36 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root" 37 │ * @exposes #cli to #api-key-exposure [high] cwe:CWE-798 -- "API keys handled in config set/show commands" 38 │ * @mitigates #cli against #api-key-exposure using #key-redaction -- "maskKey() redacts keys in show output" 39 │ * @exposes #cli to #cmd-injection [critical] cwe:CWE-78 -- "Agent launcher spawns child processes" 40 │ * @audit #cli -- "Child process spawning delegated to agents/launcher.ts with explicit args" 41 │ * @flows UserArgs -> #cli via process.argv -- "CLI argument input path" 42 │ * @flows #cli -> FileSystem via writeFile -- "Report/config output path" 43 │ * @boundary #cli and UserInput (#cli-input-boundary) -- "Trust boundary at CLI argument parsing"
L39 exposes #cli → #cmd-injection
Agent launcher spawns child processes
34 │ * @mitigates #cli against #path-traversal using #path-validation -- "resolve() canonicalizes paths; cwd-relative by design" 35 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "init/report/sarif/dashboard write files to user-specified paths" 36 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root" 37 │ * @exposes #cli to #api-key-exposure [high] cwe:CWE-798 -- "API keys handled in config set/show commands" 38 │ * @mitigates #cli against #api-key-exposure using #key-redaction -- "maskKey() redacts keys in show output" 39 │ * @exposes #cli to #cmd-injection [critical] cwe:CWE-78 -- "Agent launcher spawns child processes" 40 │ * @audit #cli -- "Child process spawning delegated to agents/launcher.ts with explicit args" 41 │ * @flows UserArgs -> #cli via process.argv -- "CLI argument input path" 42 │ * @flows #cli -> FileSystem via writeFile -- "Report/config output path" 43 │ * @boundary #cli and UserInput (#cli-input-boundary) -- "Trust boundary at CLI argument parsing" 44 │ * @handles secrets on #cli -- "Processes API keys via config commands"
L40 audit Audit: #cli
Child process spawning delegated to agents/launcher.ts with explicit args
35 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "init/report/sarif/dashboard write files to user-specified paths" 36 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root" 37 │ * @exposes #cli to #api-key-exposure [high] cwe:CWE-798 -- "API keys handled in config set/show commands" 38 │ * @mitigates #cli against #api-key-exposure using #key-redaction -- "maskKey() redacts keys in show output" 39 │ * @exposes #cli to #cmd-injection [critical] cwe:CWE-78 -- "Agent launcher spawns child processes" 40 │ * @audit #cli -- "Child process spawning delegated to agents/launcher.ts with explicit args" 41 │ * @flows UserArgs -> #cli via process.argv -- "CLI argument input path" 42 │ * @flows #cli -> FileSystem via writeFile -- "Report/config output path" 43 │ * @boundary #cli and UserInput (#cli-input-boundary) -- "Trust boundary at CLI argument parsing" 44 │ * @handles secrets on #cli -- "Processes API keys via config commands" 45 │ */
L41 flow UserArgs → #cli
CLI argument input path
36 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root" 37 │ * @exposes #cli to #api-key-exposure [high] cwe:CWE-798 -- "API keys handled in config set/show commands" 38 │ * @mitigates #cli against #api-key-exposure using #key-redaction -- "maskKey() redacts keys in show output" 39 │ * @exposes #cli to #cmd-injection [critical] cwe:CWE-78 -- "Agent launcher spawns child processes" 40 │ * @audit #cli -- "Child process spawning delegated to agents/launcher.ts with explicit args" 41 │ * @flows UserArgs -> #cli via process.argv -- "CLI argument input path" 42 │ * @flows #cli -> FileSystem via writeFile -- "Report/config output path" 43 │ * @boundary #cli and UserInput (#cli-input-boundary) -- "Trust boundary at CLI argument parsing" 44 │ * @handles secrets on #cli -- "Processes API keys via config commands" 45 │ */ 46 │
L42 flow #cli → FileSystem
Report/config output path
37 │ * @exposes #cli to #api-key-exposure [high] cwe:CWE-798 -- "API keys handled in config set/show commands" 38 │ * @mitigates #cli against #api-key-exposure using #key-redaction -- "maskKey() redacts keys in show output" 39 │ * @exposes #cli to #cmd-injection [critical] cwe:CWE-78 -- "Agent launcher spawns child processes" 40 │ * @audit #cli -- "Child process spawning delegated to agents/launcher.ts with explicit args" 41 │ * @flows UserArgs -> #cli via process.argv -- "CLI argument input path" 42 │ * @flows #cli -> FileSystem via writeFile -- "Report/config output path" 43 │ * @boundary #cli and UserInput (#cli-input-boundary) -- "Trust boundary at CLI argument parsing" 44 │ * @handles secrets on #cli -- "Processes API keys via config commands" 45 │ */ 46 │ 47 │ import { Command } from 'commander';
L43 boundary #cli ↔ UserInput
Trust boundary at CLI argument parsing
38 │ * @mitigates #cli against #api-key-exposure using #key-redaction -- "maskKey() redacts keys in show output" 39 │ * @exposes #cli to #cmd-injection [critical] cwe:CWE-78 -- "Agent launcher spawns child processes" 40 │ * @audit #cli -- "Child process spawning delegated to agents/launcher.ts with explicit args" 41 │ * @flows UserArgs -> #cli via process.argv -- "CLI argument input path" 42 │ * @flows #cli -> FileSystem via writeFile -- "Report/config output path" 43 │ * @boundary #cli and UserInput (#cli-input-boundary) -- "Trust boundary at CLI argument parsing" 44 │ * @handles secrets on #cli -- "Processes API keys via config commands" 45 │ */ 46 │ 47 │ import { Command } from 'commander'; 48 │ import { resolve, basename, join, isAbsolute, relative } from 'node:path';
L44 handles #cli: secrets
Processes API keys via config commands
39 │ * @exposes #cli to #cmd-injection [critical] cwe:CWE-78 -- "Agent launcher spawns child processes" 40 │ * @audit #cli -- "Child process spawning delegated to agents/launcher.ts with explicit args" 41 │ * @flows UserArgs -> #cli via process.argv -- "CLI argument input path" 42 │ * @flows #cli -> FileSystem via writeFile -- "Report/config output path" 43 │ * @boundary #cli and UserInput (#cli-input-boundary) -- "Trust boundary at CLI argument parsing" 44 │ * @handles secrets on #cli -- "Processes API keys via config commands" 45 │ */ 46 │ 47 │ import { Command } from 'commander'; 48 │ import { resolve, basename, join, isAbsolute, relative } from 'node:path'; 49 │ import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
L257 flow GitRepo → #cli
Opt-in attribution; without --blame the JSON is byte-identical to before
252 │ const { model, diagnostics } = await parseProject({ root, project: opts.project ?? readConfiguredProject(root) ?? undefined }); 253 │ 254 │ // Print diagnostics to stderr 255 │ printDiagnostics(diagnostics); 256 │ 257 │ // @flows GitRepo -> #cli via attachBlame -- "Opt-in attribution; without --blame the JSON is byte-identical to before" 258 │ if (opts.blame) attachBlame(root, model); 259 │ 260 │ // R10: stamp the export with the annotations it was cut from. `parse -o 261 │ // .guardlink/report.json` is how the model reaches the graph and everything 262 │ // downstream of it, and until now that file said nothing about which
L307 flow LedgerFile → #cli
status reports verified/stale/unverified claim counts alongside annotation coverage
302 │ model = filterByFeature(model, featureNames); 303 │ console.log(`Filtered to feature(s): ${featureNames.map(f => `"${f}"`).join(', ')}\n`); 304 │ } 305 │ 306 │ printDiagnostics(diagnostics); 307 │ // @flows LedgerFile -> #cli via readLedger -- "status reports verified/stale/unverified claim counts alongside annotation coverage" 308 │ // @flows LedgerFile -> #cli via readHypotheses -- "status reports how many exposures were tested, and with what outcome" 309 │ printStatus(model, classifyClaims(model, readLedger(root)), classifyHypotheses(model, readHypotheses(root))); 310 │ // @flows GitRepo -> #cli via computeBlame -- "status --blame reads attribution without touching the model" 311 │ if (opts.blame) printBlameSummary(buildBlamePayload(model, computeBlame(root, model), root)); 312 │
L308 flow LedgerFile → #cli
status reports how many exposures were tested, and with what outcome
303 │ console.log(`Filtered to feature(s): ${featureNames.map(f => `"${f}"`).join(', ')}\n`); 304 │ } 305 │ 306 │ printDiagnostics(diagnostics); 307 │ // @flows LedgerFile -> #cli via readLedger -- "status reports verified/stale/unverified claim counts alongside annotation coverage" 308 │ // @flows LedgerFile -> #cli via readHypotheses -- "status reports how many exposures were tested, and with what outcome" 309 │ printStatus(model, classifyClaims(model, readLedger(root)), classifyHypotheses(model, readHypotheses(root))); 310 │ // @flows GitRepo -> #cli via computeBlame -- "status --blame reads attribution without touching the model" 311 │ if (opts.blame) printBlameSummary(buildBlamePayload(model, computeBlame(root, model), root)); 312 │ 313 │ if (opts.notAnnotated) {
L310 flow GitRepo → #cli
status --blame reads attribution without touching the model
305 │ 306 │ printDiagnostics(diagnostics); 307 │ // @flows LedgerFile -> #cli via readLedger -- "status reports verified/stale/unverified claim counts alongside annotation coverage" 308 │ // @flows LedgerFile -> #cli via readHypotheses -- "status reports how many exposures were tested, and with what outcome" 309 │ printStatus(model, classifyClaims(model, readLedger(root)), classifyHypotheses(model, readHypotheses(root))); 310 │ // @flows GitRepo -> #cli via computeBlame -- "status --blame reads attribution without touching the model" 311 │ if (opts.blame) printBlameSummary(buildBlamePayload(model, computeBlame(root, model), root)); 312 │ 313 │ if (opts.notAnnotated) { 314 │ printUnannotatedFiles(model); 315 │ }
L377 flow LedgerFile → #cli
Read-only; a corrupt ledger becomes a validate error (ledger-corrupt) instead of being silently treated as absent
372 │ // the file still parsed and every annotation in it counted. 373 │ const galConventionDiags = findOffConventionGalFiles(model); 374 │ 375 │ // A ledger that exists but cannot be read is an error: every surface that 376 │ // reads it is silently treating it as absent until someone fixes it. 377 │ // @flows LedgerFile -> #cli via readLedger -- "Read-only; a corrupt ledger becomes a validate error (ledger-corrupt) instead of being silently treated as absent" 378 │ // @comment -- "Closes the same blind spot ci's third check already closed (src/ci/index.ts) for the validate/CI-gate surface" 379 │ const ledgerRead = readLedger(root); 380 │ const ledgerDiags = ledgerRead.diagnostic ? [ledgerRead.diagnostic] : []; 381 │ 382 │ const allDiags = [...diagnostics, ...danglingDiags, ...acceptAuditDiags, ...acceptanceDiags, ...actorDiags, ...inertDiags, ...impreciseDiags, ...provenanceDiags, ...galConventionDiags, ...ledgerDiags];
L378 comment Closes the same blind spot ci's third check already closed (src/ci/index.ts) for the validate/CI-gate surface
Closes the same blind spot ci's third check already closed (src/ci/index.ts) for the validate/CI-gate surface
373 │ const galConventionDiags = findOffConventionGalFiles(model); 374 │ 375 │ // A ledger that exists but cannot be read is an error: every surface that 376 │ // reads it is silently treating it as absent until someone fixes it. 377 │ // @flows LedgerFile -> #cli via readLedger -- "Read-only; a corrupt ledger becomes a validate error (ledger-corrupt) instead of being silently treated as absent" 378 │ // @comment -- "Closes the same blind spot ci's third check already closed (src/ci/index.ts) for the validate/CI-gate surface" 379 │ const ledgerRead = readLedger(root); 380 │ const ledgerDiags = ledgerRead.diagnostic ? [ledgerRead.diagnostic] : []; 381 │ 382 │ const allDiags = [...diagnostics, ...danglingDiags, ...acceptAuditDiags, ...acceptanceDiags, ...actorDiags, ...inertDiags, ...impreciseDiags, ...provenanceDiags, ...galConventionDiags, ...ledgerDiags]; 383 │
L503 exposes #cli → #arbitrary-write
The one command that writes .guardlink/verified.json
498 │ }); 499 │ 500 │ // ─── verify ────────────────────────────────────────────────────────── 501 │ 502 │ /** 503 │ * @exposes #cli to #arbitrary-write [low] cwe:CWE-73 -- "The one command that writes .guardlink/verified.json" 504 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Path is the constant LEDGER_FILE under a root that must already carry .guardlink/; targets only select claims, they are never written to" 505 │ * @flows UserInput -> #cli via verify -- "Targets, --by and mode flags" 506 │ * @comment -- "Re-locking a stale claim is an assertion that the control still holds, so the default form never does it: --stale, --all or a named target is required, and each says so in its output" 507 │ */ 508 │ program
L504 mitigates #path-validation mitigates #arbitrary-write
Path is the constant LEDGER_FILE under a root that must already carry .guardlink/; targets only select claims, they are never written to
499 │ 500 │ // ─── verify ────────────────────────────────────────────────────────── 501 │ 502 │ /** 503 │ * @exposes #cli to #arbitrary-write [low] cwe:CWE-73 -- "The one command that writes .guardlink/verified.json" 504 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Path is the constant LEDGER_FILE under a root that must already carry .guardlink/; targets only select claims, they are never written to" 505 │ * @flows UserInput -> #cli via verify -- "Targets, --by and mode flags" 506 │ * @comment -- "Re-locking a stale claim is an assertion that the control still holds, so the default form never does it: --stale, --all or a named target is required, and each says so in its output" 507 │ */ 508 │ program 509 │ .command('verify')
L505 flow UserInput → #cli
Targets, --by and mode flags
500 │ // ─── verify ────────────────────────────────────────────────────────── 501 │ 502 │ /** 503 │ * @exposes #cli to #arbitrary-write [low] cwe:CWE-73 -- "The one command that writes .guardlink/verified.json" 504 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Path is the constant LEDGER_FILE under a root that must already carry .guardlink/; targets only select claims, they are never written to" 505 │ * @flows UserInput -> #cli via verify -- "Targets, --by and mode flags" 506 │ * @comment -- "Re-locking a stale claim is an assertion that the control still holds, so the default form never does it: --stale, --all or a named target is required, and each says so in its output" 507 │ */ 508 │ program 509 │ .command('verify') 510 │ .description('Record that the code beneath each claim was checked — writes .guardlink/verified.json and nothing else')
L506 comment Re-locking a stale claim is an assertion that the control still holds, so the default form never does it: --stale, --all or a named target is required, and each says so in its output
Re-locking a stale claim is an assertion that the control still holds, so the default form never does it: --stale, --all or a named target is required, and each says so in its output
501 │ 502 │ /** 503 │ * @exposes #cli to #arbitrary-write [low] cwe:CWE-73 -- "The one command that writes .guardlink/verified.json" 504 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Path is the constant LEDGER_FILE under a root that must already carry .guardlink/; targets only select claims, they are never written to" 505 │ * @flows UserInput -> #cli via verify -- "Targets, --by and mode flags" 506 │ * @comment -- "Re-locking a stale claim is an assertion that the control still holds, so the default form never does it: --stale, --all or a named target is required, and each says so in its output" 507 │ */ 508 │ program 509 │ .command('verify') 510 │ .description('Record that the code beneath each claim was checked — writes .guardlink/verified.json and nothing else') 511 │ .argument('[dir]', 'Project directory (default .). A file or file:line here is taken as a target.', '.')
L622 flow GitRepo → #cli
Attribution read from blame, log -L and commit trailers
617 │ /** 618 │ * Who introduced the code beneath each claim, who declared the claim, who 619 │ * declared its fix, and which AI tool each of those commits credited — read 620 │ * from git history at run time. Nothing is written, not even a trailer. 621 │ * 622 │ * @flows GitRepo -> #cli via computeBlame -- "Attribution read from blame, log -L and commit trailers" 623 │ * @handles pii on #cli -- "Author identities printed or emitted as JSON; --identity hash (or blame.identity in config.json) redacts emails" 624 │ * @comment -- "Always exits 0 once the model parsed: outside a git checkout every claim reads as unattributed, which is an answer, not a failure. Only an unknown --identity is an error" 625 │ */ 626 │ program 627 │ .command('blame')
L623 handles #cli: pii
Author identities printed or emitted as JSON; --identity hash (or blame.identity in config.json) redacts emails
618 │ * Who introduced the code beneath each claim, who declared the claim, who 619 │ * declared its fix, and which AI tool each of those commits credited — read 620 │ * from git history at run time. Nothing is written, not even a trailer. 621 │ * 622 │ * @flows GitRepo -> #cli via computeBlame -- "Attribution read from blame, log -L and commit trailers" 623 │ * @handles pii on #cli -- "Author identities printed or emitted as JSON; --identity hash (or blame.identity in config.json) redacts emails" 624 │ * @comment -- "Always exits 0 once the model parsed: outside a git checkout every claim reads as unattributed, which is an answer, not a failure. Only an unknown --identity is an error" 625 │ */ 626 │ program 627 │ .command('blame') 628 │ .description('Who introduced, declared and fixed each claim, and which AI tool co-authored it — read from git, nothing written')
L624 comment Always exits 0 once the model parsed: outside a git checkout every claim reads as unattributed, which is an answer, not a failure. Only an unknown --identity is an error
Always exits 0 once the model parsed: outside a git checkout every claim reads as unattributed, which is an answer, not a failure. Only an unknown --identity is an error
619 │ * declared its fix, and which AI tool each of those commits credited — read 620 │ * from git history at run time. Nothing is written, not even a trailer. 621 │ * 622 │ * @flows GitRepo -> #cli via computeBlame -- "Attribution read from blame, log -L and commit trailers" 623 │ * @handles pii on #cli -- "Author identities printed or emitted as JSON; --identity hash (or blame.identity in config.json) redacts emails" 624 │ * @comment -- "Always exits 0 once the model parsed: outside a git checkout every claim reads as unattributed, which is an answer, not a failure. Only an unknown --identity is an error" 625 │ */ 626 │ program 627 │ .command('blame') 628 │ .description('Who introduced, declared and fixed each claim, and which AI tool co-authored it — read from git, nothing written') 629 │ .argument('[dir]', 'Project directory to scan', '.')
L757 flow GitRepo → #cli
report --blame attaches attribution before the metadata spread, so the shared records carry it
752 │ // Show errors if any. Per-annotation errors don't block the report — 753 │ // affected annotations are skipped, the rest of the model still renders. 754 │ const errors = diagnostics.filter(d => d.level === 'error'); 755 │ if (errors.length > 0) printDiagnostics(errors); 756 │ 757 │ // @flows GitRepo -> #cli via attachBlame -- "report --blame attaches attribution before the metadata spread, so the shared records carry it" 758 │ if (opts.blame) attachBlame(root, model); 759 │ 760 │ // Enrich with provenance metadata (git SHA, branch, workspace, schema version) 761 │ const enrichedModel = populateMetadata(model, root); 762 │
L1513 flow #cli → #gate
The model before and after the agent
1508 │ if (opts.gate === false) { console.log(' Gate skipped (--no-gate). Run: guardlink lint . --since HEAD'); return; }1509 │ 1510 │ // The gate: what did the run add, and does it meet the evidence bar?1511 │ // Re-prompt with the violations, then strip what still fails so a bad1512 │ // claim never lands in the tree as if it had been checked.1513 │ // @flows #cli -> #gate via runGate -- "The model before and after the agent"1514 │ const parseNow = async (): Promise<ThreatModel> => {1515 │ const m = (await parseProject({ root, project })).model;1516 │ attachHypotheses(m, classifyHypotheses(m, readHypotheses(root)));1517 │ return m;1518 │ };
L1648 flow ThreatModel → #cli
Which assets sit on an undefended path, for the ranking
1643 │ .option('-n, --count <n>', 'How many to list', '10')1644 │ .option('--intake', 'Print the queue as a brief for bugb intake')1645 │ .option('--json', 'Machine-readable output')1646 │ .action(async (dir: string, opts: { project?: string; count?: string; intake?: boolean; json?: boolean }) => {1647 │ const { root, project, model, c } = await hypothesisContext(dir, opts.project);1648 │ // @flows ThreatModel -> #cli via findUnmitigatedPaths -- "Which assets sit on an undefended path, for the ranking"1649 │ const pathAssets = new Set<string>();1650 │ for (const f of findUnmitigatedPaths(model)) for (const a of f.assetsOnPath) pathAssets.add(a);1651 │ const n = Math.max(1, parseInt(opts.count ?? '10', 10) || 10);1652 │ const queue = rankUntested(c.records, model, pathAssets).slice(0, opts.intake ? Number.MAX_SAFE_INTEGER : n);1653 │ if (opts.json) { console.log(JSON.stringify({ schema: 'guardlink.hypotheses-next/v1', root, queue: queue.map(r => ({ rank: r.rank, state: r.state, asset: r.asset, threat: r.threat, severity: r.severity, file: r.file, line: r.line, onPath: r.onPath, unowned: r.unowned, claim: r.claim })) }, null, 2)); return; }
L1663 flow ScanReport → #cli
cxg findings recorded as confirmations, joined to claims
1658 │ const { root, model } = await hypothesisContext(dir, opts.project);1659 │ const by = opts.by || `human:${defaultVerifier(root)}`;1660 │ const at = nowIso();1661 │ try {1662 │ if (outcome === 'confirmed' && opts.fromScan) {1663 │ // @flows ScanReport -> #cli via importScan -- "cxg findings recorded as confirmations, joined to claims"1664 │ const r = importScan(root, model, opts.fromScan, { by: opts.by || 'cxg', at });1665 │ console.log(formatImport(r));1666 │ if (opts.write) {1667 │ for (const c of r.confirmed) {1668 │ try { const w = writeConfirmedLine(root, c.record, confirmedLine(c.record, c.entry)); console.log(` wrote ${w.file}:${w.line}`); }
L2750 flow GitRepo → #cli
dashboard --blame; identities land in the page, so blame.identity=hash is the setting for a shared dashboard
2745 │ if (model.annotations_parsed === 0 && !opts.feature) {2746 │ console.error('No annotations found. Add GuardLink annotations first.');2747 │ process.exit(1);2748 │ }2749 │ 2750 │ // @flows GitRepo -> #cli via attachBlame -- "dashboard --blame; identities land in the page, so blame.identity=hash is the setting for a shared dashboard"2751 │ if (opts.blame) attachBlame(root, model);2752 │ 2753 │ // @flows GitRepo -> #cli via loadSince -- "dashboard --since <ref>: the model at the ref, diffed against this one"2754 │ let since: SinceInput | undefined;2755 │ if (opts.since) {
L2753 flow GitRepo → #cli
dashboard --since <ref>: the model at the ref, diffed against this one
2748 │ }2749 │ 2750 │ // @flows GitRepo -> #cli via attachBlame -- "dashboard --blame; identities land in the page, so blame.identity=hash is the setting for a shared dashboard"2751 │ if (opts.blame) attachBlame(root, model);2752 │ 2753 │ // @flows GitRepo -> #cli via loadSince -- "dashboard --since <ref>: the model at the ref, diffed against this one"2754 │ let since: SinceInput | undefined;2755 │ if (opts.since) {2756 │ try {2757 │ since = await loadSince(root, opts.since, project, model);2758 │ console.error(`Compared against ${opts.since}: ${since.diff.summary.newUnmitigated} new unmitigated, ${since.diff.summary.resolvedUnmitigated} resolved`);
L3426 comment Displays ledger state (absent, corrupt, or counts) computed by the caller; this function performs no I/O of its own
Displays ledger state (absent, corrupt, or counts) computed by the caller; this function performs no I/O of its own
3421 │ console.log(`${'─'.repeat(40)}`);3422 │ console.log(`Files scanned: ${model.source_files}`);3423 │ console.log(` Files annotated: ${model.annotated_files.length}`);3424 │ console.log(` Files unannotated: ${model.unannotated_files.length}`);3425 │ console.log(`Annotations: ${model.annotations_parsed}`);3426 │ // @comment -- "Displays ledger state (absent, corrupt, or counts) computed by the caller; this function performs no I/O of its own"3427 │ if (verification) {3428 │ const s = verification.summary;3429 │ if (verification.ledger === 'absent') {3430 │ console.log('Verified claims: none recorded (run guardlink verify --all)');3431 │ } else if (verification.ledger === 'corrupt') {
L3477 handles #cli: pii
Identity strings printed to the terminal
3472 │ 3473 │ /**3474 │ * The short form of `guardlink blame` for `status --blame`: the top five3475 │ * people and AI tools by exposures introduced.3476 │ *3477 │ * @handles pii on #cli -- "Identity strings printed to the terminal"3478 │ * @comment -- "Prints only what the payload already holds; no I/O here"3479 │ */3480 │ function printBlameSummary(payload: BlamePayload): void {3481 │ console.log(`${'─'.repeat(40)}`);3482 │ if (payload.status === 'no-git') {
L3478 comment Prints only what the payload already holds; no I/O here
Prints only what the payload already holds; no I/O here
3473 │ /**3474 │ * The short form of `guardlink blame` for `status --blame`: the top five3475 │ * people and AI tools by exposures introduced.3476 │ *3477 │ * @handles pii on #cli -- "Identity strings printed to the terminal"3478 │ * @comment -- "Prints only what the payload already holds; no I/O here"3479 │ */3480 │ function printBlameSummary(payload: BlamePayload): void {3481 │ console.log(`${'─'.repeat(40)}`);3482 │ if (payload.status === 'no-git') {3483 │ console.log('Attribution: not a git checkout — nothing to attribute');
src/dashboard/analytics.ts handles 1comment 1 open 2
L9 handles #dashboard: pii
Introducer identities per asset and per period, in the configured identity mode
4 │ * and the attribution grids (people × time, AI tool × severity). 5 │ * 6 │ * All pure over the unified claim rows and the model; nothing here reads a 7 │ * file or the clock. 8 │ * 9 │ * @handles pii on #dashboard -- "Introducer identities per asset and per period, in the configured identity mode" 10 │ * @comment -- "Asset details match a tile by any of its aliases (#id, dotted path, as written), case-insensitively; mitigations never count as exposures, they only change a pair's status" 11 │ */ 12 │ import type { ThreatModel } from '../types/index.js'; 13 │ import type { ClaimState } from '../parser/verification.js'; 14 │ import type { ThreatModelDiff } from '../diff/engine.js';
L10 comment Asset details match a tile by any of its aliases (#id, dotted path, as written), case-insensitively; mitigations never count as exposures, they only change a pair's status
Asset details match a tile by any of its aliases (#id, dotted path, as written), case-insensitively; mitigations never count as exposures, they only change a pair's status
5 │ * 6 │ * All pure over the unified claim rows and the model; nothing here reads a 7 │ * file or the clock. 8 │ * 9 │ * @handles pii on #dashboard -- "Introducer identities per asset and per period, in the configured identity mode" 10 │ * @comment -- "Asset details match a tile by any of its aliases (#id, dotted path, as written), case-insensitively; mitigations never count as exposures, they only change a pair's status" 11 │ */ 12 │ import type { ThreatModel } from '../types/index.js'; 13 │ import type { ClaimState } from '../parser/verification.js'; 14 │ import type { ThreatModelDiff } from '../diff/engine.js'; 15 │ import type { AssetHeatmapEntry } from './data.js';
src/dashboard/annotations.ts exposes 1mitigates 1flow 1comment 1 open 4
L5 exposes #dashboard → #path-traversal
readFileSync reads the file each annotation's location names, for the code context
1 │ /** 2 │ * GuardLink Dashboard — the per-file annotation list behind the Code page 3 │ * and its drawer, with a few lines of source context around each annotation. 4 │ * 5 │ * @exposes #dashboard to #path-traversal [medium] cwe:CWE-22 -- "readFileSync reads the file each annotation's location names, for the code context" 6 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "Relative locations are resolved against root; an absolute location is read as given because it came from the parser, not from a caller" 7 │ * @flows SourceFiles -> #dashboard via readFileSync -- "Code snippet reads" 8 │ * @comment -- "Ported unchanged from the first generate.ts; a file that cannot be read yields an empty context, never a failed page" 9 │ */ 10 │ import { readFileSync } from 'node:fs';
L6 mitigates #path-validation mitigates #path-traversal
Relative locations are resolved against root; an absolute location is read as given because it came from the parser, not from a caller
1 │ /** 2 │ * GuardLink Dashboard — the per-file annotation list behind the Code page 3 │ * and its drawer, with a few lines of source context around each annotation. 4 │ * 5 │ * @exposes #dashboard to #path-traversal [medium] cwe:CWE-22 -- "readFileSync reads the file each annotation's location names, for the code context" 6 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "Relative locations are resolved against root; an absolute location is read as given because it came from the parser, not from a caller" 7 │ * @flows SourceFiles -> #dashboard via readFileSync -- "Code snippet reads" 8 │ * @comment -- "Ported unchanged from the first generate.ts; a file that cannot be read yields an empty context, never a failed page" 9 │ */ 10 │ import { readFileSync } from 'node:fs'; 11 │ import { isAbsolute, resolve } from 'node:path';
L7 flow SourceFiles → #dashboard
Code snippet reads
2 │ * GuardLink Dashboard — the per-file annotation list behind the Code page 3 │ * and its drawer, with a few lines of source context around each annotation. 4 │ * 5 │ * @exposes #dashboard to #path-traversal [medium] cwe:CWE-22 -- "readFileSync reads the file each annotation's location names, for the code context" 6 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "Relative locations are resolved against root; an absolute location is read as given because it came from the parser, not from a caller" 7 │ * @flows SourceFiles -> #dashboard via readFileSync -- "Code snippet reads" 8 │ * @comment -- "Ported unchanged from the first generate.ts; a file that cannot be read yields an empty context, never a failed page" 9 │ */ 10 │ import { readFileSync } from 'node:fs'; 11 │ import { isAbsolute, resolve } from 'node:path'; 12 │ import type { ThreatModel } from '../types/index.js';
L8 comment Ported unchanged from the first generate.ts; a file that cannot be read yields an empty context, never a failed page
Ported unchanged from the first generate.ts; a file that cannot be read yields an empty context, never a failed page
3 │ * and its drawer, with a few lines of source context around each annotation. 4 │ * 5 │ * @exposes #dashboard to #path-traversal [medium] cwe:CWE-22 -- "readFileSync reads the file each annotation's location names, for the code context" 6 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "Relative locations are resolved against root; an absolute location is read as given because it came from the parser, not from a caller" 7 │ * @flows SourceFiles -> #dashboard via readFileSync -- "Code snippet reads" 8 │ * @comment -- "Ported unchanged from the first generate.ts; a file that cannot be read yields an empty context, never a failed page" 9 │ */ 10 │ import { readFileSync } from 'node:fs'; 11 │ import { isAbsolute, resolve } from 'node:path'; 12 │ import type { ThreatModel } from '../types/index.js'; 13 │ import type { ExposureRow, AssetHeatmapEntry } from './data.js';
src/dashboard/client-legacy.ts comment 2 open 2
L9 comment Verbatim from the previous generate.ts; behaviour unchanged. Model data never reaches these strings at generation time — they read the embedded arrays at run time
Verbatim from the previous generate.ts; behaviour unchanged. Model data never reaches these strings at generation time — they read the embedded arrays at run time
4 │ * Two blocks the upgrade keeps verbatim: the feature filter (it rewrites the 5 │ * stat tiles, headings and risk banner by label text, so those elements keep 6 │ * their labels) and the diagram + threat-report machinery (mermaid, d3 zoom, 7 │ * marked). Both are plain strings inlined into the page's script. 8 │ * 9 │ * @comment -- "Verbatim from the previous generate.ts; behaviour unchanged. Model data never reaches these strings at generation time — they read the embedded arrays at run time" 10 │ * @comment -- "One exception to 'no generation-time data': MERMAID_LIMITS is interpolated into mermaid.initialize, so the renderer is configured with the same two numbers the render budget measured against" 11 │ */ 12 │ import { MERMAID_LIMITS } from './render-budget.js'; 13 │ 14 │ export const FEATURE_FILTER_JS = `/* ===== FEATURE FILTER ===== */
L10 comment One exception to 'no generation-time data': MERMAID_LIMITS is interpolated into mermaid.initialize, so the renderer is configured with the same two numbers the render budget measured against
One exception to 'no generation-time data': MERMAID_LIMITS is interpolated into mermaid.initialize, so the renderer is configured with the same two numbers the render budget measured against
5 │ * stat tiles, headings and risk banner by label text, so those elements keep 6 │ * their labels) and the diagram + threat-report machinery (mermaid, d3 zoom, 7 │ * marked). Both are plain strings inlined into the page's script. 8 │ * 9 │ * @comment -- "Verbatim from the previous generate.ts; behaviour unchanged. Model data never reaches these strings at generation time — they read the embedded arrays at run time" 10 │ * @comment -- "One exception to 'no generation-time data': MERMAID_LIMITS is interpolated into mermaid.initialize, so the renderer is configured with the same two numbers the render budget measured against" 11 │ */ 12 │ import { MERMAID_LIMITS } from './render-budget.js'; 13 │ 14 │ export const FEATURE_FILTER_JS = `/* ===== FEATURE FILTER ===== */ 15 │ var _activeFeature = '';
src/dashboard/client.ts mitigates 1comment 1 open 2
L15 mitigates #output-encoding mitigates #xss
Every value the drawer renders from the embedded data passes through the client esc(); URLs are pre-built server-side and attribute-escaped here
10 │ * 11 │ * Nothing from the model is interpolated into this string at generation 12 │ * time; the page's data arrives through the JSON constants emitted before it, 13 │ * and everything rendered from them goes through the client `esc()`. 14 │ * 15 │ * @mitigates #dashboard against #xss using #output-encoding -- "Every value the drawer renders from the embedded data passes through the client esc(); URLs are pre-built server-side and attribute-escaped here" 16 │ * @comment -- "No network: the clipboard API with a textarea fallback, history.replaceState for filters, localStorage only for theme and sidebar state" 17 │ */ 18 │ export const CLIENT_JS = ` 19 │ /* ===== HELPERS ===== */ 20 │ function esc(s) { return s == null ? '' : String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
L16 comment No network: the clipboard API with a textarea fallback, history.replaceState for filters, localStorage only for theme and sidebar state
No network: the clipboard API with a textarea fallback, history.replaceState for filters, localStorage only for theme and sidebar state
11 │ * Nothing from the model is interpolated into this string at generation 12 │ * time; the page's data arrives through the JSON constants emitted before it, 13 │ * and everything rendered from them goes through the client `esc()`. 14 │ * 15 │ * @mitigates #dashboard against #xss using #output-encoding -- "Every value the drawer renders from the embedded data passes through the client esc(); URLs are pre-built server-side and attribute-escaped here" 16 │ * @comment -- "No network: the clipboard API with a textarea fallback, history.replaceState for filters, localStorage only for theme and sidebar state" 17 │ */ 18 │ export const CLIENT_JS = ` 19 │ /* ===== HELPERS ===== */ 20 │ function esc(s) { return s == null ? '' : String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); } 21 │ function sec(label, value) { return '<div class="d-section"><div class="d-label">' + label + '</div><div class="d-value">' + value + '</div></div>'; }
src/dashboard/data.ts comment 4handles 1flow 1 open 6
L99 comment Stats mirror the model's own scope: on a --feature model these are the feature's numbers, and the dashboard suppresses the ones that only mean something project-wide
Stats mirror the model's own scope: on a --feature model these are the feature's numbers, and the dashboard suppresses the ones that only mean something project-wide
94 │ * rendered, and `renderCodePage`/the top bar withhold it on a slice, because 95 │ * "annotated files over source files" measures a repository and a slice's 96 │ * answer to it is 100% by construction. Nothing to fix here; the caller decides 97 │ * what a number is allowed to claim. 98 │ * 99 │ * @comment -- "Stats mirror the model's own scope: on a --feature model these are the feature's numbers, and the dashboard suppresses the ones that only mean something project-wide" 100 │ */ 101 │ export function computeStats(model: ThreatModel): DashboardStats { 102 │ return { 103 │ annotations: model.annotations_parsed, 104 │ sourceFiles: model.source_files,
L291 handles #dashboard: pii
Author identities from git, already in the configured identity mode
286 │ /** 287 │ * The Attribution page's data, or null when no record carries `blame` — the 288 │ * page is then not rendered at all, so a dashboard built without `--blame` is 289 │ * byte-for-byte what it was. 290 │ * 291 │ * @handles pii on #dashboard -- "Author identities from git, already in the configured identity mode" 292 │ * @comment -- "Pure projection of record.blame; the summariser is shared with the CLI and the report so every surface agrees on the numbers" 293 │ */ 294 │ export function computeAttribution(model: ThreatModel): AttributionData | null { 295 │ const entries = entriesFromModel(model); 296 │ if (entries.length === 0) return null;
L292 comment Pure projection of record.blame; the summariser is shared with the CLI and the report so every surface agrees on the numbers
Pure projection of record.blame; the summariser is shared with the CLI and the report so every surface agrees on the numbers
287 │ * The Attribution page's data, or null when no record carries `blame` — the 288 │ * page is then not rendered at all, so a dashboard built without `--blame` is 289 │ * byte-for-byte what it was. 290 │ * 291 │ * @handles pii on #dashboard -- "Author identities from git, already in the configured identity mode" 292 │ * @comment -- "Pure projection of record.blame; the summariser is shared with the CLI and the report so every surface agrees on the numbers" 293 │ */ 294 │ export function computeAttribution(model: ThreatModel): AttributionData | null { 295 │ const entries = entriesFromModel(model); 296 │ if (entries.length === 0) return null; 297 │ const context = (model as ThreatModel & { blame_context?: BlameContext }).blame_context;
L390 comment Every item points somewhere: a hash route into the filtered view, a guardlink command, or both. Nothing here is a bare number
Every item points somewhere: a hash route into the filtered view, a guardlink command, or both. Nothing here is a bare number
385 │ * exposures, then claims whose code moved (stale), then a first verify when no 386 │ * ledger exists, then AI-introduced open exposures, then governance items, 387 │ * then repository coverage. A slice withholds the project-wide measures 388 │ * (coverage, first-verify) that only mean something for the whole repository. 389 │ * 390 │ * @comment -- "Every item points somewhere: a hash route into the filtered view, a guardlink command, or both. Nothing here is a bare number" 391 │ */ 392 │ export function computeActions(input: ActionInput): DashboardAction[] { 393 │ const { model, exposures, confirmed, verification, attribution, scope } = input; 394 │ const out: DashboardAction[] = []; 395 │
L521 flow LedgerFile → #dashboard
Claim states for the badges and the stale-claims action
516 │ * record, so identity is the join. The map is empty when no ledger exists 517 │ * (`report.ledger === 'absent'`), so a dashboard on a never-verified 518 │ * repository renders no badges rather than a wall of "unverified"; the report 519 │ * still carries the unverified count the actions list needs. 520 │ * 521 │ * @flows LedgerFile -> #dashboard via readLedger -- "Claim states for the badges and the stale-claims action" 522 │ * @comment -- "Reads .guardlink/verified.json once; the classification itself is the parser's, not re-derived here" 523 │ */ 524 │ export interface LedgerStates { 525 │ report: VerificationReport; 526 │ byLocation: Map<object, ClaimState>;
L522 comment Reads .guardlink/verified.json once; the classification itself is the parser's, not re-derived here
Reads .guardlink/verified.json once; the classification itself is the parser's, not re-derived here
517 │ * (`report.ledger === 'absent'`), so a dashboard on a never-verified 518 │ * repository renders no badges rather than a wall of "unverified"; the report 519 │ * still carries the unverified count the actions list needs. 520 │ * 521 │ * @flows LedgerFile -> #dashboard via readLedger -- "Claim states for the badges and the stale-claims action" 522 │ * @comment -- "Reads .guardlink/verified.json once; the classification itself is the parser's, not re-derived here" 523 │ */ 524 │ export interface LedgerStates { 525 │ report: VerificationReport; 526 │ byLocation: Map<object, ClaimState>; 527 │ /** Who locked the claim and when, for claims the ledger holds. */
src/dashboard/diagrams.ts 1 stale flow 1mitigates 1comment 1 open 3
L14 flow ThreatModel → #dashboard
Threat model relationships rendered as Mermaid source
9 │ * All three generators share a single alias map so that #id, bare id, name, and 10 │ * path.join() forms of an asset/threat/control collapse onto the same node. 11 │ * This removes the long-standing duplicate-node bug that made the Mermaid 12 │ * diagrams render the same asset twice whenever sources mixed ref forms. 13 │ * 14 │ * @flows ThreatModel -> #dashboard via generateThreatGraph -- "Threat model relationships rendered as Mermaid source" 15 │ * @mitigates #dashboard against #xss using #output-encoding -- "Diagram labels are sanitized before emission as Mermaid source" 16 │ * @comment -- "Alias map collapses #id / name / path-joined ref forms so all three diagrams agree on identity" 17 │ */ 18 │ 19 │ import type { ThreatModel } from '../types/index.js';
L15 mitigates #output-encoding mitigates #xss
Diagram labels are sanitized before emission as Mermaid source
10 │ * path.join() forms of an asset/threat/control collapse onto the same node. 11 │ * This removes the long-standing duplicate-node bug that made the Mermaid 12 │ * diagrams render the same asset twice whenever sources mixed ref forms. 13 │ * 14 │ * @flows ThreatModel -> #dashboard via generateThreatGraph -- "Threat model relationships rendered as Mermaid source" 15 │ * @mitigates #dashboard against #xss using #output-encoding -- "Diagram labels are sanitized before emission as Mermaid source" 16 │ * @comment -- "Alias map collapses #id / name / path-joined ref forms so all three diagrams agree on identity" 17 │ */ 18 │ 19 │ import type { ThreatModel } from '../types/index.js'; 20 │ import { buildCoverageIndex } from '../parser/coverage.js';
L16 comment Alias map collapses #id / name / path-joined ref forms so all three diagrams agree on identity
Alias map collapses #id / name / path-joined ref forms so all three diagrams agree on identity
11 │ * This removes the long-standing duplicate-node bug that made the Mermaid 12 │ * diagrams render the same asset twice whenever sources mixed ref forms. 13 │ * 14 │ * @flows ThreatModel -> #dashboard via generateThreatGraph -- "Threat model relationships rendered as Mermaid source" 15 │ * @mitigates #dashboard against #xss using #output-encoding -- "Diagram labels are sanitized before emission as Mermaid source" 16 │ * @comment -- "Alias map collapses #id / name / path-joined ref forms so all three diagrams agree on identity" 17 │ */ 18 │ 19 │ import type { ThreatModel } from '../types/index.js'; 20 │ import { buildCoverageIndex } from '../parser/coverage.js'; 21 │
src/dashboard/explore.ts exposes 1mitigates 1flow 1comment 1 open 4
L20 exposes #dashboard → #xss
buildExploreData carries asset ids, threat names, file paths and annotation descriptions from the model into strings the Explore page renders
15 │ * a flow plane for each of 18 assets) are **37 KB of Mermaid source in 25 ms**, 16 │ * against a dashboard that is already 4.2 MB. Each one is budgeted to 12 nodes, 17 │ * so the cost per view cannot grow with the model — only the number of views 18 │ * can, and that grows with assets rather than with annotations. 19 │ * 20 │ * @exposes #dashboard to #xss [high] cwe:CWE-79 -- "buildExploreData carries asset ids, threat names, file paths and annotation descriptions from the model into strings the Explore page renders" 21 │ * @mitigates #dashboard against #xss using #output-encoding -- "Every value here is raw; pages/explore.ts escapes each one through esc() at the point it is interpolated, including Mermaid source, which is escaped into the <pre> and read back as text" 22 │ * @flows ThreatModel -> #dashboard via buildExploreData -- "Model narrowed into one budgeted answer per view" 23 │ * @comment -- "Pure over an already-parsed model: no file I/O, no user input, no network" 24 │ */ 25 │ import {
L21 mitigates #output-encoding mitigates #xss
Every value here is raw; pages/explore.ts escapes each one through esc() at the point it is interpolated, including Mermaid source, which is escaped into the <pre> and read back as text
16 │ * against a dashboard that is already 4.2 MB. Each one is budgeted to 12 nodes, 17 │ * so the cost per view cannot grow with the model — only the number of views 18 │ * can, and that grows with assets rather than with annotations. 19 │ * 20 │ * @exposes #dashboard to #xss [high] cwe:CWE-79 -- "buildExploreData carries asset ids, threat names, file paths and annotation descriptions from the model into strings the Explore page renders" 21 │ * @mitigates #dashboard against #xss using #output-encoding -- "Every value here is raw; pages/explore.ts escapes each one through esc() at the point it is interpolated, including Mermaid source, which is escaped into the <pre> and read back as text" 22 │ * @flows ThreatModel -> #dashboard via buildExploreData -- "Model narrowed into one budgeted answer per view" 23 │ * @comment -- "Pure over an already-parsed model: no file I/O, no user input, no network" 24 │ */ 25 │ import { 26 │ growWithinBudget, assetThreatPlane, assetsDeclaredIn, boundarySides,
L22 flow ThreatModel → #dashboard
Model narrowed into one budgeted answer per view
17 │ * so the cost per view cannot grow with the model — only the number of views 18 │ * can, and that grows with assets rather than with annotations. 19 │ * 20 │ * @exposes #dashboard to #xss [high] cwe:CWE-79 -- "buildExploreData carries asset ids, threat names, file paths and annotation descriptions from the model into strings the Explore page renders" 21 │ * @mitigates #dashboard against #xss using #output-encoding -- "Every value here is raw; pages/explore.ts escapes each one through esc() at the point it is interpolated, including Mermaid source, which is escaped into the <pre> and read back as text" 22 │ * @flows ThreatModel -> #dashboard via buildExploreData -- "Model narrowed into one budgeted answer per view" 23 │ * @comment -- "Pure over an already-parsed model: no file I/O, no user input, no network" 24 │ */ 25 │ import { 26 │ growWithinBudget, assetThreatPlane, assetsDeclaredIn, boundarySides, 27 │ FLOW_KINDS, type NarrowingStep,
L23 comment Pure over an already-parsed model: no file I/O, no user input, no network
Pure over an already-parsed model: no file I/O, no user input, no network
18 │ * can, and that grows with assets rather than with annotations. 19 │ * 20 │ * @exposes #dashboard to #xss [high] cwe:CWE-79 -- "buildExploreData carries asset ids, threat names, file paths and annotation descriptions from the model into strings the Explore page renders" 21 │ * @mitigates #dashboard against #xss using #output-encoding -- "Every value here is raw; pages/explore.ts escapes each one through esc() at the point it is interpolated, including Mermaid source, which is escaped into the <pre> and read back as text" 22 │ * @flows ThreatModel -> #dashboard via buildExploreData -- "Model narrowed into one budgeted answer per view" 23 │ * @comment -- "Pure over an already-parsed model: no file I/O, no user input, no network" 24 │ */ 25 │ import { 26 │ growWithinBudget, assetThreatPlane, assetsDeclaredIn, boundarySides, 27 │ FLOW_KINDS, type NarrowingStep, 28 │ } from '../graph/views.js';
src/dashboard/generate.ts 2 stale flow 7mitigates 3exposes 2handles 2 open 15
L21 flow GitRepo → #dashboard
The model at --since <ref>, diffed against the one rendered
16 │ * Analytics (heatmaps, owners, sensitive data and distributions over the same 17 │ * claim rows the tables show, recomputed per feature), Threats, Diagrams (with 18 │ * one focused graph per exposed asset), Code (riskiest file first), Reports, 19 │ * Data, Assets and, with --blame, Attribution. 20 │ * 21 │ * @flows GitRepo -> #dashboard via loadSince -- "The model at --since <ref>, diffed against the one rendered" 22 │ * 23 │ * @exposes #dashboard to #xss [high] cwe:CWE-79 -- "generateDashboardHTML() interpolates model descriptions, asset names, git identities and commit trailers into the page markup and the embedded JSON constants" 24 │ * @mitigates #dashboard against #xss using #output-encoding -- "esc() HTML-encodes every interpolated value in every page module; serialized data escapes closing script tags before embedding in <script>" 25 │ * @exposes #dashboard to #path-traversal [medium] cwe:CWE-22 -- "readFileSync reads code files for annotation context" 26 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "resolve() with root constrains file access (annotations.ts)"
L23 exposes #dashboard → #xss
generateDashboardHTML() interpolates model descriptions, asset names, git identities and commit trailers into the page markup and the embedded JSON constants
18 │ * one focused graph per exposed asset), Code (riskiest file first), Reports, 19 │ * Data, Assets and, with --blame, Attribution. 20 │ * 21 │ * @flows GitRepo -> #dashboard via loadSince -- "The model at --since <ref>, diffed against the one rendered" 22 │ * 23 │ * @exposes #dashboard to #xss [high] cwe:CWE-79 -- "generateDashboardHTML() interpolates model descriptions, asset names, git identities and commit trailers into the page markup and the embedded JSON constants" 24 │ * @mitigates #dashboard against #xss using #output-encoding -- "esc() HTML-encodes every interpolated value in every page module; serialized data escapes closing script tags before embedding in <script>" 25 │ * @exposes #dashboard to #path-traversal [medium] cwe:CWE-22 -- "readFileSync reads code files for annotation context" 26 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "resolve() with root constrains file access (annotations.ts)" 27 │ * @flows ThreatModel -> #dashboard via computeStats -- "Model statistics input" 28 │ * @flows SourceFiles -> #dashboard via readFileSync -- "Code snippet reads"
L24 mitigates #output-encoding mitigates #xss
esc() HTML-encodes every interpolated value in every page module; serialized data escapes closing script tags before embedding in <script>
19 │ * Data, Assets and, with --blame, Attribution. 20 │ * 21 │ * @flows GitRepo -> #dashboard via loadSince -- "The model at --since <ref>, diffed against the one rendered" 22 │ * 23 │ * @exposes #dashboard to #xss [high] cwe:CWE-79 -- "generateDashboardHTML() interpolates model descriptions, asset names, git identities and commit trailers into the page markup and the embedded JSON constants" 24 │ * @mitigates #dashboard against #xss using #output-encoding -- "esc() HTML-encodes every interpolated value in every page module; serialized data escapes closing script tags before embedding in <script>" 25 │ * @exposes #dashboard to #path-traversal [medium] cwe:CWE-22 -- "readFileSync reads code files for annotation context" 26 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "resolve() with root constrains file access (annotations.ts)" 27 │ * @flows ThreatModel -> #dashboard via computeStats -- "Model statistics input" 28 │ * @flows SourceFiles -> #dashboard via readFileSync -- "Code snippet reads" 29 │ * @flows LedgerFile -> #dashboard via computeLedgerStates -- "Claim states for badges and the stale-claims action"
L25 exposes #dashboard → #path-traversal
readFileSync reads code files for annotation context
20 │ * 21 │ * @flows GitRepo -> #dashboard via loadSince -- "The model at --since <ref>, diffed against the one rendered" 22 │ * 23 │ * @exposes #dashboard to #xss [high] cwe:CWE-79 -- "generateDashboardHTML() interpolates model descriptions, asset names, git identities and commit trailers into the page markup and the embedded JSON constants" 24 │ * @mitigates #dashboard against #xss using #output-encoding -- "esc() HTML-encodes every interpolated value in every page module; serialized data escapes closing script tags before embedding in <script>" 25 │ * @exposes #dashboard to #path-traversal [medium] cwe:CWE-22 -- "readFileSync reads code files for annotation context" 26 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "resolve() with root constrains file access (annotations.ts)" 27 │ * @flows ThreatModel -> #dashboard via computeStats -- "Model statistics input" 28 │ * @flows SourceFiles -> #dashboard via readFileSync -- "Code snippet reads" 29 │ * @flows LedgerFile -> #dashboard via computeLedgerStates -- "Claim states for badges and the stale-claims action" 30 │ * @flows GitConfig -> #dashboard via detectRepoLinks -- "The remote's web host, for file and commit links"
L26 mitigates #path-validation mitigates #path-traversal
resolve() with root constrains file access (annotations.ts)
21 │ * @flows GitRepo -> #dashboard via loadSince -- "The model at --since <ref>, diffed against the one rendered" 22 │ * 23 │ * @exposes #dashboard to #xss [high] cwe:CWE-79 -- "generateDashboardHTML() interpolates model descriptions, asset names, git identities and commit trailers into the page markup and the embedded JSON constants" 24 │ * @mitigates #dashboard against #xss using #output-encoding -- "esc() HTML-encodes every interpolated value in every page module; serialized data escapes closing script tags before embedding in <script>" 25 │ * @exposes #dashboard to #path-traversal [medium] cwe:CWE-22 -- "readFileSync reads code files for annotation context" 26 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "resolve() with root constrains file access (annotations.ts)" 27 │ * @flows ThreatModel -> #dashboard via computeStats -- "Model statistics input" 28 │ * @flows SourceFiles -> #dashboard via readFileSync -- "Code snippet reads" 29 │ * @flows LedgerFile -> #dashboard via computeLedgerStates -- "Claim states for badges and the stale-claims action" 30 │ * @flows GitConfig -> #dashboard via detectRepoLinks -- "The remote's web host, for file and commit links" 31 │ * @flows #dashboard -> HTML via return -- "Generated HTML output"
L27 flow ThreatModel → #dashboard
Model statistics input
22 │ * 23 │ * @exposes #dashboard to #xss [high] cwe:CWE-79 -- "generateDashboardHTML() interpolates model descriptions, asset names, git identities and commit trailers into the page markup and the embedded JSON constants" 24 │ * @mitigates #dashboard against #xss using #output-encoding -- "esc() HTML-encodes every interpolated value in every page module; serialized data escapes closing script tags before embedding in <script>" 25 │ * @exposes #dashboard to #path-traversal [medium] cwe:CWE-22 -- "readFileSync reads code files for annotation context" 26 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "resolve() with root constrains file access (annotations.ts)" 27 │ * @flows ThreatModel -> #dashboard via computeStats -- "Model statistics input" 28 │ * @flows SourceFiles -> #dashboard via readFileSync -- "Code snippet reads" 29 │ * @flows LedgerFile -> #dashboard via computeLedgerStates -- "Claim states for badges and the stale-claims action" 30 │ * @flows GitConfig -> #dashboard via detectRepoLinks -- "The remote's web host, for file and commit links" 31 │ * @flows #dashboard -> HTML via return -- "Generated HTML output" 32 │ * @handles internal on #dashboard -- "Processes and displays threat model data"
L28 flow SourceFiles → #dashboard
Code snippet reads
23 │ * @exposes #dashboard to #xss [high] cwe:CWE-79 -- "generateDashboardHTML() interpolates model descriptions, asset names, git identities and commit trailers into the page markup and the embedded JSON constants" 24 │ * @mitigates #dashboard against #xss using #output-encoding -- "esc() HTML-encodes every interpolated value in every page module; serialized data escapes closing script tags before embedding in <script>" 25 │ * @exposes #dashboard to #path-traversal [medium] cwe:CWE-22 -- "readFileSync reads code files for annotation context" 26 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "resolve() with root constrains file access (annotations.ts)" 27 │ * @flows ThreatModel -> #dashboard via computeStats -- "Model statistics input" 28 │ * @flows SourceFiles -> #dashboard via readFileSync -- "Code snippet reads" 29 │ * @flows LedgerFile -> #dashboard via computeLedgerStates -- "Claim states for badges and the stale-claims action" 30 │ * @flows GitConfig -> #dashboard via detectRepoLinks -- "The remote's web host, for file and commit links" 31 │ * @flows #dashboard -> HTML via return -- "Generated HTML output" 32 │ * @handles internal on #dashboard -- "Processes and displays threat model data" 33 │ * @handles pii on #dashboard -- "Author identities from attribution, in the configured identity mode"
L29 flow LedgerFile → #dashboard
Claim states for badges and the stale-claims action
24 │ * @mitigates #dashboard against #xss using #output-encoding -- "esc() HTML-encodes every interpolated value in every page module; serialized data escapes closing script tags before embedding in <script>" 25 │ * @exposes #dashboard to #path-traversal [medium] cwe:CWE-22 -- "readFileSync reads code files for annotation context" 26 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "resolve() with root constrains file access (annotations.ts)" 27 │ * @flows ThreatModel -> #dashboard via computeStats -- "Model statistics input" 28 │ * @flows SourceFiles -> #dashboard via readFileSync -- "Code snippet reads" 29 │ * @flows LedgerFile -> #dashboard via computeLedgerStates -- "Claim states for badges and the stale-claims action" 30 │ * @flows GitConfig -> #dashboard via detectRepoLinks -- "The remote's web host, for file and commit links" 31 │ * @flows #dashboard -> HTML via return -- "Generated HTML output" 32 │ * @handles internal on #dashboard -- "Processes and displays threat model data" 33 │ * @handles pii on #dashboard -- "Author identities from attribution, in the configured identity mode" 34 │ * @feature "Dashboard" -- "Interactive HTML threat model dashboard"
L30 flow GitConfig → #dashboard
The remote's web host, for file and commit links
25 │ * @exposes #dashboard to #path-traversal [medium] cwe:CWE-22 -- "readFileSync reads code files for annotation context" 26 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "resolve() with root constrains file access (annotations.ts)" 27 │ * @flows ThreatModel -> #dashboard via computeStats -- "Model statistics input" 28 │ * @flows SourceFiles -> #dashboard via readFileSync -- "Code snippet reads" 29 │ * @flows LedgerFile -> #dashboard via computeLedgerStates -- "Claim states for badges and the stale-claims action" 30 │ * @flows GitConfig -> #dashboard via detectRepoLinks -- "The remote's web host, for file and commit links" 31 │ * @flows #dashboard -> HTML via return -- "Generated HTML output" 32 │ * @handles internal on #dashboard -- "Processes and displays threat model data" 33 │ * @handles pii on #dashboard -- "Author identities from attribution, in the configured identity mode" 34 │ * @feature "Dashboard" -- "Interactive HTML threat model dashboard" 35 │ * @mitigates #dashboard against #xss using #output-encoding -- "Feature scope names come from @feature annotations and the --feature flag; every one is rendered through esc()"
L31 flow #dashboard → HTML
Generated HTML output
26 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "resolve() with root constrains file access (annotations.ts)" 27 │ * @flows ThreatModel -> #dashboard via computeStats -- "Model statistics input" 28 │ * @flows SourceFiles -> #dashboard via readFileSync -- "Code snippet reads" 29 │ * @flows LedgerFile -> #dashboard via computeLedgerStates -- "Claim states for badges and the stale-claims action" 30 │ * @flows GitConfig -> #dashboard via detectRepoLinks -- "The remote's web host, for file and commit links" 31 │ * @flows #dashboard -> HTML via return -- "Generated HTML output" 32 │ * @handles internal on #dashboard -- "Processes and displays threat model data" 33 │ * @handles pii on #dashboard -- "Author identities from attribution, in the configured identity mode" 34 │ * @feature "Dashboard" -- "Interactive HTML threat model dashboard" 35 │ * @mitigates #dashboard against #xss using #output-encoding -- "Feature scope names come from @feature annotations and the --feature flag; every one is rendered through esc()" 36 │ * @comment -- "A model narrowed with --feature carries filtered_by_features. The page then declares itself a slice in the title, the top bar and a banner, and suppresses the project-wide measures (file coverage, unannotated files) that a slice cannot answer"
L32 handles #dashboard: internal
Processes and displays threat model data
27 │ * @flows ThreatModel -> #dashboard via computeStats -- "Model statistics input" 28 │ * @flows SourceFiles -> #dashboard via readFileSync -- "Code snippet reads" 29 │ * @flows LedgerFile -> #dashboard via computeLedgerStates -- "Claim states for badges and the stale-claims action" 30 │ * @flows GitConfig -> #dashboard via detectRepoLinks -- "The remote's web host, for file and commit links" 31 │ * @flows #dashboard -> HTML via return -- "Generated HTML output" 32 │ * @handles internal on #dashboard -- "Processes and displays threat model data" 33 │ * @handles pii on #dashboard -- "Author identities from attribution, in the configured identity mode" 34 │ * @feature "Dashboard" -- "Interactive HTML threat model dashboard" 35 │ * @mitigates #dashboard against #xss using #output-encoding -- "Feature scope names come from @feature annotations and the --feature flag; every one is rendered through esc()" 36 │ * @comment -- "A model narrowed with --feature carries filtered_by_features. The page then declares itself a slice in the title, the top bar and a banner, and suppresses the project-wide measures (file coverage, unannotated files) that a slice cannot answer" 37 │ */
L33 handles #dashboard: pii
Author identities from attribution, in the configured identity mode
28 │ * @flows SourceFiles -> #dashboard via readFileSync -- "Code snippet reads" 29 │ * @flows LedgerFile -> #dashboard via computeLedgerStates -- "Claim states for badges and the stale-claims action" 30 │ * @flows GitConfig -> #dashboard via detectRepoLinks -- "The remote's web host, for file and commit links" 31 │ * @flows #dashboard -> HTML via return -- "Generated HTML output" 32 │ * @handles internal on #dashboard -- "Processes and displays threat model data" 33 │ * @handles pii on #dashboard -- "Author identities from attribution, in the configured identity mode" 34 │ * @feature "Dashboard" -- "Interactive HTML threat model dashboard" 35 │ * @mitigates #dashboard against #xss using #output-encoding -- "Feature scope names come from @feature annotations and the --feature flag; every one is rendered through esc()" 36 │ * @comment -- "A model narrowed with --feature carries filtered_by_features. The page then declares itself a slice in the title, the top bar and a banner, and suppresses the project-wide measures (file coverage, unannotated files) that a slice cannot answer" 37 │ */ 38 │ import type { ThreatModel } from '../types/index.js';
L35 mitigates #output-encoding mitigates #xss
Feature scope names come from @feature annotations and the --feature flag; every one is rendered through esc()
30 │ * @flows GitConfig -> #dashboard via detectRepoLinks -- "The remote's web host, for file and commit links" 31 │ * @flows #dashboard -> HTML via return -- "Generated HTML output" 32 │ * @handles internal on #dashboard -- "Processes and displays threat model data" 33 │ * @handles pii on #dashboard -- "Author identities from attribution, in the configured identity mode" 34 │ * @feature "Dashboard" -- "Interactive HTML threat model dashboard" 35 │ * @mitigates #dashboard against #xss using #output-encoding -- "Feature scope names come from @feature annotations and the --feature flag; every one is rendered through esc()" 36 │ * @comment -- "A model narrowed with --feature carries filtered_by_features. The page then declares itself a slice in the title, the top bar and a banner, and suppresses the project-wide measures (file coverage, unannotated files) that a slice cannot answer" 37 │ */ 38 │ import type { ThreatModel } from '../types/index.js'; 39 │ import { listFeatures, filterByFeature } from '../parser/feature-filter.js'; 40 │ import { selectSubgraph, canonicaliser } from '../mcp/subgraph.js';
L36 comment A model narrowed with --feature carries filtered_by_features. The page then declares itself a slice in the title, the top bar and a banner, and suppresses the project-wide measures (file coverage, unannotated files) that a slice cannot answer
A model narrowed with --feature carries filtered_by_features. The page then declares itself a slice in the title, the top bar and a banner, and suppresses the project-wide measures (file coverage, unannotated files) that a slice cannot answer
31 │ * @flows #dashboard -> HTML via return -- "Generated HTML output" 32 │ * @handles internal on #dashboard -- "Processes and displays threat model data" 33 │ * @handles pii on #dashboard -- "Author identities from attribution, in the configured identity mode" 34 │ * @feature "Dashboard" -- "Interactive HTML threat model dashboard" 35 │ * @mitigates #dashboard against #xss using #output-encoding -- "Feature scope names come from @feature annotations and the --feature flag; every one is rendered through esc()" 36 │ * @comment -- "A model narrowed with --feature carries filtered_by_features. The page then declares itself a slice in the title, the top bar and a banner, and suppresses the project-wide measures (file coverage, unannotated files) that a slice cannot answer" 37 │ */ 38 │ import type { ThreatModel } from '../types/index.js'; 39 │ import { listFeatures, filterByFeature } from '../parser/feature-filter.js'; 40 │ import { selectSubgraph, canonicaliser } from '../mcp/subgraph.js'; 41 │ import { canonicalizeModelOrder } from '../parser/canonical-order.js';
L136 flow LedgerFile → #dashboard
Outcomes for the badges, the drawer and the open count
131 │ } 132 │ 133 │ export function generateDashboardHTML(rawModel: ThreatModel, root?: string, analyses?: ThreatReportWithContent[], opts: DashboardOptions = {}): string { 134 │ const model = canonicalizeModelOrder(rawModel); 135 │ // The tested state of every exposure, when a ledger exists: a refuted claim is not open. 136 │ // @flows LedgerFile -> #dashboard via readHypotheses -- "Outcomes for the badges, the drawer and the open count" 137 │ const hypotheses = root ? classifyHypotheses(model, readHypotheses(root)) : null; 138 │ if (hypotheses) attachHypotheses(model, hypotheses); 139 │ // Read from rawModel: canonicalisation reorders, it does not add fields. 140 │ const scope = featureScope(rawModel); 141 │ const { generated_at: _generatedAt, blame_context: _blameContext, ...durableModel } = model as ThreatModel & { blame_context?: unknown };
src/dashboard/html.ts mitigates 1comment 1 open 2
L8 mitigates #output-encoding mitigates #xss
esc() HTML-encodes every interpolated value; hrefs built here are encoded too, so a hash route carrying a search term cannot break out of the attribute
3 │ * 4 │ * Every value that came from the model, from git, or from a file name passes 5 │ * through `esc()` here or in the page that calls these. The one exception is 6 │ * markup these helpers build themselves. 7 │ * 8 │ * @mitigates #dashboard against #xss using #output-encoding -- "esc() HTML-encodes every interpolated value; hrefs built here are encoded too, so a hash route carrying a search term cannot break out of the attribute" 9 │ * @comment -- "statCard keeps its exact markup: the feature-slice test greps the Open Threats tile by that string" 10 │ */ 11 │ import type { RepoLinks } from './links.js'; 12 │ import type { ThreatModel } from '../types/index.js'; 13 │ import type { ClaimState } from '../parser/verification.js';
L9 comment statCard keeps its exact markup: the feature-slice test greps the Open Threats tile by that string
statCard keeps its exact markup: the feature-slice test greps the Open Threats tile by that string
4 │ * Every value that came from the model, from git, or from a file name passes 5 │ * through `esc()` here or in the page that calls these. The one exception is 6 │ * markup these helpers build themselves. 7 │ * 8 │ * @mitigates #dashboard against #xss using #output-encoding -- "esc() HTML-encodes every interpolated value; hrefs built here are encoded too, so a hash route carrying a search term cannot break out of the attribute" 9 │ * @comment -- "statCard keeps its exact markup: the feature-slice test greps the Open Threats tile by that string" 10 │ */ 11 │ import type { RepoLinks } from './links.js'; 12 │ import type { ThreatModel } from '../types/index.js'; 13 │ import type { ClaimState } from '../parser/verification.js'; 14 │
src/dashboard/index.ts 2 stale exposes 1mitigates 1flow 1comment 1 open 4
L4 exposes #dashboard → #xss
Generates HTML with threat model data
1 │ /** 2 │ * GuardLink Dashboard — Self-contained HTML threat model dashboard. 3 │ * 4 │ * @exposes #dashboard to #xss [high] cwe:CWE-79 -- "Generates HTML with threat model data" 5 │ * @mitigates #dashboard against #xss using #output-encoding -- "esc() function encodes all interpolated values" 6 │ * @flows ThreatModel -> #dashboard via generateDashboardHTML -- "Model to HTML transformation" 7 │ * @comment -- "Self-contained HTML; no external data injection after generation" 8 │ */ 9 │
L5 mitigates #output-encoding mitigates #xss
esc() function encodes all interpolated values
1 │ /** 2 │ * GuardLink Dashboard — Self-contained HTML threat model dashboard. 3 │ * 4 │ * @exposes #dashboard to #xss [high] cwe:CWE-79 -- "Generates HTML with threat model data" 5 │ * @mitigates #dashboard against #xss using #output-encoding -- "esc() function encodes all interpolated values" 6 │ * @flows ThreatModel -> #dashboard via generateDashboardHTML -- "Model to HTML transformation" 7 │ * @comment -- "Self-contained HTML; no external data injection after generation" 8 │ */ 9 │ 10 │ export { generateDashboardHTML } from './generate.js';
L6 flow ThreatModel → #dashboard
Model to HTML transformation
1 │ /** 2 │ * GuardLink Dashboard — Self-contained HTML threat model dashboard. 3 │ * 4 │ * @exposes #dashboard to #xss [high] cwe:CWE-79 -- "Generates HTML with threat model data" 5 │ * @mitigates #dashboard against #xss using #output-encoding -- "esc() function encodes all interpolated values" 6 │ * @flows ThreatModel -> #dashboard via generateDashboardHTML -- "Model to HTML transformation" 7 │ * @comment -- "Self-contained HTML; no external data injection after generation" 8 │ */ 9 │ 10 │ export { generateDashboardHTML } from './generate.js'; 11 │ export type { DashboardOptions } from './generate.js';
L7 comment Self-contained HTML; no external data injection after generation
Self-contained HTML; no external data injection after generation
2 │ * GuardLink Dashboard — Self-contained HTML threat model dashboard. 3 │ * 4 │ * @exposes #dashboard to #xss [high] cwe:CWE-79 -- "Generates HTML with threat model data" 5 │ * @mitigates #dashboard against #xss using #output-encoding -- "esc() function encodes all interpolated values" 6 │ * @flows ThreatModel -> #dashboard via generateDashboardHTML -- "Model to HTML transformation" 7 │ * @comment -- "Self-contained HTML; no external data injection after generation" 8 │ */ 9 │ 10 │ export { generateDashboardHTML } from './generate.js'; 11 │ export type { DashboardOptions } from './generate.js'; 12 │ export { loadSince } from './since.js';
src/dashboard/links.ts mitigates 3comment 3exposes 2flow 2 open 10
L10 exposes #dashboard → #path-traversal
detectRepoLinks reads <root>/.git/config and, when .git is a worktree file, follows its `gitdir:` pointer to a config elsewhere on disk; a crafted .git file can name any readable path
5 │ * file (and line) at `HEAD` of the default branch, and a commit. `HEAD` rather 6 │ * than a pinned SHA keeps a committed dashboard from churning on every 7 │ * regeneration. Detection reads `.git/config` directly — no `git` subprocess, 8 │ * no network — mirroring `readGitSha` in `src/workspace/metadata.ts`. 9 │ * 10 │ * @exposes #dashboard to #path-traversal [low] cwe:CWE-22 -- "detectRepoLinks reads <root>/.git/config and, when .git is a worktree file, follows its `gitdir:` pointer to a config elsewhere on disk; a crafted .git file can name any readable path" 11 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "Only the fixed `config` name is read under the pointed-to gitdir, only `remote.origin.url` is extracted from it, and the result is discarded unless it parses as an http(s)/ssh remote to a web host; file links reject `..` segments and absolute paths and fall back to the repo web root" 12 │ * @exposes #dashboard to #data-exposure [low] cwe:CWE-200 -- "detectRepoLinks() reads remote.origin.url from .git/config, which may embed user:token@host credentials that would land in the committed HTML" 13 │ * @mitigates #dashboard against #data-exposure using #output-encoding -- "linksFromRemote rebuilds every link from hostname and path only; userinfo is dropped before anything is emitted, so credentials never reach RepoLinks.web" 14 │ * @flows GitConfig -> #dashboard via readFileSync -- "remote.origin.url read from .git/config (or the worktree's gitdir config)" 15 │ * @flows #dashboard -> DashboardHTML via RepoLinks -- "Web, file and commit links embedded in the generated dashboard"
L11 mitigates #path-validation mitigates #path-traversal
Only the fixed `config` name is read under the pointed-to gitdir, only `remote.origin.url` is extracted from it, and the result is discarded unless it parses as an http(s)/ssh remote to a web host; file links reject `..` segments and absolute paths and fall back to the repo web root
6 │ * than a pinned SHA keeps a committed dashboard from churning on every 7 │ * regeneration. Detection reads `.git/config` directly — no `git` subprocess, 8 │ * no network — mirroring `readGitSha` in `src/workspace/metadata.ts`. 9 │ * 10 │ * @exposes #dashboard to #path-traversal [low] cwe:CWE-22 -- "detectRepoLinks reads <root>/.git/config and, when .git is a worktree file, follows its `gitdir:` pointer to a config elsewhere on disk; a crafted .git file can name any readable path" 11 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "Only the fixed `config` name is read under the pointed-to gitdir, only `remote.origin.url` is extracted from it, and the result is discarded unless it parses as an http(s)/ssh remote to a web host; file links reject `..` segments and absolute paths and fall back to the repo web root" 12 │ * @exposes #dashboard to #data-exposure [low] cwe:CWE-200 -- "detectRepoLinks() reads remote.origin.url from .git/config, which may embed user:token@host credentials that would land in the committed HTML" 13 │ * @mitigates #dashboard against #data-exposure using #output-encoding -- "linksFromRemote rebuilds every link from hostname and path only; userinfo is dropped before anything is emitted, so credentials never reach RepoLinks.web" 14 │ * @flows GitConfig -> #dashboard via readFileSync -- "remote.origin.url read from .git/config (or the worktree's gitdir config)" 15 │ * @flows #dashboard -> DashboardHTML via RepoLinks -- "Web, file and commit links embedded in the generated dashboard" 16 │ * @comment -- "Pure and deterministic: URL parsing is string work, detection is two file reads at most. Hosts are classified by hostname substring (github/gitlab/bitbucket); anything else is `other` and gets GitHub-shaped links as a best effort."
L12 exposes #dashboard → #data-exposure
detectRepoLinks() reads remote.origin.url from .git/config, which may embed user:token@host credentials that would land in the committed HTML
7 │ * regeneration. Detection reads `.git/config` directly — no `git` subprocess, 8 │ * no network — mirroring `readGitSha` in `src/workspace/metadata.ts`. 9 │ * 10 │ * @exposes #dashboard to #path-traversal [low] cwe:CWE-22 -- "detectRepoLinks reads <root>/.git/config and, when .git is a worktree file, follows its `gitdir:` pointer to a config elsewhere on disk; a crafted .git file can name any readable path" 11 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "Only the fixed `config` name is read under the pointed-to gitdir, only `remote.origin.url` is extracted from it, and the result is discarded unless it parses as an http(s)/ssh remote to a web host; file links reject `..` segments and absolute paths and fall back to the repo web root" 12 │ * @exposes #dashboard to #data-exposure [low] cwe:CWE-200 -- "detectRepoLinks() reads remote.origin.url from .git/config, which may embed user:token@host credentials that would land in the committed HTML" 13 │ * @mitigates #dashboard against #data-exposure using #output-encoding -- "linksFromRemote rebuilds every link from hostname and path only; userinfo is dropped before anything is emitted, so credentials never reach RepoLinks.web" 14 │ * @flows GitConfig -> #dashboard via readFileSync -- "remote.origin.url read from .git/config (or the worktree's gitdir config)" 15 │ * @flows #dashboard -> DashboardHTML via RepoLinks -- "Web, file and commit links embedded in the generated dashboard" 16 │ * @comment -- "Pure and deterministic: URL parsing is string work, detection is two file reads at most. Hosts are classified by hostname substring (github/gitlab/bitbucket); anything else is `other` and gets GitHub-shaped links as a best effort." 17 │ */
L13 mitigates #output-encoding mitigates #data-exposure
linksFromRemote rebuilds every link from hostname and path only; userinfo is dropped before anything is emitted, so credentials never reach RepoLinks.web
8 │ * no network — mirroring `readGitSha` in `src/workspace/metadata.ts`. 9 │ * 10 │ * @exposes #dashboard to #path-traversal [low] cwe:CWE-22 -- "detectRepoLinks reads <root>/.git/config and, when .git is a worktree file, follows its `gitdir:` pointer to a config elsewhere on disk; a crafted .git file can name any readable path" 11 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "Only the fixed `config` name is read under the pointed-to gitdir, only `remote.origin.url` is extracted from it, and the result is discarded unless it parses as an http(s)/ssh remote to a web host; file links reject `..` segments and absolute paths and fall back to the repo web root" 12 │ * @exposes #dashboard to #data-exposure [low] cwe:CWE-200 -- "detectRepoLinks() reads remote.origin.url from .git/config, which may embed user:token@host credentials that would land in the committed HTML" 13 │ * @mitigates #dashboard against #data-exposure using #output-encoding -- "linksFromRemote rebuilds every link from hostname and path only; userinfo is dropped before anything is emitted, so credentials never reach RepoLinks.web" 14 │ * @flows GitConfig -> #dashboard via readFileSync -- "remote.origin.url read from .git/config (or the worktree's gitdir config)" 15 │ * @flows #dashboard -> DashboardHTML via RepoLinks -- "Web, file and commit links embedded in the generated dashboard" 16 │ * @comment -- "Pure and deterministic: URL parsing is string work, detection is two file reads at most. Hosts are classified by hostname substring (github/gitlab/bitbucket); anything else is `other` and gets GitHub-shaped links as a best effort." 17 │ */ 18 │
L14 flow GitConfig → #dashboard
remote.origin.url read from .git/config (or the worktree's gitdir config)
9 │ * 10 │ * @exposes #dashboard to #path-traversal [low] cwe:CWE-22 -- "detectRepoLinks reads <root>/.git/config and, when .git is a worktree file, follows its `gitdir:` pointer to a config elsewhere on disk; a crafted .git file can name any readable path" 11 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "Only the fixed `config` name is read under the pointed-to gitdir, only `remote.origin.url` is extracted from it, and the result is discarded unless it parses as an http(s)/ssh remote to a web host; file links reject `..` segments and absolute paths and fall back to the repo web root" 12 │ * @exposes #dashboard to #data-exposure [low] cwe:CWE-200 -- "detectRepoLinks() reads remote.origin.url from .git/config, which may embed user:token@host credentials that would land in the committed HTML" 13 │ * @mitigates #dashboard against #data-exposure using #output-encoding -- "linksFromRemote rebuilds every link from hostname and path only; userinfo is dropped before anything is emitted, so credentials never reach RepoLinks.web" 14 │ * @flows GitConfig -> #dashboard via readFileSync -- "remote.origin.url read from .git/config (or the worktree's gitdir config)" 15 │ * @flows #dashboard -> DashboardHTML via RepoLinks -- "Web, file and commit links embedded in the generated dashboard" 16 │ * @comment -- "Pure and deterministic: URL parsing is string work, detection is two file reads at most. Hosts are classified by hostname substring (github/gitlab/bitbucket); anything else is `other` and gets GitHub-shaped links as a best effort." 17 │ */ 18 │ 19 │ import { readFileSync } from 'node:fs';
L15 flow #dashboard → DashboardHTML
Web, file and commit links embedded in the generated dashboard
10 │ * @exposes #dashboard to #path-traversal [low] cwe:CWE-22 -- "detectRepoLinks reads <root>/.git/config and, when .git is a worktree file, follows its `gitdir:` pointer to a config elsewhere on disk; a crafted .git file can name any readable path" 11 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "Only the fixed `config` name is read under the pointed-to gitdir, only `remote.origin.url` is extracted from it, and the result is discarded unless it parses as an http(s)/ssh remote to a web host; file links reject `..` segments and absolute paths and fall back to the repo web root" 12 │ * @exposes #dashboard to #data-exposure [low] cwe:CWE-200 -- "detectRepoLinks() reads remote.origin.url from .git/config, which may embed user:token@host credentials that would land in the committed HTML" 13 │ * @mitigates #dashboard against #data-exposure using #output-encoding -- "linksFromRemote rebuilds every link from hostname and path only; userinfo is dropped before anything is emitted, so credentials never reach RepoLinks.web" 14 │ * @flows GitConfig -> #dashboard via readFileSync -- "remote.origin.url read from .git/config (or the worktree's gitdir config)" 15 │ * @flows #dashboard -> DashboardHTML via RepoLinks -- "Web, file and commit links embedded in the generated dashboard" 16 │ * @comment -- "Pure and deterministic: URL parsing is string work, detection is two file reads at most. Hosts are classified by hostname substring (github/gitlab/bitbucket); anything else is `other` and gets GitHub-shaped links as a best effort." 17 │ */ 18 │ 19 │ import { readFileSync } from 'node:fs'; 20 │ import { isAbsolute, join } from 'node:path';
L16 comment Pure and deterministic: URL parsing is string work, detection is two file reads at most. Hosts are classified by hostname substring (github/gitlab/bitbucket); anything else is `other` and gets GitHub-shaped links as a best effort.
Pure and deterministic: URL parsing is string work, detection is two file reads at most. Hosts are classified by hostname substring (github/gitlab/bitbucket); anything else is `other` and gets GitHub-shaped links as a best effort.
11 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "Only the fixed `config` name is read under the pointed-to gitdir, only `remote.origin.url` is extracted from it, and the result is discarded unless it parses as an http(s)/ssh remote to a web host; file links reject `..` segments and absolute paths and fall back to the repo web root" 12 │ * @exposes #dashboard to #data-exposure [low] cwe:CWE-200 -- "detectRepoLinks() reads remote.origin.url from .git/config, which may embed user:token@host credentials that would land in the committed HTML" 13 │ * @mitigates #dashboard against #data-exposure using #output-encoding -- "linksFromRemote rebuilds every link from hostname and path only; userinfo is dropped before anything is emitted, so credentials never reach RepoLinks.web" 14 │ * @flows GitConfig -> #dashboard via readFileSync -- "remote.origin.url read from .git/config (or the worktree's gitdir config)" 15 │ * @flows #dashboard -> DashboardHTML via RepoLinks -- "Web, file and commit links embedded in the generated dashboard" 16 │ * @comment -- "Pure and deterministic: URL parsing is string work, detection is two file reads at most. Hosts are classified by hostname substring (github/gitlab/bitbucket); anything else is `other` and gets GitHub-shaped links as a best effort." 17 │ */ 18 │ 19 │ import { readFileSync } from 'node:fs'; 20 │ import { isAbsolute, join } from 'node:path'; 21 │
L38 comment Accepts scp-style (git@host:org/repo.git), ssh://, http:// and https:// remotes. Strips `.git` and trailing slashes from the path.
Accepts scp-style (git@host:org/repo.git), ssh://, http:// and https:// remotes. Strips `.git` and trailing slashes from the path.
33 │ 34 │ /** 35 │ * Parse a git remote URL into RepoLinks; null when it is not http(s)/ssh to a 36 │ * recognisable host. 37 │ * 38 │ * @comment -- "Accepts scp-style (git@host:org/repo.git), ssh://, http:// and https:// remotes. Strips `.git` and trailing slashes from the path." 39 │ */ 40 │ export function linksFromRemote(url: string): RepoLinks | null { 41 │ const parsed = parseRemote(url.trim()); 42 │ if (!parsed) return null; 43 │
L84 mitigates #path-validation mitigates #path-traversal
Absolute paths and any `..` segment (either separator) are refused; each surviving segment is encodeURIComponent-ed so `#`, `?` and spaces cannot rewrite the URL
79 │ * Encode a repo-relative path one segment at a time. Null when the path could 80 │ * escape the repo — absolute, or containing a `..` segment — or when nothing is 81 │ * left after dropping `.` and empty segments. Callers fall back to the web root 82 │ * on null: a link must never point outside the repository. 83 │ * 84 │ * @mitigates #dashboard against #path-traversal using #path-validation -- "Absolute paths and any `..` segment (either separator) are refused; each surviving segment is encodeURIComponent-ed so `#`, `?` and spaces cannot rewrite the URL" 85 │ */ 86 │ function safeFilePath(path: string): string | null { 87 │ if (path.startsWith('/') || path.startsWith('\\')) return null; 88 │ const segments = path.split(/[\/\\]/).filter((s) => s !== '' && s !== '.'); 89 │ if (segments.length === 0 || segments.includes('..')) return null;
L118 comment Best effort and side-effect free: every failure mode (no .git, junk pointer, dangling gitdir, config without origin, local-path origin) collapses to null rather than throwing, so dashboard generation never depends on this succeeding
Best effort and side-effect free: every failure mode (no .git, junk pointer, dangling gitdir, config without origin, local-path origin) collapses to null rather than throwing, so dashboard generation never depends on this succeeding
113 │ * When `.git` is a file (worktree or submodule checkout) it holds a single 114 │ * `gitdir: <path>` line; the config lives under that directory instead. The 115 │ * pointer is followed as-is — relative paths resolve against `root` — because 116 │ * git itself wrote it, and only the fixed `config` filename is read from it. 117 │ * 118 │ * @comment -- "Best effort and side-effect free: every failure mode (no .git, junk pointer, dangling gitdir, config without origin, local-path origin) collapses to null rather than throwing, so dashboard generation never depends on this succeeding" 119 │ */ 120 │ export function detectRepoLinks(root: string): RepoLinks | null { 121 │ const origin = readOriginUrl(root); 122 │ return origin === null ? null : linksFromRemote(origin); 123 │ }
src/dashboard/pages/analytics.ts mitigates 1handles 1comment 1 open 3
L11 mitigates #output-encoding mitigates #xss
Asset, threat, control, file and identity names are escaped by heatTable/barList; routes are attribute-escaped
6 │ * AI tool × severity. 7 │ * 8 │ * Every cell links into the filtered Threats page, so a number is never a 9 │ * dead end. 10 │ * 11 │ * @mitigates #dashboard against #xss using #output-encoding -- "Asset, threat, control, file and identity names are escaped by heatTable/barList; routes are attribute-escaped" 12 │ * @handles pii on #dashboard -- "Introducer identities in the people × time heatmap" 13 │ * @comment -- "Pure rendering over the analytics builders; the same claim rows feed every grid so the numbers agree with the tables" 14 │ */ 15 │ import { esc, heatTable, barList, sectionHead, subHead, routeWithQuery, badge, kpi, plural, shortPath, icon, sortableHead, numCell, sevBadge } from '../html.js'; 16 │ import { computeAssetThreatMatrix, computeControlCoverage, computeSeverityStatus, computeIntroductionHeat, computeToolSeverity, computeOwnership, computeSensitiveData, SEV_ORDER } from '../analytics.js';
L12 handles #dashboard: pii
Introducer identities in the people × time heatmap
7 │ * 8 │ * Every cell links into the filtered Threats page, so a number is never a 9 │ * dead end. 10 │ * 11 │ * @mitigates #dashboard against #xss using #output-encoding -- "Asset, threat, control, file and identity names are escaped by heatTable/barList; routes are attribute-escaped" 12 │ * @handles pii on #dashboard -- "Introducer identities in the people × time heatmap" 13 │ * @comment -- "Pure rendering over the analytics builders; the same claim rows feed every grid so the numbers agree with the tables" 14 │ */ 15 │ import { esc, heatTable, barList, sectionHead, subHead, routeWithQuery, badge, kpi, plural, shortPath, icon, sortableHead, numCell, sevBadge } from '../html.js'; 16 │ import { computeAssetThreatMatrix, computeControlCoverage, computeSeverityStatus, computeIntroductionHeat, computeToolSeverity, computeOwnership, computeSensitiveData, SEV_ORDER } from '../analytics.js'; 17 │ import type { SevKey } from '../analytics.js';
L13 comment Pure rendering over the analytics builders; the same claim rows feed every grid so the numbers agree with the tables
Pure rendering over the analytics builders; the same claim rows feed every grid so the numbers agree with the tables
8 │ * Every cell links into the filtered Threats page, so a number is never a 9 │ * dead end. 10 │ * 11 │ * @mitigates #dashboard against #xss using #output-encoding -- "Asset, threat, control, file and identity names are escaped by heatTable/barList; routes are attribute-escaped" 12 │ * @handles pii on #dashboard -- "Introducer identities in the people × time heatmap" 13 │ * @comment -- "Pure rendering over the analytics builders; the same claim rows feed every grid so the numbers agree with the tables" 14 │ */ 15 │ import { esc, heatTable, barList, sectionHead, subHead, routeWithQuery, badge, kpi, plural, shortPath, icon, sortableHead, numCell, sevBadge } from '../html.js'; 16 │ import { computeAssetThreatMatrix, computeControlCoverage, computeSeverityStatus, computeIntroductionHeat, computeToolSeverity, computeOwnership, computeSensitiveData, SEV_ORDER } from '../analytics.js'; 17 │ import type { SevKey } from '../analytics.js'; 18 │ import type { PageContext } from './context.js';
src/dashboard/pages/assets.ts mitigates 1comment 1 open 2
L4 mitigates #output-encoding mitigates #xss
Asset names and classifications are escaped
1 │ /** 2 │ * GuardLink Dashboard — Asset Risk Heatmap. 3 │ * 4 │ * @mitigates #dashboard against #xss using #output-encoding -- "Asset names and classifications are escaped" 5 │ * @comment -- "Cells keep data-ff-asset (every alias of the tile, |-separated) and the asset drawer; the legend and search text are new" 6 │ */ 7 │ import { esc, scopeLabel, sectionHead, icon } from '../html.js'; 8 │ import type { PageContext } from './context.js'; 9 │
L5 comment Cells keep data-ff-asset (every alias of the tile, |-separated) and the asset drawer; the legend and search text are new
Cells keep data-ff-asset (every alias of the tile, |-separated) and the asset drawer; the legend and search text are new
1 │ /** 2 │ * GuardLink Dashboard — Asset Risk Heatmap. 3 │ * 4 │ * @mitigates #dashboard against #xss using #output-encoding -- "Asset names and classifications are escaped" 5 │ * @comment -- "Cells keep data-ff-asset (every alias of the tile, |-separated) and the asset drawer; the legend and search text are new" 6 │ */ 7 │ import { esc, scopeLabel, sectionHead, icon } from '../html.js'; 8 │ import type { PageContext } from './context.js'; 9 │ 10 │ export function renderAssetsPage(ctx: PageContext): string {
src/dashboard/pages/attribution.ts mitigates 1handles 1comment 1 open 3
L9 mitigates #output-encoding mitigates #xss
Identities, model names, shas, paths and statuses all pass through esc(); a co-author trailer is free text in a commit message and is the realistic vector
4 │ * 5 │ * Rendered only when the model went through `attachBlame`. Every number here 6 │ * is explained once in a reading guide next to it, and every identity is a 7 │ * link that narrows the claims table to that person or tool. 8 │ * 9 │ * @mitigates #dashboard against #xss using #output-encoding -- "Identities, model names, shas, paths and statuses all pass through esc(); a co-author trailer is free text in a commit message and is the realistic vector" 10 │ * @handles pii on #dashboard -- "Author identities rendered into a page that is often committed; blame.identity=hash in config.json is the setting for a shared dashboard" 11 │ * @comment -- "No wall clock: ages are measured to the HEAD commit's date, so two generations at the same HEAD are byte-identical" 12 │ */ 13 │ import { esc, kpi, chip, sortableHead, rowAttrs, numCell, sevBadge, sevRank, locCellShort, whoLink, badge, sectionHead, subHead, plural, num, pager, colgroup, icon, wholeModelNote } from '../html.js'; 14 │ import type { AttributionData } from '../data.js';
L10 handles #dashboard: pii
Author identities rendered into a page that is often committed; blame.identity=hash in config.json is the setting for a shared dashboard
5 │ * Rendered only when the model went through `attachBlame`. Every number here 6 │ * is explained once in a reading guide next to it, and every identity is a 7 │ * link that narrows the claims table to that person or tool. 8 │ * 9 │ * @mitigates #dashboard against #xss using #output-encoding -- "Identities, model names, shas, paths and statuses all pass through esc(); a co-author trailer is free text in a commit message and is the realistic vector" 10 │ * @handles pii on #dashboard -- "Author identities rendered into a page that is often committed; blame.identity=hash in config.json is the setting for a shared dashboard" 11 │ * @comment -- "No wall clock: ages are measured to the HEAD commit's date, so two generations at the same HEAD are byte-identical" 12 │ */ 13 │ import { esc, kpi, chip, sortableHead, rowAttrs, numCell, sevBadge, sevRank, locCellShort, whoLink, badge, sectionHead, subHead, plural, num, pager, colgroup, icon, wholeModelNote } from '../html.js'; 14 │ import type { AttributionData } from '../data.js'; 15 │ import type { PageContext, ClaimView, DrawerRef } from './context.js';
L11 comment No wall clock: ages are measured to the HEAD commit's date, so two generations at the same HEAD are byte-identical
No wall clock: ages are measured to the HEAD commit's date, so two generations at the same HEAD are byte-identical
6 │ * is explained once in a reading guide next to it, and every identity is a 7 │ * link that narrows the claims table to that person or tool. 8 │ * 9 │ * @mitigates #dashboard against #xss using #output-encoding -- "Identities, model names, shas, paths and statuses all pass through esc(); a co-author trailer is free text in a commit message and is the realistic vector" 10 │ * @handles pii on #dashboard -- "Author identities rendered into a page that is often committed; blame.identity=hash in config.json is the setting for a shared dashboard" 11 │ * @comment -- "No wall clock: ages are measured to the HEAD commit's date, so two generations at the same HEAD are byte-identical" 12 │ */ 13 │ import { esc, kpi, chip, sortableHead, rowAttrs, numCell, sevBadge, sevRank, locCellShort, whoLink, badge, sectionHead, subHead, plural, num, pager, colgroup, icon, wholeModelNote } from '../html.js'; 14 │ import type { AttributionData } from '../data.js'; 15 │ import type { PageContext, ClaimView, DrawerRef } from './context.js'; 16 │ import type { Cohort, TrendBucket } from '../../blame/types.js';
src/dashboard/pages/code.ts mitigates 1comment 1 open 2
L11 mitigates #output-encoding mitigates #xss
File paths, summaries, descriptions and code lines are escaped
6 │ * File coverage is a property of the REPOSITORY — annotated files over source 7 │ * files. On a slice the number is either the project's (wrong) or the feature's 8 │ * own files over themselves (100% by construction). Both are withheld and named 9 │ * as withheld, with the command that does answer them. 10 │ * 11 │ * @mitigates #dashboard against #xss using #output-encoding -- "File paths, summaries, descriptions and code lines are escaped" 12 │ * @comment -- "The withheld-on-a-slice sentence and the coverage strings are pinned by tests; file cards gain search text and a host link" 13 │ */ 14 │ import { esc, scopeLabel, sectionHead, subHead, copyButton, plural, icon, badge, sevBadge } from '../html.js'; 15 │ import type { PageContext } from './context.js'; 16 │ import type { FileRisk } from '../analytics.js';
L12 comment The withheld-on-a-slice sentence and the coverage strings are pinned by tests; file cards gain search text and a host link
The withheld-on-a-slice sentence and the coverage strings are pinned by tests; file cards gain search text and a host link
7 │ * files. On a slice the number is either the project's (wrong) or the feature's 8 │ * own files over themselves (100% by construction). Both are withheld and named 9 │ * as withheld, with the command that does answer them. 10 │ * 11 │ * @mitigates #dashboard against #xss using #output-encoding -- "File paths, summaries, descriptions and code lines are escaped" 12 │ * @comment -- "The withheld-on-a-slice sentence and the coverage strings are pinned by tests; file cards gain search text and a host link" 13 │ */ 14 │ import { esc, scopeLabel, sectionHead, subHead, copyButton, plural, icon, badge, sevBadge } from '../html.js'; 15 │ import type { PageContext } from './context.js'; 16 │ import type { FileRisk } from '../analytics.js'; 17 │
src/dashboard/pages/context.ts flow 1handles 1comment 1 open 3
L11 flow ThreatModel → #dashboard
Records joined with coverage, ledger state and attribution into table rows
6 │ * mitigation: its coverage status, its ledger state (verified / stale / 7 │ * unverified, when a ledger exists), and its attribution (when the model went 8 │ * through `attachBlame`). Every claim gets the URL of its file on the repo 9 │ * host when one is known, so the tables and the drawer never build links. 10 │ * 11 │ * @flows ThreatModel -> #dashboard via buildClaims -- "Records joined with coverage, ledger state and attribution into table rows" 12 │ * @handles pii on #dashboard -- "Author identities from attribution, already in the configured identity mode" 13 │ * @comment -- "Pure; every string here is raw and is escaped by the page that renders it" 14 │ */ 15 │ import type { ExposureHypothesis, ThreatModel } from '../../types/index.js'; 16 │ import type { ClaimState, VerificationReport } from '../../parser/verification.js';
L12 handles #dashboard: pii
Author identities from attribution, already in the configured identity mode
7 │ * unverified, when a ledger exists), and its attribution (when the model went 8 │ * through `attachBlame`). Every claim gets the URL of its file on the repo 9 │ * host when one is known, so the tables and the drawer never build links. 10 │ * 11 │ * @flows ThreatModel -> #dashboard via buildClaims -- "Records joined with coverage, ledger state and attribution into table rows" 12 │ * @handles pii on #dashboard -- "Author identities from attribution, already in the configured identity mode" 13 │ * @comment -- "Pure; every string here is raw and is escaped by the page that renders it" 14 │ */ 15 │ import type { ExposureHypothesis, ThreatModel } from '../../types/index.js'; 16 │ import type { ClaimState, VerificationReport } from '../../parser/verification.js'; 17 │ import { buildCoverageIndex } from '../../parser/coverage.js';
L13 comment Pure; every string here is raw and is escaped by the page that renders it
Pure; every string here is raw and is escaped by the page that renders it
8 │ * through `attachBlame`). Every claim gets the URL of its file on the repo 9 │ * host when one is known, so the tables and the drawer never build links. 10 │ * 11 │ * @flows ThreatModel -> #dashboard via buildClaims -- "Records joined with coverage, ledger state and attribution into table rows" 12 │ * @handles pii on #dashboard -- "Author identities from attribution, already in the configured identity mode" 13 │ * @comment -- "Pure; every string here is raw and is escaped by the page that renders it" 14 │ */ 15 │ import type { ExposureHypothesis, ThreatModel } from '../../types/index.js'; 16 │ import type { ClaimState, VerificationReport } from '../../parser/verification.js'; 17 │ import { buildCoverageIndex } from '../../parser/coverage.js'; 18 │ import type { CommitRef, IntroducedBy } from '../../blame/types.js';
src/dashboard/pages/data-boundaries.ts mitigates 1comment 1 open 2
L6 mitigates #output-encoding mitigates #xss
Every cell is escaped, including owner names, rationale and entitlement citations
1 │ /** 2 │ * GuardLink Dashboard — Data & Boundaries: flows, trust boundaries, 3 │ * classifications and every lifecycle annotation, each table sortable and 4 │ * searchable, with jump links at the top. 5 │ * 6 │ * @mitigates #dashboard against #xss using #output-encoding -- "Every cell is escaped, including owner names, rationale and entitlement citations" 7 │ * @comment -- "The empty-state sentence and its condition are unchanged (flows are not part of it) because the feature-slice test pins the sentence" 8 │ */ 9 │ import { esc, scopeLabel, sectionHead, subHead, sortableHead, rowAttrs, locCellShort, descCell, pager, icon } from '../html.js'; 10 │ import type { PageContext } from './context.js'; 11 │
L7 comment The empty-state sentence and its condition are unchanged (flows are not part of it) because the feature-slice test pins the sentence
The empty-state sentence and its condition are unchanged (flows are not part of it) because the feature-slice test pins the sentence
2 │ * GuardLink Dashboard — Data & Boundaries: flows, trust boundaries, 3 │ * classifications and every lifecycle annotation, each table sortable and 4 │ * searchable, with jump links at the top. 5 │ * 6 │ * @mitigates #dashboard against #xss using #output-encoding -- "Every cell is escaped, including owner names, rationale and entitlement citations" 7 │ * @comment -- "The empty-state sentence and its condition are unchanged (flows are not part of it) because the feature-slice test pins the sentence" 8 │ */ 9 │ import { esc, scopeLabel, sectionHead, subHead, sortableHead, rowAttrs, locCellShort, descCell, pager, icon } from '../html.js'; 10 │ import type { PageContext } from './context.js'; 11 │ 12 │ export function renderDataPage(ctx: PageContext): string {
src/dashboard/pages/diagrams.ts mitigates 1comment 1 open 2
L5 mitigates #output-encoding mitigates #xss
Mermaid source is escaped into the <pre>; the client reads it back as text
1 │ /** 2 │ * GuardLink Dashboard — Diagrams: threat graph, data flow, attack surface. 3 │ * Mermaid sources are embedded escaped and rendered client-side. 4 │ * 5 │ * @mitigates #dashboard against #xss using #output-encoding -- "Mermaid source is escaped into the <pre>; the client reads it back as text" 6 │ * @comment -- "Ported from the first generate.ts with the same panel ids and data-variant hooks the diagram script expects; the toolbar is a segmented zoom group plus copy-source" 7 │ */ 8 │ import { esc, scopeLabel, sectionHead, icon, wholeModelNote } from '../html.js'; 9 │ import { 10 │ checkRenderBudget, oversizedStub, describeViolation,
L6 comment Ported from the first generate.ts with the same panel ids and data-variant hooks the diagram script expects; the toolbar is a segmented zoom group plus copy-source
Ported from the first generate.ts with the same panel ids and data-variant hooks the diagram script expects; the toolbar is a segmented zoom group plus copy-source
1 │ /** 2 │ * GuardLink Dashboard — Diagrams: threat graph, data flow, attack surface. 3 │ * Mermaid sources are embedded escaped and rendered client-side. 4 │ * 5 │ * @mitigates #dashboard against #xss using #output-encoding -- "Mermaid source is escaped into the <pre>; the client reads it back as text" 6 │ * @comment -- "Ported from the first generate.ts with the same panel ids and data-variant hooks the diagram script expects; the toolbar is a segmented zoom group plus copy-source" 7 │ */ 8 │ import { esc, scopeLabel, sectionHead, icon, wholeModelNote } from '../html.js'; 9 │ import { 10 │ checkRenderBudget, oversizedStub, describeViolation, 11 │ DASHBOARD_FALLBACK, MERMAID_LIMITS_SOURCE,
src/dashboard/pages/explore.ts comment 2mitigates 1 open 3
L30 mitigates #output-encoding mitigates #xss
Every interpolated value — asset ids, threat names, file paths, descriptions, boundary labels and the Mermaid source itself — goes through esc(); the Mermaid text is escaped into the <pre> and read back by the client as textContent
25 │ * it never said what it left out, so a partial picture read as a whole one. 26 │ * 27 │ * Every panel here states its size against the budget, and every narrowed panel 28 │ * names what fell outside the frame. 29 │ * 30 │ * @mitigates #dashboard against #xss using #output-encoding -- "Every interpolated value — asset ids, threat names, file paths, descriptions, boundary labels and the Mermaid source itself — goes through esc(); the Mermaid text is escaped into the <pre> and read back by the client as textContent" 31 │ * @comment -- "Panes carry .diagram-panel so the existing zoom, find and copy-source controls work unchanged; .explore-pane is what the router shows and hides" 32 │ * @comment -- "Nothing is selected in the browser: buildExploreData has already run every query at generation time, so the page holds answers rather than a query engine" 33 │ */ 34 │ import { 35 │ esc, icon, sectionHead, subHead, badge,
L31 comment Panes carry .diagram-panel so the existing zoom, find and copy-source controls work unchanged; .explore-pane is what the router shows and hides
Panes carry .diagram-panel so the existing zoom, find and copy-source controls work unchanged; .explore-pane is what the router shows and hides
26 │ * 27 │ * Every panel here states its size against the budget, and every narrowed panel 28 │ * names what fell outside the frame. 29 │ * 30 │ * @mitigates #dashboard against #xss using #output-encoding -- "Every interpolated value — asset ids, threat names, file paths, descriptions, boundary labels and the Mermaid source itself — goes through esc(); the Mermaid text is escaped into the <pre> and read back by the client as textContent" 31 │ * @comment -- "Panes carry .diagram-panel so the existing zoom, find and copy-source controls work unchanged; .explore-pane is what the router shows and hides" 32 │ * @comment -- "Nothing is selected in the browser: buildExploreData has already run every query at generation time, so the page holds answers rather than a query engine" 33 │ */ 34 │ import { 35 │ esc, icon, sectionHead, subHead, badge, 36 │ locInline, heatTable, routeWithQuery, plural, wholeModelNote,
L32 comment Nothing is selected in the browser: buildExploreData has already run every query at generation time, so the page holds answers rather than a query engine
Nothing is selected in the browser: buildExploreData has already run every query at generation time, so the page holds answers rather than a query engine
27 │ * Every panel here states its size against the budget, and every narrowed panel 28 │ * names what fell outside the frame. 29 │ * 30 │ * @mitigates #dashboard against #xss using #output-encoding -- "Every interpolated value — asset ids, threat names, file paths, descriptions, boundary labels and the Mermaid source itself — goes through esc(); the Mermaid text is escaped into the <pre> and read back by the client as textContent" 31 │ * @comment -- "Panes carry .diagram-panel so the existing zoom, find and copy-source controls work unchanged; .explore-pane is what the router shows and hides" 32 │ * @comment -- "Nothing is selected in the browser: buildExploreData has already run every query at generation time, so the page holds answers rather than a query engine" 33 │ */ 34 │ import { 35 │ esc, icon, sectionHead, subHead, badge, 36 │ locInline, heatTable, routeWithQuery, plural, wholeModelNote, 37 │ } from '../html.js';
src/dashboard/pages/reports.ts mitigates 1comment 1 open 2
L7 mitigates #output-encoding mitigates #xss
Framework names in the command chips are fixed strings; nothing from a report is interpolated server-side
2 │ * GuardLink Dashboard — Threat Reports: the saved AI analyses, rendered 3 │ * client-side from the embedded markdown by the legacy report explorer, with 4 │ * a toolbar to copy or download the one on screen and to copy the command 5 │ * that writes the next one. 6 │ * 7 │ * @mitigates #dashboard against #xss using #output-encoding -- "Framework names in the command chips are fixed strings; nothing from a report is interpolated server-side" 8 │ * @comment -- "Whole-project documents: --feature does not narrow them, and the page says so on a slice. With no saved report the page renders its own empty state so the copy buttons exist without the client" 9 │ */ 10 │ import { esc, scopeLabel, sectionHead, copyButton, icon } from '../html.js'; 11 │ import type { PageContext } from './context.js'; 12 │
L8 comment Whole-project documents: --feature does not narrow them, and the page says so on a slice. With no saved report the page renders its own empty state so the copy buttons exist without the client
Whole-project documents: --feature does not narrow them, and the page says so on a slice. With no saved report the page renders its own empty state so the copy buttons exist without the client
3 │ * client-side from the embedded markdown by the legacy report explorer, with 4 │ * a toolbar to copy or download the one on screen and to copy the command 5 │ * that writes the next one. 6 │ * 7 │ * @mitigates #dashboard against #xss using #output-encoding -- "Framework names in the command chips are fixed strings; nothing from a report is interpolated server-side" 8 │ * @comment -- "Whole-project documents: --feature does not narrow them, and the page says so on a slice. With no saved report the page renders its own empty state so the copy buttons exist without the client" 9 │ */ 10 │ import { esc, scopeLabel, sectionHead, copyButton, icon } from '../html.js'; 11 │ import type { PageContext } from './context.js'; 12 │ 13 │ const FRAMEWORKS = ['stride', 'dread', 'pasta', 'attacker', 'rapid', 'general'];
src/dashboard/pages/summary.ts mitigates 1comment 1 open 2
L9 mitigates #output-encoding mitigates #xss
Every model value, action text and identity is rendered through esc(); hrefs are attribute-escaped
4 │ * Grade, five numbers that each open the view behind them, the computed 5 │ * "What to do next" list, coverage and severity, a digest of the worst open 6 │ * exposures, an attribution teaser when the model carries blame, and the 7 │ * inventory of everything else as small linked tiles. 8 │ * 9 │ * @mitigates #dashboard against #xss using #output-encoding -- "Every model value, action text and identity is rendered through esc(); hrefs are attribute-escaped" 10 │ * @comment -- "The Open Threats tile keeps its exact markup and label, and the coverage panel keeps .coverage-pct / .posture-fill / the 'exposures mitigated' sentence: the client feature filter rewrites those by label, and a test greps the tile" 11 │ */ 12 │ import { esc, kpi, statCard, sevBadge, sevRank, scopeLabel, sectionHead, subHead, copyButton, locInline, whoLink, plural, normSev, icon, routeWithQuery } from '../html.js'; 13 │ import type { PageContext } from './context.js'; 14 │ import type { ChangeSummary, ChangedClaim } from '../analytics.js';
L10 comment The Open Threats tile keeps its exact markup and label, and the coverage panel keeps .coverage-pct / .posture-fill / the 'exposures mitigated' sentence: the client feature filter rewrites those by label, and a test greps the tile
The Open Threats tile keeps its exact markup and label, and the coverage panel keeps .coverage-pct / .posture-fill / the 'exposures mitigated' sentence: the client feature filter rewrites those by label, and a test greps the tile
5 │ * "What to do next" list, coverage and severity, a digest of the worst open 6 │ * exposures, an attribution teaser when the model carries blame, and the 7 │ * inventory of everything else as small linked tiles. 8 │ * 9 │ * @mitigates #dashboard against #xss using #output-encoding -- "Every model value, action text and identity is rendered through esc(); hrefs are attribute-escaped" 10 │ * @comment -- "The Open Threats tile keeps its exact markup and label, and the coverage panel keeps .coverage-pct / .posture-fill / the 'exposures mitigated' sentence: the client feature filter rewrites those by label, and a test greps the tile" 11 │ */ 12 │ import { esc, kpi, statCard, sevBadge, sevRank, scopeLabel, sectionHead, subHead, copyButton, locInline, whoLink, plural, normSev, icon, routeWithQuery } from '../html.js'; 13 │ import type { PageContext } from './context.js'; 14 │ import type { ChangeSummary, ChangedClaim } from '../analytics.js'; 15 │
src/dashboard/pages/threats.ts mitigates 1comment 1 open 2
L11 mitigates #output-encoding mitigates #xss
Every cell, attribute and identity is escaped; the search text is escaped as an attribute value
6 │ * `data-status`, `data-state`, `data-who`, `data-search`) and open the claim 7 │ * drawer through `data-claim`. Tables are fixed-layout so a long description 8 │ * or path clamps instead of stretching the row; the full text stays in the 9 │ * cell title and the drawer. 10 │ * 11 │ * @mitigates #dashboard against #xss using #output-encoding -- "Every cell, attribute and identity is escaped; the search text is escaped as an attribute value" 12 │ * @comment -- "Scope wording lives in a note, not in headings: the client feature filter rewrites headings by textContent" 13 │ */ 14 │ import { esc, chip, sortableHead, rowAttrs, sevBadge, sevRank, numCell, locCellShort, claimStateBadge, whoLink, badge, scopeLabel, sectionHead, subHead, normSev, descCell, colgroup, pager, icon } from '../html.js'; 15 │ import type { PageContext, ClaimView } from './context.js'; 16 │
L12 comment Scope wording lives in a note, not in headings: the client feature filter rewrites headings by textContent
Scope wording lives in a note, not in headings: the client feature filter rewrites headings by textContent
7 │ * drawer through `data-claim`. Tables are fixed-layout so a long description 8 │ * or path clamps instead of stretching the row; the full text stays in the 9 │ * cell title and the drawer. 10 │ * 11 │ * @mitigates #dashboard against #xss using #output-encoding -- "Every cell, attribute and identity is escaped; the search text is escaped as an attribute value" 12 │ * @comment -- "Scope wording lives in a note, not in headings: the client feature filter rewrites headings by textContent" 13 │ */ 14 │ import { esc, chip, sortableHead, rowAttrs, sevBadge, sevRank, numCell, locCellShort, claimStateBadge, whoLink, badge, scopeLabel, sectionHead, subHead, normSev, descCell, colgroup, pager, icon } from '../html.js'; 15 │ import type { PageContext, ClaimView } from './context.js'; 16 │ 17 │ const STATUS_LABEL: Record<string, string> = { open: 'Open', mitigated: 'Mitigated', accepted: 'Accepted', confirmed: 'Confirmed', control: 'Control', refuted: 'Refuted' };
src/dashboard/render-budget.ts exposes 1mitigates 1flow 1comment 1 open 4
L57 exposes #dashboard → #dos
Measures caller-supplied diagram text with regular expressions
52 │ * 53 │ * Raising them would not help anyway. The measured 257-file threat graph renders 54 │ * at 39,609 × 32,646 px with 0.2% of it visible at the fitted zoom; drawing more 55 │ * of that is not the same as showing it. 56 │ * 57 │ * @exposes #dashboard to #dos [low] -- "Measures caller-supplied diagram text with regular expressions" 58 │ * @mitigates #dashboard against #dos using #regex-anchoring -- "Every pattern is line-anchored or a bounded character class; no nested quantifier can backtrack across the input" 59 │ * @flows DiagramSource -> #dashboard via checkRenderBudget -- "Generated Mermaid text measured before it is written or served" 60 │ * @comment -- "The limits are Mermaid's shipped defaults, read out of mermaid@11.17.2's bundle and cited above — deriving them rather than guessing is the whole point of the module" 61 │ */ 62 │
L58 mitigates #regex-anchoring mitigates #dos
Every pattern is line-anchored or a bounded character class; no nested quantifier can backtrack across the input
53 │ * Raising them would not help anyway. The measured 257-file threat graph renders 54 │ * at 39,609 × 32,646 px with 0.2% of it visible at the fitted zoom; drawing more 55 │ * of that is not the same as showing it. 56 │ * 57 │ * @exposes #dashboard to #dos [low] -- "Measures caller-supplied diagram text with regular expressions" 58 │ * @mitigates #dashboard against #dos using #regex-anchoring -- "Every pattern is line-anchored or a bounded character class; no nested quantifier can backtrack across the input" 59 │ * @flows DiagramSource -> #dashboard via checkRenderBudget -- "Generated Mermaid text measured before it is written or served" 60 │ * @comment -- "The limits are Mermaid's shipped defaults, read out of mermaid@11.17.2's bundle and cited above — deriving them rather than guessing is the whole point of the module" 61 │ */ 62 │ 63 │ /**
L59 flow DiagramSource → #dashboard
Generated Mermaid text measured before it is written or served
54 │ * at 39,609 × 32,646 px with 0.2% of it visible at the fitted zoom; drawing more 55 │ * of that is not the same as showing it. 56 │ * 57 │ * @exposes #dashboard to #dos [low] -- "Measures caller-supplied diagram text with regular expressions" 58 │ * @mitigates #dashboard against #dos using #regex-anchoring -- "Every pattern is line-anchored or a bounded character class; no nested quantifier can backtrack across the input" 59 │ * @flows DiagramSource -> #dashboard via checkRenderBudget -- "Generated Mermaid text measured before it is written or served" 60 │ * @comment -- "The limits are Mermaid's shipped defaults, read out of mermaid@11.17.2's bundle and cited above — deriving them rather than guessing is the whole point of the module" 61 │ */ 62 │ 63 │ /** 64 │ * Mermaid's own limits, at the version the dashboard loads.
L60 comment The limits are Mermaid's shipped defaults, read out of mermaid@11.17.2's bundle and cited above — deriving them rather than guessing is the whole point of the module
The limits are Mermaid's shipped defaults, read out of mermaid@11.17.2's bundle and cited above — deriving them rather than guessing is the whole point of the module
55 │ * of that is not the same as showing it. 56 │ * 57 │ * @exposes #dashboard to #dos [low] -- "Measures caller-supplied diagram text with regular expressions" 58 │ * @mitigates #dashboard against #dos using #regex-anchoring -- "Every pattern is line-anchored or a bounded character class; no nested quantifier can backtrack across the input" 59 │ * @flows DiagramSource -> #dashboard via checkRenderBudget -- "Generated Mermaid text measured before it is written or served" 60 │ * @comment -- "The limits are Mermaid's shipped defaults, read out of mermaid@11.17.2's bundle and cited above — deriving them rather than guessing is the whole point of the module" 61 │ */ 62 │ 63 │ /** 64 │ * Mermaid's own limits, at the version the dashboard loads. 65 │ *
src/dashboard/since.ts exposes 1mitigates 1flow 1comment 1 open 4
L6 exposes #dashboard → #cmd-injection
loadSince() passes the --since ref the user typed to parseAtRef() and into the git log and git rev-list argv
1 │ /** 2 │ * GuardLink Dashboard — `--since <ref>`: the model at a git ref, diffed 3 │ * against the model being rendered, plus the ref's place in history. The 4 │ * page turns it into the "what changed" strip and the "new" marks on rows. 5 │ * 6 │ * @exposes #dashboard to #cmd-injection [low] cwe:CWE-78 -- "loadSince() passes the --since ref the user typed to parseAtRef() and into the git log and git rev-list argv" 7 │ * @mitigates #dashboard against #cmd-injection using #param-commands -- "Checked against a strict ref shape first, then only ever passed as one argv element to execFileSync; parseAtRef rev-parses it before reading anything" 8 │ * @flows GitRepo -> #dashboard via parseAtRef -- "The threat model as it was at the ref" 9 │ * @comment -- "Deterministic per ref: the ref's commit date and the commit count are facts of history, so two runs at the same HEAD produce the same page" 10 │ */ 11 │ import { execFileSync } from 'node:child_process';
L7 mitigates #param-commands mitigates #cmd-injection
Checked against a strict ref shape first, then only ever passed as one argv element to execFileSync; parseAtRef rev-parses it before reading anything
2 │ * GuardLink Dashboard — `--since <ref>`: the model at a git ref, diffed 3 │ * against the model being rendered, plus the ref's place in history. The 4 │ * page turns it into the "what changed" strip and the "new" marks on rows. 5 │ * 6 │ * @exposes #dashboard to #cmd-injection [low] cwe:CWE-78 -- "loadSince() passes the --since ref the user typed to parseAtRef() and into the git log and git rev-list argv" 7 │ * @mitigates #dashboard against #cmd-injection using #param-commands -- "Checked against a strict ref shape first, then only ever passed as one argv element to execFileSync; parseAtRef rev-parses it before reading anything" 8 │ * @flows GitRepo -> #dashboard via parseAtRef -- "The threat model as it was at the ref" 9 │ * @comment -- "Deterministic per ref: the ref's commit date and the commit count are facts of history, so two runs at the same HEAD produce the same page" 10 │ */ 11 │ import { execFileSync } from 'node:child_process'; 12 │ import type { ThreatModel } from '../types/index.js';
L8 flow GitRepo → #dashboard
The threat model as it was at the ref
3 │ * against the model being rendered, plus the ref's place in history. The 4 │ * page turns it into the "what changed" strip and the "new" marks on rows. 5 │ * 6 │ * @exposes #dashboard to #cmd-injection [low] cwe:CWE-78 -- "loadSince() passes the --since ref the user typed to parseAtRef() and into the git log and git rev-list argv" 7 │ * @mitigates #dashboard against #cmd-injection using #param-commands -- "Checked against a strict ref shape first, then only ever passed as one argv element to execFileSync; parseAtRef rev-parses it before reading anything" 8 │ * @flows GitRepo -> #dashboard via parseAtRef -- "The threat model as it was at the ref" 9 │ * @comment -- "Deterministic per ref: the ref's commit date and the commit count are facts of history, so two runs at the same HEAD produce the same page" 10 │ */ 11 │ import { execFileSync } from 'node:child_process'; 12 │ import type { ThreatModel } from '../types/index.js'; 13 │ import { parseAtRef, getChangedFiles, diffModels } from '../diff/index.js';
L9 comment Deterministic per ref: the ref's commit date and the commit count are facts of history, so two runs at the same HEAD produce the same page
Deterministic per ref: the ref's commit date and the commit count are facts of history, so two runs at the same HEAD produce the same page
4 │ * page turns it into the "what changed" strip and the "new" marks on rows. 5 │ * 6 │ * @exposes #dashboard to #cmd-injection [low] cwe:CWE-78 -- "loadSince() passes the --since ref the user typed to parseAtRef() and into the git log and git rev-list argv" 7 │ * @mitigates #dashboard against #cmd-injection using #param-commands -- "Checked against a strict ref shape first, then only ever passed as one argv element to execFileSync; parseAtRef rev-parses it before reading anything" 8 │ * @flows GitRepo -> #dashboard via parseAtRef -- "The threat model as it was at the ref" 9 │ * @comment -- "Deterministic per ref: the ref's commit date and the commit count are facts of history, so two runs at the same HEAD produce the same page" 10 │ */ 11 │ import { execFileSync } from 'node:child_process'; 12 │ import type { ThreatModel } from '../types/index.js'; 13 │ import { parseAtRef, getChangedFiles, diffModels } from '../diff/index.js'; 14 │ import type { SinceInput } from './analytics.js';
src/dashboard/styles.ts comment 1 open 1
L9 comment Static CSS only; nothing here interpolates model data
Static CSS only; nothing here interpolates model data
4 │ * BASE_CSS is the sheet the dashboard shipped with (tokens, layout, drawer, 5 │ * diagrams, code cards, heatmap, markdown). UPGRADE_CSS layers the new 6 │ * components on top and tightens type and spacing; it is appended after, so 7 │ * its rules win where they overlap. 8 │ * 9 │ * @comment -- "Static CSS only; nothing here interpolates model data" 10 │ */ 11 │ export const BASE_CSS = ` 12 │ /* ── Reset ── */ 13 │ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } 14 │ *::selection { background: color-mix(in oklab, var(--accent) 35%, transparent); color: var(--text); }
src/diff/engine.ts comment 1flow 1 open 2
L15 comment Pure model-vs-model comparator; no I/O. Entitlement staleness is the one thing it cannot derive from the two models, so the changed-file list is passed in by the caller (see getChangedFiles in git.ts)
Pure model-vs-model comparator; no I/O. Entitlement staleness is the one thing it cannot derive from the two models, so the changed-file list is passed in by the caller (see getChangedFiles in git.ts)
10 │ * - Identity keys: assets by path/id, threats/controls by id/canonical_name, 11 │ * relationships by (asset, threat) or (source, target) composite keys 12 │ * - Delta categories: added, removed, modified (severity/description changed) 13 │ * - Risk delta: tracks net change in unmitigated exposure count 14 │ * 15 │ * @comment -- "Pure model-vs-model comparator; no I/O. Entitlement staleness is the one thing it cannot derive from the two models, so the changed-file list is passed in by the caller (see getChangedFiles in git.ts)" 16 │ * @flows ThreatModel -> #diff via diffModels -- "Before/after models compared into a structured delta" 17 │ */ 18 │ 19 │ import type { 20 │ ThreatModel,
L16 flow ThreatModel → #diff
Before/after models compared into a structured delta
11 │ * relationships by (asset, threat) or (source, target) composite keys 12 │ * - Delta categories: added, removed, modified (severity/description changed) 13 │ * - Risk delta: tracks net change in unmitigated exposure count 14 │ * 15 │ * @comment -- "Pure model-vs-model comparator; no I/O. Entitlement staleness is the one thing it cannot derive from the two models, so the changed-file list is passed in by the caller (see getChangedFiles in git.ts)" 16 │ * @flows ThreatModel -> #diff via diffModels -- "Before/after models compared into a structured delta" 17 │ */ 18 │ 19 │ import type { 20 │ ThreatModel, 21 │ ThreatModelAsset, ThreatModelThreat, ThreatModelControl, ThreatModelActor,
src/diff/format.ts comment 1 open 1
L4 comment Pure string formatting; no I/O. Stale entitlements print even when the delta is otherwise empty — a claim whose cited authz code moved is exactly the thing that must not disappear quietly (actor-entitlement design §3.3)
Pure string formatting; no I/O. Stale entitlements print even when the delta is otherwise empty — a claim whose cited authz code moved is exactly the thing that must not disappear quietly (actor-entitlement design §3.3)
1 │ /** 2 │ * GuardLink Diff — Human-readable output formatter. 3 │ * 4 │ * @comment -- "Pure string formatting; no I/O. Stale entitlements print even when the delta is otherwise empty — a claim whose cited authz code moved is exactly the thing that must not disappear quietly (actor-entitlement design §3.3)" 5 │ */ 6 │ 7 │ import type { ThreatModelDiff, Change } from './engine.js'; 8 │ 9 │ export function formatDiff(diff: ThreatModelDiff): string {
src/diff/git.ts flow 5exposes 4mitigates 4boundary 1 open 14
L6 exposes #diff → #cmd-injection
execSync runs git commands with ref argument
1 │ /** 2 │ * GuardLink Diff — Git integration. 3 │ * Resolves git refs to threat models by checking out files at a given commit 4 │ * and parsing them in a temp directory. 5 │ * 6 │ * @exposes #diff to #cmd-injection [high] cwe:CWE-78 -- "execSync runs git commands with ref argument" 7 │ * @mitigates #diff against #cmd-injection using #input-sanitize -- "rev-parse validates ref exists before use in other commands" 8 │ * @exposes #diff to #arbitrary-write [medium] cwe:CWE-73 -- "writeFileSync creates files in temp directory" 9 │ * @mitigates #diff against #arbitrary-write using #path-validation -- "mkdtempSync creates isolated temp dir; rmSync cleans up" 10 │ * @exposes #diff to #path-traversal [medium] cwe:CWE-22 -- "git show extracts files based on ls-tree output" 11 │ * @mitigates #diff against #path-traversal using #glob-filtering -- "Files constrained to relevantFiles from git ls-tree"
L7 mitigates #input-sanitize mitigates #cmd-injection
rev-parse validates ref exists before use in other commands
2 │ * GuardLink Diff — Git integration. 3 │ * Resolves git refs to threat models by checking out files at a given commit 4 │ * and parsing them in a temp directory. 5 │ * 6 │ * @exposes #diff to #cmd-injection [high] cwe:CWE-78 -- "execSync runs git commands with ref argument" 7 │ * @mitigates #diff against #cmd-injection using #input-sanitize -- "rev-parse validates ref exists before use in other commands" 8 │ * @exposes #diff to #arbitrary-write [medium] cwe:CWE-73 -- "writeFileSync creates files in temp directory" 9 │ * @mitigates #diff against #arbitrary-write using #path-validation -- "mkdtempSync creates isolated temp dir; rmSync cleans up" 10 │ * @exposes #diff to #path-traversal [medium] cwe:CWE-22 -- "git show extracts files based on ls-tree output" 11 │ * @mitigates #diff against #path-traversal using #glob-filtering -- "Files constrained to relevantFiles from git ls-tree" 12 │ * @flows GitRef -> #diff via execSync -- "Git command execution"
L8 exposes #diff → #arbitrary-write
writeFileSync creates files in temp directory
3 │ * Resolves git refs to threat models by checking out files at a given commit 4 │ * and parsing them in a temp directory. 5 │ * 6 │ * @exposes #diff to #cmd-injection [high] cwe:CWE-78 -- "execSync runs git commands with ref argument" 7 │ * @mitigates #diff against #cmd-injection using #input-sanitize -- "rev-parse validates ref exists before use in other commands" 8 │ * @exposes #diff to #arbitrary-write [medium] cwe:CWE-73 -- "writeFileSync creates files in temp directory" 9 │ * @mitigates #diff against #arbitrary-write using #path-validation -- "mkdtempSync creates isolated temp dir; rmSync cleans up" 10 │ * @exposes #diff to #path-traversal [medium] cwe:CWE-22 -- "git show extracts files based on ls-tree output" 11 │ * @mitigates #diff against #path-traversal using #glob-filtering -- "Files constrained to relevantFiles from git ls-tree" 12 │ * @flows GitRef -> #diff via execSync -- "Git command execution" 13 │ * @flows #diff -> TempDir via writeFileSync -- "Extracted file writes"
L9 mitigates #path-validation mitigates #arbitrary-write
mkdtempSync creates isolated temp dir; rmSync cleans up
4 │ * and parsing them in a temp directory. 5 │ * 6 │ * @exposes #diff to #cmd-injection [high] cwe:CWE-78 -- "execSync runs git commands with ref argument" 7 │ * @mitigates #diff against #cmd-injection using #input-sanitize -- "rev-parse validates ref exists before use in other commands" 8 │ * @exposes #diff to #arbitrary-write [medium] cwe:CWE-73 -- "writeFileSync creates files in temp directory" 9 │ * @mitigates #diff against #arbitrary-write using #path-validation -- "mkdtempSync creates isolated temp dir; rmSync cleans up" 10 │ * @exposes #diff to #path-traversal [medium] cwe:CWE-22 -- "git show extracts files based on ls-tree output" 11 │ * @mitigates #diff against #path-traversal using #glob-filtering -- "Files constrained to relevantFiles from git ls-tree" 12 │ * @flows GitRef -> #diff via execSync -- "Git command execution" 13 │ * @flows #diff -> TempDir via writeFileSync -- "Extracted file writes" 14 │ * @flows #diff -> ThreatModel via parseProject -- "Parsed model output"
L10 exposes #diff → #path-traversal
git show extracts files based on ls-tree output
5 │ * 6 │ * @exposes #diff to #cmd-injection [high] cwe:CWE-78 -- "execSync runs git commands with ref argument" 7 │ * @mitigates #diff against #cmd-injection using #input-sanitize -- "rev-parse validates ref exists before use in other commands" 8 │ * @exposes #diff to #arbitrary-write [medium] cwe:CWE-73 -- "writeFileSync creates files in temp directory" 9 │ * @mitigates #diff against #arbitrary-write using #path-validation -- "mkdtempSync creates isolated temp dir; rmSync cleans up" 10 │ * @exposes #diff to #path-traversal [medium] cwe:CWE-22 -- "git show extracts files based on ls-tree output" 11 │ * @mitigates #diff against #path-traversal using #glob-filtering -- "Files constrained to relevantFiles from git ls-tree" 12 │ * @flows GitRef -> #diff via execSync -- "Git command execution" 13 │ * @flows #diff -> TempDir via writeFileSync -- "Extracted file writes" 14 │ * @flows #diff -> ThreatModel via parseProject -- "Parsed model output" 15 │ * @boundary #diff and GitRepo (#git-boundary) -- "Trust boundary at git command execution"
L11 mitigates #glob-filtering mitigates #path-traversal
Files constrained to relevantFiles from git ls-tree
6 │ * @exposes #diff to #cmd-injection [high] cwe:CWE-78 -- "execSync runs git commands with ref argument" 7 │ * @mitigates #diff against #cmd-injection using #input-sanitize -- "rev-parse validates ref exists before use in other commands" 8 │ * @exposes #diff to #arbitrary-write [medium] cwe:CWE-73 -- "writeFileSync creates files in temp directory" 9 │ * @mitigates #diff against #arbitrary-write using #path-validation -- "mkdtempSync creates isolated temp dir; rmSync cleans up" 10 │ * @exposes #diff to #path-traversal [medium] cwe:CWE-22 -- "git show extracts files based on ls-tree output" 11 │ * @mitigates #diff against #path-traversal using #glob-filtering -- "Files constrained to relevantFiles from git ls-tree" 12 │ * @flows GitRef -> #diff via execSync -- "Git command execution" 13 │ * @flows #diff -> TempDir via writeFileSync -- "Extracted file writes" 14 │ * @flows #diff -> ThreatModel via parseProject -- "Parsed model output" 15 │ * @boundary #diff and GitRepo (#git-boundary) -- "Trust boundary at git command execution" 16 │ */
L12 flow GitRef → #diff
Git command execution
7 │ * @mitigates #diff against #cmd-injection using #input-sanitize -- "rev-parse validates ref exists before use in other commands" 8 │ * @exposes #diff to #arbitrary-write [medium] cwe:CWE-73 -- "writeFileSync creates files in temp directory" 9 │ * @mitigates #diff against #arbitrary-write using #path-validation -- "mkdtempSync creates isolated temp dir; rmSync cleans up" 10 │ * @exposes #diff to #path-traversal [medium] cwe:CWE-22 -- "git show extracts files based on ls-tree output" 11 │ * @mitigates #diff against #path-traversal using #glob-filtering -- "Files constrained to relevantFiles from git ls-tree" 12 │ * @flows GitRef -> #diff via execSync -- "Git command execution" 13 │ * @flows #diff -> TempDir via writeFileSync -- "Extracted file writes" 14 │ * @flows #diff -> ThreatModel via parseProject -- "Parsed model output" 15 │ * @boundary #diff and GitRepo (#git-boundary) -- "Trust boundary at git command execution" 16 │ */ 17 │
L13 flow #diff → TempDir
Extracted file writes
8 │ * @exposes #diff to #arbitrary-write [medium] cwe:CWE-73 -- "writeFileSync creates files in temp directory" 9 │ * @mitigates #diff against #arbitrary-write using #path-validation -- "mkdtempSync creates isolated temp dir; rmSync cleans up" 10 │ * @exposes #diff to #path-traversal [medium] cwe:CWE-22 -- "git show extracts files based on ls-tree output" 11 │ * @mitigates #diff against #path-traversal using #glob-filtering -- "Files constrained to relevantFiles from git ls-tree" 12 │ * @flows GitRef -> #diff via execSync -- "Git command execution" 13 │ * @flows #diff -> TempDir via writeFileSync -- "Extracted file writes" 14 │ * @flows #diff -> ThreatModel via parseProject -- "Parsed model output" 15 │ * @boundary #diff and GitRepo (#git-boundary) -- "Trust boundary at git command execution" 16 │ */ 17 │ 18 │ import { execSync } from 'node:child_process';
L14 flow #diff → ThreatModel
Parsed model output
9 │ * @mitigates #diff against #arbitrary-write using #path-validation -- "mkdtempSync creates isolated temp dir; rmSync cleans up" 10 │ * @exposes #diff to #path-traversal [medium] cwe:CWE-22 -- "git show extracts files based on ls-tree output" 11 │ * @mitigates #diff against #path-traversal using #glob-filtering -- "Files constrained to relevantFiles from git ls-tree" 12 │ * @flows GitRef -> #diff via execSync -- "Git command execution" 13 │ * @flows #diff -> TempDir via writeFileSync -- "Extracted file writes" 14 │ * @flows #diff -> ThreatModel via parseProject -- "Parsed model output" 15 │ * @boundary #diff and GitRepo (#git-boundary) -- "Trust boundary at git command execution" 16 │ */ 17 │ 18 │ import { execSync } from 'node:child_process'; 19 │ import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
L15 boundary #diff ↔ GitRepo
Trust boundary at git command execution
10 │ * @exposes #diff to #path-traversal [medium] cwe:CWE-22 -- "git show extracts files based on ls-tree output" 11 │ * @mitigates #diff against #path-traversal using #glob-filtering -- "Files constrained to relevantFiles from git ls-tree" 12 │ * @flows GitRef -> #diff via execSync -- "Git command execution" 13 │ * @flows #diff -> TempDir via writeFileSync -- "Extracted file writes" 14 │ * @flows #diff -> ThreatModel via parseProject -- "Parsed model output" 15 │ * @boundary #diff and GitRepo (#git-boundary) -- "Trust boundary at git command execution" 16 │ */ 17 │ 18 │ import { execSync } from 'node:child_process'; 19 │ import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs'; 20 │ import { join } from 'node:path';
L92 exposes #diff → #cmd-injection
ref is interpolated into an execSync git command
87 │ * Feeds `diffModels({ changedFiles })` so an @entitles whose cited authorization 88 │ * code moved is reported as stale (actor-entitlement design §3.7). Returns [] on 89 │ * any git failure — staleness is advisory, and a diff that cannot resolve the ref 90 │ * should still report the rest of the delta. 91 │ * 92 │ * @exposes #diff to #cmd-injection [high] cwe:CWE-78 -- "ref is interpolated into an execSync git command" 93 │ * @mitigates #diff against #cmd-injection using #input-sanitize -- "rev-parse --verify must resolve ref to a single revision before it reaches the diff command; a shell metacharacter makes rev-parse fail, so the function returns [] instead of running the second command" 94 │ * @flows GitRef -> #diff via execSync -- "Ref input to git diff --name-only" 95 │ * @flows #diff -> ChangedFileList via return -- "Repo-relative paths used for entitlement staleness" 96 │ */ 97 │ export function getChangedFiles(root: string, ref: string): string[] {
L93 mitigates #input-sanitize mitigates #cmd-injection
rev-parse --verify must resolve ref to a single revision before it reaches the diff command; a shell metacharacter makes rev-parse fail, so the function returns [] instead of running the second command
88 │ * code moved is reported as stale (actor-entitlement design §3.7). Returns [] on 89 │ * any git failure — staleness is advisory, and a diff that cannot resolve the ref 90 │ * should still report the rest of the delta. 91 │ * 92 │ * @exposes #diff to #cmd-injection [high] cwe:CWE-78 -- "ref is interpolated into an execSync git command" 93 │ * @mitigates #diff against #cmd-injection using #input-sanitize -- "rev-parse --verify must resolve ref to a single revision before it reaches the diff command; a shell metacharacter makes rev-parse fail, so the function returns [] instead of running the second command" 94 │ * @flows GitRef -> #diff via execSync -- "Ref input to git diff --name-only" 95 │ * @flows #diff -> ChangedFileList via return -- "Repo-relative paths used for entitlement staleness" 96 │ */ 97 │ export function getChangedFiles(root: string, ref: string): string[] { 98 │ try {
L94 flow GitRef → #diff
Ref input to git diff --name-only
89 │ * any git failure — staleness is advisory, and a diff that cannot resolve the ref 90 │ * should still report the rest of the delta. 91 │ * 92 │ * @exposes #diff to #cmd-injection [high] cwe:CWE-78 -- "ref is interpolated into an execSync git command" 93 │ * @mitigates #diff against #cmd-injection using #input-sanitize -- "rev-parse --verify must resolve ref to a single revision before it reaches the diff command; a shell metacharacter makes rev-parse fail, so the function returns [] instead of running the second command" 94 │ * @flows GitRef -> #diff via execSync -- "Ref input to git diff --name-only" 95 │ * @flows #diff -> ChangedFileList via return -- "Repo-relative paths used for entitlement staleness" 96 │ */ 97 │ export function getChangedFiles(root: string, ref: string): string[] { 98 │ try { 99 │ execSync(`git rev-parse --verify ${ref}`, { cwd: root, stdio: 'pipe' });
L95 flow #diff → ChangedFileList
Repo-relative paths used for entitlement staleness
90 │ * should still report the rest of the delta. 91 │ * 92 │ * @exposes #diff to #cmd-injection [high] cwe:CWE-78 -- "ref is interpolated into an execSync git command" 93 │ * @mitigates #diff against #cmd-injection using #input-sanitize -- "rev-parse --verify must resolve ref to a single revision before it reaches the diff command; a shell metacharacter makes rev-parse fail, so the function returns [] instead of running the second command" 94 │ * @flows GitRef -> #diff via execSync -- "Ref input to git diff --name-only" 95 │ * @flows #diff -> ChangedFileList via return -- "Repo-relative paths used for entitlement staleness" 96 │ */ 97 │ export function getChangedFiles(root: string, ref: string): string[] { 98 │ try { 99 │ execSync(`git rev-parse --verify ${ref}`, { cwd: root, stdio: 'pipe' }); 100 │ const raw = execSync(`git diff --name-only ${ref} --`, { cwd: root, encoding: 'utf-8' });
src/diff/index.ts exposes 1audit 1flow 1 open 3
L4 exposes #diff → #cmd-injection
git.ts uses execSync with ref argument
1 │ /** 2 │ * GuardLink Diff — exports. 3 │ * 4 │ * @exposes #diff to #cmd-injection [high] cwe:CWE-78 -- "git.ts uses execSync with ref argument" 5 │ * @audit #diff -- "Git commands use execSync; ref is validated with rev-parse before use" 6 │ * @flows GitRef -> #diff via parseAtRef -- "Git reference input" 7 │ */ 8 │ 9 │ export { diffModels, type ThreatModelDiff, type DiffSummary, type Change, type ChangeKind, type DiffOptions, type StaleEntitlement } from './engine.js';
L5 audit Audit: #diff
Git commands use execSync; ref is validated with rev-parse before use
1 │ /** 2 │ * GuardLink Diff — exports. 3 │ * 4 │ * @exposes #diff to #cmd-injection [high] cwe:CWE-78 -- "git.ts uses execSync with ref argument" 5 │ * @audit #diff -- "Git commands use execSync; ref is validated with rev-parse before use" 6 │ * @flows GitRef -> #diff via parseAtRef -- "Git reference input" 7 │ */ 8 │ 9 │ export { diffModels, type ThreatModelDiff, type DiffSummary, type Change, type ChangeKind, type DiffOptions, type StaleEntitlement } from './engine.js'; 10 │ export { formatDiff, formatDiffMarkdown } from './format.js';
L6 flow GitRef → #diff
Git reference input
1 │ /** 2 │ * GuardLink Diff — exports. 3 │ * 4 │ * @exposes #diff to #cmd-injection [high] cwe:CWE-78 -- "git.ts uses execSync with ref argument" 5 │ * @audit #diff -- "Git commands use execSync; ref is validated with rev-parse before use" 6 │ * @flows GitRef -> #diff via parseAtRef -- "Git reference input" 7 │ */ 8 │ 9 │ export { diffModels, type ThreatModelDiff, type DiffSummary, type Change, type ChangeKind, type DiffOptions, type StaleEntitlement } from './engine.js'; 10 │ export { formatDiff, formatDiffMarkdown } from './format.js'; 11 │ export { parseAtRef, getCurrentRef, getChangedFiles } from './git.js';
src/gate/gate.ts flow 3exposes 1mitigates 1comment 1 open 6
L11 exposes #gate → #arbitrary-write
stripViolations rewrites source files named by the model's locations
6 │ * to find what the run added, lints only those, and reports. The follow-up is 7 │ * the re-prompt. The strip removes the lines of added claims that still fail, 8 │ * after checking each line really holds the verb, and returns them so nothing 9 │ * is lost. 10 │ * 11 │ * @exposes #gate to #arbitrary-write [medium] cwe:CWE-73 -- "stripViolations rewrites source files named by the model's locations" 12 │ * @mitigates #gate against #arbitrary-write using #path-validation -- "Every path is resolved under root and refused outside it; a line is removed only when its text carries the annotation verb the model says is there" 13 │ * @flows ThreatModel -> #gate via runGate -- "The model before and after an agent run" 14 │ * @flows #gate -> AgentPrompt via buildGateFollowUp -- "The violations as a re-prompt" 15 │ * @flows #gate -> SourceFiles via stripViolations -- "Rejected annotation lines removed" 16 │ * @comment -- "Never edits an annotation's text: a rejected claim is re-prompted, then removed; rewriting would hide that it was wrong"
L12 mitigates #path-validation mitigates #arbitrary-write
Every path is resolved under root and refused outside it; a line is removed only when its text carries the annotation verb the model says is there
7 │ * the re-prompt. The strip removes the lines of added claims that still fail, 8 │ * after checking each line really holds the verb, and returns them so nothing 9 │ * is lost. 10 │ * 11 │ * @exposes #gate to #arbitrary-write [medium] cwe:CWE-73 -- "stripViolations rewrites source files named by the model's locations" 12 │ * @mitigates #gate against #arbitrary-write using #path-validation -- "Every path is resolved under root and refused outside it; a line is removed only when its text carries the annotation verb the model says is there" 13 │ * @flows ThreatModel -> #gate via runGate -- "The model before and after an agent run" 14 │ * @flows #gate -> AgentPrompt via buildGateFollowUp -- "The violations as a re-prompt" 15 │ * @flows #gate -> SourceFiles via stripViolations -- "Rejected annotation lines removed" 16 │ * @comment -- "Never edits an annotation's text: a rejected claim is re-prompted, then removed; rewriting would hide that it was wrong" 17 │ */
L13 flow ThreatModel → #gate
The model before and after an agent run
8 │ * after checking each line really holds the verb, and returns them so nothing 9 │ * is lost. 10 │ * 11 │ * @exposes #gate to #arbitrary-write [medium] cwe:CWE-73 -- "stripViolations rewrites source files named by the model's locations" 12 │ * @mitigates #gate against #arbitrary-write using #path-validation -- "Every path is resolved under root and refused outside it; a line is removed only when its text carries the annotation verb the model says is there" 13 │ * @flows ThreatModel -> #gate via runGate -- "The model before and after an agent run" 14 │ * @flows #gate -> AgentPrompt via buildGateFollowUp -- "The violations as a re-prompt" 15 │ * @flows #gate -> SourceFiles via stripViolations -- "Rejected annotation lines removed" 16 │ * @comment -- "Never edits an annotation's text: a rejected claim is re-prompted, then removed; rewriting would hide that it was wrong" 17 │ */ 18 │ import { readFileSync, writeFileSync } from 'node:fs';
L14 flow #gate → AgentPrompt
The violations as a re-prompt
9 │ * is lost. 10 │ * 11 │ * @exposes #gate to #arbitrary-write [medium] cwe:CWE-73 -- "stripViolations rewrites source files named by the model's locations" 12 │ * @mitigates #gate against #arbitrary-write using #path-validation -- "Every path is resolved under root and refused outside it; a line is removed only when its text carries the annotation verb the model says is there" 13 │ * @flows ThreatModel -> #gate via runGate -- "The model before and after an agent run" 14 │ * @flows #gate -> AgentPrompt via buildGateFollowUp -- "The violations as a re-prompt" 15 │ * @flows #gate -> SourceFiles via stripViolations -- "Rejected annotation lines removed" 16 │ * @comment -- "Never edits an annotation's text: a rejected claim is re-prompted, then removed; rewriting would hide that it was wrong" 17 │ */ 18 │ import { readFileSync, writeFileSync } from 'node:fs'; 19 │ import { resolve, sep } from 'node:path';
L15 flow #gate → SourceFiles
Rejected annotation lines removed
10 │ * 11 │ * @exposes #gate to #arbitrary-write [medium] cwe:CWE-73 -- "stripViolations rewrites source files named by the model's locations" 12 │ * @mitigates #gate against #arbitrary-write using #path-validation -- "Every path is resolved under root and refused outside it; a line is removed only when its text carries the annotation verb the model says is there" 13 │ * @flows ThreatModel -> #gate via runGate -- "The model before and after an agent run" 14 │ * @flows #gate -> AgentPrompt via buildGateFollowUp -- "The violations as a re-prompt" 15 │ * @flows #gate -> SourceFiles via stripViolations -- "Rejected annotation lines removed" 16 │ * @comment -- "Never edits an annotation's text: a rejected claim is re-prompted, then removed; rewriting would hide that it was wrong" 17 │ */ 18 │ import { readFileSync, writeFileSync } from 'node:fs'; 19 │ import { resolve, sep } from 'node:path'; 20 │ import type { ThreatModel } from '../types/index.js';
L16 comment Never edits an annotation's text: a rejected claim is re-prompted, then removed; rewriting would hide that it was wrong
Never edits an annotation's text: a rejected claim is re-prompted, then removed; rewriting would hide that it was wrong
11 │ * @exposes #gate to #arbitrary-write [medium] cwe:CWE-73 -- "stripViolations rewrites source files named by the model's locations" 12 │ * @mitigates #gate against #arbitrary-write using #path-validation -- "Every path is resolved under root and refused outside it; a line is removed only when its text carries the annotation verb the model says is there" 13 │ * @flows ThreatModel -> #gate via runGate -- "The model before and after an agent run" 14 │ * @flows #gate -> AgentPrompt via buildGateFollowUp -- "The violations as a re-prompt" 15 │ * @flows #gate -> SourceFiles via stripViolations -- "Rejected annotation lines removed" 16 │ * @comment -- "Never edits an annotation's text: a rejected claim is re-prompted, then removed; rewriting would hide that it was wrong" 17 │ */ 18 │ import { readFileSync, writeFileSync } from 'node:fs'; 19 │ import { resolve, sep } from 'node:path'; 20 │ import type { ThreatModel } from '../types/index.js'; 21 │ import { relationRecords, type ClaimSource } from '../parser/claim-key.js';
src/gate/index.ts comment 1 open 1
L4 comment The acceptance check behind `guardlink annotate` and the standalone `guardlink lint`
The acceptance check behind `guardlink annotate` and the standalone `guardlink lint`
1 │ /** 2 │ * GuardLink Gate — barrel. 3 │ * 4 │ * @comment -- "The acceptance check behind `guardlink annotate` and the standalone `guardlink lint`" 5 │ */ 6 │ export { lintAnnotations, hasCodeReference, hasEvidenceWords, RULE_FIX, type Violation, type LintRule, type LintLevel, type LintOptions } from './lint.js'; 7 │ export { runGate, formatGateReport, buildGateFollowUp, stripViolations, type GateReport, type Stripped } from './gate.js'; 8 │
src/gate/lint.ts flow 1comment 1 open 2
L8 flow ThreatModel → #gate
Claims and their descriptions
3 │ * 4 │ * Pure over the model. Each rule mirrors one line of the evidence bar in 5 │ * src/playbooks/evidence.ts: what the prompt demands is what this checks, so 6 │ * an agent that ignored the bar is caught by the same words it ignored. 7 │ * 8 │ * @flows ThreatModel -> #gate via lintAnnotations -- "Claims and their descriptions" 9 │ * @comment -- "Errors are what the gate rejects; warnings are what it reports. Governance verbs (@accepts, @entitles) are errors only for claims under check, because a human may legitimately have written the others" 10 │ */ 11 │ import type { ThreatModel, ThreatModelExposure, ThreatModelConfirmed } from '../types/index.js'; 12 │ import { relationRecords } from '../parser/claim-key.js'; 13 │ import { buildCoverageIndex } from '../parser/coverage.js';
L9 comment Errors are what the gate rejects; warnings are what it reports. Governance verbs (@accepts, @entitles) are errors only for claims under check, because a human may legitimately have written the others
Errors are what the gate rejects; warnings are what it reports. Governance verbs (@accepts, @entitles) are errors only for claims under check, because a human may legitimately have written the others
4 │ * Pure over the model. Each rule mirrors one line of the evidence bar in 5 │ * src/playbooks/evidence.ts: what the prompt demands is what this checks, so 6 │ * an agent that ignored the bar is caught by the same words it ignored. 7 │ * 8 │ * @flows ThreatModel -> #gate via lintAnnotations -- "Claims and their descriptions" 9 │ * @comment -- "Errors are what the gate rejects; warnings are what it reports. Governance verbs (@accepts, @entitles) are errors only for claims under check, because a human may legitimately have written the others" 10 │ */ 11 │ import type { ThreatModel, ThreatModelExposure, ThreatModelConfirmed } from '../types/index.js'; 12 │ import { relationRecords } from '../parser/claim-key.js'; 13 │ import { buildCoverageIndex } from '../parser/coverage.js'; 14 │
src/graph/legibility.ts exposes 1mitigates 1flow 1comment 1 open 4
L90 exposes #dashboard → #dos
measureLegibility scans caller-supplied diagram text with regular expressions, once per line
85 │ * "legible" in one repository and not in another, and the number is a property 86 │ * of a panel size and a font size, neither of which a project chooses. A reader 87 │ * who wants more on screen does not want a bigger budget; they want a different 88 │ * question, which is what the view catalogue is for. 89 │ * 90 │ * @exposes #dashboard to #dos [low] cwe:CWE-400 -- "measureLegibility scans caller-supplied diagram text with regular expressions, once per line" 91 │ * @mitigates #dashboard against #dos using #regex-anchoring -- "Every pattern is line-anchored or a bounded character class; no nested quantifier can backtrack across the input, and each line is scanned a fixed number of times" 92 │ * @flows DiagramSource -> #dashboard via measureLegibility -- "Generated Mermaid text measured for node and edge count before a view decides to draw it" 93 │ * @comment -- "The numbers are measured against a specific panel and font size, both cited above — deriving rather than guessing is the same discipline render-budget.ts applies to Mermaid's own limits" 94 │ */ 95 │
L91 mitigates #regex-anchoring mitigates #dos
Every pattern is line-anchored or a bounded character class; no nested quantifier can backtrack across the input, and each line is scanned a fixed number of times
86 │ * of a panel size and a font size, neither of which a project chooses. A reader 87 │ * who wants more on screen does not want a bigger budget; they want a different 88 │ * question, which is what the view catalogue is for. 89 │ * 90 │ * @exposes #dashboard to #dos [low] cwe:CWE-400 -- "measureLegibility scans caller-supplied diagram text with regular expressions, once per line" 91 │ * @mitigates #dashboard against #dos using #regex-anchoring -- "Every pattern is line-anchored or a bounded character class; no nested quantifier can backtrack across the input, and each line is scanned a fixed number of times" 92 │ * @flows DiagramSource -> #dashboard via measureLegibility -- "Generated Mermaid text measured for node and edge count before a view decides to draw it" 93 │ * @comment -- "The numbers are measured against a specific panel and font size, both cited above — deriving rather than guessing is the same discipline render-budget.ts applies to Mermaid's own limits" 94 │ */ 95 │ 96 │ import { mermaidRenderText, countMermaidEdges, MERMAID_LIMITS } from '../dashboard/render-budget.js';
L92 flow DiagramSource → #dashboard
Generated Mermaid text measured for node and edge count before a view decides to draw it
87 │ * who wants more on screen does not want a bigger budget; they want a different 88 │ * question, which is what the view catalogue is for. 89 │ * 90 │ * @exposes #dashboard to #dos [low] cwe:CWE-400 -- "measureLegibility scans caller-supplied diagram text with regular expressions, once per line" 91 │ * @mitigates #dashboard against #dos using #regex-anchoring -- "Every pattern is line-anchored or a bounded character class; no nested quantifier can backtrack across the input, and each line is scanned a fixed number of times" 92 │ * @flows DiagramSource -> #dashboard via measureLegibility -- "Generated Mermaid text measured for node and edge count before a view decides to draw it" 93 │ * @comment -- "The numbers are measured against a specific panel and font size, both cited above — deriving rather than guessing is the same discipline render-budget.ts applies to Mermaid's own limits" 94 │ */ 95 │ 96 │ import { mermaidRenderText, countMermaidEdges, MERMAID_LIMITS } from '../dashboard/render-budget.js'; 97 │
L93 comment The numbers are measured against a specific panel and font size, both cited above — deriving rather than guessing is the same discipline render-budget.ts applies to Mermaid's own limits
The numbers are measured against a specific panel and font size, both cited above — deriving rather than guessing is the same discipline render-budget.ts applies to Mermaid's own limits
88 │ * question, which is what the view catalogue is for. 89 │ * 90 │ * @exposes #dashboard to #dos [low] cwe:CWE-400 -- "measureLegibility scans caller-supplied diagram text with regular expressions, once per line" 91 │ * @mitigates #dashboard against #dos using #regex-anchoring -- "Every pattern is line-anchored or a bounded character class; no nested quantifier can backtrack across the input, and each line is scanned a fixed number of times" 92 │ * @flows DiagramSource -> #dashboard via measureLegibility -- "Generated Mermaid text measured for node and edge count before a view decides to draw it" 93 │ * @comment -- "The numbers are measured against a specific panel and font size, both cited above — deriving rather than guessing is the same discipline render-budget.ts applies to Mermaid's own limits" 94 │ */ 95 │ 96 │ import { mermaidRenderText, countMermaidEdges, MERMAID_LIMITS } from '../dashboard/render-budget.js'; 97 │ 98 │ /**
src/graph/views.ts comment 2flow 1 open 3
L52 flow ThreatModel → #dashboard
Model narrowed to a neighbourhood that fits the legibility budget
47 │ * `question` is not documentation, it is part of the view. A view that exists 48 │ * because the data allows it is not the same as one somebody wants, and the 49 │ * page renders this string verbatim so a reader can tell which they are looking 50 │ * at before they read the answer. 51 │ * 52 │ * @flows ThreatModel -> #dashboard via growWithinBudget -- "Model narrowed to a neighbourhood that fits the legibility budget" 53 │ * @comment -- "Pure functions over an already-parsed ThreatModel: no file I/O, no user input, no network, and the renderer is injected so this module never depends on the dashboard" 54 │ * @comment -- "Selection goes through selectSubgraph's `nodes` option rather than reimplementing the filter, so a view and an MCP graph query narrow the model the same way" 55 │ */ 56 │ 57 │ import { selectSubgraph, graphEdges, canonicaliser, type Direction, type GraphEdge } from '../mcp/subgraph.js';
L53 comment Pure functions over an already-parsed ThreatModel: no file I/O, no user input, no network, and the renderer is injected so this module never depends on the dashboard
Pure functions over an already-parsed ThreatModel: no file I/O, no user input, no network, and the renderer is injected so this module never depends on the dashboard
48 │ * because the data allows it is not the same as one somebody wants, and the 49 │ * page renders this string verbatim so a reader can tell which they are looking 50 │ * at before they read the answer. 51 │ * 52 │ * @flows ThreatModel -> #dashboard via growWithinBudget -- "Model narrowed to a neighbourhood that fits the legibility budget" 53 │ * @comment -- "Pure functions over an already-parsed ThreatModel: no file I/O, no user input, no network, and the renderer is injected so this module never depends on the dashboard" 54 │ * @comment -- "Selection goes through selectSubgraph's `nodes` option rather than reimplementing the filter, so a view and an MCP graph query narrow the model the same way" 55 │ */ 56 │ 57 │ import { selectSubgraph, graphEdges, canonicaliser, type Direction, type GraphEdge } from '../mcp/subgraph.js'; 58 │ import { checkLegibility, LEGIBILITY_BUDGET, type LegibilityBudget, type LegibilityVerdict } from './legibility.js';
L54 comment Selection goes through selectSubgraph's `nodes` option rather than reimplementing the filter, so a view and an MCP graph query narrow the model the same way
Selection goes through selectSubgraph's `nodes` option rather than reimplementing the filter, so a view and an MCP graph query narrow the model the same way
49 │ * page renders this string verbatim so a reader can tell which they are looking 50 │ * at before they read the answer. 51 │ * 52 │ * @flows ThreatModel -> #dashboard via growWithinBudget -- "Model narrowed to a neighbourhood that fits the legibility budget" 53 │ * @comment -- "Pure functions over an already-parsed ThreatModel: no file I/O, no user input, no network, and the renderer is injected so this module never depends on the dashboard" 54 │ * @comment -- "Selection goes through selectSubgraph's `nodes` option rather than reimplementing the filter, so a view and an MCP graph query narrow the model the same way" 55 │ */ 56 │ 57 │ import { selectSubgraph, graphEdges, canonicaliser, type Direction, type GraphEdge } from '../mcp/subgraph.js'; 58 │ import { checkLegibility, LEGIBILITY_BUDGET, type LegibilityBudget, type LegibilityVerdict } from './legibility.js'; 59 │ import type { ThreatModel } from '../types/index.js';
src/hypothesis/classify.ts flow 2comment 1 open 3
L10 flow ThreatModel → #cli
Claims joined to the ledger by key
5 │ * anchor hash is the one it was recorded against; when the code beneath the 6 │ * claim moves, a refutation lapses to untested (with the old outcome 7 │ * attached) and a confirmation asks for a retest. A source `@confirmed` is a 8 │ * confirmation with no ledger entry. 9 │ * 10 │ * @flows ThreatModel -> #cli via classifyHypotheses -- "Claims joined to the ledger by key" 11 │ * @flows #cli -> ThreatModel via attachHypotheses -- "The state stamped onto each exposure for the dashboard, the report and the lint" 12 │ * @comment -- "Pure over the model and a ledger read; no clock, no file, no git — ranking is arithmetic on facts the model already holds" 13 │ */ 14 │ import type { ThreatModel, ThreatModelExposure, ExposureHypothesis, SourceLocation } from '../types/index.js'; 15 │ import { relationRecords } from '../parser/claim-key.js';
L11 flow #cli → ThreatModel
The state stamped onto each exposure for the dashboard, the report and the lint
6 │ * claim moves, a refutation lapses to untested (with the old outcome 7 │ * attached) and a confirmation asks for a retest. A source `@confirmed` is a 8 │ * confirmation with no ledger entry. 9 │ * 10 │ * @flows ThreatModel -> #cli via classifyHypotheses -- "Claims joined to the ledger by key" 11 │ * @flows #cli -> ThreatModel via attachHypotheses -- "The state stamped onto each exposure for the dashboard, the report and the lint" 12 │ * @comment -- "Pure over the model and a ledger read; no clock, no file, no git — ranking is arithmetic on facts the model already holds" 13 │ */ 14 │ import type { ThreatModel, ThreatModelExposure, ExposureHypothesis, SourceLocation } from '../types/index.js'; 15 │ import { relationRecords } from '../parser/claim-key.js'; 16 │ import type { HypothesesRead, HypothesisEntry, HypothesisOutcomeRecord } from './ledger.js';
L12 comment Pure over the model and a ledger read; no clock, no file, no git — ranking is arithmetic on facts the model already holds
Pure over the model and a ledger read; no clock, no file, no git — ranking is arithmetic on facts the model already holds
7 │ * attached) and a confirmation asks for a retest. A source `@confirmed` is a 8 │ * confirmation with no ledger entry. 9 │ * 10 │ * @flows ThreatModel -> #cli via classifyHypotheses -- "Claims joined to the ledger by key" 11 │ * @flows #cli -> ThreatModel via attachHypotheses -- "The state stamped onto each exposure for the dashboard, the report and the lint" 12 │ * @comment -- "Pure over the model and a ledger read; no clock, no file, no git — ranking is arithmetic on facts the model already holds" 13 │ */ 14 │ import type { ThreatModel, ThreatModelExposure, ExposureHypothesis, SourceLocation } from '../types/index.js'; 15 │ import { relationRecords } from '../parser/claim-key.js'; 16 │ import type { HypothesesRead, HypothesisEntry, HypothesisOutcomeRecord } from './ledger.js'; 17 │
src/hypothesis/commands.ts exposes 2mitigates 2flow 2handles 1 open 8
L10 exposes #cli → #arbitrary-write
writeConfirmedLine() edits the source file a claim's location names; importScan() reads the scan path the user passed
5 │ * anchor. `importScan` joins a cxg report's findings to claims and records 6 │ * the confirmed ones. `confirmedLine` and `writeConfirmedLine` offer, and on 7 │ * request insert, the `@confirmed` annotation that reflects a confirmation 8 │ * in the source, right beneath its `@exposes`. 9 │ * 10 │ * @exposes #cli to #arbitrary-write [medium] cwe:CWE-73 -- "writeConfirmedLine() edits the source file a claim's location names; importScan() reads the scan path the user passed" 11 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Both paths are resolved under the project root and refused outside it; the edit inserts one line beneath an @exposes whose text is checked before anything is written" 12 │ * @exposes #cli to #insecure-deser [low] cwe:CWE-502 -- "importScan() JSON.parses a scan report" 13 │ * @mitigates #cli against #insecure-deser using #config-validation -- "Only the fields the loader reads are taken, each coerced to its type; evidence goes through redactEvidence before it is stored" 14 │ * @flows ScanReport -> #cli via importScan -- "cxg findings joined to claims" 15 │ * @flows #cli -> SourceFiles via writeConfirmedLine -- "The @confirmed line, on --write"
L11 mitigates #path-validation mitigates #arbitrary-write
Both paths are resolved under the project root and refused outside it; the edit inserts one line beneath an @exposes whose text is checked before anything is written
6 │ * the confirmed ones. `confirmedLine` and `writeConfirmedLine` offer, and on 7 │ * request insert, the `@confirmed` annotation that reflects a confirmation 8 │ * in the source, right beneath its `@exposes`. 9 │ * 10 │ * @exposes #cli to #arbitrary-write [medium] cwe:CWE-73 -- "writeConfirmedLine() edits the source file a claim's location names; importScan() reads the scan path the user passed" 11 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Both paths are resolved under the project root and refused outside it; the edit inserts one line beneath an @exposes whose text is checked before anything is written" 12 │ * @exposes #cli to #insecure-deser [low] cwe:CWE-502 -- "importScan() JSON.parses a scan report" 13 │ * @mitigates #cli against #insecure-deser using #config-validation -- "Only the fields the loader reads are taken, each coerced to its type; evidence goes through redactEvidence before it is stored" 14 │ * @flows ScanReport -> #cli via importScan -- "cxg findings joined to claims" 15 │ * @flows #cli -> SourceFiles via writeConfirmedLine -- "The @confirmed line, on --write" 16 │ * @handles internal on #cli -- "Request and response evidence from scans, redacted"
L12 exposes #cli → #insecure-deser
importScan() JSON.parses a scan report
7 │ * request insert, the `@confirmed` annotation that reflects a confirmation 8 │ * in the source, right beneath its `@exposes`. 9 │ * 10 │ * @exposes #cli to #arbitrary-write [medium] cwe:CWE-73 -- "writeConfirmedLine() edits the source file a claim's location names; importScan() reads the scan path the user passed" 11 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Both paths are resolved under the project root and refused outside it; the edit inserts one line beneath an @exposes whose text is checked before anything is written" 12 │ * @exposes #cli to #insecure-deser [low] cwe:CWE-502 -- "importScan() JSON.parses a scan report" 13 │ * @mitigates #cli against #insecure-deser using #config-validation -- "Only the fields the loader reads are taken, each coerced to its type; evidence goes through redactEvidence before it is stored" 14 │ * @flows ScanReport -> #cli via importScan -- "cxg findings joined to claims" 15 │ * @flows #cli -> SourceFiles via writeConfirmedLine -- "The @confirmed line, on --write" 16 │ * @handles internal on #cli -- "Request and response evidence from scans, redacted" 17 │ * @comment -- "A confirmation is held to the same evidence bar the gate holds @confirmed to; a refutation needs evidence too, just not the same words, because 'the validator rejected it' is evidence of absence"
L13 mitigates #config-validation mitigates #insecure-deser
Only the fields the loader reads are taken, each coerced to its type; evidence goes through redactEvidence before it is stored
8 │ * in the source, right beneath its `@exposes`. 9 │ * 10 │ * @exposes #cli to #arbitrary-write [medium] cwe:CWE-73 -- "writeConfirmedLine() edits the source file a claim's location names; importScan() reads the scan path the user passed" 11 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Both paths are resolved under the project root and refused outside it; the edit inserts one line beneath an @exposes whose text is checked before anything is written" 12 │ * @exposes #cli to #insecure-deser [low] cwe:CWE-502 -- "importScan() JSON.parses a scan report" 13 │ * @mitigates #cli against #insecure-deser using #config-validation -- "Only the fields the loader reads are taken, each coerced to its type; evidence goes through redactEvidence before it is stored" 14 │ * @flows ScanReport -> #cli via importScan -- "cxg findings joined to claims" 15 │ * @flows #cli -> SourceFiles via writeConfirmedLine -- "The @confirmed line, on --write" 16 │ * @handles internal on #cli -- "Request and response evidence from scans, redacted" 17 │ * @comment -- "A confirmation is held to the same evidence bar the gate holds @confirmed to; a refutation needs evidence too, just not the same words, because 'the validator rejected it' is evidence of absence" 18 │ */
L14 flow ScanReport → #cli
cxg findings joined to claims
9 │ * 10 │ * @exposes #cli to #arbitrary-write [medium] cwe:CWE-73 -- "writeConfirmedLine() edits the source file a claim's location names; importScan() reads the scan path the user passed" 11 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Both paths are resolved under the project root and refused outside it; the edit inserts one line beneath an @exposes whose text is checked before anything is written" 12 │ * @exposes #cli to #insecure-deser [low] cwe:CWE-502 -- "importScan() JSON.parses a scan report" 13 │ * @mitigates #cli against #insecure-deser using #config-validation -- "Only the fields the loader reads are taken, each coerced to its type; evidence goes through redactEvidence before it is stored" 14 │ * @flows ScanReport -> #cli via importScan -- "cxg findings joined to claims" 15 │ * @flows #cli -> SourceFiles via writeConfirmedLine -- "The @confirmed line, on --write" 16 │ * @handles internal on #cli -- "Request and response evidence from scans, redacted" 17 │ * @comment -- "A confirmation is held to the same evidence bar the gate holds @confirmed to; a refutation needs evidence too, just not the same words, because 'the validator rejected it' is evidence of absence" 18 │ */ 19 │ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
L15 flow #cli → SourceFiles
The @confirmed line, on --write
10 │ * @exposes #cli to #arbitrary-write [medium] cwe:CWE-73 -- "writeConfirmedLine() edits the source file a claim's location names; importScan() reads the scan path the user passed" 11 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Both paths are resolved under the project root and refused outside it; the edit inserts one line beneath an @exposes whose text is checked before anything is written" 12 │ * @exposes #cli to #insecure-deser [low] cwe:CWE-502 -- "importScan() JSON.parses a scan report" 13 │ * @mitigates #cli against #insecure-deser using #config-validation -- "Only the fields the loader reads are taken, each coerced to its type; evidence goes through redactEvidence before it is stored" 14 │ * @flows ScanReport -> #cli via importScan -- "cxg findings joined to claims" 15 │ * @flows #cli -> SourceFiles via writeConfirmedLine -- "The @confirmed line, on --write" 16 │ * @handles internal on #cli -- "Request and response evidence from scans, redacted" 17 │ * @comment -- "A confirmation is held to the same evidence bar the gate holds @confirmed to; a refutation needs evidence too, just not the same words, because 'the validator rejected it' is evidence of absence" 18 │ */ 19 │ import { existsSync, readFileSync, writeFileSync } from 'node:fs'; 20 │ import { resolve, sep } from 'node:path';
L16 handles #cli: internal
Request and response evidence from scans, redacted
11 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Both paths are resolved under the project root and refused outside it; the edit inserts one line beneath an @exposes whose text is checked before anything is written" 12 │ * @exposes #cli to #insecure-deser [low] cwe:CWE-502 -- "importScan() JSON.parses a scan report" 13 │ * @mitigates #cli against #insecure-deser using #config-validation -- "Only the fields the loader reads are taken, each coerced to its type; evidence goes through redactEvidence before it is stored" 14 │ * @flows ScanReport -> #cli via importScan -- "cxg findings joined to claims" 15 │ * @flows #cli -> SourceFiles via writeConfirmedLine -- "The @confirmed line, on --write" 16 │ * @handles internal on #cli -- "Request and response evidence from scans, redacted" 17 │ * @comment -- "A confirmation is held to the same evidence bar the gate holds @confirmed to; a refutation needs evidence too, just not the same words, because 'the validator rejected it' is evidence of absence" 18 │ */ 19 │ import { existsSync, readFileSync, writeFileSync } from 'node:fs'; 20 │ import { resolve, sep } from 'node:path'; 21 │ import type { ThreatModel } from '../types/index.js';
L17 comment A confirmation is held to the same evidence bar the gate holds @confirmed to; a refutation needs evidence too, just not the same words, because 'the validator rejected it' is evidence of absence
A confirmation is held to the same evidence bar the gate holds @confirmed to; a refutation needs evidence too, just not the same words, because 'the validator rejected it' is evidence of absence
12 │ * @exposes #cli to #insecure-deser [low] cwe:CWE-502 -- "importScan() JSON.parses a scan report" 13 │ * @mitigates #cli against #insecure-deser using #config-validation -- "Only the fields the loader reads are taken, each coerced to its type; evidence goes through redactEvidence before it is stored" 14 │ * @flows ScanReport -> #cli via importScan -- "cxg findings joined to claims" 15 │ * @flows #cli -> SourceFiles via writeConfirmedLine -- "The @confirmed line, on --write" 16 │ * @handles internal on #cli -- "Request and response evidence from scans, redacted" 17 │ * @comment -- "A confirmation is held to the same evidence bar the gate holds @confirmed to; a refutation needs evidence too, just not the same words, because 'the validator rejected it' is evidence of absence" 18 │ */ 19 │ import { existsSync, readFileSync, writeFileSync } from 'node:fs'; 20 │ import { resolve, sep } from 'node:path'; 21 │ import type { ThreatModel } from '../types/index.js'; 22 │ import { hasEvidenceWords } from '../gate/lint.js';
src/hypothesis/format.ts handles 1comment 1 open 2
L4 handles #cli: internal
Evidence strings printed to the terminal
1 │ /** 2 │ * GuardLink Hypotheses — text for the terminal. 3 │ * 4 │ * @handles internal on #cli -- "Evidence strings printed to the terminal" 5 │ * @comment -- "Plain padded tables like printStatus; nothing here reads a file" 6 │ */ 7 │ import type { HypothesisClassification, HypothesisRecord, RankedHypothesis } from './classify.js'; 8 │ import type { HypothesisEntry } from './ledger.js'; 9 │ import type { ImportResult } from './commands.js';
L5 comment Plain padded tables like printStatus; nothing here reads a file
Plain padded tables like printStatus; nothing here reads a file
1 │ /** 2 │ * GuardLink Hypotheses — text for the terminal. 3 │ * 4 │ * @handles internal on #cli -- "Evidence strings printed to the terminal" 5 │ * @comment -- "Plain padded tables like printStatus; nothing here reads a file" 6 │ */ 7 │ import type { HypothesisClassification, HypothesisRecord, RankedHypothesis } from './classify.js'; 8 │ import type { HypothesisEntry } from './ledger.js'; 9 │ import type { ImportResult } from './commands.js'; 10 │
src/hypothesis/index.ts comment 1 open 1
L4 comment The tested state of every exposure: a ledger of outcomes with evidence, expiry by code hash, and a ranked queue
The tested state of every exposure: a ledger of outcomes with evidence, expiry by code hash, and a ranked queue
1 │ /** 2 │ * GuardLink Hypotheses — barrel. 3 │ * 4 │ * @comment -- "The tested state of every exposure: a ledger of outcomes with evidence, expiry by code hash, and a ranked queue" 5 │ */ 6 │ export { HYPOTHESES_FILE, HYPOTHESES_SCHEMA, readHypotheses, writeHypotheses, emptyHypotheses, serializeHypotheses } from './ledger.js'; 7 │ export type { HypothesisOutcome, HypothesisSource, HypothesisAnchor, HypothesisOutcomeRecord, HypothesisEntry, HypothesesLedger, HypothesesStatus, HypothesesRead } from './ledger.js'; 8 │ export { classifyHypotheses, attachHypotheses, rankUntested } from './classify.js'; 9 │ export type { HypothesisState, HypothesisRecord, HypothesisSummary, HypothesisClassification, RankedHypothesis } from './classify.js';
src/hypothesis/ledger.ts flow 2exposes 1mitigates 1handles 1 open 6
L10 exposes #cli → #arbitrary-write
writeHypotheses() writes .guardlink/hypotheses.json under the root the caller resolved
5 │ * tested against reality: confirmed or refuted, with the evidence, by whom, 6 │ * when, and the code hash beneath the claim at that moment. It is the sibling 7 │ * of `verified.json`, which records that a claim matches its code; this one 8 │ * records that a claim was checked against the world. 9 │ * 10 │ * @exposes #cli to #arbitrary-write [low] cwe:CWE-73 -- "writeHypotheses() writes .guardlink/hypotheses.json under the root the caller resolved" 11 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "The path is fixed relative to the project root (HYPOTHESES_FILE); nothing in an entry chooses where the file goes" 12 │ * @flows LedgerFile -> #cli via readHypotheses -- "Outcomes read back for classification" 13 │ * @flows #cli -> LedgerFile via writeHypotheses -- "Outcomes recorded" 14 │ * @handles internal on #cli -- "Evidence strings from tests and scans; scan evidence is redacted before it is written" 15 │ * @comment -- "A corrupt file is reported, never rebuilt in place: an outcome ledger that quietly emptied itself would turn refuted findings back into open ones"
L11 mitigates #path-validation mitigates #arbitrary-write
The path is fixed relative to the project root (HYPOTHESES_FILE); nothing in an entry chooses where the file goes
6 │ * when, and the code hash beneath the claim at that moment. It is the sibling 7 │ * of `verified.json`, which records that a claim matches its code; this one 8 │ * records that a claim was checked against the world. 9 │ * 10 │ * @exposes #cli to #arbitrary-write [low] cwe:CWE-73 -- "writeHypotheses() writes .guardlink/hypotheses.json under the root the caller resolved" 11 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "The path is fixed relative to the project root (HYPOTHESES_FILE); nothing in an entry chooses where the file goes" 12 │ * @flows LedgerFile -> #cli via readHypotheses -- "Outcomes read back for classification" 13 │ * @flows #cli -> LedgerFile via writeHypotheses -- "Outcomes recorded" 14 │ * @handles internal on #cli -- "Evidence strings from tests and scans; scan evidence is redacted before it is written" 15 │ * @comment -- "A corrupt file is reported, never rebuilt in place: an outcome ledger that quietly emptied itself would turn refuted findings back into open ones" 16 │ */
L12 flow LedgerFile → #cli
Outcomes read back for classification
7 │ * of `verified.json`, which records that a claim matches its code; this one 8 │ * records that a claim was checked against the world. 9 │ * 10 │ * @exposes #cli to #arbitrary-write [low] cwe:CWE-73 -- "writeHypotheses() writes .guardlink/hypotheses.json under the root the caller resolved" 11 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "The path is fixed relative to the project root (HYPOTHESES_FILE); nothing in an entry chooses where the file goes" 12 │ * @flows LedgerFile -> #cli via readHypotheses -- "Outcomes read back for classification" 13 │ * @flows #cli -> LedgerFile via writeHypotheses -- "Outcomes recorded" 14 │ * @handles internal on #cli -- "Evidence strings from tests and scans; scan evidence is redacted before it is written" 15 │ * @comment -- "A corrupt file is reported, never rebuilt in place: an outcome ledger that quietly emptied itself would turn refuted findings back into open ones" 16 │ */ 17 │ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
L13 flow #cli → LedgerFile
Outcomes recorded
8 │ * records that a claim was checked against the world. 9 │ * 10 │ * @exposes #cli to #arbitrary-write [low] cwe:CWE-73 -- "writeHypotheses() writes .guardlink/hypotheses.json under the root the caller resolved" 11 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "The path is fixed relative to the project root (HYPOTHESES_FILE); nothing in an entry chooses where the file goes" 12 │ * @flows LedgerFile -> #cli via readHypotheses -- "Outcomes read back for classification" 13 │ * @flows #cli -> LedgerFile via writeHypotheses -- "Outcomes recorded" 14 │ * @handles internal on #cli -- "Evidence strings from tests and scans; scan evidence is redacted before it is written" 15 │ * @comment -- "A corrupt file is reported, never rebuilt in place: an outcome ledger that quietly emptied itself would turn refuted findings back into open ones" 16 │ */ 17 │ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; 18 │ import { dirname, join } from 'node:path';
L14 handles #cli: internal
Evidence strings from tests and scans; scan evidence is redacted before it is written
9 │ * 10 │ * @exposes #cli to #arbitrary-write [low] cwe:CWE-73 -- "writeHypotheses() writes .guardlink/hypotheses.json under the root the caller resolved" 11 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "The path is fixed relative to the project root (HYPOTHESES_FILE); nothing in an entry chooses where the file goes" 12 │ * @flows LedgerFile -> #cli via readHypotheses -- "Outcomes read back for classification" 13 │ * @flows #cli -> LedgerFile via writeHypotheses -- "Outcomes recorded" 14 │ * @handles internal on #cli -- "Evidence strings from tests and scans; scan evidence is redacted before it is written" 15 │ * @comment -- "A corrupt file is reported, never rebuilt in place: an outcome ledger that quietly emptied itself would turn refuted findings back into open ones" 16 │ */ 17 │ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; 18 │ import { dirname, join } from 'node:path'; 19 │ import type { AnchorScope } from '../types/index.js';
L15 comment A corrupt file is reported, never rebuilt in place: an outcome ledger that quietly emptied itself would turn refuted findings back into open ones
A corrupt file is reported, never rebuilt in place: an outcome ledger that quietly emptied itself would turn refuted findings back into open ones
10 │ * @exposes #cli to #arbitrary-write [low] cwe:CWE-73 -- "writeHypotheses() writes .guardlink/hypotheses.json under the root the caller resolved" 11 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "The path is fixed relative to the project root (HYPOTHESES_FILE); nothing in an entry chooses where the file goes" 12 │ * @flows LedgerFile -> #cli via readHypotheses -- "Outcomes read back for classification" 13 │ * @flows #cli -> LedgerFile via writeHypotheses -- "Outcomes recorded" 14 │ * @handles internal on #cli -- "Evidence strings from tests and scans; scan evidence is redacted before it is written" 15 │ * @comment -- "A corrupt file is reported, never rebuilt in place: an outcome ledger that quietly emptied itself would turn refuted findings back into open ones" 16 │ */ 17 │ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; 18 │ import { dirname, join } from 'node:path'; 19 │ import type { AnchorScope } from '../types/index.js'; 20 │ import { ANCHOR_HASH_VERSION } from '../structure/hash.js';
src/init/detect.ts exposes 1mitigates 1flow 1comment 1 open 4
L5 exposes #init → #path-traversal
Reads package.json, pyproject.toml, etc. from root
1 │ /** 2 │ * GuardLink init — Project detection utilities. 3 │ * Detects language, project name, and existing agent instruction files. 4 │ * 5 │ * @exposes #init to #path-traversal [low] cwe:CWE-22 -- "Reads package.json, pyproject.toml, etc. from root" 6 │ * @mitigates #init against #path-traversal using #path-validation -- "join() with root constrains; reads well-known files only" 7 │ * @flows ProjectRoot -> #init via detectProject -- "Project detection input" 8 │ * @comment -- "Detection is read-only; no file writes" 9 │ */ 10 │
L6 mitigates #path-validation mitigates #path-traversal
join() with root constrains; reads well-known files only
1 │ /** 2 │ * GuardLink init — Project detection utilities. 3 │ * Detects language, project name, and existing agent instruction files. 4 │ * 5 │ * @exposes #init to #path-traversal [low] cwe:CWE-22 -- "Reads package.json, pyproject.toml, etc. from root" 6 │ * @mitigates #init against #path-traversal using #path-validation -- "join() with root constrains; reads well-known files only" 7 │ * @flows ProjectRoot -> #init via detectProject -- "Project detection input" 8 │ * @comment -- "Detection is read-only; no file writes" 9 │ */ 10 │ 11 │ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
L7 flow ProjectRoot → #init
Project detection input
2 │ * GuardLink init — Project detection utilities. 3 │ * Detects language, project name, and existing agent instruction files. 4 │ * 5 │ * @exposes #init to #path-traversal [low] cwe:CWE-22 -- "Reads package.json, pyproject.toml, etc. from root" 6 │ * @mitigates #init against #path-traversal using #path-validation -- "join() with root constrains; reads well-known files only" 7 │ * @flows ProjectRoot -> #init via detectProject -- "Project detection input" 8 │ * @comment -- "Detection is read-only; no file writes" 9 │ */ 10 │ 11 │ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; 12 │ import { join, basename } from 'node:path';
L8 comment Detection is read-only; no file writes
Detection is read-only; no file writes
3 │ * Detects language, project name, and existing agent instruction files. 4 │ * 5 │ * @exposes #init to #path-traversal [low] cwe:CWE-22 -- "Reads package.json, pyproject.toml, etc. from root" 6 │ * @mitigates #init against #path-traversal using #path-validation -- "join() with root constrains; reads well-known files only" 7 │ * @flows ProjectRoot -> #init via detectProject -- "Project detection input" 8 │ * @comment -- "Detection is read-only; no file writes" 9 │ */ 10 │ 11 │ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; 12 │ import { join, basename } from 'node:path'; 13 │
src/init/migrate.ts comment 1 open 1
L9 comment Migrations should never modify existing user files. Only create missing ones.
Migrations should never modify existing user files. Only create missing ones.
4 │ * Small idempotent operations for projects upgrading from older versions of 5 │ * GuardLink that don't have the full v1.5.x layout. Designed to be safe to 6 │ * call repeatedly and to fail closed (do nothing) when the project doesn't 7 │ * have a `.guardlink/` directory at all. 8 │ * 9 │ * @comment -- "Migrations should never modify existing user files. Only create missing ones." 10 │ */ 11 │ 12 │ import { existsSync, writeFileSync } from 'node:fs'; 13 │ import { join } from 'node:path'; 14 │ import { detectProject } from './detect.js';
src/init/preserve.ts flow 2exposes 1mitigates 1comment 1 open 5
L16 exposes #init → #arbitrary-write
Decides whether init may overwrite an existing definitions/config file
11 │ * authored content. A definitions file holding declarations the template does 12 │ * not, and a `config.json` carrying settings the template does not, are user 13 │ * work product. They survive `--force` and are reported as preserved. Genuine 14 │ * overwrite intent is spelled `--reset`, which is its own confirmation. 15 │ * 16 │ * @exposes #init to #arbitrary-write [high] cwe:CWE-73 -- "Decides whether init may overwrite an existing definitions/config file" 17 │ * @mitigates #init against #arbitrary-write using #config-validation -- "Overwrite refused when the target parses to declarations the template lacks; --reset required to override" 18 │ * @flows DefinitionsFile -> #init via definitionsArePopulated -- "Existing declarations read to decide preservation" 19 │ * @flows ConfigFile -> #init via configIsCustomised -- "Existing config compared against template defaults" 20 │ * @comment -- "Fail-closed: an unreadable or unparseable existing file is treated as populated, never as empty" 21 │ */
L17 mitigates #config-validation mitigates #arbitrary-write
Overwrite refused when the target parses to declarations the template lacks; --reset required to override
12 │ * not, and a `config.json` carrying settings the template does not, are user 13 │ * work product. They survive `--force` and are reported as preserved. Genuine 14 │ * overwrite intent is spelled `--reset`, which is its own confirmation. 15 │ * 16 │ * @exposes #init to #arbitrary-write [high] cwe:CWE-73 -- "Decides whether init may overwrite an existing definitions/config file" 17 │ * @mitigates #init against #arbitrary-write using #config-validation -- "Overwrite refused when the target parses to declarations the template lacks; --reset required to override" 18 │ * @flows DefinitionsFile -> #init via definitionsArePopulated -- "Existing declarations read to decide preservation" 19 │ * @flows ConfigFile -> #init via configIsCustomised -- "Existing config compared against template defaults" 20 │ * @comment -- "Fail-closed: an unreadable or unparseable existing file is treated as populated, never as empty" 21 │ */ 22 │
L18 flow DefinitionsFile → #init
Existing declarations read to decide preservation
13 │ * work product. They survive `--force` and are reported as preserved. Genuine 14 │ * overwrite intent is spelled `--reset`, which is its own confirmation. 15 │ * 16 │ * @exposes #init to #arbitrary-write [high] cwe:CWE-73 -- "Decides whether init may overwrite an existing definitions/config file" 17 │ * @mitigates #init against #arbitrary-write using #config-validation -- "Overwrite refused when the target parses to declarations the template lacks; --reset required to override" 18 │ * @flows DefinitionsFile -> #init via definitionsArePopulated -- "Existing declarations read to decide preservation" 19 │ * @flows ConfigFile -> #init via configIsCustomised -- "Existing config compared against template defaults" 20 │ * @comment -- "Fail-closed: an unreadable or unparseable existing file is treated as populated, never as empty" 21 │ */ 22 │ 23 │ import { parseString } from '../parser/parse-file.js';
L19 flow ConfigFile → #init
Existing config compared against template defaults
14 │ * overwrite intent is spelled `--reset`, which is its own confirmation. 15 │ * 16 │ * @exposes #init to #arbitrary-write [high] cwe:CWE-73 -- "Decides whether init may overwrite an existing definitions/config file" 17 │ * @mitigates #init against #arbitrary-write using #config-validation -- "Overwrite refused when the target parses to declarations the template lacks; --reset required to override" 18 │ * @flows DefinitionsFile -> #init via definitionsArePopulated -- "Existing declarations read to decide preservation" 19 │ * @flows ConfigFile -> #init via configIsCustomised -- "Existing config compared against template defaults" 20 │ * @comment -- "Fail-closed: an unreadable or unparseable existing file is treated as populated, never as empty" 21 │ */ 22 │ 23 │ import { parseString } from '../parser/parse-file.js'; 24 │
L20 comment Fail-closed: an unreadable or unparseable existing file is treated as populated, never as empty
Fail-closed: an unreadable or unparseable existing file is treated as populated, never as empty
15 │ * 16 │ * @exposes #init to #arbitrary-write [high] cwe:CWE-73 -- "Decides whether init may overwrite an existing definitions/config file" 17 │ * @mitigates #init against #arbitrary-write using #config-validation -- "Overwrite refused when the target parses to declarations the template lacks; --reset required to override" 18 │ * @flows DefinitionsFile -> #init via definitionsArePopulated -- "Existing declarations read to decide preservation" 19 │ * @flows ConfigFile -> #init via configIsCustomised -- "Existing config compared against template defaults" 20 │ * @comment -- "Fail-closed: an unreadable or unparseable existing file is treated as populated, never as empty" 21 │ */ 22 │ 23 │ import { parseString } from '../parser/parse-file.js'; 24 │ 25 │ /**
src/init/templates.ts shield 10 open 10
L39 shield Placement examples for both modes; they would otherwise parse as real annotations
34 │ * Shielded because of D22. The inline branch shows a doc-block, whose ` * @…` 35 │ * lines are indistinguishable from real annotations once the comment prefix is 36 │ * stripped — that is exactly how the GL-402 README template injected `#api`, 37 │ * `#sqli` and `cwe:CWE-89` into this repo's own model. 38 │ */ 39 │ // @shield:begin -- "Placement examples for both modes; they would otherwise parse as real annotations" 40 │ export function annotationPlacementSection( 41 │ project: ProjectInfo, 42 │ mode: AnnotationMode | null, 43 │ ): string { 44 │ // A repo with annotations in both places. This is the exact state D27 said the
L105 shield Shielded region
100 │ ${example} 101 │ ${notRead} 102 │ Do not create \`.gal\` sidecars under \`.guardlink/annotations/\` in this mode; a repo with 103 │ both is a mixed repo, and that is the failure this section exists to prevent.`; 104 │ } 105 │ // @shield:end 106 │ 107 │ // ─── Inline placement, in the host language's real doc-block ───────── 108 │ 109 │ // @shield:begin -- "Placement examples per language; every verb line below is a template, not a claim" 110 │ /**
L109 shield Placement examples per language; every verb line below is a template, not a claim
104 │ } 105 │ // @shield:end 106 │ 107 │ // ─── Inline placement, in the host language's real doc-block ───────── 108 │ 109 │ // @shield:begin -- "Placement examples per language; every verb line below is a template, not a claim" 110 │ /** 111 │ * The doc-block an author of this language actually writes, and the forms this 112 │ * parser does not read. 113 │ * 114 │ * The old version of this offered two examples — a `#` one and a JSDoc one —
L176 shield Shielded region
171 │ case 'unknown': 172 │ default: 173 │ return { example: block('ts', 'export function login(email: string) { … }'), notRead: '' }; 174 │ } 175 │ } 176 │ // @shield:end 177 │ 178 │ // ─── Canonical reference document ──────────────────────────────────── 179 │ 180 │ /** 181 │ * docs/GUARDLINK_REFERENCE.md — the single source of truth for annotation syntax.
L207 shield The annotation reference's own syntax block: verb lines at column 0 with no comment marker, which is what an annotation lost inside a docstring looks like
202 │ /** `--no-root-files` keeps everything inside `.guardlink/`; otherwise `docs/`. */ 203 │ export function referenceDocPath(rootFiles: boolean): string { 204 │ return rootFiles ? REFERENCE_DOC_IN_DOCS : REFERENCE_DOC_IN_GUARDLINK; 205 │ } 206 │ 207 │ // @shield:begin -- "The annotation reference's own syntax block: verb lines at column 0 with no comment marker, which is what an annotation lost inside a docstring looks like" 208 │ export function referenceDocContent(project: ProjectInfo): string { 209 │ return `# GuardLink — Annotation Reference 210 │ 211 │ > Canonical reference for **${project.name}**. All agent instruction files point here. 212 │ > Full specification: [docs/SPEC.md](https://github.com/Bugb-Technologies/guardlink/blob/main/docs/SPEC.md)
L305 shield Shielded region
300 │ - \`guardlink_validate\` — check for syntax errors 301 │ - \`guardlink_status\` — coverage stats 302 │ - \`guardlink_entitlement_propose\` / \`guardlink_entitlement_list\` — propose an \`@entitles\` claim and see what happened to it. There is no accept tool: a human accepts, with \`guardlink entitle\`. 303 │ `; 304 │ } 305 │ // @shield:end 306 │ 307 │ // ─── Agent instruction content (compact — points to reference doc) ─── 308 │ 309 │ // @shield:begin -- "agentInstructions renders the Quick Syntax block: verb lines at column 0 that document the grammar rather than claim anything" 310 │ /**
L309 shield agentInstructions renders the Quick Syntax block: verb lines at column 0 that document the grammar rather than claim anything
304 │ } 305 │ // @shield:end 306 │ 307 │ // ─── Agent instruction content (compact — points to reference doc) ─── 308 │ 309 │ // @shield:begin -- "agentInstructions renders the Quick Syntax block: verb lines at column 0 that document the grammar rather than claim anything" 310 │ /** 311 │ * Compact GuardLink instruction block injected into agent files. 312 │ * Points to docs/GUARDLINK_REFERENCE.md for full syntax. 313 │ * 314 │ * `mode` is not optional in spirit even though it is in the signature: D27 was
L418 shield Shielded region
413 │ --file common/archiver/filestore/archiver.go --line 61 \\ 414 │ --rationale "By design: the archival URI is namespace configuration. Authz: common/api/metadata.go:189" 415 │ \`\`\` 416 │ `.trimStart(); 417 │ } 418 │ // @shield:end 419 │ 420 │ // ─── Model-aware instruction block (for sync) ────────────────────── 421 │ 422 │ /** 423 │ * Build a threat model context section that gets embedded into agent instructions.
L906 shield README example annotations, excluded from parsing — they would otherwise register as real records
901 │ * Deliberately carries no wall-clock timestamp. It is regenerated on every 902 │ * `guardlink sync`, and a timestamp would make a tracked file churn on every 903 │ * run while telling the reader nothing they cannot get from `annotation_hash`, 904 │ * which changes only when the annotations do. 905 │ */ 906 │ // @shield:begin -- "README example annotations, excluded from parsing — they would otherwise register as real records" 907 │ export function guardlinkReadmeContent(project: ProjectInfo, ctx: ReadmeContext): string { 908 │ const defs = `definitions${project.definitionsExt}`; 909 │ const m = ctx.model; 910 │ const external = ctx.mode === 'external'; 911 │ const referencePath = ctx.referencePath;
L1218 shield Shielded region
1213 │ For CWE queries, check \`external_id.declared\` before reading \`count: 0\` as coverage:1214 │ \`false\` means this model has never heard of that weakness class, which is not the same1215 │ as declaring it and finding nothing exposed.1216 │ `;1217 │ }1218 │ // @shield:end1219 │
src/mcp/context.ts flow 2exposes 1mitigates 1comment 1 open 5
L17 exposes #mcp → #path-traversal
Caller-supplied file path is resolved against the project root
12 │ * 13 │ * The status vocabulary is the load-bearing part. "No annotations" and "never 14 │ * looked at" are different answers, and returning an empty result for both is 15 │ * how a caller concludes a file is clean when the parser simply never read it. 16 │ * 17 │ * @exposes #mcp to #path-traversal [medium] cwe:CWE-22 -- "Caller-supplied file path is resolved against the project root" 18 │ * @mitigates #mcp against #path-traversal using #path-validation -- "Paths are resolved then required to stay inside root; outside paths are reported, never read" 19 │ * @flows MCPClient -> #mcp via guardlink_context -- "File path input" 20 │ * @flows ThreatModel -> #mcp via fileContext -- "Model grouped by location.file" 21 │ * @comment -- "Pure projection over the parsed model; the only I/O is an existence check performed by the caller" 22 │ */
L18 mitigates #path-validation mitigates #path-traversal
Paths are resolved then required to stay inside root; outside paths are reported, never read
13 │ * The status vocabulary is the load-bearing part. "No annotations" and "never 14 │ * looked at" are different answers, and returning an empty result for both is 15 │ * how a caller concludes a file is clean when the parser simply never read it. 16 │ * 17 │ * @exposes #mcp to #path-traversal [medium] cwe:CWE-22 -- "Caller-supplied file path is resolved against the project root" 18 │ * @mitigates #mcp against #path-traversal using #path-validation -- "Paths are resolved then required to stay inside root; outside paths are reported, never read" 19 │ * @flows MCPClient -> #mcp via guardlink_context -- "File path input" 20 │ * @flows ThreatModel -> #mcp via fileContext -- "Model grouped by location.file" 21 │ * @comment -- "Pure projection over the parsed model; the only I/O is an existence check performed by the caller" 22 │ */ 23 │
L19 flow MCPClient → #mcp
File path input
14 │ * looked at" are different answers, and returning an empty result for both is 15 │ * how a caller concludes a file is clean when the parser simply never read it. 16 │ * 17 │ * @exposes #mcp to #path-traversal [medium] cwe:CWE-22 -- "Caller-supplied file path is resolved against the project root" 18 │ * @mitigates #mcp against #path-traversal using #path-validation -- "Paths are resolved then required to stay inside root; outside paths are reported, never read" 19 │ * @flows MCPClient -> #mcp via guardlink_context -- "File path input" 20 │ * @flows ThreatModel -> #mcp via fileContext -- "Model grouped by location.file" 21 │ * @comment -- "Pure projection over the parsed model; the only I/O is an existence check performed by the caller" 22 │ */ 23 │ 24 │ import { lookup, type LookupResult } from './lookup.js';
L20 flow ThreatModel → #mcp
Model grouped by location.file
15 │ * how a caller concludes a file is clean when the parser simply never read it. 16 │ * 17 │ * @exposes #mcp to #path-traversal [medium] cwe:CWE-22 -- "Caller-supplied file path is resolved against the project root" 18 │ * @mitigates #mcp against #path-traversal using #path-validation -- "Paths are resolved then required to stay inside root; outside paths are reported, never read" 19 │ * @flows MCPClient -> #mcp via guardlink_context -- "File path input" 20 │ * @flows ThreatModel -> #mcp via fileContext -- "Model grouped by location.file" 21 │ * @comment -- "Pure projection over the parsed model; the only I/O is an existence check performed by the caller" 22 │ */ 23 │ 24 │ import { lookup, type LookupResult } from './lookup.js'; 25 │ import { buildCoverageIndex } from '../parser/coverage.js';
L21 comment Pure projection over the parsed model; the only I/O is an existence check performed by the caller
Pure projection over the parsed model; the only I/O is an existence check performed by the caller
16 │ * 17 │ * @exposes #mcp to #path-traversal [medium] cwe:CWE-22 -- "Caller-supplied file path is resolved against the project root" 18 │ * @mitigates #mcp against #path-traversal using #path-validation -- "Paths are resolved then required to stay inside root; outside paths are reported, never read" 19 │ * @flows MCPClient -> #mcp via guardlink_context -- "File path input" 20 │ * @flows ThreatModel -> #mcp via fileContext -- "Model grouped by location.file" 21 │ * @comment -- "Pure projection over the parsed model; the only I/O is an existence check performed by the caller" 22 │ */ 23 │ 24 │ import { lookup, type LookupResult } from './lookup.js'; 25 │ import { buildCoverageIndex } from '../parser/coverage.js'; 26 │ import type {
src/mcp/freshness.ts flow 2exposes 1audit 1comment 1 open 5
L17 exposes #mcp → #info-disclosure
Envelope discloses the absolute project root and git SHA to the connected client
12 │ * that reads a known shape. 13 │ * 14 │ * Before this there was no metadata on the MCP surface at all — `populateMetadata` 15 │ * runs only in the CLI report path, and the tools returned bare payloads. 16 │ * 17 │ * @exposes #mcp to #info-disclosure [low] cwe:CWE-200 -- "Envelope discloses the absolute project root and git SHA to the connected client" 18 │ * @audit #mcp -- "Root and SHA are disclosed deliberately: without them a client cannot tell which repo answered (D9). Client is already trusted with the full threat model." 19 │ * @flows ThreatModel -> #mcp via buildEnvelope -- "Model content hashed for the freshness envelope" 20 │ * @flows GitRepo -> #mcp via readFileSync -- "HEAD read directly from .git, no subprocess" 21 │ * @comment -- "git SHA is read from .git/HEAD rather than spawned via execSync: this runs on every tool call" 22 │ */
L18 audit Audit: #mcp
Root and SHA are disclosed deliberately: without them a client cannot tell which repo answered (D9). Client is already trusted with the full threat model.
13 │ * 14 │ * Before this there was no metadata on the MCP surface at all — `populateMetadata` 15 │ * runs only in the CLI report path, and the tools returned bare payloads. 16 │ * 17 │ * @exposes #mcp to #info-disclosure [low] cwe:CWE-200 -- "Envelope discloses the absolute project root and git SHA to the connected client" 18 │ * @audit #mcp -- "Root and SHA are disclosed deliberately: without them a client cannot tell which repo answered (D9). Client is already trusted with the full threat model." 19 │ * @flows ThreatModel -> #mcp via buildEnvelope -- "Model content hashed for the freshness envelope" 20 │ * @flows GitRepo -> #mcp via readFileSync -- "HEAD read directly from .git, no subprocess" 21 │ * @comment -- "git SHA is read from .git/HEAD rather than spawned via execSync: this runs on every tool call" 22 │ */ 23 │
L19 flow ThreatModel → #mcp
Model content hashed for the freshness envelope
14 │ * Before this there was no metadata on the MCP surface at all — `populateMetadata` 15 │ * runs only in the CLI report path, and the tools returned bare payloads. 16 │ * 17 │ * @exposes #mcp to #info-disclosure [low] cwe:CWE-200 -- "Envelope discloses the absolute project root and git SHA to the connected client" 18 │ * @audit #mcp -- "Root and SHA are disclosed deliberately: without them a client cannot tell which repo answered (D9). Client is already trusted with the full threat model." 19 │ * @flows ThreatModel -> #mcp via buildEnvelope -- "Model content hashed for the freshness envelope" 20 │ * @flows GitRepo -> #mcp via readFileSync -- "HEAD read directly from .git, no subprocess" 21 │ * @comment -- "git SHA is read from .git/HEAD rather than spawned via execSync: this runs on every tool call" 22 │ */ 23 │ 24 │ import { resolve } from 'node:path';
L20 flow GitRepo → #mcp
HEAD read directly from .git, no subprocess
15 │ * runs only in the CLI report path, and the tools returned bare payloads. 16 │ * 17 │ * @exposes #mcp to #info-disclosure [low] cwe:CWE-200 -- "Envelope discloses the absolute project root and git SHA to the connected client" 18 │ * @audit #mcp -- "Root and SHA are disclosed deliberately: without them a client cannot tell which repo answered (D9). Client is already trusted with the full threat model." 19 │ * @flows ThreatModel -> #mcp via buildEnvelope -- "Model content hashed for the freshness envelope" 20 │ * @flows GitRepo -> #mcp via readFileSync -- "HEAD read directly from .git, no subprocess" 21 │ * @comment -- "git SHA is read from .git/HEAD rather than spawned via execSync: this runs on every tool call" 22 │ */ 23 │ 24 │ import { resolve } from 'node:path'; 25 │ import { readGitSha } from '../workspace/metadata.js';
L21 comment git SHA is read from .git/HEAD rather than spawned via execSync: this runs on every tool call
git SHA is read from .git/HEAD rather than spawned via execSync: this runs on every tool call
16 │ * 17 │ * @exposes #mcp to #info-disclosure [low] cwe:CWE-200 -- "Envelope discloses the absolute project root and git SHA to the connected client" 18 │ * @audit #mcp -- "Root and SHA are disclosed deliberately: without them a client cannot tell which repo answered (D9). Client is already trusted with the full threat model." 19 │ * @flows ThreatModel -> #mcp via buildEnvelope -- "Model content hashed for the freshness envelope" 20 │ * @flows GitRepo -> #mcp via readFileSync -- "HEAD read directly from .git, no subprocess" 21 │ * @comment -- "git SHA is read from .git/HEAD rather than spawned via execSync: this runs on every tool call" 22 │ */ 23 │ 24 │ import { resolve } from 'node:path'; 25 │ import { readGitSha } from '../workspace/metadata.js'; 26 │ import { computeAnnotationHash } from '../parser/annotation-hash.js';
src/mcp/instructions.ts flow 1comment 1 open 2
L25 flow ConfigFile → #mcp
Annotation mode read for the initialize instructions
20 │ * The tool *names* are prose, and are held accurate by a test that cross-checks 21 │ * every `guardlink_*` mentioned here against the server's real `tools/list`. A 22 │ * renamed or deleted tool fails that test rather than silently misdirecting an 23 │ * agent forever. 24 │ * 25 │ * @flows ConfigFile -> #mcp via readFileSync -- "Annotation mode read for the initialize instructions" 26 │ * @comment -- "Pure string builder; the only I/O is one optional config read by the caller" 27 │ */ 28 │ 29 │ import { SUPPORTED_QUERY_FORMS } from './lookup.js'; 30 │ import { readConfiguredMode, type AnnotationMode } from '../parser/annotation-mode.js';
L26 comment Pure string builder; the only I/O is one optional config read by the caller
Pure string builder; the only I/O is one optional config read by the caller
21 │ * every `guardlink_*` mentioned here against the server's real `tools/list`. A 22 │ * renamed or deleted tool fails that test rather than silently misdirecting an 23 │ * agent forever. 24 │ * 25 │ * @flows ConfigFile -> #mcp via readFileSync -- "Annotation mode read for the initialize instructions" 26 │ * @comment -- "Pure string builder; the only I/O is one optional config read by the caller" 27 │ */ 28 │ 29 │ import { SUPPORTED_QUERY_FORMS } from './lookup.js'; 30 │ import { readConfiguredMode, type AnnotationMode } from '../parser/annotation-mode.js'; 31 │
src/mcp/lookup.ts exposes 1mitigates 1flow 1comment 1 open 4
L19 exposes #mcp → #redos
Regex patterns applied to query strings
14 │ * - "actors" → declared principals (@actor) with the capabilities each is entitled to 15 │ * - "entitlements" / "entitlements for #actor" → @entitles claims, with citation and inert flag 16 │ * - "boundary #config" → boundaries involving asset 17 │ * - Free text → fuzzy match across assets, threats, controls 18 │ * 19 │ * @exposes #mcp to #redos [low] cwe:CWE-1333 -- "Regex patterns applied to query strings" 20 │ * @mitigates #mcp against #redos using #regex-anchoring -- "Patterns are simple and bounded" 21 │ * @flows QueryString -> #mcp via lookup -- "Query input path" 22 │ * @comment -- "Pure function; no I/O; operates on in-memory ThreatModel" 23 │ */ 24 │
L20 mitigates #regex-anchoring mitigates #redos
Patterns are simple and bounded
15 │ * - "entitlements" / "entitlements for #actor" → @entitles claims, with citation and inert flag 16 │ * - "boundary #config" → boundaries involving asset 17 │ * - Free text → fuzzy match across assets, threats, controls 18 │ * 19 │ * @exposes #mcp to #redos [low] cwe:CWE-1333 -- "Regex patterns applied to query strings" 20 │ * @mitigates #mcp against #redos using #regex-anchoring -- "Patterns are simple and bounded" 21 │ * @flows QueryString -> #mcp via lookup -- "Query input path" 22 │ * @comment -- "Pure function; no I/O; operates on in-memory ThreatModel" 23 │ */ 24 │ 25 │ import { buildCoverageIndex, findUnmitigatedExposures } from '../parser/coverage.js';
L21 flow QueryString → #mcp
Query input path
16 │ * - "boundary #config" → boundaries involving asset 17 │ * - Free text → fuzzy match across assets, threats, controls 18 │ * 19 │ * @exposes #mcp to #redos [low] cwe:CWE-1333 -- "Regex patterns applied to query strings" 20 │ * @mitigates #mcp against #redos using #regex-anchoring -- "Patterns are simple and bounded" 21 │ * @flows QueryString -> #mcp via lookup -- "Query input path" 22 │ * @comment -- "Pure function; no I/O; operates on in-memory ThreatModel" 23 │ */ 24 │ 25 │ import { buildCoverageIndex, findUnmitigatedExposures } from '../parser/coverage.js'; 26 │ import type {
L22 comment Pure function; no I/O; operates on in-memory ThreatModel
Pure function; no I/O; operates on in-memory ThreatModel
17 │ * - Free text → fuzzy match across assets, threats, controls 18 │ * 19 │ * @exposes #mcp to #redos [low] cwe:CWE-1333 -- "Regex patterns applied to query strings" 20 │ * @mitigates #mcp against #redos using #regex-anchoring -- "Patterns are simple and bounded" 21 │ * @flows QueryString -> #mcp via lookup -- "Query input path" 22 │ * @comment -- "Pure function; no I/O; operates on in-memory ThreatModel" 23 │ */ 24 │ 25 │ import { buildCoverageIndex, findUnmitigatedExposures } from '../parser/coverage.js'; 26 │ import type { 27 │ ThreatModel, ThreatModelAsset, ThreatModelThreat, ThreatModelControl,
src/mcp/subgraph.ts 4 stale exposes 2mitigates 2validates 1flow 1 open 7
L42 exposes #mcp → #dos
Unbounded depth on a dense graph expands the frontier
37 │ * made of refs that already exist in the model, and there is no user input left 38 │ * to interpret, so hops walk canonical identity only. Fuzzy matching at hop N 39 │ * would let `#cli` pull in `#llm-client` at every level and the frontier would 40 │ * blow out into everything sharing three characters. 41 │ * 42 │ * @exposes #mcp to #dos [low] cwe:CWE-400 -- "Unbounded depth on a dense graph expands the frontier" 43 │ * @mitigates #mcp against #dos using #resource-limits -- "Depth is clamped; visited set makes every node terminal, so cycles cannot revisit" 44 │ * @exposes #mcp to #info-disclosure [low] cwe:CWE-200 -- "D34: the subgraph spread carried the whole repo's unannotated_files inventory into a neighbourhood answer — 8094 paths, 654.8 KB, on a query that resolved nothing" 45 │ * @mitigates #mcp against #info-disclosure using #resource-limits -- "unannotated_files is filtered to the subgraph's own files here, and dropped entirely by withoutFileInventory at the graph emission boundary" 46 │ * @validates #resource-limits for #mcp -- "tests/graph-sparse-repo.test.ts builds two repos differing only in unannotated file count and pins that payload size does not follow it" 47 │ * @flows ThreatModel -> #mcp via selectSubgraph -- "Model filtered to a neighbourhood"
L43 mitigates #resource-limits mitigates #dos
Depth is clamped; visited set makes every node terminal, so cycles cannot revisit
38 │ * to interpret, so hops walk canonical identity only. Fuzzy matching at hop N 39 │ * would let `#cli` pull in `#llm-client` at every level and the frontier would 40 │ * blow out into everything sharing three characters. 41 │ * 42 │ * @exposes #mcp to #dos [low] cwe:CWE-400 -- "Unbounded depth on a dense graph expands the frontier" 43 │ * @mitigates #mcp against #dos using #resource-limits -- "Depth is clamped; visited set makes every node terminal, so cycles cannot revisit" 44 │ * @exposes #mcp to #info-disclosure [low] cwe:CWE-200 -- "D34: the subgraph spread carried the whole repo's unannotated_files inventory into a neighbourhood answer — 8094 paths, 654.8 KB, on a query that resolved nothing" 45 │ * @mitigates #mcp against #info-disclosure using #resource-limits -- "unannotated_files is filtered to the subgraph's own files here, and dropped entirely by withoutFileInventory at the graph emission boundary" 46 │ * @validates #resource-limits for #mcp -- "tests/graph-sparse-repo.test.ts builds two repos differing only in unannotated file count and pins that payload size does not follow it" 47 │ * @flows ThreatModel -> #mcp via selectSubgraph -- "Model filtered to a neighbourhood" 48 │ * @comment -- "Pure function over the model; returns a ThreatModel so the existing Mermaid generator needs no changes"
L44 exposes #mcp → #info-disclosure
D34: the subgraph spread carried the whole repo's unannotated_files inventory into a neighbourhood answer — 8094 paths, 654.8 KB, on a query that resolved nothing
39 │ * would let `#cli` pull in `#llm-client` at every level and the frontier would 40 │ * blow out into everything sharing three characters. 41 │ * 42 │ * @exposes #mcp to #dos [low] cwe:CWE-400 -- "Unbounded depth on a dense graph expands the frontier" 43 │ * @mitigates #mcp against #dos using #resource-limits -- "Depth is clamped; visited set makes every node terminal, so cycles cannot revisit" 44 │ * @exposes #mcp to #info-disclosure [low] cwe:CWE-200 -- "D34: the subgraph spread carried the whole repo's unannotated_files inventory into a neighbourhood answer — 8094 paths, 654.8 KB, on a query that resolved nothing" 45 │ * @mitigates #mcp against #info-disclosure using #resource-limits -- "unannotated_files is filtered to the subgraph's own files here, and dropped entirely by withoutFileInventory at the graph emission boundary" 46 │ * @validates #resource-limits for #mcp -- "tests/graph-sparse-repo.test.ts builds two repos differing only in unannotated file count and pins that payload size does not follow it" 47 │ * @flows ThreatModel -> #mcp via selectSubgraph -- "Model filtered to a neighbourhood" 48 │ * @comment -- "Pure function over the model; returns a ThreatModel so the existing Mermaid generator needs no changes" 49 │ */
L45 mitigates #resource-limits mitigates #info-disclosure
unannotated_files is filtered to the subgraph's own files here, and dropped entirely by withoutFileInventory at the graph emission boundary
40 │ * blow out into everything sharing three characters. 41 │ * 42 │ * @exposes #mcp to #dos [low] cwe:CWE-400 -- "Unbounded depth on a dense graph expands the frontier" 43 │ * @mitigates #mcp against #dos using #resource-limits -- "Depth is clamped; visited set makes every node terminal, so cycles cannot revisit" 44 │ * @exposes #mcp to #info-disclosure [low] cwe:CWE-200 -- "D34: the subgraph spread carried the whole repo's unannotated_files inventory into a neighbourhood answer — 8094 paths, 654.8 KB, on a query that resolved nothing" 45 │ * @mitigates #mcp against #info-disclosure using #resource-limits -- "unannotated_files is filtered to the subgraph's own files here, and dropped entirely by withoutFileInventory at the graph emission boundary" 46 │ * @validates #resource-limits for #mcp -- "tests/graph-sparse-repo.test.ts builds two repos differing only in unannotated file count and pins that payload size does not follow it" 47 │ * @flows ThreatModel -> #mcp via selectSubgraph -- "Model filtered to a neighbourhood" 48 │ * @comment -- "Pure function over the model; returns a ThreatModel so the existing Mermaid generator needs no changes" 49 │ */ 50 │
L46 validates #resource-limits validates #mcp
tests/graph-sparse-repo.test.ts builds two repos differing only in unannotated file count and pins that payload size does not follow it
41 │ * 42 │ * @exposes #mcp to #dos [low] cwe:CWE-400 -- "Unbounded depth on a dense graph expands the frontier" 43 │ * @mitigates #mcp against #dos using #resource-limits -- "Depth is clamped; visited set makes every node terminal, so cycles cannot revisit" 44 │ * @exposes #mcp to #info-disclosure [low] cwe:CWE-200 -- "D34: the subgraph spread carried the whole repo's unannotated_files inventory into a neighbourhood answer — 8094 paths, 654.8 KB, on a query that resolved nothing" 45 │ * @mitigates #mcp against #info-disclosure using #resource-limits -- "unannotated_files is filtered to the subgraph's own files here, and dropped entirely by withoutFileInventory at the graph emission boundary" 46 │ * @validates #resource-limits for #mcp -- "tests/graph-sparse-repo.test.ts builds two repos differing only in unannotated file count and pins that payload size does not follow it" 47 │ * @flows ThreatModel -> #mcp via selectSubgraph -- "Model filtered to a neighbourhood" 48 │ * @comment -- "Pure function over the model; returns a ThreatModel so the existing Mermaid generator needs no changes" 49 │ */ 50 │ 51 │ import { resolveAssetRef, type MatchKind } from './lookup.js';
L47 flow ThreatModel → #mcp
Model filtered to a neighbourhood
42 │ * @exposes #mcp to #dos [low] cwe:CWE-400 -- "Unbounded depth on a dense graph expands the frontier" 43 │ * @mitigates #mcp against #dos using #resource-limits -- "Depth is clamped; visited set makes every node terminal, so cycles cannot revisit" 44 │ * @exposes #mcp to #info-disclosure [low] cwe:CWE-200 -- "D34: the subgraph spread carried the whole repo's unannotated_files inventory into a neighbourhood answer — 8094 paths, 654.8 KB, on a query that resolved nothing" 45 │ * @mitigates #mcp against #info-disclosure using #resource-limits -- "unannotated_files is filtered to the subgraph's own files here, and dropped entirely by withoutFileInventory at the graph emission boundary" 46 │ * @validates #resource-limits for #mcp -- "tests/graph-sparse-repo.test.ts builds two repos differing only in unannotated file count and pins that payload size does not follow it" 47 │ * @flows ThreatModel -> #mcp via selectSubgraph -- "Model filtered to a neighbourhood" 48 │ * @comment -- "Pure function over the model; returns a ThreatModel so the existing Mermaid generator needs no changes" 49 │ */ 50 │ 51 │ import { resolveAssetRef, type MatchKind } from './lookup.js'; 52 │ import { filterByFeature } from '../parser/feature-filter.js';
L48 comment Pure function over the model; returns a ThreatModel so the existing Mermaid generator needs no changes
Pure function over the model; returns a ThreatModel so the existing Mermaid generator needs no changes
43 │ * @mitigates #mcp against #dos using #resource-limits -- "Depth is clamped; visited set makes every node terminal, so cycles cannot revisit" 44 │ * @exposes #mcp to #info-disclosure [low] cwe:CWE-200 -- "D34: the subgraph spread carried the whole repo's unannotated_files inventory into a neighbourhood answer — 8094 paths, 654.8 KB, on a query that resolved nothing" 45 │ * @mitigates #mcp against #info-disclosure using #resource-limits -- "unannotated_files is filtered to the subgraph's own files here, and dropped entirely by withoutFileInventory at the graph emission boundary" 46 │ * @validates #resource-limits for #mcp -- "tests/graph-sparse-repo.test.ts builds two repos differing only in unannotated file count and pins that payload size does not follow it" 47 │ * @flows ThreatModel -> #mcp via selectSubgraph -- "Model filtered to a neighbourhood" 48 │ * @comment -- "Pure function over the model; returns a ThreatModel so the existing Mermaid generator needs no changes" 49 │ */ 50 │ 51 │ import { resolveAssetRef, type MatchKind } from './lookup.js'; 52 │ import { filterByFeature } from '../parser/feature-filter.js'; 53 │ import { canonicaliser } from '../parser/canonical-ref.js';
src/parser/acceptance.ts comment 4exposes 2mitigates 2flow 2 open 12
L47 exposes #parser → #config-tamper
readAcceptancePolicy reads .guardlink/config.json to decide how strict this project's acceptance rule is, so anyone who can edit that file can lower the bar their own acceptances have to clear
42 │ * every count above it looks healthier for its absence (the argument 43 │ * `ci/index.ts` already makes about parse diagnostics). An unattributed 44 │ * acceptance parses, lands in the model, and is then NAMED — by the gate, by 45 │ * `validate`, and in the review UI. Loud beats absent. 46 │ * 47 │ * @exposes #parser to #config-tamper [medium] cwe:CWE-15 -- "readAcceptancePolicy reads .guardlink/config.json to decide how strict this project's acceptance rule is, so anyone who can edit that file can lower the bar their own acceptances have to clear" 48 │ * @mitigates #parser against #config-tamper using #config-validation -- "Every field is type- and range-checked and falls back to the built-in default; an absent, unreadable or malformed config yields DEFAULT_ACCEPTANCE_POLICY rather than an off switch, so a broken config cannot disable the policy by being broken" 49 │ * @comment -- "What config CANNOT reach is the point: scope and expiry semantics are not settings. A project can move a threshold; it cannot declare that an acceptance covers a file it was not written in, because that is what the word means rather than a preference" 50 │ * @audit #parser -- "The thresholds here are the price of an acceptance. Lowering min_justification or raising max_horizon_days in a PR is a governance change wearing a config diff, and deserves the same review as the acceptances it will admit" 51 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-502 -- "JSON.parse of a repository file" 52 │ * @mitigates #parser against #insecure-deser using #config-validation -- "Parsed inside try/catch; only four scalar fields are read, each through a type guard, and nothing from the file is executed or used as a path"
L48 mitigates #config-validation mitigates #config-tamper
Every field is type- and range-checked and falls back to the built-in default; an absent, unreadable or malformed config yields DEFAULT_ACCEPTANCE_POLICY rather than an off switch, so a broken config cannot disable the policy by being broken
43 │ * `ci/index.ts` already makes about parse diagnostics). An unattributed 44 │ * acceptance parses, lands in the model, and is then NAMED — by the gate, by 45 │ * `validate`, and in the review UI. Loud beats absent. 46 │ * 47 │ * @exposes #parser to #config-tamper [medium] cwe:CWE-15 -- "readAcceptancePolicy reads .guardlink/config.json to decide how strict this project's acceptance rule is, so anyone who can edit that file can lower the bar their own acceptances have to clear" 48 │ * @mitigates #parser against #config-tamper using #config-validation -- "Every field is type- and range-checked and falls back to the built-in default; an absent, unreadable or malformed config yields DEFAULT_ACCEPTANCE_POLICY rather than an off switch, so a broken config cannot disable the policy by being broken" 49 │ * @comment -- "What config CANNOT reach is the point: scope and expiry semantics are not settings. A project can move a threshold; it cannot declare that an acceptance covers a file it was not written in, because that is what the word means rather than a preference" 50 │ * @audit #parser -- "The thresholds here are the price of an acceptance. Lowering min_justification or raising max_horizon_days in a PR is a governance change wearing a config diff, and deserves the same review as the acceptances it will admit" 51 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-502 -- "JSON.parse of a repository file" 52 │ * @mitigates #parser against #insecure-deser using #config-validation -- "Parsed inside try/catch; only four scalar fields are read, each through a type guard, and nothing from the file is executed or used as a path" 53 │ * @flows ThreatModel -> #parser via findAcceptanceDefects -- "Acceptances read and checked against policy"
L49 comment What config CANNOT reach is the point: scope and expiry semantics are not settings. A project can move a threshold; it cannot declare that an acceptance covers a file it was not written in, because that is what the word means rather than a preference
What config CANNOT reach is the point: scope and expiry semantics are not settings. A project can move a threshold; it cannot declare that an acceptance covers a file it was not written in, because that is what the word means rather than a preference
44 │ * acceptance parses, lands in the model, and is then NAMED — by the gate, by 45 │ * `validate`, and in the review UI. Loud beats absent. 46 │ * 47 │ * @exposes #parser to #config-tamper [medium] cwe:CWE-15 -- "readAcceptancePolicy reads .guardlink/config.json to decide how strict this project's acceptance rule is, so anyone who can edit that file can lower the bar their own acceptances have to clear" 48 │ * @mitigates #parser against #config-tamper using #config-validation -- "Every field is type- and range-checked and falls back to the built-in default; an absent, unreadable or malformed config yields DEFAULT_ACCEPTANCE_POLICY rather than an off switch, so a broken config cannot disable the policy by being broken" 49 │ * @comment -- "What config CANNOT reach is the point: scope and expiry semantics are not settings. A project can move a threshold; it cannot declare that an acceptance covers a file it was not written in, because that is what the word means rather than a preference" 50 │ * @audit #parser -- "The thresholds here are the price of an acceptance. Lowering min_justification or raising max_horizon_days in a PR is a governance change wearing a config diff, and deserves the same review as the acceptances it will admit" 51 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-502 -- "JSON.parse of a repository file" 52 │ * @mitigates #parser against #insecure-deser using #config-validation -- "Parsed inside try/catch; only four scalar fields are read, each through a type guard, and nothing from the file is executed or used as a path" 53 │ * @flows ThreatModel -> #parser via findAcceptanceDefects -- "Acceptances read and checked against policy" 54 │ * @flows ConfigFile -> #parser via readAcceptancePolicy -- "Per-project policy thresholds, read only"
L50 audit Audit: #parser
The thresholds here are the price of an acceptance. Lowering min_justification or raising max_horizon_days in a PR is a governance change wearing a config diff, and deserves the same review as the acceptances it will admit
45 │ * `validate`, and in the review UI. Loud beats absent. 46 │ * 47 │ * @exposes #parser to #config-tamper [medium] cwe:CWE-15 -- "readAcceptancePolicy reads .guardlink/config.json to decide how strict this project's acceptance rule is, so anyone who can edit that file can lower the bar their own acceptances have to clear" 48 │ * @mitigates #parser against #config-tamper using #config-validation -- "Every field is type- and range-checked and falls back to the built-in default; an absent, unreadable or malformed config yields DEFAULT_ACCEPTANCE_POLICY rather than an off switch, so a broken config cannot disable the policy by being broken" 49 │ * @comment -- "What config CANNOT reach is the point: scope and expiry semantics are not settings. A project can move a threshold; it cannot declare that an acceptance covers a file it was not written in, because that is what the word means rather than a preference" 50 │ * @audit #parser -- "The thresholds here are the price of an acceptance. Lowering min_justification or raising max_horizon_days in a PR is a governance change wearing a config diff, and deserves the same review as the acceptances it will admit" 51 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-502 -- "JSON.parse of a repository file" 52 │ * @mitigates #parser against #insecure-deser using #config-validation -- "Parsed inside try/catch; only four scalar fields are read, each through a type guard, and nothing from the file is executed or used as a path" 53 │ * @flows ThreatModel -> #parser via findAcceptanceDefects -- "Acceptances read and checked against policy" 54 │ * @flows ConfigFile -> #parser via readAcceptancePolicy -- "Per-project policy thresholds, read only" 55 │ * @handles internal on #parser -- "Reads the reviewer name and justification text an acceptance carries"
L51 exposes #parser → #insecure-deser
JSON.parse of a repository file
46 │ * 47 │ * @exposes #parser to #config-tamper [medium] cwe:CWE-15 -- "readAcceptancePolicy reads .guardlink/config.json to decide how strict this project's acceptance rule is, so anyone who can edit that file can lower the bar their own acceptances have to clear" 48 │ * @mitigates #parser against #config-tamper using #config-validation -- "Every field is type- and range-checked and falls back to the built-in default; an absent, unreadable or malformed config yields DEFAULT_ACCEPTANCE_POLICY rather than an off switch, so a broken config cannot disable the policy by being broken" 49 │ * @comment -- "What config CANNOT reach is the point: scope and expiry semantics are not settings. A project can move a threshold; it cannot declare that an acceptance covers a file it was not written in, because that is what the word means rather than a preference" 50 │ * @audit #parser -- "The thresholds here are the price of an acceptance. Lowering min_justification or raising max_horizon_days in a PR is a governance change wearing a config diff, and deserves the same review as the acceptances it will admit" 51 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-502 -- "JSON.parse of a repository file" 52 │ * @mitigates #parser against #insecure-deser using #config-validation -- "Parsed inside try/catch; only four scalar fields are read, each through a type guard, and nothing from the file is executed or used as a path" 53 │ * @flows ThreatModel -> #parser via findAcceptanceDefects -- "Acceptances read and checked against policy" 54 │ * @flows ConfigFile -> #parser via readAcceptancePolicy -- "Per-project policy thresholds, read only" 55 │ * @handles internal on #parser -- "Reads the reviewer name and justification text an acceptance carries" 56 │ * @comment -- "Pure functions apart from readAcceptancePolicy; `now` is a parameter so a test can pin the clock rather than skew it"
L52 mitigates #config-validation mitigates #insecure-deser
Parsed inside try/catch; only four scalar fields are read, each through a type guard, and nothing from the file is executed or used as a path
47 │ * @exposes #parser to #config-tamper [medium] cwe:CWE-15 -- "readAcceptancePolicy reads .guardlink/config.json to decide how strict this project's acceptance rule is, so anyone who can edit that file can lower the bar their own acceptances have to clear" 48 │ * @mitigates #parser against #config-tamper using #config-validation -- "Every field is type- and range-checked and falls back to the built-in default; an absent, unreadable or malformed config yields DEFAULT_ACCEPTANCE_POLICY rather than an off switch, so a broken config cannot disable the policy by being broken" 49 │ * @comment -- "What config CANNOT reach is the point: scope and expiry semantics are not settings. A project can move a threshold; it cannot declare that an acceptance covers a file it was not written in, because that is what the word means rather than a preference" 50 │ * @audit #parser -- "The thresholds here are the price of an acceptance. Lowering min_justification or raising max_horizon_days in a PR is a governance change wearing a config diff, and deserves the same review as the acceptances it will admit" 51 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-502 -- "JSON.parse of a repository file" 52 │ * @mitigates #parser against #insecure-deser using #config-validation -- "Parsed inside try/catch; only four scalar fields are read, each through a type guard, and nothing from the file is executed or used as a path" 53 │ * @flows ThreatModel -> #parser via findAcceptanceDefects -- "Acceptances read and checked against policy" 54 │ * @flows ConfigFile -> #parser via readAcceptancePolicy -- "Per-project policy thresholds, read only" 55 │ * @handles internal on #parser -- "Reads the reviewer name and justification text an acceptance carries" 56 │ * @comment -- "Pure functions apart from readAcceptancePolicy; `now` is a parameter so a test can pin the clock rather than skew it" 57 │ * @comment -- "Scope and expiry are enforced in coverage.ts, not here — this module defines them and answers questions about them, coverage applies them"
L53 flow ThreatModel → #parser
Acceptances read and checked against policy
48 │ * @mitigates #parser against #config-tamper using #config-validation -- "Every field is type- and range-checked and falls back to the built-in default; an absent, unreadable or malformed config yields DEFAULT_ACCEPTANCE_POLICY rather than an off switch, so a broken config cannot disable the policy by being broken" 49 │ * @comment -- "What config CANNOT reach is the point: scope and expiry semantics are not settings. A project can move a threshold; it cannot declare that an acceptance covers a file it was not written in, because that is what the word means rather than a preference" 50 │ * @audit #parser -- "The thresholds here are the price of an acceptance. Lowering min_justification or raising max_horizon_days in a PR is a governance change wearing a config diff, and deserves the same review as the acceptances it will admit" 51 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-502 -- "JSON.parse of a repository file" 52 │ * @mitigates #parser against #insecure-deser using #config-validation -- "Parsed inside try/catch; only four scalar fields are read, each through a type guard, and nothing from the file is executed or used as a path" 53 │ * @flows ThreatModel -> #parser via findAcceptanceDefects -- "Acceptances read and checked against policy" 54 │ * @flows ConfigFile -> #parser via readAcceptancePolicy -- "Per-project policy thresholds, read only" 55 │ * @handles internal on #parser -- "Reads the reviewer name and justification text an acceptance carries" 56 │ * @comment -- "Pure functions apart from readAcceptancePolicy; `now` is a parameter so a test can pin the clock rather than skew it" 57 │ * @comment -- "Scope and expiry are enforced in coverage.ts, not here — this module defines them and answers questions about them, coverage applies them" 58 │ * @comment -- "acceptanceBlastRadius is read by guardlink review BEFORE the justification prompt: a reviewer typing one line sees how many exposures it removes from the gate and from the SARIF a pentest reads"
L54 flow ConfigFile → #parser
Per-project policy thresholds, read only
49 │ * @comment -- "What config CANNOT reach is the point: scope and expiry semantics are not settings. A project can move a threshold; it cannot declare that an acceptance covers a file it was not written in, because that is what the word means rather than a preference" 50 │ * @audit #parser -- "The thresholds here are the price of an acceptance. Lowering min_justification or raising max_horizon_days in a PR is a governance change wearing a config diff, and deserves the same review as the acceptances it will admit" 51 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-502 -- "JSON.parse of a repository file" 52 │ * @mitigates #parser against #insecure-deser using #config-validation -- "Parsed inside try/catch; only four scalar fields are read, each through a type guard, and nothing from the file is executed or used as a path" 53 │ * @flows ThreatModel -> #parser via findAcceptanceDefects -- "Acceptances read and checked against policy" 54 │ * @flows ConfigFile -> #parser via readAcceptancePolicy -- "Per-project policy thresholds, read only" 55 │ * @handles internal on #parser -- "Reads the reviewer name and justification text an acceptance carries" 56 │ * @comment -- "Pure functions apart from readAcceptancePolicy; `now` is a parameter so a test can pin the clock rather than skew it" 57 │ * @comment -- "Scope and expiry are enforced in coverage.ts, not here — this module defines them and answers questions about them, coverage applies them" 58 │ * @comment -- "acceptanceBlastRadius is read by guardlink review BEFORE the justification prompt: a reviewer typing one line sees how many exposures it removes from the gate and from the SARIF a pentest reads" 59 │ */
L55 handles #parser: internal
Reads the reviewer name and justification text an acceptance carries
50 │ * @audit #parser -- "The thresholds here are the price of an acceptance. Lowering min_justification or raising max_horizon_days in a PR is a governance change wearing a config diff, and deserves the same review as the acceptances it will admit" 51 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-502 -- "JSON.parse of a repository file" 52 │ * @mitigates #parser against #insecure-deser using #config-validation -- "Parsed inside try/catch; only four scalar fields are read, each through a type guard, and nothing from the file is executed or used as a path" 53 │ * @flows ThreatModel -> #parser via findAcceptanceDefects -- "Acceptances read and checked against policy" 54 │ * @flows ConfigFile -> #parser via readAcceptancePolicy -- "Per-project policy thresholds, read only" 55 │ * @handles internal on #parser -- "Reads the reviewer name and justification text an acceptance carries" 56 │ * @comment -- "Pure functions apart from readAcceptancePolicy; `now` is a parameter so a test can pin the clock rather than skew it" 57 │ * @comment -- "Scope and expiry are enforced in coverage.ts, not here — this module defines them and answers questions about them, coverage applies them" 58 │ * @comment -- "acceptanceBlastRadius is read by guardlink review BEFORE the justification prompt: a reviewer typing one line sees how many exposures it removes from the gate and from the SARIF a pentest reads" 59 │ */ 60 │
L56 comment Pure functions apart from readAcceptancePolicy; `now` is a parameter so a test can pin the clock rather than skew it
Pure functions apart from readAcceptancePolicy; `now` is a parameter so a test can pin the clock rather than skew it
51 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-502 -- "JSON.parse of a repository file" 52 │ * @mitigates #parser against #insecure-deser using #config-validation -- "Parsed inside try/catch; only four scalar fields are read, each through a type guard, and nothing from the file is executed or used as a path" 53 │ * @flows ThreatModel -> #parser via findAcceptanceDefects -- "Acceptances read and checked against policy" 54 │ * @flows ConfigFile -> #parser via readAcceptancePolicy -- "Per-project policy thresholds, read only" 55 │ * @handles internal on #parser -- "Reads the reviewer name and justification text an acceptance carries" 56 │ * @comment -- "Pure functions apart from readAcceptancePolicy; `now` is a parameter so a test can pin the clock rather than skew it" 57 │ * @comment -- "Scope and expiry are enforced in coverage.ts, not here — this module defines them and answers questions about them, coverage applies them" 58 │ * @comment -- "acceptanceBlastRadius is read by guardlink review BEFORE the justification prompt: a reviewer typing one line sees how many exposures it removes from the gate and from the SARIF a pentest reads" 59 │ */ 60 │ 61 │ import { readFileSync } from 'node:fs';
L57 comment Scope and expiry are enforced in coverage.ts, not here — this module defines them and answers questions about them, coverage applies them
Scope and expiry are enforced in coverage.ts, not here — this module defines them and answers questions about them, coverage applies them
52 │ * @mitigates #parser against #insecure-deser using #config-validation -- "Parsed inside try/catch; only four scalar fields are read, each through a type guard, and nothing from the file is executed or used as a path" 53 │ * @flows ThreatModel -> #parser via findAcceptanceDefects -- "Acceptances read and checked against policy" 54 │ * @flows ConfigFile -> #parser via readAcceptancePolicy -- "Per-project policy thresholds, read only" 55 │ * @handles internal on #parser -- "Reads the reviewer name and justification text an acceptance carries" 56 │ * @comment -- "Pure functions apart from readAcceptancePolicy; `now` is a parameter so a test can pin the clock rather than skew it" 57 │ * @comment -- "Scope and expiry are enforced in coverage.ts, not here — this module defines them and answers questions about them, coverage applies them" 58 │ * @comment -- "acceptanceBlastRadius is read by guardlink review BEFORE the justification prompt: a reviewer typing one line sees how many exposures it removes from the gate and from the SARIF a pentest reads" 59 │ */ 60 │ 61 │ import { readFileSync } from 'node:fs'; 62 │ import { join } from 'node:path';
L58 comment acceptanceBlastRadius is read by guardlink review BEFORE the justification prompt: a reviewer typing one line sees how many exposures it removes from the gate and from the SARIF a pentest reads
acceptanceBlastRadius is read by guardlink review BEFORE the justification prompt: a reviewer typing one line sees how many exposures it removes from the gate and from the SARIF a pentest reads
53 │ * @flows ThreatModel -> #parser via findAcceptanceDefects -- "Acceptances read and checked against policy" 54 │ * @flows ConfigFile -> #parser via readAcceptancePolicy -- "Per-project policy thresholds, read only" 55 │ * @handles internal on #parser -- "Reads the reviewer name and justification text an acceptance carries" 56 │ * @comment -- "Pure functions apart from readAcceptancePolicy; `now` is a parameter so a test can pin the clock rather than skew it" 57 │ * @comment -- "Scope and expiry are enforced in coverage.ts, not here — this module defines them and answers questions about them, coverage applies them" 58 │ * @comment -- "acceptanceBlastRadius is read by guardlink review BEFORE the justification prompt: a reviewer typing one line sees how many exposures it removes from the gate and from the SARIF a pentest reads" 59 │ */ 60 │ 61 │ import { readFileSync } from 'node:fs'; 62 │ import { join } from 'node:path'; 63 │ import type { ThreatModel, ThreatModelAcceptance, ThreatModelExposure, SourceLocation } from '../types/index.js';
src/parser/annotation-hash.ts comment 4flow 1assumes 1 open 6
L28 flow ThreatModel → #parser
Model content read for hashing
23 │ * 24 │ * Records are sorted before hashing, so reordering annotations within a file 25 │ * does not change the result. They are sorted as a multiset — duplicates are 26 │ * kept — so deleting one of two identical annotations still changes the hash. 27 │ * 28 │ * @flows ThreatModel -> #parser via computeAnnotationHash -- "Model content read for hashing" 29 │ * @comment -- "Pure function; no I/O; deterministic across machines and annotation modes" 30 │ * @assumes #parser -- "location.file is the logical source path in both inline and external mode (parse-file.ts resolves @source before the model is assembled)" 31 │ * @comment -- "Excludes line, origin_file, origin_line and parent_symbol so inline and external authoring of the same model hash identically" 32 │ * @comment -- "v2 adds @actor and @entitles records. Before that a migration could drop or rewrite every entitlement in a repo and the hash gate would still report the model unchanged — a silent all-clear, which is the failure mode the entitlement design exists to prevent (actor-entitlement design §2)" 33 │ * @comment -- "v3 adds an acceptance's accepted_by and expires. Those two fields decide whether an acceptance still covers anything, so a hash that could not see them would call a renewed or re-signed governance decision 'unchanged'"
L29 comment Pure function; no I/O; deterministic across machines and annotation modes
Pure function; no I/O; deterministic across machines and annotation modes
24 │ * Records are sorted before hashing, so reordering annotations within a file 25 │ * does not change the result. They are sorted as a multiset — duplicates are 26 │ * kept — so deleting one of two identical annotations still changes the hash. 27 │ * 28 │ * @flows ThreatModel -> #parser via computeAnnotationHash -- "Model content read for hashing" 29 │ * @comment -- "Pure function; no I/O; deterministic across machines and annotation modes" 30 │ * @assumes #parser -- "location.file is the logical source path in both inline and external mode (parse-file.ts resolves @source before the model is assembled)" 31 │ * @comment -- "Excludes line, origin_file, origin_line and parent_symbol so inline and external authoring of the same model hash identically" 32 │ * @comment -- "v2 adds @actor and @entitles records. Before that a migration could drop or rewrite every entitlement in a repo and the hash gate would still report the model unchanged — a silent all-clear, which is the failure mode the entitlement design exists to prevent (actor-entitlement design §2)" 33 │ * @comment -- "v3 adds an acceptance's accepted_by and expires. Those two fields decide whether an acceptance still covers anything, so a hash that could not see them would call a renewed or re-signed governance decision 'unchanged'" 34 │ */
L30 assumes Assumes: #parser
location.file is the logical source path in both inline and external mode (parse-file.ts resolves @source before the model is assembled)
25 │ * does not change the result. They are sorted as a multiset — duplicates are 26 │ * kept — so deleting one of two identical annotations still changes the hash. 27 │ * 28 │ * @flows ThreatModel -> #parser via computeAnnotationHash -- "Model content read for hashing" 29 │ * @comment -- "Pure function; no I/O; deterministic across machines and annotation modes" 30 │ * @assumes #parser -- "location.file is the logical source path in both inline and external mode (parse-file.ts resolves @source before the model is assembled)" 31 │ * @comment -- "Excludes line, origin_file, origin_line and parent_symbol so inline and external authoring of the same model hash identically" 32 │ * @comment -- "v2 adds @actor and @entitles records. Before that a migration could drop or rewrite every entitlement in a repo and the hash gate would still report the model unchanged — a silent all-clear, which is the failure mode the entitlement design exists to prevent (actor-entitlement design §2)" 33 │ * @comment -- "v3 adds an acceptance's accepted_by and expires. Those two fields decide whether an acceptance still covers anything, so a hash that could not see them would call a renewed or re-signed governance decision 'unchanged'" 34 │ */ 35 │
L31 comment Excludes line, origin_file, origin_line and parent_symbol so inline and external authoring of the same model hash identically
Excludes line, origin_file, origin_line and parent_symbol so inline and external authoring of the same model hash identically
26 │ * kept — so deleting one of two identical annotations still changes the hash. 27 │ * 28 │ * @flows ThreatModel -> #parser via computeAnnotationHash -- "Model content read for hashing" 29 │ * @comment -- "Pure function; no I/O; deterministic across machines and annotation modes" 30 │ * @assumes #parser -- "location.file is the logical source path in both inline and external mode (parse-file.ts resolves @source before the model is assembled)" 31 │ * @comment -- "Excludes line, origin_file, origin_line and parent_symbol so inline and external authoring of the same model hash identically" 32 │ * @comment -- "v2 adds @actor and @entitles records. Before that a migration could drop or rewrite every entitlement in a repo and the hash gate would still report the model unchanged — a silent all-clear, which is the failure mode the entitlement design exists to prevent (actor-entitlement design §2)" 33 │ * @comment -- "v3 adds an acceptance's accepted_by and expires. Those two fields decide whether an acceptance still covers anything, so a hash that could not see them would call a renewed or re-signed governance decision 'unchanged'" 34 │ */ 35 │ 36 │ import { createHash } from 'node:crypto';
L32 comment v2 adds @actor and @entitles records. Before that a migration could drop or rewrite every entitlement in a repo and the hash gate would still report the model unchanged — a silent all-clear, which is the failure mode the entitlement design exists to prevent (actor-entitlement design §2)
v2 adds @actor and @entitles records. Before that a migration could drop or rewrite every entitlement in a repo and the hash gate would still report the model unchanged — a silent all-clear, which is the failure mode the entitlement design exists to prevent (actor-entitlement design §2)
27 │ * 28 │ * @flows ThreatModel -> #parser via computeAnnotationHash -- "Model content read for hashing" 29 │ * @comment -- "Pure function; no I/O; deterministic across machines and annotation modes" 30 │ * @assumes #parser -- "location.file is the logical source path in both inline and external mode (parse-file.ts resolves @source before the model is assembled)" 31 │ * @comment -- "Excludes line, origin_file, origin_line and parent_symbol so inline and external authoring of the same model hash identically" 32 │ * @comment -- "v2 adds @actor and @entitles records. Before that a migration could drop or rewrite every entitlement in a repo and the hash gate would still report the model unchanged — a silent all-clear, which is the failure mode the entitlement design exists to prevent (actor-entitlement design §2)" 33 │ * @comment -- "v3 adds an acceptance's accepted_by and expires. Those two fields decide whether an acceptance still covers anything, so a hash that could not see them would call a renewed or re-signed governance decision 'unchanged'" 34 │ */ 35 │ 36 │ import { createHash } from 'node:crypto'; 37 │ import type { ThreatModel, SourceLocation } from '../types/index.js';
L33 comment v3 adds an acceptance's accepted_by and expires. Those two fields decide whether an acceptance still covers anything, so a hash that could not see them would call a renewed or re-signed governance decision 'unchanged'
v3 adds an acceptance's accepted_by and expires. Those two fields decide whether an acceptance still covers anything, so a hash that could not see them would call a renewed or re-signed governance decision 'unchanged'
28 │ * @flows ThreatModel -> #parser via computeAnnotationHash -- "Model content read for hashing" 29 │ * @comment -- "Pure function; no I/O; deterministic across machines and annotation modes" 30 │ * @assumes #parser -- "location.file is the logical source path in both inline and external mode (parse-file.ts resolves @source before the model is assembled)" 31 │ * @comment -- "Excludes line, origin_file, origin_line and parent_symbol so inline and external authoring of the same model hash identically" 32 │ * @comment -- "v2 adds @actor and @entitles records. Before that a migration could drop or rewrite every entitlement in a repo and the hash gate would still report the model unchanged — a silent all-clear, which is the failure mode the entitlement design exists to prevent (actor-entitlement design §2)" 33 │ * @comment -- "v3 adds an acceptance's accepted_by and expires. Those two fields decide whether an acceptance still covers anything, so a hash that could not see them would call a renewed or re-signed governance decision 'unchanged'" 34 │ */ 35 │ 36 │ import { createHash } from 'node:crypto'; 37 │ import type { ThreatModel, SourceLocation } from '../types/index.js'; 38 │
src/parser/annotation-mode.ts flow 1comment 1 open 2
L10 flow ThreatModel → #parser
Annotation locations read to infer storage mode
5 │ * records include/exclude globs but not the mode, so the annotations themselves 6 │ * are the only authority. An annotation carries `location.origin_file` when it 7 │ * was read from a standalone `.gal` sidecar and resolved back to its logical 8 │ * source through `@source`; inline annotations have no origin. 9 │ * 10 │ * @flows ThreatModel -> #parser via detectAnnotationMode -- "Annotation locations read to infer storage mode" 11 │ * @comment -- "Pure function; mixed is a real answer, not an error — a repo mid-migration is genuinely both" 12 │ */ 13 │ 14 │ import { readFileSync } from 'node:fs'; 15 │ import { join } from 'node:path';
L11 comment Pure function; mixed is a real answer, not an error — a repo mid-migration is genuinely both
Pure function; mixed is a real answer, not an error — a repo mid-migration is genuinely both
6 │ * are the only authority. An annotation carries `location.origin_file` when it 7 │ * was read from a standalone `.gal` sidecar and resolved back to its logical 8 │ * source through `@source`; inline annotations have no origin. 9 │ * 10 │ * @flows ThreatModel -> #parser via detectAnnotationMode -- "Annotation locations read to infer storage mode" 11 │ * @comment -- "Pure function; mixed is a real answer, not an error — a repo mid-migration is genuinely both" 12 │ */ 13 │ 14 │ import { readFileSync } from 'node:fs'; 15 │ import { join } from 'node:path'; 16 │ import type { ThreatModel, SourceLocation } from '../types/index.js';
src/parser/apply-annotations.ts exposes 2mitigates 2flow 2comment 1 open 7
L17 exposes #parser → #arbitrary-write
Writes annotation sidecars from tool input
12 │ * by declaring it acceptable; a tool that can write `@entitles` lets it close one 13 │ * by declaring the caller was allowed all along — and that second route would 14 │ * bypass the propose/accept ledger in src/review/entitlements.ts entirely, which 15 │ * is the gate that makes an entitlement carry a human's name (design §3.6). 16 │ * 17 │ * @exposes #parser to #arbitrary-write [high] cwe:CWE-73 -- "Writes annotation sidecars from tool input" 18 │ * @mitigates #parser against #arbitrary-write using #path-validation -- "Target is always resolveGalPath(root, file); the caller cannot choose the path, and a file escaping root is rejected" 19 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-20 -- "Annotation lines arrive as caller-supplied text" 20 │ * @mitigates #parser against #insecure-deser using #input-sanitize -- "Every line is re-parsed with parseLine before write; anything that does not parse, or parses as @accepts, is rejected" 21 │ * @flows MCPClient -> #parser via applyAnnotations -- "Structured annotation write path" 22 │ * @flows #parser -> FileSystem via writeFileSync -- "Sidecar append"
L18 mitigates #path-validation mitigates #arbitrary-write
Target is always resolveGalPath(root, file); the caller cannot choose the path, and a file escaping root is rejected
13 │ * by declaring the caller was allowed all along — and that second route would 14 │ * bypass the propose/accept ledger in src/review/entitlements.ts entirely, which 15 │ * is the gate that makes an entitlement carry a human's name (design §3.6). 16 │ * 17 │ * @exposes #parser to #arbitrary-write [high] cwe:CWE-73 -- "Writes annotation sidecars from tool input" 18 │ * @mitigates #parser against #arbitrary-write using #path-validation -- "Target is always resolveGalPath(root, file); the caller cannot choose the path, and a file escaping root is rejected" 19 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-20 -- "Annotation lines arrive as caller-supplied text" 20 │ * @mitigates #parser against #insecure-deser using #input-sanitize -- "Every line is re-parsed with parseLine before write; anything that does not parse, or parses as @accepts, is rejected" 21 │ * @flows MCPClient -> #parser via applyAnnotations -- "Structured annotation write path" 22 │ * @flows #parser -> FileSystem via writeFileSync -- "Sidecar append" 23 │ * @comment -- "Idempotent by construction: an identical @source block is detected and skipped rather than duplicated"
L19 exposes #parser → #insecure-deser
Annotation lines arrive as caller-supplied text
14 │ * bypass the propose/accept ledger in src/review/entitlements.ts entirely, which 15 │ * is the gate that makes an entitlement carry a human's name (design §3.6). 16 │ * 17 │ * @exposes #parser to #arbitrary-write [high] cwe:CWE-73 -- "Writes annotation sidecars from tool input" 18 │ * @mitigates #parser against #arbitrary-write using #path-validation -- "Target is always resolveGalPath(root, file); the caller cannot choose the path, and a file escaping root is rejected" 19 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-20 -- "Annotation lines arrive as caller-supplied text" 20 │ * @mitigates #parser against #insecure-deser using #input-sanitize -- "Every line is re-parsed with parseLine before write; anything that does not parse, or parses as @accepts, is rejected" 21 │ * @flows MCPClient -> #parser via applyAnnotations -- "Structured annotation write path" 22 │ * @flows #parser -> FileSystem via writeFileSync -- "Sidecar append" 23 │ * @comment -- "Idempotent by construction: an identical @source block is detected and skipped rather than duplicated" 24 │ */
L20 mitigates #input-sanitize mitigates #insecure-deser
Every line is re-parsed with parseLine before write; anything that does not parse, or parses as @accepts, is rejected
15 │ * is the gate that makes an entitlement carry a human's name (design §3.6). 16 │ * 17 │ * @exposes #parser to #arbitrary-write [high] cwe:CWE-73 -- "Writes annotation sidecars from tool input" 18 │ * @mitigates #parser against #arbitrary-write using #path-validation -- "Target is always resolveGalPath(root, file); the caller cannot choose the path, and a file escaping root is rejected" 19 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-20 -- "Annotation lines arrive as caller-supplied text" 20 │ * @mitigates #parser against #insecure-deser using #input-sanitize -- "Every line is re-parsed with parseLine before write; anything that does not parse, or parses as @accepts, is rejected" 21 │ * @flows MCPClient -> #parser via applyAnnotations -- "Structured annotation write path" 22 │ * @flows #parser -> FileSystem via writeFileSync -- "Sidecar append" 23 │ * @comment -- "Idempotent by construction: an identical @source block is detected and skipped rather than duplicated" 24 │ */ 25 │
L21 flow MCPClient → #parser
Structured annotation write path
16 │ * 17 │ * @exposes #parser to #arbitrary-write [high] cwe:CWE-73 -- "Writes annotation sidecars from tool input" 18 │ * @mitigates #parser against #arbitrary-write using #path-validation -- "Target is always resolveGalPath(root, file); the caller cannot choose the path, and a file escaping root is rejected" 19 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-20 -- "Annotation lines arrive as caller-supplied text" 20 │ * @mitigates #parser against #insecure-deser using #input-sanitize -- "Every line is re-parsed with parseLine before write; anything that does not parse, or parses as @accepts, is rejected" 21 │ * @flows MCPClient -> #parser via applyAnnotations -- "Structured annotation write path" 22 │ * @flows #parser -> FileSystem via writeFileSync -- "Sidecar append" 23 │ * @comment -- "Idempotent by construction: an identical @source block is detected and skipped rather than duplicated" 24 │ */ 25 │ 26 │ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
L22 flow #parser → FileSystem
Sidecar append
17 │ * @exposes #parser to #arbitrary-write [high] cwe:CWE-73 -- "Writes annotation sidecars from tool input" 18 │ * @mitigates #parser against #arbitrary-write using #path-validation -- "Target is always resolveGalPath(root, file); the caller cannot choose the path, and a file escaping root is rejected" 19 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-20 -- "Annotation lines arrive as caller-supplied text" 20 │ * @mitigates #parser against #insecure-deser using #input-sanitize -- "Every line is re-parsed with parseLine before write; anything that does not parse, or parses as @accepts, is rejected" 21 │ * @flows MCPClient -> #parser via applyAnnotations -- "Structured annotation write path" 22 │ * @flows #parser -> FileSystem via writeFileSync -- "Sidecar append" 23 │ * @comment -- "Idempotent by construction: an identical @source block is detected and skipped rather than duplicated" 24 │ */ 25 │ 26 │ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; 27 │ import { dirname, resolve } from 'node:path';
L23 comment Idempotent by construction: an identical @source block is detected and skipped rather than duplicated
Idempotent by construction: an identical @source block is detected and skipped rather than duplicated
18 │ * @mitigates #parser against #arbitrary-write using #path-validation -- "Target is always resolveGalPath(root, file); the caller cannot choose the path, and a file escaping root is rejected" 19 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-20 -- "Annotation lines arrive as caller-supplied text" 20 │ * @mitigates #parser against #insecure-deser using #input-sanitize -- "Every line is re-parsed with parseLine before write; anything that does not parse, or parses as @accepts, is rejected" 21 │ * @flows MCPClient -> #parser via applyAnnotations -- "Structured annotation write path" 22 │ * @flows #parser -> FileSystem via writeFileSync -- "Sidecar append" 23 │ * @comment -- "Idempotent by construction: an identical @source block is detected and skipped rather than duplicated" 24 │ */ 25 │ 26 │ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; 27 │ import { dirname, resolve } from 'node:path'; 28 │ import { parseLine } from './parse-line.js';
src/parser/canonical-order.ts flow 1comment 1 open 2
L24 flow ThreatModel → #parser
Model ordered deterministically before emission
19 │ * This sorts at the **emission boundary** rather than in the parser. Parser 20 │ * output order is observable behaviour and changing it is out of scope; what 21 │ * matters is that nothing written to disk inherits an accident of directory 22 │ * enumeration. 23 │ * 24 │ * @flows ThreatModel -> #parser via canonicalizeModelOrder -- "Model ordered deterministically before emission" 25 │ * @comment -- "Pure; returns a new model, mutates nothing. Sort is total, so the result cannot depend on input order." 26 │ */ 27 │ 28 │ import type { ThreatModel } from '../types/index.js'; 29 │
L25 comment Pure; returns a new model, mutates nothing. Sort is total, so the result cannot depend on input order.
Pure; returns a new model, mutates nothing. Sort is total, so the result cannot depend on input order.
20 │ * output order is observable behaviour and changing it is out of scope; what 21 │ * matters is that nothing written to disk inherits an accident of directory 22 │ * enumeration. 23 │ * 24 │ * @flows ThreatModel -> #parser via canonicalizeModelOrder -- "Model ordered deterministically before emission" 25 │ * @comment -- "Pure; returns a new model, mutates nothing. Sort is total, so the result cannot depend on input order." 26 │ */ 27 │ 28 │ import type { ThreatModel } from '../types/index.js'; 29 │ 30 │ /**
src/parser/canonical-ref.ts comment 1 open 1
L37 comment Aliasing can only ever ADD coverage, so the map is built from declared assets only; an undeclared ref resolves to itself, which is the pre-D47 behaviour
Aliasing can only ever ADD coverage, so the map is built from declared assets only; an undeclared ref resolves to itself, which is the pre-D47 behaviour
32 │ * A reference therefore resolves to a different string than it did before if 33 │ * and only if the model declares an asset whose dotted path or id it matches. 34 │ * Two references collapse together only when the model itself says they are the 35 │ * same declared asset. Nothing is inferred from spelling similarity. 36 │ * 37 │ * @comment -- "Aliasing can only ever ADD coverage, so the map is built from declared assets only; an undeclared ref resolves to itself, which is the pre-D47 behaviour" 38 │ */ 39 │ import type { ThreatModel } from '../types/index.js'; 40 │ 41 │ /** 42 │ * A function mapping any asset reference to its canonical id.
src/parser/citation.ts comment 1mitigates 1 open 2
L16 comment Pure function over annotation text; no I/O, no filesystem check — a citation is a claim about where authz lives, and verifying the path exists is a reviewer's job (§3.4)
Pure function over annotation text; no I/O, no filesystem check — a citation is a claim about where authz lives, and verifying the path exists is a reviewer's job (§3.4)
11 │ * 12 │ * "By design: the archival URI is namespace configuration. 13 │ * Authz: ScopeCluster/AccessAdmin at common/api/metadata.go:189" 14 │ * ^^^^^^^^^^^^^^^^^^^^^^^^^ 15 │ * 16 │ * @comment -- "Pure function over annotation text; no I/O, no filesystem check — a citation is a claim about where authz lives, and verifying the path exists is a reviewer's job (§3.4)" 17 │ * @mitigates #parser against #redos using #regex-anchoring -- "Tokenizes on whitespace first, then matches each path segment with an anchored, quantifier-free-nesting pattern — no backtracking over the whole description" 18 │ */ 19 │ 20 │ import type { EntitlementCitation } from '../types/index.js'; 21 │
L17 mitigates #regex-anchoring mitigates #redos
Tokenizes on whitespace first, then matches each path segment with an anchored, quantifier-free-nesting pattern — no backtracking over the whole description
12 │ * "By design: the archival URI is namespace configuration. 13 │ * Authz: ScopeCluster/AccessAdmin at common/api/metadata.go:189" 14 │ * ^^^^^^^^^^^^^^^^^^^^^^^^^ 15 │ * 16 │ * @comment -- "Pure function over annotation text; no I/O, no filesystem check — a citation is a claim about where authz lives, and verifying the path exists is a reviewer's job (§3.4)" 17 │ * @mitigates #parser against #redos using #regex-anchoring -- "Tokenizes on whitespace first, then matches each path segment with an anchored, quantifier-free-nesting pattern — no backtracking over the whole description" 18 │ */ 19 │ 20 │ import type { EntitlementCitation } from '../types/index.js'; 21 │ 22 │ // ─── Token patterns ──────────────────────────────────────────────────
src/parser/claim-key.ts comment 1 open 1
L20 comment Pure functions over an assembled ThreatModel; no I/O
Pure functions over an assembled ThreatModel; no I/O
15 │ * the two answer different questions. 16 │ * 17 │ * Identical claims in one file get an ordinal suffix in document order, so a 18 │ * repeated `@comment` is two ledger entries rather than one that flaps. 19 │ * 20 │ * @comment -- "Pure functions over an assembled ThreatModel; no I/O" 21 │ */ 22 │ import { createHash } from 'node:crypto'; 23 │ import type { 24 │ ThreatModel, SourceLocation, AnnotationVerb, 25 │ ThreatModelMitigation, ThreatModelExposure, ThreatModelConfirmed, ThreatModelAcceptance,
src/parser/clear.ts exposes 2comment 2flow 2mitigates 1 open 9
L8 exposes #parser → #arbitrary-write
Writes modified content back to discovered files
3 │ * Scans project source files and removes all GuardLink annotation lines. 4 │ * Standalone .gal files are treated as raw annotation text. 5 │ * 6 │ * Used by `guardlink clear` and `/clear` to let users start fresh with annotations. 7 │ * 8 │ * @exposes #parser to #arbitrary-write [high] cwe:CWE-73 -- "Writes modified content back to discovered files" 9 │ * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "Glob patterns determine which files are modified" 10 │ * @mitigates #parser against #path-traversal using #glob-filtering -- "DEFAULT_EXCLUDE blocks sensitive dirs; cwd constrains scope" 11 │ * @audit #parser -- "Destructive operation requires explicit user confirmation via dryRun flag" 12 │ * @comment -- "No @entitles here on purpose: #local-dev is genuinely entitled to the #arbitrary-write effect (clear exists to rewrite these files), but that pair already carries @mitigates, so it is never an open finding and the claim would demote nothing. Recorded and withdrawn in .guardlink/entitlement-proposals.json — an entitlement on a covered pair is decoration" 13 │ * @comment -- "#mcp-agent is granted nothing. It can only preview a clear (dry_run defaults to true at src/mcp/server.ts:505), and a preview writes nothing, so it answers no exposure — an entitlement that joins no finding is decoration. Nor is it entitled to clear-annotations: no code makes an MCP caller obtain the confirmation its tool description asks for, so granting it would be the over-grant §2 forbids"
L9 exposes #parser → #path-traversal
Glob patterns determine which files are modified
4 │ * Standalone .gal files are treated as raw annotation text. 5 │ * 6 │ * Used by `guardlink clear` and `/clear` to let users start fresh with annotations. 7 │ * 8 │ * @exposes #parser to #arbitrary-write [high] cwe:CWE-73 -- "Writes modified content back to discovered files" 9 │ * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "Glob patterns determine which files are modified" 10 │ * @mitigates #parser against #path-traversal using #glob-filtering -- "DEFAULT_EXCLUDE blocks sensitive dirs; cwd constrains scope" 11 │ * @audit #parser -- "Destructive operation requires explicit user confirmation via dryRun flag" 12 │ * @comment -- "No @entitles here on purpose: #local-dev is genuinely entitled to the #arbitrary-write effect (clear exists to rewrite these files), but that pair already carries @mitigates, so it is never an open finding and the claim would demote nothing. Recorded and withdrawn in .guardlink/entitlement-proposals.json — an entitlement on a covered pair is decoration" 13 │ * @comment -- "#mcp-agent is granted nothing. It can only preview a clear (dry_run defaults to true at src/mcp/server.ts:505), and a preview writes nothing, so it answers no exposure — an entitlement that joins no finding is decoration. Nor is it entitled to clear-annotations: no code makes an MCP caller obtain the confirmation its tool description asks for, so granting it would be the over-grant §2 forbids" 14 │ * @flows ProjectRoot -> #parser via fast-glob -- "File discovery path"
L10 mitigates #glob-filtering mitigates #path-traversal
DEFAULT_EXCLUDE blocks sensitive dirs; cwd constrains scope
5 │ * 6 │ * Used by `guardlink clear` and `/clear` to let users start fresh with annotations. 7 │ * 8 │ * @exposes #parser to #arbitrary-write [high] cwe:CWE-73 -- "Writes modified content back to discovered files" 9 │ * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "Glob patterns determine which files are modified" 10 │ * @mitigates #parser against #path-traversal using #glob-filtering -- "DEFAULT_EXCLUDE blocks sensitive dirs; cwd constrains scope" 11 │ * @audit #parser -- "Destructive operation requires explicit user confirmation via dryRun flag" 12 │ * @comment -- "No @entitles here on purpose: #local-dev is genuinely entitled to the #arbitrary-write effect (clear exists to rewrite these files), but that pair already carries @mitigates, so it is never an open finding and the claim would demote nothing. Recorded and withdrawn in .guardlink/entitlement-proposals.json — an entitlement on a covered pair is decoration" 13 │ * @comment -- "#mcp-agent is granted nothing. It can only preview a clear (dry_run defaults to true at src/mcp/server.ts:505), and a preview writes nothing, so it answers no exposure — an entitlement that joins no finding is decoration. Nor is it entitled to clear-annotations: no code makes an MCP caller obtain the confirmation its tool description asks for, so granting it would be the over-grant §2 forbids" 14 │ * @flows ProjectRoot -> #parser via fast-glob -- "File discovery path" 15 │ * @flows #parser -> SourceFiles via writeFile -- "Modified file write path"
L11 audit Audit: #parser
Destructive operation requires explicit user confirmation via dryRun flag
6 │ * Used by `guardlink clear` and `/clear` to let users start fresh with annotations. 7 │ * 8 │ * @exposes #parser to #arbitrary-write [high] cwe:CWE-73 -- "Writes modified content back to discovered files" 9 │ * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "Glob patterns determine which files are modified" 10 │ * @mitigates #parser against #path-traversal using #glob-filtering -- "DEFAULT_EXCLUDE blocks sensitive dirs; cwd constrains scope" 11 │ * @audit #parser -- "Destructive operation requires explicit user confirmation via dryRun flag" 12 │ * @comment -- "No @entitles here on purpose: #local-dev is genuinely entitled to the #arbitrary-write effect (clear exists to rewrite these files), but that pair already carries @mitigates, so it is never an open finding and the claim would demote nothing. Recorded and withdrawn in .guardlink/entitlement-proposals.json — an entitlement on a covered pair is decoration" 13 │ * @comment -- "#mcp-agent is granted nothing. It can only preview a clear (dry_run defaults to true at src/mcp/server.ts:505), and a preview writes nothing, so it answers no exposure — an entitlement that joins no finding is decoration. Nor is it entitled to clear-annotations: no code makes an MCP caller obtain the confirmation its tool description asks for, so granting it would be the over-grant §2 forbids" 14 │ * @flows ProjectRoot -> #parser via fast-glob -- "File discovery path" 15 │ * @flows #parser -> SourceFiles via writeFile -- "Modified file write path" 16 │ * @handles internal on #parser -- "Operates on project source files only"
L12 comment No @entitles here on purpose: #local-dev is genuinely entitled to the #arbitrary-write effect (clear exists to rewrite these files), but that pair already carries @mitigates, so it is never an open finding and the claim would demote nothing. Recorded and withdrawn in .guardlink/entitlement-proposals.json — an entitlement on a covered pair is decoration
No @entitles here on purpose: #local-dev is genuinely entitled to the #arbitrary-write effect (clear exists to rewrite these files), but that pair already carries @mitigates, so it is never an open finding and the claim would demote nothing. Recorded and withdrawn in .guardlink/entitlement-proposals.json — an entitlement on a covered pair is decoration
7 │ * 8 │ * @exposes #parser to #arbitrary-write [high] cwe:CWE-73 -- "Writes modified content back to discovered files" 9 │ * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "Glob patterns determine which files are modified" 10 │ * @mitigates #parser against #path-traversal using #glob-filtering -- "DEFAULT_EXCLUDE blocks sensitive dirs; cwd constrains scope" 11 │ * @audit #parser -- "Destructive operation requires explicit user confirmation via dryRun flag" 12 │ * @comment -- "No @entitles here on purpose: #local-dev is genuinely entitled to the #arbitrary-write effect (clear exists to rewrite these files), but that pair already carries @mitigates, so it is never an open finding and the claim would demote nothing. Recorded and withdrawn in .guardlink/entitlement-proposals.json — an entitlement on a covered pair is decoration" 13 │ * @comment -- "#mcp-agent is granted nothing. It can only preview a clear (dry_run defaults to true at src/mcp/server.ts:505), and a preview writes nothing, so it answers no exposure — an entitlement that joins no finding is decoration. Nor is it entitled to clear-annotations: no code makes an MCP caller obtain the confirmation its tool description asks for, so granting it would be the over-grant §2 forbids" 14 │ * @flows ProjectRoot -> #parser via fast-glob -- "File discovery path" 15 │ * @flows #parser -> SourceFiles via writeFile -- "Modified file write path" 16 │ * @handles internal on #parser -- "Operates on project source files only" 17 │ */
L13 comment #mcp-agent is granted nothing. It can only preview a clear (dry_run defaults to true at src/mcp/server.ts:505), and a preview writes nothing, so it answers no exposure — an entitlement that joins no finding is decoration. Nor is it entitled to clear-annotations: no code makes an MCP caller obtain the confirmation its tool description asks for, so granting it would be the over-grant §2 forbids
#mcp-agent is granted nothing. It can only preview a clear (dry_run defaults to true at src/mcp/server.ts:505), and a preview writes nothing, so it answers no exposure — an entitlement that joins no finding is decoration. Nor is it entitled to clear-annotations: no code makes an MCP caller obtain the confirmation its tool description asks for, so granting it would be the over-grant §2 forbids
8 │ * @exposes #parser to #arbitrary-write [high] cwe:CWE-73 -- "Writes modified content back to discovered files" 9 │ * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "Glob patterns determine which files are modified" 10 │ * @mitigates #parser against #path-traversal using #glob-filtering -- "DEFAULT_EXCLUDE blocks sensitive dirs; cwd constrains scope" 11 │ * @audit #parser -- "Destructive operation requires explicit user confirmation via dryRun flag" 12 │ * @comment -- "No @entitles here on purpose: #local-dev is genuinely entitled to the #arbitrary-write effect (clear exists to rewrite these files), but that pair already carries @mitigates, so it is never an open finding and the claim would demote nothing. Recorded and withdrawn in .guardlink/entitlement-proposals.json — an entitlement on a covered pair is decoration" 13 │ * @comment -- "#mcp-agent is granted nothing. It can only preview a clear (dry_run defaults to true at src/mcp/server.ts:505), and a preview writes nothing, so it answers no exposure — an entitlement that joins no finding is decoration. Nor is it entitled to clear-annotations: no code makes an MCP caller obtain the confirmation its tool description asks for, so granting it would be the over-grant §2 forbids" 14 │ * @flows ProjectRoot -> #parser via fast-glob -- "File discovery path" 15 │ * @flows #parser -> SourceFiles via writeFile -- "Modified file write path" 16 │ * @handles internal on #parser -- "Operates on project source files only" 17 │ */ 18 │
L14 flow ProjectRoot → #parser
File discovery path
9 │ * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "Glob patterns determine which files are modified" 10 │ * @mitigates #parser against #path-traversal using #glob-filtering -- "DEFAULT_EXCLUDE blocks sensitive dirs; cwd constrains scope" 11 │ * @audit #parser -- "Destructive operation requires explicit user confirmation via dryRun flag" 12 │ * @comment -- "No @entitles here on purpose: #local-dev is genuinely entitled to the #arbitrary-write effect (clear exists to rewrite these files), but that pair already carries @mitigates, so it is never an open finding and the claim would demote nothing. Recorded and withdrawn in .guardlink/entitlement-proposals.json — an entitlement on a covered pair is decoration" 13 │ * @comment -- "#mcp-agent is granted nothing. It can only preview a clear (dry_run defaults to true at src/mcp/server.ts:505), and a preview writes nothing, so it answers no exposure — an entitlement that joins no finding is decoration. Nor is it entitled to clear-annotations: no code makes an MCP caller obtain the confirmation its tool description asks for, so granting it would be the over-grant §2 forbids" 14 │ * @flows ProjectRoot -> #parser via fast-glob -- "File discovery path" 15 │ * @flows #parser -> SourceFiles via writeFile -- "Modified file write path" 16 │ * @handles internal on #parser -- "Operates on project source files only" 17 │ */ 18 │ 19 │ import fg from 'fast-glob';
L15 flow #parser → SourceFiles
Modified file write path
10 │ * @mitigates #parser against #path-traversal using #glob-filtering -- "DEFAULT_EXCLUDE blocks sensitive dirs; cwd constrains scope" 11 │ * @audit #parser -- "Destructive operation requires explicit user confirmation via dryRun flag" 12 │ * @comment -- "No @entitles here on purpose: #local-dev is genuinely entitled to the #arbitrary-write effect (clear exists to rewrite these files), but that pair already carries @mitigates, so it is never an open finding and the claim would demote nothing. Recorded and withdrawn in .guardlink/entitlement-proposals.json — an entitlement on a covered pair is decoration" 13 │ * @comment -- "#mcp-agent is granted nothing. It can only preview a clear (dry_run defaults to true at src/mcp/server.ts:505), and a preview writes nothing, so it answers no exposure — an entitlement that joins no finding is decoration. Nor is it entitled to clear-annotations: no code makes an MCP caller obtain the confirmation its tool description asks for, so granting it would be the over-grant §2 forbids" 14 │ * @flows ProjectRoot -> #parser via fast-glob -- "File discovery path" 15 │ * @flows #parser -> SourceFiles via writeFile -- "Modified file write path" 16 │ * @handles internal on #parser -- "Operates on project source files only" 17 │ */ 18 │ 19 │ import fg from 'fast-glob'; 20 │ import { readFile, writeFile } from 'node:fs/promises';
L16 handles #parser: internal
Operates on project source files only
11 │ * @audit #parser -- "Destructive operation requires explicit user confirmation via dryRun flag" 12 │ * @comment -- "No @entitles here on purpose: #local-dev is genuinely entitled to the #arbitrary-write effect (clear exists to rewrite these files), but that pair already carries @mitigates, so it is never an open finding and the claim would demote nothing. Recorded and withdrawn in .guardlink/entitlement-proposals.json — an entitlement on a covered pair is decoration" 13 │ * @comment -- "#mcp-agent is granted nothing. It can only preview a clear (dry_run defaults to true at src/mcp/server.ts:505), and a preview writes nothing, so it answers no exposure — an entitlement that joins no finding is decoration. Nor is it entitled to clear-annotations: no code makes an MCP caller obtain the confirmation its tool description asks for, so granting it would be the over-grant §2 forbids" 14 │ * @flows ProjectRoot -> #parser via fast-glob -- "File discovery path" 15 │ * @flows #parser -> SourceFiles via writeFile -- "Modified file write path" 16 │ * @handles internal on #parser -- "Operates on project source files only" 17 │ */ 18 │ 19 │ import fg from 'fast-glob'; 20 │ import { readFile, writeFile } from 'node:fs/promises'; 21 │ import { relative } from 'node:path';
src/parser/comment-strip.ts mitigates 2exposes 1comment 1validates 1 open 5
L7 exposes #parser → #redos
Marker and decoration matching runs on every line of every scanned file, including attacker-supplied source
2 │ 3 │ /** 4 │ * Comment prefix stripping per §2.9. 5 │ * Strips the host language's comment prefix to expose the annotation text. 6 │ * 7 │ * @exposes #parser to #redos [medium] cwe:CWE-1333 -- "Marker and decoration matching runs on every line of every scanned file, including attacker-supplied source" 8 │ * @mitigates #parser against #redos using #regex-anchoring -- "Every pattern is anchored at ^ and bounded — decoration is {1,3}, the marker tail is a character-by-character scan, and no pattern nests a quantifier" 9 │ * @mitigates #parser against #dos using #resource-limits -- "consumeMarkerTail is linear in the marker, not the line: it stops at the first character that is not the marker's own" 10 │ * @comment -- "Widening what is consumed AFTER a recognised marker is safe; widening the marker set would not be. The openers in LINE_MARKERS are unchanged from the single-marker version, so no line that was code became a comment" 11 │ * @validates #regex-anchoring for #parser -- "tests/comment-forms.test.ts pins every §2.9.1 form and, negatively, that a non-comment still returns null" 12 │ */
L8 mitigates #regex-anchoring mitigates #redos
Every pattern is anchored at ^ and bounded — decoration is {1,3}, the marker tail is a character-by-character scan, and no pattern nests a quantifier
3 │ /** 4 │ * Comment prefix stripping per §2.9. 5 │ * Strips the host language's comment prefix to expose the annotation text. 6 │ * 7 │ * @exposes #parser to #redos [medium] cwe:CWE-1333 -- "Marker and decoration matching runs on every line of every scanned file, including attacker-supplied source" 8 │ * @mitigates #parser against #redos using #regex-anchoring -- "Every pattern is anchored at ^ and bounded — decoration is {1,3}, the marker tail is a character-by-character scan, and no pattern nests a quantifier" 9 │ * @mitigates #parser against #dos using #resource-limits -- "consumeMarkerTail is linear in the marker, not the line: it stops at the first character that is not the marker's own" 10 │ * @comment -- "Widening what is consumed AFTER a recognised marker is safe; widening the marker set would not be. The openers in LINE_MARKERS are unchanged from the single-marker version, so no line that was code became a comment" 11 │ * @validates #regex-anchoring for #parser -- "tests/comment-forms.test.ts pins every §2.9.1 form and, negatively, that a non-comment still returns null" 12 │ */ 13 │
L9 mitigates #resource-limits mitigates #dos
consumeMarkerTail is linear in the marker, not the line: it stops at the first character that is not the marker's own
4 │ * Comment prefix stripping per §2.9. 5 │ * Strips the host language's comment prefix to expose the annotation text. 6 │ * 7 │ * @exposes #parser to #redos [medium] cwe:CWE-1333 -- "Marker and decoration matching runs on every line of every scanned file, including attacker-supplied source" 8 │ * @mitigates #parser against #redos using #regex-anchoring -- "Every pattern is anchored at ^ and bounded — decoration is {1,3}, the marker tail is a character-by-character scan, and no pattern nests a quantifier" 9 │ * @mitigates #parser against #dos using #resource-limits -- "consumeMarkerTail is linear in the marker, not the line: it stops at the first character that is not the marker's own" 10 │ * @comment -- "Widening what is consumed AFTER a recognised marker is safe; widening the marker set would not be. The openers in LINE_MARKERS are unchanged from the single-marker version, so no line that was code became a comment" 11 │ * @validates #regex-anchoring for #parser -- "tests/comment-forms.test.ts pins every §2.9.1 form and, negatively, that a non-comment still returns null" 12 │ */ 13 │ 14 │ /**
L10 comment Widening what is consumed AFTER a recognised marker is safe; widening the marker set would not be. The openers in LINE_MARKERS are unchanged from the single-marker version, so no line that was code became a comment
Widening what is consumed AFTER a recognised marker is safe; widening the marker set would not be. The openers in LINE_MARKERS are unchanged from the single-marker version, so no line that was code became a comment
5 │ * Strips the host language's comment prefix to expose the annotation text. 6 │ * 7 │ * @exposes #parser to #redos [medium] cwe:CWE-1333 -- "Marker and decoration matching runs on every line of every scanned file, including attacker-supplied source" 8 │ * @mitigates #parser against #redos using #regex-anchoring -- "Every pattern is anchored at ^ and bounded — decoration is {1,3}, the marker tail is a character-by-character scan, and no pattern nests a quantifier" 9 │ * @mitigates #parser against #dos using #resource-limits -- "consumeMarkerTail is linear in the marker, not the line: it stops at the first character that is not the marker's own" 10 │ * @comment -- "Widening what is consumed AFTER a recognised marker is safe; widening the marker set would not be. The openers in LINE_MARKERS are unchanged from the single-marker version, so no line that was code became a comment" 11 │ * @validates #regex-anchoring for #parser -- "tests/comment-forms.test.ts pins every §2.9.1 form and, negatively, that a non-comment still returns null" 12 │ */ 13 │ 14 │ /** 15 │ * The single-line comment openers §2.9 recognises, longest first.
L11 validates #regex-anchoring validates #parser
tests/comment-forms.test.ts pins every §2.9.1 form and, negatively, that a non-comment still returns null
6 │ * 7 │ * @exposes #parser to #redos [medium] cwe:CWE-1333 -- "Marker and decoration matching runs on every line of every scanned file, including attacker-supplied source" 8 │ * @mitigates #parser against #redos using #regex-anchoring -- "Every pattern is anchored at ^ and bounded — decoration is {1,3}, the marker tail is a character-by-character scan, and no pattern nests a quantifier" 9 │ * @mitigates #parser against #dos using #resource-limits -- "consumeMarkerTail is linear in the marker, not the line: it stops at the first character that is not the marker's own" 10 │ * @comment -- "Widening what is consumed AFTER a recognised marker is safe; widening the marker set would not be. The openers in LINE_MARKERS are unchanged from the single-marker version, so no line that was code became a comment" 11 │ * @validates #regex-anchoring for #parser -- "tests/comment-forms.test.ts pins every §2.9.1 form and, negatively, that a non-comment still returns null" 12 │ */ 13 │ 14 │ /** 15 │ * The single-line comment openers §2.9 recognises, longest first. 16 │ *
src/parser/coverage.ts comment 2flow 1 open 3
L81 flow ThreatModel → #parser
Every unmitigated-exposure answer in the product routes through this predicate
76 │ * 77 │ * Omit `symbol:` from the `@source` header — an unanchored mitigation is an 78 │ * asset-level statement and is never narrowed. No new syntax was needed, which is 79 │ * why none was added. 80 │ * 81 │ * @flows ThreatModel -> #parser via isCovered -- "Every unmitigated-exposure answer in the product routes through this predicate" 82 │ * @comment -- "Inline annotations never carry parent_symbol (parse-file.ts populates it only from @source), so this rule is inert in every inline repo — measured: 0 of 74 exposures change state on guardlink, 0 of 61 on specter-v1, 2 of 11 on expense-api" 83 │ * @comment -- "Narrowing is one-directional by construction: coversExposure starts from the (asset, threat) match the old key computed and only ever subtracts. A refactor that made it additive would be a silent-wrong-answer path in the opposite direction" 84 │ */ 85 │ 86 │ import type {
L82 comment Inline annotations never carry parent_symbol (parse-file.ts populates it only from @source), so this rule is inert in every inline repo — measured: 0 of 74 exposures change state on guardlink, 0 of 61 on specter-v1, 2 of 11 on expense-api
Inline annotations never carry parent_symbol (parse-file.ts populates it only from @source), so this rule is inert in every inline repo — measured: 0 of 74 exposures change state on guardlink, 0 of 61 on specter-v1, 2 of 11 on expense-api
77 │ * Omit `symbol:` from the `@source` header — an unanchored mitigation is an 78 │ * asset-level statement and is never narrowed. No new syntax was needed, which is 79 │ * why none was added. 80 │ * 81 │ * @flows ThreatModel -> #parser via isCovered -- "Every unmitigated-exposure answer in the product routes through this predicate" 82 │ * @comment -- "Inline annotations never carry parent_symbol (parse-file.ts populates it only from @source), so this rule is inert in every inline repo — measured: 0 of 74 exposures change state on guardlink, 0 of 61 on specter-v1, 2 of 11 on expense-api" 83 │ * @comment -- "Narrowing is one-directional by construction: coversExposure starts from the (asset, threat) match the old key computed and only ever subtracts. A refactor that made it additive would be a silent-wrong-answer path in the opposite direction" 84 │ */ 85 │ 86 │ import type { 87 │ ThreatModel, ThreatModelExposure, ThreatModelMitigation, ThreatModelAcceptance,
L83 comment Narrowing is one-directional by construction: coversExposure starts from the (asset, threat) match the old key computed and only ever subtracts. A refactor that made it additive would be a silent-wrong-answer path in the opposite direction
Narrowing is one-directional by construction: coversExposure starts from the (asset, threat) match the old key computed and only ever subtracts. A refactor that made it additive would be a silent-wrong-answer path in the opposite direction
78 │ * asset-level statement and is never narrowed. No new syntax was needed, which is 79 │ * why none was added. 80 │ * 81 │ * @flows ThreatModel -> #parser via isCovered -- "Every unmitigated-exposure answer in the product routes through this predicate" 82 │ * @comment -- "Inline annotations never carry parent_symbol (parse-file.ts populates it only from @source), so this rule is inert in every inline repo — measured: 0 of 74 exposures change state on guardlink, 0 of 61 on specter-v1, 2 of 11 on expense-api" 83 │ * @comment -- "Narrowing is one-directional by construction: coversExposure starts from the (asset, threat) match the old key computed and only ever subtracts. A refactor that made it additive would be a silent-wrong-answer path in the opposite direction" 84 │ */ 85 │ 86 │ import type { 87 │ ThreatModel, ThreatModelExposure, ThreatModelMitigation, ThreatModelAcceptance, 88 │ SourceLocation, CoverageStats,
src/parser/feature-filter.ts comment 5 open 5
L15 comment Pure filtering utility; no I/O
Pure filtering utility; no I/O
10 │ * `.guardlink/definitions.*`, which carries no `@feature`, so scoping them by 11 │ * file removed every one of them and left a feature view whose relationships 12 │ * pointed at nothing. They are instead resolved from what the kept relations 13 │ * reference — the node vocabulary follows the edges. 14 │ * 15 │ * @comment -- "Pure filtering utility; no I/O" 16 │ * @comment -- "Relations are file-scoped, definitions are reference-scoped. Filtering definitions by file emptied the asset heatmap, dropped every threat severity and rendered diagrams as bare ids — the same failure selectSubgraph avoids by always keeping the node vocabulary" 17 │ */ 18 │ 19 │ import type { ThreatModel } from '../types/index.js'; 20 │
L16 comment Relations are file-scoped, definitions are reference-scoped. Filtering definitions by file emptied the asset heatmap, dropped every threat severity and rendered diagrams as bare ids — the same failure selectSubgraph avoids by always keeping the node vocabulary
Relations are file-scoped, definitions are reference-scoped. Filtering definitions by file emptied the asset heatmap, dropped every threat severity and rendered diagrams as bare ids — the same failure selectSubgraph avoids by always keeping the node vocabulary
11 │ * file removed every one of them and left a feature view whose relationships 12 │ * pointed at nothing. They are instead resolved from what the kept relations 13 │ * reference — the node vocabulary follows the edges. 14 │ * 15 │ * @comment -- "Pure filtering utility; no I/O" 16 │ * @comment -- "Relations are file-scoped, definitions are reference-scoped. Filtering definitions by file emptied the asset heatmap, dropped every threat severity and rendered diagrams as bare ids — the same failure selectSubgraph avoids by always keeping the node vocabulary" 17 │ */ 18 │ 19 │ import type { ThreatModel } from '../types/index.js'; 20 │ 21 │ /**
L30 comment Exclusion list for the annotation counter; external_refs is derived from annotations, not an annotation
Exclusion list for the annotation counter; external_refs is derived from annotations, not an annotation
25 │ * annotation that mentioned the tag — counting it would count that annotation 26 │ * twice. It is the only such key today; the list exists so that the counter 27 │ * below can work by shape (see `countAnnotations`) instead of by a hardcoded 28 │ * inventory of collection names. 29 │ * 30 │ * @comment -- "Exclusion list for the annotation counter; external_refs is derived from annotations, not an annotation" 31 │ */ 32 │ const NON_ANNOTATION_LOCATED_KEYS = new Set<string>(['external_refs']); 33 │ 34 │ /** A row is an annotation if it carries a source location. */ 35 │ function isLocatedRow(v: unknown): v is { location: { file: string } } {
L52 comment Derives annotation totals from the model's own shape so a newly added collection is counted the day it lands
Derives annotation totals from the model's own shape so a newly added collection is counted the day it lands
47 │ * the first time a verb is added: the new collection ships in the model and is 48 │ * silently missing from the count, which is exactly the class of drift this 49 │ * whole change is fixing. Scalars and `string[]` (`annotated_files`, 50 │ * `unannotated_files`) fail the element test, so only annotation rows count. 51 │ * 52 │ * @comment -- "Derives annotation totals from the model's own shape so a newly added collection is counted the day it lands" 53 │ */ 54 │ export function countAnnotations(model: ThreatModel, files?: Set<string>): number { 55 │ let n = 0; 56 │ for (const [key, value] of Object.entries(model)) { 57 │ if (!Array.isArray(value) || value.length === 0) continue;
L100 comment Filtered models must never report project-wide totals — a reader acts on the numbers beside the contents
Filtered models must never report project-wide totals — a reader acts on the numbers beside the contents
95 │ * describes the feature rather than the project it was cut from, and which 96 │ * carries `filtered_by_features` so a consumer can say it is showing a slice. 97 │ * Feature matching is case-insensitive; `filtered_by_features` records the 98 │ * names as the caller wrote them. 99 │ * 100 │ * @comment -- "Filtered models must never report project-wide totals — a reader acts on the numbers beside the contents" 101 │ */ 102 │ export function filterByFeature(model: ThreatModel, featureNames: string[]): ThreatModel { 103 │ const wantedLower = new Set(featureNames.map(n => n.toLowerCase())); 104 │ const fileFeatureMap = buildFileFeatureMap(model); 105 │
src/parser/fingerprint.ts exposes 1mitigates 1flow 1comment 1 open 4
L20 exposes #parser → #dos
Fingerprint globs the project tree on every call
15 │ * 16 │ * External mode is the reason this matters more than it looks. When annotations 17 │ * live in `.guardlink/annotations/**.gal`, the sidecar changes while the source 18 │ * file does not, so nothing incidental signals that the model moved. 19 │ * 20 │ * @exposes #parser to #dos [low] cwe:CWE-400 -- "Fingerprint globs the project tree on every call" 21 │ * @mitigates #parser against #dos using #resource-limits -- "Reuses DEFAULT_EXCLUDE; stats come from the glob walk, no extra stat() calls, no file reads" 22 │ * @flows ProjectRoot -> #parser via fast-glob -- "Directory metadata scan for cache validity" 23 │ * @comment -- "Metadata only: path, size, mtime. File contents are never read." 24 │ */ 25 │
L21 mitigates #resource-limits mitigates #dos
Reuses DEFAULT_EXCLUDE; stats come from the glob walk, no extra stat() calls, no file reads
16 │ * External mode is the reason this matters more than it looks. When annotations 17 │ * live in `.guardlink/annotations/**.gal`, the sidecar changes while the source 18 │ * file does not, so nothing incidental signals that the model moved. 19 │ * 20 │ * @exposes #parser to #dos [low] cwe:CWE-400 -- "Fingerprint globs the project tree on every call" 21 │ * @mitigates #parser against #dos using #resource-limits -- "Reuses DEFAULT_EXCLUDE; stats come from the glob walk, no extra stat() calls, no file reads" 22 │ * @flows ProjectRoot -> #parser via fast-glob -- "Directory metadata scan for cache validity" 23 │ * @comment -- "Metadata only: path, size, mtime. File contents are never read." 24 │ */ 25 │ 26 │ import fg from 'fast-glob';
L22 flow ProjectRoot → #parser
Directory metadata scan for cache validity
17 │ * live in `.guardlink/annotations/**.gal`, the sidecar changes while the source 18 │ * file does not, so nothing incidental signals that the model moved. 19 │ * 20 │ * @exposes #parser to #dos [low] cwe:CWE-400 -- "Fingerprint globs the project tree on every call" 21 │ * @mitigates #parser against #dos using #resource-limits -- "Reuses DEFAULT_EXCLUDE; stats come from the glob walk, no extra stat() calls, no file reads" 22 │ * @flows ProjectRoot -> #parser via fast-glob -- "Directory metadata scan for cache validity" 23 │ * @comment -- "Metadata only: path, size, mtime. File contents are never read." 24 │ */ 25 │ 26 │ import fg from 'fast-glob'; 27 │ import { createHash } from 'node:crypto';
L23 comment Metadata only: path, size, mtime. File contents are never read.
Metadata only: path, size, mtime. File contents are never read.
18 │ * file does not, so nothing incidental signals that the model moved. 19 │ * 20 │ * @exposes #parser to #dos [low] cwe:CWE-400 -- "Fingerprint globs the project tree on every call" 21 │ * @mitigates #parser against #dos using #resource-limits -- "Reuses DEFAULT_EXCLUDE; stats come from the glob walk, no extra stat() calls, no file reads" 22 │ * @flows ProjectRoot -> #parser via fast-glob -- "Directory metadata scan for cache validity" 23 │ * @comment -- "Metadata only: path, size, mtime. File contents are never read." 24 │ */ 25 │ 26 │ import fg from 'fast-glob'; 27 │ import { createHash } from 'node:crypto'; 28 │ import { DEFAULT_INCLUDE, DEFAULT_EXCLUDE } from './parse-project.js';
src/parser/gal-path.ts flow 1comment 1 open 2
L20 flow SourceFile → #parser
Source path mapped to its annotation sidecar
15 │ * 16 │ * Appending `.gal` rather than replacing the extension is deliberate: it keeps 17 │ * the mapping total and reversible. `login.ts` and `login.js` in one directory 18 │ * map to distinct sidecars, which a replace-the-extension scheme would collide. 19 │ * 20 │ * @flows SourceFile -> #parser via resolveGalPath -- "Source path mapped to its annotation sidecar" 21 │ * @comment -- "Pure path arithmetic; no I/O. The inverse is exact, which is what makes migration reversible." 22 │ */ 23 │ 24 │ import { isAbsolute, join, relative, resolve } from 'node:path'; 25 │
L21 comment Pure path arithmetic; no I/O. The inverse is exact, which is what makes migration reversible.
Pure path arithmetic; no I/O. The inverse is exact, which is what makes migration reversible.
16 │ * Appending `.gal` rather than replacing the extension is deliberate: it keeps 17 │ * the mapping total and reversible. `login.ts` and `login.js` in one directory 18 │ * map to distinct sidecars, which a replace-the-extension scheme would collide. 19 │ * 20 │ * @flows SourceFile -> #parser via resolveGalPath -- "Source path mapped to its annotation sidecar" 21 │ * @comment -- "Pure path arithmetic; no I/O. The inverse is exact, which is what makes migration reversible." 22 │ */ 23 │ 24 │ import { isAbsolute, join, relative, resolve } from 'node:path'; 25 │ 26 │ /** Where externalised annotations live, relative to the project root. */
src/parser/ledger.ts exposes 2mitigates 2flow 2 open 6
L18 exposes #parser → #insecure-deser
JSON.parse on a committed file under .guardlink/
13 │ * A file that exists but does not parse, or names another schema, or holds an 14 │ * entry of the wrong shape, is CORRUPT — reported once through a diagnostic 15 │ * and otherwise treated as absent. It is never silently rewritten: `verify` 16 │ * refuses to write over it without `--force`. 17 │ * 18 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-502 -- "JSON.parse on a committed file under .guardlink/" 19 │ * @mitigates #parser against #insecure-deser using #config-validation -- "Shape is validated field by field before any entry is trusted; anything else is corrupt, not partially loaded" 20 │ * @exposes #cli to #arbitrary-write [low] cwe:CWE-73 -- "writeLedger writes one fixed path under root" 21 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "The path is the constant LEDGER_FILE joined to root; no caller supplies a path" 22 │ * @flows LedgerFile -> #parser via readLedger -- "Recorded hashes and verifiers" 23 │ * @flows #cli -> LedgerFile via writeLedger -- "The only write the verify surfaces perform"
L19 mitigates #config-validation mitigates #insecure-deser
Shape is validated field by field before any entry is trusted; anything else is corrupt, not partially loaded
14 │ * entry of the wrong shape, is CORRUPT — reported once through a diagnostic 15 │ * and otherwise treated as absent. It is never silently rewritten: `verify` 16 │ * refuses to write over it without `--force`. 17 │ * 18 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-502 -- "JSON.parse on a committed file under .guardlink/" 19 │ * @mitigates #parser against #insecure-deser using #config-validation -- "Shape is validated field by field before any entry is trusted; anything else is corrupt, not partially loaded" 20 │ * @exposes #cli to #arbitrary-write [low] cwe:CWE-73 -- "writeLedger writes one fixed path under root" 21 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "The path is the constant LEDGER_FILE joined to root; no caller supplies a path" 22 │ * @flows LedgerFile -> #parser via readLedger -- "Recorded hashes and verifiers" 23 │ * @flows #cli -> LedgerFile via writeLedger -- "The only write the verify surfaces perform" 24 │ */
L20 exposes #cli → #arbitrary-write
writeLedger writes one fixed path under root
15 │ * and otherwise treated as absent. It is never silently rewritten: `verify` 16 │ * refuses to write over it without `--force`. 17 │ * 18 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-502 -- "JSON.parse on a committed file under .guardlink/" 19 │ * @mitigates #parser against #insecure-deser using #config-validation -- "Shape is validated field by field before any entry is trusted; anything else is corrupt, not partially loaded" 20 │ * @exposes #cli to #arbitrary-write [low] cwe:CWE-73 -- "writeLedger writes one fixed path under root" 21 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "The path is the constant LEDGER_FILE joined to root; no caller supplies a path" 22 │ * @flows LedgerFile -> #parser via readLedger -- "Recorded hashes and verifiers" 23 │ * @flows #cli -> LedgerFile via writeLedger -- "The only write the verify surfaces perform" 24 │ */ 25 │ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
L21 mitigates #path-validation mitigates #arbitrary-write
The path is the constant LEDGER_FILE joined to root; no caller supplies a path
16 │ * refuses to write over it without `--force`. 17 │ * 18 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-502 -- "JSON.parse on a committed file under .guardlink/" 19 │ * @mitigates #parser against #insecure-deser using #config-validation -- "Shape is validated field by field before any entry is trusted; anything else is corrupt, not partially loaded" 20 │ * @exposes #cli to #arbitrary-write [low] cwe:CWE-73 -- "writeLedger writes one fixed path under root" 21 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "The path is the constant LEDGER_FILE joined to root; no caller supplies a path" 22 │ * @flows LedgerFile -> #parser via readLedger -- "Recorded hashes and verifiers" 23 │ * @flows #cli -> LedgerFile via writeLedger -- "The only write the verify surfaces perform" 24 │ */ 25 │ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; 26 │ import { dirname, join } from 'node:path';
L22 flow LedgerFile → #parser
Recorded hashes and verifiers
17 │ * 18 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-502 -- "JSON.parse on a committed file under .guardlink/" 19 │ * @mitigates #parser against #insecure-deser using #config-validation -- "Shape is validated field by field before any entry is trusted; anything else is corrupt, not partially loaded" 20 │ * @exposes #cli to #arbitrary-write [low] cwe:CWE-73 -- "writeLedger writes one fixed path under root" 21 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "The path is the constant LEDGER_FILE joined to root; no caller supplies a path" 22 │ * @flows LedgerFile -> #parser via readLedger -- "Recorded hashes and verifiers" 23 │ * @flows #cli -> LedgerFile via writeLedger -- "The only write the verify surfaces perform" 24 │ */ 25 │ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; 26 │ import { dirname, join } from 'node:path'; 27 │ import type { AnchorScope, AnnotationVerb, ParseDiagnostic } from '../types/index.js';
L23 flow #cli → LedgerFile
The only write the verify surfaces perform
18 │ * @exposes #parser to #insecure-deser [low] cwe:CWE-502 -- "JSON.parse on a committed file under .guardlink/" 19 │ * @mitigates #parser against #insecure-deser using #config-validation -- "Shape is validated field by field before any entry is trusted; anything else is corrupt, not partially loaded" 20 │ * @exposes #cli to #arbitrary-write [low] cwe:CWE-73 -- "writeLedger writes one fixed path under root" 21 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "The path is the constant LEDGER_FILE joined to root; no caller supplies a path" 22 │ * @flows LedgerFile -> #parser via readLedger -- "Recorded hashes and verifiers" 23 │ * @flows #cli -> LedgerFile via writeLedger -- "The only write the verify surfaces perform" 24 │ */ 25 │ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; 26 │ import { dirname, join } from 'node:path'; 27 │ import type { AnchorScope, AnnotationVerb, ParseDiagnostic } from '../types/index.js'; 28 │ import { ANCHOR_HASH_VERSION } from '../structure/hash.js';
src/parser/parse-file.ts 2 stale flow 3exposes 2audit 1comment 1 open 8
L6 exposes #parser → #path-traversal
File path from caller read via readFile; no validation here
1 │ /** 2 │ * GuardLink — File-level parser. 3 │ * Reads source files and extracts all GuardLink annotations. 4 │ * Standalone .gal files are treated as raw annotation text. 5 │ * 6 │ * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "File path from caller read via readFile; no validation here" 7 │ * @exposes #parser to #dos [medium] cwe:CWE-400 -- "Large files loaded entirely into memory" 8 │ * @audit #parser -- "Path validation delegated to callers (CLI/MCP validate root)" 9 │ * @flows FilePath -> #parser via readFile -- "Disk read path" 10 │ * @flows #parser -> Annotations via parseString -- "Parsed annotation output" 11 │ * @flows #parser -> ParseDiagnostics via parseString -- "Lines that were meant to be annotations and were not read, named rather than dropped"
L7 exposes #parser → #dos
Large files loaded entirely into memory
2 │ * GuardLink — File-level parser. 3 │ * Reads source files and extracts all GuardLink annotations. 4 │ * Standalone .gal files are treated as raw annotation text. 5 │ * 6 │ * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "File path from caller read via readFile; no validation here" 7 │ * @exposes #parser to #dos [medium] cwe:CWE-400 -- "Large files loaded entirely into memory" 8 │ * @audit #parser -- "Path validation delegated to callers (CLI/MCP validate root)" 9 │ * @flows FilePath -> #parser via readFile -- "Disk read path" 10 │ * @flows #parser -> Annotations via parseString -- "Parsed annotation output" 11 │ * @flows #parser -> ParseDiagnostics via parseString -- "Lines that were meant to be annotations and were not read, named rather than dropped" 12 │ * @comment -- "A line lost before parseLine is a security claim that reached no threat model and produced no warning; the two comment-form diagnostics here exist so the next unread form cannot survive a release unnoticed"
L8 audit Audit: #parser
Path validation delegated to callers (CLI/MCP validate root)
3 │ * Reads source files and extracts all GuardLink annotations. 4 │ * Standalone .gal files are treated as raw annotation text. 5 │ * 6 │ * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "File path from caller read via readFile; no validation here" 7 │ * @exposes #parser to #dos [medium] cwe:CWE-400 -- "Large files loaded entirely into memory" 8 │ * @audit #parser -- "Path validation delegated to callers (CLI/MCP validate root)" 9 │ * @flows FilePath -> #parser via readFile -- "Disk read path" 10 │ * @flows #parser -> Annotations via parseString -- "Parsed annotation output" 11 │ * @flows #parser -> ParseDiagnostics via parseString -- "Lines that were meant to be annotations and were not read, named rather than dropped" 12 │ * @comment -- "A line lost before parseLine is a security claim that reached no threat model and produced no warning; the two comment-form diagnostics here exist so the next unread form cannot survive a release unnoticed" 13 │ * @validates #input-sanitize for #parser -- "tests/comment-forms.test.ts pins that doc-tag and decorator traffic produces neither annotations nor diagnostics"
L9 flow FilePath → #parser
Disk read path
4 │ * Standalone .gal files are treated as raw annotation text. 5 │ * 6 │ * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "File path from caller read via readFile; no validation here" 7 │ * @exposes #parser to #dos [medium] cwe:CWE-400 -- "Large files loaded entirely into memory" 8 │ * @audit #parser -- "Path validation delegated to callers (CLI/MCP validate root)" 9 │ * @flows FilePath -> #parser via readFile -- "Disk read path" 10 │ * @flows #parser -> Annotations via parseString -- "Parsed annotation output" 11 │ * @flows #parser -> ParseDiagnostics via parseString -- "Lines that were meant to be annotations and were not read, named rather than dropped" 12 │ * @comment -- "A line lost before parseLine is a security claim that reached no threat model and produced no warning; the two comment-form diagnostics here exist so the next unread form cannot survive a release unnoticed" 13 │ * @validates #input-sanitize for #parser -- "tests/comment-forms.test.ts pins that doc-tag and decorator traffic produces neither annotations nor diagnostics" 14 │ */
L10 flow #parser → Annotations
Parsed annotation output
5 │ * 6 │ * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "File path from caller read via readFile; no validation here" 7 │ * @exposes #parser to #dos [medium] cwe:CWE-400 -- "Large files loaded entirely into memory" 8 │ * @audit #parser -- "Path validation delegated to callers (CLI/MCP validate root)" 9 │ * @flows FilePath -> #parser via readFile -- "Disk read path" 10 │ * @flows #parser -> Annotations via parseString -- "Parsed annotation output" 11 │ * @flows #parser -> ParseDiagnostics via parseString -- "Lines that were meant to be annotations and were not read, named rather than dropped" 12 │ * @comment -- "A line lost before parseLine is a security claim that reached no threat model and produced no warning; the two comment-form diagnostics here exist so the next unread form cannot survive a release unnoticed" 13 │ * @validates #input-sanitize for #parser -- "tests/comment-forms.test.ts pins that doc-tag and decorator traffic produces neither annotations nor diagnostics" 14 │ */ 15 │
L11 flow #parser → ParseDiagnostics
Lines that were meant to be annotations and were not read, named rather than dropped
6 │ * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "File path from caller read via readFile; no validation here" 7 │ * @exposes #parser to #dos [medium] cwe:CWE-400 -- "Large files loaded entirely into memory" 8 │ * @audit #parser -- "Path validation delegated to callers (CLI/MCP validate root)" 9 │ * @flows FilePath -> #parser via readFile -- "Disk read path" 10 │ * @flows #parser -> Annotations via parseString -- "Parsed annotation output" 11 │ * @flows #parser -> ParseDiagnostics via parseString -- "Lines that were meant to be annotations and were not read, named rather than dropped" 12 │ * @comment -- "A line lost before parseLine is a security claim that reached no threat model and produced no warning; the two comment-form diagnostics here exist so the next unread form cannot survive a release unnoticed" 13 │ * @validates #input-sanitize for #parser -- "tests/comment-forms.test.ts pins that doc-tag and decorator traffic produces neither annotations nor diagnostics" 14 │ */ 15 │ 16 │ import { readFile } from 'node:fs/promises';
L12 comment A line lost before parseLine is a security claim that reached no threat model and produced no warning; the two comment-form diagnostics here exist so the next unread form cannot survive a release unnoticed
A line lost before parseLine is a security claim that reached no threat model and produced no warning; the two comment-form diagnostics here exist so the next unread form cannot survive a release unnoticed
7 │ * @exposes #parser to #dos [medium] cwe:CWE-400 -- "Large files loaded entirely into memory" 8 │ * @audit #parser -- "Path validation delegated to callers (CLI/MCP validate root)" 9 │ * @flows FilePath -> #parser via readFile -- "Disk read path" 10 │ * @flows #parser -> Annotations via parseString -- "Parsed annotation output" 11 │ * @flows #parser -> ParseDiagnostics via parseString -- "Lines that were meant to be annotations and were not read, named rather than dropped" 12 │ * @comment -- "A line lost before parseLine is a security claim that reached no threat model and produced no warning; the two comment-form diagnostics here exist so the next unread form cannot survive a release unnoticed" 13 │ * @validates #input-sanitize for #parser -- "tests/comment-forms.test.ts pins that doc-tag and decorator traffic produces neither annotations nor diagnostics" 14 │ */ 15 │ 16 │ import { readFile } from 'node:fs/promises'; 17 │ import type { Annotation, ParseDiagnostic, ParseResult, SourceLocation } from '../types/index.js';
L13 validates #input-sanitize validates #parser
tests/comment-forms.test.ts pins that doc-tag and decorator traffic produces neither annotations nor diagnostics
8 │ * @audit #parser -- "Path validation delegated to callers (CLI/MCP validate root)" 9 │ * @flows FilePath -> #parser via readFile -- "Disk read path" 10 │ * @flows #parser -> Annotations via parseString -- "Parsed annotation output" 11 │ * @flows #parser -> ParseDiagnostics via parseString -- "Lines that were meant to be annotations and were not read, named rather than dropped" 12 │ * @comment -- "A line lost before parseLine is a security claim that reached no threat model and produced no warning; the two comment-form diagnostics here exist so the next unread form cannot survive a release unnoticed" 13 │ * @validates #input-sanitize for #parser -- "tests/comment-forms.test.ts pins that doc-tag and decorator traffic produces neither annotations nor diagnostics" 14 │ */ 15 │ 16 │ import { readFile } from 'node:fs/promises'; 17 │ import type { Annotation, ParseDiagnostic, ParseResult, SourceLocation } from '../types/index.js'; 18 │ import { isStandaloneAnnotationFile, stripCommentPrefix } from './comment-strip.js';
src/parser/parse-line.ts 2 stale comment 3exposes 1mitigates 1 open 5
L5 exposes #parser → #redos
Complex regex patterns applied to annotation text
1 │ /** 2 │ * GuardLink — Line-level annotation parser. 3 │ * Parses a single comment line into a typed Annotation. 4 │ * 5 │ * @exposes #parser to #redos [medium] cwe:CWE-1333 -- "Complex regex patterns applied to annotation text" 6 │ * @mitigates #parser against #redos using #regex-anchoring -- "All patterns are anchored (^...$) to prevent backtracking" 7 │ * @comment -- "Regex patterns designed with bounded quantifiers and explicit structure" 8 │ * @comment -- "@entitles capability is a single-token identifier by grammar, so prose in that position is a parse error rather than a label nothing can group or compare (actor-entitlement design §3.1)" 9 │ * @comment -- "@entitles takes optional `on <asset>` and `against <threat>` clauses because the join is the (actor, asset, threat) triple, not the capability — a capability-keyed join would demote every threat on the asset including one discovered later (actor-entitlement design §9.3)" 10 │ */
L6 mitigates #regex-anchoring mitigates #redos
All patterns are anchored (^...$) to prevent backtracking
1 │ /** 2 │ * GuardLink — Line-level annotation parser. 3 │ * Parses a single comment line into a typed Annotation. 4 │ * 5 │ * @exposes #parser to #redos [medium] cwe:CWE-1333 -- "Complex regex patterns applied to annotation text" 6 │ * @mitigates #parser against #redos using #regex-anchoring -- "All patterns are anchored (^...$) to prevent backtracking" 7 │ * @comment -- "Regex patterns designed with bounded quantifiers and explicit structure" 8 │ * @comment -- "@entitles capability is a single-token identifier by grammar, so prose in that position is a parse error rather than a label nothing can group or compare (actor-entitlement design §3.1)" 9 │ * @comment -- "@entitles takes optional `on <asset>` and `against <threat>` clauses because the join is the (actor, asset, threat) triple, not the capability — a capability-keyed join would demote every threat on the asset including one discovered later (actor-entitlement design §9.3)" 10 │ */ 11 │
L7 comment Regex patterns designed with bounded quantifiers and explicit structure
Regex patterns designed with bounded quantifiers and explicit structure
2 │ * GuardLink — Line-level annotation parser. 3 │ * Parses a single comment line into a typed Annotation. 4 │ * 5 │ * @exposes #parser to #redos [medium] cwe:CWE-1333 -- "Complex regex patterns applied to annotation text" 6 │ * @mitigates #parser against #redos using #regex-anchoring -- "All patterns are anchored (^...$) to prevent backtracking" 7 │ * @comment -- "Regex patterns designed with bounded quantifiers and explicit structure" 8 │ * @comment -- "@entitles capability is a single-token identifier by grammar, so prose in that position is a parse error rather than a label nothing can group or compare (actor-entitlement design §3.1)" 9 │ * @comment -- "@entitles takes optional `on <asset>` and `against <threat>` clauses because the join is the (actor, asset, threat) triple, not the capability — a capability-keyed join would demote every threat on the asset including one discovered later (actor-entitlement design §9.3)" 10 │ */ 11 │ 12 │ import type {
L8 comment @entitles capability is a single-token identifier by grammar, so prose in that position is a parse error rather than a label nothing can group or compare (actor-entitlement design §3.1)
@entitles capability is a single-token identifier by grammar, so prose in that position is a parse error rather than a label nothing can group or compare (actor-entitlement design §3.1)
3 │ * Parses a single comment line into a typed Annotation. 4 │ * 5 │ * @exposes #parser to #redos [medium] cwe:CWE-1333 -- "Complex regex patterns applied to annotation text" 6 │ * @mitigates #parser against #redos using #regex-anchoring -- "All patterns are anchored (^...$) to prevent backtracking" 7 │ * @comment -- "Regex patterns designed with bounded quantifiers and explicit structure" 8 │ * @comment -- "@entitles capability is a single-token identifier by grammar, so prose in that position is a parse error rather than a label nothing can group or compare (actor-entitlement design §3.1)" 9 │ * @comment -- "@entitles takes optional `on <asset>` and `against <threat>` clauses because the join is the (actor, asset, threat) triple, not the capability — a capability-keyed join would demote every threat on the asset including one discovered later (actor-entitlement design §9.3)" 10 │ */ 11 │ 12 │ import type { 13 │ Annotation, DataClassification,
L9 comment @entitles takes optional `on <asset>` and `against <threat>` clauses because the join is the (actor, asset, threat) triple, not the capability — a capability-keyed join would demote every threat on the asset including one discovered later (actor-entitlement design §9.3)
@entitles takes optional `on <asset>` and `against <threat>` clauses because the join is the (actor, asset, threat) triple, not the capability — a capability-keyed join would demote every threat on the asset including one discovered later (actor-entitlement design §9.3)
4 │ * 5 │ * @exposes #parser to #redos [medium] cwe:CWE-1333 -- "Complex regex patterns applied to annotation text" 6 │ * @mitigates #parser against #redos using #regex-anchoring -- "All patterns are anchored (^...$) to prevent backtracking" 7 │ * @comment -- "Regex patterns designed with bounded quantifiers and explicit structure" 8 │ * @comment -- "@entitles capability is a single-token identifier by grammar, so prose in that position is a parse error rather than a label nothing can group or compare (actor-entitlement design §3.1)" 9 │ * @comment -- "@entitles takes optional `on <asset>` and `against <threat>` clauses because the join is the (actor, asset, threat) triple, not the capability — a capability-keyed join would demote every threat on the asset including one discovered later (actor-entitlement design §9.3)" 10 │ */ 11 │ 12 │ import type { 13 │ Annotation, DataClassification, 14 │ ParseDiagnostic, SourceLocation,
src/parser/parse-project.ts 4 stale comment 3exposes 2mitigates 2flow 2 open 10
L5 exposes #parser → #path-traversal
Glob patterns could escape root directory
1 │ /** 2 │ * GuardLink — Project-level parser. 3 │ * Walks a directory, parses all source files, and assembles a ThreatModel. 4 │ * 5 │ * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "Glob patterns could escape root directory" 6 │ * @mitigates #parser against #path-traversal using #glob-filtering -- "DEFAULT_EXCLUDE blocks node_modules, .git; fast-glob cwd constrains scan" 7 │ * @exposes #parser to #dos [medium] cwe:CWE-400 -- "Large projects with many files could exhaust memory" 8 │ * @mitigates #parser against #dos using #resource-limits -- "DEFAULT_EXCLUDE skips build artifacts, tests; limits effective file count" 9 │ * @flows ProjectRoot -> #parser via fast-glob -- "Directory traversal path" 10 │ * @flows #parser -> ThreatModel via assembleModel -- "Aggregated threat model output"
L6 mitigates #glob-filtering mitigates #path-traversal
DEFAULT_EXCLUDE blocks node_modules, .git; fast-glob cwd constrains scan
1 │ /** 2 │ * GuardLink — Project-level parser. 3 │ * Walks a directory, parses all source files, and assembles a ThreatModel. 4 │ * 5 │ * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "Glob patterns could escape root directory" 6 │ * @mitigates #parser against #path-traversal using #glob-filtering -- "DEFAULT_EXCLUDE blocks node_modules, .git; fast-glob cwd constrains scan" 7 │ * @exposes #parser to #dos [medium] cwe:CWE-400 -- "Large projects with many files could exhaust memory" 8 │ * @mitigates #parser against #dos using #resource-limits -- "DEFAULT_EXCLUDE skips build artifacts, tests; limits effective file count" 9 │ * @flows ProjectRoot -> #parser via fast-glob -- "Directory traversal path" 10 │ * @flows #parser -> ThreatModel via assembleModel -- "Aggregated threat model output" 11 │ * @comment -- "Scans standalone .gal files in addition to comment-based source annotations"
L7 exposes #parser → #dos
Large projects with many files could exhaust memory
2 │ * GuardLink — Project-level parser. 3 │ * Walks a directory, parses all source files, and assembles a ThreatModel. 4 │ * 5 │ * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "Glob patterns could escape root directory" 6 │ * @mitigates #parser against #path-traversal using #glob-filtering -- "DEFAULT_EXCLUDE blocks node_modules, .git; fast-glob cwd constrains scan" 7 │ * @exposes #parser to #dos [medium] cwe:CWE-400 -- "Large projects with many files could exhaust memory" 8 │ * @mitigates #parser against #dos using #resource-limits -- "DEFAULT_EXCLUDE skips build artifacts, tests; limits effective file count" 9 │ * @flows ProjectRoot -> #parser via fast-glob -- "Directory traversal path" 10 │ * @flows #parser -> ThreatModel via assembleModel -- "Aggregated threat model output" 11 │ * @comment -- "Scans standalone .gal files in addition to comment-based source annotations" 12 │ * @comment -- "Every @entitles gets its citation resolved at assembly time and carries inert:true when uncited, so no consumer can read an uncited privilege claim as an effective one by forgetting to check (actor-entitlement design §3.4)"
L8 mitigates #resource-limits mitigates #dos
DEFAULT_EXCLUDE skips build artifacts, tests; limits effective file count
3 │ * Walks a directory, parses all source files, and assembles a ThreatModel. 4 │ * 5 │ * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "Glob patterns could escape root directory" 6 │ * @mitigates #parser against #path-traversal using #glob-filtering -- "DEFAULT_EXCLUDE blocks node_modules, .git; fast-glob cwd constrains scan" 7 │ * @exposes #parser to #dos [medium] cwe:CWE-400 -- "Large projects with many files could exhaust memory" 8 │ * @mitigates #parser against #dos using #resource-limits -- "DEFAULT_EXCLUDE skips build artifacts, tests; limits effective file count" 9 │ * @flows ProjectRoot -> #parser via fast-glob -- "Directory traversal path" 10 │ * @flows #parser -> ThreatModel via assembleModel -- "Aggregated threat model output" 11 │ * @comment -- "Scans standalone .gal files in addition to comment-based source annotations" 12 │ * @comment -- "Every @entitles gets its citation resolved at assembly time and carries inert:true when uncited, so no consumer can read an uncited privilege claim as an effective one by forgetting to check (actor-entitlement design §3.4)" 13 │ * @comment -- "Same reasoning for imprecise:true when the claim omits `on <asset>` or `against <threat>`, plus canEntitlementDemote/entitlementDemotionBlockers so the cited-and-precise test is written once here rather than re-derived by every consumer — a consumer that checks only !inert would demote on an entitlement that joins nothing (actor-entitlement design §9.7)"
L9 flow ProjectRoot → #parser
Directory traversal path
4 │ * 5 │ * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "Glob patterns could escape root directory" 6 │ * @mitigates #parser against #path-traversal using #glob-filtering -- "DEFAULT_EXCLUDE blocks node_modules, .git; fast-glob cwd constrains scan" 7 │ * @exposes #parser to #dos [medium] cwe:CWE-400 -- "Large projects with many files could exhaust memory" 8 │ * @mitigates #parser against #dos using #resource-limits -- "DEFAULT_EXCLUDE skips build artifacts, tests; limits effective file count" 9 │ * @flows ProjectRoot -> #parser via fast-glob -- "Directory traversal path" 10 │ * @flows #parser -> ThreatModel via assembleModel -- "Aggregated threat model output" 11 │ * @comment -- "Scans standalone .gal files in addition to comment-based source annotations" 12 │ * @comment -- "Every @entitles gets its citation resolved at assembly time and carries inert:true when uncited, so no consumer can read an uncited privilege claim as an effective one by forgetting to check (actor-entitlement design §3.4)" 13 │ * @comment -- "Same reasoning for imprecise:true when the claim omits `on <asset>` or `against <threat>`, plus canEntitlementDemote/entitlementDemotionBlockers so the cited-and-precise test is written once here rather than re-derived by every consumer — a consumer that checks only !inert would demote on an entitlement that joins nothing (actor-entitlement design §9.7)" 14 │ * @boundary #parser and FileSystem (#fs-boundary) -- "Trust boundary between parser and disk I/O"
L10 flow #parser → ThreatModel
Aggregated threat model output
5 │ * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "Glob patterns could escape root directory" 6 │ * @mitigates #parser against #path-traversal using #glob-filtering -- "DEFAULT_EXCLUDE blocks node_modules, .git; fast-glob cwd constrains scan" 7 │ * @exposes #parser to #dos [medium] cwe:CWE-400 -- "Large projects with many files could exhaust memory" 8 │ * @mitigates #parser against #dos using #resource-limits -- "DEFAULT_EXCLUDE skips build artifacts, tests; limits effective file count" 9 │ * @flows ProjectRoot -> #parser via fast-glob -- "Directory traversal path" 10 │ * @flows #parser -> ThreatModel via assembleModel -- "Aggregated threat model output" 11 │ * @comment -- "Scans standalone .gal files in addition to comment-based source annotations" 12 │ * @comment -- "Every @entitles gets its citation resolved at assembly time and carries inert:true when uncited, so no consumer can read an uncited privilege claim as an effective one by forgetting to check (actor-entitlement design §3.4)" 13 │ * @comment -- "Same reasoning for imprecise:true when the claim omits `on <asset>` or `against <threat>`, plus canEntitlementDemote/entitlementDemotionBlockers so the cited-and-precise test is written once here rather than re-derived by every consumer — a consumer that checks only !inert would demote on an entitlement that joins nothing (actor-entitlement design §9.7)" 14 │ * @boundary #parser and FileSystem (#fs-boundary) -- "Trust boundary between parser and disk I/O" 15 │ */
L11 comment Scans standalone .gal files in addition to comment-based source annotations
Scans standalone .gal files in addition to comment-based source annotations
6 │ * @mitigates #parser against #path-traversal using #glob-filtering -- "DEFAULT_EXCLUDE blocks node_modules, .git; fast-glob cwd constrains scan" 7 │ * @exposes #parser to #dos [medium] cwe:CWE-400 -- "Large projects with many files could exhaust memory" 8 │ * @mitigates #parser against #dos using #resource-limits -- "DEFAULT_EXCLUDE skips build artifacts, tests; limits effective file count" 9 │ * @flows ProjectRoot -> #parser via fast-glob -- "Directory traversal path" 10 │ * @flows #parser -> ThreatModel via assembleModel -- "Aggregated threat model output" 11 │ * @comment -- "Scans standalone .gal files in addition to comment-based source annotations" 12 │ * @comment -- "Every @entitles gets its citation resolved at assembly time and carries inert:true when uncited, so no consumer can read an uncited privilege claim as an effective one by forgetting to check (actor-entitlement design §3.4)" 13 │ * @comment -- "Same reasoning for imprecise:true when the claim omits `on <asset>` or `against <threat>`, plus canEntitlementDemote/entitlementDemotionBlockers so the cited-and-precise test is written once here rather than re-derived by every consumer — a consumer that checks only !inert would demote on an entitlement that joins nothing (actor-entitlement design §9.7)" 14 │ * @boundary #parser and FileSystem (#fs-boundary) -- "Trust boundary between parser and disk I/O" 15 │ */ 16 │
L12 comment Every @entitles gets its citation resolved at assembly time and carries inert:true when uncited, so no consumer can read an uncited privilege claim as an effective one by forgetting to check (actor-entitlement design §3.4)
Every @entitles gets its citation resolved at assembly time and carries inert:true when uncited, so no consumer can read an uncited privilege claim as an effective one by forgetting to check (actor-entitlement design §3.4)
7 │ * @exposes #parser to #dos [medium] cwe:CWE-400 -- "Large projects with many files could exhaust memory" 8 │ * @mitigates #parser against #dos using #resource-limits -- "DEFAULT_EXCLUDE skips build artifacts, tests; limits effective file count" 9 │ * @flows ProjectRoot -> #parser via fast-glob -- "Directory traversal path" 10 │ * @flows #parser -> ThreatModel via assembleModel -- "Aggregated threat model output" 11 │ * @comment -- "Scans standalone .gal files in addition to comment-based source annotations" 12 │ * @comment -- "Every @entitles gets its citation resolved at assembly time and carries inert:true when uncited, so no consumer can read an uncited privilege claim as an effective one by forgetting to check (actor-entitlement design §3.4)" 13 │ * @comment -- "Same reasoning for imprecise:true when the claim omits `on <asset>` or `against <threat>`, plus canEntitlementDemote/entitlementDemotionBlockers so the cited-and-precise test is written once here rather than re-derived by every consumer — a consumer that checks only !inert would demote on an entitlement that joins nothing (actor-entitlement design §9.7)" 14 │ * @boundary #parser and FileSystem (#fs-boundary) -- "Trust boundary between parser and disk I/O" 15 │ */ 16 │ 17 │ import fg from 'fast-glob';
L13 comment Same reasoning for imprecise:true when the claim omits `on <asset>` or `against <threat>`, plus canEntitlementDemote/entitlementDemotionBlockers so the cited-and-precise test is written once here rather than re-derived by every consumer — a consumer that checks only !inert would demote on an entitlement that joins nothing (actor-entitlement design §9.7)
Same reasoning for imprecise:true when the claim omits `on <asset>` or `against <threat>`, plus canEntitlementDemote/entitlementDemotionBlockers so the cited-and-precise test is written once here rather than re-derived by every consumer — a consumer that checks only !inert would demote on an entitlement that joins nothing (actor-entitlement design §9.7)
8 │ * @mitigates #parser against #dos using #resource-limits -- "DEFAULT_EXCLUDE skips build artifacts, tests; limits effective file count" 9 │ * @flows ProjectRoot -> #parser via fast-glob -- "Directory traversal path" 10 │ * @flows #parser -> ThreatModel via assembleModel -- "Aggregated threat model output" 11 │ * @comment -- "Scans standalone .gal files in addition to comment-based source annotations" 12 │ * @comment -- "Every @entitles gets its citation resolved at assembly time and carries inert:true when uncited, so no consumer can read an uncited privilege claim as an effective one by forgetting to check (actor-entitlement design §3.4)" 13 │ * @comment -- "Same reasoning for imprecise:true when the claim omits `on <asset>` or `against <threat>`, plus canEntitlementDemote/entitlementDemotionBlockers so the cited-and-precise test is written once here rather than re-derived by every consumer — a consumer that checks only !inert would demote on an entitlement that joins nothing (actor-entitlement design §9.7)" 14 │ * @boundary #parser and FileSystem (#fs-boundary) -- "Trust boundary between parser and disk I/O" 15 │ */ 16 │ 17 │ import fg from 'fast-glob'; 18 │ import { isAbsolute, relative } from 'node:path';
L14 boundary #parser ↔ FileSystem
Trust boundary between parser and disk I/O
9 │ * @flows ProjectRoot -> #parser via fast-glob -- "Directory traversal path" 10 │ * @flows #parser -> ThreatModel via assembleModel -- "Aggregated threat model output" 11 │ * @comment -- "Scans standalone .gal files in addition to comment-based source annotations" 12 │ * @comment -- "Every @entitles gets its citation resolved at assembly time and carries inert:true when uncited, so no consumer can read an uncited privilege claim as an effective one by forgetting to check (actor-entitlement design §3.4)" 13 │ * @comment -- "Same reasoning for imprecise:true when the claim omits `on <asset>` or `against <threat>`, plus canEntitlementDemote/entitlementDemotionBlockers so the cited-and-precise test is written once here rather than re-derived by every consumer — a consumer that checks only !inert would demote on an entitlement that joins nothing (actor-entitlement design §9.7)" 14 │ * @boundary #parser and FileSystem (#fs-boundary) -- "Trust boundary between parser and disk I/O" 15 │ */ 16 │ 17 │ import fg from 'fast-glob'; 18 │ import { isAbsolute, relative } from 'node:path'; 19 │ import type {
src/parser/reanchor.ts exposes 1mitigates 1flow 1comment 1 open 4
L19 exposes #parser → #path-traversal
Reads source files named by @source blocks
14 │ * Reports, never repairs. A proposed line is a suggestion for a human or an 15 │ * agent to accept — silently rewriting an anchor would move an annotation onto 16 │ * code nobody chose for it, which is the same failure as the drift, arrived at 17 │ * faster. 18 │ * 19 │ * @exposes #parser to #path-traversal [low] cwe:CWE-22 -- "Reads source files named by @source blocks" 20 │ * @mitigates #parser against #path-traversal using #path-validation -- "Paths are joined to root and skipped when missing; nothing outside the model is read" 21 │ * @flows ThreatModel -> #parser via findAnchorDrift -- "Recorded anchors compared against current source" 22 │ * @comment -- "Symbol matching is deliberately loose: it looks for the name as a whole word, not for a specific declaration syntax, so it works across languages" 23 │ */ 24 │
L20 mitigates #path-validation mitigates #path-traversal
Paths are joined to root and skipped when missing; nothing outside the model is read
15 │ * agent to accept — silently rewriting an anchor would move an annotation onto 16 │ * code nobody chose for it, which is the same failure as the drift, arrived at 17 │ * faster. 18 │ * 19 │ * @exposes #parser to #path-traversal [low] cwe:CWE-22 -- "Reads source files named by @source blocks" 20 │ * @mitigates #parser against #path-traversal using #path-validation -- "Paths are joined to root and skipped when missing; nothing outside the model is read" 21 │ * @flows ThreatModel -> #parser via findAnchorDrift -- "Recorded anchors compared against current source" 22 │ * @comment -- "Symbol matching is deliberately loose: it looks for the name as a whole word, not for a specific declaration syntax, so it works across languages" 23 │ */ 24 │ 25 │ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
L21 flow ThreatModel → #parser
Recorded anchors compared against current source
16 │ * code nobody chose for it, which is the same failure as the drift, arrived at 17 │ * faster. 18 │ * 19 │ * @exposes #parser to #path-traversal [low] cwe:CWE-22 -- "Reads source files named by @source blocks" 20 │ * @mitigates #parser against #path-traversal using #path-validation -- "Paths are joined to root and skipped when missing; nothing outside the model is read" 21 │ * @flows ThreatModel -> #parser via findAnchorDrift -- "Recorded anchors compared against current source" 22 │ * @comment -- "Symbol matching is deliberately loose: it looks for the name as a whole word, not for a specific declaration syntax, so it works across languages" 23 │ */ 24 │ 25 │ import { existsSync, readFileSync, writeFileSync } from 'node:fs'; 26 │ import { join } from 'node:path';
L22 comment Symbol matching is deliberately loose: it looks for the name as a whole word, not for a specific declaration syntax, so it works across languages
Symbol matching is deliberately loose: it looks for the name as a whole word, not for a specific declaration syntax, so it works across languages
17 │ * faster. 18 │ * 19 │ * @exposes #parser to #path-traversal [low] cwe:CWE-22 -- "Reads source files named by @source blocks" 20 │ * @mitigates #parser against #path-traversal using #path-validation -- "Paths are joined to root and skipped when missing; nothing outside the model is read" 21 │ * @flows ThreatModel -> #parser via findAnchorDrift -- "Recorded anchors compared against current source" 22 │ * @comment -- "Symbol matching is deliberately loose: it looks for the name as a whole word, not for a specific declaration syntax, so it works across languages" 23 │ */ 24 │ 25 │ import { existsSync, readFileSync, writeFileSync } from 'node:fs'; 26 │ import { join } from 'node:path'; 27 │ import type { ThreatModel, SourceLocation } from '../types/index.js';
src/parser/validate.ts comment 2mitigates 1 open 3
L7 mitigates #prefix-ownership mitigates #tag-collision
findDanglingRefs ensures #id refs resolve to definitions
2 │ * GuardLink — Shared validation helpers. 3 │ * 4 │ * Extracted from cli/index.ts and tui/commands.ts to eliminate duplication 5 │ * and ensure consistent validation logic across all entry points. 6 │ * 7 │ * @mitigates #parser against #tag-collision using #prefix-ownership -- "findDanglingRefs ensures #id refs resolve to definitions" 8 │ * @comment -- "@confirmed refs validated same as @exposes for asset and threat" 9 │ * @comment -- "findUndeclaredActors / findInertEntitlements implement the two mechanical @entitles checks from docs/prd/actor-entitlement-design.md §3.7 — both typo-class; entitlement intent is not machine-checkable" 10 │ */ 11 │ 12 │ import {
L8 comment @confirmed refs validated same as @exposes for asset and threat
@confirmed refs validated same as @exposes for asset and threat
3 │ * 4 │ * Extracted from cli/index.ts and tui/commands.ts to eliminate duplication 5 │ * and ensure consistent validation logic across all entry points. 6 │ * 7 │ * @mitigates #parser against #tag-collision using #prefix-ownership -- "findDanglingRefs ensures #id refs resolve to definitions" 8 │ * @comment -- "@confirmed refs validated same as @exposes for asset and threat" 9 │ * @comment -- "findUndeclaredActors / findInertEntitlements implement the two mechanical @entitles checks from docs/prd/actor-entitlement-design.md §3.7 — both typo-class; entitlement intent is not machine-checkable" 10 │ */ 11 │ 12 │ import { 13 │ isConventionalGalPath, sourceFileForGal, galPathFor, offConventionMessage,
L9 comment findUndeclaredActors / findInertEntitlements implement the two mechanical @entitles checks from docs/prd/actor-entitlement-design.md §3.7 — both typo-class; entitlement intent is not machine-checkable
findUndeclaredActors / findInertEntitlements implement the two mechanical @entitles checks from docs/prd/actor-entitlement-design.md §3.7 — both typo-class; entitlement intent is not machine-checkable
4 │ * Extracted from cli/index.ts and tui/commands.ts to eliminate duplication 5 │ * and ensure consistent validation logic across all entry points. 6 │ * 7 │ * @mitigates #parser against #tag-collision using #prefix-ownership -- "findDanglingRefs ensures #id refs resolve to definitions" 8 │ * @comment -- "@confirmed refs validated same as @exposes for asset and threat" 9 │ * @comment -- "findUndeclaredActors / findInertEntitlements implement the two mechanical @entitles checks from docs/prd/actor-entitlement-design.md §3.7 — both typo-class; entitlement intent is not machine-checkable" 10 │ */ 11 │ 12 │ import { 13 │ isConventionalGalPath, sourceFileForGal, galPathFor, offConventionMessage, 14 │ } from './gal-path.js';
src/parser/verification.ts flow 2comment 1 open 3
L29 flow ThreatModel → #parser
Anchors on every relation record compared with the ledger
24 │ * never stale: comparing a fallback hash against a token hash recorded on a 25 │ * machine where the grammar loaded is meaningless, and treating that as 26 │ * staleness would fail `--strict` builds on a packaging defect rather than a 27 │ * code change. 28 │ * 29 │ * @flows ThreatModel -> #parser via classifyClaims -- "Anchors on every relation record compared with the ledger" 30 │ * @flows LedgerFile -> #parser via classifyClaims -- "Recorded hashes, already read by the caller" 31 │ * @comment -- "Pure function; no I/O. Demotion is a SET of claim keys handed to coverage.ts, never a rewrite of the model" 32 │ */ 33 │ import type { ThreatModel, SourceLocation, Anchor } from '../types/index.js'; 34 │ import { relationRecords, type ClaimVerb } from './claim-key.js';
L30 flow LedgerFile → #parser
Recorded hashes, already read by the caller
25 │ * machine where the grammar loaded is meaningless, and treating that as 26 │ * staleness would fail `--strict` builds on a packaging defect rather than a 27 │ * code change. 28 │ * 29 │ * @flows ThreatModel -> #parser via classifyClaims -- "Anchors on every relation record compared with the ledger" 30 │ * @flows LedgerFile -> #parser via classifyClaims -- "Recorded hashes, already read by the caller" 31 │ * @comment -- "Pure function; no I/O. Demotion is a SET of claim keys handed to coverage.ts, never a rewrite of the model" 32 │ */ 33 │ import type { ThreatModel, SourceLocation, Anchor } from '../types/index.js'; 34 │ import { relationRecords, type ClaimVerb } from './claim-key.js'; 35 │ import type { Ledger, LedgerEntry, LedgerRead, LedgerStatus } from './ledger.js';
L31 comment Pure function; no I/O. Demotion is a SET of claim keys handed to coverage.ts, never a rewrite of the model
Pure function; no I/O. Demotion is a SET of claim keys handed to coverage.ts, never a rewrite of the model
26 │ * staleness would fail `--strict` builds on a packaging defect rather than a 27 │ * code change. 28 │ * 29 │ * @flows ThreatModel -> #parser via classifyClaims -- "Anchors on every relation record compared with the ledger" 30 │ * @flows LedgerFile -> #parser via classifyClaims -- "Recorded hashes, already read by the caller" 31 │ * @comment -- "Pure function; no I/O. Demotion is a SET of claim keys handed to coverage.ts, never a rewrite of the model" 32 │ */ 33 │ import type { ThreatModel, SourceLocation, Anchor } from '../types/index.js'; 34 │ import { relationRecords, type ClaimVerb } from './claim-key.js'; 35 │ import type { Ledger, LedgerEntry, LedgerRead, LedgerStatus } from './ledger.js'; 36 │ import { ANCHOR_HASH_VERSION } from '../structure/hash.js';
src/parser/verify.ts flow 1exposes 1mitigates 1comment 1 open 4
L12 flow #cli → LedgerFile
The ledger the verify surfaces write
7 │ * applyVerification(ledger, plan, identity) → the new ledger, not yet written 8 │ * 9 │ * Re-locking a stale claim asserts the control still holds, so the default 10 │ * mode never does it; `stale`, `all` and a named target do, and say so. 11 │ * 12 │ * @flows #cli -> LedgerFile via applyVerification -- "The ledger the verify surfaces write" 13 │ * @exposes #cli to #cmd-injection [low] cwe:CWE-78 -- "git is spawned for the verifier name and HEAD" 14 │ * @mitigates #cli against #cmd-injection using #param-commands -- "execFileSync with a fixed argv; no shell, no caller-supplied argument" 15 │ * @comment -- "Identity comes from git config or the OS user, is prefixed human: here and agent: in the MCP tool, and is recorded verbatim so a later gate can key on the prefix" 16 │ */ 17 │ import { execFileSync } from 'node:child_process';
L13 exposes #cli → #cmd-injection
git is spawned for the verifier name and HEAD
8 │ * 9 │ * Re-locking a stale claim asserts the control still holds, so the default 10 │ * mode never does it; `stale`, `all` and a named target do, and say so. 11 │ * 12 │ * @flows #cli -> LedgerFile via applyVerification -- "The ledger the verify surfaces write" 13 │ * @exposes #cli to #cmd-injection [low] cwe:CWE-78 -- "git is spawned for the verifier name and HEAD" 14 │ * @mitigates #cli against #cmd-injection using #param-commands -- "execFileSync with a fixed argv; no shell, no caller-supplied argument" 15 │ * @comment -- "Identity comes from git config or the OS user, is prefixed human: here and agent: in the MCP tool, and is recorded verbatim so a later gate can key on the prefix" 16 │ */ 17 │ import { execFileSync } from 'node:child_process'; 18 │ import { userInfo } from 'node:os';
L14 mitigates #param-commands mitigates #cmd-injection
execFileSync with a fixed argv; no shell, no caller-supplied argument
9 │ * Re-locking a stale claim asserts the control still holds, so the default 10 │ * mode never does it; `stale`, `all` and a named target do, and say so. 11 │ * 12 │ * @flows #cli -> LedgerFile via applyVerification -- "The ledger the verify surfaces write" 13 │ * @exposes #cli to #cmd-injection [low] cwe:CWE-78 -- "git is spawned for the verifier name and HEAD" 14 │ * @mitigates #cli against #cmd-injection using #param-commands -- "execFileSync with a fixed argv; no shell, no caller-supplied argument" 15 │ * @comment -- "Identity comes from git config or the OS user, is prefixed human: here and agent: in the MCP tool, and is recorded verbatim so a later gate can key on the prefix" 16 │ */ 17 │ import { execFileSync } from 'node:child_process'; 18 │ import { userInfo } from 'node:os'; 19 │ import type { Ledger, LedgerEntry } from './ledger.js';
L15 comment Identity comes from git config or the OS user, is prefixed human: here and agent: in the MCP tool, and is recorded verbatim so a later gate can key on the prefix
Identity comes from git config or the OS user, is prefixed human: here and agent: in the MCP tool, and is recorded verbatim so a later gate can key on the prefix
10 │ * mode never does it; `stale`, `all` and a named target do, and say so. 11 │ * 12 │ * @flows #cli -> LedgerFile via applyVerification -- "The ledger the verify surfaces write" 13 │ * @exposes #cli to #cmd-injection [low] cwe:CWE-78 -- "git is spawned for the verifier name and HEAD" 14 │ * @mitigates #cli against #cmd-injection using #param-commands -- "execFileSync with a fixed argv; no shell, no caller-supplied argument" 15 │ * @comment -- "Identity comes from git config or the OS user, is prefixed human: here and agent: in the MCP tool, and is recorded verbatim so a later gate can key on the prefix" 16 │ */ 17 │ import { execFileSync } from 'node:child_process'; 18 │ import { userInfo } from 'node:os'; 19 │ import type { Ledger, LedgerEntry } from './ledger.js'; 20 │ import { emptyLedger } from './ledger.js';
src/paths/format.ts comment 1 open 1
L4 comment Pure string formatting; no I/O. Every hop prints its file:line because a path that cannot be walked in an editor is an assertion rather than a finding, and the empty case states which kind of empty it is — no undefended route in the annotated graph is not the same claim as no undefended route in the code
Pure string formatting; no I/O. Every hop prints its file:line because a path that cannot be walked in an editor is an assertion rather than a finding, and the empty case states which kind of empty it is — no undefended route in the annotated graph is not the same claim as no undefended route in the code
1 │ /** 2 │ * GuardLink Paths — human-readable output formatter. 3 │ * 4 │ * @comment -- "Pure string formatting; no I/O. Every hop prints its file:line because a path that cannot be walked in an editor is an assertion rather than a finding, and the empty case states which kind of empty it is — no undefended route in the annotated graph is not the same claim as no undefended route in the code" 5 │ */ 6 │ 7 │ import type { PathFinding, EndpointClassification } from './index.js'; 8 │ 9 │ /** `UserInput --req.body--> #api --writeFileSync--> FileSystem` */
src/paths/index.ts comment 2flow 1 open 3
L17 flow ThreatModel → #parser
Reads flows, assets, mitigations and boundaries from the parsed model
12 │ * and which side it sits on follows from its degree — no inbound flow means 13 │ * data originates there, no outbound flow means data terminates there. On this 14 │ * repo that lands on UserPrompt/EnvVars/RawStdin and FileSystem/Commands/ 15 │ * TempDir without naming any of them here. 16 │ * 17 │ * @flows ThreatModel -> #parser via findUnmitigatedPaths -- "Reads flows, assets, mitigations and boundaries from the parsed model" 18 │ * @comment -- "Pure analyzer: no file I/O, no user input, no network. Operates only on an already-parsed ThreatModel, which is why it needs no @exposes of its own" 19 │ * @comment -- "Walks flow edges only. @boundary edges are undirected and used for crossing detection, and @transfers moves responsibility rather than data, so neither is a hop a path may take" 20 │ */ 21 │ 22 │ import { graphEdges } from '../mcp/subgraph.js';
L18 comment Pure analyzer: no file I/O, no user input, no network. Operates only on an already-parsed ThreatModel, which is why it needs no @exposes of its own
Pure analyzer: no file I/O, no user input, no network. Operates only on an already-parsed ThreatModel, which is why it needs no @exposes of its own
13 │ * data originates there, no outbound flow means data terminates there. On this 14 │ * repo that lands on UserPrompt/EnvVars/RawStdin and FileSystem/Commands/ 15 │ * TempDir without naming any of them here. 16 │ * 17 │ * @flows ThreatModel -> #parser via findUnmitigatedPaths -- "Reads flows, assets, mitigations and boundaries from the parsed model" 18 │ * @comment -- "Pure analyzer: no file I/O, no user input, no network. Operates only on an already-parsed ThreatModel, which is why it needs no @exposes of its own" 19 │ * @comment -- "Walks flow edges only. @boundary edges are undirected and used for crossing detection, and @transfers moves responsibility rather than data, so neither is a hop a path may take" 20 │ */ 21 │ 22 │ import { graphEdges } from '../mcp/subgraph.js'; 23 │ import { canonicaliser } from '../parser/canonical-ref.js';
L19 comment Walks flow edges only. @boundary edges are undirected and used for crossing detection, and @transfers moves responsibility rather than data, so neither is a hop a path may take
Walks flow edges only. @boundary edges are undirected and used for crossing detection, and @transfers moves responsibility rather than data, so neither is a hop a path may take
14 │ * repo that lands on UserPrompt/EnvVars/RawStdin and FileSystem/Commands/ 15 │ * TempDir without naming any of them here. 16 │ * 17 │ * @flows ThreatModel -> #parser via findUnmitigatedPaths -- "Reads flows, assets, mitigations and boundaries from the parsed model" 18 │ * @comment -- "Pure analyzer: no file I/O, no user input, no network. Operates only on an already-parsed ThreatModel, which is why it needs no @exposes of its own" 19 │ * @comment -- "Walks flow edges only. @boundary edges are undirected and used for crossing detection, and @transfers moves responsibility rather than data, so neither is a hop a path may take" 20 │ */ 21 │ 22 │ import { graphEdges } from '../mcp/subgraph.js'; 23 │ import { canonicaliser } from '../parser/canonical-ref.js'; 24 │ import type { PathHop } from '../mcp/subgraph.js';
src/playbooks/annotate.ts comment 1 open 1
L5 comment Bodies are markdown that lands verbatim in the agent prompt and in the SKILL.md; keep each one readable on its own
Bodies are markdown that lands verbatim in the agent prompt and in the SKILL.md; keep each one readable on its own
1 │ /** 2 │ * The six annotate playbooks. Each body is a complete method: purpose, phases, 3 │ * what to write, what not to, and when to stop. They share the evidence bar. 4 │ * 5 │ * @comment -- "Bodies are markdown that lands verbatim in the agent prompt and in the SKILL.md; keep each one readable on its own" 6 │ */ 7 │ import type { Playbook } from './types.js'; 8 │ import { EVIDENCE_BAR, WRITE_LAST, STOP_CONDITIONS } from './evidence.js'; 9 │ 10 │ const NEVER = `Never write \`@accepts\` or \`@entitles\`; never delete or rewrite an existing annotation; never write executable code.`;
src/playbooks/evidence.ts comment 1 open 1
L5 comment Kept in one place so the lint in src/gate mirrors it rule for rule: what the prompt demands is what the gate checks
Kept in one place so the lint in src/gate mirrors it rule for rule: what the prompt demands is what the gate checks
1 │ /** 2 │ * The evidence bar every annotate playbook shares. This is the text that makes 3 │ * "only real threats" the default rather than something a user has to ask for. 4 │ * 5 │ * @comment -- "Kept in one place so the lint in src/gate mirrors it rule for rule: what the prompt demands is what the gate checks" 6 │ */ 7 │ 8 │ export const EVIDENCE_BAR = `### The evidence bar — what an @exposes must carry 9 │ An \`@exposes\` is a claim that an attacker can reach this code with input they control and that nothing in the path stops them. Write one only when you can name all four: 10 │ 1. **Entry point** — the handler, command, consumer or listener where the input arrives (function name, route or file:line).
src/playbooks/index.ts comment 1 open 1
L4 comment Annotate playbooks compose into buildAnnotatePrompt; report shapes into buildUserMessage; skills into guardlink init
Annotate playbooks compose into buildAnnotatePrompt; report shapes into buildUserMessage; skills into guardlink init
1 │ /** 2 │ * GuardLink Playbooks — barrel. 3 │ * 4 │ * @comment -- "Annotate playbooks compose into buildAnnotatePrompt; report shapes into buildUserMessage; skills into guardlink init" 5 │ */ 6 │ export type { PlaybookKind, PlaybookId, AnnotatePlaybookId, ReportShapeId, Playbook, PlaybookSelection } from './types.js'; 7 │ export { ANNOTATE_PLAYBOOKS } from './annotate.js'; 8 │ export { REPORT_SHAPES } from './report.js'; 9 │ export { EVIDENCE_BAR, WRITE_LAST, STOP_CONDITIONS } from './evidence.js';
src/playbooks/report.ts comment 1 open 1
L6 comment 'full' is the framework's own structure and appends nothing; the other three are audience shapes
'full' is the framework's own structure and appends nothing; the other three are audience shapes
1 │ /** 2 │ * Report shapes: who the report is for changes what goes on the page, not 3 │ * what the analysis is. The framework (STRIDE, DREAD, …) stays the method; 4 │ * the shape is appended to the user message. 5 │ * 6 │ * @comment -- "'full' is the framework's own structure and appends nothing; the other three are audience shapes" 7 │ */ 8 │ import type { Playbook } from './types.js'; 9 │ 10 │ export const REPORT_SHAPES: readonly Playbook[] = [ 11 │ {
src/playbooks/select.ts exposes 1mitigates 1comment 1 open 3
L6 exposes #cli → #redos
selectAnnotatePlaybook() runs each Playbook.triggers RegExp against the prompt text the user typed
1 │ /** 2 │ * Selecting a playbook: an explicit id wins, then keyword rules in a fixed 3 │ * order, then the default. Rules, not a model, so the same words always pick 4 │ * the same method and the choice can be printed with its reason. 5 │ * 6 │ * @exposes #cli to #redos [low] cwe:CWE-1333 -- "selectAnnotatePlaybook() runs each Playbook.triggers RegExp against the prompt text the user typed" 7 │ * @mitigates #cli against #redos using #regex-anchoring -- "Every trigger is a short literal or word-boundary pattern with no nested quantifiers; the prompt is capped before matching" 8 │ * @comment -- "Order matters where prompts match several playbooks: chains beats exploitable because a prompt that asks for chains also asks for exploitability, and diff beats coverage because a change is narrower than the tree" 9 │ */ 10 │ import type { AnnotatePlaybookId, ReportShapeId, Playbook, PlaybookSelection, PlaybookId } from './types.js'; 11 │ import { ANNOTATE_PLAYBOOKS } from './annotate.js';
L7 mitigates #regex-anchoring mitigates #redos
Every trigger is a short literal or word-boundary pattern with no nested quantifiers; the prompt is capped before matching
2 │ * Selecting a playbook: an explicit id wins, then keyword rules in a fixed 3 │ * order, then the default. Rules, not a model, so the same words always pick 4 │ * the same method and the choice can be printed with its reason. 5 │ * 6 │ * @exposes #cli to #redos [low] cwe:CWE-1333 -- "selectAnnotatePlaybook() runs each Playbook.triggers RegExp against the prompt text the user typed" 7 │ * @mitigates #cli against #redos using #regex-anchoring -- "Every trigger is a short literal or word-boundary pattern with no nested quantifiers; the prompt is capped before matching" 8 │ * @comment -- "Order matters where prompts match several playbooks: chains beats exploitable because a prompt that asks for chains also asks for exploitability, and diff beats coverage because a change is narrower than the tree" 9 │ */ 10 │ import type { AnnotatePlaybookId, ReportShapeId, Playbook, PlaybookSelection, PlaybookId } from './types.js'; 11 │ import { ANNOTATE_PLAYBOOKS } from './annotate.js'; 12 │ import { REPORT_SHAPES } from './report.js';
L8 comment Order matters where prompts match several playbooks: chains beats exploitable because a prompt that asks for chains also asks for exploitability, and diff beats coverage because a change is narrower than the tree
Order matters where prompts match several playbooks: chains beats exploitable because a prompt that asks for chains also asks for exploitability, and diff beats coverage because a change is narrower than the tree
3 │ * order, then the default. Rules, not a model, so the same words always pick 4 │ * the same method and the choice can be printed with its reason. 5 │ * 6 │ * @exposes #cli to #redos [low] cwe:CWE-1333 -- "selectAnnotatePlaybook() runs each Playbook.triggers RegExp against the prompt text the user typed" 7 │ * @mitigates #cli against #redos using #regex-anchoring -- "Every trigger is a short literal or word-boundary pattern with no nested quantifiers; the prompt is capped before matching" 8 │ * @comment -- "Order matters where prompts match several playbooks: chains beats exploitable because a prompt that asks for chains also asks for exploitability, and diff beats coverage because a change is narrower than the tree" 9 │ */ 10 │ import type { AnnotatePlaybookId, ReportShapeId, Playbook, PlaybookSelection, PlaybookId } from './types.js'; 11 │ import { ANNOTATE_PLAYBOOKS } from './annotate.js'; 12 │ import { REPORT_SHAPES } from './report.js'; 13 │
src/playbooks/skills.ts comment 1 open 1
L7 comment Frontmatter is the two keys every skill loader reads (name, description); the body is the playbook verbatim plus how to invoke the CLI equivalent
Frontmatter is the two keys every skill loader reads (name, description); the body is the playbook verbatim plus how to invoke the CLI equivalent
2 │ * The same method as a skill file, so a developer's own agent session can run 3 │ * it without going through the CLI. `guardlink init` writes these under 4 │ * `.claude/skills/`; the generated marker is how `sync` tells a file it wrote 5 │ * from one a person edited. 6 │ * 7 │ * @comment -- "Frontmatter is the two keys every skill loader reads (name, description); the body is the playbook verbatim plus how to invoke the CLI equivalent" 8 │ */ 9 │ import type { Playbook } from './types.js'; 10 │ 11 │ export const SKILL_GENERATED_MARKER = '<!-- guardlink:generated -->'; 12 │
src/playbooks/types.ts comment 1 open 1
L10 comment Pure data and pure functions: nothing here reads a file, runs a process or talks to a model
Pure data and pure functions: nothing here reads a file, runs a process or talks to a model
5 │ * threats, mapping architecture, chasing chains, reporting for an executive. 6 │ * The user's prompt narrows scope and intent; the playbook supplies the 7 │ * phases, the evidence bar and the stop conditions, so a vague prompt and an 8 │ * expert prompt run the same method. 9 │ * 10 │ * @comment -- "Pure data and pure functions: nothing here reads a file, runs a process or talks to a model" 11 │ */ 12 │ 13 │ export type PlaybookKind = 'annotate' | 'report'; 14 │ 15 │ export type AnnotatePlaybookId = 'map' | 'exploitable' | 'chains' | 'diff' | 'coverage' | 'verify';
src/report/index.ts comment 2 open 2
L4 comment Report generation is pure transformation; no I/O in this module
Report generation is pure transformation; no I/O in this module
1 │ /** 2 │ * GuardLink Report — exports. 3 │ * 4 │ * @comment -- "Report generation is pure transformation; no I/O in this module" 5 │ * @comment -- "File writes handled by CLI/MCP callers" 6 │ */ 7 │ 8 │ export { generateMermaid } from './mermaid.js'; 9 │ export { generateReport } from './report.js';
L5 comment File writes handled by CLI/MCP callers
File writes handled by CLI/MCP callers
1 │ /** 2 │ * GuardLink Report — exports. 3 │ * 4 │ * @comment -- "Report generation is pure transformation; no I/O in this module" 5 │ * @comment -- "File writes handled by CLI/MCP callers" 6 │ */ 7 │ 8 │ export { generateMermaid } from './mermaid.js'; 9 │ export { generateReport } from './report.js'; 10 │ export { generateSequenceDiagram } from './sequence.js';
src/report/mermaid.ts flow 1mitigates 1comment 1 open 3
L105 flow FeatureName → #report
A @feature name written in source reaches the diagram title
100 │ // system"; four nodes titled `feature slice "Dashboard"` read as a slice. 101 │ // Mermaid frontmatter, so the title renders rather than sitting in a `%%` 102 │ // comment nobody sees. Emitted only for a filtered model, so unfiltered output 103 │ // (and its cross-process hash, D23) is untouched. 104 │ // 105 │ // @flows FeatureName -> #report via filtered_by_features -- "A @feature name written in source reaches the diagram title" 106 │ // @mitigates #report against #xss using #output-encoding -- "The name is quote-escaped before it enters the YAML frontmatter scalar, so it cannot close the string and append Mermaid directives or an init block into a diagram a viewer renders as HTML" 107 │ // @comment -- "@feature names are single-line by construction (parse-line.ts), so there is no newline to break the scalar; the quote is the only delimiter that needs escaping" 108 │ const features = filteredFeatures(model); 109 │ if (features.length > 0) { 110 │ lines.push('---');
L106 mitigates #output-encoding mitigates #xss
The name is quote-escaped before it enters the YAML frontmatter scalar, so it cannot close the string and append Mermaid directives or an init block into a diagram a viewer renders as HTML
101 │ // Mermaid frontmatter, so the title renders rather than sitting in a `%%` 102 │ // comment nobody sees. Emitted only for a filtered model, so unfiltered output 103 │ // (and its cross-process hash, D23) is untouched. 104 │ // 105 │ // @flows FeatureName -> #report via filtered_by_features -- "A @feature name written in source reaches the diagram title" 106 │ // @mitigates #report against #xss using #output-encoding -- "The name is quote-escaped before it enters the YAML frontmatter scalar, so it cannot close the string and append Mermaid directives or an init block into a diagram a viewer renders as HTML" 107 │ // @comment -- "@feature names are single-line by construction (parse-line.ts), so there is no newline to break the scalar; the quote is the only delimiter that needs escaping" 108 │ const features = filteredFeatures(model); 109 │ if (features.length > 0) { 110 │ lines.push('---'); 111 │ lines.push(`title: "Feature slice — ${features.map(f => f.replace(/"/g, "'")).join(', ')} (not the whole project)"`);
L107 comment @feature names are single-line by construction (parse-line.ts), so there is no newline to break the scalar; the quote is the only delimiter that needs escaping
@feature names are single-line by construction (parse-line.ts), so there is no newline to break the scalar; the quote is the only delimiter that needs escaping
102 │ // comment nobody sees. Emitted only for a filtered model, so unfiltered output 103 │ // (and its cross-process hash, D23) is untouched. 104 │ // 105 │ // @flows FeatureName -> #report via filtered_by_features -- "A @feature name written in source reaches the diagram title" 106 │ // @mitigates #report against #xss using #output-encoding -- "The name is quote-escaped before it enters the YAML frontmatter scalar, so it cannot close the string and append Mermaid directives or an init block into a diagram a viewer renders as HTML" 107 │ // @comment -- "@feature names are single-line by construction (parse-line.ts), so there is no newline to break the scalar; the quote is the only delimiter that needs escaping" 108 │ const features = filteredFeatures(model); 109 │ if (features.length > 0) { 110 │ lines.push('---'); 111 │ lines.push(`title: "Feature slice — ${features.map(f => f.replace(/"/g, "'")).join(', ')} (not the whole project)"`); 112 │ lines.push('---');
src/report/report.ts comment 5flow 3handles 1 open 9
L6 comment Pure function: transforms ThreatModel to markdown string
Pure function: transforms ThreatModel to markdown string
1 │ /** 2 │ * GuardLink Report — Markdown report generator. 3 │ * Produces a human-readable threat model report with 4 │ * embedded Mermaid diagram, finding tables, and coverage stats. 5 │ * 6 │ * @comment -- "Pure function: transforms ThreatModel to markdown string" 7 │ * @comment -- "No file I/O; caller (CLI/MCP) handles write" 8 │ * @comment -- "A model narrowed by --feature carries filtered_by_features, and every heading, caption and empty-state sentence here is scoped from it: a report that reads as project-wide while describing one feature is a wrong answer about what is and is not covered, not a cosmetic problem" 9 │ * @flows FeatureName -> #report via filtered_by_features -- "@feature names reach the title, every heading and the footer" 10 │ * @comment -- "Feature names are model data and land in markdown text, never in HTML or a shell — the markdown consumer escapes; the two Mermaid titles that do need escaping carry their own @mitigates" 11 │ * @flows ThreatModel -> #report via generateReport -- "Model input"
L7 comment No file I/O; caller (CLI/MCP) handles write
No file I/O; caller (CLI/MCP) handles write
2 │ * GuardLink Report — Markdown report generator. 3 │ * Produces a human-readable threat model report with 4 │ * embedded Mermaid diagram, finding tables, and coverage stats. 5 │ * 6 │ * @comment -- "Pure function: transforms ThreatModel to markdown string" 7 │ * @comment -- "No file I/O; caller (CLI/MCP) handles write" 8 │ * @comment -- "A model narrowed by --feature carries filtered_by_features, and every heading, caption and empty-state sentence here is scoped from it: a report that reads as project-wide while describing one feature is a wrong answer about what is and is not covered, not a cosmetic problem" 9 │ * @flows FeatureName -> #report via filtered_by_features -- "@feature names reach the title, every heading and the footer" 10 │ * @comment -- "Feature names are model data and land in markdown text, never in HTML or a shell — the markdown consumer escapes; the two Mermaid titles that do need escaping carry their own @mitigates" 11 │ * @flows ThreatModel -> #report via generateReport -- "Model input" 12 │ * @flows #report -> Markdown via return -- "Report output"
L8 comment A model narrowed by --feature carries filtered_by_features, and every heading, caption and empty-state sentence here is scoped from it: a report that reads as project-wide while describing one feature is a wrong answer about what is and is not covered, not a cosmetic problem
A model narrowed by --feature carries filtered_by_features, and every heading, caption and empty-state sentence here is scoped from it: a report that reads as project-wide while describing one feature is a wrong answer about what is and is not covered, not a cosmetic problem
3 │ * Produces a human-readable threat model report with 4 │ * embedded Mermaid diagram, finding tables, and coverage stats. 5 │ * 6 │ * @comment -- "Pure function: transforms ThreatModel to markdown string" 7 │ * @comment -- "No file I/O; caller (CLI/MCP) handles write" 8 │ * @comment -- "A model narrowed by --feature carries filtered_by_features, and every heading, caption and empty-state sentence here is scoped from it: a report that reads as project-wide while describing one feature is a wrong answer about what is and is not covered, not a cosmetic problem" 9 │ * @flows FeatureName -> #report via filtered_by_features -- "@feature names reach the title, every heading and the footer" 10 │ * @comment -- "Feature names are model data and land in markdown text, never in HTML or a shell — the markdown consumer escapes; the two Mermaid titles that do need escaping carry their own @mitigates" 11 │ * @flows ThreatModel -> #report via generateReport -- "Model input" 12 │ * @flows #report -> Markdown via return -- "Report output" 13 │ */
L9 flow FeatureName → #report
@feature names reach the title, every heading and the footer
4 │ * embedded Mermaid diagram, finding tables, and coverage stats. 5 │ * 6 │ * @comment -- "Pure function: transforms ThreatModel to markdown string" 7 │ * @comment -- "No file I/O; caller (CLI/MCP) handles write" 8 │ * @comment -- "A model narrowed by --feature carries filtered_by_features, and every heading, caption and empty-state sentence here is scoped from it: a report that reads as project-wide while describing one feature is a wrong answer about what is and is not covered, not a cosmetic problem" 9 │ * @flows FeatureName -> #report via filtered_by_features -- "@feature names reach the title, every heading and the footer" 10 │ * @comment -- "Feature names are model data and land in markdown text, never in HTML or a shell — the markdown consumer escapes; the two Mermaid titles that do need escaping carry their own @mitigates" 11 │ * @flows ThreatModel -> #report via generateReport -- "Model input" 12 │ * @flows #report -> Markdown via return -- "Report output" 13 │ */ 14 │
L10 comment Feature names are model data and land in markdown text, never in HTML or a shell — the markdown consumer escapes; the two Mermaid titles that do need escaping carry their own @mitigates
Feature names are model data and land in markdown text, never in HTML or a shell — the markdown consumer escapes; the two Mermaid titles that do need escaping carry their own @mitigates
5 │ * 6 │ * @comment -- "Pure function: transforms ThreatModel to markdown string" 7 │ * @comment -- "No file I/O; caller (CLI/MCP) handles write" 8 │ * @comment -- "A model narrowed by --feature carries filtered_by_features, and every heading, caption and empty-state sentence here is scoped from it: a report that reads as project-wide while describing one feature is a wrong answer about what is and is not covered, not a cosmetic problem" 9 │ * @flows FeatureName -> #report via filtered_by_features -- "@feature names reach the title, every heading and the footer" 10 │ * @comment -- "Feature names are model data and land in markdown text, never in HTML or a shell — the markdown consumer escapes; the two Mermaid titles that do need escaping carry their own @mitigates" 11 │ * @flows ThreatModel -> #report via generateReport -- "Model input" 12 │ * @flows #report -> Markdown via return -- "Report output" 13 │ */ 14 │ 15 │ import type { ThreatModel, ThreatModelExposure, ThreatModelEntitlement, Severity } from '../types/index.js';
L11 flow ThreatModel → #report
Model input
6 │ * @comment -- "Pure function: transforms ThreatModel to markdown string" 7 │ * @comment -- "No file I/O; caller (CLI/MCP) handles write" 8 │ * @comment -- "A model narrowed by --feature carries filtered_by_features, and every heading, caption and empty-state sentence here is scoped from it: a report that reads as project-wide while describing one feature is a wrong answer about what is and is not covered, not a cosmetic problem" 9 │ * @flows FeatureName -> #report via filtered_by_features -- "@feature names reach the title, every heading and the footer" 10 │ * @comment -- "Feature names are model data and land in markdown text, never in HTML or a shell — the markdown consumer escapes; the two Mermaid titles that do need escaping carry their own @mitigates" 11 │ * @flows ThreatModel -> #report via generateReport -- "Model input" 12 │ * @flows #report -> Markdown via return -- "Report output" 13 │ */ 14 │ 15 │ import type { ThreatModel, ThreatModelExposure, ThreatModelEntitlement, Severity } from '../types/index.js'; 16 │ import { generateMermaid } from './mermaid.js';
L12 flow #report → Markdown
Report output
7 │ * @comment -- "No file I/O; caller (CLI/MCP) handles write" 8 │ * @comment -- "A model narrowed by --feature carries filtered_by_features, and every heading, caption and empty-state sentence here is scoped from it: a report that reads as project-wide while describing one feature is a wrong answer about what is and is not covered, not a cosmetic problem" 9 │ * @flows FeatureName -> #report via filtered_by_features -- "@feature names reach the title, every heading and the footer" 10 │ * @comment -- "Feature names are model data and land in markdown text, never in HTML or a shell — the markdown consumer escapes; the two Mermaid titles that do need escaping carry their own @mitigates" 11 │ * @flows ThreatModel -> #report via generateReport -- "Model input" 12 │ * @flows #report -> Markdown via return -- "Report output" 13 │ */ 14 │ 15 │ import type { ThreatModel, ThreatModelExposure, ThreatModelEntitlement, Severity } from '../types/index.js'; 16 │ import { generateMermaid } from './mermaid.js'; 17 │ import { generateSequenceDiagram } from './sequence.js';
L1514 handles #report: pii
Author identities from git, in the configured identity mode
1509 │ * who declared its fix, and which AI tool co-authored those commits. Rendered1510 │ * only when records carry `blame` (`guardlink report --blame`); otherwise the1511 │ * report is byte-for-byte what it was. Identities arrive already in the1512 │ * configured mode (name, email or hash), so nothing here decides what to show.1513 │ *1514 │ * @handles pii on #report -- "Author identities from git, in the configured identity mode"1515 │ * @comment -- "AI credit is what a commit declared in a trailer, never detected from code; a claim whose commits carry no trailer stays human. Every degraded status is listed so the reader never mistakes an unattributed claim for a clean one"1516 │ */1517 │ function renderAttribution(model: ThreatModel, slice: Slice): string[] {1518 │ const entries = entriesFromModel(model);1519 │ if (entries.length === 0) return [];
L1515 comment AI credit is what a commit declared in a trailer, never detected from code; a claim whose commits carry no trailer stays human. Every degraded status is listed so the reader never mistakes an unattributed claim for a clean one
AI credit is what a commit declared in a trailer, never detected from code; a claim whose commits carry no trailer stays human. Every degraded status is listed so the reader never mistakes an unattributed claim for a clean one
1510 │ * only when records carry `blame` (`guardlink report --blame`); otherwise the1511 │ * report is byte-for-byte what it was. Identities arrive already in the1512 │ * configured mode (name, email or hash), so nothing here decides what to show.1513 │ *1514 │ * @handles pii on #report -- "Author identities from git, in the configured identity mode"1515 │ * @comment -- "AI credit is what a commit declared in a trailer, never detected from code; a claim whose commits carry no trailer stays human. Every degraded status is listed so the reader never mistakes an unattributed claim for a clean one"1516 │ */1517 │ function renderAttribution(model: ThreatModel, slice: Slice): string[] {1518 │ const entries = entriesFromModel(model);1519 │ if (entries.length === 0) return [];1520 │ const summary = summarise(entries);
src/report/sequence.ts comment 2flow 2mitigates 1 open 5
L6 comment Pure function: transforms ThreatModel flows to Mermaid sequence diagram
Pure function: transforms ThreatModel flows to Mermaid sequence diagram
1 │ /** 2 │ * GuardLink Report — Mermaid sequence diagram generator. 3 │ * Builds a sequence diagram from @flows annotations showing 4 │ * the step-by-step interactions between system participants. 5 │ * 6 │ * @comment -- "Pure function: transforms ThreatModel flows to Mermaid sequence diagram" 7 │ * @comment -- "Titles itself from filtered_by_features when the model is a --feature slice: a sequence of three participants reads as the system's interactions unless the picture says which slice it is" 8 │ * @flows ThreatModel -> #report via generateSequenceDiagram -- "Sequence diagram generation" 9 │ */ 10 │ 11 │ import type { ThreatModel } from '../types/index.js';
L7 comment Titles itself from filtered_by_features when the model is a --feature slice: a sequence of three participants reads as the system's interactions unless the picture says which slice it is
Titles itself from filtered_by_features when the model is a --feature slice: a sequence of three participants reads as the system's interactions unless the picture says which slice it is
2 │ * GuardLink Report — Mermaid sequence diagram generator. 3 │ * Builds a sequence diagram from @flows annotations showing 4 │ * the step-by-step interactions between system participants. 5 │ * 6 │ * @comment -- "Pure function: transforms ThreatModel flows to Mermaid sequence diagram" 7 │ * @comment -- "Titles itself from filtered_by_features when the model is a --feature slice: a sequence of three participants reads as the system's interactions unless the picture says which slice it is" 8 │ * @flows ThreatModel -> #report via generateSequenceDiagram -- "Sequence diagram generation" 9 │ */ 10 │ 11 │ import type { ThreatModel } from '../types/index.js'; 12 │
L8 flow ThreatModel → #report
Sequence diagram generation
3 │ * Builds a sequence diagram from @flows annotations showing 4 │ * the step-by-step interactions between system participants. 5 │ * 6 │ * @comment -- "Pure function: transforms ThreatModel flows to Mermaid sequence diagram" 7 │ * @comment -- "Titles itself from filtered_by_features when the model is a --feature slice: a sequence of three participants reads as the system's interactions unless the picture says which slice it is" 8 │ * @flows ThreatModel -> #report via generateSequenceDiagram -- "Sequence diagram generation" 9 │ */ 10 │ 11 │ import type { ThreatModel } from '../types/index.js'; 12 │ 13 │ /**
L48 flow FeatureName → #report
A @feature name written in source reaches the sequence diagram title
43 │ } 44 │ 45 │ export function generateSequenceDiagram(model: ThreatModel): string { 46 │ const lines: string[] = []; 47 │ 48 │ // @flows FeatureName -> #report via filtered_by_features -- "A @feature name written in source reaches the sequence diagram title" 49 │ // @mitigates #report against #xss using #output-encoding -- "Quote-escaped before entering the YAML frontmatter scalar, and the `:` that would terminate a Note label is stripped below — a feature name is model data, not markup" 50 │ const features = filteredFeatures(model); 51 │ if (features.length > 0) { 52 │ lines.push('---'); 53 │ lines.push(`title: "Feature slice — ${features.map(f => f.replace(/"/g, "'")).join(', ')} (not the whole project)"`);
L49 mitigates #output-encoding mitigates #xss
Quote-escaped before entering the YAML frontmatter scalar, and the `:` that would terminate a Note label is stripped below — a feature name is model data, not markup
44 │ 45 │ export function generateSequenceDiagram(model: ThreatModel): string { 46 │ const lines: string[] = []; 47 │ 48 │ // @flows FeatureName -> #report via filtered_by_features -- "A @feature name written in source reaches the sequence diagram title" 49 │ // @mitigates #report against #xss using #output-encoding -- "Quote-escaped before entering the YAML frontmatter scalar, and the `:` that would terminate a Note label is stripped below — a feature name is model data, not markup" 50 │ const features = filteredFeatures(model); 51 │ if (features.length > 0) { 52 │ lines.push('---'); 53 │ lines.push(`title: "Feature slice — ${features.map(f => f.replace(/"/g, "'")).join(', ')} (not the whole project)"`); 54 │ lines.push('---');
src/review/entitlements.ts mitigates 5flow 5exposes 3comment 2 open 17
L27 exposes #cli → #arbitrary-write
Accepting a proposal writes an @entitles line into a source file named by the proposal
22 │ * validation error, so writing one directly is visible rather than free. 23 │ * 24 │ * The ledger is the audit trail: a decision is appended, never a record deleted, 25 │ * so a rejection stays readable next to the claim it refused. 26 │ * 27 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "Accepting a proposal writes an @entitles line into a source file named by the proposal" 28 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Target must resolve inside the project root and name an existing file with the anchor line in range" 29 │ * @exposes #cli to #insecure-deser [medium] cwe:CWE-502 -- "JSON.parse of .guardlink/entitlement-proposals.json, which an agent or another repo may have written" 30 │ * @mitigates #cli against #insecure-deser using #config-validation -- "parseLedger validates shape field by field and drops nothing silently — a malformed ledger is an error, not a partial read; `inert` is recomputed from the citation rather than trusted from the file" 31 │ * @mitigates #cli against #insecure-deser using #resource-limits -- "Ledger reads are capped at MAX_LEDGER_BYTES" 32 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-74 -- "Agent-supplied rationale and human decision notes are interpolated into annotation text, where a newline would forge a second annotation"
L28 mitigates #path-validation mitigates #arbitrary-write
Target must resolve inside the project root and name an existing file with the anchor line in range
23 │ * 24 │ * The ledger is the audit trail: a decision is appended, never a record deleted, 25 │ * so a rejection stays readable next to the claim it refused. 26 │ * 27 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "Accepting a proposal writes an @entitles line into a source file named by the proposal" 28 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Target must resolve inside the project root and name an existing file with the anchor line in range" 29 │ * @exposes #cli to #insecure-deser [medium] cwe:CWE-502 -- "JSON.parse of .guardlink/entitlement-proposals.json, which an agent or another repo may have written" 30 │ * @mitigates #cli against #insecure-deser using #config-validation -- "parseLedger validates shape field by field and drops nothing silently — a malformed ledger is an error, not a partial read; `inert` is recomputed from the citation rather than trusted from the file" 31 │ * @mitigates #cli against #insecure-deser using #resource-limits -- "Ledger reads are capped at MAX_LEDGER_BYTES" 32 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-74 -- "Agent-supplied rationale and human decision notes are interpolated into annotation text, where a newline would forge a second annotation" 33 │ * @mitigates #cli against #arbitrary-write using #input-sanitize -- "oneLine() collapses newlines/CR/tabs before escapeDesc, and every built line is re-parsed with parseLine before it is written"
L29 exposes #cli → #insecure-deser
JSON.parse of .guardlink/entitlement-proposals.json, which an agent or another repo may have written
24 │ * The ledger is the audit trail: a decision is appended, never a record deleted, 25 │ * so a rejection stays readable next to the claim it refused. 26 │ * 27 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "Accepting a proposal writes an @entitles line into a source file named by the proposal" 28 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Target must resolve inside the project root and name an existing file with the anchor line in range" 29 │ * @exposes #cli to #insecure-deser [medium] cwe:CWE-502 -- "JSON.parse of .guardlink/entitlement-proposals.json, which an agent or another repo may have written" 30 │ * @mitigates #cli against #insecure-deser using #config-validation -- "parseLedger validates shape field by field and drops nothing silently — a malformed ledger is an error, not a partial read; `inert` is recomputed from the citation rather than trusted from the file" 31 │ * @mitigates #cli against #insecure-deser using #resource-limits -- "Ledger reads are capped at MAX_LEDGER_BYTES" 32 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-74 -- "Agent-supplied rationale and human decision notes are interpolated into annotation text, where a newline would forge a second annotation" 33 │ * @mitigates #cli against #arbitrary-write using #input-sanitize -- "oneLine() collapses newlines/CR/tabs before escapeDesc, and every built line is re-parsed with parseLine before it is written" 34 │ * @flows AgentProposal -> #cli via proposeEntitlement -- "Agent-side proposal input"
L30 mitigates #config-validation mitigates #insecure-deser
parseLedger validates shape field by field and drops nothing silently — a malformed ledger is an error, not a partial read; `inert` is recomputed from the citation rather than trusted from the file
25 │ * so a rejection stays readable next to the claim it refused. 26 │ * 27 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "Accepting a proposal writes an @entitles line into a source file named by the proposal" 28 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Target must resolve inside the project root and name an existing file with the anchor line in range" 29 │ * @exposes #cli to #insecure-deser [medium] cwe:CWE-502 -- "JSON.parse of .guardlink/entitlement-proposals.json, which an agent or another repo may have written" 30 │ * @mitigates #cli against #insecure-deser using #config-validation -- "parseLedger validates shape field by field and drops nothing silently — a malformed ledger is an error, not a partial read; `inert` is recomputed from the citation rather than trusted from the file" 31 │ * @mitigates #cli against #insecure-deser using #resource-limits -- "Ledger reads are capped at MAX_LEDGER_BYTES" 32 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-74 -- "Agent-supplied rationale and human decision notes are interpolated into annotation text, where a newline would forge a second annotation" 33 │ * @mitigates #cli against #arbitrary-write using #input-sanitize -- "oneLine() collapses newlines/CR/tabs before escapeDesc, and every built line is re-parsed with parseLine before it is written" 34 │ * @flows AgentProposal -> #cli via proposeEntitlement -- "Agent-side proposal input" 35 │ * @flows #cli -> ProposalLedger via writeFile -- "Proposal artifact output"
L31 mitigates #resource-limits mitigates #insecure-deser
Ledger reads are capped at MAX_LEDGER_BYTES
26 │ * 27 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "Accepting a proposal writes an @entitles line into a source file named by the proposal" 28 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Target must resolve inside the project root and name an existing file with the anchor line in range" 29 │ * @exposes #cli to #insecure-deser [medium] cwe:CWE-502 -- "JSON.parse of .guardlink/entitlement-proposals.json, which an agent or another repo may have written" 30 │ * @mitigates #cli against #insecure-deser using #config-validation -- "parseLedger validates shape field by field and drops nothing silently — a malformed ledger is an error, not a partial read; `inert` is recomputed from the citation rather than trusted from the file" 31 │ * @mitigates #cli against #insecure-deser using #resource-limits -- "Ledger reads are capped at MAX_LEDGER_BYTES" 32 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-74 -- "Agent-supplied rationale and human decision notes are interpolated into annotation text, where a newline would forge a second annotation" 33 │ * @mitigates #cli against #arbitrary-write using #input-sanitize -- "oneLine() collapses newlines/CR/tabs before escapeDesc, and every built line is re-parsed with parseLine before it is written" 34 │ * @flows AgentProposal -> #cli via proposeEntitlement -- "Agent-side proposal input" 35 │ * @flows #cli -> ProposalLedger via writeFile -- "Proposal artifact output" 36 │ * @flows ProposalLedger -> #cli via readFile -- "Proposal artifact input"
L32 exposes #cli → #arbitrary-write
Agent-supplied rationale and human decision notes are interpolated into annotation text, where a newline would forge a second annotation
27 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "Accepting a proposal writes an @entitles line into a source file named by the proposal" 28 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Target must resolve inside the project root and name an existing file with the anchor line in range" 29 │ * @exposes #cli to #insecure-deser [medium] cwe:CWE-502 -- "JSON.parse of .guardlink/entitlement-proposals.json, which an agent or another repo may have written" 30 │ * @mitigates #cli against #insecure-deser using #config-validation -- "parseLedger validates shape field by field and drops nothing silently — a malformed ledger is an error, not a partial read; `inert` is recomputed from the citation rather than trusted from the file" 31 │ * @mitigates #cli against #insecure-deser using #resource-limits -- "Ledger reads are capped at MAX_LEDGER_BYTES" 32 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-74 -- "Agent-supplied rationale and human decision notes are interpolated into annotation text, where a newline would forge a second annotation" 33 │ * @mitigates #cli against #arbitrary-write using #input-sanitize -- "oneLine() collapses newlines/CR/tabs before escapeDesc, and every built line is re-parsed with parseLine before it is written" 34 │ * @flows AgentProposal -> #cli via proposeEntitlement -- "Agent-side proposal input" 35 │ * @flows #cli -> ProposalLedger via writeFile -- "Proposal artifact output" 36 │ * @flows ProposalLedger -> #cli via readFile -- "Proposal artifact input" 37 │ * @flows #cli -> SourceFiles via applyProposalDecision -- "Accepted entitlement lands as an @entitles annotation"
L33 mitigates #input-sanitize mitigates #arbitrary-write
oneLine() collapses newlines/CR/tabs before escapeDesc, and every built line is re-parsed with parseLine before it is written
28 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Target must resolve inside the project root and name an existing file with the anchor line in range" 29 │ * @exposes #cli to #insecure-deser [medium] cwe:CWE-502 -- "JSON.parse of .guardlink/entitlement-proposals.json, which an agent or another repo may have written" 30 │ * @mitigates #cli against #insecure-deser using #config-validation -- "parseLedger validates shape field by field and drops nothing silently — a malformed ledger is an error, not a partial read; `inert` is recomputed from the citation rather than trusted from the file" 31 │ * @mitigates #cli against #insecure-deser using #resource-limits -- "Ledger reads are capped at MAX_LEDGER_BYTES" 32 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-74 -- "Agent-supplied rationale and human decision notes are interpolated into annotation text, where a newline would forge a second annotation" 33 │ * @mitigates #cli against #arbitrary-write using #input-sanitize -- "oneLine() collapses newlines/CR/tabs before escapeDesc, and every built line is re-parsed with parseLine before it is written" 34 │ * @flows AgentProposal -> #cli via proposeEntitlement -- "Agent-side proposal input" 35 │ * @flows #cli -> ProposalLedger via writeFile -- "Proposal artifact output" 36 │ * @flows ProposalLedger -> #cli via readFile -- "Proposal artifact input" 37 │ * @flows #cli -> SourceFiles via applyProposalDecision -- "Accepted entitlement lands as an @entitles annotation" 38 │ * @handles internal on #cli -- "Processes actor/capability claims, authz citations, and the name of the accepting human"
L34 flow AgentProposal → #cli
Agent-side proposal input
29 │ * @exposes #cli to #insecure-deser [medium] cwe:CWE-502 -- "JSON.parse of .guardlink/entitlement-proposals.json, which an agent or another repo may have written" 30 │ * @mitigates #cli against #insecure-deser using #config-validation -- "parseLedger validates shape field by field and drops nothing silently — a malformed ledger is an error, not a partial read; `inert` is recomputed from the citation rather than trusted from the file" 31 │ * @mitigates #cli against #insecure-deser using #resource-limits -- "Ledger reads are capped at MAX_LEDGER_BYTES" 32 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-74 -- "Agent-supplied rationale and human decision notes are interpolated into annotation text, where a newline would forge a second annotation" 33 │ * @mitigates #cli against #arbitrary-write using #input-sanitize -- "oneLine() collapses newlines/CR/tabs before escapeDesc, and every built line is re-parsed with parseLine before it is written" 34 │ * @flows AgentProposal -> #cli via proposeEntitlement -- "Agent-side proposal input" 35 │ * @flows #cli -> ProposalLedger via writeFile -- "Proposal artifact output" 36 │ * @flows ProposalLedger -> #cli via readFile -- "Proposal artifact input" 37 │ * @flows #cli -> SourceFiles via applyProposalDecision -- "Accepted entitlement lands as an @entitles annotation" 38 │ * @handles internal on #cli -- "Processes actor/capability claims, authz citations, and the name of the accepting human" 39 │ * @audit #cli -- "Every acceptance records who accepted, when, and where the annotation landed; an inert or ownership-class acceptance records the acknowledgement that was required to make it"
L35 flow #cli → ProposalLedger
Proposal artifact output
30 │ * @mitigates #cli against #insecure-deser using #config-validation -- "parseLedger validates shape field by field and drops nothing silently — a malformed ledger is an error, not a partial read; `inert` is recomputed from the citation rather than trusted from the file" 31 │ * @mitigates #cli against #insecure-deser using #resource-limits -- "Ledger reads are capped at MAX_LEDGER_BYTES" 32 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-74 -- "Agent-supplied rationale and human decision notes are interpolated into annotation text, where a newline would forge a second annotation" 33 │ * @mitigates #cli against #arbitrary-write using #input-sanitize -- "oneLine() collapses newlines/CR/tabs before escapeDesc, and every built line is re-parsed with parseLine before it is written" 34 │ * @flows AgentProposal -> #cli via proposeEntitlement -- "Agent-side proposal input" 35 │ * @flows #cli -> ProposalLedger via writeFile -- "Proposal artifact output" 36 │ * @flows ProposalLedger -> #cli via readFile -- "Proposal artifact input" 37 │ * @flows #cli -> SourceFiles via applyProposalDecision -- "Accepted entitlement lands as an @entitles annotation" 38 │ * @handles internal on #cli -- "Processes actor/capability claims, authz citations, and the name of the accepting human" 39 │ * @audit #cli -- "Every acceptance records who accepted, when, and where the annotation landed; an inert or ownership-class acceptance records the acknowledgement that was required to make it" 40 │ * @comment -- "There is no MCP accept tool and no agent-callable accept path: the only writer of an accepted entitlement is a human at `guardlink entitle`, which is why the CLI is the surface and this module refuses a decision with no name on it"
L36 flow ProposalLedger → #cli
Proposal artifact input
31 │ * @mitigates #cli against #insecure-deser using #resource-limits -- "Ledger reads are capped at MAX_LEDGER_BYTES" 32 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-74 -- "Agent-supplied rationale and human decision notes are interpolated into annotation text, where a newline would forge a second annotation" 33 │ * @mitigates #cli against #arbitrary-write using #input-sanitize -- "oneLine() collapses newlines/CR/tabs before escapeDesc, and every built line is re-parsed with parseLine before it is written" 34 │ * @flows AgentProposal -> #cli via proposeEntitlement -- "Agent-side proposal input" 35 │ * @flows #cli -> ProposalLedger via writeFile -- "Proposal artifact output" 36 │ * @flows ProposalLedger -> #cli via readFile -- "Proposal artifact input" 37 │ * @flows #cli -> SourceFiles via applyProposalDecision -- "Accepted entitlement lands as an @entitles annotation" 38 │ * @handles internal on #cli -- "Processes actor/capability claims, authz citations, and the name of the accepting human" 39 │ * @audit #cli -- "Every acceptance records who accepted, when, and where the annotation landed; an inert or ownership-class acceptance records the acknowledgement that was required to make it" 40 │ * @comment -- "There is no MCP accept tool and no agent-callable accept path: the only writer of an accepted entitlement is a human at `guardlink entitle`, which is why the CLI is the surface and this module refuses a decision with no name on it" 41 │ */
L37 flow #cli → SourceFiles
Accepted entitlement lands as an @entitles annotation
32 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-74 -- "Agent-supplied rationale and human decision notes are interpolated into annotation text, where a newline would forge a second annotation" 33 │ * @mitigates #cli against #arbitrary-write using #input-sanitize -- "oneLine() collapses newlines/CR/tabs before escapeDesc, and every built line is re-parsed with parseLine before it is written" 34 │ * @flows AgentProposal -> #cli via proposeEntitlement -- "Agent-side proposal input" 35 │ * @flows #cli -> ProposalLedger via writeFile -- "Proposal artifact output" 36 │ * @flows ProposalLedger -> #cli via readFile -- "Proposal artifact input" 37 │ * @flows #cli -> SourceFiles via applyProposalDecision -- "Accepted entitlement lands as an @entitles annotation" 38 │ * @handles internal on #cli -- "Processes actor/capability claims, authz citations, and the name of the accepting human" 39 │ * @audit #cli -- "Every acceptance records who accepted, when, and where the annotation landed; an inert or ownership-class acceptance records the acknowledgement that was required to make it" 40 │ * @comment -- "There is no MCP accept tool and no agent-callable accept path: the only writer of an accepted entitlement is a human at `guardlink entitle`, which is why the CLI is the surface and this module refuses a decision with no name on it" 41 │ */ 42 │
L38 handles #cli: internal
Processes actor/capability claims, authz citations, and the name of the accepting human
33 │ * @mitigates #cli against #arbitrary-write using #input-sanitize -- "oneLine() collapses newlines/CR/tabs before escapeDesc, and every built line is re-parsed with parseLine before it is written" 34 │ * @flows AgentProposal -> #cli via proposeEntitlement -- "Agent-side proposal input" 35 │ * @flows #cli -> ProposalLedger via writeFile -- "Proposal artifact output" 36 │ * @flows ProposalLedger -> #cli via readFile -- "Proposal artifact input" 37 │ * @flows #cli -> SourceFiles via applyProposalDecision -- "Accepted entitlement lands as an @entitles annotation" 38 │ * @handles internal on #cli -- "Processes actor/capability claims, authz citations, and the name of the accepting human" 39 │ * @audit #cli -- "Every acceptance records who accepted, when, and where the annotation landed; an inert or ownership-class acceptance records the acknowledgement that was required to make it" 40 │ * @comment -- "There is no MCP accept tool and no agent-callable accept path: the only writer of an accepted entitlement is a human at `guardlink entitle`, which is why the CLI is the surface and this module refuses a decision with no name on it" 41 │ */ 42 │ 43 │ import { execFileSync } from 'node:child_process';
L39 audit Audit: #cli
Every acceptance records who accepted, when, and where the annotation landed; an inert or ownership-class acceptance records the acknowledgement that was required to make it
34 │ * @flows AgentProposal -> #cli via proposeEntitlement -- "Agent-side proposal input" 35 │ * @flows #cli -> ProposalLedger via writeFile -- "Proposal artifact output" 36 │ * @flows ProposalLedger -> #cli via readFile -- "Proposal artifact input" 37 │ * @flows #cli -> SourceFiles via applyProposalDecision -- "Accepted entitlement lands as an @entitles annotation" 38 │ * @handles internal on #cli -- "Processes actor/capability claims, authz citations, and the name of the accepting human" 39 │ * @audit #cli -- "Every acceptance records who accepted, when, and where the annotation landed; an inert or ownership-class acceptance records the acknowledgement that was required to make it" 40 │ * @comment -- "There is no MCP accept tool and no agent-callable accept path: the only writer of an accepted entitlement is a human at `guardlink entitle`, which is why the CLI is the surface and this module refuses a decision with no name on it" 41 │ */ 42 │ 43 │ import { execFileSync } from 'node:child_process'; 44 │ import { mkdir, readFile, stat, writeFile } from 'node:fs/promises';
L40 comment There is no MCP accept tool and no agent-callable accept path: the only writer of an accepted entitlement is a human at `guardlink entitle`, which is why the CLI is the surface and this module refuses a decision with no name on it
There is no MCP accept tool and no agent-callable accept path: the only writer of an accepted entitlement is a human at `guardlink entitle`, which is why the CLI is the surface and this module refuses a decision with no name on it
35 │ * @flows #cli -> ProposalLedger via writeFile -- "Proposal artifact output" 36 │ * @flows ProposalLedger -> #cli via readFile -- "Proposal artifact input" 37 │ * @flows #cli -> SourceFiles via applyProposalDecision -- "Accepted entitlement lands as an @entitles annotation" 38 │ * @handles internal on #cli -- "Processes actor/capability claims, authz citations, and the name of the accepting human" 39 │ * @audit #cli -- "Every acceptance records who accepted, when, and where the annotation landed; an inert or ownership-class acceptance records the acknowledgement that was required to make it" 40 │ * @comment -- "There is no MCP accept tool and no agent-callable accept path: the only writer of an accepted entitlement is a human at `guardlink entitle`, which is why the CLI is the surface and this module refuses a decision with no name on it" 41 │ */ 42 │ 43 │ import { execFileSync } from 'node:child_process'; 44 │ import { mkdir, readFile, stat, writeFile } from 'node:fs/promises'; 45 │ import { dirname, join, resolve, sep } from 'node:path';
L502 comment Writes `against <threat>` as well as `on <asset>`, because the join is the triple (design §9.3): a claim missing either clause matches no finding and demotes nothing. Omitting the threat here made acceptance produce a DEAD annotation — the proposal ledger carried it, the written line did not, so the only sanctioned way to create an entitlement produced one that could never work. Observed on a real repository before this fix, not hypothesised.
Writes `against <threat>` as well as `on <asset>`, because the join is the triple (design §9.3): a claim missing either clause matches no finding and demotes nothing. Omitting the threat here made acceptance produce a DEAD annotation — the proposal ledger carried it, the written line did not, so the only sanctioned way to create an entitlement produced one that could never work. Observed on a real repository before this fix, not hypothesised.
497 │ * 498 │ * Validating with the real parser rather than a second regex is the point: if 499 │ * `guardlink parse` would not read the line back as an entitlement, the claim 500 │ * would be silently absent from the model — the failure this design is about. 501 │ */ 502 │ // @comment -- "Writes `against <threat>` as well as `on <asset>`, because the join is the triple (design §9.3): a claim missing either clause matches no finding and demotes nothing. Omitting the threat here made acceptance produce a DEAD annotation — the proposal ledger carried it, the written line did not, so the only sanctioned way to create an entitlement produced one that could never work. Observed on a real repository before this fix, not hypothesised." 503 │ export function buildEntitlesBody(p: { 504 │ actor: string; capability: string; asset?: string; threat?: string; rationale: string; 505 │ }): string { 506 │ const asset = p.asset ? ` on ${p.asset}` : ''; 507 │ const threat = p.threat ? ` against ${p.threat}` : '';
L852 mitigates #param-commands mitigates #cmd-injection
execFileSync with a fixed argv and no shell; nothing user-supplied reaches the command
847 │ * Resolve the name a decision is recorded under: an explicit name wins, then 848 │ * git's configured identity, then $USER. Returns undefined when nothing 849 │ * identifies a person — better to ask than to record "unknown" as the 850 │ * maintainer who granted a privilege. 851 │ * 852 │ * @mitigates #cli against #cmd-injection using #param-commands -- "execFileSync with a fixed argv and no shell; nothing user-supplied reaches the command" 853 │ * @flows GitConfig -> #cli via execFileSync -- "Reads the local git identity to attribute a decision" 854 │ */ 855 │ export function defaultDecider(root: string, explicit?: string): string | undefined { 856 │ const given = oneLine(explicit || ''); 857 │ if (given) return given;
L853 flow GitConfig → #cli
Reads the local git identity to attribute a decision
848 │ * git's configured identity, then $USER. Returns undefined when nothing 849 │ * identifies a person — better to ask than to record "unknown" as the 850 │ * maintainer who granted a privilege. 851 │ * 852 │ * @mitigates #cli against #cmd-injection using #param-commands -- "execFileSync with a fixed argv and no shell; nothing user-supplied reaches the command" 853 │ * @flows GitConfig -> #cli via execFileSync -- "Reads the local git identity to attribute a decision" 854 │ */ 855 │ export function defaultDecider(root: string, explicit?: string): string | undefined { 856 │ const given = oneLine(explicit || ''); 857 │ if (given) return given; 858 │ try {
src/review/index.ts 2 stale exposes 3mitigates 3flow 3comment 2 open 13
L28 exposes #cli → #arbitrary-write
Writes @accepts/@audit annotations into source files
23 │ * `by <who>` the name of the human who signed, as `guardlink entitle` has 24 │ * always recorded for the other human-only verb 25 │ * `until <date>` a horizon, after which it stops covering anything 26 │ * a justification long enough to be a reason rather than a category 27 │ * 28 │ * @exposes #cli to #arbitrary-write [medium] cwe:CWE-73 -- "Writes @accepts/@audit annotations into source files" 29 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Only modifies files already in the parsed project" 30 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-74 -- "Reviewer-supplied justification and author name are interpolated into annotation text, where a newline would forge a second annotation" 31 │ * @mitigates #cli against #arbitrary-write using #input-sanitize -- "oneLine() collapses newlines/CR/tabs before escapeDesc on every field that reaches a built line" 32 │ * @audit #cli -- "Every acceptance records who decided, why, and until when; the rule is enforced in applyReviewAction so no caller can route around it" 33 │ * @flows ThreatModel -> #cli via getReviewableExposures -- "Exposure list input"
L29 mitigates #path-validation mitigates #arbitrary-write
Only modifies files already in the parsed project
24 │ * always recorded for the other human-only verb 25 │ * `until <date>` a horizon, after which it stops covering anything 26 │ * a justification long enough to be a reason rather than a category 27 │ * 28 │ * @exposes #cli to #arbitrary-write [medium] cwe:CWE-73 -- "Writes @accepts/@audit annotations into source files" 29 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Only modifies files already in the parsed project" 30 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-74 -- "Reviewer-supplied justification and author name are interpolated into annotation text, where a newline would forge a second annotation" 31 │ * @mitigates #cli against #arbitrary-write using #input-sanitize -- "oneLine() collapses newlines/CR/tabs before escapeDesc on every field that reaches a built line" 32 │ * @audit #cli -- "Every acceptance records who decided, why, and until when; the rule is enforced in applyReviewAction so no caller can route around it" 33 │ * @flows ThreatModel -> #cli via getReviewableExposures -- "Exposure list input" 34 │ * @flows #cli -> SourceFiles via writeFile -- "Annotation insertion output"
L30 exposes #cli → #arbitrary-write
Reviewer-supplied justification and author name are interpolated into annotation text, where a newline would forge a second annotation
25 │ * `until <date>` a horizon, after which it stops covering anything 26 │ * a justification long enough to be a reason rather than a category 27 │ * 28 │ * @exposes #cli to #arbitrary-write [medium] cwe:CWE-73 -- "Writes @accepts/@audit annotations into source files" 29 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Only modifies files already in the parsed project" 30 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-74 -- "Reviewer-supplied justification and author name are interpolated into annotation text, where a newline would forge a second annotation" 31 │ * @mitigates #cli against #arbitrary-write using #input-sanitize -- "oneLine() collapses newlines/CR/tabs before escapeDesc on every field that reaches a built line" 32 │ * @audit #cli -- "Every acceptance records who decided, why, and until when; the rule is enforced in applyReviewAction so no caller can route around it" 33 │ * @flows ThreatModel -> #cli via getReviewableExposures -- "Exposure list input" 34 │ * @flows #cli -> SourceFiles via writeFile -- "Annotation insertion output" 35 │ * @handles internal on #cli -- "Processes exposure metadata, reviewer identity and justification text"
L31 mitigates #input-sanitize mitigates #arbitrary-write
oneLine() collapses newlines/CR/tabs before escapeDesc on every field that reaches a built line
26 │ * a justification long enough to be a reason rather than a category 27 │ * 28 │ * @exposes #cli to #arbitrary-write [medium] cwe:CWE-73 -- "Writes @accepts/@audit annotations into source files" 29 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Only modifies files already in the parsed project" 30 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-74 -- "Reviewer-supplied justification and author name are interpolated into annotation text, where a newline would forge a second annotation" 31 │ * @mitigates #cli against #arbitrary-write using #input-sanitize -- "oneLine() collapses newlines/CR/tabs before escapeDesc on every field that reaches a built line" 32 │ * @audit #cli -- "Every acceptance records who decided, why, and until when; the rule is enforced in applyReviewAction so no caller can route around it" 33 │ * @flows ThreatModel -> #cli via getReviewableExposures -- "Exposure list input" 34 │ * @flows #cli -> SourceFiles via writeFile -- "Annotation insertion output" 35 │ * @handles internal on #cli -- "Processes exposure metadata, reviewer identity and justification text" 36 │ * @comment -- "applyReviewAction throws rather than returning a flag: a writer that can be ignored by a caller who forgot to check is the hole this replaced"
L32 audit Audit: #cli
Every acceptance records who decided, why, and until when; the rule is enforced in applyReviewAction so no caller can route around it
27 │ * 28 │ * @exposes #cli to #arbitrary-write [medium] cwe:CWE-73 -- "Writes @accepts/@audit annotations into source files" 29 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Only modifies files already in the parsed project" 30 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-74 -- "Reviewer-supplied justification and author name are interpolated into annotation text, where a newline would forge a second annotation" 31 │ * @mitigates #cli against #arbitrary-write using #input-sanitize -- "oneLine() collapses newlines/CR/tabs before escapeDesc on every field that reaches a built line" 32 │ * @audit #cli -- "Every acceptance records who decided, why, and until when; the rule is enforced in applyReviewAction so no caller can route around it" 33 │ * @flows ThreatModel -> #cli via getReviewableExposures -- "Exposure list input" 34 │ * @flows #cli -> SourceFiles via writeFile -- "Annotation insertion output" 35 │ * @handles internal on #cli -- "Processes exposure metadata, reviewer identity and justification text" 36 │ * @comment -- "applyReviewAction throws rather than returning a flag: a writer that can be ignored by a caller who forgot to check is the hole this replaced" 37 │ */
L33 flow ThreatModel → #cli
Exposure list input
28 │ * @exposes #cli to #arbitrary-write [medium] cwe:CWE-73 -- "Writes @accepts/@audit annotations into source files" 29 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Only modifies files already in the parsed project" 30 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-74 -- "Reviewer-supplied justification and author name are interpolated into annotation text, where a newline would forge a second annotation" 31 │ * @mitigates #cli against #arbitrary-write using #input-sanitize -- "oneLine() collapses newlines/CR/tabs before escapeDesc on every field that reaches a built line" 32 │ * @audit #cli -- "Every acceptance records who decided, why, and until when; the rule is enforced in applyReviewAction so no caller can route around it" 33 │ * @flows ThreatModel -> #cli via getReviewableExposures -- "Exposure list input" 34 │ * @flows #cli -> SourceFiles via writeFile -- "Annotation insertion output" 35 │ * @handles internal on #cli -- "Processes exposure metadata, reviewer identity and justification text" 36 │ * @comment -- "applyReviewAction throws rather than returning a flag: a writer that can be ignored by a caller who forgot to check is the hole this replaced" 37 │ */ 38 │
L34 flow #cli → SourceFiles
Annotation insertion output
29 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Only modifies files already in the parsed project" 30 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-74 -- "Reviewer-supplied justification and author name are interpolated into annotation text, where a newline would forge a second annotation" 31 │ * @mitigates #cli against #arbitrary-write using #input-sanitize -- "oneLine() collapses newlines/CR/tabs before escapeDesc on every field that reaches a built line" 32 │ * @audit #cli -- "Every acceptance records who decided, why, and until when; the rule is enforced in applyReviewAction so no caller can route around it" 33 │ * @flows ThreatModel -> #cli via getReviewableExposures -- "Exposure list input" 34 │ * @flows #cli -> SourceFiles via writeFile -- "Annotation insertion output" 35 │ * @handles internal on #cli -- "Processes exposure metadata, reviewer identity and justification text" 36 │ * @comment -- "applyReviewAction throws rather than returning a flag: a writer that can be ignored by a caller who forgot to check is the hole this replaced" 37 │ */ 38 │ 39 │ import { readFile, writeFile } from 'node:fs/promises';
L35 handles #cli: internal
Processes exposure metadata, reviewer identity and justification text
30 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-74 -- "Reviewer-supplied justification and author name are interpolated into annotation text, where a newline would forge a second annotation" 31 │ * @mitigates #cli against #arbitrary-write using #input-sanitize -- "oneLine() collapses newlines/CR/tabs before escapeDesc on every field that reaches a built line" 32 │ * @audit #cli -- "Every acceptance records who decided, why, and until when; the rule is enforced in applyReviewAction so no caller can route around it" 33 │ * @flows ThreatModel -> #cli via getReviewableExposures -- "Exposure list input" 34 │ * @flows #cli -> SourceFiles via writeFile -- "Annotation insertion output" 35 │ * @handles internal on #cli -- "Processes exposure metadata, reviewer identity and justification text" 36 │ * @comment -- "applyReviewAction throws rather than returning a flag: a writer that can be ignored by a caller who forgot to check is the hole this replaced" 37 │ */ 38 │ 39 │ import { readFile, writeFile } from 'node:fs/promises'; 40 │ import { extname, resolve } from 'node:path';
L36 comment applyReviewAction throws rather than returning a flag: a writer that can be ignored by a caller who forgot to check is the hole this replaced
applyReviewAction throws rather than returning a flag: a writer that can be ignored by a caller who forgot to check is the hole this replaced
31 │ * @mitigates #cli against #arbitrary-write using #input-sanitize -- "oneLine() collapses newlines/CR/tabs before escapeDesc on every field that reaches a built line" 32 │ * @audit #cli -- "Every acceptance records who decided, why, and until when; the rule is enforced in applyReviewAction so no caller can route around it" 33 │ * @flows ThreatModel -> #cli via getReviewableExposures -- "Exposure list input" 34 │ * @flows #cli -> SourceFiles via writeFile -- "Annotation insertion output" 35 │ * @handles internal on #cli -- "Processes exposure metadata, reviewer identity and justification text" 36 │ * @comment -- "applyReviewAction throws rather than returning a flag: a writer that can be ignored by a caller who forgot to check is the hole this replaced" 37 │ */ 38 │ 39 │ import { readFile, writeFile } from 'node:fs/promises'; 40 │ import { extname, resolve } from 'node:path'; 41 │ import { commentStyleForExt, stripCommentPrefix } from '../parser/comment-strip.js';
L389 comment Shared by exposure review (@accepts/@audit) and entitlement acceptance (@entitles) so both writers place annotations by the same rules
Shared by exposure review (@accepts/@audit) and entitlement acceptance (@entitles) so both writers place annotations by the same rules
384 │ * a caller composes annotation text without knowing whether the target is a 385 │ * JSDoc block, a `#` comment, or a raw `.gal` file. 386 │ * 387 │ * Returns the number of lines inserted. 388 │ * 389 │ * @comment -- "Shared by exposure review (@accepts/@audit) and entitlement acceptance (@entitles) so both writers place annotations by the same rules" 390 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "Writes annotation lines into a caller-supplied file path" 391 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Anchor line must exist in the file, and callers resolve the path against the parsed project root" 392 │ * @flows #cli -> SourceFiles via writeFile -- "Annotation insertion output" 393 │ */ 394 │ export async function insertAnnotationsAt(
L390 exposes #cli → #arbitrary-write
Writes annotation lines into a caller-supplied file path
385 │ * JSDoc block, a `#` comment, or a raw `.gal` file. 386 │ * 387 │ * Returns the number of lines inserted. 388 │ * 389 │ * @comment -- "Shared by exposure review (@accepts/@audit) and entitlement acceptance (@entitles) so both writers place annotations by the same rules" 390 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "Writes annotation lines into a caller-supplied file path" 391 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Anchor line must exist in the file, and callers resolve the path against the parsed project root" 392 │ * @flows #cli -> SourceFiles via writeFile -- "Annotation insertion output" 393 │ */ 394 │ export async function insertAnnotationsAt( 395 │ root: string,
L391 mitigates #path-validation mitigates #arbitrary-write
Anchor line must exist in the file, and callers resolve the path against the parsed project root
386 │ * 387 │ * Returns the number of lines inserted. 388 │ * 389 │ * @comment -- "Shared by exposure review (@accepts/@audit) and entitlement acceptance (@entitles) so both writers place annotations by the same rules" 390 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "Writes annotation lines into a caller-supplied file path" 391 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Anchor line must exist in the file, and callers resolve the path against the parsed project root" 392 │ * @flows #cli -> SourceFiles via writeFile -- "Annotation insertion output" 393 │ */ 394 │ export async function insertAnnotationsAt( 395 │ root: string, 396 │ anchor: { file: string; line: number },
L392 flow #cli → SourceFiles
Annotation insertion output
387 │ * Returns the number of lines inserted. 388 │ * 389 │ * @comment -- "Shared by exposure review (@accepts/@audit) and entitlement acceptance (@entitles) so both writers place annotations by the same rules" 390 │ * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "Writes annotation lines into a caller-supplied file path" 391 │ * @mitigates #cli against #arbitrary-write using #path-validation -- "Anchor line must exist in the file, and callers resolve the path against the parsed project root" 392 │ * @flows #cli -> SourceFiles via writeFile -- "Annotation insertion output" 393 │ */ 394 │ export async function insertAnnotationsAt( 395 │ root: string, 396 │ anchor: { file: string; line: number }, 397 │ build: (style: CommentStyle) => string[],
src/structure/anchor.ts comment 1 open 1
L16 comment Pure functions over an in-memory tree; no I/O, no user input beyond the source text already read by the parser
Pure functions over an in-memory tree; no I/O, no user input beyond the source text already read by the parser
11 │ * 12 │ * The order of the special cases matters and is the spec's: no grammar, then 13 │ * first-node, then no-sibling, then import-sibling, then YAML, then the 14 │ * general rule. First match wins. 15 │ * 16 │ * @comment -- "Pure functions over an in-memory tree; no I/O, no user input beyond the source text already read by the parser" 17 │ */ 18 │ import type { Node } from 'web-tree-sitter'; 19 │ import type { Anchor, AnchorReason } from '../types/index.js'; 20 │ import { hashNode, isCommentType } from './hash.js'; 21 │
src/structure/attach.ts exposes 2mitigates 2flow 1comment 1 open 6
L10 exposes #parser → #path-traversal
Reads the file each annotation's location names, and a .gal @source path is author-supplied text that survives normalisation with ../ intact
5 │ * paths and before the model is assembled. `assembleModel` shares each 6 │ * location object by reference, so an anchor set here is the anchor every 7 │ * surface sees. Files are grouped by LOGICAL path: in external mode that is 8 │ * the source the sidecar describes, never the sidecar. 9 │ * 10 │ * @exposes #parser to #path-traversal [low] cwe:CWE-22 -- "Reads the file each annotation's location names, and a .gal @source path is author-supplied text that survives normalisation with ../ intact" 11 │ * @mitigates #parser against #path-traversal using #path-validation -- "Each logical path is resolved against root and skipped unless it is root or lies under root + sep; the file is never opened otherwise, so nothing outside the scanned tree is read" 12 │ * @exposes #parser to #dos [low] cwe:CWE-400 -- "One structure parse per annotated file" 13 │ * @mitigates #parser against #dos using #resource-limits -- "Only files that carry annotations are parsed, once each, and the tree is released immediately" 14 │ * @flows SourceFiles -> #parser via attachAnchors -- "Annotated files re-read for structure" 15 │ * @comment -- "A file that cannot be read or cannot be parsed nulls that file's anchors and warns once; it never fails the parse, because one pathological file must not take down every command in the repository"
L11 mitigates #path-validation mitigates #path-traversal
Each logical path is resolved against root and skipped unless it is root or lies under root + sep; the file is never opened otherwise, so nothing outside the scanned tree is read
6 │ * location object by reference, so an anchor set here is the anchor every 7 │ * surface sees. Files are grouped by LOGICAL path: in external mode that is 8 │ * the source the sidecar describes, never the sidecar. 9 │ * 10 │ * @exposes #parser to #path-traversal [low] cwe:CWE-22 -- "Reads the file each annotation's location names, and a .gal @source path is author-supplied text that survives normalisation with ../ intact" 11 │ * @mitigates #parser against #path-traversal using #path-validation -- "Each logical path is resolved against root and skipped unless it is root or lies under root + sep; the file is never opened otherwise, so nothing outside the scanned tree is read" 12 │ * @exposes #parser to #dos [low] cwe:CWE-400 -- "One structure parse per annotated file" 13 │ * @mitigates #parser against #dos using #resource-limits -- "Only files that carry annotations are parsed, once each, and the tree is released immediately" 14 │ * @flows SourceFiles -> #parser via attachAnchors -- "Annotated files re-read for structure" 15 │ * @comment -- "A file that cannot be read or cannot be parsed nulls that file's anchors and warns once; it never fails the parse, because one pathological file must not take down every command in the repository" 16 │ */
L12 exposes #parser → #dos
One structure parse per annotated file
7 │ * surface sees. Files are grouped by LOGICAL path: in external mode that is 8 │ * the source the sidecar describes, never the sidecar. 9 │ * 10 │ * @exposes #parser to #path-traversal [low] cwe:CWE-22 -- "Reads the file each annotation's location names, and a .gal @source path is author-supplied text that survives normalisation with ../ intact" 11 │ * @mitigates #parser against #path-traversal using #path-validation -- "Each logical path is resolved against root and skipped unless it is root or lies under root + sep; the file is never opened otherwise, so nothing outside the scanned tree is read" 12 │ * @exposes #parser to #dos [low] cwe:CWE-400 -- "One structure parse per annotated file" 13 │ * @mitigates #parser against #dos using #resource-limits -- "Only files that carry annotations are parsed, once each, and the tree is released immediately" 14 │ * @flows SourceFiles -> #parser via attachAnchors -- "Annotated files re-read for structure" 15 │ * @comment -- "A file that cannot be read or cannot be parsed nulls that file's anchors and warns once; it never fails the parse, because one pathological file must not take down every command in the repository" 16 │ */ 17 │ import { readFile } from 'node:fs/promises';
L13 mitigates #resource-limits mitigates #dos
Only files that carry annotations are parsed, once each, and the tree is released immediately
8 │ * the source the sidecar describes, never the sidecar. 9 │ * 10 │ * @exposes #parser to #path-traversal [low] cwe:CWE-22 -- "Reads the file each annotation's location names, and a .gal @source path is author-supplied text that survives normalisation with ../ intact" 11 │ * @mitigates #parser against #path-traversal using #path-validation -- "Each logical path is resolved against root and skipped unless it is root or lies under root + sep; the file is never opened otherwise, so nothing outside the scanned tree is read" 12 │ * @exposes #parser to #dos [low] cwe:CWE-400 -- "One structure parse per annotated file" 13 │ * @mitigates #parser against #dos using #resource-limits -- "Only files that carry annotations are parsed, once each, and the tree is released immediately" 14 │ * @flows SourceFiles -> #parser via attachAnchors -- "Annotated files re-read for structure" 15 │ * @comment -- "A file that cannot be read or cannot be parsed nulls that file's anchors and warns once; it never fails the parse, because one pathological file must not take down every command in the repository" 16 │ */ 17 │ import { readFile } from 'node:fs/promises'; 18 │ import { resolve, sep } from 'node:path';
L14 flow SourceFiles → #parser
Annotated files re-read for structure
9 │ * 10 │ * @exposes #parser to #path-traversal [low] cwe:CWE-22 -- "Reads the file each annotation's location names, and a .gal @source path is author-supplied text that survives normalisation with ../ intact" 11 │ * @mitigates #parser against #path-traversal using #path-validation -- "Each logical path is resolved against root and skipped unless it is root or lies under root + sep; the file is never opened otherwise, so nothing outside the scanned tree is read" 12 │ * @exposes #parser to #dos [low] cwe:CWE-400 -- "One structure parse per annotated file" 13 │ * @mitigates #parser against #dos using #resource-limits -- "Only files that carry annotations are parsed, once each, and the tree is released immediately" 14 │ * @flows SourceFiles -> #parser via attachAnchors -- "Annotated files re-read for structure" 15 │ * @comment -- "A file that cannot be read or cannot be parsed nulls that file's anchors and warns once; it never fails the parse, because one pathological file must not take down every command in the repository" 16 │ */ 17 │ import { readFile } from 'node:fs/promises'; 18 │ import { resolve, sep } from 'node:path'; 19 │ import type { Annotation } from '../types/index.js';
L15 comment A file that cannot be read or cannot be parsed nulls that file's anchors and warns once; it never fails the parse, because one pathological file must not take down every command in the repository
A file that cannot be read or cannot be parsed nulls that file's anchors and warns once; it never fails the parse, because one pathological file must not take down every command in the repository
10 │ * @exposes #parser to #path-traversal [low] cwe:CWE-22 -- "Reads the file each annotation's location names, and a .gal @source path is author-supplied text that survives normalisation with ../ intact" 11 │ * @mitigates #parser against #path-traversal using #path-validation -- "Each logical path is resolved against root and skipped unless it is root or lies under root + sep; the file is never opened otherwise, so nothing outside the scanned tree is read" 12 │ * @exposes #parser to #dos [low] cwe:CWE-400 -- "One structure parse per annotated file" 13 │ * @mitigates #parser against #dos using #resource-limits -- "Only files that carry annotations are parsed, once each, and the tree is released immediately" 14 │ * @flows SourceFiles -> #parser via attachAnchors -- "Annotated files re-read for structure" 15 │ * @comment -- "A file that cannot be read or cannot be parsed nulls that file's anchors and warns once; it never fails the parse, because one pathological file must not take down every command in the repository" 16 │ */ 17 │ import { readFile } from 'node:fs/promises'; 18 │ import { resolve, sep } from 'node:path'; 19 │ import type { Annotation } from '../types/index.js'; 20 │ import { parseStructure } from './index.js';
src/structure/grammars.ts comment 1 open 1
L14 comment Data only: no I/O, no user input. The paths it produces are joined under the package's own grammars/ directory
Data only: no I/O, no user input. The paths it produces are joined under the package's own grammars/ directory
9 │ * 10 │ * Every version is exact. The WASM inside a grammar package is built against a 11 │ * tree-sitter ABI the pinned `web-tree-sitter` runtime must accept; a caret 12 │ * here would let a grammar move to an ABI the runtime rejects. 13 │ * 14 │ * @comment -- "Data only: no I/O, no user input. The paths it produces are joined under the package's own grammars/ directory" 15 │ */ 16 │ import { dirname, join, resolve } from 'node:path'; 17 │ import { fileURLToPath } from 'node:url'; 18 │ 19 │ export interface GrammarSource {
src/structure/hash.ts comment 1 open 1
L14 comment Pure functions over an in-memory syntax tree or a string; no I/O
Pure functions over an in-memory syntax tree or a string; no I/O
9 │ * 10 │ * Tokens are joined with a control character rather than concatenated, so the 11 │ * sequences (ab, c) and (a, bc) cannot collide — the same reasoning as 12 │ * annotation-hash.ts. 13 │ * 14 │ * @comment -- "Pure functions over an in-memory syntax tree or a string; no I/O" 15 │ */ 16 │ import { createHash } from 'node:crypto'; 17 │ import type { Node } from 'web-tree-sitter'; 18 │ 19 │ /** Bump when token selection or the separator changes. Part of every emitted hash. */
src/structure/index.ts flow 1comment 1 open 2
L11 flow SourceFile → #parser
File content already read by the parser, parsed again for structure
6 │ * line describe" and "where is the declaration called X", both as `Anchor`s 7 │ * carrying a content hash. Files without a grammar answer with a file-scope 8 │ * anchor over a plain-text hash, so every caller gets an anchor and no caller 9 │ * has to know whether tree-sitter was involved. 10 │ * 11 │ * @flows SourceFile -> #parser via parseStructure -- "File content already read by the parser, parsed again for structure" 12 │ * @comment -- "Takes content, not a path to read: the parser owns file I/O and its path validation; this layer never opens a file" 13 │ */ 14 │ import { extname } from 'node:path'; 15 │ import type { Tree, Node } from 'web-tree-sitter'; 16 │ import type { Anchor, AnchorReason } from '../types/index.js';
L12 comment Takes content, not a path to read: the parser owns file I/O and its path validation; this layer never opens a file
Takes content, not a path to read: the parser owns file I/O and its path validation; this layer never opens a file
7 │ * carrying a content hash. Files without a grammar answer with a file-scope 8 │ * anchor over a plain-text hash, so every caller gets an anchor and no caller 9 │ * has to know whether tree-sitter was involved. 10 │ * 11 │ * @flows SourceFile -> #parser via parseStructure -- "File content already read by the parser, parsed again for structure" 12 │ * @comment -- "Takes content, not a path to read: the parser owns file I/O and its path validation; this layer never opens a file" 13 │ */ 14 │ import { extname } from 'node:path'; 15 │ import type { Tree, Node } from 'web-tree-sitter'; 16 │ import type { Anchor, AnchorReason } from '../types/index.js'; 17 │ import { languageForExtension } from './grammars.js';
src/structure/runtime.ts exposes 1mitigates 1flow 1comment 1 open 4
L16 exposes #parser → #dos
Grammar WASM is loaded into memory per language; a pathological source file costs one parse
11 │ * is still attempted — silent `no-grammar` when absent, `grammar-failed` and 12 │ * warned once if present but unloadable. A file that exists but fails to load is 13 │ * always `grammar-failed` and warned once, because that is a runtime packaging 14 │ * defect, not a fallback. 15 │ * 16 │ * @exposes #parser to #dos [low] cwe:CWE-400 -- "Grammar WASM is loaded into memory per language; a pathological source file costs one parse" 17 │ * @mitigates #parser against #dos using #resource-limits -- "One runtime init and one load per language per process; trees are parsed on demand and deleted by callers" 18 │ * @flows GrammarFile -> #parser via Language.load -- "Bundled WASM read from the package's grammars/ directory" 19 │ * @comment -- "Paths come only from grammarPath(language) over the package's own grammars/ directory; no caller-supplied path reaches Language.load" 20 │ */ 21 │ import { existsSync } from 'node:fs';
L17 mitigates #resource-limits mitigates #dos
One runtime init and one load per language per process; trees are parsed on demand and deleted by callers
12 │ * warned once if present but unloadable. A file that exists but fails to load is 13 │ * always `grammar-failed` and warned once, because that is a runtime packaging 14 │ * defect, not a fallback. 15 │ * 16 │ * @exposes #parser to #dos [low] cwe:CWE-400 -- "Grammar WASM is loaded into memory per language; a pathological source file costs one parse" 17 │ * @mitigates #parser against #dos using #resource-limits -- "One runtime init and one load per language per process; trees are parsed on demand and deleted by callers" 18 │ * @flows GrammarFile -> #parser via Language.load -- "Bundled WASM read from the package's grammars/ directory" 19 │ * @comment -- "Paths come only from grammarPath(language) over the package's own grammars/ directory; no caller-supplied path reaches Language.load" 20 │ */ 21 │ import { existsSync } from 'node:fs'; 22 │ import { join } from 'node:path';
L18 flow GrammarFile → #parser
Bundled WASM read from the package's grammars/ directory
13 │ * always `grammar-failed` and warned once, because that is a runtime packaging 14 │ * defect, not a fallback. 15 │ * 16 │ * @exposes #parser to #dos [low] cwe:CWE-400 -- "Grammar WASM is loaded into memory per language; a pathological source file costs one parse" 17 │ * @mitigates #parser against #dos using #resource-limits -- "One runtime init and one load per language per process; trees are parsed on demand and deleted by callers" 18 │ * @flows GrammarFile -> #parser via Language.load -- "Bundled WASM read from the package's grammars/ directory" 19 │ * @comment -- "Paths come only from grammarPath(language) over the package's own grammars/ directory; no caller-supplied path reaches Language.load" 20 │ */ 21 │ import { existsSync } from 'node:fs'; 22 │ import { join } from 'node:path'; 23 │ import { Parser, Language } from 'web-tree-sitter';
L19 comment Paths come only from grammarPath(language) over the package's own grammars/ directory; no caller-supplied path reaches Language.load
Paths come only from grammarPath(language) over the package's own grammars/ directory; no caller-supplied path reaches Language.load
14 │ * defect, not a fallback. 15 │ * 16 │ * @exposes #parser to #dos [low] cwe:CWE-400 -- "Grammar WASM is loaded into memory per language; a pathological source file costs one parse" 17 │ * @mitigates #parser against #dos using #resource-limits -- "One runtime init and one load per language per process; trees are parsed on demand and deleted by callers" 18 │ * @flows GrammarFile -> #parser via Language.load -- "Bundled WASM read from the package's grammars/ directory" 19 │ * @comment -- "Paths come only from grammarPath(language) over the package's own grammars/ directory; no caller-supplied path reaches Language.load" 20 │ */ 21 │ import { existsSync } from 'node:fs'; 22 │ import { join } from 'node:path'; 23 │ import { Parser, Language } from 'web-tree-sitter'; 24 │ import type { Tree } from 'web-tree-sitter';
src/tui/config.ts flow 2exposes 1mitigates 1handles 1 open 5
L7 exposes #tui → #api-key-exposure
API keys loaded from and saved to config files
2 │ * GuardLink TUI — Config persistence for LLM settings. 3 │ * 4 │ * Now delegates to the unified agents/config.ts resolution chain. 5 │ * Keeps backward compatibility with tui-config.json (legacy). 6 │ * 7 │ * @exposes #tui to #api-key-exposure [high] cwe:CWE-798 -- "API keys loaded from and saved to config files" 8 │ * @mitigates #tui against #api-key-exposure using #key-redaction -- "Delegates to agents/config.ts with masking" 9 │ * @flows ConfigFile -> #tui via loadProjectConfig -- "Config load path" 10 │ * @flows #tui -> ConfigFile via saveProjectConfig -- "Config save path" 11 │ * @handles secrets on #tui -- "API keys stored in .guardlink/config.json" 12 │ */
L8 mitigates #key-redaction mitigates #api-key-exposure
Delegates to agents/config.ts with masking
3 │ * 4 │ * Now delegates to the unified agents/config.ts resolution chain. 5 │ * Keeps backward compatibility with tui-config.json (legacy). 6 │ * 7 │ * @exposes #tui to #api-key-exposure [high] cwe:CWE-798 -- "API keys loaded from and saved to config files" 8 │ * @mitigates #tui against #api-key-exposure using #key-redaction -- "Delegates to agents/config.ts with masking" 9 │ * @flows ConfigFile -> #tui via loadProjectConfig -- "Config load path" 10 │ * @flows #tui -> ConfigFile via saveProjectConfig -- "Config save path" 11 │ * @handles secrets on #tui -- "API keys stored in .guardlink/config.json" 12 │ */ 13 │
L9 flow ConfigFile → #tui
Config load path
4 │ * Now delegates to the unified agents/config.ts resolution chain. 5 │ * Keeps backward compatibility with tui-config.json (legacy). 6 │ * 7 │ * @exposes #tui to #api-key-exposure [high] cwe:CWE-798 -- "API keys loaded from and saved to config files" 8 │ * @mitigates #tui against #api-key-exposure using #key-redaction -- "Delegates to agents/config.ts with masking" 9 │ * @flows ConfigFile -> #tui via loadProjectConfig -- "Config load path" 10 │ * @flows #tui -> ConfigFile via saveProjectConfig -- "Config save path" 11 │ * @handles secrets on #tui -- "API keys stored in .guardlink/config.json" 12 │ */ 13 │ 14 │ import type { LLMConfig, LLMProvider } from '../analyze/llm.js';
L10 flow #tui → ConfigFile
Config save path
5 │ * Keeps backward compatibility with tui-config.json (legacy). 6 │ * 7 │ * @exposes #tui to #api-key-exposure [high] cwe:CWE-798 -- "API keys loaded from and saved to config files" 8 │ * @mitigates #tui against #api-key-exposure using #key-redaction -- "Delegates to agents/config.ts with masking" 9 │ * @flows ConfigFile -> #tui via loadProjectConfig -- "Config load path" 10 │ * @flows #tui -> ConfigFile via saveProjectConfig -- "Config save path" 11 │ * @handles secrets on #tui -- "API keys stored in .guardlink/config.json" 12 │ */ 13 │ 14 │ import type { LLMConfig, LLMProvider } from '../analyze/llm.js'; 15 │ import { resolveConfig, saveProjectConfig, loadProjectConfig } from '../agents/config.js';
L11 handles #tui: secrets
API keys stored in .guardlink/config.json
6 │ * 7 │ * @exposes #tui to #api-key-exposure [high] cwe:CWE-798 -- "API keys loaded from and saved to config files" 8 │ * @mitigates #tui against #api-key-exposure using #key-redaction -- "Delegates to agents/config.ts with masking" 9 │ * @flows ConfigFile -> #tui via loadProjectConfig -- "Config load path" 10 │ * @flows #tui -> ConfigFile via saveProjectConfig -- "Config save path" 11 │ * @handles secrets on #tui -- "API keys stored in .guardlink/config.json" 12 │ */ 13 │ 14 │ import type { LLMConfig, LLMProvider } from '../analyze/llm.js'; 15 │ import { resolveConfig, saveProjectConfig, loadProjectConfig } from '../agents/config.js'; 16 │
src/tui/index.ts 3 stale exposes 2flow 2mitigates 1audit 1 open 8
L9 exposes #tui → #path-traversal
User-supplied dir argument resolved via path.resolve
4 │ * GuardLink TUI — Interactive terminal interface. 5 │ * 6 │ * Claude Code-style inline REPL: stays in your terminal, 7 │ * slash commands + freeform AI chat, Ctrl+C to exit. 8 │ * 9 │ * @exposes #tui to #path-traversal [high] cwe:CWE-22 -- "User-supplied dir argument resolved via path.resolve" 10 │ * @mitigates #tui against #path-traversal using #path-validation -- "resolve() canonicalizes paths; starts from cwd" 11 │ * @exposes #tui to #api-key-exposure [medium] cwe:CWE-798 -- "API keys displayed in banner via resolveLLMConfig" 12 │ * @audit #tui -- "API keys masked via maskKey() in banner display" 13 │ * @flows UserInput -> #tui via readline -- "Interactive command input" 14 │ * @flows #tui -> Commands via dispatch -- "Command routing"
L10 mitigates #path-validation mitigates #path-traversal
resolve() canonicalizes paths; starts from cwd
5 │ * 6 │ * Claude Code-style inline REPL: stays in your terminal, 7 │ * slash commands + freeform AI chat, Ctrl+C to exit. 8 │ * 9 │ * @exposes #tui to #path-traversal [high] cwe:CWE-22 -- "User-supplied dir argument resolved via path.resolve" 10 │ * @mitigates #tui against #path-traversal using #path-validation -- "resolve() canonicalizes paths; starts from cwd" 11 │ * @exposes #tui to #api-key-exposure [medium] cwe:CWE-798 -- "API keys displayed in banner via resolveLLMConfig" 12 │ * @audit #tui -- "API keys masked via maskKey() in banner display" 13 │ * @flows UserInput -> #tui via readline -- "Interactive command input" 14 │ * @flows #tui -> Commands via dispatch -- "Command routing" 15 │ * @boundary #tui and UserInput (#tui-input-boundary) -- "Trust boundary at interactive input"
L11 exposes #tui → #api-key-exposure
API keys displayed in banner via resolveLLMConfig
6 │ * Claude Code-style inline REPL: stays in your terminal, 7 │ * slash commands + freeform AI chat, Ctrl+C to exit. 8 │ * 9 │ * @exposes #tui to #path-traversal [high] cwe:CWE-22 -- "User-supplied dir argument resolved via path.resolve" 10 │ * @mitigates #tui against #path-traversal using #path-validation -- "resolve() canonicalizes paths; starts from cwd" 11 │ * @exposes #tui to #api-key-exposure [medium] cwe:CWE-798 -- "API keys displayed in banner via resolveLLMConfig" 12 │ * @audit #tui -- "API keys masked via maskKey() in banner display" 13 │ * @flows UserInput -> #tui via readline -- "Interactive command input" 14 │ * @flows #tui -> Commands via dispatch -- "Command routing" 15 │ * @boundary #tui and UserInput (#tui-input-boundary) -- "Trust boundary at interactive input" 16 │ * @handles secrets on #tui -- "Displays LLM config including masked API keys"
L12 audit Audit: #tui
API keys masked via maskKey() in banner display
7 │ * slash commands + freeform AI chat, Ctrl+C to exit. 8 │ * 9 │ * @exposes #tui to #path-traversal [high] cwe:CWE-22 -- "User-supplied dir argument resolved via path.resolve" 10 │ * @mitigates #tui against #path-traversal using #path-validation -- "resolve() canonicalizes paths; starts from cwd" 11 │ * @exposes #tui to #api-key-exposure [medium] cwe:CWE-798 -- "API keys displayed in banner via resolveLLMConfig" 12 │ * @audit #tui -- "API keys masked via maskKey() in banner display" 13 │ * @flows UserInput -> #tui via readline -- "Interactive command input" 14 │ * @flows #tui -> Commands via dispatch -- "Command routing" 15 │ * @boundary #tui and UserInput (#tui-input-boundary) -- "Trust boundary at interactive input" 16 │ * @handles secrets on #tui -- "Displays LLM config including masked API keys" 17 │ */
L13 flow UserInput → #tui
Interactive command input
8 │ * 9 │ * @exposes #tui to #path-traversal [high] cwe:CWE-22 -- "User-supplied dir argument resolved via path.resolve" 10 │ * @mitigates #tui against #path-traversal using #path-validation -- "resolve() canonicalizes paths; starts from cwd" 11 │ * @exposes #tui to #api-key-exposure [medium] cwe:CWE-798 -- "API keys displayed in banner via resolveLLMConfig" 12 │ * @audit #tui -- "API keys masked via maskKey() in banner display" 13 │ * @flows UserInput -> #tui via readline -- "Interactive command input" 14 │ * @flows #tui -> Commands via dispatch -- "Command routing" 15 │ * @boundary #tui and UserInput (#tui-input-boundary) -- "Trust boundary at interactive input" 16 │ * @handles secrets on #tui -- "Displays LLM config including masked API keys" 17 │ */ 18 │
L14 flow #tui → Commands
Command routing
9 │ * @exposes #tui to #path-traversal [high] cwe:CWE-22 -- "User-supplied dir argument resolved via path.resolve" 10 │ * @mitigates #tui against #path-traversal using #path-validation -- "resolve() canonicalizes paths; starts from cwd" 11 │ * @exposes #tui to #api-key-exposure [medium] cwe:CWE-798 -- "API keys displayed in banner via resolveLLMConfig" 12 │ * @audit #tui -- "API keys masked via maskKey() in banner display" 13 │ * @flows UserInput -> #tui via readline -- "Interactive command input" 14 │ * @flows #tui -> Commands via dispatch -- "Command routing" 15 │ * @boundary #tui and UserInput (#tui-input-boundary) -- "Trust boundary at interactive input" 16 │ * @handles secrets on #tui -- "Displays LLM config including masked API keys" 17 │ */ 18 │ 19 │ import { createInterface } from 'node:readline';
L15 boundary #tui ↔ UserInput
Trust boundary at interactive input
10 │ * @mitigates #tui against #path-traversal using #path-validation -- "resolve() canonicalizes paths; starts from cwd" 11 │ * @exposes #tui to #api-key-exposure [medium] cwe:CWE-798 -- "API keys displayed in banner via resolveLLMConfig" 12 │ * @audit #tui -- "API keys masked via maskKey() in banner display" 13 │ * @flows UserInput -> #tui via readline -- "Interactive command input" 14 │ * @flows #tui -> Commands via dispatch -- "Command routing" 15 │ * @boundary #tui and UserInput (#tui-input-boundary) -- "Trust boundary at interactive input" 16 │ * @handles secrets on #tui -- "Displays LLM config including masked API keys" 17 │ */ 18 │ 19 │ import { createInterface } from 'node:readline'; 20 │ import { resolve, basename } from 'node:path';
L16 handles #tui: secrets
Displays LLM config including masked API keys
11 │ * @exposes #tui to #api-key-exposure [medium] cwe:CWE-798 -- "API keys displayed in banner via resolveLLMConfig" 12 │ * @audit #tui -- "API keys masked via maskKey() in banner display" 13 │ * @flows UserInput -> #tui via readline -- "Interactive command input" 14 │ * @flows #tui -> Commands via dispatch -- "Command routing" 15 │ * @boundary #tui and UserInput (#tui-input-boundary) -- "Trust boundary at interactive input" 16 │ * @handles secrets on #tui -- "Displays LLM config including masked API keys" 17 │ */ 18 │ 19 │ import { createInterface } from 'node:readline'; 20 │ import { resolve, basename } from 'node:path'; 21 │ import { existsSync, readFileSync } from 'node:fs';
src/tui/input.ts flow 2exposes 1mitigates 1comment 1 open 5
L15 exposes #tui → #dos
Rapid keystrokes could consume CPU in raw mode
10 │ * /files Browse files 11 │ * /assets Asset tree 12 │ * 13 │ * Uses raw stdin mode for full keystroke control. 14 │ * 15 │ * @exposes #tui to #dos [low] cwe:CWE-400 -- "Rapid keystrokes could consume CPU in raw mode" 16 │ * @mitigates #tui against #dos using #resource-limits -- "Keystroke buffer bounded by terminal width" 17 │ * @flows RawStdin -> #tui via process.stdin -- "Raw keystroke input" 18 │ * @flows #tui -> Terminal via process.stdout -- "ANSI escape sequence output" 19 │ * @comment -- "Raw mode enables full keystroke control for command palette" 20 │ */
L16 mitigates #resource-limits mitigates #dos
Keystroke buffer bounded by terminal width
11 │ * /assets Asset tree 12 │ * 13 │ * Uses raw stdin mode for full keystroke control. 14 │ * 15 │ * @exposes #tui to #dos [low] cwe:CWE-400 -- "Rapid keystrokes could consume CPU in raw mode" 16 │ * @mitigates #tui against #dos using #resource-limits -- "Keystroke buffer bounded by terminal width" 17 │ * @flows RawStdin -> #tui via process.stdin -- "Raw keystroke input" 18 │ * @flows #tui -> Terminal via process.stdout -- "ANSI escape sequence output" 19 │ * @comment -- "Raw mode enables full keystroke control for command palette" 20 │ */ 21 │
L17 flow RawStdin → #tui
Raw keystroke input
12 │ * 13 │ * Uses raw stdin mode for full keystroke control. 14 │ * 15 │ * @exposes #tui to #dos [low] cwe:CWE-400 -- "Rapid keystrokes could consume CPU in raw mode" 16 │ * @mitigates #tui against #dos using #resource-limits -- "Keystroke buffer bounded by terminal width" 17 │ * @flows RawStdin -> #tui via process.stdin -- "Raw keystroke input" 18 │ * @flows #tui -> Terminal via process.stdout -- "ANSI escape sequence output" 19 │ * @comment -- "Raw mode enables full keystroke control for command palette" 20 │ */ 21 │ 22 │ import chalk from 'chalk';
L18 flow #tui → Terminal
ANSI escape sequence output
13 │ * Uses raw stdin mode for full keystroke control. 14 │ * 15 │ * @exposes #tui to #dos [low] cwe:CWE-400 -- "Rapid keystrokes could consume CPU in raw mode" 16 │ * @mitigates #tui against #dos using #resource-limits -- "Keystroke buffer bounded by terminal width" 17 │ * @flows RawStdin -> #tui via process.stdin -- "Raw keystroke input" 18 │ * @flows #tui -> Terminal via process.stdout -- "ANSI escape sequence output" 19 │ * @comment -- "Raw mode enables full keystroke control for command palette" 20 │ */ 21 │ 22 │ import chalk from 'chalk'; 23 │
L19 comment Raw mode enables full keystroke control for command palette
Raw mode enables full keystroke control for command palette
14 │ * 15 │ * @exposes #tui to #dos [low] cwe:CWE-400 -- "Rapid keystrokes could consume CPU in raw mode" 16 │ * @mitigates #tui against #dos using #resource-limits -- "Keystroke buffer bounded by terminal width" 17 │ * @flows RawStdin -> #tui via process.stdin -- "Raw keystroke input" 18 │ * @flows #tui -> Terminal via process.stdout -- "ANSI escape sequence output" 19 │ * @comment -- "Raw mode enables full keystroke control for command palette" 20 │ */ 21 │ 22 │ import chalk from 'chalk'; 23 │ 24 │ const BRAVOS = '#2dd4a7';
src/version.ts flow 1comment 1 open 2
L10 flow PackageJson → #cli
Version string read from package.json
5 │ * never report a version it is not. The CLI already does this (`--version` used 6 │ * to be a hardcoded literal that silently fell out of sync); this is the same 7 │ * technique behind one export so the next surface that needs it does not invent 8 │ * a third copy. 9 │ * 10 │ * @flows PackageJson -> #cli via readFileSync -- "Version string read from package.json" 11 │ * @comment -- "Resolved relative to this module, so it is correct from both src/ and dist/" 12 │ */ 13 │ 14 │ import { readFileSync } from 'node:fs'; 15 │
L11 comment Resolved relative to this module, so it is correct from both src/ and dist/
Resolved relative to this module, so it is correct from both src/ and dist/
6 │ * to be a hardcoded literal that silently fell out of sync); this is the same 7 │ * technique behind one export so the next surface that needs it does not invent 8 │ * a third copy. 9 │ * 10 │ * @flows PackageJson -> #cli via readFileSync -- "Version string read from package.json" 11 │ * @comment -- "Resolved relative to this module, so it is correct from both src/ and dist/" 12 │ */ 13 │ 14 │ import { readFileSync } from 'node:fs'; 15 │ 16 │ /**
src/workspace/index.ts comment 1 open 1
L4 comment Workspace module: config loading, merge engine, link-project setup
Workspace module: config loading, merge engine, link-project setup
1 │ /** 2 │ * GuardLink Workspace — Multi-repo linking and merge. 3 │ * 4 │ * @comment -- "Workspace module: config loading, merge engine, link-project setup" 5 │ */ 6 │ 7 │ export type { 8 │ WorkspaceConfig, WorkspaceRepo, 9 │ TagOwnership, UnresolvedRef, MergeWarning, MergeWarningCode,
src/workspace/link.ts flow 2asset 1 open 3
L7 asset Workspace.Link
Multi-repo workspace linking setup
2 │ * GuardLink Workspace — link-project command logic. 3 │ * 4 │ * Scaffolds workspace.yaml in each repo and updates agent instruction 5 │ * files with workspace context so agents write cross-repo-aware annotations. 6 │ * 7 │ * @asset Workspace.Link (#workspace-link) -- "Multi-repo workspace linking setup" 8 │ * @flows UserArgs -> #workspace-link via linkProject -- "CLI args to workspace scaffolding" 9 │ * @flows #workspace-link -> AgentFiles via updateAgentWorkspaceContext -- "Inject workspace context" 10 │ */ 11 │ 12 │ import { existsSync, readFileSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs';
L8 flow UserArgs → #workspace-link
CLI args to workspace scaffolding
3 │ * 4 │ * Scaffolds workspace.yaml in each repo and updates agent instruction 5 │ * files with workspace context so agents write cross-repo-aware annotations. 6 │ * 7 │ * @asset Workspace.Link (#workspace-link) -- "Multi-repo workspace linking setup" 8 │ * @flows UserArgs -> #workspace-link via linkProject -- "CLI args to workspace scaffolding" 9 │ * @flows #workspace-link -> AgentFiles via updateAgentWorkspaceContext -- "Inject workspace context" 10 │ */ 11 │ 12 │ import { existsSync, readFileSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs'; 13 │ import { writeFileSync } from 'node:fs';
L9 flow #workspace-link → AgentFiles
Inject workspace context
4 │ * Scaffolds workspace.yaml in each repo and updates agent instruction 5 │ * files with workspace context so agents write cross-repo-aware annotations. 6 │ * 7 │ * @asset Workspace.Link (#workspace-link) -- "Multi-repo workspace linking setup" 8 │ * @flows UserArgs -> #workspace-link via linkProject -- "CLI args to workspace scaffolding" 9 │ * @flows #workspace-link -> AgentFiles via updateAgentWorkspaceContext -- "Inject workspace context" 10 │ */ 11 │ 12 │ import { existsSync, readFileSync, mkdirSync, readdirSync, statSync, unlinkSync } from 'node:fs'; 13 │ import { writeFileSync } from 'node:fs'; 14 │ import { resolve, basename, dirname, join } from 'node:path';
src/workspace/merge.ts flow 2asset 1threat 1mitigates 1 open 5
L7 asset Workspace.Merge
Cross-repo threat model unification
2 │ * GuardLink Workspace — Merge engine for multi-repo reports. 3 │ * 4 │ * Takes N per-repo report JSONs and produces a unified MergedReport 5 │ * with cross-repo tag resolution, warning detection, and aggregated stats. 6 │ * 7 │ * @asset Workspace.Merge (#merge-engine) -- "Cross-repo threat model unification" 8 │ * @threat Tag_Collision (#tag-collision) [medium] -- "Duplicate tag definitions across repos" 9 │ * @mitigates #merge-engine against #tag-collision using #prefix-ownership -- "Tag prefix determines owning repo" 10 │ * @flows ReportJSON -> #merge-engine via mergeReports -- "Per-repo reports feed into merge" 11 │ * @flows #merge-engine -> MergedReport via mergeReports -- "Unified output" 12 │ */
L8 threat Tag_Collision
Duplicate tag definitions across repos
3 │ * 4 │ * Takes N per-repo report JSONs and produces a unified MergedReport 5 │ * with cross-repo tag resolution, warning detection, and aggregated stats. 6 │ * 7 │ * @asset Workspace.Merge (#merge-engine) -- "Cross-repo threat model unification" 8 │ * @threat Tag_Collision (#tag-collision) [medium] -- "Duplicate tag definitions across repos" 9 │ * @mitigates #merge-engine against #tag-collision using #prefix-ownership -- "Tag prefix determines owning repo" 10 │ * @flows ReportJSON -> #merge-engine via mergeReports -- "Per-repo reports feed into merge" 11 │ * @flows #merge-engine -> MergedReport via mergeReports -- "Unified output" 12 │ */ 13 │
L9 mitigates #prefix-ownership mitigates #tag-collision
Tag prefix determines owning repo
4 │ * Takes N per-repo report JSONs and produces a unified MergedReport 5 │ * with cross-repo tag resolution, warning detection, and aggregated stats. 6 │ * 7 │ * @asset Workspace.Merge (#merge-engine) -- "Cross-repo threat model unification" 8 │ * @threat Tag_Collision (#tag-collision) [medium] -- "Duplicate tag definitions across repos" 9 │ * @mitigates #merge-engine against #tag-collision using #prefix-ownership -- "Tag prefix determines owning repo" 10 │ * @flows ReportJSON -> #merge-engine via mergeReports -- "Per-repo reports feed into merge" 11 │ * @flows #merge-engine -> MergedReport via mergeReports -- "Unified output" 12 │ */ 13 │ 14 │ import { readFile } from 'node:fs/promises';
L10 flow ReportJSON → #merge-engine
Per-repo reports feed into merge
5 │ * with cross-repo tag resolution, warning detection, and aggregated stats. 6 │ * 7 │ * @asset Workspace.Merge (#merge-engine) -- "Cross-repo threat model unification" 8 │ * @threat Tag_Collision (#tag-collision) [medium] -- "Duplicate tag definitions across repos" 9 │ * @mitigates #merge-engine against #tag-collision using #prefix-ownership -- "Tag prefix determines owning repo" 10 │ * @flows ReportJSON -> #merge-engine via mergeReports -- "Per-repo reports feed into merge" 11 │ * @flows #merge-engine -> MergedReport via mergeReports -- "Unified output" 12 │ */ 13 │ 14 │ import { readFile } from 'node:fs/promises'; 15 │ import { basename } from 'node:path';
L11 flow #merge-engine → MergedReport
Unified output
6 │ * 7 │ * @asset Workspace.Merge (#merge-engine) -- "Cross-repo threat model unification" 8 │ * @threat Tag_Collision (#tag-collision) [medium] -- "Duplicate tag definitions across repos" 9 │ * @mitigates #merge-engine against #tag-collision using #prefix-ownership -- "Tag prefix determines owning repo" 10 │ * @flows ReportJSON -> #merge-engine via mergeReports -- "Per-repo reports feed into merge" 11 │ * @flows #merge-engine -> MergedReport via mergeReports -- "Unified output" 12 │ */ 13 │ 14 │ import { readFile } from 'node:fs/promises'; 15 │ import { basename } from 'node:path'; 16 │ import type {
src/workspace/metadata.ts flow 2asset 1 open 3
L7 asset Workspace.Metadata
Report provenance data
2 │ * GuardLink Workspace — Report metadata population. 3 │ * 4 │ * Enriches a ThreatModel with provenance metadata (git SHA, branch, 5 │ * workspace info) for the report JSON contract. 6 │ * 7 │ * @asset Workspace.Metadata (#report-metadata) -- "Report provenance data" 8 │ * @flows GitRepo -> #report-metadata via execSync -- "Git info extraction" 9 │ * @flows #report-metadata -> ThreatModel via populateMetadata -- "Metadata injection" 10 │ */ 11 │ 12 │ import { execSync } from 'node:child_process';
L8 flow GitRepo → #report-metadata
Git info extraction
3 │ * 4 │ * Enriches a ThreatModel with provenance metadata (git SHA, branch, 5 │ * workspace info) for the report JSON contract. 6 │ * 7 │ * @asset Workspace.Metadata (#report-metadata) -- "Report provenance data" 8 │ * @flows GitRepo -> #report-metadata via execSync -- "Git info extraction" 9 │ * @flows #report-metadata -> ThreatModel via populateMetadata -- "Metadata injection" 10 │ */ 11 │ 12 │ import { execSync } from 'node:child_process'; 13 │ import { readFileSync, existsSync } from 'node:fs';
L9 flow #report-metadata → ThreatModel
Metadata injection
4 │ * Enriches a ThreatModel with provenance metadata (git SHA, branch, 5 │ * workspace info) for the report JSON contract. 6 │ * 7 │ * @asset Workspace.Metadata (#report-metadata) -- "Report provenance data" 8 │ * @flows GitRepo -> #report-metadata via execSync -- "Git info extraction" 9 │ * @flows #report-metadata -> ThreatModel via populateMetadata -- "Metadata injection" 10 │ */ 11 │ 12 │ import { execSync } from 'node:child_process'; 13 │ import { readFileSync, existsSync } from 'node:fs'; 14 │ import { join } from 'node:path';
src/workspace/types.ts asset 1threat 1mitigates 1 open 3
L7 asset Workspace.Config
Multi-repo workspace definition
2 │ * GuardLink Workspace — Types for multi-repo linking. 3 │ * 4 │ * workspace.yaml lives in each repo's .guardlink/ directory. 5 │ * It declares membership in a workspace and lists sibling repos. 6 │ * 7 │ * @asset Workspace.Config (#workspace-config) -- "Multi-repo workspace definition" 8 │ * @threat Config_Tampering (#config-tamper) [medium] cwe:CWE-15 -- "Malicious workspace.yaml could misdirect agent annotations" 9 │ * @mitigates #workspace-config against #config-tamper using #yaml-validation -- "Schema validation on load" 10 │ */ 11 │ 12 │ import type { ThreatModel, ExternalRef } from '../types/index.js';
L8 threat Config_Tampering
Malicious workspace.yaml could misdirect agent annotations
3 │ * 4 │ * workspace.yaml lives in each repo's .guardlink/ directory. 5 │ * It declares membership in a workspace and lists sibling repos. 6 │ * 7 │ * @asset Workspace.Config (#workspace-config) -- "Multi-repo workspace definition" 8 │ * @threat Config_Tampering (#config-tamper) [medium] cwe:CWE-15 -- "Malicious workspace.yaml could misdirect agent annotations" 9 │ * @mitigates #workspace-config against #config-tamper using #yaml-validation -- "Schema validation on load" 10 │ */ 11 │ 12 │ import type { ThreatModel, ExternalRef } from '../types/index.js'; 13 │
L9 mitigates #yaml-validation mitigates #config-tamper
Schema validation on load
4 │ * workspace.yaml lives in each repo's .guardlink/ directory. 5 │ * It declares membership in a workspace and lists sibling repos. 6 │ * 7 │ * @asset Workspace.Config (#workspace-config) -- "Multi-repo workspace definition" 8 │ * @threat Config_Tampering (#config-tamper) [medium] cwe:CWE-15 -- "Malicious workspace.yaml could misdirect agent annotations" 9 │ * @mitigates #workspace-config against #config-tamper using #yaml-validation -- "Schema validation on load" 10 │ */ 11 │ 12 │ import type { ThreatModel, ExternalRef } from '../types/index.js'; 13 │ 14 │ // ─── Workspace Configuration (workspace.yaml) ───────────────────────
File Coverage
130 of 150 files have GuardLink annotations
Unannotated Files (20)guardlink unannotated .

Source files with no GuardLink annotations. Not all files need annotations — only those touching security boundaries.

.github/ISSUE_TEMPLATE/bug_report.yml
.github/ISSUE_TEMPLATE/config.yml
.github/ISSUE_TEMPLATE/feature_request.yml
.github/workflows/ci.yml
.github/workflows/release.yml
docs/examples/guardlink-pentest.html
eslint.config.js
examples/ci/per-repo-report.yml
examples/ci/workspace-merge.yml
examples/github-action.yml
scripts/build-grammars.ts
src/analyze/format.ts
src/graph/index.ts
src/index.ts
src/init/picker.ts
src/parser/format.ts
src/parser/index.ts
src/parser/normalize.ts
src/tui/format.ts
src/types/index.ts
Data & Boundaries

Where data moves, where trust changes hands, what is classified, and the lifecycle claims (validations, ownership, audits, entitlements, assumptions). Type / to search across all of them.

Data Flows173
Description
EnvVars → #agent-launcher process.env Environment variable input
ConfigFile → #agent-launcher readFileSync Config file read
#agent-launcher → ConfigFile writeFileSync Config file write
UserPrompt → #agent-launcher launchAgent Prompt input path
#agent-launcher → AgentProcess spawn Process spawn path
AgentProcess → #agent-launcher stdout Agent output capture
UserPrompt → #agent-launcher buildAnnotatePrompt User instruction input
UserPrompt → #agent-launcher buildTranslatePrompt Template translation instruction input
UserPrompt → #agent-launcher buildAskPrompt Threat model question input
ThreatModel → #agent-launcher model Model context injection
#agent-launcher → AgentPrompt return Assembled prompt output
SavedReport → #llm-client parseFindingsBlock Findings out of a saved report
ThreatModel → #llm-client serializeModel Model serialization input
ProjectFiles → #llm-client readFileSync Project context read
#llm-client → ReportFile writeFileSync Report output
PentestFindings → #llm-client readFileSync Reads CXG scan results for dashboard and report context
LLMConfig → #llm-client chatCompletion Config and prompt input
#llm-client → LLMProvider fetch API request output
LLMProvider → #llm-client response API response input
LLMToolCall → #llm-client createToolExecutor Tool invocation input
#llm-client → NVD fetch CVE lookup API call
ProjectFiles → #llm-client readFileSync Codebase search reads
ThreatModel → #sarif generateSarif Model input
#sarif → SarifLog return SARIF output
ThreatModel → #dashboard emitArtifacts Model rendered to disk artifacts
#dashboard → FileSystem writeFileSync Artifact write path
#blame → ThreatModel attachBlame record.blame on exposures, confirmed and mitigations
ThreatModel → #blame computeBlame Record locations and anchors
#blame → ThreatModel attachBlame record.blame, only on the CLI --blame paths
ConfigFile → #blame readBlameConfig Attribution settings
#blame → #cli formatBlameText Human-readable attribution
GitRepo → #blame execFileSync blame, log, ls-files, status and rev-parse output
GitRepo → #blame listCommits The reachable history, for commit counts and the HEAD author date
#blame → #cli buildBlamePayload guardlink.blame/v1 payload for --json, the MCP tool and the TUI
#blame → #dashboard summarise Per-person and per-tool rows, the quarterly trend, the cohort comparison and the hot files the Attribution page renders
CommitTrailers → #blame parseTrailerBlock Co-authored-by and Assisted-by values
ThreatModel → #cli runCiChecks Parsed model checked for uncovered exposures, confirmed exploits and unqualified acceptances
ParseDiagnostics → #cli runCiChecks Diagnostics from the parse are reported, never recomputed
SourceFiles → #cli findAnchorDrift Recorded anchors compared against current source
LedgerFile → #cli readLedger Recorded claim hashes, read only
UserArgs → #cli process.argv CLI argument input path
#cli → FileSystem writeFile Report/config output path
GitRepo → #cli attachBlame Opt-in attribution; without --blame the JSON is byte-identical to before
LedgerFile → #cli readLedger status reports verified/stale/unverified claim counts alongside annotation coverage
LedgerFile → #cli readHypotheses status reports how many exposures were tested, and with what outcome
GitRepo → #cli computeBlame status --blame reads attribution without touching the model
LedgerFile → #cli readLedger Read-only; a corrupt ledger becomes a validate error (ledger-corrupt) instead of being silently treated as absent
UserInput → #cli verify Targets, --by and mode flags
GitRepo → #cli computeBlame Attribution read from blame, log -L and commit trailers
GitRepo → #cli attachBlame report --blame attaches attribution before the metadata spread, so the shared records carry it
#cli → #gate runGate The model before and after the agent
ThreatModel → #cli findUnmitigatedPaths Which assets sit on an undefended path, for the ranking
ScanReport → #cli importScan cxg findings recorded as confirmations, joined to claims
GitRepo → #cli attachBlame dashboard --blame; identities land in the page, so blame.identity=hash is the setting for a shared dashboard
GitRepo → #cli loadSince dashboard --since <ref>: the model at the ref, diffed against this one
SourceFiles → #dashboard readFileSync Code snippet reads
LedgerFile → #dashboard readLedger Claim states for the badges and the stale-claims action
ThreatModel → #dashboard generateThreatGraph Threat model relationships rendered as Mermaid source
ThreatModel → #dashboard buildExploreData Model narrowed into one budgeted answer per view
GitRepo → #dashboard loadSince The model at --since <ref>, diffed against the one rendered
ThreatModel → #dashboard computeStats Model statistics input
SourceFiles → #dashboard readFileSync Code snippet reads
LedgerFile → #dashboard computeLedgerStates Claim states for badges and the stale-claims action
GitConfig → #dashboard detectRepoLinks The remote's web host, for file and commit links
#dashboard → HTML return Generated HTML output
LedgerFile → #dashboard readHypotheses Outcomes for the badges, the drawer and the open count
ThreatModel → #dashboard generateDashboardHTML Model to HTML transformation
GitConfig → #dashboard readFileSync remote.origin.url read from .git/config (or the worktree's gitdir config)
#dashboard → DashboardHTML RepoLinks Web, file and commit links embedded in the generated dashboard
ThreatModel → #dashboard buildClaims Records joined with coverage, ledger state and attribution into table rows
DiagramSource → #dashboard checkRenderBudget Generated Mermaid text measured before it is written or served
GitRepo → #dashboard parseAtRef The threat model as it was at the ref
ThreatModel → #diff diffModels Before/after models compared into a structured delta
GitRef → #diff execSync Git command execution
#diff → TempDir writeFileSync Extracted file writes
#diff → ThreatModel parseProject Parsed model output
GitRef → #diff execSync Ref input to git diff --name-only
#diff → ChangedFileList return Repo-relative paths used for entitlement staleness
GitRef → #diff parseAtRef Git reference input
ThreatModel → #gate runGate The model before and after an agent run
#gate → AgentPrompt buildGateFollowUp The violations as a re-prompt
#gate → SourceFiles stripViolations Rejected annotation lines removed
ThreatModel → #gate lintAnnotations Claims and their descriptions
DiagramSource → #dashboard measureLegibility Generated Mermaid text measured for node and edge count before a view decides to draw it
ThreatModel → #dashboard growWithinBudget Model narrowed to a neighbourhood that fits the legibility budget
ThreatModel → #cli classifyHypotheses Claims joined to the ledger by key
#cli → ThreatModel attachHypotheses The state stamped onto each exposure for the dashboard, the report and the lint
ScanReport → #cli importScan cxg findings joined to claims
#cli → SourceFiles writeConfirmedLine The @confirmed line, on --write
LedgerFile → #cli readHypotheses Outcomes read back for classification
#cli → LedgerFile writeHypotheses Outcomes recorded
ProjectRoot → #init detectProject Project detection input
ProjectRoot → #init options.root Project root input
#init → AgentFiles writeFileSync Agent instruction file writes
#init → ConfigFile writeFileSync Config file write
DefinitionsFile → #init definitionsArePopulated Existing declarations read to decide preservation
ConfigFile → #init configIsCustomised Existing config compared against template defaults
MCPClient → #mcp guardlink_context File path input
ThreatModel → #mcp fileContext Model grouped by location.file
ThreatModel → #mcp buildEnvelope Model content hashed for the freshness envelope
GitRepo → #mcp readFileSync HEAD read directly from .git, no subprocess
MCPClient → #mcp stdio MCP protocol transport
ConfigFile → #mcp readFileSync Annotation mode read for the initialize instructions
QueryString → #mcp lookup Query input path
MCPClient → #mcp tool_call Tool invocation input
#mcp → FileSystem writeFile Report/dashboard output
#mcp → #llm-client generateThreatReport LLM API call path
#mcp → MCPClient resource Threat model data output
GitRepo → #mcp computeBlame Attribution read from blame, log -L and commit trailers for the connected agent
ThreatModel → #mcp selectSubgraph Model filtered to a neighbourhood
FilePath → #suggest readFileSync File read path
#suggest → Suggestions suggestAnnotations Suggestion output
ThreatModel → #parser findAcceptanceDefects Acceptances read and checked against policy
ConfigFile → #parser readAcceptancePolicy Per-project policy thresholds, read only
ThreatModel → #parser computeAnnotationHash Model content read for hashing
ThreatModel → #parser detectAnnotationMode Annotation locations read to infer storage mode
MCPClient → #parser applyAnnotations Structured annotation write path
#parser → FileSystem writeFileSync Sidecar append
ThreatModel → #parser canonicalizeModelOrder Model ordered deterministically before emission
ProjectRoot → #parser fast-glob File discovery path
#parser → SourceFiles writeFile Modified file write path
ThreatModel → #parser isCovered Every unmitigated-exposure answer in the product routes through this predicate
ProjectRoot → #parser fast-glob Directory metadata scan for cache validity
SourceFile → #parser resolveGalPath Source path mapped to its annotation sidecar
LedgerFile → #parser readLedger Recorded hashes and verifiers
#cli → LedgerFile writeLedger The only write the verify surfaces perform
ThreatModel → #parser migrateAnnotationMode Model drives which files are rewritten
#parser → FileSystem writeFileSync Source rewrite and sidecar creation
FilePath → #parser readFile Disk read path
#parser → Annotations parseString Parsed annotation output
#parser → ParseDiagnostics parseString Lines that were meant to be annotations and were not read, named rather than dropped
ProjectRoot → #parser fast-glob Directory traversal path
#parser → ThreatModel assembleModel Aggregated threat model output
ThreatModel → #parser findAnchorDrift Recorded anchors compared against current source
ThreatModel → #parser classifyClaims Anchors on every relation record compared with the ledger
LedgerFile → #parser classifyClaims Recorded hashes, already read by the caller
#cli → LedgerFile applyVerification The ledger the verify surfaces write
ThreatModel → #parser findUnmitigatedPaths Reads flows, assets, mitigations and boundaries from the parsed model
FeatureName → #report filtered_by_features A @feature name written in source reaches the diagram title
FeatureName → #report filtered_by_features @feature names reach the title, every heading and the footer
ThreatModel → #report generateReport Model input
#report → Markdown return Report output
ThreatModel → #report generateSequenceDiagram Sequence diagram generation
FeatureName → #report filtered_by_features A @feature name written in source reaches the sequence diagram title
AgentProposal → #cli proposeEntitlement Agent-side proposal input
#cli → ProposalLedger writeFile Proposal artifact output
ProposalLedger → #cli readFile Proposal artifact input
#cli → SourceFiles applyProposalDecision Accepted entitlement lands as an @entitles annotation
GitConfig → #cli execFileSync Reads the local git identity to attribute a decision
ThreatModel → #cli getReviewableExposures Exposure list input
#cli → SourceFiles writeFile Annotation insertion output
#cli → SourceFiles writeFile Annotation insertion output
SourceFiles → #parser attachAnchors Annotated files re-read for structure
SourceFile → #parser parseStructure File content already read by the parser, parsed again for structure
GrammarFile → #parser Language.load Bundled WASM read from the package's grammars/ directory
UserArgs → #tui args Command argument input
#tui → FileSystem writeFile Report/config output
#tui → #agent-launcher launchAgent Agent spawn path
#tui → #llm-client chatCompletion LLM API call path
GitRepo → #tui computeBlame Attribution read from blame, log -L and commit trailers
ConfigFile → #tui loadProjectConfig Config load path
#tui → ConfigFile saveProjectConfig Config save path
UserInput → #tui readline Interactive command input
#tui → Commands dispatch Command routing
RawStdin → #tui process.stdin Raw keystroke input
#tui → Terminal process.stdout ANSI escape sequence output
PackageJson → #cli readFileSync Version string read from package.json
UserArgs → #workspace-link linkProject CLI args to workspace scaffolding
#workspace-link → AgentFiles updateAgentWorkspaceContext Inject workspace context
ReportJSON → #merge-engine mergeReports Per-repo reports feed into merge
#merge-engine → MergedReport mergeReports Unified output
GitRepo → #report-metadata execSync Git info extraction
#report-metadata → ThreatModel populateMetadata Metadata injection
Trust Boundaries9
Description
#agent-launcher ↔ AgentProcess Trust boundary at process spawn
#llm-client ↔ LLMProvider Trust boundary at external API call
#llm-client ↔ NVD Trust boundary at external API
#cli ↔ UserInput Trust boundary at CLI argument parsing
#diff ↔ GitRepo Trust boundary at git command execution
#mcp ↔ MCPClient Trust boundary at MCP protocol
#mcp ↔ MCPClient Trust boundary at tool argument parsing
#parser ↔ FileSystem Trust boundary between parser and disk I/O
#tui ↔ UserInput Trust boundary at interactive input
Data Classifications37
Description
secrets #agent-launcher Processes and stores LLM API keys
internal #agent-launcher Serializes threat model IDs and flows into prompt
internal #llm-client Processes project dependencies, env examples, code snippets
internal #llm-client Processes pentest scan output (JSON/SARIF)
secrets #llm-client Processes API keys for authentication
pii #blame Author identities attached to each record, and every identity in the history counted for the per-100-commits rates
pii #blame Every author and human co-author identity in the history, counted per person
pii #blame Identity strings printed to the terminal
pii #blame Author names, emails and dates from git log
pii #blame Author names, emails and co-author trailers of every commit in the history
pii #blame Identity strings aggregated per person
pii #blame Git author names and email addresses
secrets #cli Processes API keys via config commands
pii #cli Author identities printed or emitted as JSON; --identity hash (or blame.identity in config.json) redacts emails
pii #cli Identity strings printed to the terminal
pii #dashboard Introducer identities per asset and per period, in the configured identity mode
pii #dashboard Author identities from git, already in the configured identity mode
internal #dashboard Processes and displays threat model data
pii #dashboard Author identities from attribution, in the configured identity mode
pii #dashboard Introducer identities in the people × time heatmap
pii #dashboard Author identities rendered into a page that is often committed; blame.identity=hash in config.json is the setting for a shared dashboard
pii #dashboard Author identities from attribution, already in the configured identity mode
internal #cli Request and response evidence from scans, redacted
internal #cli Evidence strings printed to the terminal
internal #cli Evidence strings from tests and scans; scan evidence is redacted before it is written
internal #init Generates definitions and agent instruction content
internal #mcp Processes project annotations and threat model data
pii #mcp Author identities, in the configured identity mode (blame.identity in config.json), reach the MCP client
internal #parser Reads the reviewer name and justification text an acceptance carries
internal #parser Operates on project source files only
pii #report Author identities from git, in the configured identity mode
internal #cli Processes actor/capability claims, authz citations, and the name of the accepting human
internal #cli Processes exposure metadata, reviewer identity and justification text
secrets #tui Processes and stores API keys via /model
pii #tui Author identities printed to the terminal in the configured identity mode
secrets #tui API keys stored in .guardlink/config.json
secrets #tui Displays LLM config including masked API keys
Validations3
Description
#resource-limits #mcp tests/graph-sparse-repo.test.ts builds two repos differing only in unannotated file count and pins that payload size does not follow it
#regex-anchoring #parser tests/comment-forms.test.ts pins every §2.9.1 form and, negatively, that a non-comment still returns null
#input-sanitize #parser tests/comment-forms.test.ts pins that doc-tag and decorator traffic produces neither annotations nor diagnostics
Audit Items24

Each @audit marks a risk that has no control yet and needs a human decision.

Description
#agent-launcher Prompt content is opaque to agent binary; injection risk depends on agent implementation
#agent-launcher Timeout intentionally omitted for interactive sessions; inline mode has implicit control
#agent-launcher Prompt injection mitigated by agent's own safety measures; GuardLink prompt is read-only context
#agent-launcher Environment override paths are optional convenience; verify trusted local paths in CI
#llm-client Threat model data intentionally sent to LLM for analysis
#llm-client Prompt injection mitigated by LLM provider safety; local code is read-only
#sarif SARIF output intentionally reveals security findings for CI/CD integration
#cli Child process spawning delegated to agents/launcher.ts with explicit args
#diff Git commands use execSync; ref is validated with rev-parse before use
#init Config file may contain API keys; .gitignore entry added automatically
#mcp Root and SHA are disclosed deliberately: without them a client cannot tell which repo answered (D9). Client is already trusted with the full threat model.
#mcp All tool calls validated by server.ts before execution
#mcp User prompts passed to LLM; model context is read-only
#mcp Threat model data intentionally exposed to connected agents
#suggest File size is bounded by project scope; production use involves reasonable file sizes
#parser
The thresholds here are the price of an acceptance. Lowering min_justification or raising max_horizon_days in a PR is a governance change wearing a config diff, and deserves the same review as the acceptances it will admit
#parser Destructive operation requires explicit user confirmation via dryRun flag
#parser
Migration is destructive by nature — the dry-run path and the hash check are the safety net, and both should be exercised before this is trusted on a repo without clean version control
#parser Path validation delegated to callers (CLI/MCP validate root)
#cli
Every acceptance records who accepted, when, and where the annotation landed; an inert or ownership-class acceptance records the acknowledgement that was required to make it
#cli Every acceptance records who decided, why, and until when; the rule is enforced in applyReviewAction so no caller can route around it
#tui Child process spawning delegated to agents/launcher.ts
#tui User freeform text passed to LLM via cmdChat; model context is read-only
#tui API keys masked via maskKey() in banner display
Entitlements1

Capabilities a principal is claimed to hold by design. The join is (actor, asset, threat) — a row with either missing joins no finding and cannot demote one. An entitlement never suppresses a finding and never gates testing; it only changes what downstream triage recommends. A claim that cites no authorization code is inert and has no effect.

Rationale
#mcp-agent read-threat-model #mcp #data-exposure src/mcp/index.ts:23
By design: guardlink mcp exists to hand a connected coding agent the threat model — that disclosure is the product, not a leak. No privilege gain either: the agent already reads the annotated source these records are parsed from, so the assembled model tells it nothing it could not derive itself. Authorization is the channel: the server is stdio-only with no network listener, so the only client is the process the operator launched, at src/mcp/index.ts:23
Assumptions1

Unverified assumptions that should be periodically reviewed.

Assumption
#parser location.file is the logical source path in both inline and external mode (parse-file.ts resolves @source before the model is assembled)
Shielded Regions34

Code regions where annotations are intentionally suppressed via @shield.

Reason
Example annotation block for reference, excluded from parsing
No reason provided
Description examples, excluded from parsing
No reason provided
Flow examples, excluded from parsing
No reason provided
Boundary examples, excluded from parsing
No reason provided
Actor example, excluded from parsing
No reason provided
Entitlement example, excluded from parsing
No reason provided
@accepts alternative examples, excluded from parsing
No reason provided
Definition syntax examples, excluded from parsing
No reason provided
Relationship syntax examples, excluded from parsing
No reason provided
Externalized relationship examples, excluded from parsing
No reason provided
graph/index example content, excluded from parsing
No reason provided
graph/README example content, excluded from parsing
No reason provided
Placement examples for both modes; they would otherwise parse as real annotations
No reason provided
Placement examples per language; every verb line below is a template, not a claim
No reason provided
The annotation reference's own syntax block: verb lines at column 0 with no comment marker, which is what an annotation lost inside a docstring looks like
No reason provided
agentInstructions renders the Quick Syntax block: verb lines at column 0 that document the grammar rather than claim anything
No reason provided
README example annotations, excluded from parsing — they would otherwise register as real records
No reason provided
Developer Comments174
Comment
Agent binaries are hardcoded; no user-controlled binary names
parseAgentFlag extracts flags from args; no injection risk
The @entitles section instructs agents to file a proposal rather than write the annotation: the rule itself is enforced in src/review/entitlements.ts (an @entitles with no accepted proposal is a validation error), because a prompt is guidance and this claim needs a gate (actor-entitlement design §3.6)
Row counts are uncapped on purpose and the byte budget is the only ceiling: a truncated flow graph made the agent duplicate edges it was never shown, and a missing hop reads as findPath's 'no directed path' — a false clean rather than a coverage gap
The method sits above the generic rules on purpose: the agent reads it first, and the evidence bar it carries is what the gate later checks
The last block wins when a report carries more than one, because a model that restated its findings restated them last
Prompt templates are static; no user input interpolation in system prompts
customPrompt is appended to user message, not system prompt — bounded injection risk
The old `parseInt(args.max_results || '20', 10)` yielded NaN on a malformed argument, and `results.length >= NaN` is false forever — so the bound vanished and the walk covered the whole tree. Clamping is what makes the limit a limit
SARIF generation is pure transformation; no I/O in this module
File writes handled by CLI/MCP callers
Pure function: transforms ThreatModel to SARIF JSON; no I/O
runs[0].properties.annotation_hash stamps the export with the annotations it was cut from (R10), so a hygiene gate can tell a current SARIF from one built three commits ago — this file is the pentest surface, and a stale one decides which exposures get tested
@entitles has no export semantics by design: SARIF for a model with entitlements is byte-identical to one without, so an entitlement can never hide an exposure from the pentest export (actor-entitlement design §3.2)
Exposure and confirmed results carry codegraph_reachability{http_method,http_path} derived from the asset's inbound @flows route so downstream HTTP consumers (e.g. cert-x-gen) can target the endpoint; emitted verbatim from the annotation, no base path assumed
Artifacts carry a provenance header so a stale one is detectable rather than merely wrong
by-feature/*.mmd declare themselves partial in the header: a narrowed diagram that does not say it is narrowed reads as a complete one that is missing things
annotation_hash stays project-wide on per-feature files — it records the state the view was cut from, which is what keeps `validate --artifacts` a single comparison for every artifact
The field is invisible to the annotation hash (a whitelist of claim fields) and stripped from the committed model.json, so attaching it changes no tracked artifact; the same goes for blame_context, the commit counts the dashboard's rates need
Opt-in and non-mutating by default: computeBlame returns a Map; the model is byte-identical afterwards. Attribution of an AI is only ever what a commit declared
as_of is the HEAD commit's author date, never the wall clock, so every age in the summary is a function of the checkout alone
A commit credits its author and each human co-author once; a bot-authored commit with no human co-author has an agent: author and credits no person. An AI tool is credited once per commit however many trailers name it
The default ignore_revs is .git-blame-ignore-revs, the file GitHub already honours; git.ts passes it only when it exists
Every degraded status is printed next to the claim it degrades, so a reader never mistakes an unattributed claim for a clean one
A rate with no denominator (no commit counts, or an identity with no commits) prints as a dash, never as 0: an absent number must not read as a good one
Read-only by construction: no command here writes to the repository, and optional index refreshes are disabled
Reachable from HEAD only, never --all: what other branches carry is not this checkout's history
Public surface of src/blame: computeBlame (non-mutating), attachBlame (CLI --blame), the payload builders, the config reader and listCommits, the one history walk behind the commit counts
Every list is sorted (rows by introduced desc, then identity; hot files by open desc; quarters contiguous and ascending) so two runs on the same HEAD print the same bytes — the dashboard and report determinism tests depend on it
Pure over BlameEntry[] and CommitCounts: no I/O, no clock. The trend axis is bounded by the date range of the entries, everything else by their count
Declared, not detected: a commit is credited to an AI only through a matching author identity or trailer. Nothing here inspects code or guesses
Pure type declarations for the blame module; no I/O. The optional `blame` field these describe is invisible to the annotation hash and stripped from the committed model.json, exactly like `anchor`
Exit code is a pure function of (strict, exposures, confirmed, drift, demotable stale, parse errors, unqualified acceptances) and lives in the summary, so JSON consumers see the same verdict the shell got
Exposures and drift are serialized as the types the parser already produces — no renamed fields, so guardlink.ci/v1 cannot drift from the model it reports
The third check reads .guardlink/verified.json and never writes it; a corrupt ledger is reported once and treated as absent
@confirmed is not filtered by acceptance state at all: @accepts does not silence a reproduced exploit anywhere else in the product and must not do so here
--severity narrows risk findings only; drift, parse errors, stale claims and unqualified acceptances carry no severity and a severity threshold says nothing about them
Closes the same blind spot ci's third check already closed (src/ci/index.ts) for the validate/CI-gate surface
Re-locking a stale claim is an assertion that the control still holds, so the default form never does it: --stale, --all or a named target is required, and each says so in its output
Always exits 0 once the model parsed: outside a git checkout every claim reads as unattributed, which is an answer, not a failure. Only an unknown --identity is an error
Displays ledger state (absent, corrupt, or counts) computed by the caller; this function performs no I/O of its own
Prints only what the payload already holds; no I/O here
Asset details match a tile by any of its aliases (#id, dotted path, as written), case-insensitively; mitigations never count as exposures, they only change a pair's status
Ported unchanged from the first generate.ts; a file that cannot be read yields an empty context, never a failed page
Verbatim from the previous generate.ts; behaviour unchanged. Model data never reaches these strings at generation time — they read the embedded arrays at run time
One exception to 'no generation-time data': MERMAID_LIMITS is interpolated into mermaid.initialize, so the renderer is configured with the same two numbers the render budget measured against
No network: the clipboard API with a textarea fallback, history.replaceState for filters, localStorage only for theme and sidebar state
Stats mirror the model's own scope: on a --feature model these are the feature's numbers, and the dashboard suppresses the ones that only mean something project-wide
Pure projection of record.blame; the summariser is shared with the CLI and the report so every surface agrees on the numbers
Every item points somewhere: a hash route into the filtered view, a guardlink command, or both. Nothing here is a bare number
Reads .guardlink/verified.json once; the classification itself is the parser's, not re-derived here
Alias map collapses #id / name / path-joined ref forms so all three diagrams agree on identity
Pure over an already-parsed model: no file I/O, no user input, no network
A model narrowed with --feature carries filtered_by_features. The page then declares itself a slice in the title, the top bar and a banner, and suppresses the project-wide measures (file coverage, unannotated files) that a slice cannot answer
statCard keeps its exact markup: the feature-slice test greps the Open Threats tile by that string
Self-contained HTML; no external data injection after generation
Pure and deterministic: URL parsing is string work, detection is two file reads at most. Hosts are classified by hostname substring (github/gitlab/bitbucket); anything else is `other` and gets GitHub-shaped links as a best effort.
Accepts scp-style (git@host:org/repo.git), ssh://, http:// and https:// remotes. Strips `.git` and trailing slashes from the path.
Best effort and side-effect free: every failure mode (no .git, junk pointer, dangling gitdir, config without origin, local-path origin) collapses to null rather than throwing, so dashboard generation never depends on this succeeding
Pure rendering over the analytics builders; the same claim rows feed every grid so the numbers agree with the tables
Cells keep data-ff-asset (every alias of the tile, |-separated) and the asset drawer; the legend and search text are new
No wall clock: ages are measured to the HEAD commit's date, so two generations at the same HEAD are byte-identical
The withheld-on-a-slice sentence and the coverage strings are pinned by tests; file cards gain search text and a host link
Pure; every string here is raw and is escaped by the page that renders it
The empty-state sentence and its condition are unchanged (flows are not part of it) because the feature-slice test pins the sentence
Ported from the first generate.ts with the same panel ids and data-variant hooks the diagram script expects; the toolbar is a segmented zoom group plus copy-source
Panes carry .diagram-panel so the existing zoom, find and copy-source controls work unchanged; .explore-pane is what the router shows and hides
Nothing is selected in the browser: buildExploreData has already run every query at generation time, so the page holds answers rather than a query engine
Whole-project documents: --feature does not narrow them, and the page says so on a slice. With no saved report the page renders its own empty state so the copy buttons exist without the client
The Open Threats tile keeps its exact markup and label, and the coverage panel keeps .coverage-pct / .posture-fill / the 'exposures mitigated' sentence: the client feature filter rewrites those by label, and a test greps the tile
Scope wording lives in a note, not in headings: the client feature filter rewrites headings by textContent
The limits are Mermaid's shipped defaults, read out of mermaid@11.17.2's bundle and cited above — deriving them rather than guessing is the whole point of the module
Deterministic per ref: the ref's commit date and the commit count are facts of history, so two runs at the same HEAD produce the same page
Static CSS only; nothing here interpolates model data
Pure model-vs-model comparator; no I/O. Entitlement staleness is the one thing it cannot derive from the two models, so the changed-file list is passed in by the caller (see getChangedFiles in git.ts)
Pure string formatting; no I/O. Stale entitlements print even when the delta is otherwise empty — a claim whose cited authz code moved is exactly the thing that must not disappear quietly (actor-entitlement design §3.3)
Never edits an annotation's text: a rejected claim is re-prompted, then removed; rewriting would hide that it was wrong
The acceptance check behind `guardlink annotate` and the standalone `guardlink lint`
Errors are what the gate rejects; warnings are what it reports. Governance verbs (@accepts, @entitles) are errors only for claims under check, because a human may legitimately have written the others
The numbers are measured against a specific panel and font size, both cited above — deriving rather than guessing is the same discipline render-budget.ts applies to Mermaid's own limits
Pure functions over an already-parsed ThreatModel: no file I/O, no user input, no network, and the renderer is injected so this module never depends on the dashboard
Selection goes through selectSubgraph's `nodes` option rather than reimplementing the filter, so a view and an MCP graph query narrow the model the same way
Pure over the model and a ledger read; no clock, no file, no git — ranking is arithmetic on facts the model already holds
A confirmation is held to the same evidence bar the gate holds @confirmed to; a refutation needs evidence too, just not the same words, because 'the validator rejected it' is evidence of absence
Plain padded tables like printStatus; nothing here reads a file
The tested state of every exposure: a ledger of outcomes with evidence, expiry by code hash, and a ranked queue
A corrupt file is reported, never rebuilt in place: an outcome ledger that quietly emptied itself would turn refuted findings back into open ones
Detection is read-only; no file writes
Migrations should never modify existing user files. Only create missing ones.
Fail-closed: an unreadable or unparseable existing file is treated as populated, never as empty
Pure projection over the parsed model; the only I/O is an existence check performed by the caller
git SHA is read from .git/HEAD rather than spawned via execSync: this runs on every tool call
D35: the `guardlink-mcp` bin executes this module directly, so it carries a shebang and a main guard. Startup errors go to stderr — stdout is the JSON-RPC channel and a stray line there corrupts the transport.
Pure string builder; the only I/O is one optional config read by the caller
Pure function; no I/O; operates on in-memory ThreatModel
No guardlink_entitlement_accept tool exists on purpose: proposing is an agent's job, granting authority is not (actor-entitlement design §3.6)
Entitlement accepted by zippon on 2026-08-10 via guardlink entitle (proposal ent-mcp_agent.mcp.data_exposure).
Non-mutating: computeBlame leaves the cached model untouched, so a later guardlink_parse in the same session never carries blame unasked. Nothing is written to the repository
Pure function over the model; returns a ThreatModel so the existing Mermaid generator needs no changes
Skips node_modules and .guardlink directories
What config CANNOT reach is the point: scope and expiry semantics are not settings. A project can move a threshold; it cannot declare that an acceptance covers a file it was not written in, because that is what the word means rather than a preference
Pure functions apart from readAcceptancePolicy; `now` is a parameter so a test can pin the clock rather than skew it
Scope and expiry are enforced in coverage.ts, not here — this module defines them and answers questions about them, coverage applies them
acceptanceBlastRadius is read by guardlink review BEFORE the justification prompt: a reviewer typing one line sees how many exposures it removes from the gate and from the SARIF a pentest reads
Pure function; no I/O; deterministic across machines and annotation modes
Excludes line, origin_file, origin_line and parent_symbol so inline and external authoring of the same model hash identically
v2 adds @actor and @entitles records. Before that a migration could drop or rewrite every entitlement in a repo and the hash gate would still report the model unchanged — a silent all-clear, which is the failure mode the entitlement design exists to prevent (actor-entitlement design §2)
v3 adds an acceptance's accepted_by and expires. Those two fields decide whether an acceptance still covers anything, so a hash that could not see them would call a renewed or re-signed governance decision 'unchanged'
Pure function; mixed is a real answer, not an error — a repo mid-migration is genuinely both
Idempotent by construction: an identical @source block is detected and skipped rather than duplicated
Pure; returns a new model, mutates nothing. Sort is total, so the result cannot depend on input order.
Aliasing can only ever ADD coverage, so the map is built from declared assets only; an undeclared ref resolves to itself, which is the pre-D47 behaviour
Pure function over annotation text; no I/O, no filesystem check — a citation is a claim about where authz lives, and verifying the path exists is a reviewer's job (§3.4)
Pure functions over an assembled ThreatModel; no I/O
No @entitles here on purpose: #local-dev is genuinely entitled to the #arbitrary-write effect (clear exists to rewrite these files), but that pair already carries @mitigates, so it is never an open finding and the claim would demote nothing. Recorded and withdrawn in .guardlink/entitlement-proposals.json — an entitlement on a covered pair is decoration
#mcp-agent is granted nothing. It can only preview a clear (dry_run defaults to true at src/mcp/server.ts:505), and a preview writes nothing, so it answers no exposure — an entitlement that joins no finding is decoration. Nor is it entitled to clear-annotations: no code makes an MCP caller obtain the confirmation its tool description asks for, so granting it would be the over-grant §2 forbids
Widening what is consumed AFTER a recognised marker is safe; widening the marker set would not be. The openers in LINE_MARKERS are unchanged from the single-marker version, so no line that was code became a comment
Inline annotations never carry parent_symbol (parse-file.ts populates it only from @source), so this rule is inert in every inline repo — measured: 0 of 74 exposures change state on guardlink, 0 of 61 on specter-v1, 2 of 11 on expense-api
Narrowing is one-directional by construction: coversExposure starts from the (asset, threat) match the old key computed and only ever subtracts. A refactor that made it additive would be a silent-wrong-answer path in the opposite direction
Pure filtering utility; no I/O
Relations are file-scoped, definitions are reference-scoped. Filtering definitions by file emptied the asset heatmap, dropped every threat severity and rendered diagrams as bare ids — the same failure selectSubgraph avoids by always keeping the node vocabulary
Exclusion list for the annotation counter; external_refs is derived from annotations, not an annotation
Derives annotation totals from the model's own shape so a newly added collection is counted the day it lands
Filtered models must never report project-wide totals — a reader acts on the numbers beside the contents
Metadata only: path, size, mtime. File contents are never read.
Pure path arithmetic; no I/O. The inverse is exact, which is what makes migration reversible.
Restoring a prefix is inferred from context (block comment vs line comment) rather than recorded in the .gal — recording it would put presentation data in the threat model
A line lost before parseLine is a security claim that reached no threat model and produced no warning; the two comment-form diagnostics here exist so the next unread form cannot survive a release unnoticed
Regex patterns designed with bounded quantifiers and explicit structure
@entitles capability is a single-token identifier by grammar, so prose in that position is a parse error rather than a label nothing can group or compare (actor-entitlement design §3.1)
@entitles takes optional `on <asset>` and `against <threat>` clauses because the join is the (actor, asset, threat) triple, not the capability — a capability-keyed join would demote every threat on the asset including one discovered later (actor-entitlement design §9.3)
Scans standalone .gal files in addition to comment-based source annotations
Every @entitles gets its citation resolved at assembly time and carries inert:true when uncited, so no consumer can read an uncited privilege claim as an effective one by forgetting to check (actor-entitlement design §3.4)
Same reasoning for imprecise:true when the claim omits `on <asset>` or `against <threat>`, plus canEntitlementDemote/entitlementDemotionBlockers so the cited-and-precise test is written once here rather than re-derived by every consumer — a consumer that checks only !inert would demote on an entitlement that joins nothing (actor-entitlement design §9.7)
Symbol matching is deliberately loose: it looks for the name as a whole word, not for a specific declaration syntax, so it works across languages
@confirmed refs validated same as @exposes for asset and threat
findUndeclaredActors / findInertEntitlements implement the two mechanical @entitles checks from docs/prd/actor-entitlement-design.md §3.7 — both typo-class; entitlement intent is not machine-checkable
Pure function; no I/O. Demotion is a SET of claim keys handed to coverage.ts, never a rewrite of the model
Identity comes from git config or the OS user, is prefixed human: here and agent: in the MCP tool, and is recorded verbatim so a later gate can key on the prefix
Pure string formatting; no I/O. Every hop prints its file:line because a path that cannot be walked in an editor is an assertion rather than a finding, and the empty case states which kind of empty it is — no undefended route in the annotated graph is not the same claim as no undefended route in the code
Pure analyzer: no file I/O, no user input, no network. Operates only on an already-parsed ThreatModel, which is why it needs no @exposes of its own
Walks flow edges only. @boundary edges are undirected and used for crossing detection, and @transfers moves responsibility rather than data, so neither is a hop a path may take
Bodies are markdown that lands verbatim in the agent prompt and in the SKILL.md; keep each one readable on its own
Kept in one place so the lint in src/gate mirrors it rule for rule: what the prompt demands is what the gate checks
Annotate playbooks compose into buildAnnotatePrompt; report shapes into buildUserMessage; skills into guardlink init
'full' is the framework's own structure and appends nothing; the other three are audience shapes
Order matters where prompts match several playbooks: chains beats exploitable because a prompt that asks for chains also asks for exploitability, and diff beats coverage because a change is narrower than the tree
Frontmatter is the two keys every skill loader reads (name, description); the body is the playbook verbatim plus how to invoke the CLI equivalent
Pure data and pure functions: nothing here reads a file, runs a process or talks to a model
Report generation is pure transformation; no I/O in this module
File writes handled by CLI/MCP callers
@feature names are single-line by construction (parse-line.ts), so there is no newline to break the scalar; the quote is the only delimiter that needs escaping
Pure function: transforms ThreatModel to markdown string
No file I/O; caller (CLI/MCP) handles write
A model narrowed by --feature carries filtered_by_features, and every heading, caption and empty-state sentence here is scoped from it: a report that reads as project-wide while describing one feature is a wrong answer about what is and is not covered, not a cosmetic problem
Feature names are model data and land in markdown text, never in HTML or a shell — the markdown consumer escapes; the two Mermaid titles that do need escaping carry their own @mitigates
AI credit is what a commit declared in a trailer, never detected from code; a claim whose commits carry no trailer stays human. Every degraded status is listed so the reader never mistakes an unattributed claim for a clean one
Pure function: transforms ThreatModel flows to Mermaid sequence diagram
Titles itself from filtered_by_features when the model is a --feature slice: a sequence of three participants reads as the system's interactions unless the picture says which slice it is
There is no MCP accept tool and no agent-callable accept path: the only writer of an accepted entitlement is a human at `guardlink entitle`, which is why the CLI is the surface and this module refuses a decision with no name on it
Writes `against <threat>` as well as `on <asset>`, because the join is the triple (design §9.3): a claim missing either clause matches no finding and demotes nothing. Omitting the threat here made acceptance produce a DEAD annotation — the proposal ledger carried it, the written line did not, so the only sanctioned way to create an entitlement produced one that could never work. Observed on a real repository before this fix, not hypothesised.
applyReviewAction throws rather than returning a flag: a writer that can be ignored by a caller who forgot to check is the hole this replaced
Shared by exposure review (@accepts/@audit) and entitlement acceptance (@entitles) so both writers place annotations by the same rules
Pure functions over an in-memory tree; no I/O, no user input beyond the source text already read by the parser
A file that cannot be read or cannot be parsed nulls that file's anchors and warns once; it never fails the parse, because one pathological file must not take down every command in the repository
Data only: no I/O, no user input. The paths it produces are joined under the package's own grammars/ directory
Pure functions over an in-memory syntax tree or a string; no I/O
Takes content, not a path to read: the parser owns file I/O and its path validation; this layer never opens a file
Paths come only from grammarPath(language) over the package's own grammars/ directory; no caller-supplied path reaches Language.load
Non-mutating on purpose: ctx.model is reused by every later command, and /blame must not make /report carry attribution unasked
Derived query over @flows and @mitigates; every hop it prints is an existing annotation location, so a finding here cannot cite a line that was never written
Raw mode enables full keystroke control for command palette
Resolved relative to this module, so it is correct from both src/ and dist/
Workspace module: config loading, merge engine, link-project setup
Asset Risk Heatmap69 assets

Assets sorted by risk. Risk rises with unmitigated exposures: critical 3 or more open, high 2, medium 1, low exposed but covered. Click an asset for its threats, controls, flows, owners, files and who introduced its open exposures.

Legend Critical3 High0 Medium6 Low4 No exposure56
#tui
9 6 11
secretspii
#mcp
12 8 13
internalpii
#agent-launcher
10 7 12
secretsinternal
#parser
20 19 27
internal
#llm-client
10 9 13
internalsecrets
#init
6 5 6
internal
#diff
5 4 7
#sarif
1 0 2
#suggest
3 2 2
#cli
17 18 38
secretspiiinternal
#dashboard
11 26 22
piiinternal
#blame
10 10 10
pii
#gate
1 1 5
#report
0 2 6
pii
#workspace-link
0 0 2
#merge-engine
0 1 2
#report-metadata
0 0 2
#workspace-config
0 1 0
EnvVars
0 0 1
ConfigFile
0 0 9
UserPrompt
0 0 4
AgentProcess
0 0 2
ThreatModel
0 0 38
AgentPrompt
0 0 2
SavedReport
0 0 1
ProjectFiles
0 0 2
ReportFile
0 0 1
PentestFindings
0 0 1
LLMConfig
0 0 1
LLMProvider
0 0 2
LLMToolCall
0 0 1
NVD
0 0 1
SarifLog
0 0 1
FileSystem
0 0 6
GitRepo
0 0 14
CommitTrailers
0 0 1
ParseDiagnostics
0 0 2
SourceFiles
0 0 10
LedgerFile
0 0 13
UserArgs
0 0 3
UserInput
0 0 2
ScanReport
0 0 2
GitConfig
0 0 3
HTML
0 0 1
DashboardHTML
0 0 1
DiagramSource
0 0 2
GitRef
0 0 3
TempDir
0 0 1
ChangedFileList
0 0 1
ProjectRoot
0 0 5
AgentFiles
0 0 2
DefinitionsFile
0 0 1
MCPClient
0 0 5
QueryString
0 0 1
FilePath
0 0 2
Suggestions
0 0 1
SourceFile
0 0 2
Annotations
0 0 1
FeatureName
0 0 3
Markdown
0 0 1
AgentProposal
0 0 1
ProposalLedger
0 0 2
GrammarFile
0 0 1
Commands
0 0 1
RawStdin
0 0 1
Terminal
0 0 1
PackageJson
0 0 1
ReportJSON
0 0 1
MergedReport
0 0 1
Attributionas of 2026-09-12

Who introduced the code beneath each claim, who declared it, who declared its fix, and which AI tool co-authored those commits — read from git history. AI credit is declared by commit trailers (Co-Authored-By, Assisted-by), never detected from code; a commit with no trailer stays human. Click any person or tool to see exactly their claims.

Exposures over timeby quarter of the introducing commit

Each column is a quarter. The left bar is exposures whose code was introduced that quarter, split into human-only and AI-assisted commits; the right bar is exposures whose fix was declared that quarter. A rising "open at period end" means introductions outpace fixes.

Introduced (human)Introduced (AI-assisted)Fixed
2026-Q1
2026-Q2
2026-Q3
Open at period endlatest: 14 at 2026-Q3
2026-Q1
2026-Q2
2026-Q3
Human vs AI-assisted294 commits in history

AI-assisted commits are 56% of history and introduced 43% of the attributed exposures. "Per 100 commits" is the fair comparison: it divides each cohort's exposures by its own commit count.

Human-only commits

Commits in history128
Exposures introduced66
Per 100 commits51.6
Fixed53
Still open13
Median days to fix5.8

Commits whose author and trailers credit no AI tool.

AI-assisted commits

Commits in history166
Exposures introduced49
Per 100 commits29.5
Fixed48
Still open1
Median days to fix0

Commits whose author or a trailer credits an AI tool.

By person4 credited

Introduced credits the author, every human co-author and every AI tool on the commit that introduced the exposure's code. Risk score weights that person's still-open introductions by severity (critical 8, high 4, medium 2, low 1). Touched counts exposures whose span they currently own lines of, whoever introduced them. Per 100 commits divides introductions by the person's commits in history.

human:Animesh Srivastava 80
76142922535.686166412025.8
human:zippon 35
25006653979005—0
human:Jordi Murgó 0
0001036321——
human:jpmo 0
0001036321——
By AI tool5 credited

Same measures per tool and model. A tool is credited only when a commit declared it; the granularity is the commit, so a co-authored commit means the tool was involved, not that it wrote every line.

claude-code Claude Fable 5.1 27
18003871.1623477—0
claude-code Claude Opus 5 (1M context) 19
161112315.4778497330
claude-code Claude Sonnet 4.6 3
0002150453083—4.2
warp — 0
0002021291——
claude-code Claude Opus 4.8 (1M context) 0
00010143——
Files most rewritten under open exposurestop 10

Files whose open exposures have the most distinct commits still owning lines — where many hands, and possibly many tools, keep touching exposed code.

Claims234
Show Status
mitigated high
#agent-launcher#api-key-exposure
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated medium
#agent-launcher#path-traversal
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated medium
#agent-launcher#arbitrary-write
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated critical
#agent-launcher#child-proc-injection
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
open medium
#agent-launcher#prompt-injection
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
open — ok
mitigated low
#agent-launcher#dos
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
c4d87dd7 2026-08-25
human:zippon
claude-code (Claude Opus 5 (1M context))
185.3 ok
open high
#agent-launcher#prompt-injection
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
open — ok
mitigated medium
#agent-launcher#path-traversal
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
open medium
#agent-launcher#config-tamper
6cb5b7ba 2026-02-21
human:Animesh Srivastava
7d3fe7bb 2026-04-09
human:zippon
claude-code (Claude Sonnet 4.6)
open — ok
mitigated low
#agent-launcher#dos
c4d87dd7 2026-08-25
human:zippon
claude-code (Claude Opus 5 (1M context))
c4d87dd7 2026-08-25
human:zippon
claude-code (Claude Opus 5 (1M context))
c4d87dd7 2026-08-25
human:zippon
claude-code (Claude Opus 5 (1M context))
0 ok
mitigated low
#llm-client#insecure-deser
1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
0 ok
mitigated medium
#llm-client#path-traversal
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated medium
#llm-client#arbitrary-write
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
open low
#llm-client#data-exposure
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
open — ok
mitigated medium
#llm-client#ssrf
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated high
#llm-client#api-key-exposure
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
open medium
#llm-client#prompt-injection
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
open — ok
mitigated medium
#llm-client#ssrf
5851d9fa 2026-02-23
human:zippon
claude-code (Claude Sonnet 4.6)
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
4.2 ok
mitigated medium
#llm-client#path-traversal
5851d9fa 2026-02-23
human:zippon
claude-code (Claude Sonnet 4.6)
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
4.2 ok
mitigated low
#llm-client#dos
5851d9fa 2026-02-23
human:zippon
claude-code (Claude Sonnet 4.6)
ac5f5d8e 2026-08-25
human:zippon
claude-code (Claude Opus 5 (1M context))
ac5f5d8e 2026-08-25
human:zippon
claude-code (Claude Opus 5 (1M context))
183.8 ok
open low
#sarif#data-exposure
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
open — ok
mitigated medium
#dashboard#arbitrary-write
2b431366 2026-08-09
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
2b431366 2026-08-09
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
2b431366 2026-08-09
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
0 ok
mitigated low
#blame#path-traversal
c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
0 ok
mitigated low
#blame#dos
c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
ef68261c 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
0 ok
mitigated low
#blame#redos
c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
0 ok
mitigated low
#blame#path-traversal
c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
0 ok
mitigated low
#blame#cmd-injection
c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
0 ok
mitigated low
#blame#path-traversal
c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
0 ok
mitigated low
#blame#dos
c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
ef68261c 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
ef68261c 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
0 ok
mitigated low
#blame#dos
ef68261c 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
ef68261c 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
0 ok
mitigated medium
#blame#data-exposure
c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
0 ok
mitigated low
#blame#redos
c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
0 ok
mitigated high
#cli#path-traversal
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated high
#cli#arbitrary-write
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated high
#cli#api-key-exposure
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated critical
#cli#cmd-injection
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
7cbf1893 2026-08-10
human:zippon
170 ok
mitigated low
#cli#arbitrary-write
ca390f08 2026-09-03
human:zippon
claude-code (Claude Fable 5.1)
ca390f08 2026-09-03
human:zippon
claude-code (Claude Fable 5.1)
fa661d1a 2026-02-26
human:Animesh Srivastava
0 ok
mitigated medium
#dashboard#path-traversal
54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
0 ok
mitigated high
#dashboard#xss
b18619fd 2026-09-11
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
b18619fd 2026-09-11
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
fa661d1a 2026-02-26
human:Animesh Srivastava
0 ok
mitigated high
#dashboard#xss
6cb5b7ba 2026-02-21
human:Animesh Srivastava
1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated medium
#dashboard#path-traversal
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
195.6 ok
mitigated high
#dashboard#xss
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated low
#dashboard#path-traversal
54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
0 ok
mitigated low
#dashboard#data-exposure
54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
0 ok
mitigated low
#dashboard#dos
79042eae 2026-09-11
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
79042eae 2026-09-11
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
79042eae 2026-09-11
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
0 ok
mitigated low
#dashboard#cmd-injection
ca55e7e5 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
ca55e7e5 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
0 ok
mitigated high
#diff#cmd-injection
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated medium
#diff#arbitrary-write
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated medium
#diff#path-traversal
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated high
#diff#cmd-injection
7cbf1893 2026-08-10
human:zippon
7cbf1893 2026-08-10
human:zippon
fa661d1a 2026-02-26
human:Animesh Srivastava
0 ok
mitigated high
#diff#cmd-injection
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated medium
#gate#arbitrary-write
1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
0 ok
mitigated low
#dashboard#dos
b18619fd 2026-09-11
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
b18619fd 2026-09-11
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
79042eae 2026-09-11
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
0 ok
mitigated medium
#cli#arbitrary-write
497b75e4 2026-09-12
human:zippon
claude-code (Claude Fable 5.1)
497b75e4 2026-09-12
human:zippon
claude-code (Claude Fable 5.1)
fa661d1a 2026-02-26
human:Animesh Srivastava
0 ok
mitigated low
#cli#insecure-deser
497b75e4 2026-09-12
human:zippon
claude-code (Claude Fable 5.1)
497b75e4 2026-09-12
human:zippon
claude-code (Claude Fable 5.1)
7cbf1893 2026-08-10
human:zippon
0 ok
mitigated low
#cli#arbitrary-write
497b75e4 2026-09-12
human:zippon
claude-code (Claude Fable 5.1)
497b75e4 2026-09-12
human:zippon
claude-code (Claude Fable 5.1)
fa661d1a 2026-02-26
human:Animesh Srivastava
0 ok
mitigated low
#init#path-traversal
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated high
#init#arbitrary-write
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated medium
#init#path-traversal
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
open low
#init#data-exposure
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
open — ok
mitigated high
#init#arbitrary-write
6cb5b7ba 2026-02-21
human:Animesh Srivastava
7e7e0a38 2026-07-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated high
#init#arbitrary-write
35f25975 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
35f25975 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
fa661d1a 2026-02-26
human:Animesh Srivastava
0 ok
mitigated medium
#mcp#path-traversal
6c50959a 2026-08-09
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
6c50959a 2026-08-09
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
fa661d1a 2026-02-26
human:Animesh Srivastava
0 ok
mitigated low
#mcp#info-disclosure
2bf58538 2026-08-09
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
2bf58538 2026-08-09
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
a42aabe0 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
0.8 ok
open high
#mcp#cmd-injection
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
open — ok
mitigated low
#mcp#redos
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated high
#mcp#path-traversal
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated high
#mcp#arbitrary-write
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
open medium
#mcp#prompt-injection
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
open — ok
mitigated medium
#mcp#api-key-exposure
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
open medium
#mcp#data-exposure
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
open — ok
mitigated medium
#mcp#arbitrary-write
6cb5b7ba 2026-02-21
human:Animesh Srivastava
7cbf1893 2026-08-10
human:zippon
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated low
#mcp#dos
d70376ff 2026-08-09
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
d70376ff 2026-08-09
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
d70376ff 2026-08-09
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
0 ok
mitigated low
#mcp#info-disclosure
d70376ff 2026-08-09
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
a42aabe0 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
a42aabe0 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
0.8 ok
mitigated high
#suggest#path-traversal
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated medium
#suggest#redos
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
open low
#suggest#dos
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
open — ok
mitigated medium
#parser#config-tamper
0f46f416 2026-09-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
0f46f416 2026-09-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
0f46f416 2026-09-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
0 ok
mitigated low
#parser#insecure-deser
0f46f416 2026-09-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
0f46f416 2026-09-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
89cbbba6 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
0 ok
mitigated high
#parser#arbitrary-write
89cbbba6 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
89cbbba6 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
89cbbba6 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
0 ok
mitigated low
#parser#insecure-deser
89cbbba6 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
89cbbba6 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
89cbbba6 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
0 ok
mitigated high
#parser#arbitrary-write
b68ebebf 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
89cbbba6 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
164.1 ok
mitigated high
#parser#path-traversal
b68ebebf 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
0 ok
mitigated medium
#parser#redos
fe689dad 2026-09-08
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
fe689dad 2026-09-08
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
fa661d1a 2026-02-26
human:Animesh Srivastava
0 ok
mitigated low
#parser#dos
8e594d8c 2026-08-09
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
8e594d8c 2026-08-09
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
fa661d1a 2026-02-26
human:Animesh Srivastava
0 ok
mitigated low
#parser#insecure-deser
1c48ffe7 2026-09-03
human:zippon
claude-code (Claude Fable 5.1)
1c48ffe7 2026-09-03
human:zippon
claude-code (Claude Fable 5.1)
89cbbba6 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
0 ok
mitigated low
#cli#arbitrary-write
1c48ffe7 2026-09-03
human:zippon
claude-code (Claude Fable 5.1)
1c48ffe7 2026-09-03
human:zippon
claude-code (Claude Fable 5.1)
fa661d1a 2026-02-26
human:Animesh Srivastava
0 ok
mitigated high
#parser#arbitrary-write
376df260 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
376df260 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
89cbbba6 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
0 ok
open low
#parser#data-exposure
376df260 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
376df260 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
open — ok
mitigated high
#parser#path-traversal
6cb5b7ba 2026-02-21
human:Animesh Srivastava
b0ed7285 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated medium
#parser#dos
6cb5b7ba 2026-02-21
human:Animesh Srivastava
b0ed7285 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated medium
#parser#redos
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated high
#parser#path-traversal
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated medium
#parser#dos
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated low
#parser#path-traversal
9362d83d 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
9362d83d 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
fa661d1a 2026-02-26
human:Animesh Srivastava
0 ok
mitigated low
#cli#cmd-injection
136153b7 2026-09-03
human:zippon
claude-code (Claude Fable 5.1)
136153b7 2026-09-03
human:zippon
claude-code (Claude Fable 5.1)
7cbf1893 2026-08-10
human:zippon
0 ok
mitigated low
#cli#redos
1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
0 ok
mitigated high
#cli#arbitrary-write
7cbf1893 2026-08-10
human:zippon
7cbf1893 2026-08-10
human:zippon
fa661d1a 2026-02-26
human:Animesh Srivastava
0 ok
mitigated medium
#cli#insecure-deser
7cbf1893 2026-08-10
human:zippon
7cbf1893 2026-08-10
human:zippon
7cbf1893 2026-08-10
human:zippon
0 ok
mitigated high
#cli#arbitrary-write
7cbf1893 2026-08-10
human:zippon
7cbf1893 2026-08-10
human:zippon
fa661d1a 2026-02-26
human:Animesh Srivastava
0 ok
mitigated medium
#cli#arbitrary-write
fb11e3d6 2026-02-26
human:Animesh Srivastava
fb11e3d6 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
0 ok
mitigated high
#cli#arbitrary-write
fb11e3d6 2026-02-26
human:Animesh Srivastava
0f46f416 2026-09-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
fa661d1a 2026-02-26
human:Animesh Srivastava
0 ok
mitigated high
#cli#arbitrary-write
fb11e3d6 2026-02-26
human:Animesh Srivastava
7cbf1893 2026-08-10
human:zippon
fa661d1a 2026-02-26
human:Animesh Srivastava
0 ok
mitigated low
#parser#path-traversal
bb2a36ec 2026-09-03
human:zippon
claude-code (Claude Fable 5.1)
fba31f39 2026-09-04
human:zippon
claude-code (Claude Fable 5.1)
fa661d1a 2026-02-26
human:Animesh Srivastava
0 ok
mitigated low
#parser#dos
bb2a36ec 2026-09-03
human:zippon
claude-code (Claude Fable 5.1)
bb2a36ec 2026-09-03
human:zippon
claude-code (Claude Fable 5.1)
fa661d1a 2026-02-26
human:Animesh Srivastava
0 ok
mitigated low
#parser#dos
b0c7d7a5 2026-09-03
human:zippon
claude-code (Claude Fable 5.1)
b0c7d7a5 2026-09-03
human:zippon
claude-code (Claude Fable 5.1)
fa661d1a 2026-02-26
human:Animesh Srivastava
0 ok
mitigated high
#tui#path-traversal
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated high
#tui#arbitrary-write
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
open high
#tui#cmd-injection
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
open — ok
mitigated high
#tui#api-key-exposure
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
open medium
#tui#prompt-injection
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
open — ok
mitigated high
#tui#api-key-exposure
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated high
#tui#path-traversal
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated medium
#tui#api-key-exposure
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
mitigated low
#tui#dos
6cb5b7ba 2026-02-21
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
fa661d1a 2026-02-26
human:Animesh Srivastava
5.8 ok
control —
#agent-launcher#api-key-exposure
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#agent-launcher#path-traversal
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#agent-launcher#arbitrary-write
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#agent-launcher#child-proc-injection
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#agent-launcher#cmd-injection
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#agent-launcher#path-traversal
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#agent-launcher#dos
— c4d87dd7 2026-08-25
human:zippon
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#llm-client#insecure-deser
— 1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#llm-client#path-traversal
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#llm-client#arbitrary-write
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#llm-client#path-traversal
— 7d3fe7bb 2026-04-09
human:zippon
claude-code (Claude Sonnet 4.6)
— — ok
control —
#llm-client#ssrf
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#llm-client#api-key-exposure
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#llm-client#ssrf
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#llm-client#path-traversal
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#llm-client#dos
— ac5f5d8e 2026-08-25
human:zippon
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#dashboard#arbitrary-write
— 2b431366 2026-08-09
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#blame#path-traversal
— c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#blame#dos
— ef68261c 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#blame#redos
— c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#blame#path-traversal
— c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#blame#cmd-injection
— c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#blame#path-traversal
— c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#blame#dos
— ef68261c 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#blame#dos
— ef68261c 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#blame#data-exposure
— c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#blame#redos
— c88257e1 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#cli#path-traversal
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#cli#arbitrary-write
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#cli#api-key-exposure
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#cli#arbitrary-write
— fba31f39 2026-09-04
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#dashboard#path-traversal
— 54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#dashboard#xss
— 54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#dashboard#xss
— 5ca53ebf 2026-08-04
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#dashboard#xss
— b18619fd 2026-09-11
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#dashboard#xss
— 54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#dashboard#path-traversal
— 54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#dashboard#xss
— 11dae5f4 2026-08-19
human:zippon
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#dashboard#xss
— 54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#dashboard#xss
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#dashboard#path-traversal
— 54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#dashboard#data-exposure
— 54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#dashboard#path-traversal
— 54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#dashboard#xss
— d8f26406 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#dashboard#xss
— 54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#dashboard#xss
— 54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#dashboard#xss
— 54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#dashboard#xss
— 54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#dashboard#xss
— 54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#dashboard#xss
— b18619fd 2026-09-11
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#dashboard#xss
— d8f26406 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#dashboard#xss
— 54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#dashboard#xss
— 54a5c444 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#dashboard#dos
— 79042eae 2026-09-11
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#dashboard#cmd-injection
— ca55e7e5 2026-09-05
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#diff#cmd-injection
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#diff#arbitrary-write
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#diff#path-traversal
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#diff#cmd-injection
— 7cbf1893 2026-08-10
human:zippon
— — ok
control —
#gate#arbitrary-write
— 1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#dashboard#dos
— b18619fd 2026-09-11
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#cli#arbitrary-write
— 497b75e4 2026-09-12
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#cli#insecure-deser
— 497b75e4 2026-09-12
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#cli#arbitrary-write
— 497b75e4 2026-09-12
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#init#path-traversal
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#init#arbitrary-write
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#init#path-traversal
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#init#arbitrary-write
— 7e7e0a38 2026-07-21
human:Animesh Srivastava
— — ok
control —
#init#arbitrary-write
— 35f25975 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#mcp#path-traversal
— 6c50959a 2026-08-09
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#mcp#redos
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#mcp#path-traversal
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#mcp#arbitrary-write
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#mcp#api-key-exposure
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#mcp#arbitrary-write
— 7cbf1893 2026-08-10
human:zippon
— — ok
control —
#mcp#dos
— d70376ff 2026-08-09
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#mcp#info-disclosure
— a42aabe0 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#suggest#path-traversal
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#suggest#redos
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#parser#config-tamper
— 0f46f416 2026-09-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#parser#insecure-deser
— 0f46f416 2026-09-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#parser#arbitrary-write
— 89cbbba6 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#parser#insecure-deser
— 89cbbba6 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#parser#redos
— 7cbf1893 2026-08-10
human:zippon
— — ok
control —
#parser#path-traversal
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#parser#redos
— fe689dad 2026-09-08
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#parser#dos
— fe689dad 2026-09-08
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#parser#dos
— 8e594d8c 2026-08-09
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#parser#insecure-deser
— 1c48ffe7 2026-09-03
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#cli#arbitrary-write
— 1c48ffe7 2026-09-03
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#parser#arbitrary-write
— 376df260 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#parser#redos
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#parser#path-traversal
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#parser#dos
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#parser#path-traversal
— 9362d83d 2026-08-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#parser#tag-collision
— 7d3fe7bb 2026-04-09
human:zippon
claude-code (Claude Sonnet 4.6)
— — ok
control —
#cli#cmd-injection
— 136153b7 2026-09-03
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#cli#redos
— 1b6c8776 2026-09-10
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#report#xss
— 4f7e4796 2026-08-19
human:zippon
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#report#xss
— 4f7e4796 2026-08-19
human:zippon
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#cli#arbitrary-write
— 7cbf1893 2026-08-10
human:zippon
— — ok
control —
#cli#insecure-deser
— 7cbf1893 2026-08-10
human:zippon
— — ok
control —
#cli#insecure-deser
— 7cbf1893 2026-08-10
human:zippon
— — ok
control —
#cli#arbitrary-write
— 7cbf1893 2026-08-10
human:zippon
— — ok
control —
#cli#cmd-injection
— 7cbf1893 2026-08-10
human:zippon
— — ok
control —
#cli#arbitrary-write
— fb11e3d6 2026-02-26
human:Animesh Srivastava
— — ok
control —
#cli#arbitrary-write
— 0f46f416 2026-09-10
human:Animesh Srivastava
claude-code (Claude Opus 5 (1M context))
— — ok
control —
#cli#arbitrary-write
— 7cbf1893 2026-08-10
human:zippon
— — ok
control —
#parser#path-traversal
— fba31f39 2026-09-04
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#parser#dos
— bb2a36ec 2026-09-03
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#parser#dos
— b0c7d7a5 2026-09-03
human:zippon
claude-code (Claude Fable 5.1)
— — ok
control —
#tui#path-traversal
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#tui#arbitrary-write
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#tui#api-key-exposure
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#tui#api-key-exposure
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#tui#path-traversal
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#tui#dos
— fa661d1a 2026-02-26
human:Animesh Srivastava
— — ok
control —
#merge-engine#tag-collision
— 002b5863 2026-02-27
human:Animesh Srivastava
— — ok
control —
#workspace-config#config-tamper
— 002b5863 2026-02-27
human:Animesh Srivastava
— — ok

claims read from git at 2026-09-12. Identities are shown as configured by blame.identity (name, email or hash).

Details