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.
Files changed (75) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/Dockerfile +15 -0
  3. package/README.md +230 -10
  4. package/action.yml +7 -3
  5. package/docs/assets/terminal-demo.svg +19 -0
  6. package/docs/comparison.md +168 -0
  7. package/docs/launch-plan.md +35 -17
  8. package/docs/market-analysis.md +3 -1
  9. package/docs/marketplace-listing.md +59 -0
  10. package/docs/npm-publishing.md +68 -0
  11. package/docs/release-checklist.md +71 -0
  12. package/docs/report-gallery.md +166 -0
  13. package/docs/roadmap.md +41 -7
  14. package/docs/rule-authoring.md +99 -0
  15. package/docs/schemas.md +16 -0
  16. package/docs/setup-recipes.md +199 -0
  17. package/docs/site/index.html +280 -0
  18. package/examples/.gitlab-ci.yml +6 -0
  19. package/examples/.vscode/tasks.json +33 -0
  20. package/examples/README.md +11 -0
  21. package/examples/awguard.config.example.json +14 -0
  22. package/examples/corpus/.cursor/rules/autonomy.mdc +3 -0
  23. package/examples/corpus/.github/prompts/auto-fix.prompt.md +3 -0
  24. package/examples/corpus/.github/workflows/agentic-pr-review.yml +20 -0
  25. package/examples/corpus/.github/workflows/pull-request-target-head.yml +13 -0
  26. package/examples/corpus/.mcp.json +15 -0
  27. package/examples/corpus/AGENTS.md +5 -0
  28. package/examples/corpus/README.md +23 -0
  29. package/examples/dashboard/README.md +55 -0
  30. package/examples/dashboard/index.html +313 -0
  31. package/examples/dashboard/sample-history.json +53 -0
  32. package/examples/lab/README.md +33 -0
  33. package/examples/lab/fixed/.github/workflows/ai-triage.yml +20 -0
  34. package/examples/lab/fixed/.mcp.json +12 -0
  35. package/examples/lab/fixed/AGENTS.md +5 -0
  36. package/examples/lab/unsafe/.github/workflows/ai-triage.yml +16 -0
  37. package/examples/lab/unsafe/.mcp.json +11 -0
  38. package/examples/lab/unsafe/AGENTS.md +4 -0
  39. package/examples/pr-comment-bot.yml +43 -0
  40. package/examples/pre-commit-config.yaml +8 -0
  41. package/examples/pull-request-target.yml +1 -1
  42. package/examples/safe-agent.yml +1 -1
  43. package/examples/unsafe-agent.yml +1 -1
  44. package/examples/vscode-extension/README.md +49 -0
  45. package/examples/vscode-extension/assets/problems-panel.svg +23 -0
  46. package/examples/vscode-extension/package.json +68 -0
  47. package/examples/vscode-extension/src/extension.js +116 -0
  48. package/package.json +3 -1
  49. package/schemas/awguard.badge.schema.json +25 -0
  50. package/schemas/awguard.baseline.schema.json +40 -0
  51. package/schemas/awguard.comparison.schema.json +146 -0
  52. package/schemas/awguard.config.schema.json +167 -0
  53. package/schemas/awguard.inventory.schema.json +124 -0
  54. package/schemas/awguard.report.schema.json +121 -0
  55. package/src/autofix.js +201 -0
  56. package/src/badges.js +63 -0
  57. package/src/baseline.js +77 -0
  58. package/src/cli.js +281 -5
  59. package/src/compare.js +166 -0
  60. package/src/config.js +58 -2
  61. package/src/demo.js +90 -0
  62. package/src/doctor.js +189 -0
  63. package/src/explain.js +147 -0
  64. package/src/graph.js +6 -1
  65. package/src/init.js +84 -0
  66. package/src/inventory.js +11 -0
  67. package/src/migration.js +10 -0
  68. package/src/policy-packs.js +99 -0
  69. package/src/policy-wizard.js +165 -0
  70. package/src/presets.js +2 -1
  71. package/src/remediation.js +92 -1
  72. package/src/reporters.js +92 -5
  73. package/src/scanner.js +295 -10
  74. package/src/score.js +3 -0
  75. package/src/templates.js +132 -0
