awguard 1.4.0 → 1.6.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/src/config.js CHANGED
@@ -33,7 +33,8 @@ 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)
37
38
  };
38
39
  }
39
40
 
@@ -51,7 +52,8 @@ function mergePresetConfigs(rawConfig, source) {
51
52
 
52
53
  return mergeConfigObjects(merged, {
53
54
  rules: rawConfig.rules || {},
54
- suppressions: rawConfig.suppressions || {}
55
+ suppressions: rawConfig.suppressions || {},
56
+ policy: rawConfig.policy || {}
55
57
  });
56
58
  }
57
59
 
@@ -64,6 +66,10 @@ function mergeConfigObjects(base, override) {
64
66
  suppressions: {
65
67
  ...(base.suppressions || {}),
66
68
  ...(override.suppressions || {})
69
+ },
70
+ policy: {
71
+ ...(base.policy || {}),
72
+ ...(override.policy || {})
67
73
  }
68
74
  };
69
75
  }
@@ -149,6 +155,27 @@ function normalizeSuppressions(suppressions, source) {
149
155
  };
150
156
  }
151
157
 
158
+ function normalizePolicy(policy, source) {
159
+ if (!isObject(policy)) {
160
+ throw new Error(`${source} policy must be an object`);
161
+ }
162
+
163
+ return {
164
+ approvedFiles: normalizeStringArray(policy.approvedFiles || [], `${source} policy.approvedFiles`),
165
+ approvedMcpServers: normalizeStringArray(policy.approvedMcpServers || [], `${source} policy.approvedMcpServers`),
166
+ approvedMcpPackages: normalizeStringArray(policy.approvedMcpPackages || [], `${source} policy.approvedMcpPackages`),
167
+ approvedMcpCommands: normalizeStringArray(policy.approvedMcpCommands || [], `${source} policy.approvedMcpCommands`)
168
+ };
169
+ }
170
+
171
+ function normalizeStringArray(value, source) {
172
+ if (!Array.isArray(value)) {
173
+ throw new Error(`${source} must be an array`);
174
+ }
175
+
176
+ return value.map((item) => String(item));
177
+ }
178
+
152
179
  function ensureKnownRule(ruleId, source) {
153
180
  if (!ruleCatalog[ruleId]) {
154
181
  throw new Error(`${source} references unknown rule id: ${ruleId}`);
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,81 @@
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@v4',
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
+ extends: ['strict'],
44
+ policy: {
45
+ approvedFiles: ['AGENTS.md', '.github/workflows/*'],
46
+ approvedMcpServers: [],
47
+ approvedMcpPackages: [],
48
+ approvedMcpCommands: ['npx', 'node', 'uvx', 'docker']
49
+ },
50
+ suppressions: {
51
+ minimumReasonLength: 20
52
+ }
53
+ },
54
+ null,
55
+ 2
56
+ ),
57
+ '```',
58
+ '',
59
+ 'Adopt without breaking existing CI:',
60
+ '',
61
+ '```bash',
62
+ 'npx awguard@latest . --write-baseline awguard.baseline.json --fail-on none',
63
+ 'npx awguard@latest . --baseline awguard.baseline.json --fail-on high',
64
+ '```',
65
+ '',
66
+ 'Generate useful reports:',
67
+ '',
68
+ '```bash',
69
+ 'npx awguard@latest . --format inventory',
70
+ 'npx awguard@latest . --format inventory-json --output awguard-inventory.json',
71
+ 'npx awguard@latest . --format score',
72
+ 'npx awguard@latest . --format badge --output docs/awguard-badge.json',
73
+ '```',
74
+ '',
75
+ 'README badge:',
76
+ '',
77
+ '```markdown',
78
+ '[![AWI risk](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/OWNER/REPO/main/docs/awguard-badge.json)](docs/awguard-badge.json)',
79
+ '```'
80
+ ].join('\n');
81
+ }
@@ -0,0 +1,159 @@
1
+ import path from 'node:path';
2
+ import { classifyScanFile, severityRank } from './scanner.js';
3
+
4
+ const surfaceLabels = {
5
+ 'github-workflow': 'GitHub Actions workflows',
6
+ 'agent-context': 'Agent context files',
7
+ 'mcp-config': 'MCP configs',
8
+ other: 'Other scanned files'
9
+ };
10
+
11
+ const surfaceOrder = ['github-workflow', 'agent-context', 'mcp-config', 'other'];
12
+
13
+ export function buildInventory(result) {
14
+ const fileRows = result.scannedFiles.map((file) => {
15
+ const relativeFile = path.relative(result.root, file) || file;
16
+ const surface = classifyScanFile(file, result.root);
17
+ const findings = result.findings.filter((finding) => finding.file === relativeFile);
18
+
19
+ return {
20
+ file: relativeFile,
21
+ surface,
22
+ label: surfaceLabels[surface] || surfaceLabels.other,
23
+ findings: findings.length,
24
+ highest: highestSeverity(findings),
25
+ rules: [...new Set(findings.map((finding) => finding.ruleId))]
26
+ };
27
+ });
28
+
29
+ const surfaces = surfaceOrder
30
+ .map((surface) => {
31
+ const files = fileRows.filter((file) => file.surface === surface);
32
+ const findings = result.findings.filter((finding) => files.some((file) => file.file === finding.file));
33
+
34
+ return {
35
+ surface,
36
+ label: surfaceLabels[surface],
37
+ files: files.length,
38
+ findings: findings.length,
39
+ highest: highestSeverity(findings),
40
+ rules: [...new Set(findings.map((finding) => finding.ruleId))]
41
+ };
42
+ })
43
+ .filter((surface) => surface.files > 0);
44
+
45
+ return {
46
+ summary: {
47
+ scannedFiles: result.scannedFiles.length,
48
+ surfaces: surfaces.length,
49
+ findings: result.findings.length,
50
+ highest: result.summary.highest
51
+ },
52
+ surfaces,
53
+ files: fileRows,
54
+ recommendations: recommendationsFor(surfaces, result.findings)
55
+ };
56
+ }
57
+
58
+ export function renderInventory(result) {
59
+ const inventory = buildInventory(result);
60
+ const lines = [
61
+ '# Agentic Surface Inventory',
62
+ '',
63
+ `Scanned files: **${inventory.summary.scannedFiles}**`,
64
+ `Agentic surfaces: **${inventory.summary.surfaces}**`,
65
+ `Findings: **${inventory.summary.findings}**`,
66
+ `Highest severity: **${inventory.summary.highest}**`,
67
+ '',
68
+ '## Surface Summary',
69
+ '',
70
+ '| Surface | Files | Findings | Highest | Rules |',
71
+ '| --- | ---: | ---: | --- | --- |'
72
+ ];
73
+
74
+ if (inventory.surfaces.length === 0) {
75
+ lines.push('| None found | 0 | 0 | none | |');
76
+ } else {
77
+ for (const surface of inventory.surfaces) {
78
+ lines.push(
79
+ `| ${surface.label} | ${surface.files} | ${surface.findings} | ${surface.highest} | ${
80
+ surface.rules.length > 0 ? surface.rules.join(', ') : ''
81
+ } |`
82
+ );
83
+ }
84
+ }
85
+
86
+ lines.push('', '## Files', '', '| Surface | File | Findings | Highest | Rules |', '| --- | --- | ---: | --- | --- |');
87
+
88
+ if (inventory.files.length === 0) {
89
+ lines.push('| None found | | 0 | none | |');
90
+ } else {
91
+ for (const file of inventory.files) {
92
+ lines.push(
93
+ `| ${file.label} | \`${escapeMarkdown(file.file)}\` | ${file.findings} | ${file.highest} | ${
94
+ file.rules.length > 0 ? file.rules.join(', ') : ''
95
+ } |`
96
+ );
97
+ }
98
+ }
99
+
100
+ lines.push('', '## Recommended Next Steps', '');
101
+ for (const recommendation of inventory.recommendations) {
102
+ lines.push(`- ${recommendation}`);
103
+ }
104
+
105
+ return lines.join('\n');
106
+ }
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
+
119
+ function recommendationsFor(surfaces, findings) {
120
+ const surfaceNames = new Set(surfaces.map((surface) => surface.surface));
121
+ const rules = new Set(findings.map((finding) => finding.ruleId));
122
+ const recommendations = [];
123
+
124
+ if (rules.has('AWG014')) {
125
+ recommendations.push('Remove and rotate committed MCP credentials before widening agent access.');
126
+ }
127
+
128
+ if (rules.has('AWG013')) {
129
+ recommendations.push('Pin MCP server packages, container images, and startup commands before enabling repository-scoped tools.');
130
+ }
131
+
132
+ if (rules.has('AWG012')) {
133
+ recommendations.push('Review persistent agent context files before relying on workflow permission boundaries.');
134
+ }
135
+
136
+ if (!surfaceNames.has('agent-context')) {
137
+ recommendations.push('Add an explicit `AGENTS.md` or `.github/copilot-instructions.md` with conservative safety rules before introducing agents.');
138
+ }
139
+
140
+ if (!surfaceNames.has('mcp-config')) {
141
+ recommendations.push('Keep MCP configs absent until there is a reviewed tool allowlist and credential handling plan.');
142
+ }
143
+
144
+ if (recommendations.length === 0) {
145
+ recommendations.push('Keep this inventory in CI so new agent surfaces are visible during review.');
146
+ }
147
+
148
+ return recommendations;
149
+ }
150
+
151
+ function highestSeverity(findings) {
152
+ return findings.reduce((current, finding) => {
153
+ return severityRank[finding.severity] > severityRank[current] ? finding.severity : current;
154
+ }, 'none');
155
+ }
156
+
157
+ function escapeMarkdown(value) {
158
+ return String(value).replaceAll('|', '\\|');
159
+ }
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
  }
package/src/presets.js CHANGED
@@ -8,7 +8,8 @@ export const presetCatalog = {
8
8
  AWG006: 'critical',
9
9
  AWG008: 'high',
10
10
  AWG010: 'medium',
11
- AWG013: 'critical'
11
+ AWG013: 'critical',
12
+ AWG015: 'high'
12
13
  },
13
14
  suppressions: {
14
15
  minimumReasonLength: 25
@@ -68,6 +68,11 @@ const fixCatalog = {
68
68
  'Move MCP credentials into prompt inputs, environment variables, or a managed secret store.',
69
69
  'Use placeholders such as ${input:token} or ${TOKEN} instead of committed literal values.',
70
70
  'Rotate any token, API key, password, or auth header that was committed.'
71
+ ],
72
+ AWG015: [
73
+ 'Review the new agentic surface before approving it in policy.',
74
+ 'Add reviewed files to policy.approvedFiles and reviewed MCP tools to the MCP policy allowlists.',
75
+ 'Remove or quarantine unapproved workflows, agent instructions, prompts, skills, and MCP configs.'
71
76
  ]
72
77
  };
73
78
 
@@ -181,5 +186,19 @@ run: |
181
186
  };
182
187
  }
183
188
 
189
+ if (finding.ruleId === 'AWG015') {
190
+ return {
191
+ language: 'json',
192
+ text: `{
193
+ "policy": {
194
+ "approvedFiles": ["AGENTS.md", ".github/workflows/*", ".github/agents/*"],
195
+ "approvedMcpServers": ["github"],
196
+ "approvedMcpPackages": ["@modelcontextprotocol/server-github@1.2.3"],
197
+ "approvedMcpCommands": ["npx", "node"]
198
+ }
199
+ }`
200
+ };
201
+ }
202
+
184
203
  return null;
