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.
- package/CHANGELOG.md +44 -0
- package/README.md +43 -1
- package/dist/agents/launcher.d.ts +1 -1
- package/dist/agents/launcher.js +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +300 -54
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +38 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/suggest.d.ts +1 -0
- package/dist/mcp/suggest.d.ts.map +1 -1
- package/dist/mcp/suggest.js +1 -0
- package/dist/mcp/suggest.js.map +1 -1
- package/dist/parser/parse-project.d.ts.map +1 -1
- package/dist/parser/parse-project.js +103 -0
- package/dist/parser/parse-project.js.map +1 -1
- package/dist/tui/commands.d.ts +3 -0
- package/dist/tui/commands.d.ts.map +1 -1
- package/dist/tui/commands.js +297 -39
- package/dist/tui/commands.js.map +1 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +17 -1
- package/dist/tui/index.js.map +1 -1
- package/dist/types/index.d.ts +39 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/workspace/index.d.ts +12 -0
- package/dist/workspace/index.d.ts.map +1 -0
- package/dist/workspace/index.js +9 -0
- package/dist/workspace/index.js.map +1 -0
- package/dist/workspace/link.d.ts +91 -0
- package/dist/workspace/link.d.ts.map +1 -0
- package/dist/workspace/link.js +581 -0
- package/dist/workspace/link.js.map +1 -0
- package/dist/workspace/merge.d.ts +104 -0
- package/dist/workspace/merge.d.ts.map +1 -0
- package/dist/workspace/merge.js +752 -0
- package/dist/workspace/merge.js.map +1 -0
- package/dist/workspace/metadata.d.ts +34 -0
- package/dist/workspace/metadata.d.ts.map +1 -0
- package/dist/workspace/metadata.js +181 -0
- package/dist/workspace/metadata.js.map +1 -0
- package/dist/workspace/types.d.ts +134 -0
- package/dist/workspace/types.d.ts.map +1 -0
- package/dist/workspace/types.js +12 -0
- package/dist/workspace/types.js.map +1 -0
- 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
|
|
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
|
|
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(
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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] [
|
|
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('[
|
|
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
|
|
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('[
|
|
1028
|
-
console.log(D(' Declare a named threat. Severity
|
|
1029
|
-
console.log(EX(' // @threat SQL Injection
|
|
1030
|
-
console.log(EX(' // @threat Token Theft
|
|
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('[
|
|
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
|
|
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
|
|
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
|
|
1044
|
-
console.log(EX(' // @exposes db.users to Token Theft
|
|
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('[
|
|
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(
|
|
1050
|
-
console.log(EX(' // @mitigates
|
|
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('[
|
|
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
|
|
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('[
|
|
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
|
|
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('
|
|
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
|
|
1068
|
-
console.log(EX(' // @flows mobile.app
|
|
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('[
|
|
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(
|
|
1074
|
-
console.log(EX(' // @boundary api.gateway
|
|
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('[
|
|
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
|
|
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('[
|
|
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('
|
|
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
|
|
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('[
|
|
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
|
|
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('[
|
|
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
|
|
1329
|
+
console.log(EX(' // @assumes api.gateway -- "Upstream WAF filters malformed requests"'));
|
|
1100
1330
|
console.log('');
|
|
1101
|
-
console.log(` ${V('@comment')} ${D('[
|
|
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
|
|
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'));
|