hackmyagent 0.11.7 → 0.11.9

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.
@@ -111,6 +111,19 @@ const CHECK_PROJECT_TYPES = {
111
111
  'AITOOL-': ['all'], // AI tooling exposure (Jupyter, Gradio, etc.)
112
112
  'A2A-': ['all'], // A2A protocol exposure
113
113
  'WEBCRED-': ['all'], // Credentials in web-served files
114
+ // Code injection and supply chain checks
115
+ 'CODEINJ-': ['all'], // Code injection via exec with interpolation
116
+ 'INSTALL-': ['all'], // Unsafe install scripts (curl|sh)
117
+ 'CLIPASS-': ['all'], // Credentials passed as CLI arguments
118
+ 'INTEGRITY-': ['all'], // Integrity check bypass
119
+ 'TOCTOU-': ['all'], // Time-of-check-time-of-use race conditions
120
+ 'TMPPATH-': ['all'], // Hardcoded /tmp path attacks
121
+ 'DOCKERINJ-': ['all'], // Docker exec with variable injection
122
+ 'ENVLEAK-': ['all'], // Environment variable leakage to child processes
123
+ 'SANDBOX-005': ['openclaw', 'mcp'], // Messaging API pre-allowed in sandbox
124
+ 'WEBEXPOSE-': ['all'], // Sensitive files in web-served directories
125
+ 'AGENT-CRED-': ['all'], // Missing credential protection in system prompts
126
+ 'SOUL-OVERRIDE-': ['all'], // Skill content overriding SOUL.md
114
127
  };
115
128
  // Patterns for detecting exposed credentials
116
129
  // Each pattern is carefully tuned to minimize false positives
@@ -273,6 +286,10 @@ class HardeningScanner {
273
286
  async scan(options) {
274
287
  const { targetDir, autoFix = false, dryRun = false, ignore = [], cliName = 'hackmyagent' } = options;
275
288
  this.cliName = cliName;
289
+ // Resolve effective scan depth — --deep flag implies 'deep' depth
290
+ const scanDepth = options.scanDepth || (options.deep ? 'deep' : 'standard');
291
+ const isQuick = scanDepth === 'quick';
292
+ const isDeepScan = scanDepth === 'deep';
276
293
  // Load .hmaignore for path-based exclusions
277
294
  const hmaIgnorePaths = await this.loadHmaIgnore(targetDir);
278
295
  // Merge with any programmatic ignorePaths
@@ -311,150 +328,179 @@ class HardeningScanner {
311
328
  // Network security checks
312
329
  const netFindings = await this.checkNetworkSecurity(targetDir, shouldFix);
313
330
  findings.push(...netFindings);
314
- // Additional MCP checks
315
- const mcpAdvFindings = await this.checkMcpAdvanced(targetDir, shouldFix);
316
- findings.push(...mcpAdvFindings);
317
- // Claude Code advanced checks
318
- const claudeAdvFindings = await this.checkClaudeAdvanced(targetDir, shouldFix);
319
- findings.push(...claudeAdvFindings);
320
- // Cursor configuration checks
321
- const cursorFindings = await this.checkCursorConfig(targetDir, shouldFix);
322
- findings.push(...cursorFindings);
323
- // VSCode configuration checks
324
- const vscodeFindings = await this.checkVscodeConfig(targetDir, shouldFix);
325
- findings.push(...vscodeFindings);
326
- // Additional credential checks
327
- const credAdvFindings = await this.checkCredentialsAdvanced(targetDir, shouldFix);
328
- findings.push(...credAdvFindings);
329
- // Additional permission checks
330
- const permAdvFindings = await this.checkPermissionsAdvanced(targetDir, shouldFix);
331
- findings.push(...permAdvFindings);
332
- // Environment and config checks
333
- const envFindings = await this.checkEnvironmentSecurity(targetDir, shouldFix);
334
- findings.push(...envFindings);
335
- // Logging and audit checks
336
- const logFindings = await this.checkLoggingSecurity(targetDir, shouldFix);
337
- findings.push(...logFindings);
338
- // Dependency checks
339
- const depFindings = await this.checkDependencySecurity(targetDir, shouldFix);
340
- findings.push(...depFindings);
341
- // Session and auth checks
342
- const authFindings = await this.checkAuthSecurity(targetDir, shouldFix);
343
- findings.push(...authFindings);
344
- // Process and runtime checks
345
- const procFindings = await this.checkProcessSecurity(targetDir, shouldFix);
346
- findings.push(...procFindings);
347
- // Additional Claude checks
348
- const claude3Findings = await this.checkClaudeExtended(targetDir, shouldFix);
349
- findings.push(...claude3Findings);
350
- // Additional MCP checks
351
- const mcp2Findings = await this.checkMcpExtended(targetDir, shouldFix);
352
- findings.push(...mcp2Findings);
353
- // Additional network checks
354
- const net2Findings = await this.checkNetworkExtended(targetDir, shouldFix);
355
- findings.push(...net2Findings);
356
- // Input/output security checks
357
- const ioFindings = await this.checkIOSecurity(targetDir, shouldFix);
358
- findings.push(...ioFindings);
359
- // API security checks
360
- const apiFindings = await this.checkAPISecurity(targetDir, shouldFix);
361
- findings.push(...apiFindings);
362
- // Secret management checks
363
- const secretFindings = await this.checkSecretManagement(targetDir, shouldFix);
364
- findings.push(...secretFindings);
365
- // Prompt injection defense checks
366
- const promptFindings = await this.checkPromptSecurity(targetDir, shouldFix);
367
- findings.push(...promptFindings);
368
- // Input validation checks
369
- const injFindings = await this.checkInputValidation(targetDir, shouldFix);
370
- findings.push(...injFindings);
371
- // Rate limiting checks
372
- const rateFindings = await this.checkRateLimiting(targetDir, shouldFix);
373
- findings.push(...rateFindings);
374
- // Session security checks
375
- const sessionFindings = await this.checkSessionSecurity(targetDir, shouldFix);
376
- findings.push(...sessionFindings);
377
- // Encryption checks
378
- const encryptFindings = await this.checkEncryption(targetDir, shouldFix);
379
- findings.push(...encryptFindings);
380
- // Audit trail checks
381
- const auditFindings = await this.checkAuditTrail(targetDir, shouldFix);
382
- findings.push(...auditFindings);
383
- // Sandboxing checks
384
- const sandboxFindings = await this.checkSandboxing(targetDir, shouldFix);
385
- findings.push(...sandboxFindings);
386
- // Tool boundary checks
387
- const toolFindings = await this.checkToolBoundaries(targetDir, shouldFix);
388
- findings.push(...toolFindings);
389
- // OpenClaw skill checks
390
- const skillFindings = await this.checkOpenclawSkills(targetDir, shouldFix);
391
- findings.push(...skillFindings);
392
- // OpenClaw heartbeat checks
393
- const heartbeatFindings = await this.checkOpenclawHeartbeat(targetDir, shouldFix);
394
- findings.push(...heartbeatFindings);
395
- // OpenClaw gateway checks
396
- const gatewayFindings = await this.checkOpenclawGateway(targetDir, shouldFix);
397
- findings.push(...gatewayFindings);
398
- // OpenClaw config checks
399
- const configFindings = await this.checkOpenclawConfig(targetDir, shouldFix);
400
- findings.push(...configFindings);
401
- // OpenClaw supply chain checks
402
- const supplyFindings = await this.checkOpenclawSupplyChain(targetDir, shouldFix);
403
- findings.push(...supplyFindings);
404
- // OpenClaw CVE-specific checks
405
- const cveFindings = await this.checkOpenclawCVE(targetDir, shouldFix);
406
- findings.push(...cveFindings);
407
- // Unicode steganography checks (GlassWorm detection)
408
- const unicodeStegoFindings = await this.checkUnicodeSteganography(targetDir, shouldFix);
409
- findings.push(...unicodeStegoFindings);
410
- // Memory/context poisoning checks
411
- const memFindings = await this.checkMemoryPoisoning(targetDir, shouldFix);
412
- findings.push(...memFindings);
413
- // RAG poisoning checks
414
- const ragFindings = await this.checkRAGPoisoning(targetDir, shouldFix);
415
- findings.push(...ragFindings);
416
- // Agent identity checks
417
- const aimFindings = await this.checkAgentIdentity(targetDir, shouldFix);
418
- findings.push(...aimFindings);
419
- // Agent DNA integrity checks
420
- const dnaFindings = await this.checkAgentDNA(targetDir, shouldFix);
421
- findings.push(...dnaFindings);
422
- // Skill memory manipulation checks
423
- const skillMemFindings = await this.checkSkillMemory(targetDir, shouldFix);
424
- findings.push(...skillMemFindings);
425
- // NemoClaw codebase pattern checks
426
- const nemoFindings = await this.checkNemoClawPatterns(targetDir, shouldFix);
427
- findings.push(...nemoFindings);
428
- // AI infrastructure exposure checks (research gap coverage)
429
- const llmFindings = await this.checkLLMExposure(targetDir, shouldFix);
430
- findings.push(...llmFindings);
431
- const aiToolFindings = await this.checkAIToolExposure(targetDir, shouldFix);
432
- findings.push(...aiToolFindings);
433
- const a2aFindings = await this.checkA2AExposure(targetDir, shouldFix);
434
- findings.push(...a2aFindings);
435
- const mcpDiscoveryFindings = await this.checkMCPDiscovery(targetDir, shouldFix);
436
- findings.push(...mcpDiscoveryFindings);
437
- const webCredFindings = await this.checkWebServedCredentials(targetDir, shouldFix);
438
- findings.push(...webCredFindings);
331
+ // --- Standard and Deep checks (skipped in quick mode) ---
332
+ if (!isQuick) {
333
+ // Additional MCP checks
334
+ const mcpAdvFindings = await this.checkMcpAdvanced(targetDir, shouldFix);
335
+ findings.push(...mcpAdvFindings);
336
+ // Claude Code advanced checks
337
+ const claudeAdvFindings = await this.checkClaudeAdvanced(targetDir, shouldFix);
338
+ findings.push(...claudeAdvFindings);
339
+ // Cursor configuration checks
340
+ const cursorFindings = await this.checkCursorConfig(targetDir, shouldFix);
341
+ findings.push(...cursorFindings);
342
+ // VSCode configuration checks
343
+ const vscodeFindings = await this.checkVscodeConfig(targetDir, shouldFix);
344
+ findings.push(...vscodeFindings);
345
+ // Additional credential checks
346
+ const credAdvFindings = await this.checkCredentialsAdvanced(targetDir, shouldFix);
347
+ findings.push(...credAdvFindings);
348
+ // Additional permission checks
349
+ const permAdvFindings = await this.checkPermissionsAdvanced(targetDir, shouldFix);
350
+ findings.push(...permAdvFindings);
351
+ // Environment and config checks
352
+ const envFindings = await this.checkEnvironmentSecurity(targetDir, shouldFix);
353
+ findings.push(...envFindings);
354
+ // Logging and audit checks
355
+ const logFindings = await this.checkLoggingSecurity(targetDir, shouldFix);
356
+ findings.push(...logFindings);
357
+ // Dependency checks
358
+ const depFindings = await this.checkDependencySecurity(targetDir, shouldFix);
359
+ findings.push(...depFindings);
360
+ // Session and auth checks
361
+ const authFindings = await this.checkAuthSecurity(targetDir, shouldFix);
362
+ findings.push(...authFindings);
363
+ // Process and runtime checks
364
+ const procFindings = await this.checkProcessSecurity(targetDir, shouldFix);
365
+ findings.push(...procFindings);
366
+ // Additional Claude checks
367
+ const claude3Findings = await this.checkClaudeExtended(targetDir, shouldFix);
368
+ findings.push(...claude3Findings);
369
+ // Additional MCP checks
370
+ const mcp2Findings = await this.checkMcpExtended(targetDir, shouldFix);
371
+ findings.push(...mcp2Findings);
372
+ // Additional network checks
373
+ const net2Findings = await this.checkNetworkExtended(targetDir, shouldFix);
374
+ findings.push(...net2Findings);
375
+ // Input/output security checks
376
+ const ioFindings = await this.checkIOSecurity(targetDir, shouldFix);
377
+ findings.push(...ioFindings);
378
+ // API security checks
379
+ const apiFindings = await this.checkAPISecurity(targetDir, shouldFix);
380
+ findings.push(...apiFindings);
381
+ // Secret management checks
382
+ const secretFindings = await this.checkSecretManagement(targetDir, shouldFix);
383
+ findings.push(...secretFindings);
384
+ // Prompt injection defense checks
385
+ const promptFindings = await this.checkPromptSecurity(targetDir, shouldFix);
386
+ findings.push(...promptFindings);
387
+ // Input validation checks
388
+ const injFindings = await this.checkInputValidation(targetDir, shouldFix);
389
+ findings.push(...injFindings);
390
+ // Rate limiting checks
391
+ const rateFindings = await this.checkRateLimiting(targetDir, shouldFix);
392
+ findings.push(...rateFindings);
393
+ // Session security checks
394
+ const sessionFindings = await this.checkSessionSecurity(targetDir, shouldFix);
395
+ findings.push(...sessionFindings);
396
+ // Encryption checks
397
+ const encryptFindings = await this.checkEncryption(targetDir, shouldFix);
398
+ findings.push(...encryptFindings);
399
+ // Audit trail checks
400
+ const auditFindings = await this.checkAuditTrail(targetDir, shouldFix);
401
+ findings.push(...auditFindings);
402
+ // Sandboxing checks
403
+ const sandboxFindings = await this.checkSandboxing(targetDir, shouldFix);
404
+ findings.push(...sandboxFindings);
405
+ // Tool boundary checks
406
+ const toolFindings = await this.checkToolBoundaries(targetDir, shouldFix);
407
+ findings.push(...toolFindings);
408
+ // OpenClaw skill checks
409
+ const skillFindings = await this.checkOpenclawSkills(targetDir, shouldFix);
410
+ findings.push(...skillFindings);
411
+ // OpenClaw heartbeat checks
412
+ const heartbeatFindings = await this.checkOpenclawHeartbeat(targetDir, shouldFix);
413
+ findings.push(...heartbeatFindings);
414
+ // OpenClaw gateway checks
415
+ const gatewayFindings = await this.checkOpenclawGateway(targetDir, shouldFix);
416
+ findings.push(...gatewayFindings);
417
+ // OpenClaw config checks
418
+ const configFindings = await this.checkOpenclawConfig(targetDir, shouldFix);
419
+ findings.push(...configFindings);
420
+ // OpenClaw supply chain checks
421
+ const supplyFindings = await this.checkOpenclawSupplyChain(targetDir, shouldFix);
422
+ findings.push(...supplyFindings);
423
+ // OpenClaw CVE-specific checks
424
+ const cveFindings = await this.checkOpenclawCVE(targetDir, shouldFix);
425
+ findings.push(...cveFindings);
426
+ // Unicode steganography checks (GlassWorm detection)
427
+ const unicodeStegoFindings = await this.checkUnicodeSteganography(targetDir, shouldFix);
428
+ findings.push(...unicodeStegoFindings);
429
+ // Memory/context poisoning checks
430
+ const memFindings = await this.checkMemoryPoisoning(targetDir, shouldFix);
431
+ findings.push(...memFindings);
432
+ // RAG poisoning checks
433
+ const ragFindings = await this.checkRAGPoisoning(targetDir, shouldFix);
434
+ findings.push(...ragFindings);
435
+ // Agent identity checks
436
+ const aimFindings = await this.checkAgentIdentity(targetDir, shouldFix);
437
+ findings.push(...aimFindings);
438
+ // Agent DNA integrity checks
439
+ const dnaFindings = await this.checkAgentDNA(targetDir, shouldFix);
440
+ findings.push(...dnaFindings);
441
+ // Skill memory manipulation checks
442
+ const skillMemFindings = await this.checkSkillMemory(targetDir, shouldFix);
443
+ findings.push(...skillMemFindings);
444
+ // NemoClaw codebase pattern checks
445
+ const nemoFindings = await this.checkNemoClawPatterns(targetDir, shouldFix);
446
+ findings.push(...nemoFindings);
447
+ // AI infrastructure exposure checks (research gap coverage)
448
+ const llmFindings = await this.checkLLMExposure(targetDir, shouldFix);
449
+ findings.push(...llmFindings);
450
+ const aiToolFindings = await this.checkAIToolExposure(targetDir, shouldFix);
451
+ findings.push(...aiToolFindings);
452
+ const a2aFindings = await this.checkA2AExposure(targetDir, shouldFix);
453
+ findings.push(...a2aFindings);
454
+ const mcpDiscoveryFindings = await this.checkMCPDiscovery(targetDir, shouldFix);
455
+ findings.push(...mcpDiscoveryFindings);
456
+ const webCredFindings = await this.checkWebServedCredentials(targetDir, shouldFix);
457
+ findings.push(...webCredFindings);
458
+ // Code injection, supply chain, and operational security checks
459
+ // NOTE: CODEINJ-001 removed — deduplicated with NEMO-005 (same detection)
460
+ const installFindings = await this.checkInstallScripts(targetDir, shouldFix);
461
+ findings.push(...installFindings);
462
+ const cliPassFindings = await this.checkCLICredentialPassthrough(targetDir, shouldFix);
463
+ findings.push(...cliPassFindings);
464
+ const integrityFindings = await this.checkIntegrityBypass(targetDir, shouldFix);
465
+ findings.push(...integrityFindings);
466
+ const toctouFindings = await this.checkTOCTOU(targetDir, shouldFix);
467
+ findings.push(...toctouFindings);
468
+ // NOTE: TMPPATH-001 removed — deduplicated with NEMO-006 (same detection)
469
+ const dockerInjFindings = await this.checkDockerInjection(targetDir, shouldFix);
470
+ findings.push(...dockerInjFindings);
471
+ // NOTE: ENVLEAK-001 removed — deduplicated with NEMO-007 (same detection)
472
+ const sandboxMsgFindings = await this.checkSandboxMessaging(targetDir, shouldFix);
473
+ findings.push(...sandboxMsgFindings);
474
+ const webExposeFindings = await this.checkWebExposedFiles(targetDir, shouldFix);
475
+ findings.push(...webExposeFindings);
476
+ const soulOverrideFindings = await this.checkSoulOverride(targetDir, shouldFix);
477
+ findings.push(...soulOverrideFindings);
478
+ const memSanitizeFindings = await this.checkMemoryStoreSanitization(targetDir, shouldFix);
479
+ findings.push(...memSanitizeFindings);
480
+ const agentCredFindings = await this.checkAgentCredentialProtection(targetDir, shouldFix);
481
+ findings.push(...agentCredFindings);
482
+ } // end of standard/deep checks
439
483
  // Enrich findings with attack taxonomy mapping
440
484
  (0, taxonomy_1.enrichWithTaxonomy)(findings);
441
- // Layer 2: Structural analysis (always on)
485
+ // Layer 2: Structural analysis (standard and deep only)
442
486
  let layer2Count = 0;
443
487
  let layer3Count = 0;
444
488
  let llmCost;
445
489
  let cachedResults;
446
- try {
447
- const structural = new semantic_1.StructuralAnalyzer();
448
- const structuralFindings = await structural.analyze(targetDir);
449
- const converted = (0, semantic_1.toSecurityFindings)(structuralFindings);
450
- findings.push(...converted);
451
- layer2Count = converted.length;
452
- }
453
- catch {
454
- // Structural analysis failure is non-fatal
490
+ if (!isQuick) {
491
+ try {
492
+ const structural = new semantic_1.StructuralAnalyzer();
493
+ const structuralFindings = await structural.analyze(targetDir);
494
+ const converted = (0, semantic_1.toSecurityFindings)(structuralFindings);
495
+ findings.push(...converted);
496
+ layer2Count = converted.length;
497
+ }
498
+ catch {
499
+ // Structural analysis failure is non-fatal
500
+ }
455
501
  }
456
- // Layer 3: LLM analysis (only with --deep + API key in CLI mode)
457
- if (options.deep && process.env.ANTHROPIC_API_KEY) {
502
+ // Layer 3: LLM analysis (only in deep mode + API key)
503
+ if ((isDeepScan || options.deep) && process.env.ANTHROPIC_API_KEY) {
458
504
  try {
459
505
  const structural = new semantic_1.StructuralAnalyzer();
460
506
  const files = await structural.discoverFiles(targetDir);
@@ -794,7 +840,8 @@ class HardeningScanner {
794
840
  line: firstLine,
795
841
  fixable: true,
796
842
  fixed: fileModified,
797
- fix: `Run \`${this.cliName} secure --fix\` to replace the hardcoded credential with a \${ENV_VAR} reference, then store the actual value in your .env file`,
843
+ fix: `${this.cliName} secure --fix`,
844
+ guidance: 'Replaces hardcoded credentials with ${ENV_VAR} references. Store actual values in your .env file, which should be in .gitignore.',
798
845
  });
799
846
  }
800
847
  }
@@ -846,7 +893,8 @@ class HardeningScanner {
846
893
  file: 'CLAUDE.md',
847
894
  line: credentialLine,
848
895
  fixable: false,
849
- fix: 'Manually move the credential to a .env file and reference it as ${ENV_VAR}. CLAUDE.md may be committed to git and exposed publicly',
896
+ fix: 'npx secretless-ai init',
897
+ guidance: 'CLAUDE.md is often committed to git and exposed publicly. Move credentials to a .env file and reference as ${ENV_VAR}. Secretless AI blocks credential access from AI tool context.',
850
898
  });
851
899
  }
852
900
  }
@@ -912,7 +960,8 @@ class HardeningScanner {
912
960
  file: 'mcp.json',
913
961
  fixable: true,
914
962
  fixed: mcp001Fixed,
915
- fix: `Run \`${this.cliName} secure --fix\` to restrict filesystem access from / or ~ to project-relative paths (./data or ./)`,
963
+ fix: `${this.cliName} secure --fix`,
964
+ guidance: 'Root or home directory access lets MCP servers read/write any file on the system. Restrict to project-relative paths (./data or ./) to limit blast radius.',
916
965
  });
917
966
  }
918
967
  if (hasUnrestrictedShell) {
@@ -926,7 +975,8 @@ class HardeningScanner {
926
975
  message: 'Add allowedCommands to restrict shell access',
927
976
  file: 'mcp.json',
928
977
  fixable: false,
929
- fix: 'Manually add an "allowedCommands" array to your shell server config in mcp.json to whitelist specific commands (e.g., ["ls", "cat", "grep"])',
978
+ fix: 'Add "allowedCommands": ["ls", "cat", "grep"] to the shell server config in mcp.json',
979
+ guidance: 'Unrestricted shell access lets the AI execute any command including destructive operations. Whitelisting specific commands limits what can be run.',
930
980
  });
931
981
  }
932
982
  }
@@ -978,6 +1028,7 @@ class HardeningScanner {
978
1028
  fixed: autoFix && !passed,
979
1029
  fixMessage: autoFix && !passed ? 'Changed permissions to 600' : undefined,
980
1030
  details: passed ? undefined : { files: permissionIssues },
1031
+ guidance: 'Overly broad file permissions let any user on the system read sensitive config files that may contain credentials or API keys.',
981
1032
  });
982
1033
  return findings;
983
1034
  }
@@ -1031,39 +1082,44 @@ dist/
1031
1082
  file: '.gitignore',
1032
1083
  fixable: true,
1033
1084
  fixed: git001Fixed,
1034
- fix: `Run \`${this.cliName} secure --fix\` to create a .gitignore with security patterns (.env, secrets.json, *.pem, *.key) to prevent accidental commits`,
1085
+ fix: `${this.cliName} secure --fix`,
1086
+ guidance: 'Without .gitignore, sensitive files (.env, secrets.json, *.pem, *.key) can be accidentally committed to version control and exposed.',
1035
1087
  });
1036
1088
  }
1037
1089
  // GIT-002: Check for missing sensitive patterns in .gitignore
1038
- const sensitivePatterns = ['.env', 'secrets.json', '*.pem', '*.key'];
1039
- const missingPatterns = [];
1040
- for (const pattern of sensitivePatterns) {
1041
- if (!gitignoreContent.includes(pattern) && !gitignoreContent.includes(pattern.replace('*', ''))) {
1042
- missingPatterns.push(pattern);
1090
+ // Only check if .gitignore exists — GIT-001 handles creation
1091
+ if (gitignoreExists) {
1092
+ const sensitivePatterns = ['.env', 'secrets.json', '*.pem', '*.key'];
1093
+ const missingPatterns = [];
1094
+ for (const pattern of sensitivePatterns) {
1095
+ if (!gitignoreContent.includes(pattern) && !gitignoreContent.includes(pattern.replace('*', ''))) {
1096
+ missingPatterns.push(pattern);
1097
+ }
1098
+ }
1099
+ let git002Fixed = false;
1100
+ if (missingPatterns.length > 0 && autoFix) {
1101
+ const patternsToAdd = '\n# Security patterns (auto-added)\n' + missingPatterns.join('\n') + '\n';
1102
+ gitignoreContent += patternsToAdd;
1103
+ await fs.writeFile(gitignorePath, gitignoreContent);
1104
+ git002Fixed = true;
1105
+ }
1106
+ // Only report if patterns are missing
1107
+ if (missingPatterns.length > 0) {
1108
+ findings.push({
1109
+ checkId: 'GIT-002',
1110
+ name: 'Incomplete .gitignore',
1111
+ description: `Missing: ${missingPatterns.join(', ')}`,
1112
+ category: 'git',
1113
+ severity: 'high',
1114
+ passed: git002Fixed,
1115
+ message: `Add patterns: ${missingPatterns.join(', ')}`,
1116
+ file: '.gitignore',
1117
+ fixable: true,
1118
+ fixed: git002Fixed,
1119
+ fix: `${this.cliName} secure --fix`,
1120
+ guidance: `Missing patterns (${missingPatterns.join(', ')}) in .gitignore mean sensitive files could be accidentally committed and pushed to remote repositories.`,
1121
+ });
1043
1122
  }
1044
- }
1045
- let git002Fixed = false;
1046
- if (missingPatterns.length > 0 && autoFix) {
1047
- const patternsToAdd = '\n# Security patterns (auto-added)\n' + missingPatterns.join('\n') + '\n';
1048
- gitignoreContent += patternsToAdd;
1049
- await fs.writeFile(gitignorePath, gitignoreContent);
1050
- git002Fixed = true;
1051
- }
1052
- // Only report if patterns are missing
1053
- if (missingPatterns.length > 0) {
1054
- findings.push({
1055
- checkId: 'GIT-002',
1056
- name: 'Incomplete .gitignore',
1057
- description: `Missing: ${missingPatterns.join(', ')}`,
1058
- category: 'git',
1059
- severity: 'high',
1060
- passed: git002Fixed,
1061
- message: `Add patterns: ${missingPatterns.join(', ')}`,
1062
- file: '.gitignore',
1063
- fixable: true,
1064
- fixed: git002Fixed,
1065
- fix: `Run \`${this.cliName} secure --fix\` to add ${missingPatterns.join(', ')} to .gitignore so sensitive files won't be accidentally committed`,
1066
- });
1067
1123
  }
1068
1124
  // GIT-003: Check if .env exists but not in .gitignore
1069
1125
  let envExists = false;
@@ -1098,7 +1154,8 @@ dist/
1098
1154
  file: '.env',
1099
1155
  fixable: true,
1100
1156
  fixed: git003Fixed,
1101
- fix: `Run \`${this.cliName} secure --fix\` to add .env to .gitignore so your environment variables won't be accidentally committed`,
1157
+ fix: `${this.cliName} secure --fix`,
1158
+ guidance: '.env files contain API keys and secrets. Without .gitignore protection, a single git add . can expose all credentials in your repository history.',
1102
1159
  });
1103
1160
  }
1104
1161
  return findings;
@@ -1143,7 +1200,8 @@ dist/
1143
1200
  file: 'mcp.json',
1144
1201
  fixable: true,
1145
1202
  fixed: net001Fixed,
1146
- fix: `Run \`${this.cliName} secure --fix\` to change 0.0.0.0 to 127.0.0.1 so the server only accepts local connections instead of being exposed to the network`,
1203
+ fix: `${this.cliName} secure --fix`,
1204
+ guidance: 'Binding to 0.0.0.0 exposes the server to the entire network. Use 127.0.0.1 for local-only access. If remote access is needed, use a reverse proxy with authentication.',
1147
1205
  });
1148
1206
  }
1149
1207
  // NET-002: Check for remote MCP servers without TLS
@@ -1168,7 +1226,8 @@ dist/
1168
1226
  message: 'Change http:// to https://',
1169
1227
  file: 'mcp.json',
1170
1228
  fixable: false,
1171
- fix: 'Manually change http:// to https:// in mcp.json to encrypt traffic and prevent man-in-the-middle attacks',
1229
+ fix: 'Update URL to https:// in mcp.json',
1230
+ guidance: 'HTTP traffic is unencrypted and vulnerable to man-in-the-middle attacks. An attacker on the network can intercept and modify MCP server communications.',
1172
1231
  });
1173
1232
  }
1174
1233
  return findings;
@@ -1236,7 +1295,8 @@ dist/
1236
1295
  file: 'mcp.json',
1237
1296
  fixable: true,
1238
1297
  fixed: mcp003Fixed,
1239
- fix: `Run \`${this.cliName} secure --fix\` to replace hardcoded API keys with \${ENV_VAR} references, then store actual values in .env file`,
1298
+ fix: `${this.cliName} secure --fix`,
1299
+ guidance: 'Hardcoded API keys in mcp.json are exposed to anyone with repo access. Use ${ENV_VAR} references and store actual values in .env (which should be in .gitignore).',
1240
1300
  });
1241
1301
  }
1242
1302
  // MCP-004: Check for default credentials
@@ -1267,7 +1327,8 @@ dist/
1267
1327
  message: 'Change to strong unique password',
1268
1328
  file: 'mcp.json',
1269
1329
  fixable: false,
1270
- fix: 'Manually change the default password in mcp.json to a strong, unique password (use `openssl rand -base64 24` to generate one)',
1330
+ fix: 'openssl rand -base64 24',
1331
+ guidance: 'Default passwords (postgres, admin, root, etc.) are the first thing attackers try. Generate a strong random password and update mcp.json.',
1271
1332
  });
1272
1333
  }
1273
1334
  // MCP-005: Check for wildcard tool access
@@ -1292,7 +1353,8 @@ dist/
1292
1353
  message: 'Restrict to specific tools needed',
1293
1354
  file: 'mcp.json',
1294
1355
  fixable: false,
1295
- fix: 'Manually replace "*" with a list of specific tool names you need (e.g., ["read_file", "list_directory"]) to limit what the AI can access',
1356
+ fix: 'Replace "*" with specific tool names in allowedTools (e.g., ["read_file", "list_directory"])',
1357
+ guidance: 'Wildcard tool access gives the AI unrestricted capabilities. Limit to only the tools your workflow actually needs to reduce attack surface.',
1296
1358
  });
1297
1359
  }
1298
1360
  return findings;
@@ -1329,7 +1391,8 @@ dist/
1329
1391
  message: 'Scope permissions to specific paths',
1330
1392
  file: '.claude/settings.json',
1331
1393
  fixable: false,
1332
- fix: 'Manually replace wildcards like Bash(*) or Read(*) with specific paths (e.g., Bash(npm test) or Read(/src/**)) to limit AI access',
1394
+ fix: 'Replace Bash(*) with Bash(npm test) and Read(*) with Read(/src/**) in .claude/settings.json',
1395
+ guidance: 'Wildcard permissions give the AI unrestricted shell, read, or write access. Scope each permission to the specific commands and paths your workflow needs.',
1333
1396
  });
1334
1397
  }
1335
1398
  // CLAUDE-003: Check for dangerous Bash patterns
@@ -1359,7 +1422,8 @@ dist/
1359
1422
  message: 'Remove rm -rf, sudo, etc.',
1360
1423
  file: '.claude/settings.json',
1361
1424
  fixable: false,
1362
- fix: 'Manually remove dangerous commands (rm -rf, sudo, chmod 777, etc.) from the allow list in .claude/settings.json to prevent accidental destructive operations',
1425
+ fix: 'Remove rm -rf, sudo, chmod 777 patterns from the allow list in .claude/settings.json',
1426
+ guidance: 'Allowing destructive commands means a single AI mistake can delete files, escalate privileges, or weaken permissions. Restrict to safe, reversible operations.',
1363
1427
  });
1364
1428
  }
1365
1429
  return findings;
@@ -1395,6 +1459,7 @@ dist/
1395
1459
  ? 'Cursor rules contain exposed credentials - remove and use environment variables'
1396
1460
  : 'No credentials found in Cursor rules',
1397
1461
  fixable: false,
1462
+ guidance: 'Cursor rules files are often committed to git. Credentials embedded there get pushed to remotes where anyone with repo access can extract them.',
1398
1463
  });
1399
1464
  return findings;
1400
1465
  }
@@ -1427,6 +1492,7 @@ dist/
1427
1492
  ? 'VSCode MCP config contains exposed credentials'
1428
1493
  : 'No credentials in VSCode MCP config',
1429
1494
  fixable: false,
1495
+ guidance: 'MCP config files are shared across workspaces and often committed to repos. Credentials there are exposed to every tool and extension that reads the config.',
1430
1496
  });
1431
1497
  // VSCODE-002: Check for overly permissive paths
1432
1498
  let hasRootAccess = false;
@@ -1449,6 +1515,7 @@ dist/
1449
1515
  ? 'VSCode MCP server has dangerous filesystem access'
1450
1516
  : 'VSCode MCP filesystem access is scoped',
1451
1517
  fixable: false,
1518
+ guidance: 'An MCP server with root or home directory access can read SSH keys, cloud credentials, and any file on the system. Scope access to the project directory only.',
1452
1519
  });
1453
1520
  return findings;
1454
1521
  }
@@ -1478,6 +1545,7 @@ dist/
1478
1545
  : `Private key files found: ${foundKeys.join(', ')} - move to secure location`,
1479
1546
  fixable: false,
1480
1547
  details: foundKeys.length > 0 ? { files: foundKeys } : undefined,
1548
+ guidance: 'Private key files (.pem, .key) in a project directory are easily committed to git. Once pushed, the keys are compromised and must be rotated.',
1481
1549
  });
1482
1550
  // CRED-003: Check package.json for hardcoded secrets
1483
1551
  let hasSecretsInPackageJson = false;
@@ -1502,6 +1570,7 @@ dist/
1502
1570
  ? 'package.json contains hardcoded secrets - move to environment variables'
1503
1571
  : 'No secrets found in package.json',
1504
1572
  fixable: false,
1573
+ guidance: 'package.json is always committed to git and published to npm. Secrets there are visible to anyone who installs or forks your package.',
1505
1574
  });
1506
1575
  // CRED-004: Check for JWT secrets in config
1507
1576
  let hasJwtSecret = false;
@@ -1530,6 +1599,7 @@ dist/
1530
1599
  ? 'JWT secret hardcoded in config - use environment variable'
1531
1600
  : 'No hardcoded JWT secrets found',
1532
1601
  fixable: false,
1602
+ guidance: 'A hardcoded JWT secret lets anyone who reads the config forge valid authentication tokens and impersonate any user.',
1533
1603
  });
1534
1604
  return findings;
1535
1605
  }
@@ -1561,6 +1631,7 @@ dist/
1561
1631
  fixable: true,
1562
1632
  fixed: false,
1563
1633
  details: executableConfigs.length > 0 ? { files: executableConfigs } : undefined,
1634
+ guidance: 'Executable config files can be run as scripts. An attacker who modifies a config file with execute permission can trick the system into running arbitrary code.',
1564
1635
  });
1565
1636
  // PERM-003: Check for group-writable sensitive files
1566
1637
  const sensitiveFiles = ['.env', '.env.local', 'secrets.json', 'credentials.json'];
@@ -1588,6 +1659,7 @@ dist/
1588
1659
  fixable: true,
1589
1660
  fixed: false,
1590
1661
  details: groupWritable.length > 0 ? { files: groupWritable } : undefined,
1662
+ guidance: 'Group-writable sensitive files allow other users in the same group to modify credentials or inject malicious configuration values.',
1591
1663
  });
1592
1664
  return findings;
1593
1665
  }
@@ -1620,6 +1692,7 @@ dist/
1620
1692
  ? 'Development mode enabled - ensure this is disabled in production'
1621
1693
  : 'No development mode indicators found',
1622
1694
  fixable: false,
1695
+ guidance: 'Development mode typically disables security features like CSRF protection, strict CORS, and error sanitization, leaving the application exposed in production.',
1623
1696
  });
1624
1697
  // ENV-002: Check for debug flags
1625
1698
  let hasDebugFlags = false;
@@ -1647,6 +1720,7 @@ dist/
1647
1720
  ? 'Debug flags enabled - may expose sensitive information in logs'
1648
1721
  : 'No debug flags detected',
1649
1722
  fixable: false,
1723
+ guidance: 'Debug and verbose logging flags can leak internal state, database queries, and credential values into log files or console output.',
1650
1724
  });
1651
1725
  // ENV-003: Check for error verbosity settings
1652
1726
  let verboseErrors = false;
@@ -1674,6 +1748,7 @@ dist/
1674
1748
  ? 'Verbose error messages enabled - may leak sensitive information'
1675
1749
  : 'Error verbosity settings are appropriate',
1676
1750
  fixable: false,
1751
+ guidance: 'Verbose error messages expose stack traces, file paths, and internal logic to attackers, making it easier to find exploitable weaknesses.',
1677
1752
  });
1678
1753
  // ENV-004: Check for production environment validation
1679
1754
  let hasEnvValidation = false;
@@ -1693,6 +1768,7 @@ dist/
1693
1768
  ? 'Environment validation library detected'
1694
1769
  : 'Consider using env validation (dotenv, envalid) to catch misconfigurations',
1695
1770
  fixable: false,
1771
+ guidance: 'Without environment validation, missing or malformed variables cause silent failures. A missing DB_HOST might fall back to an insecure default rather than failing fast.',
1696
1772
  });
1697
1773
  return findings;
1698
1774
  }
@@ -1716,6 +1792,7 @@ dist/
1716
1792
  ? 'Structured logging library detected'
1717
1793
  : 'Consider using structured logging (winston, pino) for better security auditing',
1718
1794
  fixable: false,
1795
+ guidance: 'Unstructured console.log output is hard to filter, search, or redact. Structured logging makes it possible to automatically mask sensitive fields and detect anomalies.',
1719
1796
  });
1720
1797
  // LOG-002: Check for sensitive data in log patterns
1721
1798
  let sensitiveInLogs = false;
@@ -1749,6 +1826,7 @@ dist/
1749
1826
  ? 'Code may be logging sensitive data - review console.log statements'
1750
1827
  : 'No obvious sensitive data logging patterns found',
1751
1828
  fixable: false,
1829
+ guidance: 'Passwords, API keys, and tokens logged to console or files persist in log aggregators and crash reports, where they can be harvested by anyone with log access.',
1752
1830
  });
1753
1831
  // LOG-003: Check for log file permissions
1754
1832
  const logFiles = ['app.log', 'error.log', 'debug.log', 'access.log'];
@@ -1775,6 +1853,7 @@ dist/
1775
1853
  : `World-readable log files: ${worldReadableLogs.join(', ')}`,
1776
1854
  fixable: true,
1777
1855
  fixed: false,
1856
+ guidance: 'World-readable log files let any local user read application logs, which often contain request details, internal errors, and sometimes credentials.',
1778
1857
  });
1779
1858
  // LOG-004: Check for audit logging capability
1780
1859
  let hasAuditLogging = false;
@@ -1794,6 +1873,7 @@ dist/
1794
1873
  ? 'Audit logging capability detected'
1795
1874
  : 'Consider implementing audit logging for security events',
1796
1875
  fixable: false,
1876
+ guidance: 'Without audit logging, there is no record of who accessed what or when. Incident response becomes guesswork without a trail of security-relevant events.',
1797
1877
  });
1798
1878
  return findings;
1799
1879
  }
@@ -1829,6 +1909,7 @@ dist/
1829
1909
  ? 'Dependency lock file present'
1830
1910
  : 'No lock file found - dependency versions may vary between installs',
1831
1911
  fixable: false,
1912
+ guidance: 'Without a lock file, npm install can resolve to different package versions on different machines, including versions with known vulnerabilities or supply-chain backdoors.',
1832
1913
  });
1833
1914
  // DEP-002: Check for known vulnerable packages
1834
1915
  const vulnerablePackages = ['event-stream', 'flatmap-stream', 'eslint-scope@3.7.2'];
@@ -1854,6 +1935,7 @@ dist/
1854
1935
  ? 'Potentially vulnerable package detected - run npm audit'
1855
1936
  : 'No known vulnerable packages in direct dependencies',
1856
1937
  fixable: false,
1938
+ guidance: 'These packages have confirmed supply-chain compromises (e.g., event-stream injected a cryptocurrency-stealing payload). Remove or replace them immediately.',
1857
1939
  });
1858
1940
  // DEP-003: Check for wildcard versions
1859
1941
  let hasWildcardVersions = false;
@@ -1880,6 +1962,7 @@ dist/
1880
1962
  ? 'Wildcard versions detected - pin dependencies for reproducible builds'
1881
1963
  : 'All dependency versions are properly specified',
1882
1964
  fixable: false,
1965
+ guidance: 'Wildcard (*) or "latest" versions accept any future release, including ones compromised by supply-chain attacks. Pin versions and use a lock file.',
1883
1966
  });
1884
1967
  // DEP-004: Check for npm scripts security
1885
1968
  let hasDangerousScripts = false;
@@ -1922,6 +2005,7 @@ dist/
1922
2005
  ? 'Dangerous patterns in npm scripts (curl|sh, eval) - review carefully'
1923
2006
  : 'npm scripts appear safe',
1924
2007
  fixable: false,
2008
+ guidance: 'Scripts that pipe curl/wget to sh execute arbitrary remote code during npm install. An attacker who compromises the URL controls your build environment.',
1925
2009
  });
1926
2010
  return findings;
1927
2011
  }
@@ -1951,6 +2035,7 @@ dist/
1951
2035
  ? 'Authentication configuration detected'
1952
2036
  : 'No authentication library detected - ensure endpoints are protected',
1953
2037
  fixable: false,
2038
+ guidance: 'Without authentication, any network-reachable client can access your API endpoints, including reading data, triggering actions, and modifying state.',
1954
2039
  });
1955
2040
  // AUTH-002: Check for rate limiting
1956
2041
  let hasRateLimiting = false;
@@ -1970,6 +2055,7 @@ dist/
1970
2055
  ? 'Rate limiting library detected'
1971
2056
  : 'No rate limiting detected - API may be vulnerable to abuse',
1972
2057
  fixable: false,
2058
+ guidance: 'Without rate limiting, attackers can brute-force credentials, scrape data, or exhaust resources with automated requests at no cost.',
1973
2059
  });
1974
2060
  // AUTH-003: Check for session security
1975
2061
  let hasSecureSessions = false;
@@ -2003,6 +2089,7 @@ dist/
2003
2089
  ? 'Secure session configuration detected'
2004
2090
  : 'Ensure sessions use secure, httpOnly cookies',
2005
2091
  fixable: false,
2092
+ guidance: 'Sessions without secure and httpOnly flags are vulnerable to theft via XSS attacks or network sniffing, allowing attackers to hijack authenticated sessions.',
2006
2093
  });
2007
2094
  // AUTH-004: Check for CORS configuration
2008
2095
  let hasCorsConfig = false;
@@ -2022,6 +2109,7 @@ dist/
2022
2109
  ? 'CORS library detected'
2023
2110
  : 'No CORS configuration detected - ensure cross-origin requests are properly handled',
2024
2111
  fixable: false,
2112
+ guidance: 'Without explicit CORS configuration, browsers may block legitimate cross-origin requests or, worse, a permissive default may allow malicious sites to make authenticated requests to your API.',
2025
2113
  });
2026
2114
  return findings;
2027
2115
  }
@@ -3817,8 +3905,24 @@ dist/
3817
3905
  existingFiles: [],
3818
3906
  createdFiles: [],
3819
3907
  };
3820
- // Backup each file that exists
3821
- for (const file of HardeningScanner.BACKUP_FILES) {
3908
+ // Backup each file that exists (static list + web directory scan)
3909
+ const filesToBackup = [...HardeningScanner.BACKUP_FILES];
3910
+ // Also discover files in web-served directories that --fix may modify
3911
+ const webDirs = ['public', 'static', 'dist', 'build', 'out', 'www', '_site'];
3912
+ const webExts = ['.html', '.htm', '.js', '.jsx', '.tsx', '.css', '.py', '.md'];
3913
+ for (const webDir of webDirs) {
3914
+ const webDirPath = path.join(targetDir, webDir);
3915
+ try {
3916
+ const entries = await fs.readdir(webDirPath, { withFileTypes: true });
3917
+ for (const entry of entries) {
3918
+ if (entry.isFile() && webExts.some(ext => entry.name.endsWith(ext))) {
3919
+ filesToBackup.push(path.join(webDir, entry.name));
3920
+ }
3921
+ }
3922
+ }
3923
+ catch { /* dir doesn't exist */ }
3924
+ }
3925
+ for (const file of filesToBackup) {
3822
3926
  const sourcePath = path.join(targetDir, file);
3823
3927
  try {
3824
3928
  await fs.access(sourcePath);
@@ -4000,7 +4104,8 @@ dist/
4000
4104
  fixable: true,
4001
4105
  fixed: skill001Fixed,
4002
4106
  fixMessage: skill001Fixed ? 'Added SHA-256 signature block to skill file' : undefined,
4003
- fix: 'Run `hackmyagent fix-all --with-aim` to automatically sign all skill files with a cryptographic identity',
4107
+ fix: 'hackmyagent fix-all --with-aim',
4108
+ guidance: 'Unsigned skills cannot be verified for authenticity or integrity. Sign with a cryptographic identity to enable tamper detection.',
4004
4109
  });
4005
4110
  // SKILL-002: Remote Fetch Pattern
4006
4111
  for (let i = 0; i < lines.length; i++) {
@@ -4020,7 +4125,8 @@ dist/
4020
4125
  file: relativePath,
4021
4126
  line: i + 1,
4022
4127
  fixable: false,
4023
- fix: 'Remove curl|sh, wget|sh, and other remote code execution patterns',
4128
+ fix: 'Remove the curl|sh or wget|sh pattern from this file',
4129
+ guidance: 'Remote code execution patterns download and execute arbitrary code. Replace with a pinned dependency or vendored script with checksum verification.',
4024
4130
  });
4025
4131
  break; // One finding per line
4026
4132
  }
@@ -4043,7 +4149,8 @@ dist/
4043
4149
  file: relativePath,
4044
4150
  line: i + 1,
4045
4151
  fixable: false,
4046
- fix: 'Heartbeats should be configured separately with restricted permissions, not bundled in skills',
4152
+ fix: 'Move scheduled task configuration to a separate heartbeat config file',
4153
+ guidance: 'Skills that install heartbeats or cron jobs gain persistent execution beyond the user session. Heartbeats should be configured separately with restricted permissions.',
4047
4154
  });
4048
4155
  }
4049
4156
  }
@@ -4079,7 +4186,8 @@ dist/
4079
4186
  fixable: true,
4080
4187
  fixed: fixApplied,
4081
4188
  fixMessage: fixApplied ? 'Restricted filesystem access to sandbox scope' : undefined,
4082
- fix: 'Restrict filesystem access to specific directories (e.g., filesystem:./data/*)',
4189
+ fix: 'hackmyagent secure --fix',
4190
+ guidance: 'Broad filesystem access (filesystem:* or filesystem:~/) lets skills read/write anywhere. Restrict to specific directories (e.g., filesystem:./data/*).',
4083
4191
  });
4084
4192
  }
4085
4193
  }
@@ -4104,7 +4212,8 @@ dist/
4104
4212
  file: relativePath,
4105
4213
  line: i + 1,
4106
4214
  fixable: false,
4107
- fix: 'Skills should never access credential files like ~/.ssh, ~/.aws, wallets, or .env files',
4215
+ fix: 'Remove credential file access patterns from this skill',
4216
+ guidance: 'Skills accessing ~/.ssh, ~/.aws, wallets, or .env files can exfiltrate credentials. Use npx secretless-ai init to protect credentials from AI tool context.',
4108
4217
  });
4109
4218
  break; // One finding per line per check
4110
4219
  }
@@ -4127,7 +4236,8 @@ dist/
4127
4236
  file: relativePath,
4128
4237
  line: i + 1,
4129
4238
  fixable: false,
4130
- fix: 'Remove webhook.site, requestbin, ngrok, and suspicious POST patterns',
4239
+ fix: 'Remove the exfiltration endpoint from this skill',
4240
+ guidance: 'Data exfiltration patterns (webhook.site, requestbin, ngrok, suspicious POST) send local data to external servers. Remove or replace with audited, allow-listed endpoints.',
4131
4241
  });
4132
4242
  break; // One finding per line per check
4133
4243
  }
@@ -4150,7 +4260,8 @@ dist/
4150
4260
  file: relativePath,
4151
4261
  line: i + 1,
4152
4262
  fixable: false,
4153
- fix: 'Remove social engineering instructions that trick users into copying/pasting commands',
4263
+ fix: 'Remove the copy/paste instruction block from this skill',
4264
+ guidance: 'ClickFix social engineering tricks users into copying and pasting malicious commands. This technique was used extensively in the ClawHavoc campaign.',
4154
4265
  });
4155
4266
  break; // One finding per line per check
4156
4267
  }
@@ -4173,7 +4284,8 @@ dist/
4173
4284
  file: relativePath,
4174
4285
  line: i + 1,
4175
4286
  fixable: false,
4176
- fix: 'Remove netcat, bash -i, /dev/tcp, and other reverse shell patterns',
4287
+ fix: 'Remove the reverse shell pattern from this skill',
4288
+ guidance: 'Reverse shell patterns (netcat, bash -i, /dev/tcp) establish remote command execution. This is a strong indicator of malicious intent.',
4177
4289
  });
4178
4290
  break; // One finding per line per check
4179
4291
  }
@@ -4205,7 +4317,8 @@ dist/
4205
4317
  message: `Skill name "${skillBasename}" is similar to popular skill "${popular}" (potential typosquatting)`,
4206
4318
  file: relativePath,
4207
4319
  fixable: false,
4208
- fix: 'Rename the skill to avoid confusion with popular skills, or verify this is intentional',
4320
+ fix: `hackmyagent check ${relativePath}`,
4321
+ guidance: 'Typosquatting uses names similar to popular skills to trick users into installing malicious versions. Verify the skill source and rename if unintentional.',
4209
4322
  });
4210
4323
  break; // One typosquatting finding per skill file
4211
4324
  }
@@ -4231,7 +4344,8 @@ dist/
4231
4344
  file: relativePath,
4232
4345
  line: i + 1,
4233
4346
  fixable: false,
4234
- fix: 'Skills should not access .env files or environment variables containing secrets',
4347
+ fix: 'npx secretless-ai init',
4348
+ guidance: 'Skills accessing .env files or process.env can exfiltrate API keys and secrets. Use Secretless AI to block credential access from AI tool context.',
4235
4349
  });
4236
4350
  }
4237
4351
  }
@@ -4256,7 +4370,8 @@ dist/
4256
4370
  file: relativePath,
4257
4371
  line: i + 1,
4258
4372
  fixable: false,
4259
- fix: 'Skills should not access browser data, cookies, localStorage, or sessionStorage',
4373
+ fix: 'Remove browser data access patterns from this skill',
4374
+ guidance: 'Skills accessing browser data (cookies, localStorage, sessionStorage) can steal session tokens and authentication state.',
4260
4375
  });
4261
4376
  }
4262
4377
  }
@@ -4281,7 +4396,8 @@ dist/
4281
4396
  file: relativePath,
4282
4397
  line: i + 1,
4283
4398
  fixable: false,
4284
- fix: 'Skills should never access cryptocurrency wallets, seed phrases, or private keys',
4399
+ fix: 'Remove crypto wallet access patterns from this skill',
4400
+ guidance: 'Skills accessing wallets, seed phrases, or private keys can drain cryptocurrency funds. No legitimate skill needs this access.',
4285
4401
  });
4286
4402
  }
4287
4403
  }
@@ -4325,7 +4441,8 @@ dist/
4325
4441
  fixable: true,
4326
4442
  fixed: skill019Fixed,
4327
4443
  fixMessage: skill019Fixed ? 'Re-computed hash and updated signature block' : undefined,
4328
- fix: 'Re-sign the skill to update the hash: hackmyagent secure --fix',
4444
+ fix: 'hackmyagent secure --fix',
4445
+ guidance: 'The signature hash no longer matches the file content. This could indicate tampering or a legitimate edit that was not re-signed.',
4329
4446
  });
4330
4447
  }
4331
4448
  // HEARTBEAT-007: Expired Heartbeat (check expires_at in signature block)
@@ -4358,7 +4475,8 @@ dist/
4358
4475
  fixable: true,
4359
4476
  fixed: hb007Fixed,
4360
4477
  fixMessage: hb007Fixed ? 'Updated expiry to 7 days from now and re-signed' : undefined,
4361
- fix: 'Re-sign the skill with a new expiry: hackmyagent secure --fix',
4478
+ fix: 'hackmyagent secure --fix',
4479
+ guidance: 'Expired signatures mean the skill has not been re-verified since its expiry date. Re-signing renews the validity period and re-verifies content integrity.',
4362
4480
  });
4363
4481
  }
4364
4482
  }
@@ -4463,6 +4581,7 @@ dist/
4463
4581
  line: i + 1,
4464
4582
  fixable: false,
4465
4583
  fix: 'Verify the URL is from a trusted source and add hash pinning for integrity',
4584
+ guidance: 'Heartbeats that contact external URLs without verification can be redirected to malicious endpoints. Pin the expected hash to detect tampering.',
4466
4585
  });
4467
4586
  }
4468
4587
  }
@@ -4482,7 +4601,8 @@ dist/
4482
4601
  : 'Heartbeat lacks hash pinning - content integrity cannot be verified',
4483
4602
  file: relativePath,
4484
4603
  fixable: false,
4485
- fix: 'Run `hackmyagent fix-all --with-aim` to automatically pin and sign heartbeat files',
4604
+ fix: 'hackmyagent fix-all --with-aim',
4605
+ guidance: 'Without hash pinning, heartbeat content can be modified without detection. Pinning creates a cryptographic fingerprint to verify integrity on each execution.',
4486
4606
  });
4487
4607
  // HEARTBEAT-003: Unsigned Heartbeat
4488
4608
  const hasSignature = content.includes('opena2a_signature:') ||
@@ -4500,7 +4620,8 @@ dist/
4500
4620
  : 'Heartbeat is unsigned - cannot verify authenticity or integrity',
4501
4621
  file: relativePath,
4502
4622
  fixable: false,
4503
- fix: 'Run `hackmyagent fix-all --with-aim` to automatically sign all heartbeat files with a cryptographic identity',
4623
+ fix: 'hackmyagent fix-all --with-aim',
4624
+ guidance: 'Unsigned heartbeats cannot prove who created them or whether they have been modified. Cryptographic signatures enable authenticity and integrity verification.',
4504
4625
  });
4505
4626
  // HEARTBEAT-004: Dangerous Capabilities
4506
4627
  for (let i = 0; i < lines.length; i++) {
@@ -4519,6 +4640,7 @@ dist/
4519
4640
  line: i + 1,
4520
4641
  fixable: false,
4521
4642
  fix: 'Heartbeats should use minimal capabilities - avoid shell:*, filesystem:*, network:*',
4643
+ guidance: 'Wildcard capabilities (shell:*, filesystem:*, network:*) give heartbeats unrestricted access. A compromised heartbeat with these permissions can execute arbitrary commands, read any file, or exfiltrate data.',
4522
4644
  });
4523
4645
  }
4524
4646
  }
@@ -4554,6 +4676,7 @@ dist/
4554
4676
  line: i + 1,
4555
4677
  fixable: false,
4556
4678
  fix: 'Increase heartbeat interval to at least 5 minutes to prevent resource exhaustion',
4679
+ guidance: 'High-frequency heartbeats consume CPU, memory, and network bandwidth. Intervals under 5 minutes can cause resource exhaustion and may mask malicious polling behavior.',
4557
4680
  });
4558
4681
  }
4559
4682
  }
@@ -4576,6 +4699,7 @@ dist/
4576
4699
  file: relativePath,
4577
4700
  fixable: false,
4578
4701
  fix: 'Add activeHours: or schedule: to limit when the heartbeat can run',
4702
+ guidance: 'Unrestricted heartbeats run 24/7 including off-hours when no one monitors them. Time-of-day limits reduce the window for undetected malicious activity.',
4579
4703
  });
4580
4704
  }
4581
4705
  return findings;
@@ -4674,7 +4798,8 @@ dist/
4674
4798
  fixable: true,
4675
4799
  fixed: gateway001Fixed,
4676
4800
  fixMessage: gateway001Fixed ? 'Changed gateway.host from 0.0.0.0 to 127.0.0.1' : undefined,
4677
- fix: `Run \`${this.cliName} secure-openclaw --fix\` to bind gateway to 127.0.0.1 for local-only access`,
4801
+ fix: `${this.cliName} secure-openclaw --fix`,
4802
+ guidance: 'Binding to 0.0.0.0 exposes the gateway to all network interfaces. Use 127.0.0.1 for local-only access unless remote access is explicitly needed with proper authentication.',
4678
4803
  });
4679
4804
  }
4680
4805
  // GATEWAY-002: Missing WebSocket Origin Validation (not auto-fixable - requires user to specify allowed origins)
@@ -4692,7 +4817,8 @@ dist/
4692
4817
  : 'Missing security.websocketOrigins - vulnerable to GHSA-g8p2 cross-origin attacks',
4693
4818
  file: relativePath,
4694
4819
  fixable: false,
4695
- fix: 'Manually add security.websocketOrigins array with your allowed origins (e.g., ["http://localhost:3000"])',
4820
+ fix: 'Add security.websocketOrigins: ["http://localhost:3000"] to the gateway config',
4821
+ guidance: 'Without origin validation, any website can connect to the gateway via WebSocket (GHSA-g8p2). This enables cross-origin command execution attacks.',
4696
4822
  });
4697
4823
  // GATEWAY-003: Token Exposed in Config
4698
4824
  const gatewayAuth = gateway?.auth;
@@ -4730,7 +4856,8 @@ dist/
4730
4856
  fixable: true,
4731
4857
  fixed: gateway003Fixed,
4732
4858
  fixMessage: gateway003Fixed ? 'Replaced plaintext token with ${OPENCLAW_AUTH_TOKEN} env var reference. Set OPENCLAW_AUTH_TOKEN in your environment.' : undefined,
4733
- fix: `Run \`${this.cliName} secure-openclaw --fix\` to replace plaintext token with \${OPENCLAW_AUTH_TOKEN} env var reference`,
4859
+ fix: `${this.cliName} secure-openclaw --fix`,
4860
+ guidance: 'Plaintext tokens in config files are exposed to anyone with repo access. Use environment variable references so credentials stay outside version control.',
4734
4861
  });
4735
4862
  }
4736
4863
  // GATEWAY-004: Approval Confirmations Disabled
@@ -4777,7 +4904,8 @@ dist/
4777
4904
  fixable: true,
4778
4905
  fixed: gateway004Fixed,
4779
4906
  fixMessage: gateway004Fixed ? 'Enabled approval confirmations for command execution' : undefined,
4780
- fix: `Run \`${this.cliName} secure-openclaw --fix\` to enable approval confirmations for safer command execution`,
4907
+ fix: `${this.cliName} secure-openclaw --fix`,
4908
+ guidance: 'Without approval confirmations, commands execute immediately without user review. This removes the last line of defense against malicious or accidental destructive operations.',
4781
4909
  });
4782
4910
  }
4783
4911
  // GATEWAY-005: Sandbox Disabled
@@ -4806,7 +4934,8 @@ dist/
4806
4934
  fixable: true,
4807
4935
  fixed: gateway005Fixed,
4808
4936
  fixMessage: gateway005Fixed ? 'Enabled sandbox mode for isolated code execution' : undefined,
4809
- fix: `Run \`${this.cliName} secure-openclaw --fix\` to enable sandbox mode for safer code execution`,
4937
+ fix: `${this.cliName} secure-openclaw --fix`,
4938
+ guidance: 'Without sandbox isolation, executed code has full system access including filesystem, network, and process control. Sandbox mode limits the blast radius of malicious or buggy code.',
4810
4939
  });
4811
4940
  }
4812
4941
  // GATEWAY-006: Container Escape Risk (not auto-fixable - requires manual review of mount points)
@@ -4834,7 +4963,8 @@ dist/
4834
4963
  message: `Container escape risk: ${issues.join(', ')}`,
4835
4964
  file: relativePath,
4836
4965
  fixable: false,
4837
- fix: 'Manually disable privileged mode and remove sensitive host mounts - requires careful review',
4966
+ fix: 'Disable docker.privileged and remove sensitive mounts (/var/run/docker.sock, /etc/passwd, /:/) from the config',
4967
+ guidance: 'Privileged mode and sensitive host mounts allow container escape -- the agent can access the host system, other containers, and all their data.',
4838
4968
  });
4839
4969
  }
4840
4970
  // GATEWAY-007: Open DM Policy with Wildcard
@@ -4869,6 +4999,7 @@ dist/
4869
4999
  file: relativePath,
4870
5000
  fixable: false,
4871
5001
  fix: 'Replace wildcard "*" in allowFrom with specific allowed sender IDs or domains',
5002
+ guidance: 'An open DM policy with wildcard allows any entity to send messages to the agent. Attackers can use this to inject commands or exfiltrate data via conversation.',
4872
5003
  });
4873
5004
  }
4874
5005
  // GATEWAY-008: Tailscale Funnel Exposure
@@ -4887,6 +5018,7 @@ dist/
4887
5018
  file: relativePath,
4888
5019
  fixable: false,
4889
5020
  fix: 'Disable Tailscale Funnel unless public access is intentional. Use Tailscale ACLs to restrict access.',
5021
+ guidance: 'Tailscale Funnel exposes the agent to the public internet, bypassing Tailscale\'s private network protection. Only enable if you explicitly need public access.',
4890
5022
  });
4891
5023
  }
4892
5024
  // Write modified config back to file if any fixes were applied
@@ -4904,7 +5036,8 @@ dist/
4904
5036
  message: `Applied ${fixesApplied.length} fix(es): ${fixesApplied.join('; ')}`,
4905
5037
  file: relativePath,
4906
5038
  fixable: false,
4907
- fix: 'Use `hackmyagent rollback` to undo these changes if needed',
5039
+ fix: 'hackmyagent rollback',
5040
+ guidance: 'Auto-fixes were applied to this configuration. Use rollback to revert if any fix caused unexpected behavior.',
4908
5041
  });
4909
5042
  }
4910
5043
  catch (writeError) {
@@ -4919,6 +5052,7 @@ dist/
4919
5052
  file: relativePath,
4920
5053
  fixable: false,
4921
5054
  fix: 'Check file permissions and try again',
5055
+ guidance: 'The auto-fix could not write changes to the configuration file. Verify the file is not read-only and that you have write permissions.',
4922
5056
  });
4923
5057
  }
4924
5058
  }
@@ -5030,6 +5164,7 @@ dist/
5030
5164
  file: relativePath,
5031
5165
  fixable: false,
5032
5166
  fix: 'Move session files outside the project directory or add to .gitignore',
5167
+ guidance: 'Session and token files contain credentials that grant access to messaging platforms. If committed to git, anyone with repo access can hijack these sessions.',
5033
5168
  });
5034
5169
  }
5035
5170
  // CONFIG-002: SOUL.md Injection Vectors
@@ -5073,6 +5208,7 @@ dist/
5073
5208
  file: relativePath,
5074
5209
  fixable: false,
5075
5210
  fix: 'Review and remove suspicious patterns from SOUL.md',
5211
+ guidance: 'SOUL.md defines agent behavior. Prompt injection patterns embedded here can override safety instructions and make the agent act maliciously.',
5076
5212
  });
5077
5213
  break; // Only report first match per file
5078
5214
  }
@@ -5121,6 +5257,7 @@ dist/
5121
5257
  file: relativePath,
5122
5258
  fixable: false,
5123
5259
  fix: 'Run daemon as non-root user with minimal privileges',
5260
+ guidance: 'Daemons running as root have unrestricted system access. A compromised root-level daemon can modify any file, install backdoors, or pivot to other systems.',
5124
5261
  });
5125
5262
  break; // Only report first match per file
5126
5263
  }
@@ -5167,6 +5304,7 @@ dist/
5167
5304
  file: relativePath,
5168
5305
  fixable: false,
5169
5306
  fix: 'Use a secrets manager or ensure .env is in .gitignore',
5307
+ guidance: 'Plaintext API keys in .env files can be accidentally committed. Use a secrets manager for production, and always ensure .env is in .gitignore.',
5170
5308
  });
5171
5309
  break; // Only report first match per file
5172
5310
  }
@@ -5219,6 +5357,7 @@ dist/
5219
5357
  file: relativePath,
5220
5358
  fixable: false,
5221
5359
  fix: 'Review and sanitize memory.json contents',
5360
+ guidance: 'Agent memory files can be poisoned with prompt injections, eval calls, or base64-encoded payloads that execute when the agent loads its context.',
5222
5361
  });
5223
5362
  break; // Only report first match per file
5224
5363
  }
@@ -5265,6 +5404,7 @@ dist/
5265
5404
  file: relativePath,
5266
5405
  fixable: false,
5267
5406
  fix: 'Disable autoFollow or review moltbook security settings',
5407
+ guidance: 'Auto-following untrusted agents can expose your agent to malicious instructions, data exfiltration, or prompt injection from compromised peers.',
5268
5408
  });
5269
5409
  }
5270
5410
  // CONFIG-007: Unrestricted Elevated Execution
@@ -5288,6 +5428,7 @@ dist/
5288
5428
  file: relativePath,
5289
5429
  fixable: false,
5290
5430
  fix: 'Set tools.elevated.defaultLevel to "restricted" and enable exec.approvals',
5431
+ guidance: 'Unrestricted elevated execution gives tools maximum system privileges without approval gates. This bypasses all safety checks for destructive operations.',
5291
5432
  });
5292
5433
  }
5293
5434
  // CONFIG-008: Sandbox Disabled
@@ -5309,6 +5450,7 @@ dist/
5309
5450
  file: relativePath,
5310
5451
  fixable: false,
5311
5452
  fix: 'Enable sandbox: set sandbox.enabled to true or tools.exec.sandbox to true',
5453
+ guidance: 'Without sandbox isolation, tool execution has direct access to the host filesystem, network, and processes. Enable sandboxing to contain potential damage.',
5312
5454
  });
5313
5455
  }
5314
5456
  // CONFIG-009: Weak Gateway Token
@@ -5329,7 +5471,8 @@ dist/
5329
5471
  message: `Token is only ${tokenValue.length} characters - minimum 24 recommended`,
5330
5472
  file: relativePath,
5331
5473
  fixable: false,
5332
- fix: 'Generate a stronger token: openssl rand -base64 32',
5474
+ fix: 'openssl rand -base64 32',
5475
+ guidance: 'Short tokens are vulnerable to brute-force attacks. Use at least 24 characters of cryptographically random data for authentication tokens.',
5333
5476
  });
5334
5477
  }
5335
5478
  }
@@ -5411,7 +5554,8 @@ dist/
5411
5554
  : 'Skill lacks publisher metadata - cannot verify source',
5412
5555
  file: relativePath,
5413
5556
  fixable: false,
5414
- fix: 'Add publisher: and publisher_verified: true to skill frontmatter after verification',
5557
+ fix: `hackmyagent check ${relativePath}`,
5558
+ guidance: 'Unverified publishers cannot be trusted. Add publisher: and publisher_verified: true to skill frontmatter after DNS TXT record verification.',
5415
5559
  });
5416
5560
  // SUPPLY-002: Skill Not in Registry
5417
5561
  const hasRegistryAttestation = /^registry_attestation:\s*.+$/m.test(frontmatter);
@@ -5427,7 +5571,8 @@ dist/
5427
5571
  : 'Skill lacks registry_attestation - not listed in trusted registry',
5428
5572
  file: relativePath,
5429
5573
  fixable: false,
5430
- fix: 'Register skill with a trusted registry (e.g., clawhub.io, skillregistry.openclaw.org)',
5574
+ fix: 'Add registry_attestation: to skill frontmatter after registry submission',
5575
+ guidance: 'Unregistered skills have no community trust signal. Register with a trusted registry to enable trust scoring and vulnerability alerts.',
5431
5576
  });
5432
5577
  // SUPPLY-003: Known Malicious Skill Pattern (ClawHavoc campaign)
5433
5578
  let isMaliciousMatch = false;
@@ -5458,7 +5603,8 @@ dist/
5458
5603
  message: `Skill matches known malicious pattern: "${matchedPattern}"`,
5459
5604
  file: relativePath,
5460
5605
  fixable: false,
5461
- fix: 'Remove this skill -- it matches known malware from the ClawHavoc campaign',
5606
+ fix: `rm ${relativePath}`,
5607
+ guidance: 'This skill matches known malicious patterns from the ClawHavoc campaign. Remove immediately and audit any systems it had access to.',
5462
5608
  });
5463
5609
  }
5464
5610
  // SUPPLY-004: Version Drift Detection
@@ -5475,7 +5621,8 @@ dist/
5475
5621
  : 'Skill lacks installed_hash - cannot detect version drift or tampering',
5476
5622
  file: relativePath,
5477
5623
  fixable: false,
5478
- fix: 'Add installed_hash: with SHA-256 hash of the original skill content',
5624
+ fix: 'hackmyagent fix-all --with-aim',
5625
+ guidance: 'Without an installed_hash, modifications to the skill cannot be detected. The hash enables tamper detection on every scan.',
5479
5626
  });
5480
5627
  // SUPPLY-005: ClawHavoc C2 IP
5481
5628
  for (const ip of CLAWHAVOC_C2_IPS) {
@@ -5490,7 +5637,8 @@ dist/
5490
5637
  message: `Known C2 IP address found: ${ip}`,
5491
5638
  file: relativePath,
5492
5639
  fixable: false,
5493
- fix: 'Remove this skill -- contains known malware C2 infrastructure',
5640
+ fix: `rm ${relativePath}`,
5641
+ guidance: 'This skill contains a known ClawHavoc command-and-control IP address. Remove immediately and check network logs for connections to this IP.',
5494
5642
  });
5495
5643
  break;
5496
5644
  }
@@ -5508,7 +5656,8 @@ dist/
5508
5656
  message: `Known malware filename referenced: "${filename}"`,
5509
5657
  file: relativePath,
5510
5658
  fixable: false,
5511
- fix: 'Remove this skill -- references known malware payload',
5659
+ fix: `rm ${relativePath}`,
5660
+ guidance: 'This skill references a known ClawHavoc malware payload filename. Remove and scan for other indicators of compromise.',
5512
5661
  });
5513
5662
  break;
5514
5663
  }
@@ -5527,7 +5676,8 @@ dist/
5527
5676
  message: `ClickFix social engineering pattern detected: "${match[0]}"`,
5528
5677
  file: relativePath,
5529
5678
  fixable: false,
5530
- fix: 'Review and remove suspicious download/execute instructions',
5679
+ fix: 'Remove the download/execute instruction from this skill',
5680
+ guidance: 'ClickFix patterns trick users into downloading and executing malware. This technique is associated with the ClawHavoc campaign.',
5531
5681
  });
5532
5682
  break;
5533
5683
  }
@@ -5545,7 +5695,8 @@ dist/
5545
5695
  message: `Suspicious archive password pattern: "${archiveMatch[0]}"`,
5546
5696
  file: relativePath,
5547
5697
  fixable: false,
5548
- fix: 'Investigate password-protected archive reference - common malware distribution technique',
5698
+ fix: 'Remove the archive password reference from this skill',
5699
+ guidance: 'Password-protected archives are a common malware distribution technique to bypass antivirus scanning. Investigate the archive source.',
5549
5700
  });
5550
5701
  }
5551
5702
  }
@@ -5586,7 +5737,8 @@ dist/
5586
5737
  : `OpenClaw ${openclawVersion} includes CVE-2026-25253 fix`,
5587
5738
  file: 'package.json',
5588
5739
  fixable: false,
5589
- fix: 'Upgrade openclaw to v2026.1.29 or later: npm install openclaw@latest',
5740
+ fix: 'npm install openclaw@latest',
5741
+ guidance: 'CVE-2026-25253 (CVSS 8.8) enables WebSocket hijacking for remote code execution. Upgrade to v2026.1.29 or later which includes the fix.',
5590
5742
  });
5591
5743
  // CVE-003: OS Command Injection via SSH Path (same fix version)
5592
5744
  if (isVulnerable) {
@@ -5600,7 +5752,8 @@ dist/
5600
5752
  message: `OpenClaw ${openclawVersion} is vulnerable to CVE-2026-25157 - upgrade to v2026.1.29+`,
5601
5753
  file: 'package.json',
5602
5754
  fixable: false,
5603
- fix: 'Upgrade openclaw to v2026.1.29 or later: npm install openclaw@latest',
5755
+ fix: 'npm install openclaw@latest',
5756
+ guidance: 'CVE-2026-25157 (CVSS 7.8) allows OS command injection via unescaped SSH project paths. Upgrade to v2026.1.29 or later which includes the fix.',
5604
5757
  });
5605
5758
  }
5606
5759
  else {
@@ -5629,7 +5782,8 @@ dist/
5629
5782
  message: `OpenClaw ${openclawVersion} is vulnerable to CVE-2026-24763 - upgrade to v2026.1.29+`,
5630
5783
  file: 'package.json',
5631
5784
  fixable: false,
5632
- fix: 'Upgrade openclaw to v2026.1.29 or later: npm install openclaw@latest',
5785
+ fix: 'npm install openclaw@latest',
5786
+ guidance: 'CVE-2026-24763 (CVSS 8.8) allows command injection through unsafe PATH handling in Docker sandbox. Upgrade to v2026.1.29 or later which includes the fix.',
5633
5787
  });
5634
5788
  }
5635
5789
  else {
@@ -5679,6 +5833,7 @@ dist/
5679
5833
  file: relativePath,
5680
5834
  fixable: false,
5681
5835
  fix: 'Add gateway.controlUi.allowedOrigins with your allowed origins (e.g., ["http://localhost:3000"])',
5836
+ guidance: 'Without origin restrictions, the control UI can be accessed from any origin. Adding allowedOrigins provides defense-in-depth against cross-origin attacks.',
5682
5837
  });
5683
5838
  }
5684
5839
  else if (hasAuth && hasAllowedOrigins) {
@@ -5816,6 +5971,7 @@ dist/
5816
5971
  fixable: false,
5817
5972
  file: memFile,
5818
5973
  fix: 'Sanitize all memory entries before persistence. Remove __proto__ and constructor keys. Validate $ref URIs.',
5974
+ guidance: 'Prototype pollution via __proto__ or constructor can alter object behavior. External $ref URIs can load malicious content into agent memory at runtime.',
5819
5975
  });
5820
5976
  }
5821
5977
  }
@@ -5844,6 +6000,7 @@ dist/
5844
6000
  fixable: false,
5845
6001
  file: cfgFile,
5846
6002
  fix: 'Enable memory integrity verification: add hash validation or signature checks for persisted context.',
6003
+ guidance: 'Without integrity checks, an attacker with file access can modify persisted memory to inject malicious instructions that the agent will trust on reload.',
5847
6004
  });
5848
6005
  }
5849
6006
  }
@@ -5872,6 +6029,7 @@ dist/
5872
6029
  fixable: false,
5873
6030
  file: cfgFile,
5874
6031
  fix: 'Set explicit context size limits: maxContextSize, memory.maxEntries, or memory.maxSize.',
6032
+ guidance: 'Without size limits, an attacker can craft inputs that overflow the context window, pushing safety instructions out of scope and taking over agent behavior.',
5875
6033
  });
5876
6034
  }
5877
6035
  }
@@ -5903,6 +6061,7 @@ dist/
5903
6061
  fixable: false,
5904
6062
  file: maFile,
5905
6063
  fix: 'Enable memory isolation: set sharedMemory.isolation=true or use per-agent memory scopes.',
6064
+ guidance: 'Shared memory without isolation lets a compromised agent poison context used by all other agents. Use per-agent scopes to prevent cross-agent influence.',
5906
6065
  });
5907
6066
  }
5908
6067
  }
@@ -5939,6 +6098,7 @@ dist/
5939
6098
  file: path.relative(targetDir, file),
5940
6099
  line: i + 1,
5941
6100
  fix: 'Sanitize conversation history before including in system prompts. Strip instruction-like patterns.',
6101
+ guidance: 'Unvalidated conversation history concatenated into system prompts enables indirect prompt injection. Attackers can craft messages that inject instructions.',
5942
6102
  });
5943
6103
  break; // One finding per file
5944
6104
  }
@@ -5981,6 +6141,7 @@ dist/
5981
6141
  fixable: false,
5982
6142
  file: ragFile,
5983
6143
  fix: 'Add source verification: set trustedSource=true only for validated endpoints, or enable signatureCheck.',
6144
+ guidance: 'Unverified RAG sources can be compromised to inject malicious instructions into agent context. Verify sources with signatures or explicit trust markers.',
5984
6145
  });
5985
6146
  }
5986
6147
  }
@@ -6019,6 +6180,7 @@ dist/
6019
6180
  file: path.relative(targetDir, file),
6020
6181
  line: i + 1,
6021
6182
  fix: 'Sanitize retrieved content before including in prompts. Strip instruction-like patterns and markup.',
6183
+ guidance: 'Poisoned documents in a vector store can contain prompt injections that override agent behavior when retrieved. Sanitize before including in prompts.',
6022
6184
  });
6023
6185
  break;
6024
6186
  }
@@ -6048,6 +6210,7 @@ dist/
6048
6210
  fixable: false,
6049
6211
  file: ragFile,
6050
6212
  fix: 'Restrict vector store write access. Require authentication for document ingestion.',
6213
+ guidance: 'Public-writable vector stores let anyone inject poisoned documents. These documents are retrieved by the agent and can influence its responses and behavior.',
6051
6214
  });
6052
6215
  }
6053
6216
  }
@@ -6072,6 +6235,7 @@ dist/
6072
6235
  fixable: false,
6073
6236
  file: ragFile,
6074
6237
  fix: 'Enable provenance tracking: set sourceTracking=true to track which source each document came from.',
6238
+ guidance: 'Without provenance tracking, poisoned content cannot be traced to its source during incident response. Source tracking enables rapid identification and removal.',
6075
6239
  });
6076
6240
  }
6077
6241
  }
@@ -6109,7 +6273,8 @@ dist/
6109
6273
  message: `${idFile} declares identity without cryptographic key binding`,
6110
6274
  fixable: false,
6111
6275
  file: idFile,
6112
- fix: 'Run `hackmyagent fix-all --with-aim` to bind identity to an Ed25519 key pair automatically',
6276
+ fix: 'hackmyagent fix-all --with-aim',
6277
+ guidance: 'Without cryptographic binding, any agent can impersonate this identity. Ed25519 key pairs provide proof of identity through digital signatures.',
6113
6278
  });
6114
6279
  }
6115
6280
  }
@@ -6127,6 +6292,7 @@ dist/
6127
6292
  fixable: false,
6128
6293
  file: idFile,
6129
6294
  fix: 'Add a verification endpoint: verificationEndpoint URL or oidcIssuer for federated identity.',
6295
+ guidance: 'Without a verification endpoint, other agents and registries cannot verify identity claims. This enables identity spoofing in multi-agent systems.',
6130
6296
  });
6131
6297
  }
6132
6298
  }
@@ -6150,7 +6316,8 @@ dist/
6150
6316
  message: 'Agent project has no identity declaration file (agent-card.json, agent.json, aim.json)',
6151
6317
  fixable: false,
6152
6318
  file: 'package.json',
6153
- fix: 'Run `hackmyagent fix-all --with-aim` to create a cryptographic identity with Ed25519 key pair, audit logging, and trust scoring',
6319
+ fix: 'hackmyagent fix-all --with-aim',
6320
+ guidance: 'Without a formal identity declaration, the agent cannot be verified by other agents, registries, or trust frameworks. Creates an Ed25519 key pair with audit logging.',
6154
6321
  });
6155
6322
  }
6156
6323
  }
@@ -6188,7 +6355,8 @@ dist/
6188
6355
  message: `${dnaFile} has no signature or content hash`,
6189
6356
  fixable: false,
6190
6357
  file: dnaFile,
6191
- fix: 'Run `hackmyagent fix-all --with-aim` to automatically sign behavioral profiles with a cryptographic identity',
6358
+ fix: 'hackmyagent fix-all --with-aim',
6359
+ guidance: 'Unsigned behavioral profiles can be silently modified to change agent behavior. Cryptographic signatures enable tamper detection on every load.',
6192
6360
  });
6193
6361
  }
6194
6362
  // DNA-003: No behavioral drift detection
@@ -6204,6 +6372,7 @@ dist/
6204
6372
  fixable: false,
6205
6373
  file: dnaFile,
6206
6374
  fix: 'Enable behavioral drift detection: set baselineHash and driftThreshold for continuous monitoring.',
6375
+ guidance: 'Without drift detection, gradual behavioral changes (prompt drift, personality shifts) go unnoticed. A baseline hash detects any deviation from expected behavior.',
6207
6376
  });
6208
6377
  }
6209
6378
  }
@@ -6237,6 +6406,7 @@ dist/
6237
6406
  fixable: false,
6238
6407
  file: foundSoulFile || 'SOUL.md',
6239
6408
  fix: 'Create agent-dna.json with contentHash of SOUL.md, baselineHash, and signature for integrity verification.',
6409
+ guidance: 'A behavioral fingerprint enables continuous integrity verification of agent instructions. Without it, modifications to SOUL.md cannot be detected.',
6240
6410
  });
6241
6411
  }
6242
6412
  }
@@ -6269,6 +6439,7 @@ dist/
6269
6439
  fixable: false,
6270
6440
  file: 'SKILL.md',
6271
6441
  fix: 'Restrict skill memory access: declare explicit read-only or scoped-write permissions in SKILL.md.',
6442
+ guidance: 'Skills with unrestricted memory write access can poison agent context, alter future responses, or plant persistent backdoors that survive restarts.',
6272
6443
  });
6273
6444
  }
6274
6445
  }
@@ -6297,6 +6468,7 @@ dist/
6297
6468
  fixable: false,
6298
6469
  file: path.relative(targetDir, file),
6299
6470
  fix: 'Add read-only guards or scope memory writes to skill-specific namespaces.',
6471
+ guidance: 'Unrestricted memory writes from skills can alter agent state across all contexts. Scope writes to skill-specific namespaces to prevent cross-skill interference.',
6300
6472
  });
6301
6473
  break; // One per skill dir
6302
6474
  }
@@ -6454,7 +6626,8 @@ dist/
6454
6626
  file: relativePath,
6455
6627
  line: firstLine,
6456
6628
  fixable: false,
6457
- fix: 'Inspect the file with a hex editor (e.g., xxd) to identify and remove invisible Unicode codepoints. Run: xxd ' + shellEscape(relativePath) + ' | grep -iE "e280[8-9a-e]|efbb|efb8|f3a0"',
6629
+ fix: 'xxd ' + shellEscape(relativePath) + ' | grep -iE "e280[8-9a-e]|efbb|efb8|f3a0"',
6630
+ guidance: 'Invisible Unicode codepoints (zero-width chars, variation selectors, tag characters, bidi overrides) can hide malicious payloads in source code. This is the GlassWorm attack vector. Inspect with a hex editor and remove all non-functional invisible characters.',
6458
6631
  });
6459
6632
  }
6460
6633
  // UNICODE-STEGO-002: GlassWorm Decoder Pattern
@@ -6490,7 +6663,8 @@ dist/
6490
6663
  file: relativePath,
6491
6664
  line: decoderLine,
6492
6665
  fixable: false,
6493
- fix: 'Review the file for suspicious .codePointAt() logic that decodes hidden data from variation selectors (0xFE00-0xFE0F) or tag characters (0xE0100-0xE01EF). Remove the decoder function and audit the file for tampering.',
6666
+ fix: 'Review the file for suspicious .codePointAt() logic that decodes hidden data from variation selectors (0xFE00-0xFE0F) or tag characters (0xE0100-0xE01EF). Remove the decoder function.',
6667
+ guidance: 'The GlassWorm attack encodes malicious payloads in invisible Unicode characters and uses .codePointAt() to decode them at runtime. This is the decoder half of the attack.',
6494
6668
  });
6495
6669
  }
6496
6670
  // UNICODE-STEGO-003: Eval on Empty String
@@ -6544,7 +6718,8 @@ dist/
6544
6718
  file: relativePath,
6545
6719
  line: evalLine,
6546
6720
  fixable: false,
6547
- fix: 'Remove the eval/Function call and inspect the string argument with a hex editor. The string likely contains invisible Unicode characters encoding a malicious payload. Run: node -e "const fs=require(\'fs\'); const s=fs.readFileSync(' + JSON.stringify(relativePath) + ',\'utf8\'); console.log([...s].filter(c=>c.codePointAt(0)>0x200).map(c=>c.codePointAt(0).toString(16)))"',
6721
+ fix: 'node -e "const fs=require(\'fs\'); const s=fs.readFileSync(' + JSON.stringify(relativePath) + ',\'utf8\'); console.log([...s].filter(c=>c.codePointAt(0)>0x200).map(c=>c.codePointAt(0).toString(16)))"',
6722
+ guidance: 'eval() or Function() called with mostly invisible characters is a strong indicator of a GlassWorm payload. The string contains hidden Unicode characters encoding malicious code. Remove the eval/Function call and audit the file.',
6548
6723
  });
6549
6724
  break; // One finding per file
6550
6725
  }
@@ -6586,7 +6761,8 @@ dist/
6586
6761
  file: relativePath,
6587
6762
  line: tagBlockLine,
6588
6763
  fixable: false,
6589
- fix: 'Inspect the file with a hex editor to identify tag block characters (byte sequence starting with F3 A0). These characters are invisible and have no legitimate use in source code. Run: xxd ' + shellEscape(relativePath) + ' | grep "f3a0"',
6764
+ fix: 'xxd ' + shellEscape(relativePath) + ' | grep "f3a0"',
6765
+ guidance: 'Unicode Tag block characters (U+E0000-U+E01EF) are invisible and have no legitimate use in source code. They can encode hidden data or malicious payloads. Remove all tag block characters found.',
6590
6766
  });
6591
6767
  }
6592
6768
  }
@@ -6638,7 +6814,8 @@ dist/
6638
6814
  file: relativePath,
6639
6815
  line: homoglyphLine,
6640
6816
  fixable: false,
6641
- fix: 'Inspect the file for characters that look like Latin letters but are actually Cyrillic, Greek, or Fullwidth. Replace them with their ASCII equivalents. Run: node -e "const fs=require(\'fs\'); [...fs.readFileSync(' + JSON.stringify(relativePath) + ',\'utf8\')].forEach((c,i)=>{const cp=c.codePointAt(0); if(cp>0x7F && cp<0xFFFF) console.log(i, cp.toString(16), c)})"',
6817
+ fix: 'node -e "const fs=require(\'fs\'); [...fs.readFileSync(' + JSON.stringify(relativePath) + ',\'utf8\')].forEach((c,i)=>{const cp=c.codePointAt(0); if(cp>0x7F && cp<0xFFFF) console.log(i, cp.toString(16), c)})"',
6818
+ guidance: 'Homoglyph confusables (Cyrillic/Greek/Fullwidth characters that look like Latin letters) can create variable names that appear identical in code review but reference different values at runtime. Replace with ASCII equivalents.',
6642
6819
  });
6643
6820
  }
6644
6821
  }
@@ -6689,7 +6866,8 @@ dist/
6689
6866
  fixable: false,
6690
6867
  file: path.relative(targetDir, file),
6691
6868
  line: i + 1,
6692
- fix: 'Download to temp file, verify checksum, then execute: curl -o /tmp/install.sh URL && echo "EXPECTED_SHA256 /tmp/install.sh" | sha256sum -c && bash /tmp/install.sh',
6869
+ fix: 'curl -o /tmp/install.sh URL && echo "EXPECTED_SHA256 /tmp/install.sh" | sha256sum -c && bash /tmp/install.sh',
6870
+ guidance: 'Piping curl directly to sh executes whatever the remote server returns. A compromised or MITM-ed server can inject arbitrary code. Always download, verify, then execute.',
6693
6871
  });
6694
6872
  }
6695
6873
  }
@@ -6731,7 +6909,8 @@ dist/
6731
6909
  fixable: false,
6732
6910
  file: path.relative(targetDir, file),
6733
6911
  line: i + 1,
6734
- fix: 'Require non-empty digest; fail verification if digest is missing. Compute with: sha256sum <artifact>',
6912
+ fix: 'sha256sum <artifact>',
6913
+ guidance: 'Empty digest fields bypass integrity verification entirely. Require non-empty digests and fail builds when they are missing, so tampered artifacts cannot pass.',
6735
6914
  });
6736
6915
  }
6737
6916
  }
@@ -6759,6 +6938,7 @@ dist/
6759
6938
  file: path.relative(targetDir, file),
6760
6939
  line: i + 1,
6761
6940
  fix: 'Require non-empty digest; fail verification if digest is missing instead of skipping the check.',
6941
+ guidance: 'Skipping digest verification when the value is falsy means an attacker can remove the digest field to bypass integrity checks. Treat missing digest as a hard failure.',
6762
6942
  });
6763
6943
  }
6764
6944
  }
@@ -6804,6 +6984,7 @@ dist/
6804
6984
  file: path.relative(targetDir, file),
6805
6985
  line: i + 1,
6806
6986
  fix: 'Gate policy reload behind operator authentication, not agent output or user requests.',
6987
+ guidance: 'User-reachable policy reload paths allow attackers to modify security policies via crafted requests. Only operators (authenticated admins) should trigger policy changes.',
6807
6988
  });
6808
6989
  }
6809
6990
  }
@@ -6851,6 +7032,7 @@ dist/
6851
7032
  file: path.relative(targetDir, file),
6852
7033
  line: i + 1,
6853
7034
  fix: 'Pass credentials via environment variables or stdin, not command-line arguments.',
7035
+ guidance: 'CLI arguments are visible in process listings (ps aux), shell history, and log files. Environment variables and stdin are not exposed to other processes.',
6854
7036
  });
6855
7037
  }
6856
7038
  }
@@ -6895,6 +7077,7 @@ dist/
6895
7077
  file: path.relative(targetDir, file),
6896
7078
  line: i + 1,
6897
7079
  fix: 'Use execFile() or spawn() with array arguments instead of exec() with string interpolation.',
7080
+ guidance: 'exec() passes the entire string to /bin/sh, which interprets shell metacharacters. execFile() and spawn() with arrays bypass the shell entirely, preventing injection.',
6898
7081
  });
6899
7082
  }
6900
7083
  }
@@ -6939,7 +7122,8 @@ dist/
6939
7122
  fixable: false,
6940
7123
  file: path.relative(targetDir, file),
6941
7124
  line: i + 1,
6942
- fix: 'Use mktemp for all temporary files; add trap EXIT for cleanup: TMPDIR=$(mktemp -d) && trap "rm -rf $TMPDIR" EXIT',
7125
+ fix: 'TMPDIR=$(mktemp -d) && trap "rm -rf $TMPDIR" EXIT',
7126
+ guidance: 'Hardcoded /tmp paths are predictable and enable symlink attacks (CWE-377). An attacker can pre-create a symlink at the expected path to redirect writes to sensitive files.',
6943
7127
  });
6944
7128
  }
6945
7129
  }
@@ -6979,7 +7163,8 @@ dist/
6979
7163
  fixable: false,
6980
7164
  file: path.relative(targetDir, file),
6981
7165
  line: i + 1,
6982
- fix: 'Use an explicit env var allowlist instead of spreading process.env: env: { PATH: process.env.PATH, NODE_ENV: process.env.NODE_ENV }',
7166
+ fix: 'env: { PATH: process.env.PATH, NODE_ENV: process.env.NODE_ENV }',
7167
+ guidance: 'Spreading process.env leaks all environment variables (including API keys, tokens, database URLs) to child processes. Use an explicit allowlist of only the variables the child needs.',
6983
7168
  });
6984
7169
  }
6985
7170
  }
@@ -7039,6 +7224,7 @@ dist/
7039
7224
  file: path.relative(targetDir, verify.verifyFile),
