Skip to content

release/v1.4.3 — multi-hop @flows, quoted refs, opt-in evidence redaction#8

Merged
Animesh-Sri-bugb merged 23 commits into
mainfrom
release/v1.4.3
May 13, 2026
Merged

release/v1.4.3 — multi-hop @flows, quoted refs, opt-in evidence redaction#8
Animesh-Sri-bugb merged 23 commits into
mainfrom
release/v1.4.3

Conversation

@Animesh-Sri-bugb

Copy link
Copy Markdown
Contributor

What does this PR do?

Releases v1.4.3 — the latest in the release/v1.4.X line.

Headline changes:

  • Multi-hop @flows chains@flows A -> B -> C -> D is now valid
    syntax. Parser expands chains of any length into N-1 pairwise flows
    sharing mechanism, description, and source location.
  • Quoted asset and threat refs in relationships — URL paths, names
    with whitespace, and other identifiers that weren't valid #id or
    Dotted.Path forms now work in @flows, @exposes, @confirmed,
    @boundary, etc. Example: @flows User -> "/rest/user/login" -> "SQLite db".
  • Opt-in pentest evidence redaction (guardlink config set redact-evidence true) — surgical redaction for teams whose compliance
    posture requires no cleartext credentials at rest. JWT signatures
    stripped (header+payload preserved as proof), Authorization: Basic/Digest/NTLM values fully redacted, credential field values in
    JSON / query-strings / cookies masked (field names preserved). Default
    OFF. Dashboard banner indicates active state.
  • @confirmed annotation for verified exploitable findings.
    Distinct from @exposes (theoretical). Full pipeline: parser, model
    assembly, dangling-ref validation, SARIF error-level export, CLI
    status output, dashboard emphasis, MCP guardlink_lookup "confirmed".
  • @feature file-level tagging + guardlink translate (CXG template
    generation) + guardlink ask (NL Q&A) — Shahid's v1.5.0 feature set
    from feat/v1.5.0.

Bug fixes (v1.5.1-deferred batch):

  • Pentest finding confidence renders defensively across CXG output shapes
    (integer, severity-string, missing). New formatConfidence() helper.
    (Note: CXG itself currently hardcodes confidence; a CXG-side fix
    lands separately.)
  • Topology dedupes undeclared refs across kinds — no more duplicate
    nodes when an identifier is referenced as both asset and threat.
  • Multi-hop @flows no longer rejected as malformed.
  • URL-style and whitespace-containing refs work in relationships.
  • .guardlink/prompt.md auto-migrates for v1.4.x projects on first
    guardlink report (with one-line stderr nudge; idempotent).
  • Several MCP guardlink_lookup resolver fixes (cross-kind dedup,
    no_match hint quoting, pentest template metadata regex).
  • guardlink status row labels: Files annotated / Files unannotated
    instead of Annotated / Not annotated.
  • guardlink report no longer prints misleading "Fix errors above"
    message.

Pre-release polish:

  • SARIF tool driver version bumped from a stale 1.1.0 (untouched
    since v1.2) to 1.4.3. Every SARIF emitted since v1.2 has reported
    a misleading driver version in GitHub Advanced Security.
  • CXG default-path constants in src/agents/prompts.ts now use CXG's
    canonical install layout (~/.cert-x-gen/templates/official/)
    matching PathResolver::user_template_dir() in cert-x-gen/src/ template/paths.rs, instead of a developer-specific source-repo path.
  • docs/examples/* regenerated end-to-end: threat-model.md and
    threat-dashboard.html produced fresh against the v1.4.3 codebase;
    guardlink-pentest.{json,sarif,html} scrubbed of developer-specific
    paths (40 occurrences → /path/to/guardlink-sample). Zero /Users/
    absolute paths anywhere in the repo.
  • Agent instruction files regenerated via guardlink sync against the
    merged 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:

  • fatal diagnostic tier reserved as vocabulary for v1.6. No code path
    currently emits one; non-breaking type widening with a TODO note
    enumerating audit sites.
  • New docs/handling-evidence.md — operational guide for pentest
    evidence handling (VCS ignore patterns + opt-in redaction).
  • Test coverage: 72 → 184 tests across 12 files. New: lookup.test.ts,
    pentest-loader.test.ts, format.test.ts, migrate.test.ts,
    diagnostics.test.ts, redact.test.ts. Plus the four new files from
    the v1.4.2 merge: prompts.test.ts, review.test.ts, agents.test.ts,
    diff.test.ts.

Merge structure: 22 commits ahead of main — 16 from feat/v1.5.0,
1 CHANGELOG section rename to [1.4.3], 1 merge commit resolving 14
files 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.2 came in via #6.

Type

  • Bug fix
  • New feature
  • Annotation spec change
  • Documentation
  • CI / tooling

Checklist

  • npm run build passes
  • npm test passes — 184/184 tests across 12 test files
  • guardlink validate . passes (annotations changed via guardlink sync regeneration; validated)
  • CHANGELOG.md updated — new [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/
  • @source directive — .gal annotations attribute to source file
    positions, not .gal file path ✓
  • guardlink annotate --stdout — prints prompt to stdout, no agent
    launch ✓
  • HTML / CSS comment-style annotation detection — <!-- ... --> and
    /* ... */ annotations parse correctly; guardlink review --list
    picks them up at the right source positions ✓
  • Multi-hop @flowsFlows: 2 produced from a single A -> B -> C
    annotation ✓
  • Quoted refs — @flows User -> "/api/login" -> #user-credentials
    parses without error ✓
  • @confirmed pipeline — renders as "🔴 1 confirmed exploitable
    finding(s)" in status and validate output ✓
  • @feature + guardlink feature list — roll-up per feature works ✓
  • Evidence redaction OFF (default) — no banner, full evidence visible ✓
  • Evidence redaction ON — banner appears, JWT signatures gone, api_key
    values gone, JWT decoded payload (proof of escalation) preserved ✓
  • prompt.md migration — missing file on guardlink report triggers
    stderr nudge + creates file; idempotent on second run ✓

Spec changes

This release introduces three additive annotation-language changes:

  1. @confirmed verb — new relationship verb for verified exploitable
    findings. 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 the
    regenerated agent instruction files.

  2. @feature verb — file-level tagging that groups annotations into
    logical features. Syntax: @feature "Feature Name" -- "description".
    Drives the new --feature CLI flag on report, dashboard, status,
    translate. Documented as above.

  3. Multi-hop @flows@flows A -> B -> C -> D is now valid
    syntax for chains of any length. Single-hop syntax (A -> B) unchanged
    and behaves identically. Quoted refs ("name with spaces",
    "/url/path") are accepted as chain participants in addition to
    #id and Dotted.Path forms. The model layer continues to receive
    pairwise flows — chain syntax is a parser-side expansion only.
    Definition annotations (@asset, @threat, @control) remain
    strict (no quoted refs allowed in declarations).

The fatal diagnostic level was added to the ParseDiagnostic.level
union ('error' | 'warning' | 'fatal') but is not yet emitted by any
code path. This is a non-breaking vocabulary reservation for v1.6; see
the TODO(fatal-tier) comment in src/types/index.ts for the
enumerated audit sites.

Shahid-BugB and others added 23 commits April 9, 2026 23:57
…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.
@Animesh-Sri-bugb Animesh-Sri-bugb merged commit 52d21e9 into main May 13, 2026
3 checks passed
@Animesh-Sri-bugb Animesh-Sri-bugb deleted the release/v1.4.3 branch May 13, 2026 05:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants