@musashimiyamoto/agent-guard 0.4.2 → 0.4.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@musashimiyamoto/agent-guard",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "Security scanner for AI agent configurations. Detects misconfigurations, exposed secrets, and unsafe skill patterns.",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -45,6 +45,6 @@
45
45
  "README.md"
46
46
  ],
47
47
  "dependencies": {
48
- "yaml": "^2.3.0"
48
+ "yaml": "^2.8.2"
49
49
  }
50
50
  }
package/src/cli.js CHANGED
@@ -9,7 +9,7 @@ import { resolve } from 'path';
9
9
  import { createProxy } from './proxy/index.js';
10
10
  import { getLicense, getUpgradePrompt } from './license.js';
11
11
 
12
- const VERSION = '0.4.2';
12
+ const VERSION = '0.4.3';
13
13
 
14
14
  const FEEDBACK_URL = 'https://github.com/MusashiMiyamoto1-cloud/agent-guard/issues/new';
15
15
  const REPO_URL = 'https://github.com/MusashiMiyamoto1-cloud/agent-guard';
@@ -74,6 +74,7 @@ ${COLORS.bold}Options:${COLORS.reset}
74
74
  --json Output results as JSON (Pro)
75
75
  --quiet Only show findings (no banner)
76
76
  --policy <file> Use custom policy file
77
+ --ignore-file <file> Read accepted risks from file (.agentguard.ignore)
77
78
 
78
79
  ${COLORS.bold}Free vs Pro:${COLORS.reset}
79
80
  Free: 50 files, 10 findings, 5 basic rules
@@ -103,6 +104,10 @@ function printScore(report) {
103
104
 
104
105
  const bar = '█'.repeat(Math.floor(score / 5)) + '░'.repeat(20 - Math.floor(score / 5));
105
106
 
107
+ const ignoredInfo = report.ignoredFindings > 0
108
+ ? `\n ${COLORS.gray}Accepted risks: ${report.ignoredFindings} (skipped)${COLORS.reset}`
109
+ : '';
110
+
106
111
  console.log(`
107
112
  ${COLORS.bold}Security Score:${COLORS.reset} ${gradeColor}${score}/100 (${grade})${COLORS.reset}
108
113
 
@@ -114,7 +119,7 @@ ${COLORS.bold}Summary:${COLORS.reset}
114
119
  ${COLORS.yellow}Medium:${COLORS.reset} ${report.summary.medium}
115
120
  ${COLORS.cyan}Low:${COLORS.reset} ${report.summary.low}
116
121
 
117
- Files scanned: ${report.scannedFiles}
122
+ Files scanned: ${report.scannedFiles}${ignoredInfo}
118
123
  `);
119
124
  }
120
125
 