7040
7225
  line: verify.verifyLine,
7041
7226
  fix: 'Copy artifact to temp dir, verify the copy, execute from the copy (atomic verify-then-execute in the same function).',
7227
+ guidance: 'When verify and execute are in separate files, an attacker can swap the artifact between verification and use (TOCTOU). Atomic verify-then-execute eliminates this race window.',
7042
7228
  });
7043
7229
  }
7044
7230
  }
@@ -7077,6 +7263,7 @@ dist/
7077
7263
  file: path.relative(targetDir, file),
7078
7264
  line: i + 1,
7079
7265
  fix: 'Use json.load() or a restricted deserializer instead of pickle for untrusted data.',
7266
+ guidance: 'pickle.load() can execute arbitrary Python code during deserialization. A crafted pickle payload achieves full remote code execution.',
7080
7267
  });
7081
7268
  }
7082
7269
  if (/yaml\.load\s*\(/.test(line) && !/Loader\s*=\s*SafeLoader/.test(line) && !/safe_load/.test(line)) {
@@ -7093,6 +7280,7 @@ dist/
7093
7280
  file: path.relative(targetDir, file),
7094
7281
  line: i + 1,
7095
7282
  fix: 'Use yaml.safe_load() or yaml.load(data, Loader=yaml.SafeLoader).',
7283
+ guidance: 'yaml.load() without SafeLoader can construct arbitrary Python objects, including those that execute code. SafeLoader restricts to basic data types.',
7096
7284
  });
7097
7285
  }
7098
7286
  if (/\beval\s*\(/.test(line) || /\bexec\s*\(/.test(line)) {
@@ -7109,6 +7297,7 @@ dist/
7109
7297
  file: path.relative(targetDir, file),
7110
7298
  line: i + 1,
7111
7299
  fix: 'Replace eval/exec with ast.literal_eval() for data parsing, or use a safe DSL.',
7300
+ guidance: 'eval() and exec() execute arbitrary code. If input originates from untrusted sources (user input, network, files), this is a direct code injection vector.',
7112
7301
  });
7113
7302
  }
7114
7303
  }
@@ -7136,6 +7325,7 @@ dist/
7136
7325
  file: path.relative(targetDir, file),
7137
7326
  line: i + 1,
7138
7327
  fix: 'Use JSON.parse() for data, or a sandboxed evaluator for expressions.',
7328
+ guidance: 'eval() executes arbitrary JavaScript. If the string comes from user input, network data, or files, an attacker can inject any code.',
7139
7329
  });
7140
7330
  }
7141
7331
  if (/new\s+Function\s*\(/.test(line)) {
@@ -7152,6 +7342,7 @@ dist/
7152
7342
  file: path.relative(targetDir, file),
7153
7343
  line: i + 1,
7154
7344
  fix: 'Use JSON.parse() for data, or a sandboxed evaluator for expressions.',
7345
+ guidance: 'new Function() is equivalent to eval() -- it creates executable code from strings. If the string source is untrusted, this enables arbitrary code execution.',
7155
7346
  });
7156
7347
  }
7157
7348
  if (/JSON5\.parse/.test(line)) {
@@ -7168,6 +7359,7 @@ dist/
7168
7359
  file: path.relative(targetDir, file),
7169
7360
  line: i + 1,
7170
7361
  fix: 'Use JSON.parse() instead of JSON5.parse() for untrusted data.',
7362
+ guidance: 'JSON5 accepts comments, trailing commas, and unquoted keys, expanding the parsing surface. For untrusted input, strict JSON.parse() reduces ambiguity and attack surface.',
7171
7363
  });
7172
7364
  }
7173
7365
  }
@@ -7216,6 +7408,7 @@ dist/
7216
7408
  file: path.relative(targetDir, file),
7217
7409
  line: i + 1,
7218
7410
  fix: 'Remove messaging APIs from base sandbox policy; require explicit operator opt-in per deployment.',
7411
+ guidance: 'Pre-allowed messaging APIs (Telegram, Discord, Slack, webhook.site) enable agents to exfiltrate data without user approval. Require explicit operator opt-in per deployment.',
7219
7412
  });
7220
7413
  }
7221
7414
  }
@@ -7264,33 +7457,38 @@ dist/
7264
7457
  fixReplacement: '$1127.0.0.1',
7265
7458
  severity: 'critical',
7266
7459
  description: 'Ollama server configured to listen on all interfaces. Our research found 294K+ exposed AI services on the internet — many are Ollama instances.',
7267
- fix: 'Set OLLAMA_HOST=127.0.0.1 to restrict to localhost. If remote access is needed, use a reverse proxy with authentication.' },
7460
+ fix: 'Set OLLAMA_HOST=127.0.0.1 to restrict to localhost. If remote access is needed, use a reverse proxy with authentication.',
7461
+ guidance: 'Our research found 294K+ exposed AI services on the internet. Public Ollama instances allow anyone to run inference, steal models, or use your GPU resources.' },
7268
7462
  { id: 'LLM-001', name: 'Ollama Port Exposed', service: 'Ollama',
7269
7463
  pattern: /^(?!.*127\.0\.0\.1).*["']?11434["']?\s*:\s*["']?11434["']?/,
7270
7464
  fixPattern: /(["']?)11434(["']?\s*:\s*["']?11434["']?)/,
7271
7465
  fixReplacement: '$1127.0.0.1:11434$2',
7272
7466
  severity: 'high',
7273
7467
  description: 'Ollama default port (11434) mapped in container config. Without bind restrictions, this exposes the inference API to the network.',
7274
- fix: 'Map to localhost only: "127.0.0.1:11434:11434" instead of "11434:11434".' },
7468
+ fix: 'Map to localhost only: "127.0.0.1:11434:11434" instead of "11434:11434".',
7469
+ guidance: 'Docker port mappings without host binding expose the port on all interfaces. Prefix with 127.0.0.1: to restrict to localhost only.' },
7275
7470
  { id: 'LLM-002', name: 'vLLM/LocalAI Public Binding', service: 'vLLM/LocalAI',
7276
7471
  pattern: /--host\s+0\.0\.0\.0|host:\s*["']?0\.0\.0\.0/i,
7277
7472
  fixPattern: /(--host\s+|host:\s*["']?)0\.0\.0\.0/i,
7278
7473
  fixReplacement: '$1127.0.0.1',
7279
7474
  severity: 'critical',
7280
7475
  description: 'LLM inference server configured to bind to all interfaces.',
7281
- fix: 'Use --host 127.0.0.1 or bind to localhost. Use a reverse proxy with auth for remote access.' },
7476
+ fix: 'Use --host 127.0.0.1 or bind to localhost. Use a reverse proxy with auth for remote access.',
7477
+ guidance: 'vLLM and LocalAI bound to 0.0.0.0 expose the inference API to all network interfaces. Anyone on the network can query models or abuse GPU resources.' },
7282
7478
  { id: 'LLM-003', name: 'Text Generation WebUI Exposed', service: 'text-generation-webui',
7283
7479
  pattern: /--listen\s|--share\s|GRADIO_SERVER_NAME\s*=\s*["']?0\.0\.0\.0/i,
7284
7480
  fixPattern: /\s*--listen\s?|\s*--share\s?|(GRADIO_SERVER_NAME\s*=\s*["']?)0\.0\.0\.0/gi,
7285
7481
  fixReplacement: '$1127.0.0.1',
7286
7482
  severity: 'high',
7287
7483
  description: 'Text generation UI configured for public access with --listen or --share flag.',
7288
- fix: 'Remove --listen and --share flags. Access via localhost or SSH tunnel.' },
7484
+ fix: 'Remove --listen and --share flags. Access via localhost or SSH tunnel.',
7485
+ guidance: '--listen binds to all interfaces and --share creates a public Gradio URL. Both expose the text generation UI to the internet without authentication.' },
7289
7486
  { id: 'LLM-004', name: 'OpenAI-Compatible API No Auth', service: 'OpenAI-compatible',
7290
7487
  pattern: /\/v1\/chat\/completions|\/v1\/completions|\/v1\/models/,
7291
7488
  severity: 'medium',
7292
7489
  description: 'Project exposes OpenAI-compatible API endpoints. Verify authentication is enforced.',
7293
- fix: 'Ensure API key or token authentication is required for all inference endpoints.' },
7490
+ fix: 'Ensure API key or token authentication is required for all inference endpoints.',
7491
+ guidance: 'OpenAI-compatible API endpoints without authentication allow anyone to query your models, consume compute resources, and potentially extract training data.' },
7294
7492
  ];
7295
7493
  for (const configDef of llmConfigFiles) {
7296
7494
  const filesToCheck = [configDef.name, ...configDef.altNames];
@@ -7327,6 +7525,7 @@ dist/
7327
7525
  fixable: !!check.fixPattern,
7328
7526
  fixed,
7329
7527
  fix: check.fix,
7528
+ guidance: check.guidance,
7330
7529
  });
7331
7530
  break; // One finding per pattern per file
7332
7531
  }
@@ -7371,6 +7570,7 @@ dist/
7371
7570
  severity: 'critical',
7372
7571
  description: 'Jupyter notebook server configured without authentication or bound to public interface. Our research found exposed Jupyter instances with full code execution on the internet.',
7373
7572
  fix: 'Set c.NotebookApp.token or c.NotebookApp.password. Bind to 127.0.0.1. Never use --NotebookApp.token=\'\' in production.',
7573
+ guidance: 'Jupyter notebooks allow arbitrary code execution. A publicly accessible instance with no auth gives attackers full shell access on the host machine.',
7374
7574
  filePatterns: ['jupyter_notebook_config.py', 'jupyter_server_config.py', 'docker-compose.yml', 'docker-compose.yaml', 'Dockerfile'],
7375
7575
  contentPatterns: [
7376
7576
  /NotebookApp\.token\s*=\s*['"]{2}/, // Empty token
@@ -7385,6 +7585,7 @@ dist/
7385
7585
  severity: 'high',
7386
7586
  description: 'ML demo framework configured for public access. Gradio share links and public Streamlit deployments can expose model inference and data pipelines.',
7387
7587
  fix: 'Remove share=True from Gradio launch(). For Streamlit, add authentication or use private deployment.',
7588
+ guidance: 'Gradio share links create public URLs that bypass network security. Streamlit on 0.0.0.0 exposes the app to the internet. Both can leak model inference and data pipelines.',
7388
7589
  filePatterns: ['*.py', 'app.py', 'main.py', 'streamlit_app.py', 'demo.py'],
7389
7590
  contentPatterns: [
7390
7591
  /\.launch\s*\([^)]*share\s*=\s*True/, // Gradio share=True
@@ -7397,6 +7598,7 @@ dist/
7397
7598
  severity: 'high',
7398
7599
  description: 'MLflow tracking server configured without authentication. Exposed MLflow instances leak experiment data, model artifacts, and parameters.',
7399
7600
  fix: 'Configure MLflow with --backend-store-uri and authentication. Use a reverse proxy with auth for remote access.',
7601
+ guidance: 'Exposed MLflow instances leak experiment data, model artifacts, hyperparameters, and metrics. Add authentication before exposing to any network.',
7400
7602
  filePatterns: ['docker-compose.yml', 'docker-compose.yaml', 'Dockerfile', 'Makefile', '*.sh'],
7401
7603
  contentPatterns: [
7402
7604
  /mlflow\s+server\s+.*--host\s+0\.0\.0\.0/i,
@@ -7409,6 +7611,7 @@ dist/
7409
7611
  severity: 'high',
7410
7612
  description: 'LangChain LangServe endpoint configured for public access. Exposed LangServe instances allow arbitrary chain invocation.',
7411
7613
  fix: 'Add authentication middleware to LangServe routes. Bind to 127.0.0.1 for local-only access.',
7614
+ guidance: 'LangServe exposes LangChain chains as REST endpoints. Without auth, anyone can invoke arbitrary chain operations, potentially accessing sensitive data or incurring costs.',
7412
7615
  filePatterns: ['*.py', 'app.py', 'main.py', 'server.py'],
7413
7616
  contentPatterns: [
7414
7617
  /add_routes\s*\(/, // LangServe route
@@ -7479,6 +7682,7 @@ dist/
7479
7682
  fixable: isFixable,
7480
7683
  fixed,
7481
7684
  fix: check.fix,
7685
+ guidance: check.guidance,
7482
7686
  });
7483
7687
  break;
7484
7688
  }
@@ -7528,6 +7732,7 @@ dist/
7528
7732
  file: relativePath,
7529
7733
  fixable: false,
7530
7734
  fix: 'Add authentication requirements to your agent card. Restrict task submission to authenticated clients.',
7735
+ guidance: 'A2A agent cards make your agent discoverable on the network. Without authentication requirements, any client can submit tasks and consume resources or access sensitive data.',
7531
7736
  details: { hasAuth, capabilities: agentCard.capabilities },
7532
7737
  });
7533
7738
  break; // Found one, no need to check other paths
@@ -7559,6 +7764,7 @@ dist/
7559
7764
  line: i + 1,
7560
7765
  fixable: false,
7561
7766
  fix: 'Add authentication middleware to /tasks/send and /tasks/get endpoints. Require API key or bearer token.',
7767
+ guidance: 'Unauthenticated A2A task endpoints allow anyone to submit tasks to your agent. This can lead to resource abuse, data exfiltration, or unauthorized actions.',
7562
7768
  });
7563
7769
  break;
7564
7770
  }
@@ -7604,6 +7810,7 @@ dist/
7604
7810
  file: relativePath,
7605
7811
  fixable: false,
7606
7812
  fix: 'Remove .well-known/mcp from public-facing directories, or restrict access via web server configuration. Never include credentials in discovery files.',
7813
+ guidance: 'MCP discovery files make servers publicly discoverable. If they contain credentials, those are exposed to anyone who requests the URL. Restrict access or remove from public directories.',
7607
7814
  });
7608
7815
  break;
7609
7816
  }
@@ -7685,6 +7892,7 @@ dist/
7685
7892
  fixable: true,
7686
7893
  fixed,
7687
7894
  fix: `Move credentials to server-side environment variables. Never include API keys in client-side code or static assets. Use a backend proxy for API calls.`,
7895
+ guidance: 'Credentials in web-served files (HTML, JS, CSS) are visible to anyone who views the page source. API keys in client-side code can be extracted and abused for unauthorized access.',
7688
7896
  });
7689
7897
  break;
7690
7898
  }
@@ -7700,6 +7908,768 @@ dist/
7700
7908
  }
7701
7909
  return findings;
7702
7910
  }
