guardlink 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +43 -1
  3. package/dist/agents/launcher.d.ts +1 -1
  4. package/dist/agents/launcher.js +1 -1
  5. package/dist/cli/index.d.ts +2 -0
  6. package/dist/cli/index.d.ts.map +1 -1
  7. package/dist/cli/index.js +300 -54
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp/server.d.ts +1 -0
  14. package/dist/mcp/server.d.ts.map +1 -1
  15. package/dist/mcp/server.js +38 -1
  16. package/dist/mcp/server.js.map +1 -1
  17. package/dist/mcp/suggest.d.ts +1 -0
  18. package/dist/mcp/suggest.d.ts.map +1 -1
  19. package/dist/mcp/suggest.js +1 -0
  20. package/dist/mcp/suggest.js.map +1 -1
  21. package/dist/parser/parse-project.d.ts.map +1 -1
  22. package/dist/parser/parse-project.js +103 -0
  23. package/dist/parser/parse-project.js.map +1 -1
  24. package/dist/tui/commands.d.ts +3 -0
  25. package/dist/tui/commands.d.ts.map +1 -1
  26. package/dist/tui/commands.js +297 -39
  27. package/dist/tui/commands.js.map +1 -1
  28. package/dist/tui/index.d.ts.map +1 -1
  29. package/dist/tui/index.js +17 -1
  30. package/dist/tui/index.js.map +1 -1
  31. package/dist/types/index.d.ts +39 -0
  32. package/dist/types/index.d.ts.map +1 -1
  33. package/dist/workspace/index.d.ts +12 -0
  34. package/dist/workspace/index.d.ts.map +1 -0
  35. package/dist/workspace/index.js +9 -0
  36. package/dist/workspace/index.js.map +1 -0
  37. package/dist/workspace/link.d.ts +91 -0
  38. package/dist/workspace/link.d.ts.map +1 -0
  39. package/dist/workspace/link.js +581 -0
  40. package/dist/workspace/link.js.map +1 -0
  41. package/dist/workspace/merge.d.ts +104 -0
  42. package/dist/workspace/merge.d.ts.map +1 -0
  43. package/dist/workspace/merge.js +752 -0
  44. package/dist/workspace/merge.js.map +1 -0
  45. package/dist/workspace/metadata.d.ts +34 -0
  46. package/dist/workspace/metadata.d.ts.map +1 -0
  47. package/dist/workspace/metadata.js +181 -0
  48. package/dist/workspace/metadata.js.map +1 -0
  49. package/dist/workspace/types.d.ts +134 -0
  50. package/dist/workspace/types.d.ts.map +1 -0
  51. package/dist/workspace/types.js +12 -0
  52. package/dist/workspace/types.js.map +1 -0
  53. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -18,6 +18,8 @@
18
18
  * guardlink mcp Start MCP server (stdio) for Claude Code, Cursor, etc.
19
19
  * guardlink tui [dir] Interactive TUI with slash commands + AI chat
20
20
  * guardlink gal Display GAL annotation language quick reference
21
+ * guardlink link-project <repos...> Link repos into a shared workspace
22
+ * guardlink merge <files...> Merge repo reports into unified dashboard
21
23
  *
22
24
  * @exposes #cli to #path-traversal [high] cwe:CWE-22 -- "User-supplied dir argument resolved via path.resolve"
23
25
  * @mitigates #cli against #path-traversal using #path-validation -- "resolve() canonicalizes paths; cwd-relative by design"
@@ -46,6 +48,7 @@ import { generateDashboardHTML } from '../dashboard/index.js';
46
48
  import { AGENTS, agentFromOpts, launchAgent, launchAgentInline, buildAnnotatePrompt } from '../agents/index.js';
47
49
  import { resolveConfig, saveProjectConfig, saveGlobalConfig, loadProjectConfig, loadGlobalConfig, maskKey, describeConfigSource } from '../agents/config.js';
48
50
  import { getReviewableExposures, applyReviewAction, formatExposureForReview, summarizeReview } from '../review/index.js';
51
+ import { populateMetadata, mergeReports, formatMergeSummary, diffMergedReports, formatDiffSummary, linkProject, addToWorkspace, removeFromWorkspace } from '../workspace/index.js';
49
52
  import gradient from 'gradient-string';
50
53
  const program = new Command();
