awguard 1.5.0 → 1.7.0
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 +40 -0
- package/Dockerfile +15 -0
- package/README.md +230 -10
- package/action.yml +7 -3
- package/docs/assets/terminal-demo.svg +19 -0
- package/docs/comparison.md +168 -0
- package/docs/launch-plan.md +35 -17
- package/docs/market-analysis.md +3 -1
- package/docs/marketplace-listing.md +59 -0
- package/docs/npm-publishing.md +68 -0
- package/docs/release-checklist.md +71 -0
- package/docs/report-gallery.md +166 -0
- package/docs/roadmap.md +41 -7
- package/docs/rule-authoring.md +99 -0
- package/docs/schemas.md +16 -0
- package/docs/setup-recipes.md +199 -0
- package/docs/site/index.html +280 -0
- package/examples/.gitlab-ci.yml +6 -0
- package/examples/.vscode/tasks.json +33 -0
- package/examples/README.md +11 -0
- package/examples/awguard.config.example.json +14 -0
- package/examples/corpus/.cursor/rules/autonomy.mdc +3 -0
- package/examples/corpus/.github/prompts/auto-fix.prompt.md +3 -0
- package/examples/corpus/.github/workflows/agentic-pr-review.yml +20 -0
- package/examples/corpus/.github/workflows/pull-request-target-head.yml +13 -0
- package/examples/corpus/.mcp.json +15 -0
- package/examples/corpus/AGENTS.md +5 -0
- package/examples/corpus/README.md +23 -0
- package/examples/dashboard/README.md +55 -0
- package/examples/dashboard/index.html +313 -0
- package/examples/dashboard/sample-history.json +53 -0
- package/examples/lab/README.md +33 -0
- package/examples/lab/fixed/.github/workflows/ai-triage.yml +20 -0
- package/examples/lab/fixed/.mcp.json +12 -0
- package/examples/lab/fixed/AGENTS.md +5 -0
- package/examples/lab/unsafe/.github/workflows/ai-triage.yml +16 -0
- package/examples/lab/unsafe/.mcp.json +11 -0
- package/examples/lab/unsafe/AGENTS.md +4 -0
- package/examples/pr-comment-bot.yml +43 -0
- package/examples/pre-commit-config.yaml +8 -0
- package/examples/pull-request-target.yml +1 -1
- package/examples/safe-agent.yml +1 -1
- package/examples/unsafe-agent.yml +1 -1
- package/examples/vscode-extension/README.md +49 -0
- package/examples/vscode-extension/assets/problems-panel.svg +23 -0
- package/examples/vscode-extension/package.json +68 -0
- package/examples/vscode-extension/src/extension.js +116 -0
- package/package.json +3 -1
- package/schemas/awguard.badge.schema.json +25 -0
- package/schemas/awguard.baseline.schema.json +40 -0
- package/schemas/awguard.comparison.schema.json +146 -0
- package/schemas/awguard.config.schema.json +167 -0
- package/schemas/awguard.inventory.schema.json +124 -0
- package/schemas/awguard.report.schema.json +121 -0
- package/src/autofix.js +201 -0
- package/src/badges.js +63 -0
- package/src/baseline.js +77 -0
- package/src/cli.js +281 -5
- package/src/compare.js +166 -0
- package/src/config.js +58 -2
- package/src/demo.js +90 -0
- package/src/doctor.js +189 -0
- package/src/explain.js +147 -0
- package/src/graph.js +6 -1
- package/src/init.js +84 -0
- package/src/inventory.js +11 -0
- package/src/migration.js +10 -0
- package/src/policy-packs.js +99 -0
- package/src/policy-wizard.js +165 -0
- package/src/presets.js +2 -1
- package/src/remediation.js +92 -1
- package/src/reporters.js +92 -5
- package/src/scanner.js +295 -10
- package/src/score.js +3 -0
- package/src/templates.js +132 -0
package/src/cli.js
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { applyAutofixPlan, buildAutofixPlan, renderAutofixPlan } from './autofix.js';
|
|
5
|
+
import {
|
|
6
|
+
applyBaseline,
|
|
7
|
+
createBaseline,
|
|
8
|
+
loadBaseline,
|
|
9
|
+
pruneBaseline,
|
|
10
|
+
renderBaselineReview,
|
|
11
|
+
reviewBaseline,
|
|
12
|
+
writeBaseline
|
|
13
|
+
} from './baseline.js';
|
|
14
|
+
import { renderBadgeSnippets } from './badges.js';
|
|
15
|
+
import { loadReport, renderComparison, renderComparisonJson } from './compare.js';
|
|
16
|
+
import { renderDemoWalkthrough } from './demo.js';
|
|
5
17
|
import { loadConfig } from './config.js';
|
|
18
|
+
import { buildDoctorReport, renderDoctorReport } from './doctor.js';
|
|
19
|
+
import { renderRuleExplanation } from './explain.js';
|
|
20
|
+
import { renderInitGuide } from './init.js';
|
|
21
|
+
import { renderPolicyPack } from './policy-packs.js';
|
|
22
|
+
import { buildPolicyWizard, renderPolicyWizard } from './policy-wizard.js';
|
|
6
23
|
import { renderFixDryRun } from './remediation.js';
|
|
7
24
|
import { scanWorkflows, severityRank } from './scanner.js';
|
|
25
|
+
import { renderTemplates } from './templates.js';
|
|
8
26
|
import {
|
|
9
27
|
renderBadge,
|
|
10
28
|
renderGithubAnnotations,
|
|
11
29
|
renderGraph,
|
|
30
|
+
renderGithubStepSummary,
|
|
12
31
|
renderHtml,
|
|
13
32
|
renderJson,
|
|
14
33
|
renderMarkdown,
|
|
@@ -16,15 +35,35 @@ import {
|
|
|
16
35
|
renderSarif,
|
|
17
36
|
renderScore,
|
|
18
37
|
renderSurfaceInventory,
|
|
38
|
+
renderSurfaceInventoryJson,
|
|
19
39
|
renderText
|
|
20
40
|
} from './reporters.js';
|
|
21
41
|
|
|
22
42
|
const HELP = `Agentic Workflow Guard
|
|
23
43
|
|
|
24
44
|
Usage:
|
|
25
|
-
awguard [path] [--config file] [--preset name] [--format text|json|markdown|github|sarif|graph|html|migration|score|badge|inventory] [--output file] [--baseline file] [--write-baseline file] [--fix-dry-run] [--fail-on none|low|medium|high|critical]
|
|
45
|
+
awguard [path] [--config file] [--preset name] [--format text|json|markdown|github|sarif|graph|html|migration|score|badge|inventory|inventory-json] [--output file] [--baseline file] [--write-baseline file] [--fix-dry-run] [--fix] [--fail-on none|low|medium|high|critical]
|
|
46
|
+
awguard init
|
|
47
|
+
awguard doctor [path] [--config file] [--preset name]
|
|
48
|
+
awguard explain [AWG###]
|
|
49
|
+
awguard badges [--repo OWNER/REPO] [--branch main] [--badge-file docs/awguard-badge.json] [--site URL]
|
|
50
|
+
awguard demo
|
|
51
|
+
awguard templates [all|github|code-scanning|gitlab|pre-commit|vscode]
|
|
52
|
+
awguard policy-pack [oss|strict|enterprise]
|
|
53
|
+
awguard policy-wizard [path] [--config file] [--preset name] [--dry-run] [--format markdown|json] [--output file]
|
|
54
|
+
awguard baseline-review [path] --baseline file [--config file] [--preset name] [--format text|json] [--prune]
|
|
55
|
+
awguard --compare previous.json current.json
|
|
26
56
|
|
|
27
57
|
Examples:
|
|
58
|
+
awguard init
|
|
59
|
+
awguard doctor
|
|
60
|
+
awguard explain AWG001
|
|
61
|
+
awguard badges --repo OWNER/REPO --site https://OWNER.github.io/REPO/
|
|
62
|
+
awguard demo
|
|
63
|
+
awguard templates github
|
|
64
|
+
awguard policy-pack strict
|
|
65
|
+
awguard policy-wizard . --dry-run
|
|
66
|
+
awguard baseline-review . --baseline awguard.baseline.json
|
|
28
67
|
awguard .
|
|
29
68
|
awguard .mcp.json
|
|
30
69
|
awguard . --config awguard.config.json
|
|
@@ -33,16 +72,92 @@ Examples:
|
|
|
33
72
|
awguard . --format html --output awguard-report.html
|
|
34
73
|
awguard . --format migration --output awguard-migration.md
|
|
35
74
|
awguard . --format inventory
|
|
75
|
+
awguard . --format inventory-json --output awguard-inventory.json
|
|
36
76
|
awguard . --format score
|
|
37
77
|
awguard . --format badge --output awguard-badge.json
|
|
38
78
|
awguard . --fix-dry-run
|
|
79
|
+
awguard . --fix
|
|
39
80
|
awguard . --format sarif --output awguard.sarif --fail-on none
|
|
40
81
|
awguard . --write-baseline awguard.baseline.json
|
|
41
82
|
awguard . --baseline awguard.baseline.json --fail-on high
|
|
83
|
+
awguard --compare old-awguard.json new-awguard.json
|
|
42
84
|
awguard . --format github --fail-on medium
|
|
43
85
|
`;
|
|
44
86
|
|
|
45
87
|
export async function runCli(args, env = process.env) {
|
|
88
|
+
if (args[0] === 'init') {
|
|
89
|
+
console.log(renderInitGuide());
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (args[0] === 'doctor') {
|
|
94
|
+
const options = parseArgs(args.slice(1), env);
|
|
95
|
+
const report = buildDoctorReport({
|
|
96
|
+
root: options.path,
|
|
97
|
+
configPath: options.config,
|
|
98
|
+
presets: options.presets,
|
|
99
|
+
env
|
|
100
|
+
});
|
|
101
|
+
console.log(renderDoctorReport(report));
|
|
102
|
+
if (report.status === 'fail') process.exitCode = 1;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (args[0] === 'explain') {
|
|
107
|
+
console.log(renderRuleExplanation(args[1]));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (args[0] === 'badges') {
|
|
112
|
+
console.log(renderBadgeSnippets(parseBadgeArgs(args.slice(1))));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (args[0] === 'demo') {
|
|
117
|
+
console.log(renderDemoWalkthrough());
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (args[0] === 'templates') {
|
|
122
|
+
console.log(renderTemplates(args[1] || 'all'));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (args[0] === 'policy-pack') {
|
|
127
|
+
console.log(renderPolicyPack(args[1] || 'oss'));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (args[0] === 'policy-wizard') {
|
|
132
|
+
const options = parsePolicyWizardArgs(args.slice(1));
|
|
133
|
+
const loaded = loadConfig({ configPath: options.config, root: options.path, presets: options.presets });
|
|
134
|
+
const result = scanWorkflows({ root: options.path, config: loaded.config });
|
|
135
|
+
const wizard = buildPolicyWizard(result, { existingConfig: loaded.path ? JSON.parse(fs.readFileSync(loaded.path, 'utf8')) : {} });
|
|
136
|
+
const output = renderPolicyWizard(wizard, { format: options.format });
|
|
137
|
+
if (options.output && !options.dryRun) {
|
|
138
|
+
const outputFile = writeOutput(options.output, `${output}\n`);
|
|
139
|
+
console.error(`Wrote ${outputFile}`);
|
|
140
|
+
} else {
|
|
141
|
+
console.log(output);
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (args[0] === 'baseline-review') {
|
|
147
|
+
const options = parseBaselineReviewArgs(args.slice(1));
|
|
148
|
+
if (!options.baseline) throw new Error('baseline-review requires --baseline file');
|
|
149
|
+
const { config } = loadConfig({ configPath: options.config, root: options.path, presets: options.presets });
|
|
150
|
+
const result = scanWorkflows({ root: options.path, config });
|
|
151
|
+
const baseline = loadBaseline(options.baseline);
|
|
152
|
+
const review = reviewBaseline(result, baseline);
|
|
153
|
+
if (options.prune) {
|
|
154
|
+
writeBaseline(options.baseline, pruneBaseline(baseline, review));
|
|
155
|
+
}
|
|
156
|
+
console.log(renderBaselineReview(review, { format: options.format, baselineFile: options.baseline }));
|
|
157
|
+
if (options.prune && options.format !== 'json') console.error(`Pruned baseline ${path.resolve(options.baseline)}`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
46
161
|
const options = parseArgs(args, env);
|
|
47
162
|
|
|
48
163
|
if (options.help) {
|
|
@@ -50,6 +165,19 @@ export async function runCli(args, env = process.env) {
|
|
|
50
165
|
return;
|
|
51
166
|
}
|
|
52
167
|
|
|
168
|
+
if (options.compare.length > 0) {
|
|
169
|
+
const previous = loadReport(options.compare[0]);
|
|
170
|
+
const current = loadReport(options.compare[1]);
|
|
171
|
+
const output = options.format === 'json' ? renderComparisonJson(previous, current) : renderComparison(previous, current);
|
|
172
|
+
if (options.output) {
|
|
173
|
+
const outputFile = writeOutput(options.output, output);
|
|
174
|
+
console.error(`Wrote ${outputFile}`);
|
|
175
|
+
} else {
|
|
176
|
+
console.log(output);
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
53
181
|
const { config } = loadConfig({ configPath: options.config, root: options.path, presets: options.presets });
|
|
54
182
|
let result = scanWorkflows({ root: options.path, config });
|
|
55
183
|
|
|
@@ -62,15 +190,24 @@ export async function runCli(args, env = process.env) {
|
|
|
62
190
|
console.error(`Wrote baseline ${baselineFile}`);
|
|
63
191
|
}
|
|
64
192
|
|
|
65
|
-
|
|
193
|
+
if (options.fix) {
|
|
194
|
+
const plan = applyAutofixPlan(buildAutofixPlan(result));
|
|
195
|
+
console.log(renderAutofixPlan(plan, { applied: true }));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const output = options.fixDryRun ? renderFixDryRun(result, { autofixPlan: buildAutofixPlan(result) }) : render(result, options.format);
|
|
66
200
|
|
|
201
|
+
let outputFile = '';
|
|
67
202
|
if (options.output) {
|
|
68
|
-
|
|
203
|
+
outputFile = writeOutput(options.output, output);
|
|
69
204
|
console.error(`Wrote ${outputFile}`);
|
|
70
205
|
} else if (output.trim().length > 0) {
|
|
71
206
|
console.log(output);
|
|
72
207
|
}
|
|
73
208
|
|
|
209
|
+
writeGithubStepSummary({ env, result, options, outputFile });
|
|
210
|
+
|
|
74
211
|
const findingsToFailOn = result.findings.filter((finding) => finding.baselineState !== 'known');
|
|
75
212
|
if (shouldFail(findingsToFailOn, options.failOn)) {
|
|
76
213
|
process.exitCode = 1;
|
|
@@ -87,8 +224,10 @@ export function parseArgs(args, env = {}) {
|
|
|
87
224
|
baseline: readInput(env, 'baseline') || '',
|
|
88
225
|
writeBaseline: readInput(env, 'write_baseline') || readInput(env, 'write-baseline') || '',
|
|
89
226
|
config: readInput(env, 'config') || '',
|
|
227
|
+
compare: [],
|
|
90
228
|
presets: splitList(readInput(env, 'preset') || readInput(env, 'presets') || ''),
|
|
91
229
|
fixDryRun: readBoolInput(env, 'fix_dry_run') || readBoolInput(env, 'fix-dry-run'),
|
|
230
|
+
fix: readBoolInput(env, 'fix'),
|
|
92
231
|
help: false
|
|
93
232
|
};
|
|
94
233
|
|
|
@@ -121,12 +260,18 @@ export function parseArgs(args, env = {}) {
|
|
|
121
260
|
options.config = args[++index];
|
|
122
261
|
} else if (arg.startsWith('--config=')) {
|
|
123
262
|
options.config = arg.slice('--config='.length);
|
|
263
|
+
} else if (arg === '--compare') {
|
|
264
|
+
options.compare = [args[++index], args[++index]].filter(Boolean);
|
|
265
|
+
} else if (arg.startsWith('--compare=')) {
|
|
266
|
+
options.compare = arg.slice('--compare='.length).split(',').map((item) => item.trim()).filter(Boolean);
|
|
124
267
|
} else if (arg === '--preset') {
|
|
125
268
|
options.presets.push(...splitList(args[++index]));
|
|
126
269
|
} else if (arg.startsWith('--preset=')) {
|
|
127
270
|
options.presets.push(...splitList(arg.slice('--preset='.length)));
|
|
128
271
|
} else if (arg === '--fix-dry-run') {
|
|
129
272
|
options.fixDryRun = true;
|
|
273
|
+
} else if (arg === '--fix') {
|
|
274
|
+
options.fix = true;
|
|
130
275
|
} else if (!arg.startsWith('-')) {
|
|
131
276
|
options.path = arg;
|
|
132
277
|
} else {
|
|
@@ -145,9 +290,13 @@ export function parseArgs(args, env = {}) {
|
|
|
145
290
|
'migration',
|
|
146
291
|
'score',
|
|
147
292
|
'badge',
|
|
148
|
-
'inventory'
|
|
293
|
+
'inventory',
|
|
294
|
+
'inventory-json'
|
|
149
295
|
]);
|
|
150
296
|
validateEnum('fail-on', options.failOn, ['none', 'low', 'medium', 'high', 'critical']);
|
|
297
|
+
if (options.compare.length !== 0 && options.compare.length !== 2) {
|
|
298
|
+
throw new Error('--compare requires two awguard --format json report files');
|
|
299
|
+
}
|
|
151
300
|
|
|
152
301
|
return options;
|
|
153
302
|
}
|
|
@@ -167,6 +316,7 @@ function render(result, format) {
|
|
|
167
316
|
if (format === 'score') return renderScore(result);
|
|
168
317
|
if (format === 'badge') return renderBadge(result);
|
|
169
318
|
if (format === 'inventory') return renderSurfaceInventory(result);
|
|
319
|
+
if (format === 'inventory-json') return renderSurfaceInventoryJson(result);
|
|
170
320
|
if (format === 'github') return renderGithubAnnotations(result);
|
|
171
321
|
return renderText(result);
|
|
172
322
|
}
|
|
@@ -183,6 +333,119 @@ function readBoolInput(env, name) {
|
|
|
183
333
|
return value === 'true' || value === '1' || value === 'yes';
|
|
184
334
|
}
|
|
185
335
|
|
|
336
|
+
function parsePolicyWizardArgs(args) {
|
|
337
|
+
const options = {
|
|
338
|
+
path: '.',
|
|
339
|
+
config: '',
|
|
340
|
+
presets: [],
|
|
341
|
+
dryRun: false,
|
|
342
|
+
format: 'markdown',
|
|
343
|
+
formatSpecified: false,
|
|
344
|
+
output: ''
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
348
|
+
const arg = args[index];
|
|
349
|
+
if (arg === '--config') {
|
|
350
|
+
options.config = args[++index];
|
|
351
|
+
} else if (arg.startsWith('--config=')) {
|
|
352
|
+
options.config = arg.slice('--config='.length);
|
|
353
|
+
} else if (arg === '--preset') {
|
|
354
|
+
options.presets.push(...splitList(args[++index]));
|
|
355
|
+
} else if (arg.startsWith('--preset=')) {
|
|
356
|
+
options.presets.push(...splitList(arg.slice('--preset='.length)));
|
|
357
|
+
} else if (arg === '--dry-run') {
|
|
358
|
+
options.dryRun = true;
|
|
359
|
+
} else if (arg === '--format') {
|
|
360
|
+
options.format = args[++index];
|
|
361
|
+
options.formatSpecified = true;
|
|
362
|
+
} else if (arg.startsWith('--format=')) {
|
|
363
|
+
options.format = arg.slice('--format='.length);
|
|
364
|
+
options.formatSpecified = true;
|
|
365
|
+
} else if (arg === '--output') {
|
|
366
|
+
options.output = args[++index];
|
|
367
|
+
} else if (arg.startsWith('--output=')) {
|
|
368
|
+
options.output = arg.slice('--output='.length);
|
|
369
|
+
} else if (!arg.startsWith('-')) {
|
|
370
|
+
options.path = arg;
|
|
371
|
+
} else {
|
|
372
|
+
throw new Error(`unknown policy-wizard option: ${arg}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (options.output && !options.formatSpecified) options.format = 'json';
|
|
377
|
+
validateEnum('policy-wizard format', options.format, ['markdown', 'json']);
|
|
378
|
+
return options;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function parseBaselineReviewArgs(args) {
|
|
382
|
+
const options = {
|
|
383
|
+
path: '.',
|
|
384
|
+
baseline: '',
|
|
385
|
+
config: '',
|
|
386
|
+
presets: [],
|
|
387
|
+
format: 'text',
|
|
388
|
+
prune: false
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
392
|
+
const arg = args[index];
|
|
393
|
+
if (arg === '--baseline') {
|
|
394
|
+
options.baseline = args[++index];
|
|
395
|
+
} else if (arg.startsWith('--baseline=')) {
|
|
396
|
+
options.baseline = arg.slice('--baseline='.length);
|
|
397
|
+
} else if (arg === '--config') {
|
|
398
|
+
options.config = args[++index];
|
|
399
|
+
} else if (arg.startsWith('--config=')) {
|
|
400
|
+
options.config = arg.slice('--config='.length);
|
|
401
|
+
} else if (arg === '--preset') {
|
|
402
|
+
options.presets.push(...splitList(args[++index]));
|
|
403
|
+
} else if (arg.startsWith('--preset=')) {
|
|
404
|
+
options.presets.push(...splitList(arg.slice('--preset='.length)));
|
|
405
|
+
} else if (arg === '--format') {
|
|
406
|
+
options.format = args[++index];
|
|
407
|
+
} else if (arg.startsWith('--format=')) {
|
|
408
|
+
options.format = arg.slice('--format='.length);
|
|
409
|
+
} else if (arg === '--prune') {
|
|
410
|
+
options.prune = true;
|
|
411
|
+
} else if (!arg.startsWith('-')) {
|
|
412
|
+
options.path = arg;
|
|
413
|
+
} else {
|
|
414
|
+
throw new Error(`unknown baseline-review option: ${arg}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
validateEnum('baseline-review format', options.format, ['text', 'json']);
|
|
419
|
+
return options;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function parseBadgeArgs(args) {
|
|
423
|
+
const options = {};
|
|
424
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
425
|
+
const arg = args[index];
|
|
426
|
+
if (arg === '--repo') {
|
|
427
|
+
options.repo = args[++index];
|
|
428
|
+
} else if (arg.startsWith('--repo=')) {
|
|
429
|
+
options.repo = arg.slice('--repo='.length);
|
|
430
|
+
} else if (arg === '--branch') {
|
|
431
|
+
options.branch = args[++index];
|
|
432
|
+
} else if (arg.startsWith('--branch=')) {
|
|
433
|
+
options.branch = arg.slice('--branch='.length);
|
|
434
|
+
} else if (arg === '--badge-file') {
|
|
435
|
+
options.badgeFile = args[++index];
|
|
436
|
+
} else if (arg.startsWith('--badge-file=')) {
|
|
437
|
+
options.badgeFile = arg.slice('--badge-file='.length);
|
|
438
|
+
} else if (arg === '--site') {
|
|
439
|
+
options.site = args[++index];
|
|
440
|
+
} else if (arg.startsWith('--site=')) {
|
|
441
|
+
options.site = arg.slice('--site='.length);
|
|
442
|
+
} else {
|
|
443
|
+
throw new Error(`unknown badges option: ${arg}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return options;
|
|
447
|
+
}
|
|
448
|
+
|
|
186
449
|
function writeOutput(file, output) {
|
|
187
450
|
const absoluteFile = path.resolve(file);
|
|
188
451
|
fs.mkdirSync(path.dirname(absoluteFile), { recursive: true });
|
|
@@ -190,6 +453,19 @@ function writeOutput(file, output) {
|
|
|
190
453
|
return absoluteFile;
|
|
191
454
|
}
|
|
192
455
|
|
|
456
|
+
function writeGithubStepSummary({ env, result, options, outputFile }) {
|
|
457
|
+
if (env.GITHUB_ACTIONS !== 'true' || !env.GITHUB_STEP_SUMMARY) return;
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
fs.appendFileSync(
|
|
461
|
+
env.GITHUB_STEP_SUMMARY,
|
|
462
|
+
`${renderGithubStepSummary(result, { format: options.format, failOn: options.failOn, outputFile })}\n`
|
|
463
|
+
);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
console.error(`awguard: could not write GitHub job summary: ${error.message}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
193
469
|
function shouldFail(findings, threshold) {
|
|
194
470
|
if (threshold === 'none') return false;
|
|
195
471
|
const thresholdRank = severityRank[threshold];
|
package/src/compare.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { classifyScanFile } from './scanner.js';
|
|
4
|
+
|
|
5
|
+
const surfaceLabels = {
|
|
6
|
+
'github-workflow': 'GitHub Actions workflows',
|
|
7
|
+
'agent-context': 'Agent context files',
|
|
8
|
+
'mcp-config': 'MCP configs',
|
|
9
|
+
other: 'Other scanned files'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function loadReport(file) {
|
|
13
|
+
const absoluteFile = path.resolve(file);
|
|
14
|
+
if (!fs.existsSync(absoluteFile)) {
|
|
15
|
+
throw new Error(`report file does not exist: ${absoluteFile}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const report = JSON.parse(fs.readFileSync(absoluteFile, 'utf8'));
|
|
19
|
+
if (!Array.isArray(report.findings) || !Array.isArray(report.scannedFiles)) {
|
|
20
|
+
throw new Error(`report file must be awguard --format json output: ${absoluteFile}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return report;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function buildComparison(previous, current) {
|
|
27
|
+
const previousFindings = mapByFingerprint(previous.findings);
|
|
28
|
+
const currentFindings = mapByFingerprint(current.findings);
|
|
29
|
+
const previousFiles = new Set(previous.scannedFiles || []);
|
|
30
|
+
const currentFiles = new Set(current.scannedFiles || []);
|
|
31
|
+
|
|
32
|
+
const introducedFindings = [...currentFindings.entries()]
|
|
33
|
+
.filter(([fingerprint]) => !previousFindings.has(fingerprint))
|
|
34
|
+
.map(([, finding]) => finding);
|
|
35
|
+
const resolvedFindings = [...previousFindings.entries()]
|
|
36
|
+
.filter(([fingerprint]) => !currentFindings.has(fingerprint))
|
|
37
|
+
.map(([, finding]) => finding);
|
|
38
|
+
const unchangedFindings = [...currentFindings.keys()].filter((fingerprint) => previousFindings.has(fingerprint));
|
|
39
|
+
const addedFiles = [...currentFiles].filter((file) => !previousFiles.has(file)).sort();
|
|
40
|
+
const removedFiles = [...previousFiles].filter((file) => !currentFiles.has(file)).sort();
|
|
41
|
+
const addedSurfaces = groupFilesBySurface(addedFiles, current.root || previous.root);
|
|
42
|
+
const removedSurfaces = groupFilesBySurface(removedFiles, previous.root || current.root);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
summary: {
|
|
46
|
+
previousFindings: previous.findings.length,
|
|
47
|
+
currentFindings: current.findings.length,
|
|
48
|
+
introducedFindings: introducedFindings.length,
|
|
49
|
+
resolvedFindings: resolvedFindings.length,
|
|
50
|
+
unchangedFindings: unchangedFindings.length,
|
|
51
|
+
addedFiles: addedFiles.length,
|
|
52
|
+
removedFiles: removedFiles.length,
|
|
53
|
+
addedSurfaces: addedSurfaces.length,
|
|
54
|
+
removedSurfaces: removedSurfaces.length
|
|
55
|
+
},
|
|
56
|
+
introducedFindings,
|
|
57
|
+
resolvedFindings,
|
|
58
|
+
addedFiles,
|
|
59
|
+
removedFiles,
|
|
60
|
+
addedSurfaces,
|
|
61
|
+
removedSurfaces
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function renderComparison(previous, current) {
|
|
66
|
+
const comparison = buildComparison(previous, current);
|
|
67
|
+
const lines = [
|
|
68
|
+
'# Agentic Workflow Guard Comparison',
|
|
69
|
+
'',
|
|
70
|
+
`Previous findings: **${comparison.summary.previousFindings}**`,
|
|
71
|
+
`Current findings: **${comparison.summary.currentFindings}**`,
|
|
72
|
+
`Introduced findings: **${comparison.summary.introducedFindings}**`,
|
|
73
|
+
`Resolved findings: **${comparison.summary.resolvedFindings}**`,
|
|
74
|
+
`Unchanged findings: **${comparison.summary.unchangedFindings}**`,
|
|
75
|
+
`Added scanned files: **${comparison.summary.addedFiles}**`,
|
|
76
|
+
`Removed scanned files: **${comparison.summary.removedFiles}**`,
|
|
77
|
+
''
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
lines.push('## Introduced Findings', '');
|
|
81
|
+
appendFindings(lines, comparison.introducedFindings);
|
|
82
|
+
lines.push('', '## Resolved Findings', '');
|
|
83
|
+
appendFindings(lines, comparison.resolvedFindings);
|
|
84
|
+
lines.push('', '## Added Agentic Surfaces', '');
|
|
85
|
+
appendSurfaces(lines, comparison.addedSurfaces);
|
|
86
|
+
lines.push('', '## Removed Agentic Surfaces', '');
|
|
87
|
+
appendSurfaces(lines, comparison.removedSurfaces);
|
|
88
|
+
lines.push('', '## Added Files', '');
|
|
89
|
+
appendFiles(lines, comparison.addedFiles);
|
|
90
|
+
lines.push('', '## Removed Files', '');
|
|
91
|
+
appendFiles(lines, comparison.removedFiles);
|
|
92
|
+
|
|
93
|
+
return lines.join('\n');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function renderComparisonJson(previous, current) {
|
|
97
|
+
return JSON.stringify(buildComparison(previous, current), null, 2);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function appendFindings(lines, findings) {
|
|
101
|
+
if (findings.length === 0) {
|
|
102
|
+
lines.push('None.');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
lines.push('| Severity | Rule | Location | Finding |');
|
|
107
|
+
lines.push('| --- | --- | --- | --- |');
|
|
108
|
+
for (const finding of findings) {
|
|
109
|
+
lines.push(
|
|
110
|
+
`| ${escapeMarkdown(finding.severity)} | ${escapeMarkdown(finding.ruleId)} | ${escapeMarkdown(
|
|
111
|
+
`${finding.file}:${finding.line}`
|
|
112
|
+
)} | ${escapeMarkdown(finding.title)} |`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function appendFiles(lines, files) {
|
|
118
|
+
if (files.length === 0) {
|
|
119
|
+
lines.push('None.');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const file of files) {
|
|
124
|
+
lines.push(`- \`${file.replaceAll('`', '\\`')}\``);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function appendSurfaces(lines, surfaces) {
|
|
129
|
+
if (surfaces.length === 0) {
|
|
130
|
+
lines.push('None.');
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
lines.push('| Surface | Files |');
|
|
135
|
+
lines.push('| --- | ---: |');
|
|
136
|
+
for (const surface of surfaces) {
|
|
137
|
+
lines.push(`| ${escapeMarkdown(surface.label)} | ${surface.files.length} |`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function mapByFingerprint(findings) {
|
|
142
|
+
return new Map(findings.map((finding) => [finding.fingerprint || `${finding.ruleId}:${finding.file}:${finding.line}`, finding]));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function groupFilesBySurface(files, root = process.cwd()) {
|
|
146
|
+
const groups = new Map();
|
|
147
|
+
for (const file of files) {
|
|
148
|
+
const surface = classifyScanFile(file, root || process.cwd());
|
|
149
|
+
if (!groups.has(surface)) {
|
|
150
|
+
groups.set(surface, {
|
|
151
|
+
surface,
|
|
152
|
+
label: surfaceLabels[surface] || surfaceLabels.other,
|
|
153
|
+
files: []
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
groups.get(surface).files.push(file);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return [...groups.values()]
|
|
160
|
+
.map((group) => ({ ...group, files: group.files.sort() }))
|
|
161
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function escapeMarkdown(value) {
|
|
165
|
+
return String(value).replaceAll('|', '\\|');
|
|
166
|
+
}
|
package/src/config.js
CHANGED
|
@@ -33,7 +33,9 @@ export function normalizeConfig(rawConfig = {}, source = 'config') {
|
|
|
33
33
|
|
|
34
34
|
return {
|
|
35
35
|
rules: normalizeRules(mergedConfig.rules || {}, source),
|
|
36
|
-
suppressions: normalizeSuppressions(mergedConfig.suppressions || {}, source)
|
|
36
|
+
suppressions: normalizeSuppressions(mergedConfig.suppressions || {}, source),
|
|
37
|
+
policy: normalizePolicy(mergedConfig.policy || {}, source),
|
|
38
|
+
scan: normalizeScan(mergedConfig.scan || {}, source)
|
|
37
39
|
};
|
|
38
40
|
}
|
|
39
41
|
|
|
@@ -51,7 +53,9 @@ function mergePresetConfigs(rawConfig, source) {
|
|
|
51
53
|
|
|
52
54
|
return mergeConfigObjects(merged, {
|
|
53
55
|
rules: rawConfig.rules || {},
|
|
54
|
-
suppressions: rawConfig.suppressions || {}
|
|
56
|
+
suppressions: rawConfig.suppressions || {},
|
|
57
|
+
policy: rawConfig.policy || {},
|
|
58
|
+
scan: rawConfig.scan || {}
|
|
55
59
|
});
|
|
56
60
|
}
|
|
57
61
|
|
|
@@ -64,6 +68,14 @@ function mergeConfigObjects(base, override) {
|
|
|
64
68
|
suppressions: {
|
|
65
69
|
...(base.suppressions || {}),
|
|
66
70
|
...(override.suppressions || {})
|
|
71
|
+
},
|
|
72
|
+
policy: {
|
|
73
|
+
...(base.policy || {}),
|
|
74
|
+
...(override.policy || {})
|
|
75
|
+
},
|
|
76
|
+
scan: {
|
|
77
|
+
...(base.scan || {}),
|
|
78
|
+
...(override.scan || {})
|
|
67
79
|
}
|
|
68
80
|
};
|
|
69
81
|
}
|
|
@@ -149,6 +161,50 @@ function normalizeSuppressions(suppressions, source) {
|
|
|
149
161
|
};
|
|
150
162
|
}
|
|
151
163
|
|
|
164
|
+
function normalizePolicy(policy, source) {
|
|
165
|
+
if (!isObject(policy)) {
|
|
166
|
+
throw new Error(`${source} policy must be an object`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
approvedFiles: normalizeStringArray(policy.approvedFiles || [], `${source} policy.approvedFiles`),
|
|
171
|
+
approvedMcpServers: normalizeStringArray(policy.approvedMcpServers || [], `${source} policy.approvedMcpServers`),
|
|
172
|
+
approvedMcpPackages: normalizeStringArray(policy.approvedMcpPackages || [], `${source} policy.approvedMcpPackages`),
|
|
173
|
+
approvedMcpPackageScopes: normalizeStringArray(policy.approvedMcpPackageScopes || [], `${source} policy.approvedMcpPackageScopes`),
|
|
174
|
+
approvedMcpCommands: normalizeStringArray(policy.approvedMcpCommands || [], `${source} policy.approvedMcpCommands`)
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function normalizeScan(scan, source) {
|
|
179
|
+
if (!isObject(scan)) {
|
|
180
|
+
throw new Error(`${source} scan must be an object`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
include: normalizeStringArray(scan.include || [], `${source} scan.include`),
|
|
185
|
+
exclude: normalizeStringArray(scan.exclude || [], `${source} scan.exclude`),
|
|
186
|
+
maxFiles: normalizeOptionalPositiveInteger(scan.maxFiles, `${source} scan.maxFiles`),
|
|
187
|
+
maxFileBytes: normalizeOptionalPositiveInteger(scan.maxFileBytes, `${source} scan.maxFileBytes`)
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function normalizeOptionalPositiveInteger(value, source) {
|
|
192
|
+
if (value === undefined || value === null || value === '') return undefined;
|
|
193
|
+
const number = Number(value);
|
|
194
|
+
if (!Number.isInteger(number) || number < 1) {
|
|
195
|
+
throw new Error(`${source} must be a positive integer`);
|
|
196
|
+
}
|
|
197
|
+
return number;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function normalizeStringArray(value, source) {
|
|
201
|
+
if (!Array.isArray(value)) {
|
|
202
|
+
throw new Error(`${source} must be an array`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return value.map((item) => String(item));
|
|
206
|
+
}
|
|
207
|
+
|
|
152
208
|
function ensureKnownRule(ruleId, source) {
|
|
153
209
|
if (!ruleCatalog[ruleId]) {
|
|
154
210
|
throw new Error(`${source} references unknown rule id: ${ruleId}`);
|