package/src/demo.js ADDED
@@ -0,0 +1,90 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { buildComparison } from './compare.js';
4
+ import { scanWorkflows } from './scanner.js';
5
+ import { calculateScore } from './score.js';
6
+
7
+ export function renderDemoWalkthrough({ packageRoot = getPackageRoot() } = {}) {
8
+ const unsafeRoot = path.join(packageRoot, 'examples', 'lab', 'unsafe');
9
+ const fixedRoot = path.join(packageRoot, 'examples', 'lab', 'fixed');
10
+ const unsafe = scanWorkflows({ root: unsafeRoot });
11
+ const fixed = scanWorkflows({ root: fixedRoot });
12
+ const comparison = buildComparison(toPortableReport(unsafe), toPortableReport(fixed));
13
+ const unsafeScore = calculateScore(unsafe);
14
+ const fixedScore = calculateScore(fixed);
15
+
16
+ const lines = [
17
+ '# Agentic Workflow Guard Demo',
18
+ '',
19
+ 'This offline demo scans the built-in vulnerable lab and its fixed version.',
20
+ '',
21
+ '## Commands',
22
+ '',
23
+ '```bash',
24
+ 'npx awguard@latest examples/lab/unsafe --format inventory',
25
+ 'npx awguard@latest examples/lab/fixed --format inventory',
26
+ 'npx awguard@latest examples/lab/fixed --fail-on high',
27
+ '```',
28
+ '',
29
+ '## Before And After',
30
+ '',
31
+ '| Lab | Scanned files | Findings | Highest | AWI score |',
32
+ '| --- | ---: | ---: | --- | --- |',
33
+ `| Unsafe | ${unsafe.scannedFiles.length} | ${unsafe.findings.length} | ${unsafe.summary.highest} | ${unsafeScore.grade} ${unsafeScore.score}/100 |`,
34
+ `| Fixed | ${fixed.scannedFiles.length} | ${fixed.findings.length} | ${fixed.summary.highest} | ${fixedScore.grade} ${fixedScore.score}/100 |`,
35
+ '',
36
+ '## Resolved Risk',
37
+ '',
38
+ `Resolved findings: **${comparison.summary.resolvedFindings}**`,
39
+ `Remaining findings: **${comparison.summary.currentFindings}**`,
40
+ '',
41
+ '## Unsafe Findings',
42
+ ''
43
+ ];
44
+
45
+ appendFindings(lines, unsafe.findings);
46
+ lines.push(
47
+ '',
48
+ '## Fixed Result',
49
+ '',
50
+ fixed.findings.length === 0
51
+ ? 'The fixed lab is clean for the enabled rules.'
52
+ : `The fixed lab still has ${fixed.findings.length} finding(s).`,
53
+ '',
54
+ 'Lab docs: `examples/lab/README.md`'
55
+ );
56
+
57
+ return lines.join('\n');
58
+ }
59
+
60
+ function appendFindings(lines, findings) {
61
+ if (findings.length === 0) {
62
+ lines.push('None.');
63
+ return;
64
+ }
65
+
66
+ lines.push('| Severity | Rule | Location | Finding |', '| --- | --- | --- | --- |');
67
+ for (const finding of findings) {
68
+ lines.push(
69
+ `| ${escapeMarkdown(finding.severity)} | ${escapeMarkdown(finding.ruleId)} | \`${escapeMarkdown(
70
+ `${finding.file}:${finding.line}`
71
+ )}\` | ${escapeMarkdown(finding.title)} |`
72
+ );
73
+ }
74
+ }
75
+
76
+ function toPortableReport(result) {
77
+ return {
78
+ ...result,
79
+ scannedFiles: result.scannedFiles.map((file) => path.relative(result.root, file) || file)
80
+ };
81
+ }
82
+
83
+ function getPackageRoot() {
84
+ const sourceDir = path.dirname(fileURLToPath(import.meta.url));
85
+ return path.resolve(sourceDir, '..');
86
+ }
87
+
88
+ function escapeMarkdown(value) {
89
+ return String(value).replaceAll('|', '\\|');
90
+ }
package/src/doctor.js ADDED
@@ -0,0 +1,189 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import process from 'node:process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { loadConfig } from './config.js';
6
+ import { scanWorkflows } from './scanner.js';
7
+
8
+ const schemaPath = 'schemas/awguard.config.schema.json';
9
+
10
+ export function buildDoctorReport({ root = '.', configPath = '', presets = [], env = process.env } = {}) {
11
+ const checks = [];
12
+ const packageInfo = readPackageInfo();
13
+ const packageRoot = getPackageRoot();
14
+
15
+ addCheck(
16
+ checks,
17
+ nodeMajor(process.version) >= 20 ? 'ok' : 'fail',
18
+ 'Node.js runtime',
19
+ `${process.version} detected; AWGuard requires Node.js 20 or newer.`,
20
+ 'Install a current Node.js release before running AWGuard.'
21
+ );
22
+
23
+ addCheck(
24
+ checks,
25
+ packageInfo.version ? 'ok' : 'warn',
26
+ 'AWGuard package metadata',
27
+ packageInfo.version ? `awguard ${packageInfo.version}` : 'Could not read package.json version.',
28
+ 'Reinstall the package if this command is running from a broken checkout.'
29
+ );
30
+
31
+ const absoluteRoot = path.resolve(root);
32
+ if (!fs.existsSync(absoluteRoot)) {
33
+ addCheck(
34
+ checks,
35
+ 'fail',
36
+ 'Scan target',
37
+ `Target does not exist: ${absoluteRoot}`,
38
+ 'Pass a repository directory, workflow file, agent instruction file, or MCP config file.'
39
+ );
40
+ return finish(checks);
41
+ }
42
+
43
+ const targetStat = fs.statSync(absoluteRoot);
44
+ addCheck(
45
+ checks,
46
+ targetStat.isDirectory() || targetStat.isFile() ? 'ok' : 'fail',
47
+ 'Scan target',
48
+ `${absoluteRoot} is a ${targetStat.isDirectory() ? 'directory' : targetStat.isFile() ? 'file' : 'special file'}.`,
49
+ 'Pass a regular file or directory.'
50
+ );
51
+
52
+ let configResult;
53
+ try {
54
+ configResult = loadConfig({ configPath, root: absoluteRoot, presets });
55
+ addCheck(
56
+ checks,
57
+ 'ok',
58
+ 'Configuration',
59
+ configResult.path
60
+ ? `Loaded ${path.relative(process.cwd(), configResult.path) || configResult.path}.`
61
+ : 'No config file found; using defaults and CLI presets.',
62
+ `Add "$schema": "https://raw.githubusercontent.com/Mughal-Baig/agentic-workflow-guard/main/${schemaPath}" to awguard.config.json for editor validation.`
63
+ );
64
+ } catch (error) {
65
+ addCheck(checks, 'fail', 'Configuration', error.message, 'Fix the config file or run without --config.');
66
+ return finish(checks);
67
+ }
68
+
69
+ addCheck(
70
+ checks,
71
+ fs.existsSync(path.join(packageRoot, schemaPath)) ? 'ok' : 'warn',
72
+ 'Config schema',
73
+ `Schema path: ${schemaPath}`,
74
+ 'Publish package files with the schemas directory included.'
75
+ );
76
+
77
+ let result;
78
+ try {
79
+ result = scanWorkflows({ root: absoluteRoot, config: configResult.config });
80
+ } catch (error) {
81
+ addCheck(checks, 'fail', 'Scanner smoke test', error.message, 'Check file permissions and scan target contents.');
82
+ return finish(checks);
83
+ }
84
+
85
+ if (result.scannedFiles.length === 0) {
86
+ addCheck(
87
+ checks,
88
+ 'warn',
89
+ 'Scanner smoke test',
90
+ 'No GitHub Actions workflow, agent instruction, or MCP config files were found.',
91
+ 'Run AWGuard again after adding agent workflows, AGENTS.md-style files, or MCP configs.'
92
+ );
93
+ } else {
94
+ const findingText =
95
+ result.findings.length === 0
96
+ ? 'no findings'
97
+ : `${result.findings.length} finding(s), highest severity ${result.summary.highest}`;
98
+ addCheck(
99
+ checks,
100
+ result.findings.length === 0 ? 'ok' : 'warn',
101
+ 'Scanner smoke test',
102
+ `Scanned ${result.scannedFiles.length} file(s); ${findingText}.`,
103
+ 'Use --format score, --format inventory, or --format sarif for CI-ready reports.'
104
+ );
105
+ }
106
+
107
+ if (env.GITHUB_ACTIONS === 'true') {
108
+ addCheck(
109
+ checks,
110
+ env.GITHUB_STEP_SUMMARY ? 'ok' : 'warn',
111
+ 'GitHub Actions summary',
112
+ env.GITHUB_STEP_SUMMARY
113
+ ? 'GITHUB_STEP_SUMMARY is available for job summary output.'
114
+ : 'GITHUB_STEP_SUMMARY is not available in this environment.',
115
+ 'Run inside a modern GitHub Actions job to receive the Markdown job summary.'
116
+ );
117
+ }
118
+
119
+ return finish(checks);
120
+ }
121
+
122
+ export function renderDoctorReport(report) {
123
+ const lines = [
124
+ '# Agentic Workflow Guard Doctor',
125
+ '',
126
+ `Status: **${report.status.toUpperCase()}**`,
127
+ '',
128
+ '| Check | Status | Detail |',
129
+ '| --- | --- | --- |'
130
+ ];
131
+
132
+ for (const check of report.checks) {
133
+ lines.push(`| ${escapeMarkdown(check.title)} | ${statusLabel(check.status)} | ${escapeMarkdown(check.detail)} |`);
134
+ }
135
+
136
+ const nextSteps = report.checks.filter((check) => check.status !== 'ok' && check.nextStep);
137
+ if (nextSteps.length > 0) {
138
+ lines.push('', '## Next Steps', '');
139
+ for (const check of nextSteps) {
140
+ lines.push(`- ${check.title}: ${check.nextStep}`);
141
+ }
142
+ }
143
+
144
+ return lines.join('\n');
145
+ }
146
+
147
+ function addCheck(checks, status, title, detail, nextStep = '') {
148
+ checks.push({ status, title, detail, nextStep });
149
+ }
150
+
151
+ function finish(checks) {
152
+ return {
153
+ status: checks.some((check) => check.status === 'fail')
154
+ ? 'fail'
155
+ : checks.some((check) => check.status === 'warn')
156
+ ? 'warn'
157
+ : 'ok',
158
+ checks
159
+ };
160
+ }
161
+
162
+ function nodeMajor(version) {
163
+ const match = String(version).match(/^v?(\d+)/);
164
+ return match ? Number(match[1]) : 0;
165
+ }
166
+
167
+ function readPackageInfo() {
168
+ const packageFile = path.join(getPackageRoot(), 'package.json');
169
+ try {
170
+ return JSON.parse(fs.readFileSync(packageFile, 'utf8'));
171
+ } catch {
172
+ return {};
173
+ }
174
+ }
175
+
176
+ function getPackageRoot() {
177
+ const sourceDir = path.dirname(fileURLToPath(import.meta.url));
178
+ return path.resolve(sourceDir, '..');
179
+ }
180
+
181
+ function statusLabel(status) {
182
+ if (status === 'ok') return 'OK';
183
+ if (status === 'warn') return 'WARN';
184
+ return 'FAIL';
185
+ }
186
+
187
+ function escapeMarkdown(value) {
188
+ return String(value).replaceAll('|', '\\|').replaceAll('\n', ' ');
189
+ }
package/src/explain.js ADDED
@@ -0,0 +1,147 @@
1
+ import { ruleCatalog } from './scanner.js';
2
+
3
+ const ruleDetails = {
4
+ AWG001: {
5
+ detects: 'User-controlled GitHub issue, pull request, comment, branch, or event text reaching an AI prompt.',
6
+ why: 'The model may treat attacker-supplied text as instructions and use privileged tools or repository context.',
7
+ safePattern: 'Delimit untrusted text, keep the job read-only, and require human review before any write action.'
8
+ },
9
+ AWG002: {
10
+ detects: 'Untrusted GitHub context interpolated directly into shell scripts.',
11
+ why: 'Shell interpolation can turn event text into command execution or argument injection.',
12
+ safePattern: 'Move values into environment variables and reference them with shell quoting.'
13
+ },
14
+ AWG003: {
15
+ detects: 'pull_request_target workflows that check out pull request head code.',
16
+ why: 'Untrusted fork code can run with base-repository privileges.',
17
+ safePattern: 'Use pull_request for untrusted code or keep pull_request_target limited to metadata-only work.'
18
+ },
19
+ AWG004: {
20
+ detects: 'Agent jobs with broad write-capable GitHub token permissions.',
21
+ why: 'Prompt injection becomes much more damaging when the agent can write repository state.',
22
+ safePattern: 'Use contents: read by default and isolate write actions behind a reviewed apply job.'
23
+ },
24
+ AWG005: {
25
+ detects: 'Secrets exposed to workflows driven by untrusted agent input.',
26
+ why: 'An attacker can steer the agent or scripts into revealing credentials.',
27
+ safePattern: 'Keep secrets out of untrusted event workflows and use a separate approved privileged workflow.'
28
+ },
29
+ AWG006: {
30
+ detects: 'Agent command flags that skip approvals or permission prompts.',
31
+ why: 'Autonomous execution removes the human gate that normally stops unsafe tool use.',
32
+ safePattern: 'Require confirmation for file writes, command execution, and repository changes.'
33
+ },
34
+ AWG007: {
35
+ detects: 'Model or agent output flowing into eval, shell, or pipe-to-shell execution.',
36
+ why: 'Model output is data, not trusted code.',
37
+ safePattern: 'Validate structured output with a narrow parser before using it.'
38
+ },
39
+ AWG008: {
40
+ detects: 'Agent workflows without explicit permissions.',
41
+ why: 'Default permissions are easy to misunderstand and can drift as repository settings change.',
42
+ safePattern: 'Declare the minimum required permissions in every agent workflow.'
43
+ },
44
+ AWG009: {
45
+ detects: 'workflow_run jobs that consume artifacts before scripts execute.',
46
+ why: 'Artifacts can carry untrusted content from an earlier workflow into a more privileged job.',
47
+ safePattern: 'Verify artifact provenance, names, checksums, and expected schema before use.'
48
+ },
49
+ AWG010: {
50
+ detects: 'Third-party actions that are not pinned to commit SHAs in sensitive agent workflows.',
51
+ why: 'Mutable tags can change behavior after review.',
52
+ safePattern: 'Pin third-party actions to reviewed commit SHAs.'
53
+ },
54
+ AWG011: {
55
+ detects: 'Suppression comments that are malformed, unjustified, or disallowed by config.',
56
+ why: 'Weak suppressions can hide real agentic workflow risk.',
57
+ safePattern: 'Use narrow rule IDs and a clear reviewed false-positive reason.'
58
+ },
59
+ AWG012: {
60
+ detects: 'Persistent agent instructions that weaken approval, permission, or secret boundaries.',
61
+ why: 'Instruction files are durable context that can affect future agent runs.',
62
+ safePattern: 'Keep repository instructions conservative and explicit about human review.'
63
+ },
64
+ AWG013: {
65
+ detects: 'MCP configs that start mutable packages, unpinned containers, or shell wrappers.',
66
+ why: 'MCP servers expand agent tool authority before runtime scanners can inspect behavior.',
67
+ safePattern: 'Pin packages/images and avoid shell wrappers for project-scoped MCP servers.'
68
+ },
69
+ AWG014: {
70
+ detects: 'MCP configs that hardcode credentials or authorization material.',
71
+ why: 'Committed secrets can be used by anyone with repository access and may be exposed by agents.',
72
+ safePattern: 'Move credentials into prompts, environment variables, or a secret manager.'
73
+ },
74
+ AWG015: {
75
+ detects: 'Agentic files, MCP servers, packages, or commands outside configured policy allowlists.',
76
+ why: 'New agent surfaces should be visible in review before they gain trust.',
77
+ safePattern: 'Approve only reviewed files, servers, packages, and commands.'
78
+ },
79
+ AWG016: {
80
+ detects: 'actions/checkout credentials persisting in elevated agent workflows.',
81
+ why: 'Persisted checkout credentials can give later agent or shell steps repository write access.',
82
+ safePattern: 'Use persist-credentials: false and keep writeback in a separate reviewed job.'
83
+ },
84
+ AWG017: {
85
+ detects: 'Agent jobs with write permissions that commit, tag, publish, or push without a reviewable boundary.',
86
+ why: 'Autonomous writeback can change protected repository state before a maintainer reviews the result.',
87
+ safePattern: 'Push to an isolated branch, open a draft pull request, or upload artifacts for a human apply step.'
88
+ },
89
+ AWG018: {
90
+ detects: 'Untrusted GitHub event text flowing into MCP tool arguments or environment variables.',
91
+ why: 'MCP tools can bridge prompt input into external systems, so injected text can become tool instructions.',
92
+ safePattern: 'Treat event text as untrusted data, sanitize it, and require review before passing it to MCP tools.'
93
+ },
94
+ AWG019: {
95
+ detects: 'MCP package specs outside configured trusted package scopes.',
96
+ why: 'Project-scoped MCP packages expand agent capability, so publisher reputation should be reviewed before trust is granted.',
97
+ safePattern: 'Keep trusted package scopes narrow, pin approved packages, and document the review in policy.'
98
+ }
99
+ };
100
+
101
+ export function renderRuleExplanation(ruleId = '') {
102
+ const normalizedRuleId = String(ruleId || '').toUpperCase();
103
+ if (!normalizedRuleId) return renderRuleIndex();
104
+
105
+ const rule = ruleCatalog[normalizedRuleId];
106
+ if (!rule) {
107
+ throw new Error(`unknown rule id: ${normalizedRuleId}. Use awguard explain to list available rules.`);
108
+ }
109
+
110
+ const details = ruleDetails[normalizedRuleId] || {};
111
+ return [
112
+ `# ${normalizedRuleId}: ${rule.title}`,
113
+ '',
114
+ `Severity: **${rule.severity}**`,
115
+ '',
116
+ `Remediation code: \`${rule.remediationCode}\``,
117
+ '',
118
+ `Detects: ${details.detects || rule.title}`,
119
+ '',
120
+ `Why it matters: ${details.why || 'This pattern can increase agentic workflow risk.'}`,
121
+ '',
122
+ `Safe pattern: ${details.safePattern || rule.suggestion}`,
123
+ '',
124
+ `Suggested fix: ${rule.suggestion}`,
125
+ '',
126
+ 'Useful commands:',
127
+ '',
128
+ '```bash',
129
+ 'npx awguard@latest . --format inventory',
130
+ 'npx awguard@latest . --fix-dry-run',
131
+ 'npx awguard@latest . --format sarif --output awguard.sarif --fail-on none',
132
+ '```'
133
+ ].join('\n');
134
+ }
135
+
136
+ function renderRuleIndex() {
137
+ const lines = ['# Agentic Workflow Guard Rules', '', '| Rule | Severity | Remediation Code | Title |', '| --- | --- | --- | --- |'];
138
+ for (const [ruleId, rule] of Object.entries(ruleCatalog)) {
139
+ lines.push(`| ${ruleId} | ${rule.severity} | \`${rule.remediationCode}\` | ${escapeMarkdown(rule.title)} |`);
140
+ }
141
+ lines.push('', 'Run `awguard explain AWG001` for details about one rule.');
142
+ return lines.join('\n');
143
+ }
144
+
145
+ function escapeMarkdown(value) {
146
+ return String(value).replaceAll('|', '\\|');
147
+ }
package/src/graph.js CHANGED
@@ -14,7 +14,8 @@ const impactByRule = {
14
14
  AWG011: 'Suppression policy can hide real risk',
15
15
  AWG012: 'Persistent agent instructions can weaken CI guardrails',
16
16
  AWG013: 'Mutable MCP tool server can change agent capabilities',
17
- AWG014: 'Committed MCP credential can expose external tools or data'
17
+ AWG014: 'Committed MCP credential can expose external tools or data',
18
+ AWG015: 'Unapproved agentic surface can drift outside policy'
18
19
  };