51
54
  const ASCII_LOGO = `
@@ -92,7 +95,7 @@ function detectProjectName(root, explicit) {
92
95
  program
93
96
  .name('guardlink')
94
97
  .description('GuardLink — Security annotations for code. Threat modeling that lives in your codebase.')
95
- .version('1.1.0')
98
+ .version('1.4.1')
96
99
  .addHelpText('before', gradient(['#00ff41', '#00d4ff'])(ASCII_LOGO));
97
100
  // ─── init ────────────────────────────────────────────────────────────
98
101
  program
@@ -266,9 +269,10 @@ program
266
269
  .description('Generate a threat model report with Mermaid diagram')
267
270
  .argument('[dir]', 'Project directory to scan', '.')
268
271
  .option('-p, --project <n>', 'Project name', 'unknown')
269
- .option('-o, --output <file>', 'Write report to file (default: threat-model.md)')
272
+ .option('-o, --output <file>', 'Write report to file')
273
+ .option('-f, --format <fmt>', 'Output format: md, json, or both (default: md)', 'md')
270
274
  .option('--diagram-only', 'Output only the Mermaid diagram, no report wrapper')
271
- .option('--json', 'Also output threat-model.json alongside the report')
275
+ .option('--json', 'Also output threat-model.json alongside the report (legacy; prefer --format)')
272
276
  .action(async (dir, opts) => {
273
277
  const root = resolve(dir);
274
278
  const { model, diagnostics } = await parseProject({ root, project: opts.project });
@@ -278,9 +282,11 @@ program
278
282
  printDiagnostics(errors);
279
283
  console.error(`Fix errors above before generating report.\n`);
280
284
  }
285
+ // Enrich with provenance metadata (git SHA, branch, workspace, schema version)
286
+ const enrichedModel = populateMetadata(model, root);
281
287
  if (opts.diagramOnly) {
282
288
  // Just output Mermaid
283
- const mermaid = generateMermaid(model);
289
+ const mermaid = generateMermaid(enrichedModel);
284
290
  if (opts.output) {
285
291
  const { writeFile } = await import('node:fs/promises');
286
292
  await writeFile(opts.output, mermaid + '\n');
@@ -289,19 +295,23 @@ program
289
295
  else {
290
296
  console.log(mermaid);
291
297
  }
298
+ return;
292
299
  }
293
- else {
294
- // Full report
295
- const report = generateReport(model);
296
- const outFile = opts.output || 'threat-model.md';
297
- const { writeFile } = await import('node:fs/promises');
298
- await writeFile(resolve(root, outFile), report + '\n');
299
- console.error(`✓ Wrote threat model report to ${outFile}`);
300
- if (opts.json) {
301
- const jsonFile = outFile.replace(/\.md$/, '.json');
302
- await writeFile(resolve(root, jsonFile), JSON.stringify(model, null, 2) + '\n');
303
- console.error(`✓ Wrote threat model JSON to ${jsonFile}`);
304
- }
300
+ const { writeFile } = await import('node:fs/promises');
301
+ const wantJson = opts.format === 'json' || opts.format === 'both' || opts.json;
302
+ const wantMd = opts.format === 'md' || opts.format === 'both' || opts.json;
303
+ if (wantMd) {
304
+ const report = generateReport(enrichedModel);
305
+ const mdFile = opts.output || (opts.format === 'md' ? 'threat-model.md' : 'threat-model.md');
306
+ await writeFile(resolve(root, mdFile), report + '\n');
307
+ console.error(`✓ Wrote threat model report to ${mdFile}`);
308
+ }
309
+ if (wantJson) {
310
+ const jsonFile = opts.output && opts.format === 'json'
311
+ ? opts.output
312
+ : (opts.output || 'threat-model').replace(/\.md$/, '') + '.json';
313
+ await writeFile(resolve(root, jsonFile), JSON.stringify(enrichedModel, null, 2) + '\n');
314
+ console.error(`✓ Wrote threat model JSON to ${jsonFile} (schema v${enrichedModel.metadata?.schema_version})`);
305
315
  }
306
316
  });
307
317
  // ─── diff ────────────────────────────────────────────────────────────
@@ -971,6 +981,224 @@ program
971
981
  console.error(`✓ Dashboard generated: ${outFile}`);
972
982
  console.error(` Open in browser to view. Toggle ☀️/🌙 for light/dark mode.`);
973
983
  });
984
+ // ─── link-project ────────────────────────────────────────────────────
985
+ program
986
+ .command('link-project')
987
+ .description('Link repos into a shared workspace for cross-repo threat modeling')
988
+ .argument('[repos...]', 'Repo directories to link (fresh setup: 2+ paths)')
989
+ .option('-w, --workspace <n>', 'Workspace name (fresh link only)', 'workspace')
990
+ .option('-r, --registry <url>', 'GitHub/GitLab org base URL (e.g. github.com/unstructured)')
991
+ .option('--add <path>', 'Add a new repo to an existing workspace (provide path to new repo)')
992
+ .option('--remove <name>', 'Remove a repo from the workspace by name')
993
+ .option('--from <path>', 'Existing workspace repo to read config from (used with --add or --remove)')
994
+ .action(async (repos, opts) => {
995
+ let result;
996
+ if (opts.remove) {
997
+ // ── Remove mode: remove a repo from an existing workspace ──
998
+ if (!opts.from) {
999
+ console.error('⚠ --remove requires --from <existing-workspace-repo-path>');
1000
+ console.error(' Example: guardlink link-project --remove payment-svc --from ./api-gateway');
1001
+ process.exit(1);
1002
+ }
1003
+ console.error(`Removing "${opts.remove}" from workspace...`);
1004
+ console.error(` Reference repo: ${opts.from}`);
1005
+ console.error('');
1006
+ result = removeFromWorkspace({
1007
+ repoName: opts.remove,
1008
+ existingRepoPath: opts.from,
1009
+ });
1010
+ for (const name of result.updated) {
1011
+ console.error(` ↻ ${name} — workspace.yaml updated`);
1012
+ }
1013
+ for (const f of result.agentFilesUpdated) {
1014
+ if (f.includes('(cleaned)'))
1015
+ console.error(` 🧹 ${f}`);
1016
+ }
1017
+ for (const s of result.skipped) {
1018
+ console.error(` ✗ ${s.name} — ${s.reason}`);
1019
+ }
1020
+ console.error('');
1021
+ if (result.updated.length > 0) {
1022
+ console.error(`✓ Removed "${opts.remove}", updated ${result.updated.length} remaining repo(s)`);
1023
+ console.error('');
1024
+ console.error('Next steps:');
1025
+ console.error(' 1. Review and commit changes in each updated repo');
1026
+ }
1027
+ else if (result.skipped.length > 0) {
1028
+ process.exit(1);
1029
+ }
1030
+ }
1031
+ else if (opts.add) {
1032
+ // ── Add mode: add a new repo to an existing workspace ──
1033
+ if (!opts.from) {
1034
+ console.error('⚠ --add requires --from <existing-workspace-repo-path>');
1035
+ console.error(' Example: guardlink link-project --add ./new-service --from ./api-gateway');
1036
+ process.exit(1);
1037
+ }
1038
+ console.error(`Adding repo to existing workspace...`);
1039
+ console.error(` New repo: ${opts.add}`);
1040
+ console.error(` From workspace: ${opts.from}`);
1041
+ console.error('');
1042
+ result = addToWorkspace({
1043
+ newRepoPath: opts.add,
1044
+ existingRepoPath: opts.from,
1045
+ registry: opts.registry,
1046
+ });
1047
+ // Report results
1048
+ for (const name of result.initialized) {
1049
+ console.error(` ⚡ ${name} — auto-initialized (no prior guardlink setup)`);
1050
+ }
1051
+ for (const name of result.linked) {
1052
+ console.error(` ✓ ${name} — linked to workspace`);
1053
+ }
1054
+ for (const name of result.updated) {
1055
+ console.error(` ↻ ${name} — workspace.yaml updated`);
1056
+ }
1057
+ for (const s of result.skipped) {
1058
+ console.error(` ✗ ${s.name} — skipped: ${s.reason}`);
1059
+ }
1060
+ console.error('');
1061
+ if (result.linked.length > 0 || result.updated.length > 0) {
1062
+ const total = result.linked.length + result.updated.length;
1063
+ console.error(`✓ ${result.linked.length} repo(s) added, ${result.updated.length} existing repo(s) updated`);
1064
+ if (result.agentFilesUpdated.length > 0) {
1065
+ console.error(` ↻ Updated ${result.agentFilesUpdated.length} agent instruction file(s)`);
1066
+ }
1067
+ // Warn about repos not found on disk
1068
+ // (they're in workspace.yaml but we couldn't locate their directory)
1069
+ console.error('');
1070
+ console.error('Next steps:');
1071
+ console.error(' 1. Review and commit changes in each updated repo');
1072
+ console.error(' 2. Add "guardlink report --format json" to the new repo\'s CI');
1073
+ console.error(' 3. Run "guardlink merge" with all repo reports');
1074
+ }
1075
+ else {
1076
+ console.error('✗ No repos were added. Check the paths provided.');
1077
+ process.exit(1);
1078
+ }
1079
+ }
1080
+ else {
1081
+ // ── Fresh link mode: link N repos together ──
1082
+ if (repos.length < 2) {
1083
+ console.error('⚠ Fresh linking requires at least 2 repo paths.');
1084
+ console.error(' To add a repo to an existing workspace, use:');
1085
+ console.error(' guardlink link-project --add <new-repo> --from <existing-repo>');
1086
+ process.exit(1);
1087
+ }
1088
+ console.error(`Linking ${repos.length} repos into workspace "${opts.workspace}"...`);
1089
+ console.error('');
1090
+ result = linkProject({
1091
+ workspace: opts.workspace,
1092
+ repoPaths: repos,
1093
+ registry: opts.registry,
1094
+ });
1095
+ for (const name of result.initialized) {
1096
+ console.error(` ⚡ ${name} — auto-initialized (no prior guardlink setup)`);
1097
+ }
1098
+ for (const name of result.linked) {
1099
+ console.error(` ✓ ${name} — workspace.yaml written, agent files updated`);
1100
+ }
1101
+ for (const s of result.skipped) {
1102
+ console.error(` ✗ ${s.name} — skipped: ${s.reason}`);
1103
+ }
1104
+ console.error('');
1105
+ if (result.linked.length > 0) {
1106
+ console.error(`✓ Linked ${result.linked.length} repo(s) into "${opts.workspace}"`);
1107
+ if (result.agentFilesUpdated.length > 0) {
1108
+ console.error(` ↻ Updated ${result.agentFilesUpdated.length} agent instruction file(s)`);
1109
+ }
1110
+ console.error('');
1111
+ console.error('Next steps:');
1112
+ console.error(' 1. Review and commit .guardlink/workspace.yaml in each repo');
1113
+ console.error(' 2. Add "guardlink report --format json -o guardlink-report.json" to each repo\'s CI');
1114
+ console.error(' 3. Use "guardlink merge" to combine reports into a unified dashboard');
1115
+ }
1116
+ else {
1117
+ console.error('✗ No repos were linked. Check the paths provided.');
1118
+ process.exit(1);
1119
+ }
1120
+ }
1121
+ });
1122
+ // ─── merge ───────────────────────────────────────────────────────────
1123
+ program
1124
+ .command('merge')
1125
+ .description('Merge multiple repo report JSONs into a unified workspace threat model')
1126
+ .argument('<files...>', 'Report JSON file paths (glob supported)')
1127
+ .option('-o, --output <file>', 'Output file for merged dashboard HTML (default: workspace-dashboard.html)')
1128
+ .option('--json <file>', 'Also write merged report JSON to this file')
1129
+ .option('--diff-against <file>', 'Compare against a previous merged JSON for weekly summary')
1130
+ .option('-w, --workspace <name>', 'Workspace name (auto-detected from reports if not set)')
1131
+ .option('--summary-only', 'Print only the text summary, skip dashboard generation')
1132
+ .action(async (files, opts) => {
1133
+ const { writeFile, readFile } = await import('node:fs/promises');
1134
+ const { resolve: resolvePath } = await import('node:path');
1135
+ // Resolve file paths (support globs via shell expansion — files already expanded by shell)
1136
+ const resolvedFiles = files.map(f => resolvePath(f));
1137
+ if (resolvedFiles.length === 0) {
1138
+ console.error('✗ No report files provided.');
1139
+ process.exit(1);
1140
+ }
1141
+ console.error(`Merging ${resolvedFiles.length} report(s)...`);
1142
+ // Run merge
1143
+ const merged = await mergeReports(resolvedFiles, {
1144
+ workspace: opts.workspace,
1145
+ });
1146
+ const t = merged.totals;
1147
+ // Print summary to stderr
1148
+ console.error('');
1149
+ console.error(`✓ ${merged.workspace} — ${t.repos_loaded}/${t.repos} repos loaded`);
1150
+ console.error(` ${t.annotations} annotations | ${t.assets} assets | ${t.threats} threats | ${t.controls} controls`);
1151
+ console.error(` ${t.mitigations} mitigations | ${t.exposures} exposures | ${t.unmitigated_exposures} unmitigated`);
1152
+ console.error(` ${t.flows} flows | ${t.external_refs_resolved} refs resolved | ${t.external_refs_unresolved} unresolved`);
1153
+ // Print warnings
1154
+ for (const w of merged.warnings) {
1155
+ const icon = w.level === 'error' ? '✗' : w.level === 'warning' ? '⚠' : 'ℹ';
1156
+ console.error(` ${icon} ${w.message}`);
1157
+ }
1158
+ console.error('');
1159
+ // Write merged JSON
1160
+ if (opts.json) {
1161
+ const jsonPath = resolvePath(opts.json);
1162
+ await writeFile(jsonPath, JSON.stringify(merged, null, 2) + '\n');
1163
+ console.error(`✓ Wrote merged JSON to ${opts.json}`);
1164
+ }
1165
+ // Diff against previous
1166
+ if (opts.diffAgainst) {
1167
+ try {
1168
+ const prevRaw = await readFile(resolvePath(opts.diffAgainst), 'utf-8');
1169
+ const previous = JSON.parse(prevRaw);
1170
+ const diff = diffMergedReports(merged, previous);
1171
+ const diffMd = formatDiffSummary(diff, merged.workspace);
1172
+ // Write diff markdown
1173
+ const diffFile = (opts.json || 'workspace-merge').replace(/\.json$/, '') + '-weekly-diff.md';
1174
+ await writeFile(resolvePath(diffFile), diffMd + '\n');
1175
+ console.error(`✓ Wrote weekly diff to ${diffFile}`);
1176
+ // Also print a compact version to stderr
1177
+ const riskIcon = diff.risk_delta === 'increased' ? '🔴'
1178
+ : diff.risk_delta === 'decreased' ? '🟢' : '⚪';
1179
+ console.error(` ${riskIcon} Risk ${diff.risk_delta} since last merge`);
1180
+ if (diff.new_unmitigated > 0)
1181
+ console.error(` 🔴 +${diff.new_unmitigated} new unmitigated exposure(s)`);
1182
+ if (diff.resolved_unmitigated > 0)
1183
+ console.error(` 🟢 ${diff.resolved_unmitigated} exposure(s) now mitigated`);
1184
+ if (diff.mitigations_removed > 0)
1185
+ console.error(` ⚠️ ${diff.mitigations_removed} mitigation(s) removed`);
1186
+ console.error('');
1187
+ }
1188
+ catch (err) {
1189
+ console.error(`⚠ Could not diff against ${opts.diffAgainst}: ${err instanceof Error ? err.message : err}`);
1190
+ }
1191
+ }
1192
+ // Generate dashboard HTML (unless --summary-only)
1193
+ if (!opts.summaryOnly) {
1194
+ const html = generateDashboardHTML(merged.model);
1195
+ const outFile = opts.output || 'workspace-dashboard.html';
1196
+ await writeFile(resolvePath(outFile), html);
1197
+ console.error(`✓ Wrote workspace dashboard to ${outFile}`);
1198
+ }
1199
+ // Print full markdown summary to stdout (pipeable)
1200
+ console.log(formatMergeSummary(merged));
1201
+ });
974
1202
  // ─── mcp ─────────────────────────────────────────────────────────────
975
1203
  program
976
1204
  .command('mcp')
@@ -1013,112 +1241,130 @@ program
1013
1241
  console.log(D(' Annotations live in source code comments. GuardLink parses'));
1014
1242
  console.log(D(' them to build a live threat model from your codebase.'));
1015
1243
  console.log('');
1016
- console.log(D(' Syntax: @verb subject [preposition object] [: description]'));
1244
+ console.log(D(' Syntax: @verb subject [preposition object] [-- "description"]'));
1017
1245
  console.log('');
1018
1246
  // ── DEFINITIONS ──
1019
1247
  console.log(H(' ── Definitions ─────────────────────────────────────────────'));
1020
1248
  console.log('');
1021
- console.log(` ${V('@asset')} ${K('<path>')} ${D('[: description]')}`);
1249
+ console.log(` ${V('@asset')} ${K('<path>')} ${D('[-- "description"]')}`);
1022
1250
  console.log(D(' Declare a named asset (component, service, data store).'));
1023
1251
  console.log(D(' Path uses dot notation for hierarchy.'));
1024
- console.log(EX(' // @asset api.auth.token_store : Stores JWT refresh tokens'));
1252
+ console.log(EX(' // @asset api.auth.token_store -- "Stores JWT refresh tokens"'));
1025
1253
  console.log(EX(' // @asset db.users'));
1026
1254
  console.log('');
1027
- console.log(` ${V('@threat')} ${K('<name>')} ${D('[severity: critical|high|medium|low] [: description]')}`);
1028
- console.log(D(' Declare a named threat. Severity aliases: P0=critical P1=high P2=medium P3=low.'));
1029
- console.log(EX(' // @threat SQL Injection severity:high : Unsanitized input reaches DB'));
1030
- console.log(EX(' // @threat Token Theft severity:P0'));
1255
+ console.log(` ${V('@threat')} ${K('<name>')} ${D('(#id)')} ${D('[critical|high|medium|low]')} ${D('[ext-refs]')} ${D('[-- "description"]')}`);
1256
+ console.log(D(' Declare a named threat. Severity in brackets: [P0]=[critical] [P1]=[high] [P2]=[medium] [P3]=[low].'));
1257
+ console.log(EX(' // @threat SQL Injection (#sql-inj) [high] cwe:CWE-89 -- "Unsanitized input reaches DB"'));
1258
+ console.log(EX(' // @threat Token Theft [P0]'));
1031
1259
  console.log('');
1032
- console.log(` ${V('@control')} ${K('<name>')} ${D('[: description]')}`);
1260
+ console.log(` ${V('@control')} ${K('<name>')} ${D('(#id)')} ${D('[-- "description"]')}`);
1033
1261
  console.log(D(' Declare a security control (mitigation mechanism).'));
1034
- console.log(EX(' // @control Input Validation : Sanitize all user-supplied strings'));
1262
+ console.log(EX(' // @control Input Validation (#input-val) -- "Sanitize all user-supplied strings"'));
1035
1263
  console.log(EX(' // @control Rate Limiting'));
1036
1264
  console.log('');
1037
1265
  // ── RELATIONSHIPS ──
1038
1266
  console.log(H(' ── Relationships ───────────────────────────────────────────'));
1039
1267
  console.log('');
1040
- console.log(` ${V('@exposes')} ${K('<asset>')} ${D('to')} ${K('<threat>')} ${D('[severity: ...] [: description]')}`);
1268
+ console.log(` ${V('@exposes')} ${K('<asset>')} ${D('to')} ${K('<threat>')} ${D('[severity]')} ${D('[ext-refs]')} ${D('[-- "description"]')}`);
1041
1269
  console.log(D(' Mark an asset as exposed to a threat at this code location.'));
1042
1270
  console.log(D(' This is the primary annotation — every exposure creates a finding.'));
1043
- console.log(EX(' // @exposes api.auth to SQL Injection severity:high'));
1044
- console.log(EX(' // @exposes db.users to Token Theft severity:critical : No token rotation'));
1271
+ console.log(EX(' // @exposes api.auth to SQL Injection [high] cwe:CWE-89'));
1272
+ console.log(EX(' // @exposes db.users to Token Theft [critical] -- "No token rotation"'));
1045
1273
  console.log('');
1046
- console.log(` ${V('@mitigates')} ${K('<asset>')} ${D('against')} ${K('<threat>')} ${D('[with')} ${K('<control>')}${D('] [: description]')}`);
1274
+ console.log(` ${V('@mitigates')} ${K('<asset>')} ${D('against')} ${K('<threat>')} ${D('[using')} ${K('<control>')}${D(']')} ${D('[-- "description"]')}`);
1047
1275
  console.log(D(' Mark that a control mitigates a threat on an asset.'));
1048
1276
  console.log(D(' Closes the exposure — removes it from open findings.'));
1049
- console.log(EX(' // @mitigates api.auth against SQL Injection with Input Validation'));
1050
- console.log(EX(' // @mitigates db.users against Token Theft : Rotation implemented in v2'));
1277
+ console.log(D(' "using" is the primary keyword; "with" also accepted.'));
1278
+ console.log(EX(' // @mitigates api.auth against SQL Injection using Input Validation'));
1279
+ console.log(EX(' // @mitigates db.users against Token Theft -- "Rotation implemented in v2"'));
1051
1280
  console.log('');
1052
- console.log(` ${V('@accepts')} ${K('<threat>')} ${D('on')} ${K('<asset>')} ${D('[: reason]')}`);
1281
+ console.log(` ${V('@accepts')} ${K('<threat>')} ${D('on')} ${K('<asset>')} ${D('[-- "reason"]')}`);
1053
1282
  console.log(D(' Explicitly accept a risk. Removes it from open findings.'));
1054
1283
  console.log(D(' Use when the risk is known and intentionally not mitigated.'));
1055
- console.log(EX(' // @accepts Timing Attack on api.auth : Acceptable for current threat model'));
1284
+ console.log(EX(' // @accepts Timing Attack on api.auth -- "Acceptable for current threat model"'));
1056
1285
  console.log('');
1057
- console.log(` ${V('@transfers')} ${K('<threat>')} ${D('from')} ${K('<source>')} ${D('to')} ${K('<target>')} ${D('[: description]')}`);
1286
+ console.log(` ${V('@transfers')} ${K('<threat>')} ${D('from')} ${K('<source>')} ${D('to')} ${K('<target>')} ${D('[-- "description"]')}`);
1058
1287
  console.log(D(' Transfer responsibility for a threat to another asset/team.'));
1059
- console.log(EX(' // @transfers DDoS from api.gateway to cdn.cloudflare : Handled by CDN layer'));
1288
+ console.log(EX(' // @transfers DDoS from api.gateway to cdn.cloudflare -- "Handled by CDN layer"'));
1060
1289
  console.log('');
1061
1290
  // ── DATA FLOWS ──
1062
1291
  console.log(H(' ── Data Flows & Boundaries ─────────────────────────────────'));
1063
1292
  console.log('');
1064
- console.log(` ${V('@flows')} ${K('<source>')} ${D('to')} ${K('<target>')} ${D('[via')} ${K('<mechanism>')}${D('] [: description]')}`);
1293
+ console.log(` ${V('@flows')} ${K('<source>')} ${D('->')} ${K('<target>')} ${D('[via')} ${K('<mechanism>')}${D(']')} ${D('[-- "description"]')}`);
1065
1294
  console.log(D(' Document data movement between components.'));
1066
1295
  console.log(D(' Appears in the Data Flow Diagram.'));
1067
- console.log(EX(' // @flows api.auth to db.users via TLS 1.3'));
1068
- console.log(EX(' // @flows mobile.app to api.gateway via HTTPS : User credentials'));
1296
+ console.log(EX(' // @flows api.auth -> db.users via TLS 1.3'));
1297
+ console.log(EX(' // @flows mobile.app -> api.gateway via HTTPS -- "User credentials"'));
1069
1298
  console.log('');
1070
- console.log(` ${V('@boundary')} ${K('<asset_a>')} ${D('and')} ${K('<asset_b>')} ${D('[: description]')}`);
1299
+ console.log(` ${V('@boundary')} ${K('<asset_a>')} ${D('and')} ${K('<asset_b>')} ${D('(#id)')} ${D('[-- "description"]')}`);
1071
1300
  console.log(D(' Declare a trust boundary between two assets.'));
1072
1301
  console.log(D(' Groups assets in the Data Flow Diagram.'));
1073
- console.log(EX(' // @boundary internet and api.gateway : Public-facing edge'));
1074
- console.log(EX(' // @boundary api.gateway and db.users : Internal network boundary'));
1302
+ console.log(D(' Alternate: @boundary between A and B or @boundary A | B'));
1303
+ console.log(EX(' // @boundary internet and api.gateway (#edge) -- "Public-facing edge"'));
1304
+ console.log(EX(' // @boundary api.gateway | db.users -- "Internal network boundary"'));
1075
1305
  console.log('');
1076
1306
  // ── LIFECYCLE ──
1077
1307
  console.log(H(' ── Lifecycle & Governance ──────────────────────────────────'));
1078
1308
  console.log('');
1079
- console.log(` ${V('@handles')} ${K('<classification>')} ${D('on')} ${K('<asset>')} ${D('[: description]')}`);
1309
+ console.log(` ${V('@handles')} ${K('<classification>')} ${D('on')} ${K('<asset>')} ${D('[-- "description"]')}`);
1080
1310
  console.log(D(' Declare data classification handled by an asset.'));
1081
1311
  console.log(D(' Classifications: pii phi financial secrets internal public'));
1082
- console.log(EX(' // @handles pii on db.users : Stores name, email, phone'));
1312
+ console.log(EX(' // @handles pii on db.users -- "Stores name, email, phone"'));
1083
1313
  console.log(EX(' // @handles secrets on api.auth.token_store'));
1084
1314
  console.log('');
1085
- console.log(` ${V('@owns')} ${K('<owner>')} ${K('<asset>')} ${D('[: description]')}`);
1315
+ console.log(` ${V('@owns')} ${K('<owner>')} ${D('for')} ${K('<asset>')} ${D('[-- "description"]')}`);
1086
1316
  console.log(D(' Assign ownership of an asset to a team or person.'));
1087
- console.log(EX(' // @owns platform-team api.auth'));
1317
+ console.log(EX(' // @owns platform-team for api.auth'));
1088
1318
  console.log('');
1089
- console.log(` ${V('@validates')} ${K('<control>')} ${D('on')} ${K('<asset>')} ${D('[: description]')}`);
1319
+ console.log(` ${V('@validates')} ${K('<control>')} ${D('for')} ${K('<asset>')} ${D('[-- "description"]')}`);
1090
1320
  console.log(D(' Assert that a control has been validated/tested on an asset.'));
1091
- console.log(EX(' // @validates Input Validation on api.auth : Pen-tested 2024-Q3'));
1321
+ console.log(EX(' // @validates Input Validation for api.auth -- "Pen-tested 2024-Q3"'));
1092
1322
  console.log('');
1093
- console.log(` ${V('@audit')} ${K('<asset>')} ${D('[: description]')}`);
1323
+ console.log(` ${V('@audit')} ${K('<asset>')} ${D('[-- "description"]')}`);
1094
1324
  console.log(D(' Mark that this code path is an audit trail point.'));
1095
- console.log(EX(' // @audit db.users : All writes logged to audit_log table'));
1325
+ console.log(EX(' // @audit db.users -- "All writes logged to audit_log table"'));
1096
1326
  console.log('');
1097
- console.log(` ${V('@assumes')} ${K('<asset>')} ${D('[: description]')}`);
1327
+ console.log(` ${V('@assumes')} ${K('<asset>')} ${D('[-- "description"]')}`);
1098
1328
  console.log(D(' Document a security assumption about an asset.'));
1099
- console.log(EX(' // @assumes api.gateway : Upstream WAF filters malformed requests'));
1329
+ console.log(EX(' // @assumes api.gateway -- "Upstream WAF filters malformed requests"'));
1100
1330
  console.log('');
1101
- console.log(` ${V('@comment')} ${D('[: description]')}`);
1331
+ console.log(` ${V('@comment')} ${D('[-- "description"]')}`);
1102
1332
  console.log(D(' Free-form developer security note (no structural effect).'));
1103
- console.log(EX(' // @comment : TODO — add rate limiting before v2 launch'));
1333
+ console.log(EX(' // @comment -- "TODO — add rate limiting before v2 launch"'));
1104
1334
  console.log('');
1105
1335
  // ── SHIELD BLOCKS ──
1106
1336
  console.log(H(' ── Shield Blocks ───────────────────────────────────────────'));
1107
1337
  console.log('');
1338
+ console.log(` ${V('@shield')} ${D('[-- "reason"]')}`);
1339
+ console.log(D(' Single-line marker for a security-sensitive code point.'));
1340
+ console.log(EX(' // @shield -- "Crypto key derivation — do not refactor without review"'));
1341
+ console.log('');
1108
1342
  console.log(` ${V('@shield:begin')} ${D('/')} ${V('@shield:end')}`);
1109
1343
  console.log(D(' Wrap a code block to mark it as security-sensitive.'));
1110
1344
  console.log(D(' GuardLink will flag unannotated symbols inside the block.'));
1111
- console.log(EX(' // @shield:begin'));
1345
+ console.log(EX(' // @shield:begin -- "Auth verification block"'));
1112
1346
  console.log(EX(' function verifyToken(token: string) { ... }'));
1113
1347
  console.log(EX(' // @shield:end'));
1114
1348
  console.log('');
1349
+ // ── EXTERNAL REFERENCES ──
1350
+ console.log(H(' ── External References ─────────────────────────────────────'));
1351
+ console.log('');
1352
+ console.log(D(' Append space-separated refs after severity on @threat and @exposes:'));
1353
+ console.log(EX(' cwe:CWE-89 owasp:A03:2021 capec:CAPEC-66 attack:T1190'));
1354
+ console.log('');
1355
+ console.log(D(' Example:'));
1356
+ console.log(EX(' // @exposes api.auth to SQL Injection [high] cwe:CWE-89 owasp:A03:2021'));
1357
+ console.log('');
1115
1358
  // ── TIPS ──
1116
1359
  console.log(H(' ── Tips ────────────────────────────────────────────────────'));
1117
1360
  console.log('');
1361
+ console.log(D(' • Descriptions use -- "quoted text" format (not : colon)'));
1362
+ console.log(D(' • Severity uses brackets: [critical] [high] [medium] [low] or [P0]-[P3]'));
1118
1363
  console.log(D(' • Annotations work in any comment style: // /* # -- <!-- -->'));
1119
1364
  console.log(D(' • Place annotations on the line ABOVE the code they describe'));
1120
1365
  console.log(D(' • Asset names are case-insensitive and normalized (spaces→underscores)'));
1121
1366
  console.log(D(' • Threat/control names can reference IDs with #id syntax'));
1367
+ console.log(D(' • @flows uses -> arrow syntax (not "to")'));
1122
1368
  console.log(D(' • Run guardlink parse after adding annotations to update the threat model'));
1123
1369
  console.log(D(' • Run guardlink validate to check for syntax errors and dangling references'));
1124
1370
  console.log(D(' • Run guardlink annotate to have an AI agent add annotations automatically'));