release/v1.4.3 — multi-hop @flows, quoted refs, opt-in evidence redaction#8
Merged
Conversation
…ded report — v1.5.0 New annotation verbs: - @confirmed: mark a threat as verified exploitable (pentest/scan evidence), distinct from @exposes (theoretical). Parser, model, SARIF error-level, CLI status, dashboard, MCP lookup, and LLM report support. - @feature "Name": tag files with a product feature name; drives --feature filtering across status, report, dashboard, and the new /feature TUI command. New commands: - guardlink translate: generate CERT-X-GEN pentest templates from threat findings (all agent backends: --claude-code, --codex, --gemini, --cursor, --windsurf, --clipboard) - guardlink ask: natural-language questions about the threat model and codebase Pentest integration: - Loads CXG scan results from .guardlink/pentest-findings/ (JSON) - Injects findings as <pentest_findings> context into threat-report and AI analyses - Dashboard gains a Pentest Findings sidebar section with scan tables and detail drawers - New PentestFinding/PentestScanResult/PentestData interfaces in src/analyze/index.ts Expanded guardlink report (10 structured sections): - Application Overview, Scope, Architecture, Key Flows & Sequence, Data Inventory, Roles & Access, Dependencies, Secrets & Credentials, Logging & Audit, AI/ML System Details (conditional) - Reads .guardlink/prompt.md for Application Overview (created by init/sync) - Confirmed findings row in Executive Summary table - GuardLink version + git commit/branch in report header New files: - src/parser/feature-filter.ts: listFeatures(), filterByFeature(), getFeatureSummaries() - src/report/sequence.ts: Mermaid sequenceDiagram generator from @flows annotations Docs: CHANGELOG.md v1.5.0 entry, README pentest integration section, @feature in annotation table + commands table, SPEC.md @feature spec, GUARDLINK_REFERENCE.md translate/ask commands. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add canonical diagram aliasing for mixed #id, bare id, name, and asset path refs - Generate structured topology data for the dashboard D3 graph - Add topology filtering, zoom, inspector details, and relationship highlighting - Improve Mermaid threat graph, data flow, and attack surface rendering - Cover normalized refs, confirmed findings, and control-to-asset protect links in tests - Refresh generated dashboard output and synced GuardLink agent instructions
…mmand The report command printed 'Fix errors above before generating report' when diagnostics contained errors, then proceeded to generate the report anyway. Per-annotation parse errors don't block report generation — malformed annotations are skipped and the rest of the model still renders. This matches the existing behavior of dashboard, sarif, threat-report, and mcp commands, which print diagnostics and continue silently. Fixes punch-list bug #5.
Three bugs in src/mcp/lookup.ts surfaced during v1.5.0 testing where two queries against the same identifier disagreed about whether it existed: - `asset #login` returned 0 results when #login was referenced via @confirmed but never declared in definitions.ts, while `threats for #login` and `confirmed` both returned the joined record. (Bug 1) - Bare `#login-sqli` returned no_match when the identifier appeared only as an asset/threat ref in exposures or confirmed and was never declared, even though `unmitigated` happily returned it. (Bug 2) - The no_match hint contained literal double quotes that got escaped twice through MCP (content wrap + JSON-RPC envelope), rendering as \\\" in clients that print the raw response. (Bug 3) Root cause for 1 and 2: the resolver only knew about declared entities (model.assets/threats/controls). Now both lookupAsset and lookupFuzzy fall back to the annotation graph (exposures, confirmed, mitigations, acceptances, audits, flows, boundaries) and synthesize stub records marked `declared: false` with a `referenced_in: [...]` audit trail. Consumers can distinguish synthesized stubs from real declarations. Bug 3 fix: the hint now uses backticks around examples instead of double quotes, so it survives both JSON.stringify passes intact. Adds tests/lookup.test.ts (14 tests, previously zero coverage on this 500-line module). Includes regression guards for the working queries (unmitigated, confirmed, features, threats for) and failing-then-fixed cases for all three bugs. Fixes punch-list bugs #1, #2, #3. Closes test-coverage gap #16.
The pentest template loader used loose regexes that matched 'id' and 'severity' as substrings inside other words: /id[:\s]*["']?([a-z0-9_-]+)["']?/i /severity[:\s]*["']?(critical|high|medium|low|info)["']?/i In a Python template containing the word 'bridge' (b-r-id-g-e), the id regex matched 'id' as a substring and captured 'ge'. In a template containing 'guide', it captured 'e'. The dashboard rendered these as the template card titles — visible as the 'ge' and 'e' labels in the Pentest Findings tab. Severity always defaulted to 'medium' because Python templates use `severity = "critical"` (equals separator) while the regex only allowed [:\s]* between the field name and the value, so the assignment form never matched. New regexes anchor on a complete field name with optional surrounding quotes (for JSON-style "id": "x") and require either : or = as the separator before a quoted value: /["']?(?:template_)?id["']?\s*[:=]\s*["']([a-z0-9_-]+)["']/i /["']?severity["']?\s*[:=]\s*["']?(critical|high|medium|low|info)["']?/i Adds tests/pentest-loader.test.ts (10 tests) covering JSON, Python, and YAML conventions plus defensive cases for substring false matches. Verified live against the user's Juice Shop session — template cards now show 'login-sqli-network' / 'login-sqli-source-audit' with 'critical' severity instead of 'ge'/'e' with 'medium'. Fixes punch-list bugs #6 and #8.
…ated' The status panel showed two adjacent rows with confusingly similar names: Annotated: 3 Annotations: 5 The first counts files that contain at least one annotation; the second counts annotations across the project. Reading the output, it was easy to misread the first as 'you have 3 annotations'. Renames the file-counting rows to 'Files annotated' / 'Files unannotated' so the relationship to 'Files scanned' above is unambiguous and the distinction from 'Annotations' below is explicit. Fixes punch-list bug #10.
…pies The v1.5.0 branch committed five generated outputs at the repo root: threat-dashboard.html (1.3MB), threat-model.md (75KB), and three guardlink-pentest.* files. Running 'guardlink dashboard .' or 'guardlink report .' from the repo root rewrites these on every invocation, producing churn in every PR that touches the tool against the project's own annotations. Moves all five files to docs/examples/ where they serve as canonical output samples — referenceable from the README and the docs site without being touched by every local run. Adds docs/examples/README.md explaining what each file is, how to regenerate them deliberately, and why they live there instead of at the repo root. Adds .gitignore entries for the root-level paths so any accidental local regeneration (the tools default to writing there) stays out of git. Fixes punch-list bug #13.
Documents the six bug fixes from the v1.5.0 testing sprint plus the internal cleanup. Each entry describes the user-visible symptom and the underlying fix so the changelog reads as a useful reference for anyone debugging similar behavior. The Internal section captures the docs/ samples relocation and the new test suites (suite size 72 → 96). Version number not bumped — this branch will be reconciled against main (currently at 1.4.2) before tagging. Fixes punch-list bug #15.
A @flows annotation may now contain more than two participants connected by ->. The parser expands the chain into N-1 pairwise flows sharing the same mechanism, description, and source location: // @flows User -> #api -> #db via HTTPS -- 'auth path' becomes { source: 'User', target: '#api', mechanism: 'HTTPS', description: 'auth path' } { source: '#api', target: '#db', mechanism: 'HTTPS', description: 'auth path' } Previously, more than one -> caused 'Malformed @flows annotation: could not parse arguments' because the regex required exactly two ASSET_REF captures separated by a single arrow. Users had to manually decompose multi-hop flows into N-1 separate annotation lines. Implementation: - src/parser/parse-line.ts: PATTERNS.flows now captures the full chain as a single group (ASSET_REF (-> ASSET_REF)+) and the parser splits it on /\\s+->\\s+/ to recover the participants array. - ParseLineResult gains an optional extraAnnotations: Annotation[] field for parser branches that emit multiple annotations from one line. New okMulti() helper wraps an array into the result shape. - src/parser/parse-file.ts: call site pushes extraAnnotations and updates lastAnnotation to the final hop so '--' continuations on the next line attach to the last emitted flow (matches existing semantics). All downstream consumers (dashboard DFD, sequence diagram generator, MCP guardlink_lookup flows queries, SARIF export) still see the pairwise flow shape they always saw — multi-hop is purely a parser-side expansion. Single-hop A -> B continues to emit exactly one flow with identical fields to before. Adds 8 tests in tests/parser.test.ts covering two/three/four-hop chains, mechanism propagation, description propagation, mixed #id refs, source-location preservation across hops, and a regression guard confirming single-hop output is unchanged. Known limitation (not addressed here): the ASSET_REF pattern still only accepts #id or Dotted.Path, so URL-style refs like /rest/user/login or whitespace-containing refs like 'SQLite db' will still fail with 'Malformed @flows annotation' even after this fix. Users should declare such targets as @asset App.Routes.Login (#login) in .guardlink/definitions.ts and reference them as #login. Extending ASSET_REF to support quoted or URL-shaped refs is a separate concern. Fixes punch-list bug #4.
…ut shapes
Pentest finding confidence renders as 'N%' literal in the dashboard
sidebar drawer and the findings table. The previous code assumed CXG
always emits confidence as an integer percentage:
'<span style="font-weight:600">' + f.confidence + '%</span>'
CXG output has actually varied across versions and template authors:
- Most current versions: integer percentage (50)
- Some templates pre-normalization: severity-style string ('high')
- Older or partial scans: missing / null entirely
When confidence was a string, the dashboard rendered 'high%'. When it
was missing, 'undefined%'. When it was an object (broken upstream),
'[object Object]%'.
Adds src/analyze/format.ts with formatConfidence(), a small pure
helper that normalizes any of the above into a renderable string:
- number -> 'N%' (clamped to [0,100], rounded)
- numeric string ('50' or '50%') -> 'N%'
- severity-word string -> uppercase ('HIGH')
- null/undefined/empty/non-renderable -> em-dash
Wires the helper into both render sites:
- src/dashboard/generate.ts server-side template (line 1742): uses
the imported helper directly.
- The browser-side openPentestDrawer JS (line 474, runs in dashboard
runtime) gets a small inline mirror function with the same logic.
Comments mark these two as needing to stay in sync.
Loosens PentestFinding.confidence type from 'number' to
'number | string | null' to match what CXG actually emits.
Adds tests/format.test.ts (9 tests) covering all input shapes
including adversarial input (Symbol, Date, Error) — formatConfidence
never throws.
What this fix does NOT do: change the user-visible '50%' that CXG
currently emits for every finding regardless of evidence quality.
That is a CXG-side bug — CXG normalizes template-emitted strings
('high', 'medium') down to a hardcoded integer and the integer is
always 50 in current builds. This GuardLink fix means the dashboard
will display the right value as soon as the CXG bug is addressed.
Track upstream CXG fix separately. Address punch-list bug #7
defensively from the GuardLink side.
Topology rendered the same identifier as two separate nodes when an undeclared ref like #login-sqli was referenced under two different kinds — typically as an asset by @exposes and as a threat by @confirmed. The user saw #login-sqli appear twice in the topology graph: once as a blue asset circle and once as a red threat circle, in different clusters of the force-directed layout. Root cause: ModelAliases.resolve(kind, ref) only consulted byRef[kind]. When the ref wasn't found there, it synthesized a fresh node tagged with the requested kind, even if the same identifier was already registered under a different kind. With nothing declared in .guardlink/definitions.ts, every ref took this synthesis path and inconsistent uses bred duplicates. Two changes: - Cross-kind fallback in resolve(): before synthesizing, scan the other two kinds' byRef maps. If the ref already exists anywhere, return that node — a single canonical identity is more useful than two duplicates the user can't tell apart visually. The kind that first encountered the ref wins, with declared assets/threats/ controls always taking priority since they're pre-registered. - New 'declared' boolean on AliasNode and DiagramTopologyNode. True for entities registered from model.assets/threats/controls. False for nodes synthesized in resolve() because no declaration existed. Once declared, never downgraded by later synthesis attempts. Consumers (the D3 renderer in a follow-up commit, MCP queries that expose topology data) can use this to visually distinguish synthesized stubs from real declarations. What this fix does NOT do: change the visual style of undeclared nodes. The data layer now carries declared:false but the dashboard's D3 styling still renders them identically to declared nodes. Visual treatment (dashed border, '?' badge) is a UI follow-up — keeping this commit focused on the data-layer correctness fix. Adds 4 tests in tests/dashboard.test.ts covering: cross-kind dedup of the bug case (single ref as both asset and threat), declared:true on all three declared entity types, declared:false on synthesized nodes, and declared kind winning when a declared entity is later referenced under a different kind by mistake. Verified live against the user's Juice Shop session: #login-sqli now produces exactly one 'asset:login-sqli' node instead of the previous asset+threat pair. Fixes punch-list bug #9.
A v1.5.0 project initialized with 'guardlink init' gets
.guardlink/prompt.md from the template. The report's Application
Overview section reads this file to render a customized project
description.
Projects upgraded from v1.4.x don't have prompt.md — 'init'
short-circuits when .guardlink/ already exists, so existing projects
got no migration. The report then silently fell back to a generic
annotation-derived overview with no signal that prompt.md was even a
feature. Documented workarounds were 'guardlink init --force' (which
clobbers other config) or 'guardlink sync' (which is undocumented as
a migration path).
This commit makes the report command auto-create prompt.md on first
run if .guardlink/ exists but the file doesn't, and prints a
one-line stderr nudge so the user discovers the feature:
• Created .guardlink/prompt.md — fill it in to customize the
report's Application Overview.
Subsequent runs are silent. Idempotent — never modifies existing
prompt.md content, never overwrites user customizations.
Implementation:
- New src/init/migrate.ts with ensurePromptMd(root) returning one of:
'exists' — file already there, no change
'created' — was missing, now written
'skipped-no-guardlink-dir' — .guardlink/ doesn't exist yet
Pure function with no logging — caller decides whether to print.
Deliberately does NOT create .guardlink/ itself; that's init's
responsibility. A truly new project still needs 'guardlink init'.
- src/cli/index.ts report command calls ensurePromptMd before reading
the file, prints the nudge once on 'created'.
Adds tests/migrate.test.ts (5 tests) covering all three outcomes,
idempotence, and the invariant that existing user content is never
overwritten.
What this fix does NOT do: refactor syncAgentFiles in
src/init/index.ts to use the same helper. syncAgentFiles has
sync-specific dryRun handling and creates .guardlink/ itself when
missing — different contract. The duplicate logic (~3 lines) is
deliberately left for a future refactor pass.
Verified live against a synthetic v1.4.x layout (.guardlink/ with
just definitions.ts): first 'guardlink report' prints the nudge and
creates prompt.md; second run is silent; report with no .guardlink/
at all does not bootstrap the directory.
Fixes punch-list bug #14.
ASSET_REF and THREAT_REF previously matched only two forms: '#id' (with characters limited to [a-zA-Z0-9_-]) and 'Dotted.Path'. Real projects often want to reference URL-style routes like /rest/user/login or human-readable identifiers with spaces like 'SQLite db' or 'User Browser'. Without a quoting mechanism, those annotations failed with 'Malformed @<verb> annotation: could not parse arguments' and forced users to either declare every reference as an @asset first or rewrite identifiers as awkward dotted paths ('/rest/user/login' as 'Rest.User.Login'). This commit adds a third alternative — double-quoted strings — to both ASSET_REF and THREAT_REF: // @flows User -> '/rest/user/login' -> 'SQLite db' // @exposes '/api/v1/users' to 'Cross Site Scripting' [high] // @confirmed 'SQL Injection' on '/login' [critical] // @boundary 'User Browser' and 'Backend API' // @Audit '/admin/dashboard' -- 'release-gated review' (Single quotes shown above for readability — the actual syntax is ASCII double-quote, mirroring the existing DESC pattern.) Implementation: - New QUOTED_REF regex fragment: '(?:[^"\\\n]|\\.)*' matching any non-newline content with backslash-escape support. - ASSET_REF and THREAT_REF gain QUOTED_REF as a third alternative alongside #id and Dotted.Path / Name. - resolveRef() (previously a no-op pass-through) now strips surrounding double quotes and processes \" / \\\ escape sequences via unescapeDescription. Single semantics for all three ref forms — quoted is just sugar for the canonical string. - Every captured asset/threat ref in the parser branches now goes through resolveRef(). Most relationship types previously assigned m[N] raw to asset/source/target slots, which would have stored literal quotes in the model. After this change they all normalize. - @flows chain extraction switches from m[1].split(/\s+->\s+/) to matchAll(new RegExp(ASSET_REF, 'g')) so a quoted ref containing a literal '->' (e.g. 'step1 -> step2') isn't shredded by the arrow separator. The outer chain regex has already validated shape, so matchAll on the inner pattern is safe. What this fix does NOT do: extend definition annotations (@asset, @threat, @control). Those use COMPONENT and NAME patterns respectively. Declarations remain strict — the canonical id system (#id) is the right way to define entities with awkward names. Quoted refs are for *referencing* informal identifiers in relationships, not for *declaring* them. Adds 11 tests in tests/parser.test.ts covering: URL-style refs in @flows, whitespace-containing refs, the user's actual Juice Shop annotation, mixed quoted+unquoted in a chain, the 'foo -> bar' chain edge case, escape sequences, quoted refs in @exposes / @confirmed / @boundary / @Audit, and a regression case confirming unquoted forms still parse identically. Verified live against a Juice Shop fixture: User -> '/rest/user/login' -> 'SQLite db' parses to two clean flows with no quotes in the stored source/target strings. Fixes punch-list bug #5.
All ParseDiagnostic records have historically carried level: 'error'
or 'warning' and the CLI treats both the same way — print and
continue. There is no notion of a diagnostic so severe that the
consumer should abort rather than render a partial threat model.
Today this is benign because the parser only ever emits per-
annotation errors, which are legitimately skip-and-continue. But the
type system has no vocabulary for cases where the whole model is
unsafe — schema version mismatch on a saved report, definitions.ts
entirely unparseable, structurally invalid input — and v1.6 work in
that direction has nowhere clean to land.
This commit adds 'fatal' to the level union as reserved vocabulary.
No code path emits a fatal diagnostic today; this is a non-breaking
type widening intended purely so v1.6 can introduce the first
emission site without a coordinated cross-file change.
Implementation:
- src/types/index.ts: ParseDiagnostic.level extends from
'error' | 'warning' to 'error' | 'warning' | 'fatal' with detailed
JSDoc explaining each tier's semantics and consumer obligation. A
TODO(fatal-tier) note enumerates the audit work required before
the first fatal emission lands: every d.level === 'error' filter
across the codebase (8 in cli/index.ts, 2 in tui/commands.ts, 1 in
mcp/server.ts) currently silently drops fatals because of how
narrowing works against the wider union. Those filters need to
become 'error' || 'fatal' before any code path produces a fatal,
otherwise fatals would bypass the existing exit-1 / abort logic.
- src/parser/format.ts (new): diagnosticIcon(level) pure helper
returning the icon character for each level. Exhaustive switch
means TypeScript will flag this function if a future iteration
adds a fourth level without updating the mapping. Icons: ✗✗ for
fatal (visually distinct from error), ✗ for error, ⚠ for warning.
- src/cli/index.ts printDiagnostics(): uses diagnosticIcon() instead
of the inline ternary, so fatals would render with the distinct
icon if any code path emitted them. Summary line gains an optional
fatals count, only printed when non-zero so today's output is
unchanged: '2 error(s), 0 warning(s)' for an error-only run, but
'1 fatal(s), 0 error(s), 0 warning(s)' if a fatal ever arrives.
- src/tui/commands.ts status command: same icon helper, wrapped in
C.error() color for fatal + error and C.warn() for warning. Two-
space indent and color treatment preserved exactly.
- tests/diagnostics.test.ts (new, 7 tests): asserts the three icons
are correct and distinct, plus three compile-time type assertions
constructing ParseDiagnostic records of each level — if the union
ever silently loses a member, this test file fails to build.
What this fix does NOT do:
- Identify or promote any existing 'error' condition to 'fatal'.
Option 1 of the v1.5.1 punch-list discussion explicitly said
vocabulary-only, no behavior change. Promoting silent-continue
conditions to fatal is a behavior change that needs more thought
than a bug-fix release warrants. The TODO note captures the audit
obligation so the v1.6 implementer can do it deliberately.
- Audit the 11 'd.level === "error"' filter sites mentioned above
to also accept fatal. They still work — fatals just get silently
dropped today, which is fine because nothing emits them. Auditing
is a v1.6 task gated on the first emission.
Verified end-to-end against the Juice Shop fixture: existing two-
error rendering is byte-identical to before the change ('✗' icon,
'2 error(s), 0 warning(s)' summary, no fatal count shown). Build
clean. 140/140 tests pass.
Addresses punch-list bug #6.
Pentest finding JSON files in .guardlink/pentest-findings/ contain live
credentials captured from successful exploits — JWTs, session cookies,
Authorization headers, password fields in request bodies. This is
intentional: confirmation evidence requires preserving what the exploit
actually returned, or the finding becomes a claim rather than proof.
Two paths to handling this safely:
1. The customer's existing VCS practices — add the directory to
.gitignore (or equivalent for hg/jj/etc.). Documented in the new
docs/handling-evidence.md. GuardLink does not auto-modify the
ignore file; the project may not be VCS-initialized, and teams
disagree on what should be checked in.
2. An opt-in 'redact-evidence' config flag for teams whose compliance
posture requires no cleartext credentials at rest. Default OFF —
OSS users running against test targets (OWASP Juice Shop, their
own staging) see full evidence and full proof. Enterprise users
with audit requirements flip one switch.
The redaction is SURGICAL, not blanket. The principle: redact what
enables replay, keep what proves the exploit. Specifically:
- JWTs (eyJhdr.eyJpld.sig): keep header.payload, replace signature
with <signature-redacted>. The decoded payload ({'role':'admin'})
is the proof of escalation — anyone with a screenshot can decode
the claims at jwt.io. Nobody can replay the token because the
signature is gone. For HMAC-signed JWTs this also removes any
brute-force-signing-key surface.
- Authorization Bearer <jwt>: same JWT split rule.
- Authorization Bearer <opaque>: first 4 + last 4 chars (fingerprint
for correlation, not enough for replay).
- Authorization Basic/Digest/NTLM: full value redacted — these ARE
the credential, no useful prefix.
- JSON credential fields (password, api_key, access_token,
refresh_token, secret, token): keep field name (structural proof
the request used a credential field), redact value.
- Query-string credentials: same — keep field name, redact value.
- Cookie / Set-Cookie: keep cookie name (proves cookie-based auth),
redact value. Set-Cookie attributes (Path, HttpOnly, Secure) are
preserved.
What redaction does NOT touch — and this matters for the product:
SQL injection payloads (the exploit input is the demonstration),
decoded JWT payload claims (the proof of what was bypassed),
response role/permission fields ({'role':'admin'}), HTTP status
codes and response shape, matched_patterns, timestamps.
Implementation:
- src/analyze/format.ts: new redactSensitiveTokens() pure helper
with six pattern passes (JWT, Bearer-opaque, Basic/Digest/NTLM,
JSON credential fields, query-string credentials, Cookie /
Set-Cookie). Idempotent — running redaction on already-redacted
output is a no-op. New EvidenceLike interface (structural shape
to avoid circular import with analyze/index.ts) and redactEvidence()
which applies the string redactor to request/response and walks
data{} recursively. deepRedact() also inspects object keys — if
a key matches a credential field name, the value is replaced
directly, catching the parsed-object form ({api_key: 'sk-live-...'})
that the string-pattern matcher cannot see.
- src/agents/config.ts: SavedConfig gains redactEvidence?: boolean.
- src/analyze/index.ts: PentestData gains redactionApplied?: boolean.
loadPentestData() reads the project config after all scans load
and, if redactEvidence is true, walks every finding and replaces
evidence with the redacted form. The JSON files on disk are NOT
modified — redaction is a read-time transform.
- src/cli/index.ts: 'guardlink config set redact-evidence <value>'
accepts true/false/on/off/1/0/yes/no. 'guardlink config show'
displays the current state with a pointer to the doc.
- src/dashboard/generate.ts: teal banner rendered at the top of the
Pentest Findings tab when pentestData.redactionApplied is true.
Tells the viewer 'JWT signatures stripped, credential values
masked. Claims and exploit payloads preserved.' so there's no
confusion about whether a screenshot shows full or redacted data.
Adds tests/redact.test.ts — 27 tests across six describe blocks
covering JWT split-redact (preserves header+payload, strips
signature, multiple JWTs, idempotence, structural validity),
Authorization headers (Bearer JWT/opaque, Basic/Digest/NTLM,
case-insensitivity), credential fields (JSON object form, six
camelCase/snake_case variants, query strings, Cookie/Set-Cookie),
safety properties (no-op for clean input, null/undefined/empty,
adversarial inputs don't throw, exploit payload preservation, role
field preservation), and redactEvidence (request/response, recursive
data walk with key inspection, no input mutation, null handling).
Adds docs/handling-evidence.md — operational guide covering what's
in pentest-findings/, recommended ignore patterns for git/hg/jj,
guidance for sharing dashboards externally, the redaction feature
with full pattern table, what redaction preserves/doesn't touch,
caveats (PII in payloads, novel token formats, performance,
runtime toggle behavior), and a quick reference.
README.md gains a short paragraph in the Pentest Integration section
pointing to the new doc.
Verified end-to-end against the Juice Shop fixture: redaction OFF
shows full admin JWT and no banner; redaction ON shows the banner,
'redactionApplied:true' in the embedded data, and per-finding
<signature-redacted> markers replacing the JWT signatures while the
decoded payloads remain intact.
Suite total: 140 -> 167 tests. Build clean.
Fixes punch-list bug #11.
Reconcile version references across the project to 1.4.3, the agreed
target for the v1.5.1-deferred bug-fix batch on the feat/v1.5.0 branch.
Touched:
- package.json: 1.4.1 -> 1.4.3
- package-lock.json: 1.4.1 -> 1.4.3 (root + packages[''])
- src/cli/index.ts: program.version('1.4.1') -> '1.4.3'
- src/mcp/server.ts: McpServer version '1.4.0' -> '1.4.3'
The MCP server was inconsistently at 1.4.0 even when other surfaces
reported 1.4.1; reconciling all four to 1.4.3 closes that gap.
Scope rationale (from the v1.5.1 discussion): the work on this branch
is materially bug-fix oriented — confidence rendering (#7), topology
dedup (#9), prompt.md migration (#14), fatal tier reservation (#6),
JWT redaction opt-in (#11) — even though two additive features
landed alongside (multi-hop @flows chains, quoted refs in #5). Patch
bump rather than minor reflects the intent: this is the v1.4.x line
plus tight fixes, not a v1.5 product cut. The minor bump and broader
release notes will happen at the rebase against main and the formal
v1.5.0 cut.
Verified: 'guardlink --version' prints 1.4.3; npm build clean;
167/167 tests pass.
Fixes punch-list bug #12.
…erred entries The work on feat/v1.5.0 ships under the v1.4.3 patch tag (package.json already bumped in d04c5aa). The CHANGELOG was still framing all of it under a hypothetical [1.5.0] section header. Rename so the document matches what's actually being released. Three new Added entries for the v1.5.1-deferred features: - Multi-hop @flows chains (A -> B -> C -> D expands to N-1 pairwise flows) - Quoted asset and threat refs in relationships (URL-style and whitespace-containing identifiers) - Opt-in pentest evidence redaction via 'guardlink config set redact-evidence true' Five new Fixed entries for the v1.5.1-deferred bug fixes: - Pentest finding confidence defensive rendering (with honest CXG-side caveat — GuardLink renders correctly when CXG ships its own fix) - Topology cross-kind dedup (no more duplicate nodes for undeclared refs) - Multi-hop @flows annotations no longer rejected - URL-style and whitespace-containing refs work in relationships - prompt.md auto-migration for v1.4.x projects on first report One new Internal entry: - 'fatal' diagnostic tier reserved as vocabulary for v1.6 (with the 11-site audit obligation explicitly flagged in the TODO note) Updated the existing test coverage line: 72 -> 167 across 8 test files, enumerating each. No code changes — pure CHANGELOG documentation. The header date (2026-04-25) reflects the work-completion date; it should be updated to the actual merge date at PR-merge time if it slips. Prep for Phase 2: create release/v1.4.3 branch from feat/v1.5.0 and merge origin/main into it.
Brings v1.4.2 work (PR #6 — externalized annotations + --stdout flag from @jordi-murgo, plus the CI Node 24 fix and README demo video section) into the feat/v1.5.0 branch that becomes v1.4.3. Conflicts resolved (14 files): Mechanical version conflicts — kept 1.4.3: - package.json - package-lock.json (two occurrences) - src/mcp/server.ts CHANGELOG.md: - Kept our [1.4.3] section intact, inserted main's [1.4.2] section between [1.4.3] and [1.4.1]. Header style normalized to escaped-bracket form for consistency with the rest of the file. Auto-generated agent instruction files (take HEAD; re-sync planned): - .github/copilot-instructions.md - .windsurfrules - AGENTS.md - CLAUDE.md - .clinerules - .cursor/rules/guardlink.mdc - .gemini/GEMINI.md - src/init/templates.ts These are generated by 'guardlink sync' from the source-of-truth prompts in src/init/templates.ts. The HEAD versions describe our v1.5.0 verb set (@confirmed, @feature, multi-hop @flows, etc.); main's versions describe the v1.4.2 vocabulary. Took HEAD because the v1.5.0 vocabulary is the superset. The external-mode content main added to these files lives in src/agents/prompts.ts and is preserved through the prompts.ts resolution below — running 'guardlink sync' post-merge regenerates the agent files with both v1.5.0 verbs and external-mode docs. True union merges (kept content from both sides): - docs/GUARDLINK_REFERENCE.md: kept main's '--mode inline|external' flag on annotate AND our translate/ask commands - src/parser/parse-line.ts: union of knownVerbs Set — 'feature' (v1.5.0) + 'source' (v1.4.2) - src/cli/index.ts: union of imports from ../agents/index.js — buildTranslatePrompt + buildAskPrompt (v1.5.0) + resolveAnnotationMode (v1.4.2) - src/agents/prompts.ts: both blocks preserved — v1.5.0's DEFAULT_CXG_ROOT / DEFAULT_CXG_SKELETON_DIR / readIfExists() helper sit before v1.4.2's AnnotationMode type and annotationModeInstructions() helper Step-numbering renumber + comment union: - src/init/index.ts: four conflict regions all stemmed from the same pattern — we added 'create prompt.md' as a new step 4 (shifting everything after by 1), while main added external-mode explanatory comments to the same step headers without changing numbers. Resolved by keeping our renumbering (4 → 8) and merging in main's external-mode explanatory comments to every affected step header. Build clean. Test suite: 184/184 passing (up from 167 — the +17 came from main's new test files: prompts.test.ts, review.test.ts, agents.test.ts, diff.test.ts, plus +4 parser tests). My conflict resolutions did not break any of main's new tests. Known issue carried forward (will be fixed in a follow-up commit on this branch before the PR opens): the DEFAULT_CXG_ROOT and DEFAULT_CXG_SKELETON_DIR constants in src/agents/prompts.ts point at a developer-specific source-repo layout that doesn't exist on end-user systems. They were already on feat/v1.5.0 so this isn't a merge regression, but the next commit on this branch replaces them with CXG's canonical installed layout (~/.cert-x-gen/templates/official) matching what 'cargo install cert-x-gen' produces.
…faults
The DEFAULT_CXG_ROOT and DEFAULT_CXG_SKELETON_DIR constants in
src/agents/prompts.ts pointed at a developer-specific source-repo
layout (~/Downloads/cert-x-gen-fix-template-update-url-migration-and-cli
with a nested cert-x-gen-templates-main/ directory). That layout
only exists on the machine where the v1.5.0 translate feature was
originally developed; on every other system 'guardlink translate'
would silently fall back to a leaner prompt because all readIfExists()
calls returned empty.
Now defaults match what 'cargo install cert-x-gen' produces and what
'cxg template fetch' writes:
~/.cert-x-gen/templates/official/ <- CXG_ROOT
├── docs/TEMPLATE_GUIDE.md <- read for prompt
└── templates/
└── skeleton/ <- CXG_SKELETON_DIR
├── python-template-skeleton.py
├── yaml-template-skeleton.yaml
└── …
This matches PathResolver::user_template_dir() in cert-x-gen/src/
template/paths.rs — CXG's authoritative path-resolution module. The
'official' subdirectory is CXG's name for the
Bugb-Technologies/cert-x-gen-templates remote; the same shape applies
for forks and additional repos under templates/.
Specific changes:
1. New 'homedir' import from node:os; DEFAULT_CXG_ROOT now derives
from resolve(homedir(), '.cert-x-gen', 'templates', 'official').
GUARDLINK_CXG_ROOT env var override is preserved for developers
who want to point at a source checkout instead.
2. Removed the 'cert-x-gen-templates-main' nesting from the
TEMPLATE_GUIDE.md path — that name was a zip-extraction artifact
from the original development environment, not part of the
actual installed layout.
3. The prompt.rs read at resolve(cxgRoot, 'src', 'ai', 'prompt.rs')
is kept but now has a comment explaining it only exists when
GUARDLINK_CXG_ROOT points at the CXG source repo (developer use).
For end users readIfExists() returns '' gracefully — the prompt
still works, they just get a slightly leaner template-authoring
guide.
4. The three 'target/release/cxg --help' invocation lines in the
generated prompt now reference bare 'cxg' since the binary is
on $PATH after 'cargo install cert-x-gen'. Users who built from
source can either install or set GUARDLINK_CXG_ROOT.
Verified on the local M5 machine: every path the new defaults
reference exists at the expected location, 'cxg' is on $PATH, the
binary is the v0.x release CXG. Build clean. 184/184 tests pass.
The SARIF tool.driver.version field in src/analyzer/sarif.ts was hardcoded as '1.1.0' and never updated through the 1.2.0, 1.3.0, 1.4.0, 1.4.1, 1.4.2, or 1.4.3 releases. Every SARIF file emitted since v1.2 has reported a stale version that no longer corresponds to the running binary. This is misleading for any consumer relying on the field — GitHub Advanced Security and similar tools surface it as 'GuardLink 1.1.0' in their UI when triaging findings. Caught during pre-merge testing on the release/v1.4.3 branch; not a regression introduced by this merge. Future improvement (deferred): wire this to package.json so it auto-stays in sync. Doing it manually for now since the bump is trivial and the ESM JSON-import dance for package.json adds more complexity than the bug warrants. Build clean. 184/184 tests pass.
Running 'guardlink sync' against the merged tree regenerates the 7 agent instruction files from src/init/templates.ts (their source of truth). During the merge resolution we took HEAD's versions of these files since they're auto-generated; this commit makes them reflect the merged template which combines: - v1.5.0 verb vocabulary (@confirmed, @feature, multi-hop @flows) from feat/v1.5.0 - v1.4.2 external-mode documentation (.gal annotation placement, @source directive guidance) from origin/main Diff is mostly mechanical: - Annotation ordering reshuffled by the model walk - One annotation position updated (src/parser/clear.ts:7 → :8) due to natural code drift - Minor trailing whitespace Verified that both feature sets are present in every regenerated file via grep before committing.
…paths
Two parts:
1. Path scrub across the pentest example files
The guardlink-pentest.{json,sarif,html} files contained 40
occurrences of '/Users/shahidhakim/Downloads/guardlink-main' in
the 'target' field of each finding — left over from when these
examples were originally generated on the v1.5.0 development
machine. The threat-dashboard.html embedded the same path via
its Pentest Findings section.
Replaced everywhere with '/path/to/guardlink-sample' — a clearly
non-real placeholder that signals 'this is example output, not
a literal path on disk'. The findings data, evidence blocks,
template IDs, severity ratings, CWE references, and remediation
guidance are preserved unchanged; only the path field is
rewritten.
2. Fresh regeneration of guardlink-produced examples
- threat-model.md regenerated via 'guardlink report .' against
the release/v1.4.3 codebase. Was previously generated on
feat/v1.5.0 at GuardLink version 1.4.1; now reflects:
* GuardLink version: 1.4.3
* Generated: 2026-05-13 (today)
* Files scanned: 67 (up from 64) | Annotations: 310
* Commit anchored to release/v1.4.3
* v1.4.3 vocabulary (@confirmed, @feature) where used in source
* Pentest data merged in from the scrubbed JSON
- threat-dashboard.html regenerated via 'guardlink dashboard .'
against the same codebase, with the scrubbed pentest JSON
temporarily staged in .guardlink/pentest-findings/ so the
dashboard's Pentest Findings tab embeds the portable path data.
Verified: zero '/Users/' absolute paths anywhere in
docs/examples/. Roundtrip checked — a sample finding id from the
JSON appears exactly once in the regenerated dashboard,
confirming the load → embed pipeline worked. 184/184 tests
still passing.
Features exercised during regeneration:
- prompt.md migration path (already-migrated; idempotent)
- @confirmed pipeline (rendered in report + dashboard)
- @feature tagging (rendered in report)
- formatConfidence helper (rendered on pentest findings)
- Topology dedup (force-directed graph in dashboard)
- Pentest Findings tab rendering with portable paths
- Multi-hop @flows / quoted refs — parser-supported but not
exercised in guardlink's own dogfood annotations (covered by
the unit suite instead)
Not exercised by this regeneration:
- Real fresh 'cxg scan' against guardlink. The pentest example
files still reflect the v1.5.0-era scan; only the paths are
updated. Producing genuinely-current pentest examples requires
the .guardlink/cxg-templates/ directory (gitignored, not in
repo) plus a real cxg run. Worth a separate sprint where we
ship both fresh cxg templates and fresh scan output.
- v1.4.2 features (Jordi Murgo's external mode + --stdout flag).
These don't appear in docs/examples/ output by design — they
write to .guardlink/annotations/ or stdout. Covered by the
v1.4.2 test files that came in via the merge
(review.test.ts, prompts.test.ts, agents.test.ts, diff.test.ts).
- Evidence redaction banner. Redaction is OFF by default and
this regeneration shipped with default state; the banner-on
case still needs a manual screenshot for documentation/marketing.
Cleanup: the temporary .guardlink/pentest-findings/ directory
created for the dashboard regeneration was removed after the
dashboard was written.
Diff: 5 files changed, 573 insertions(+), 542 deletions(-).
Larger files (dashboard, threat-model) are full regenerations;
smaller pentest files (json/sarif/html) are path-scrub-only
changes.
The [1.4.3] section was originally dated 2026-04-25 — the date the v1.5.1-deferred bug-fix batch finished and the release branch was prepared. Updating to 2026-05-13, the actual merge/release date. Per keep-a-changelog convention, the date in a CHANGELOG section is the release date, not the work-completion date.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Releases v1.4.3 — the latest in the
release/v1.4.Xline.Headline changes:
@flowschains —@flows A -> B -> C -> Dis now validsyntax. Parser expands chains of any length into N-1 pairwise flows
sharing mechanism, description, and source location.
with whitespace, and other identifiers that weren't valid
#idorDotted.Pathforms now work in@flows,@exposes,@confirmed,@boundary, etc. Example:@flows User -> "/rest/user/login" -> "SQLite db".guardlink config set redact-evidence true) — surgical redaction for teams whose complianceposture requires no cleartext credentials at rest. JWT signatures
stripped (header+payload preserved as proof),
Authorization: Basic/Digest/NTLMvalues fully redacted, credential field values inJSON / query-strings / cookies masked (field names preserved). Default
OFF. Dashboard banner indicates active state.
@confirmedannotation for verified exploitable findings.Distinct from
@exposes(theoretical). Full pipeline: parser, modelassembly, dangling-ref validation, SARIF
error-level export, CLIstatusoutput, dashboard emphasis, MCPguardlink_lookup "confirmed".@featurefile-level tagging +guardlink translate(CXG templategeneration) +
guardlink ask(NL Q&A) — Shahid's v1.5.0 feature setfrom
feat/v1.5.0.Bug fixes (v1.5.1-deferred batch):
(integer, severity-string, missing). New
formatConfidence()helper.(Note: CXG itself currently hardcodes confidence; a CXG-side fix
lands separately.)
nodes when an identifier is referenced as both asset and threat.
@flowsno longer rejected as malformed..guardlink/prompt.mdauto-migrates for v1.4.x projects on firstguardlink report(with one-line stderr nudge; idempotent).guardlink_lookupresolver fixes (cross-kind dedup,no_match hint quoting, pentest template metadata regex).
guardlink statusrow labels:Files annotated/Files unannotatedinstead of
Annotated/Not annotated.guardlink reportno longer prints misleading "Fix errors above"message.
Pre-release polish:
1.1.0(untouchedsince v1.2) to
1.4.3. Every SARIF emitted since v1.2 has reporteda misleading driver version in GitHub Advanced Security.
src/agents/prompts.tsnow use CXG'scanonical install layout (
~/.cert-x-gen/templates/official/)matching
PathResolver::user_template_dir()incert-x-gen/src/ template/paths.rs, instead of a developer-specific source-repo path.docs/examples/*regenerated end-to-end:threat-model.mdandthreat-dashboard.htmlproduced fresh against the v1.4.3 codebase;guardlink-pentest.{json,sarif,html}scrubbed of developer-specificpaths (40 occurrences →
/path/to/guardlink-sample). Zero/Users/absolute paths anywhere in the repo.
guardlink syncagainst themerged tree, so the seven generated files (
.github/copilot- instructions.md,.windsurfrules,AGENTS.md,CLAUDE.md,.clinerules,.cursor/rules/guardlink.mdc,.gemini/GEMINI.md)reflect both v1.5.0 verb vocabulary AND v1.4.2 external-mode docs.
Internal:
fataldiagnostic tier reserved as vocabulary for v1.6. No code pathcurrently emits one; non-breaking type widening with a TODO note
enumerating audit sites.
docs/handling-evidence.md— operational guide for pentestevidence handling (VCS ignore patterns + opt-in redaction).
lookup.test.ts,pentest-loader.test.ts,format.test.ts,migrate.test.ts,diagnostics.test.ts,redact.test.ts. Plus the four new files fromthe v1.4.2 merge:
prompts.test.ts,review.test.ts,agents.test.ts,diff.test.ts.Merge structure: 22 commits ahead of
main— 16 fromfeat/v1.5.0,1 CHANGELOG section rename to
[1.4.3], 1 merge commit resolving 14files of conflicts, plus 4 follow-up polish commits (CXG path fix,
SARIF version, agent file regen, docs/examples regen). Suggested merge
strategy: regular merge commit (not squash, not rebase) — matches
how
release/v1.4.2came in via #6.Type
Checklist
npm run buildpassesnpm testpasses — 184/184 tests across 12 test filesguardlink validate .passes (annotations changed viaguardlink syncregeneration; validated)[1.4.3]section with full Added /Fixed / Internal subsections covering every user-facing change.
Main's
[1.4.2]section preserved in its proper position.End-to-end verification performed:
guardlink init --mode external— zero footprint outside.guardlink/✓@sourcedirective —.galannotations attribute to source filepositions, not
.galfile path ✓guardlink annotate --stdout— prints prompt to stdout, no agentlaunch ✓
<!-- ... -->and/* ... */annotations parse correctly;guardlink review --listpicks them up at the right source positions ✓
@flows—Flows: 2produced from a singleA -> B -> Cannotation ✓
@flows User -> "/api/login" -> #user-credentialsparses without error ✓
@confirmedpipeline — renders as "🔴 1 confirmed exploitablefinding(s)" in status and validate output ✓
@feature+guardlink feature list— roll-up per feature works ✓values gone, JWT decoded payload (proof of escalation) preserved ✓
prompt.mdmigration — missing file onguardlink reporttriggersstderr nudge + creates file; idempotent on second run ✓
Spec changes
This release introduces three additive annotation-language changes:
@confirmedverb — new relationship verb for verified exploitablefindings. Distinct from
@exposes(hypothesis) and@accepts(governance acknowledgement). Syntax:
@confirmed #threat on Asset [severity] cwe:CWE-NNN -- "evidence".Documented in
docs/SPEC.md,docs/GUARDLINK_REFERENCE.md, and theregenerated agent instruction files.
@featureverb — file-level tagging that groups annotations intological features. Syntax:
@feature "Feature Name" -- "description".Drives the new
--featureCLI flag onreport,dashboard,status,translate. Documented as above.Multi-hop
@flows—@flows A -> B -> C -> Dis now validsyntax for chains of any length. Single-hop syntax (
A -> B) unchangedand behaves identically. Quoted refs (
"name with spaces","/url/path") are accepted as chain participants in addition to#idandDotted.Pathforms. The model layer continues to receivepairwise flows — chain syntax is a parser-side expansion only.
Definition annotations (
@asset,@threat,@control) remainstrict (no quoted refs allowed in declarations).
The
fataldiagnostic level was added to theParseDiagnostic.levelunion (
'error' | 'warning' | 'fatal') but is not yet emitted by anycode path. This is a non-breaking vocabulary reservation for v1.6; see
the
TODO(fatal-tier)comment insrc/types/index.tsfor theenumerated audit sites.