185
204
  }
package/src/reporters.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { findingFingerprint } from './fingerprints.js';
3
3
  import { renderGraphMarkdown, renderHtmlReport } from './graph.js';
4
+ import { renderInventory, renderInventoryJson } from './inventory.js';
4
5
  import { renderMigrationPlan } from './migration.js';
5
6
  import { renderBadgeJson, renderScorecard } from './score.js';
6
7
  import { ruleCatalog } from './scanner.js';
@@ -83,7 +84,7 @@ export function renderSarif(result) {
83
84
  driver: {
84
85
  name: 'Agentic Workflow Guard',
85
86
  informationUri: 'https://github.com/Mughal-Baig/agentic-workflow-guard',
86
- semanticVersion: '1.4.0',
87
+ semanticVersion: '1.6.0',
87
88
  rules: Object.entries(ruleCatalog).map(([id, rule]) => ({
88
89
  id,
89
90
  name: id,
@@ -205,6 +206,14 @@ export function renderBadge(result) {
205
206
  return renderBadgeJson(result);
206
207
  }
207
208
 
209
+ export function renderSurfaceInventory(result) {
210
+ return renderInventory(result);
211
+ }
212
+
213
+ export function renderSurfaceInventoryJson(result) {
214
+ return renderInventoryJson(result);
215
+ }
216
+
208
217
  export function renderGithubAnnotations(result) {
209
218
  if (result.findings.length === 0) {
210
219
  return 'Agentic Workflow Guard: no findings.';
package/src/scanner.js CHANGED
@@ -175,6 +175,12 @@ export const ruleCatalog = {
175
175
  severity: 'critical',
176
176
  suggestion:
177
177
  'Move MCP credentials into input prompts, environment variables, or a secret manager. Do not commit bearer tokens, API keys, passwords, or auth headers in MCP config files.'
178
+ },
179
+ AWG015: {
180
+ title: 'Agentic surface is not approved by policy',
181
+ severity: 'medium',
182
+ suggestion:
183
+ 'Add the workflow, agent context file, MCP config, MCP server, package, or command to the policy allowlist only after review. Otherwise remove or harden it.'
178
184
  }
179
185
  };
180
186
 
@@ -306,15 +312,24 @@ export function scanMcpConfigText(text, file = '.mcp.json', root = process.cwd()
306
312
  return context.findings;
307
313
  }
308
314
 
315
+ export function classifyScanFile(file, root = process.cwd()) {
316
+ if (isMcpConfigFile(file, root)) return 'mcp-config';
317
+ if (isAgentInstructionFile(file, root)) return 'agent-context';
318
+ if (workflowExtensions.has(path.extname(file))) return 'github-workflow';
319
+ return 'other';
320
+ }
321
+
309
322
  function scanFile(file, root, config) {
310
323
  const text = fs.readFileSync(file, 'utf8');
324
+ let findings;
311
325
  if (isAgentInstructionFile(file, root)) {
312
- return scanAgentInstructionText(text, file, root, config);
313
- }
314
- if (isMcpConfigFile(file, root)) {
315
- return scanMcpConfigText(text, file, root, config);
326
+ findings = scanAgentInstructionText(text, file, root, config);
327
+ } else if (isMcpConfigFile(file, root)) {
328
+ findings = scanMcpConfigText(text, file, root, config);
329
+ } else {
330
+ findings = scanWorkflowText(text, file, root, config);
316
331
  }
317
- return scanWorkflowText(text, file, root, config);
332
+ return [...findings, ...detectFilePolicy(file, root, config)];
318
333
  }
319
334
 
320
335
  function discoverScanFiles(root) {
@@ -546,6 +561,7 @@ function detectMcpConfigRisks(context, config) {
546
561
  for (const server of servers) {
547
562
  detectMutableMcpServer(context, server);
548
563
  detectMcpSecretMaterial(context, server);
564
+ detectMcpPolicy(context, server);
549
565
  }
550
566
  }
551
567
 
@@ -603,6 +619,71 @@ function detectMcpSecretMaterial(context, server) {
603
619
  }
604
620
  }
605
621
 
622
+ function detectMcpPolicy(context, server) {
623
+ const policy = context.config.policy || {};
624
+ const command = normalizeCommand(stringValue(server.config.command));
625
+ const args = arrayOfStrings(server.config.args);
626
+ const packageSpec = findMcpPackageSpec(command, args);
627
+
628
+ if (policy.approvedMcpServers?.length > 0 && !policy.approvedMcpServers.includes(server.name)) {
629
+ addFinding(context, 'AWG015', locateMcpLine(context, server, server.name), {
630
+ evidence: `MCP server "${server.name}"`,
631
+ message: `MCP server "${server.name}" is not listed in policy.approvedMcpServers.`
632
+ });
633
+ }
634
+
635
+ if (policy.approvedMcpCommands?.length > 0 && command && !policy.approvedMcpCommands.includes(command)) {
636
+ addFinding(context, 'AWG015', locateMcpLine(context, server, command), {
637
+ evidence: `MCP server "${server.name}" command: ${command}`,
638
+ message: `MCP server "${server.name}" uses a command not listed in policy.approvedMcpCommands.`
639
+ });
640
+ }
641
+
642
+ if (policy.approvedMcpPackages?.length > 0 && packageSpec && !policy.approvedMcpPackages.includes(packageSpec)) {
643
+ addFinding(context, 'AWG015', locateMcpLine(context, server, packageSpec), {
644
+ evidence: `MCP server "${server.name}" package: ${packageSpec}`,
645
+ message: `MCP server "${server.name}" uses a package not listed in policy.approvedMcpPackages.`
646
+ });
647
+ }
648
+ }
649
+
650
+ function detectFilePolicy(file, root, config) {
651
+ const policy = config.policy || {};
652
+ if (!policy.approvedFiles || policy.approvedFiles.length === 0) return [];
653
+
654
+ const relativeFile = path.relative(root, file).split(path.sep).join('/') || path.basename(file);
655
+ if (matchesAnyPolicyPattern(relativeFile, policy.approvedFiles)) return [];
656
+
657
+ const context = createPolicyContext(file, root, config);
658
+ addFinding(context, 'AWG015', 1, {
659
+ evidence: relativeFile,
660
+ message: `${relativeFile} is an agentic surface that is not listed in policy.approvedFiles.`
661
+ });
662
+ return context.findings;
663
+ }
664
+
665
+ function createPolicyContext(file, root, config) {
666
+ return {
667
+ file,
668
+ relativeFile: path.isAbsolute(file) ? path.relative(root, file) || path.basename(file) : file,
669
+ lines: [],
670
+ runBlocks: new Set(),
671
+ triggers: new Set(),
672
+ hasAgent: true,
673
+ hasPromptBoundary: true,
674
+ hasUntrustedTrigger: false,
675
+ hasPermissionBlock: false,
676
+ hasBroadPermission: false,
677
+ hasSecret: false,
678
+ config,
679
+ suppressions: new Map(),
680
+ invalidSuppressions: [],
681
+ suppressedFindings: [],
682
+ findings: [],
683
+ seen: new Set()
684
+ };
685
+ }
686
+
606
687
  function addFinding(context, ruleId, line, overrides = {}) {
607
688
  const docs = ruleCatalog[ruleId];
608
689
  const ruleConfig = context.config.rules?.[ruleId];
@@ -831,6 +912,21 @@ function discoverAgentInstructionFiles(root) {
831
912
  files.push(...walk(githubInstructionsDir).filter((file) => file.endsWith('.instructions.md')));
832
913
  }
833
914
 
915
+ const githubAgentsDir = path.join(root, '.github', 'agents');
916
+ if (fs.existsSync(githubAgentsDir)) {
917
+ files.push(...walk(githubAgentsDir).filter((file) => file.endsWith('.md')));
918
+ }
919
+
920
+ const githubPromptsDir = path.join(root, '.github', 'prompts');
921
+ if (fs.existsSync(githubPromptsDir)) {
922
+ files.push(...walk(githubPromptsDir).filter((file) => file.endsWith('.prompt.md')));
923
+ }
924
+
925
+ const githubSkillsDir = path.join(root, '.github', 'skills');
926
+ if (fs.existsSync(githubSkillsDir)) {
927
+ files.push(...walk(githubSkillsDir).filter((file) => path.basename(file).toLowerCase() === 'skill.md'));
928
+ }
929
+
834
930
  const cursorRulesDir = path.join(root, '.cursor', 'rules');
835
931
  if (fs.existsSync(cursorRulesDir)) {
836
932
  files.push(...walk(cursorRulesDir).filter((file) => ['.md', '.mdc', '.txt'].includes(path.extname(file))));
@@ -859,6 +955,12 @@ function isAgentInstructionFile(file, root) {
859
955
  /\/\.github\/copilot-instructions\.md$/i.test(normalizedFile) ||
860
956
  /^\.github\/instructions\/.+\.instructions\.md$/i.test(relativeFile) ||
861
957
  /\/\.github\/instructions\/.+\.instructions\.md$/i.test(normalizedFile) ||
958
+ /^\.github\/agents\/.+\.md$/i.test(relativeFile) ||
959
+ /\/\.github\/agents\/.+\.md$/i.test(normalizedFile) ||
960
+ /^\.github\/prompts\/.+\.prompt\.md$/i.test(relativeFile) ||
961
+ /\/\.github\/prompts\/.+\.prompt\.md$/i.test(normalizedFile) ||
962
+ /^\.github\/skills\/.+\/skill\.md$/i.test(relativeFile) ||
963
+ /\/\.github\/skills\/.+\/skill\.md$/i.test(normalizedFile) ||
862
964
  /^\.cursor\/rules\/.+\.(?:md|mdc|txt)$/i.test(relativeFile) ||
863
965
  /\/\.cursor\/rules\/.+\.(?:md|mdc|txt)$/i.test(normalizedFile)
864
966
  );
@@ -1179,6 +1281,18 @@ function isPlainObject(value) {
1179
1281
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
1180
1282
  }
1181
1283
 
1284
+ function matchesAnyPolicyPattern(value, patterns) {
1285
+ return patterns.some((pattern) => wildcardToRegExp(pattern).test(value));
1286
+ }
1287
+
1288
+ function wildcardToRegExp(pattern) {
1289
+ const escaped = String(pattern)
1290
+ .split('*')
1291
+ .map((part) => escapeRegex(part))
1292
+ .join('.*');
1293
+ return new RegExp(`^${escaped}$`);
1294
+ }
1295
+
1182
1296
  function windowAround(lines, index, radius) {
1183
1297
  return lines.slice(Math.max(0, index - radius), Math.min(lines.length, index + radius + 1));
1184
1298
  }
package/src/score.js CHANGED
@@ -114,6 +114,9 @@ function actionFor(result, counts) {
114
114
  }
115
115
 
116
116
  if (counts.medium > 0) {
117
+ if (rules.has('AWG015')) {
118
+ return 'Review policy drift and approve only expected agentic surfaces.';
119
+ }
117
120
  return 'Tighten explicit permissions, artifact boundaries, and suppression policy.';
118
121
  }
119
122