19
20
 
20
21
  export function buildAttackGraphs(result) {
@@ -218,6 +219,7 @@ function inferSource(finding) {
218
219
  if (finding.ruleId === 'AWG012') return 'persistent agent instruction file';
219
220
  if (finding.ruleId === 'AWG013') return 'project-scoped MCP server config';
220
221
  if (finding.ruleId === 'AWG014') return 'committed MCP credential material';
222
+ if (finding.ruleId === 'AWG015') return 'repository policy';
221
223
  return 'workflow configuration';
222
224
  }
223
225
 
@@ -228,6 +230,7 @@ function inferBoundary(finding) {
228
230
  if (finding.ruleId === 'AWG007') return 'command execution sink';
229
231
  if (finding.ruleId === 'AWG012') return 'agent instruction context';
230
232
  if (finding.ruleId === 'AWG013' || finding.ruleId === 'AWG014') return 'MCP tool boundary';
233
+ if (finding.ruleId === 'AWG015') return 'policy allowlist boundary';
231
234
  return 'workflow execution';
232
235
  }
233
236
 
@@ -238,6 +241,7 @@ function inferCapability(finding) {
238
241
  if (finding.ruleId === 'AWG012') return 'persistent prompt steering';
239
242
  if (finding.ruleId === 'AWG013') return 'MCP server startup';
240
243
  if (finding.ruleId === 'AWG014') return 'credentialed MCP tool access';
244
+ if (finding.ruleId === 'AWG015') return 'agentic surface drift';
241
245
  return 'CI runner and agent tools';
242
246
  }
243
247
 
@@ -248,6 +252,7 @@ function inferAuthority(finding) {
248
252
  if (finding.ruleId === 'AWG012') return 'agent policy context';
249
253
  if (finding.ruleId === 'AWG013') return 'developer machine or CI tool process';
250
254
  if (finding.ruleId === 'AWG014') return 'MCP server secrets';
255
+ if (finding.ruleId === 'AWG015') return 'repository policy approval';
251
256
  return 'workflow permissions';
252
257
  }
253
258
 
package/src/init.js ADDED
@@ -0,0 +1,84 @@
1
+ export function renderInitGuide({ actionRef = 'v0' } = {}) {
2
+ return [
3
+ '# Agentic Workflow Guard Setup',
4
+ '',
5
+ 'Create `.github/workflows/awguard.yml`:',
6
+ '',
7
+ '```yaml',
8
+ `name: Agentic Workflow Guard`,
9
+ '',
10
+ 'on:',
11
+ ' pull_request:',
12
+ ' workflow_dispatch:',
13
+ ' schedule:',
14
+ " - cron: '17 4 * * 1'",
15
+ '',
16
+ 'permissions:',
17
+ ' contents: read',
18
+ ' security-events: write',
19
+ '',
20
+ 'jobs:',
21
+ ' scan:',
22
+ ' runs-on: ubuntu-latest',
23
+ ' steps:',
24
+ ' - uses: actions/checkout@v6',
25
+ ` - uses: Mughal-Baig/agentic-workflow-guard@${actionRef}`,
26
+ ' with:',
27
+ ' preset: strict',
28
+ ' format: sarif',
29
+ ' output: awguard.sarif',
30
+ ' fail-on: high',
31
+ ' - uses: github/codeql-action/upload-sarif@v4',
32
+ ' if: always()',
33
+ ' with:',
34
+ ' sarif_file: awguard.sarif',
35
+ ' category: agentic-workflow-guard',
36
+ '```',
37
+ '',
38
+ 'Create `awguard.config.json`:',
39
+ '',
40
+ '```json',
41
+ JSON.stringify(
42
+ {
43
+ $schema:
44
+ 'https://raw.githubusercontent.com/Mughal-Baig/agentic-workflow-guard/main/schemas/awguard.config.schema.json',
45
+ extends: ['strict'],
46
+ policy: {
47
+ approvedFiles: ['AGENTS.md', '.github/workflows/*'],
48
+ approvedMcpServers: [],
49
+ approvedMcpPackages: [],
50
+ approvedMcpPackageScopes: ['@modelcontextprotocol/'],
51
+ approvedMcpCommands: ['npx', 'node', 'uvx', 'docker']
52
+ },
53
+ suppressions: {
54
+ minimumReasonLength: 20
55
+ }
56
+ },
57
+ null,
58
+ 2
59
+ ),
60
+ '```',
61
+ '',
62
+ 'Adopt without breaking existing CI:',
63
+ '',
64
+ '```bash',
65
+ 'npx awguard@latest . --write-baseline awguard.baseline.json --fail-on none',
66
+ 'npx awguard@latest . --baseline awguard.baseline.json --fail-on high',
67
+ '```',
68
+ '',
69
+ 'Generate useful reports:',
70
+ '',
71
+ '```bash',
72
+ 'npx awguard@latest . --format inventory',
73
+ 'npx awguard@latest . --format inventory-json --output awguard-inventory.json',
74
+ 'npx awguard@latest . --format score',
75
+ 'npx awguard@latest . --format badge --output docs/awguard-badge.json',
76
+ '```',
77
+ '',
78
+ 'README badge:',
79
+ '',
80
+ '```markdown',
81
+ '[![AWI risk](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/OWNER/REPO/main/docs/awguard-badge.json)](docs/awguard-badge.json)',
82
+ '```'
83
+ ].join('\n');
84
+ }
package/src/inventory.js CHANGED
@@ -105,6 +105,17 @@ export function renderInventory(result) {
105
105
  return lines.join('\n');
106
106
  }
107
107
 
108
+ export function renderInventoryJson(result) {
109
+ return JSON.stringify(
110
+ {
111
+ root: result.root,
112
+ ...buildInventory(result)
113
+ },
114
+ null,
115
+ 2
116
+ );
117
+ }
118
+
108
119
  function recommendationsFor(surfaces, findings) {
109
120
  const surfaceNames = new Set(surfaces.map((surface) => surface.surface));
110
121
  const rules = new Set(findings.map((finding) => finding.ruleId));
package/src/migration.js CHANGED
@@ -63,6 +63,11 @@ const ruleActions = {
63
63
  'Remove committed MCP tokens, API keys, passwords, and auth headers.',
64
64
  'Use prompted inputs, environment variables, or managed secrets for MCP credentials.',
65
65
  'Rotate credentials that were present in repository history.'
66
+ ],
67
+ AWG015: [
68
+ 'Review the unapproved agentic surface and decide whether it belongs in the repository.',
69
+ 'Add approved files, MCP servers, packages, and commands to policy allowlists.',
70
+ 'Fail CI on policy drift so new agent surfaces are visible in review.'
66
71
  ]
67
72
  };
68
73
 
@@ -180,6 +185,7 @@ function riskShapeFor(findings) {
180
185
  if (rules.has('AWG012')) pieces.push('persistent agent instructions weaken review or permission boundaries');
181
186
  if (rules.has('AWG013')) pieces.push('project MCP config can change agent tool capabilities through mutable startup');
182
187
  if (rules.has('AWG014')) pieces.push('project MCP config contains committed credentials');
188
+ if (rules.has('AWG015')) pieces.push('agentic surface is outside the repository policy');
183
189
 
184
190
  return pieces.length > 0 ? pieces.join('; ') : 'workflow hardening issue';
185
191
  }
@@ -230,6 +236,10 @@ function allowedOperationsFor(findings) {
230
236
  operations.add('MCP credentials supplied by prompt input, environment variable, or secret manager only');
231
237
  }
232
238
 
239
+ if (rules.has('AWG015')) {
240
+ operations.add('policy approval only after reviewing the workflow, agent context, MCP server, package, and command');
241
+ }
242
+
233
243
  operations.add('noop or missing-data report when validation fails');
234
244
  return [...operations];
235
245
  }