awguard 1.5.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/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
 
@@ -315,13 +321,15 @@ export function classifyScanFile(file, root = process.cwd()) {
315
321
 
316
322
  function scanFile(file, root, config) {
317
323
  const text = fs.readFileSync(file, 'utf8');
324
+ let findings;
318
325
  if (isAgentInstructionFile(file, root)) {
319
- return scanAgentInstructionText(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);
320
331
  }
321
- if (isMcpConfigFile(file, root)) {
322
- return scanMcpConfigText(text, file, root, config);
323
- }
324
- return scanWorkflowText(text, file, root, config);
332
+ return [...findings, ...detectFilePolicy(file, root, config)];
325
333
  }
326
334
 
327
335
  function discoverScanFiles(root) {
@@ -553,6 +561,7 @@ function detectMcpConfigRisks(context, config) {
553
561
  for (const server of servers) {
554
562
  detectMutableMcpServer(context, server);
555
563
  detectMcpSecretMaterial(context, server);
564
+ detectMcpPolicy(context, server);
556
565
  }
557
566
  }
558
567
 
@@ -610,6 +619,71 @@ function detectMcpSecretMaterial(context, server) {
610
619
  }
611
620
  }
612
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
+
613
687
  function addFinding(context, ruleId, line, overrides = {}) {
614
688
  const docs = ruleCatalog[ruleId];
615
689
  const ruleConfig = context.config.rules?.[ruleId];
@@ -1207,6 +1281,18 @@ function isPlainObject(value) {
1207
1281
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
1208
1282
  }
1209
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
+
1210
1296
  function windowAround(lines, index, radius) {
1211
1297
  return lines.slice(Math.max(0, index - radius), Math.min(lines.length, index + radius + 1));
1212
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