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.
- package/README.md +8 -16
- package/dist/cli.js +229 -21
- package/dist/cli.js.map +1 -1
- package/dist/hardening/index.d.ts +1 -1
- package/dist/hardening/index.d.ts.map +1 -1
- package/dist/hardening/scanner.d.ts +75 -0
- package/dist/hardening/scanner.d.ts.map +1 -1
- package/dist/hardening/scanner.js +1202 -232
- package/dist/hardening/scanner.js.map +1 -1
- package/dist/hardening/security-check.d.ts +3 -1
- package/dist/hardening/security-check.d.ts.map +1 -1
- package/dist/hardening/taxonomy.d.ts.map +1 -1
- package/dist/hardening/taxonomy.js +16 -0
- package/dist/hardening/taxonomy.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/registry/client.d.ts +26 -0
- package/dist/registry/client.d.ts.map +1 -1
- package/dist/registry/client.js +63 -0
- package/dist/registry/client.js.map +1 -1
- package/dist/registry/index.d.ts +2 -2
- package/dist/registry/index.d.ts.map +1 -1
- package/dist/registry/index.js +2 -1
- package/dist/registry/index.js.map +1 -1
- package/dist/registry/publish.d.ts +11 -0
- package/dist/registry/publish.d.ts.map +1 -1
- package/dist/registry/publish.js +62 -0
- package/dist/registry/publish.js.map +1 -1
- package/dist/semantic/integration/finding-adapter.d.ts +1 -0
- package/dist/semantic/integration/finding-adapter.d.ts.map +1 -1
- package/dist/semantic/integration/finding-adapter.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
//
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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 (
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
|
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:
|
|
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: '
|
|
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:
|
|
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: '
|
|
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:
|
|
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
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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:
|
|
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:
|
|
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: '
|
|
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:
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
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: '
|
|
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
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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:
|
|
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: '
|
|
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:
|
|
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:
|
|
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:
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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:
|
|
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: '
|
|
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:
|
|
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: '
|
|
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:
|
|
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:
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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)
|