@@ -307,7 +312,15 @@ async function main() {
307
312
 
308
313
  const jsonOutput = args.includes('--json');
309
314
  const quiet = args.includes('--quiet');
310
- const filteredArgs = args.filter(a => !a.startsWith('--'));
315
+
316
+ // Parse --ignore-file option
317
+ let ignoreFile = null;
318
+ const ignoreFileIdx = args.findIndex(a => a === '--ignore-file');
319
+ if (ignoreFileIdx >= 0 && args[ignoreFileIdx + 1]) {
320
+ ignoreFile = args[ignoreFileIdx + 1];
321
+ }
322
+
323
+ const filteredArgs = args.filter((a, i) => !a.startsWith('--') && args[i - 1] !== '--ignore-file');
311
324
 
312
325
  // Load license early
313
326
  const license = await getLicense();
@@ -391,7 +404,7 @@ async function main() {
391
404
  }
392
405
 
393
406
  try {
394
- const report = await scan(targetPath, { license });
407
+ const report = await scan(targetPath, { license, ignoreFile });
395
408
 
396
409
  // Apply license limits
397
410
  const limits = license.getLimits();
package/src/license.js CHANGED
@@ -10,7 +10,7 @@ const CONFIG_DIR = join(homedir(), '.agent-guard');
10
10
  const LICENSE_FILE = join(CONFIG_DIR, 'license.json');
11
11
 
12
12
  // Keygen account & product IDs
13
- const KEYGEN_ACCOUNT = process.env.KEYGEN_ACCOUNT || 'd4e0f169-7857-4b98-85f5-68e56b7a6e1b';
13
+ const KEYGEN_ACCOUNT = process.env.KEYGEN_ACCOUNT || 'musashimiyamoto1';
14
14
  const KEYGEN_PRODUCT = '96d1b566-bd48-4bc9-9185-6ddbcb54683b';
15
15
  const KEYGEN_API = `https://api.keygen.sh/v1/accounts/${KEYGEN_ACCOUNT}`;
16
16
 
@@ -150,17 +150,22 @@ export class License {
150
150
  'FINGERPRINT_SCOPE_MISMATCH': 'License is activated on another machine. Deactivate first or upgrade to Team.',
151
151
  'NO_MACHINES': 'License needs activation. Activating now...',
152
152
  'NO_MACHINE': 'License needs activation. Activating now...',
153
+ 'FINGERPRINT_SCOPE_REQUIRED': 'Activating machine...',
153
154
  'EXPIRED': 'License has expired. Please renew.',
154
155
  'SUSPENDED': 'License has been suspended. Contact support.',
155
156
  'NOT_FOUND': 'Invalid license key.',
156
157
  };
157
158
 
158
- if (code === 'NO_MACHINES' || code === 'NO_MACHINE' || code === 'FINGERPRINT_SCOPE_MISMATCH') {
159
+ // These codes mean the license key itself is valid but needs machine activation
160
+ const needsActivation = ['NO_MACHINES', 'NO_MACHINE', 'FINGERPRINT_SCOPE_MISMATCH', 'FINGERPRINT_SCOPE_REQUIRED'];
161
+
162
+ if (needsActivation.includes(code)) {
159
163
  // Try to activate this machine
160
164
  const activateResult = await this.activateMachine(key, validateData.data?.id);
161
- if (!activateResult.success) {
165
+ if (!activateResult.success && !activateResult.skipMachine) {
162
166
  return activateResult;
163
167
  }
168
+ // If skipMachine is true, the license is valid but policy doesn't require machine activation
164
169
  } else {
165
170
  return {
166
171
  success: false,
@@ -216,11 +221,12 @@ export class License {
216
221
 
217
222
  async activateMachine(licenseKey, licenseId) {
218
223
  try {
224
+ // Use the license key as Bearer token for machine activation
219
225
  const res = await fetch(`${KEYGEN_API}/machines`, {
220
226
  method: 'POST',
221
227
  headers: {
222
- 'Content-Type': 'application/json',
223
- 'Accept': 'application/json',
228
+ 'Content-Type': 'application/vnd.api+json',
229
+ 'Accept': 'application/vnd.api+json',
224
230
  'Authorization': `License ${licenseKey}`,
225
231
  },
226
232
  body: JSON.stringify({
@@ -250,10 +256,15 @@ export class License {
250
256
  message: 'Machine limit reached. Deactivate another machine or upgrade.'
251
257
  };
252
258
  }
259
+ if (err.code === 'FORBIDDEN' || err.detail?.includes('not allowed')) {
260
+ // Policy doesn't allow license key auth - but license is valid
261
+ // Return success to use the license without machine activation
262
+ return { success: true, skipMachine: true };
263
+ }
253
264
  return { success: false, message: err.detail || 'Activation failed' };
254
265
  }
255
266
 
256
- return { success: true };
267
+ return { success: true, machineId: data.data?.id };
257
268
  } catch (err) {
258
269
  return { success: false, message: err.message };
259
270
  }
package/src/scanner.js CHANGED
@@ -2,8 +2,10 @@
2
2
  // Phase 1: Configuration & Secret Scanner
3
3
 
4
4
  import { readdir, readFile, stat } from 'fs/promises';
5
- import { join, basename, extname } from 'path';
5
+ import { join, basename, extname, relative } from 'path';
6
+ import { existsSync } from 'fs';
6
7
  import { rules } from './rules/index.js';
8
+ import { parse as parseYAML } from 'yaml';
7
9
 
8
10
  const IGNORE_DIRS = [
9
11
  'node_modules', '.git', '.venv', '__pycache__',
@@ -21,10 +23,13 @@ export class Scanner {
21
23
  constructor(options = {}) {
22
24
  this.findings = [];
23
25
  this.scannedFiles = 0;
26
+ this.ignoredFindings = 0;
27
+ this.acceptedRisks = [];
24
28
  this.options = {
25
29
  maxFileSize: options.maxFileSize || 1024 * 1024, // 1MB
26
30
  followSymlinks: options.followSymlinks || false,
27
31
  maxFiles: options.maxFiles || Infinity,
32
+ ignoreFile: options.ignoreFile || null,
28
33
  ...options
29
34
  };
30
35
 
@@ -34,9 +39,65 @@ export class Scanner {
34
39
  : rules.filter(r => BASIC_RULE_IDS.includes(r.id));
35
40
  }
36
41
 
42
+ async loadIgnoreFile(basePath) {
43
+ // Look for ignore file
44
+ const ignoreFilePath = this.options.ignoreFile || join(basePath, '.agentguard.ignore');
45
+
46
+ if (!existsSync(ignoreFilePath)) {
47
+ return;
48
+ }
49
+
50
+ try {
51
+ const content = await readFile(ignoreFilePath, 'utf-8');
52
+ const parsed = parseYAML(content);
53
+
54
+ if (parsed?.accepted_risks && Array.isArray(parsed.accepted_risks)) {
55
+ this.acceptedRisks = parsed.accepted_risks;
56
+ }
57
+ } catch (err) {
58
+ // Invalid ignore file, skip
59
+ console.warn(`Warning: Could not parse ${ignoreFilePath}: ${err.message}`);
60
+ }
61
+ }
62
+
63
+ isAcceptedRisk(rule, filePath, lineNum) {
64
+ if (this.acceptedRisks.length === 0) return false;
65
+
66
+ for (const risk of this.acceptedRisks) {
67
+ // Match rule
68
+ if (risk.rule && risk.rule !== rule) continue;
69
+
70
+ // Match file (supports glob patterns)
71
+ if (risk.file) {
72
+ const riskFile = risk.file;
73
+ if (riskFile.includes('*')) {
74
+ // Glob pattern
75
+ const regex = new RegExp('^' + riskFile.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*') + '$');
76
+ if (!regex.test(filePath)) continue;
77
+ } else {
78
+ // Exact match or suffix match
79
+ if (!filePath.endsWith(riskFile) && filePath !== riskFile) continue;
80
+ }
81
+ }
82
+
83
+ // Match line (optional)
84
+ if (risk.line !== undefined && risk.line !== null && risk.line !== lineNum) continue;
85
+
86
+ // All criteria matched - this is an accepted risk
87
+ return true;
88
+ }
89
+
90
+ return false;
91
+ }
92
+
37
93
  async scan(targetPath) {
38
94
  this.findings = [];
39
95
  this.scannedFiles = 0;
96
+ this.ignoredFindings = 0;
97
+ this.basePath = targetPath;
98
+
99
+ // Load accepted risks from ignore file
100
+ await this.loadIgnoreFile(targetPath);
40
101
 
41
102
  const stats = await stat(targetPath);
42
103
 
@@ -191,6 +252,21 @@ export class Scanner {
191
252
  }
192
253
  }
193
254
 
255
+ // Deduplicate key for this finding
256
+ const findingKey = `${rule.id}:${filePath}:${lineNum}`;
257
+
258
+ // Check if this is an accepted risk
259
+ const relPath = this.basePath ? relative(this.basePath, filePath) : filePath;
260
+ if (this.isAcceptedRisk(rule.id, relPath, lineNum) || this.isAcceptedRisk(rule.id, filePath, lineNum)) {
261
+ // Only count once per unique finding
262
+ if (!this.ignoredFindingKeys) this.ignoredFindingKeys = new Set();
263
+ if (!this.ignoredFindingKeys.has(findingKey)) {
264
+ this.ignoredFindingKeys.add(findingKey);
265
+ this.ignoredFindings++;
266
+ }
267
+ return; // Skip accepted risks
268
+ }
269
+
194
270
  // Deduplicate: skip if same rule+file+line already recorded
195
271
  const isDuplicate = this.findings.some(f =>
196
272
  f.rule === rule.id && f.file === filePath && f.line === lineNum
@@ -254,6 +330,8 @@ export class Scanner {
254
330
  grade,
255
331
  scannedFiles: this.scannedFiles,
256
332
  totalFindings: this.findings.length,
333
+ ignoredFindings: this.ignoredFindings,
334
+ acceptedRisks: this.acceptedRisks.length,
257
335
  summary: {
258
336
  critical: critical.length,
259
337
  high: high.length,