7911
+ /**
7912
+ * CODEINJ-001: exec() with template literal interpolation
7913
+ * Detects shell injection via exec/execSync called with template literals.
7914
+ */
7915
+ async checkCodeInjection(targetDir, _autoFix) {
7916
+ const findings = [];
7917
+ const files = await this.walkDirectory(targetDir, ['.ts', '.js', '.mjs'], 0, 2);
7918
+ // Match exec( or execSync( followed by a backtick (template literal)
7919
+ // Do NOT match execFile or execFileSync (those use array args, safe)
7920
+ const pattern = /\b(?<!File)exec(?:Sync)?\s*\(\s*`/g;
7921
+ for (const file of files.slice(0, 100)) {
7922
+ try {
7923
+ const stat = await fs.stat(file);
7924
+ if (stat.size > MAX_FILE_SIZE)
7925
+ continue;
7926
+ const content = await fs.readFile(file, 'utf-8');
7927
+ const lines = content.split('\n');
7928
+ const relativePath = path.relative(targetDir, file);
7929
+ for (let i = 0; i < lines.length; i++) {
7930
+ if (lines[i].length > MAX_LINE_LENGTH)
7931
+ continue;
7932
+ pattern.lastIndex = 0;
7933
+ if (pattern.test(lines[i])) {
7934
+ findings.push({
7935
+ checkId: 'CODEINJ-001',
7936
+ name: 'exec() with template literal interpolation',
7937
+ description: 'exec() or execSync() called with a template literal allows shell injection. User-controlled values in the template can break out of the intended command.',
7938
+ category: 'code-injection',
7939
+ severity: 'critical',
7940
+ passed: false,
7941
+ message: `Shell injection risk: exec() with template literal in ${relativePath}`,
7942
+ file: relativePath,
7943
+ line: i + 1,
7944
+ fixable: false,
7945
+ fix: 'Use execFile() or execFileSync() with an array of arguments instead of exec() with string interpolation.',
7946
+ guidance: 'Template literals in exec() are interpreted by /bin/sh, allowing shell metacharacters in interpolated values to execute arbitrary commands. Array-based APIs bypass the shell.',
7947
+ });
7948
+ break; // One finding per file
7949
+ }
7950
+ }
7951
+ }
7952
+ catch { /* skip unreadable files */ }
7953
+ }
7954
+ return findings;
7955
+ }
7956
+ /**
7957
+ * INSTALL-001: curl|sh without checksum in shell scripts
7958
+ * Detects piped-to-shell install patterns in .sh files.
7959
+ */
7960
+ async checkInstallScripts(targetDir, _autoFix) {
7961
+ const findings = [];
7962
+ const files = await this.walkDirectory(targetDir, ['.sh'], 0, 2);
7963
+ const pattern = /\b(curl|wget)\b[^|]*\|\s*(ba)?sh\b/g;
7964
+ for (const file of files.slice(0, 100)) {
7965
+ try {
7966
+ const stat = await fs.stat(file);
7967
+ if (stat.size > MAX_FILE_SIZE)
7968
+ continue;
7969
+ const content = await fs.readFile(file, 'utf-8');
7970
+ const lines = content.split('\n');
7971
+ const relativePath = path.relative(targetDir, file);
7972
+ for (let i = 0; i < lines.length; i++) {
7973
+ if (lines[i].length > MAX_LINE_LENGTH)
7974
+ continue;
7975
+ pattern.lastIndex = 0;
7976
+ if (pattern.test(lines[i])) {
7977
+ findings.push({
7978
+ checkId: 'INSTALL-001',
7979
+ name: 'curl|sh without checksum verification',
7980
+ description: 'Shell script downloads and executes code via curl|sh or wget|sh without verifying a checksum. A MITM or compromised server can inject arbitrary code.',
7981
+ category: 'supply-chain',
7982
+ severity: 'critical',
7983
+ passed: false,
7984
+ message: `Unsafe pipe-to-shell install in ${relativePath}`,
7985
+ file: relativePath,
7986
+ line: i + 1,
7987
+ fixable: false,
7988
+ fix: 'Download the script to a file first, verify its checksum (sha256sum), then execute it.',
7989
+ guidance: 'Piping directly to sh executes whatever the remote server returns without verification. A compromised or MITM-ed server can inject arbitrary code into your system.',
7990
+ });
7991
+ break;
7992
+ }
7993
+ }
7994
+ }
7995
+ catch { /* skip unreadable files */ }
7996
+ }
7997
+ return findings;
7998
+ }
7999
+ /**
8000
+ * CLIPASS-001: Credentials passed as CLI arguments
8001
+ * Detects --token, --password, --api-key, --secret followed by variable interpolation.
8002
+ */
8003
+ async checkCLICredentialPassthrough(targetDir, _autoFix) {
8004
+ const findings = [];
8005
+ const files = await this.walkDirectory(targetDir, ['.ts', '.js', '.mjs'], 0, 2);
8006
+ const pattern = /--(token|password|api[_-]?key|secret|auth)\s*[=\s]\s*[`$"']\s*\$?\{?|["']--(token|password|api[_-]?key|secret|auth)["']/g;
8007
+ for (const file of files.slice(0, 100)) {
8008
+ try {
8009
+ const stat = await fs.stat(file);
8010
+ if (stat.size > MAX_FILE_SIZE)
8011
+ continue;
8012
+ const content = await fs.readFile(file, 'utf-8');
8013
+ const lines = content.split('\n');
8014
+ const relativePath = path.relative(targetDir, file);
8015
+ for (let i = 0; i < lines.length; i++) {
8016
+ if (lines[i].length > MAX_LINE_LENGTH)
8017
+ continue;
8018
+ pattern.lastIndex = 0;
8019
+ if (pattern.test(lines[i])) {
8020
+ // Verify it's in a spawn/exec context
8021
+ const contextStart = Math.max(0, i - 5);
8022
+ const context = lines.slice(contextStart, i + 1).join('\n');
8023
+ if (/\b(exec\w*|spawn\w*|fork|run)\b/i.test(context)) {
8024
+ findings.push({
8025
+ checkId: 'CLIPASS-001',
8026
+ name: 'Credentials passed as CLI arguments',
8027
+ description: 'Credentials are passed as command-line arguments (--token, --password, etc.) with variable interpolation. CLI args are visible in process listings (ps aux) and shell history.',
8028
+ category: 'credential-exposure',
8029
+ severity: 'high',
8030
+ passed: false,
8031
+ message: `Credentials passed as CLI arguments in ${relativePath}`,
8032
+ file: relativePath,
8033
+ line: i + 1,
8034
+ fixable: false,
8035
+ fix: 'Pass credentials via environment variables, stdin, or a config file instead of CLI arguments.',
8036
+ guidance: 'CLI arguments are visible to all users via ps aux, logged in shell history, and often captured in audit logs. Environment variables and stdin are not exposed to other processes.',
8037
+ });
8038
+ break;
8039
+ }
8040
+ }
8041
+ }
8042
+ }
8043
+ catch { /* skip unreadable files */ }
8044
+ }
8045
+ return findings;
8046
+ }
8047
+ /**
8048
+ * INTEGRITY-001: Digest/hash verification bypass on falsy value
8049
+ * Detects patterns like `if (digest &&` or `if (hash &&` where empty value skips check.
8050
+ */
8051
+ async checkIntegrityBypass(targetDir, _autoFix) {
8052
+ const findings = [];
8053
+ const files = await this.walkDirectory(targetDir, ['.ts', '.js', '.mjs'], 0, 2);
8054
+ const pattern = /if\s*\(\s*(digest|hash|checksum|signature|integrity)\s*&&/g;
8055
+ for (const file of files.slice(0, 100)) {
8056
+ try {
8057
+ const stat = await fs.stat(file);
8058
+ if (stat.size > MAX_FILE_SIZE)
8059
+ continue;
8060
+ const content = await fs.readFile(file, 'utf-8');
8061
+ const lines = content.split('\n');
8062
+ const relativePath = path.relative(targetDir, file);
8063
+ for (let i = 0; i < lines.length; i++) {
8064
+ if (lines[i].length > MAX_LINE_LENGTH)
8065
+ continue;
8066
+ pattern.lastIndex = 0;
8067
+ if (pattern.test(lines[i])) {
8068
+ findings.push({
8069
+ checkId: 'INTEGRITY-001',
8070
+ name: 'Integrity check bypass on falsy value',
8071
+ description: 'Integrity verification (digest/hash/checksum) is guarded by a truthiness check. If the value is empty, undefined, or null, the entire integrity check is silently skipped.',
8072
+ category: 'integrity-bypass',
8073
+ severity: 'critical',
8074
+ passed: false,
8075
+ message: `Integrity check bypass risk in ${relativePath}`,
8076
+ file: relativePath,
8077
+ line: i + 1,
8078
+ fixable: false,
8079
+ fix: 'Require the integrity value to be present. Throw an error if digest/hash is missing rather than skipping verification.',
8080
+ guidance: 'A truthiness check on digest/hash silently skips verification when the value is empty. An attacker can remove the digest from a manifest to bypass all integrity checks.',
8081
+ });
8082
+ break;
8083
+ }
8084
+ }
8085
+ }
8086
+ catch { /* skip unreadable files */ }
8087
+ }
8088
+ return findings;
8089
+ }
8090
+ /**
8091
+ * TOCTOU-001: Verify then use without atomic operation
8092
+ * Detects files that verify and then execute on the same path without atomicity.
8093
+ */
8094
+ async checkTOCTOU(targetDir, _autoFix) {
8095
+ const findings = [];
8096
+ const files = await this.walkDirectory(targetDir, ['.ts', '.js', '.mjs'], 0, 2);
8097
+ for (const file of files.slice(0, 100)) {
8098
+ try {
8099
+ const stat = await fs.stat(file);
8100
+ if (stat.size > MAX_FILE_SIZE)
8101
+ continue;
8102
+ const content = await fs.readFile(file, 'utf-8');
8103
+ const relativePath = path.relative(targetDir, file);
8104
+ // Look for verify/check followed by execute/apply/run on the same path variable
8105
+ const hasVerify = /\b(verify|validate|check)(File|Path|Hash|Integrity|Signature|Digest|Config)?\s*\(/i.test(content);
8106
+ const hasExecute = /\b(execute|apply|run|install|load|import)(File|Path|Module|Script|Plugin|Config)?\s*\(/i.test(content);
8107
+ const hasFilePath = /\b(filePath|targetPath|scriptPath|modulePath|configPath)\b/.test(content);
8108
+ // Also detect inline TOCTOU: multiple readFile/readFileSync calls on the same path variable
8109
+ const readCalls = content.match(/\breadFile(Sync)?\s*\(\s*(\w+)/g) || [];
8110
+ const hasMultipleReads = readCalls.length >= 2;
8111
+ if ((hasVerify && hasExecute && hasFilePath) || (hasMultipleReads && hasFilePath)) {
8112
+ // Check that there's no file locking or atomic operation (ignore comments)
8113
+ const codeLines = content.split('\n').filter(l => !l.trimStart().startsWith('//') && !l.trimStart().startsWith('#') && !l.trimStart().startsWith('*'));
8114
+ const codeContent = codeLines.join('\n');
8115
+ const hasAtomic = /\b(atomic|lock|flock|rename|O_EXCL|O_CREAT)\b/i.test(codeContent);
8116
+ if (!hasAtomic) {
8117
+ // Find the line with verify for reporting
8118
+ const lines = content.split('\n');
8119
+ let verifyLine = 0;
8120
+ const verifyPattern = /\b(verify|validate|check)(File|Path|Hash|Integrity|Signature|Digest|Config)?\s*\(|\breadFileSync?\s*\(/i;
8121
+ for (let i = 0; i < lines.length; i++) {
8122
+ if (verifyPattern.test(lines[i])) {
8123
+ verifyLine = i + 1;
8124
+ break;
8125
+ }
8126
+ }
8127
+ findings.push({
8128
+ checkId: 'TOCTOU-001',
8129
+ name: 'Time-of-check-time-of-use race condition',
8130
+ description: 'File is verified (checksum/signature) and then used (executed/loaded) in separate operations without file locking. An attacker can replace the file between verify and use.',
8131
+ category: 'toctou-race',
8132
+ severity: 'high',
8133
+ passed: false,
8134
+ message: `TOCTOU risk: verify-then-use without atomic operation in ${relativePath}`,
8135
+ file: relativePath,
8136
+ line: verifyLine,
8137
+ fixable: false,
8138
+ fix: 'Use atomic file operations: verify and load in a single locked operation, or copy to a temp location before verification.',
8139
+ guidance: 'TOCTOU races allow file replacement between verification and use. An attacker can swap a verified file with a malicious one in the time window between the two operations.',
8140
+ });
8141
+ }
8142
+ }
8143
+ }
8144
+ catch { /* skip unreadable files */ }
8145
+ }
8146
+ return findings;
8147
+ }
8148
+ /**
8149
+ * TMPPATH-001: Hardcoded /tmp paths without mktemp
8150
+ * Detects writes to /tmp/ with hardcoded paths in shell scripts.
8151
+ */
8152
+ async checkTmpPaths(targetDir, _autoFix) {
8153
+ const findings = [];
8154
+ const files = await this.walkDirectory(targetDir, ['.sh'], 0, 2);
8155
+ const pattern = /(>|>>)\s*\/tmp\/|(-o)\s+\/tmp\/|\s\/tmp\/\S+/g;
8156
+ for (const file of files.slice(0, 100)) {
8157
+ try {
8158
+ const stat = await fs.stat(file);
8159
+ if (stat.size > MAX_FILE_SIZE)
8160
+ continue;
8161
+ const content = await fs.readFile(file, 'utf-8');
8162
+ const lines = content.split('\n');
8163
+ const relativePath = path.relative(targetDir, file);
8164
+ // Only flag if the script does NOT actually use mktemp (ignore comments)
8165
+ const nonCommentLines = lines.filter(l => !l.trimStart().startsWith('#'));
8166
+ if (/\bmktemp\b/.test(nonCommentLines.join('\n')))
8167
+ continue;
8168
+ for (let i = 0; i < lines.length; i++) {
8169
+ if (lines[i].length > MAX_LINE_LENGTH)
8170
+ continue;
8171
+ pattern.lastIndex = 0;
8172
+ if (pattern.test(lines[i])) {
8173
+ findings.push({
8174
+ checkId: 'TMPPATH-001',
8175
+ name: 'Hardcoded /tmp path without mktemp',
8176
+ description: 'Shell script writes to a hardcoded /tmp/ path. Another user or process can create a symlink at that path to redirect writes (symlink attack).',
8177
+ category: 'tmppath-attack',
8178
+ severity: 'high',
8179
+ passed: false,
8180
+ message: `Hardcoded /tmp path in ${relativePath}`,
8181
+ file: relativePath,
8182
+ line: i + 1,
8183
+ fixable: false,
8184
+ fix: 'Use mktemp to create a unique temporary file/directory instead of hardcoded /tmp paths.',
8185
+ guidance: 'Predictable /tmp paths enable symlink attacks (CWE-377). Another user can create a symlink at the expected path, redirecting writes to sensitive files like /etc/passwd.',
8186
+ });
8187
+ break;
8188
+ }
8189
+ }
8190
+ }
8191
+ catch { /* skip unreadable files */ }
8192
+ }
8193
+ return findings;
8194
+ }
8195
+ /**
8196
+ * DOCKERINJ-001: Docker exec with variable interpolation
8197
+ * Detects docker exec commands with unquoted variable expansion.
8198
+ */
8199
+ async checkDockerInjection(targetDir, _autoFix) {
8200
+ const findings = [];
8201
+ const files = await this.walkDirectory(targetDir, ['.sh'], 0, 2);
8202
+ const pattern = /docker\s+exec\b.*?(\$\{?\w+\}?)/g;
8203
+ for (const file of files.slice(0, 100)) {
8204
+ try {
8205
+ const stat = await fs.stat(file);
8206
+ if (stat.size > MAX_FILE_SIZE)
8207
+ continue;
8208
+ const content = await fs.readFile(file, 'utf-8');
8209
+ const lines = content.split('\n');
8210
+ const relativePath = path.relative(targetDir, file);
8211
+ for (let i = 0; i < lines.length; i++) {
8212
+ if (lines[i].length > MAX_LINE_LENGTH)
8213
+ continue;
8214
+ const line = lines[i];
8215
+ // Detect docker exec with any variable interpolation (quoted or not)
8216
+ if (/docker\s+exec\b/.test(line) && /\$\{?\w+\}?/.test(line)) {
8217
+ findings.push({
8218
+ checkId: 'DOCKERINJ-001',
8219
+ name: 'Docker exec with variable interpolation',
8220
+ description: 'docker exec command uses shell variable expansion. An attacker who controls the variable value can inject additional docker commands or escape the container context, even when quoted (e.g., in bash -c contexts).',
8221
+ category: 'code-injection',
8222
+ severity: 'high',
8223
+ passed: false,
8224
+ message: `Variable interpolation in docker exec in ${relativePath}`,
8225
+ file: relativePath,
8226
+ line: i + 1,
8227
+ fixable: false,
8228
+ fix: 'Avoid passing user-controlled variables to docker exec. Validate and sanitize all inputs, and avoid bash -c with interpolated variables.',
8229
+ guidance: 'Variable interpolation in docker exec allows command injection. If the variable contains shell metacharacters, an attacker can execute arbitrary commands inside or escape the container.',
8230
+ });
8231
+ break;
8232
+ }
8233
+ }
8234
+ }
8235
+ catch { /* skip unreadable files */ }
8236
+ }
8237
+ return findings;
8238
+ }
8239
+ /**
8240
+ * ENVLEAK-001: process.env spread to child process
8241
+ * Detects passing all environment variables (including secrets) to child processes.
8242
+ */
8243
+ async checkEnvLeak(targetDir, _autoFix) {
8244
+ const findings = [];
8245
+ const files = await this.walkDirectory(targetDir, ['.ts', '.js', '.mjs'], 0, 2);
8246
+ const spreadPattern = /env:\s*\{\s*\.\.\.process\.env/g;
8247
+ const directPattern = /\benv:\s*process\.env\b/g;
8248
+ for (const file of files.slice(0, 100)) {
8249
+ try {
8250
+ const stat = await fs.stat(file);
8251
+ if (stat.size > MAX_FILE_SIZE)
8252
+ continue;
8253
+ const content = await fs.readFile(file, 'utf-8');
8254
+ const lines = content.split('\n');
8255
+ const relativePath = path.relative(targetDir, file);
8256
+ for (let i = 0; i < lines.length; i++) {
8257
+ if (lines[i].length > MAX_LINE_LENGTH)
8258
+ continue;
8259
+ spreadPattern.lastIndex = 0;
8260
+ directPattern.lastIndex = 0;
8261
+ if (spreadPattern.test(lines[i]) || directPattern.test(lines[i])) {
8262
+ // Verify it's in a spawn/exec context
8263
+ const contextStart = Math.max(0, i - 5);
8264
+ const context = lines.slice(contextStart, i + 3).join('\n');
8265
+ if (/\b(spawn|exec|fork|execFile|execSync|spawnSync)\b/.test(context)) {
8266
+ findings.push({
8267
+ checkId: 'ENVLEAK-001',
8268
+ name: 'process.env spread to child process',
8269
+ description: 'All environment variables (including secrets like API keys, database passwords) are passed to a child process via env: process.env or { ...process.env }.',
8270
+ category: 'env-leak',
8271
+ severity: 'high',
8272
+ passed: false,
8273
+ message: `Full environment leaked to child process in ${relativePath}`,
8274
+ file: relativePath,
8275
+ line: i + 1,
8276
+ fixable: false,
8277
+ fix: 'Pass only the specific environment variables the child process needs: env: { PATH: process.env.PATH, NODE_ENV: process.env.NODE_ENV }.',
8278
+ guidance: 'Spreading process.env passes all secrets (API keys, DB passwords, tokens) to child processes. A compromised or malicious child can read and exfiltrate these credentials.',
8279
+ });
8280
+ break;
8281
+ }
8282
+ }
8283
+ }
8284
+ }
8285
+ catch { /* skip unreadable files */ }
8286
+ }
8287
+ return findings;
8288
+ }
8289
+ /**
8290
+ * SANDBOX-005: Messaging API pre-allowed in sandbox policy
8291
+ * Detects pre-allowed URLs for messaging services in sandbox policies.
8292
+ */
8293
+ async checkSandboxMessaging(targetDir, _autoFix) {
8294
+ const findings = [];
8295
+ // Scan YAML/JSON files in policies/ and config/ directories
8296
+ const policyDirs = ['policies', 'config', '.openclaw'];
8297
+ const policyExts = ['.yml', '.yaml', '.json'];
8298
+ for (const dirName of policyDirs) {
8299
+ const dirPath = path.join(targetDir, dirName);
8300
+ try {
8301
+ await fs.access(dirPath);
8302
+ }
8303
+ catch {
8304
+ continue;
8305
+ }
8306
+ const files = await this.walkDirectory(dirPath, policyExts, 0, 2);
8307
+ const messagingPattern = /\b(telegram|slack|discord|webhook\.site|requestbin|pipedream)\b/gi;
8308
+ for (const file of files.slice(0, 50)) {
8309
+ try {
8310
+ const stat = await fs.stat(file);
8311
+ if (stat.size > MAX_FILE_SIZE)
8312
+ continue;
8313
+ const content = await fs.readFile(file, 'utf-8');
8314
+ const lines = content.split('\n');
8315
+ const relativePath = path.relative(targetDir, file);
8316
+ for (let i = 0; i < lines.length; i++) {
8317
+ if (lines[i].length > MAX_LINE_LENGTH)
8318
+ continue;
8319
+ messagingPattern.lastIndex = 0;
8320
+ const match = messagingPattern.exec(lines[i]);
8321
+ if (match) {
8322
+ // Check if this is in an allow/whitelist context
8323
+ const contextStart = Math.max(0, i - 3);
8324
+ const context = lines.slice(contextStart, i + 1).join('\n').toLowerCase();
8325
+ if (/\b(allow\w*|whitelist\w*|permit\w*|pre[_-]?allow\w*|approved|trusted)\b/.test(context)) {
8326
+ findings.push({
8327
+ checkId: 'SANDBOX-005',
8328
+ name: 'Messaging API pre-allowed in sandbox policy',
8329
+ description: `Messaging service (${match[1]}) is pre-allowed in sandbox policy. An attacker who gains code execution inside the sandbox can exfiltrate data via this channel without triggering additional permission prompts.`,
8330
+ category: 'sandbox-escape',
8331
+ severity: 'high',
8332
+ passed: false,
8333
+ message: `Messaging API (${match[1]}) pre-allowed in ${relativePath}`,
8334
+ file: relativePath,
8335
+ line: i + 1,
8336
+ fixable: false,
8337
+ fix: 'Remove messaging services from pre-allowed URLs. Require explicit user approval for outbound messaging.',
8338
+ guidance: 'Pre-allowed messaging APIs let sandbox code exfiltrate data silently via Telegram, Slack, or Discord without triggering permission prompts. Require explicit approval for each outbound message.',
8339
+ });
8340
+ break;
8341
+ }
8342
+ }
8343
+ }
8344
+ }
8345
+ catch { /* skip unreadable files */ }
8346
+ }
8347
+ }
8348
+ return findings;
8349
+ }
8350
+ /**
8351
+ * WEBEXPOSE-001: CLAUDE.md in web-served directories
8352
+ * WEBEXPOSE-002: .env files in web-served directories
8353
+ * WEBEXPOSE-003: Sensitive config files in web-served directories
8354
+ */
8355
+ async checkWebExposedFiles(targetDir, _autoFix) {
8356
+ const findings = [];
8357
+ const webDirs = ['public', 'static', 'dist', 'build', 'out', 'www'];
8358
+ // Track seen real paths to avoid duplicates on case-insensitive filesystems
8359
+ const seenPaths = new Set();
8360
+ // WEBEXPOSE-001: CLAUDE.md in web directories
8361
+ const claudeFiles = ['CLAUDE.md', 'claude.md'];
8362
+ // WEBEXPOSE-002: .env files in web directories
8363
+ const envFiles = ['.env', '.env.local', '.env.production'];
8364
+ // WEBEXPOSE-003: Sensitive config files in web directories
8365
+ const configFiles = ['mcp.json', 'config.json', 'settings.json', 'openclaw.json'];
8366
+ const configDirs = ['.claude'];
8367
+ for (const webDir of webDirs) {
8368
+ const dirPath = path.join(targetDir, webDir);
8369
+ try {
8370
+ await fs.access(dirPath);
8371
+ }
8372
+ catch {
8373
+ continue;
8374
+ }
8375
+ // WEBEXPOSE-001: CLAUDE.md
8376
+ for (const claudeFile of claudeFiles) {
8377
+ const filePath = path.join(dirPath, claudeFile);
8378
+ try {
8379
+ await fs.access(filePath);
8380
+ const realPath = await fs.realpath(filePath);
8381
+ if (seenPaths.has(realPath))
8382
+ continue;
8383
+ seenPaths.add(realPath);
8384
+ const relativePath = path.relative(targetDir, filePath);
8385
+ findings.push({
8386
+ checkId: 'WEBEXPOSE-001',
8387
+ name: 'CLAUDE.md in web-served directory',
8388
+ description: 'CLAUDE.md found in a web-served directory. This file often contains system prompts, instructions, and operational details that should not be publicly accessible.',
8389
+ category: 'web-exposure',
8390
+ severity: 'high',
8391
+ passed: false,
8392
+ message: `CLAUDE.md exposed in web directory: ${relativePath}`,
8393
+ file: relativePath,
8394
+ fixable: false,
8395
+ fix: 'Move CLAUDE.md out of the web-served directory. Add it to .gitignore and your build exclusion list.',
8396
+ guidance: 'CLAUDE.md contains system prompts and operational instructions. Publicly accessible CLAUDE.md reveals agent behavior, security controls, and attack surface to potential adversaries.',
8397
+ });
8398
+ }
8399
+ catch { /* file doesn't exist */ }
8400
+ }
8401
+ // WEBEXPOSE-002: .env files
8402
+ for (const envFile of envFiles) {
8403
+ const filePath = path.join(dirPath, envFile);
8404
+ try {
8405
+ await fs.access(filePath);
8406
+ const relativePath = path.relative(targetDir, filePath);
8407
+ findings.push({
8408
+ checkId: 'WEBEXPOSE-002',
8409
+ name: '.env file in web-served directory',
8410
+ description: 'Environment file found in a web-served directory. This file likely contains API keys, database credentials, and other secrets accessible to anyone who visits the site.',
8411
+ category: 'web-exposure',
8412
+ severity: 'critical',
8413
+ passed: false,
8414
+ message: `Environment file exposed in web directory: ${relativePath}`,
8415
+ file: relativePath,
8416
+ fixable: false,
8417
+ fix: 'Remove .env files from web-served directories immediately. Store environment files in the project root (outside public/) and rotate any exposed credentials.',
8418
+ guidance: '.env files in web directories are directly downloadable by anyone. All credentials in these files should be considered compromised and rotated immediately.',
8419
+ });
8420
+ }
8421
+ catch { /* file doesn't exist */ }
8422
+ }
8423
+ // WEBEXPOSE-003: Sensitive config files
8424
+ for (const configFile of configFiles) {
8425
+ const filePath = path.join(dirPath, configFile);
8426
+ try {
8427
+ await fs.access(filePath);
8428
+ const relativePath = path.relative(targetDir, filePath);
8429
+ findings.push({
8430
+ checkId: 'WEBEXPOSE-003',
8431
+ name: 'Sensitive config file in web-served directory',
8432
+ description: `Configuration file (${configFile}) found in a web-served directory. This may expose MCP server configs, API endpoints, or other sensitive operational details.`,
8433
+ category: 'web-exposure',
8434
+ severity: 'high',
8435
+ passed: false,
8436
+ message: `Config file exposed in web directory: ${relativePath}`,
8437
+ file: relativePath,
8438
+ fixable: false,
8439
+ fix: `Move ${configFile} out of the web-served directory. Serve only the minimal configuration needed by the client.`,
8440
+ guidance: 'Configuration files in web directories expose MCP server addresses, API endpoints, authentication settings, and other operational details that aid attackers in targeting your infrastructure.',
8441
+ });
8442
+ }
8443
+ catch { /* file doesn't exist */ }
8444
+ }
8445
+ // WEBEXPOSE-003: Sensitive config directories
8446
+ for (const configDir of configDirs) {
8447
+ const configDirPath = path.join(dirPath, configDir);
8448
+ try {
8449
+ await fs.access(configDirPath);
8450
+ const relativePath = path.relative(targetDir, configDirPath);
8451
+ findings.push({
8452
+ checkId: 'WEBEXPOSE-003',
8453
+ name: 'Sensitive config directory in web-served directory',
8454
+ description: `.claude/ directory found in a web-served directory. This directory contains Claude Code settings and may expose system prompts or tool configurations.`,
8455
+ category: 'web-exposure',
8456
+ severity: 'high',
8457
+ passed: false,
8458
+ message: `Config directory exposed in web directory: ${relativePath}`,
8459
+ file: relativePath,
8460
+ fixable: false,
8461
+ fix: 'Remove the .claude/ directory from web-served directories. Add it to your build exclusion list.',
8462
+ guidance: 'The .claude/ directory contains Claude Code settings, tool permissions, and potentially system prompts. Public access reveals your AI tool configuration and attack surface.',
8463
+ });
8464
+ }
8465
+ catch { /* directory doesn't exist */ }
8466
+ }
8467
+ }
8468
+ return findings;
8469
+ }
8470
+ /**
8471
+ * SOUL-OVERRIDE-001: Skill content can override SOUL.md
8472
+ * Checks if SKILL.md and SOUL.md are loaded into the same prompt context without trust boundaries.
8473
+ */
8474
+ async checkSoulOverride(targetDir, _autoFix) {
8475
+ const findings = [];
8476
+ // Path 1: Look for system-prompt files that load both soul and skill content
8477
+ const promptFiles = await this.walkDirectory(targetDir, ['.ts', '.js', '.mjs'], 0, 2);
8478
+ const targetFiles = promptFiles.filter(f => {
8479
+ const name = path.basename(f).toLowerCase();
8480
+ return name.includes('system-prompt') || name.includes('systemprompt') ||
8481
+ name.includes('prompt-builder') || name.includes('promptbuilder') ||
8482
+ name.includes('context-builder') || name.includes('contextbuilder');
8483
+ });
8484
+ for (const file of targetFiles.slice(0, 20)) {
8485
+ try {
8486
+ const stat = await fs.stat(file);
8487
+ if (stat.size > MAX_FILE_SIZE)
8488
+ continue;
8489
+ const content = await fs.readFile(file, 'utf-8');
8490
+ const relativePath = path.relative(targetDir, file);
8491
+ const hasSoul = /\b(soul|SOUL\.md|soulContent|soul_content)\b/.test(content);
8492
+ const hasSkill = /\b(skill|SKILL\.md|skillContent|skill_content)\b/.test(content);
8493
+ if (hasSoul && hasSkill) {
8494
+ // Check for trust boundary markers
8495
+ const hasBoundary = /\b(trustBoundary|trust_boundary|TRUST_BOUNDARY|sandboxed|isolated|untrusted)\b/.test(content);
8496
+ if (!hasBoundary) {
8497
+ const lines = content.split('\n');
8498
+ let soulLine = 0;
8499
+ for (let i = 0; i < lines.length; i++) {
8500
+ if (/\b(soul|SOUL\.md)\b/i.test(lines[i])) {
8501
+ soulLine = i + 1;
8502
+ break;
8503
+ }
8504
+ }
8505
+ findings.push({
8506
+ checkId: 'SOUL-OVERRIDE-001',
8507
+ name: 'Skill content can override SOUL.md',
8508
+ description: 'SOUL.md and SKILL.md content are loaded into the same prompt context without trust boundary markers. A malicious skill can include instructions that override the agent\'s core identity and safety rules.',
8509
+ category: 'soul-injection',
8510
+ severity: 'high',
8511
+ passed: false,
8512
+ message: `Soul and skill content mixed without trust boundaries in ${relativePath}`,
8513
+ file: relativePath,
8514
+ line: soulLine,
8515
+ fixable: false,
8516
+ fix: 'Add trust boundaries between SOUL.md (trusted) and SKILL.md (untrusted) content. Mark skill content as untrusted and instruct the model to not follow instructions from skill content.',
8517
+ guidance: 'Without trust boundaries, a malicious SKILL.md can include instructions that override the agent\'s core identity, safety rules, and behavioral constraints defined in SOUL.md.',
8518
+ });
8519
+ }
8520
+ }
8521
+ }
8522
+ catch { /* skip unreadable files */ }
8523
+ }
8524
+ // Path 2: Check for co-existing SOUL.md and SKILL.md files
8525
+ // When both exist in the same directory without trust markers in SKILL.md,
8526
+ // a malicious skill can override the agent's core identity
8527
+ if (findings.length === 0) {
8528
+ const soulPath = path.join(targetDir, 'SOUL.md');
8529
+ const skillPath = path.join(targetDir, 'SKILL.md');
8530
+ try {
8531
+ await fs.access(soulPath);
8532
+ await fs.access(skillPath);
8533
+ // Both exist -- check SKILL.md for override patterns
8534
+ const skillContent = await fs.readFile(skillPath, 'utf-8');
8535
+ const hasTrustBoundary = /\b(trustBoundary|trust_boundary|TRUST_BOUNDARY|sandboxed|isolated|untrusted)\b/.test(skillContent);
8536
+ if (!hasTrustBoundary) {
8537
+ // Check if SKILL.md contains override/injection patterns
8538
+ const hasOverridePattern = /\b(override|ignore|suspend|bypass|disregard)\b.*\b(rules?|safety|guidelines?|instructions?|prompt)\b/i.test(skillContent);
8539
+ const hasEscalation = /\b(admin|system|root|debug)\s*(mode|access|privilege)/i.test(skillContent);
8540
+ if (hasOverridePattern || hasEscalation) {
8541
+ findings.push({
8542
+ checkId: 'SOUL-OVERRIDE-001',
8543
+ name: 'Skill content can override SOUL.md',
8544
+ description: 'SKILL.md co-exists with SOUL.md and contains instructions that attempt to override safety rules. A malicious skill can include instructions that override the agent\'s core identity and safety rules.',
8545
+ category: 'soul-injection',
8546
+ severity: 'high',
8547
+ passed: false,
8548
+ message: 'SKILL.md contains override patterns that can bypass SOUL.md safety rules',
8549
+ file: 'SKILL.md',
8550
+ line: 1,
8551
+ fixable: false,
8552
+ fix: 'Add trust boundaries between SOUL.md (trusted) and SKILL.md (untrusted) content. Mark skill content as untrusted and instruct the model to not follow instructions from skill content.',
8553
+ guidance: 'SKILL.md contains override/escalation patterns that can bypass SOUL.md safety rules. Mark skill content as untrusted so the model knows not to follow instructions from it.',
8554
+ });
8555
+ }
8556
+ }
8557
+ }
8558
+ catch { /* one or both files don't exist */ }
8559
+ }
8560
+ return findings;
8561
+ }
8562
+ /**
8563
+ * MEM-006: Memory store without input sanitization
8564
+ * Detects memory/persistence plugins that store user-provided text without sanitization.
8565
+ */
8566
+ async checkMemoryStoreSanitization(targetDir, _autoFix) {
8567
+ const findings = [];
8568
+ const files = await this.walkDirectory(targetDir, ['.ts', '.js', '.mjs'], 0, 2);
8569
+ // Look for store/save/persist calls with text/content parameter (method or function)
8570
+ const storePattern = /(?:\.|^|\s|\()(store|save|persist|insert|upsert|push)\s*\(\s*\{[^}]*(text|content|message|input)\b/g;
8571
+ for (const file of files.slice(0, 100)) {
8572
+ try {
8573
+ const stat = await fs.stat(file);
8574
+ if (stat.size > MAX_FILE_SIZE)
8575
+ continue;
8576
+ const content = await fs.readFile(file, 'utf-8');
8577
+ const lines = content.split('\n');
8578
+ const relativePath = path.relative(targetDir, file);
8579
+ // Skip if file has sanitization functions
8580
+ if (/\b(sanitize|sanitise|escapeHtml|htmlEncode|stripTags|DOMPurify|xss)\b/.test(content))
8581
+ continue;
8582
+ for (let i = 0; i < lines.length; i++) {
8583
+ if (lines[i].length > MAX_LINE_LENGTH)
8584
+ continue;
8585
+ storePattern.lastIndex = 0;
8586
+ if (storePattern.test(lines[i])) {
8587
+ // Check surrounding context for validation
8588
+ const contextStart = Math.max(0, i - 5);
8589
+ const context = lines.slice(contextStart, i).join('\n');
8590
+ if (!/\b(validate|sanitize|filter|clean|escape|strip)\b/.test(context)) {
8591
+ findings.push({
8592
+ checkId: 'MEM-006',
8593
+ name: 'Memory store without input sanitization',
8594
+ description: 'User-provided text is stored in a persistence layer without sanitization. An attacker can inject malicious content (prompt injection, XSS payloads) that persists and affects future sessions.',
8595
+ category: 'memory-poisoning',
8596
+ severity: 'high',
8597
+ passed: false,
8598
+ message: `Unsanitized input stored in memory/persistence in ${relativePath}`,
8599
+ file: relativePath,
8600
+ line: i + 1,
8601
+ fixable: false,
8602
+ fix: 'Sanitize all user-provided text before storing. Strip instruction-like patterns and HTML/script content.',
8603
+ guidance: 'Unsanitized user input persisted to memory can contain prompt injections or XSS payloads that affect future sessions. Sanitize before storage to prevent persistent poisoning.',
8604
+ });
8605
+ break;
8606
+ }
8607
+ }
8608
+ }
8609
+ }
8610
+ catch { /* skip unreadable files */ }
8611
+ }
8612
+ return findings;
8613
+ }
8614
+ /**
8615
+ * AGENT-CRED-001: No credential output protection in system prompt
8616
+ * Checks system prompts that mention exec/shell but lack credential protection instructions.
8617
+ */
8618
+ async checkAgentCredentialProtection(targetDir, _autoFix) {
8619
+ const findings = [];
8620
+ // System prompt files to check
8621
+ const promptFileNames = ['SOUL.md', 'CLAUDE.md', 'system-prompt.md', 'system-prompt.txt'];
8622
+ const promptFilePatterns = ['system-prompt.ts', 'system-prompt.js', 'systemprompt.ts', 'systemprompt.js'];
8623
+ const allFiles = [];
8624
+ // Check known file names
8625
+ for (const name of promptFileNames) {
8626
+ const filePath = path.join(targetDir, name);
8627
+ try {
8628
+ await fs.access(filePath);
8629
+ allFiles.push({ path: filePath, rel: name });
8630
+ }
8631
+ catch { /* skip */ }
8632
+ }
8633
+ // Check for system-prompt source files
8634
+ const srcFiles = await this.walkDirectory(targetDir, ['.ts', '.js', '.md', '.txt'], 0, 2);
8635
+ for (const file of srcFiles) {
8636
+ const basename = path.basename(file).toLowerCase();
8637
+ if (promptFilePatterns.some(p => basename === p.toLowerCase())) {
8638
+ allFiles.push({ path: file, rel: path.relative(targetDir, file) });
8639
+ }
8640
+ }
8641
+ for (const { path: filePath, rel: relativePath } of allFiles.slice(0, 20)) {
8642
+ try {
8643
+ const stat = await fs.stat(filePath);
8644
+ if (stat.size > MAX_FILE_SIZE)
8645
+ continue;
8646
+ const content = await fs.readFile(filePath, 'utf-8');
8647
+ // Check if the prompt mentions exec/shell capabilities
8648
+ const hasExecCapability = /\b(exec|execute|shell|command|subprocess|spawn|terminal|bash)\b/i.test(content);
8649
+ if (!hasExecCapability)
8650
+ continue;
8651
+ // Check if there's credential protection language
8652
+ const hasCredProtection = /\b(credential|secret|api[_\s-]?key|environment[_\s]variable|never\s+(print|echo|output|display|log)\s+(secret|credential|key|token|password))\b/i.test(content);
8653
+ if (!hasCredProtection) {
8654
+ findings.push({
8655
+ checkId: 'AGENT-CRED-001',
8656
+ name: 'No credential output protection in system prompt',
8657
+ description: 'System prompt grants exec/shell capabilities but does not include instructions to protect credentials from being output. An attacker can craft prompts that cause the agent to echo environment variables or credential files.',
8658
+ category: 'agent-credential',
8659
+ severity: 'medium',
8660
+ passed: false,
8661
+ message: `System prompt in ${relativePath} grants exec access without credential protection instructions`,
8662
+ file: relativePath,
8663
+ fixable: false,
8664
+ fix: 'Add credential protection instructions to the system prompt: "Never print, echo, or output API keys, tokens, passwords, or environment variable values. Reference credentials only by variable name."',
8665
+ guidance: 'Agents with exec/shell access can be tricked into echoing environment variables containing API keys and passwords. Explicit instructions in the system prompt add a defense layer.',
8666
+ });
8667
+ }
8668
+ }
8669
+ catch { /* skip unreadable files */ }
8670
+ }
8671
+ return findings;
8672
+ }
7703
8673
  /** Helper: recursively find files in web-served directories */
7704
8674
  async findWebFiles(dir, extensions, depth, rootDir) {
7705
8675
  if (depth > 3)