agentic-qe 2.5.6 → 2.5.8
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/.claude/agents/n8n/n8n-base-agent.md +376 -0
- package/.claude/agents/n8n/n8n-bdd-scenario-tester.md +613 -0
- package/.claude/agents/n8n/n8n-chaos-tester.md +654 -0
- package/.claude/agents/n8n/n8n-ci-orchestrator.md +850 -0
- package/.claude/agents/n8n/n8n-compliance-validator.md +685 -0
- package/.claude/agents/n8n/n8n-expression-validator.md +560 -0
- package/.claude/agents/n8n/n8n-integration-test.md +602 -0
- package/.claude/agents/n8n/n8n-monitoring-validator.md +589 -0
- package/.claude/agents/n8n/n8n-node-validator.md +455 -0
- package/.claude/agents/n8n/n8n-performance-tester.md +630 -0
- package/.claude/agents/n8n/n8n-security-auditor.md +786 -0
- package/.claude/agents/n8n/n8n-trigger-test.md +500 -0
- package/.claude/agents/n8n/n8n-unit-tester.md +633 -0
- package/.claude/agents/n8n/n8n-version-comparator.md +567 -0
- package/.claude/agents/n8n/n8n-workflow-executor.md +392 -0
- package/.claude/skills/n8n-expression-testing/SKILL.md +434 -0
- package/.claude/skills/n8n-integration-testing-patterns/SKILL.md +540 -0
- package/.claude/skills/n8n-security-testing/SKILL.md +599 -0
- package/.claude/skills/n8n-trigger-testing-strategies/SKILL.md +541 -0
- package/.claude/skills/n8n-workflow-testing-fundamentals/SKILL.md +447 -0
- package/CHANGELOG.md +127 -0
- package/README.md +7 -4
- package/dist/agents/BaseAgent.d.ts +142 -0
- package/dist/agents/BaseAgent.d.ts.map +1 -1
- package/dist/agents/BaseAgent.js +372 -2
- package/dist/agents/BaseAgent.js.map +1 -1
- package/dist/agents/TestGeneratorAgent.d.ts +5 -0
- package/dist/agents/TestGeneratorAgent.d.ts.map +1 -1
- package/dist/agents/TestGeneratorAgent.js +38 -0
- package/dist/agents/TestGeneratorAgent.js.map +1 -1
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/n8n/N8nAPIClient.d.ts +121 -0
- package/dist/agents/n8n/N8nAPIClient.d.ts.map +1 -0
- package/dist/agents/n8n/N8nAPIClient.js +367 -0
- package/dist/agents/n8n/N8nAPIClient.js.map +1 -0
- package/dist/agents/n8n/N8nAuditPersistence.d.ts +120 -0
- package/dist/agents/n8n/N8nAuditPersistence.d.ts.map +1 -0
- package/dist/agents/n8n/N8nAuditPersistence.js +473 -0
- package/dist/agents/n8n/N8nAuditPersistence.js.map +1 -0
- package/dist/agents/n8n/N8nBDDScenarioTesterAgent.d.ts +159 -0
- package/dist/agents/n8n/N8nBDDScenarioTesterAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nBDDScenarioTesterAgent.js +697 -0
- package/dist/agents/n8n/N8nBDDScenarioTesterAgent.js.map +1 -0
- package/dist/agents/n8n/N8nBaseAgent.d.ts +126 -0
- package/dist/agents/n8n/N8nBaseAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nBaseAgent.js +446 -0
- package/dist/agents/n8n/N8nBaseAgent.js.map +1 -0
- package/dist/agents/n8n/N8nCIOrchestratorAgent.d.ts +164 -0
- package/dist/agents/n8n/N8nCIOrchestratorAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nCIOrchestratorAgent.js +610 -0
- package/dist/agents/n8n/N8nCIOrchestratorAgent.js.map +1 -0
- package/dist/agents/n8n/N8nChaosTesterAgent.d.ts +205 -0
- package/dist/agents/n8n/N8nChaosTesterAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nChaosTesterAgent.js +729 -0
- package/dist/agents/n8n/N8nChaosTesterAgent.js.map +1 -0
- package/dist/agents/n8n/N8nComplianceValidatorAgent.d.ts +228 -0
- package/dist/agents/n8n/N8nComplianceValidatorAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nComplianceValidatorAgent.js +986 -0
- package/dist/agents/n8n/N8nComplianceValidatorAgent.js.map +1 -0
- package/dist/agents/n8n/N8nContractTesterAgent.d.ts +213 -0
- package/dist/agents/n8n/N8nContractTesterAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nContractTesterAgent.js +989 -0
- package/dist/agents/n8n/N8nContractTesterAgent.js.map +1 -0
- package/dist/agents/n8n/N8nExpressionValidatorAgent.d.ts +99 -0
- package/dist/agents/n8n/N8nExpressionValidatorAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nExpressionValidatorAgent.js +632 -0
- package/dist/agents/n8n/N8nExpressionValidatorAgent.js.map +1 -0
- package/dist/agents/n8n/N8nFailureModeTesterAgent.d.ts +238 -0
- package/dist/agents/n8n/N8nFailureModeTesterAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nFailureModeTesterAgent.js +956 -0
- package/dist/agents/n8n/N8nFailureModeTesterAgent.js.map +1 -0
- package/dist/agents/n8n/N8nIdempotencyTesterAgent.d.ts +242 -0
- package/dist/agents/n8n/N8nIdempotencyTesterAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nIdempotencyTesterAgent.js +992 -0
- package/dist/agents/n8n/N8nIdempotencyTesterAgent.js.map +1 -0
- package/dist/agents/n8n/N8nIntegrationTestAgent.d.ts +104 -0
- package/dist/agents/n8n/N8nIntegrationTestAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nIntegrationTestAgent.js +653 -0
- package/dist/agents/n8n/N8nIntegrationTestAgent.js.map +1 -0
- package/dist/agents/n8n/N8nMonitoringValidatorAgent.d.ts +210 -0
- package/dist/agents/n8n/N8nMonitoringValidatorAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nMonitoringValidatorAgent.js +669 -0
- package/dist/agents/n8n/N8nMonitoringValidatorAgent.js.map +1 -0
- package/dist/agents/n8n/N8nNodeValidatorAgent.d.ts +142 -0
- package/dist/agents/n8n/N8nNodeValidatorAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nNodeValidatorAgent.js +1090 -0
- package/dist/agents/n8n/N8nNodeValidatorAgent.js.map +1 -0
- package/dist/agents/n8n/N8nPerformanceTesterAgent.d.ts +198 -0
- package/dist/agents/n8n/N8nPerformanceTesterAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nPerformanceTesterAgent.js +653 -0
- package/dist/agents/n8n/N8nPerformanceTesterAgent.js.map +1 -0
- package/dist/agents/n8n/N8nReplayabilityTesterAgent.d.ts +245 -0
- package/dist/agents/n8n/N8nReplayabilityTesterAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nReplayabilityTesterAgent.js +952 -0
- package/dist/agents/n8n/N8nReplayabilityTesterAgent.js.map +1 -0
- package/dist/agents/n8n/N8nSecretsHygieneAuditorAgent.d.ts +325 -0
- package/dist/agents/n8n/N8nSecretsHygieneAuditorAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nSecretsHygieneAuditorAgent.js +1187 -0
- package/dist/agents/n8n/N8nSecretsHygieneAuditorAgent.js.map +1 -0
- package/dist/agents/n8n/N8nSecurityAuditorAgent.d.ts +91 -0
- package/dist/agents/n8n/N8nSecurityAuditorAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nSecurityAuditorAgent.js +825 -0
- package/dist/agents/n8n/N8nSecurityAuditorAgent.js.map +1 -0
- package/dist/agents/n8n/N8nTestHarness.d.ts +131 -0
- package/dist/agents/n8n/N8nTestHarness.d.ts.map +1 -0
- package/dist/agents/n8n/N8nTestHarness.js +456 -0
- package/dist/agents/n8n/N8nTestHarness.js.map +1 -0
- package/dist/agents/n8n/N8nTriggerTestAgent.d.ts +119 -0
- package/dist/agents/n8n/N8nTriggerTestAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nTriggerTestAgent.js +652 -0
- package/dist/agents/n8n/N8nTriggerTestAgent.js.map +1 -0
- package/dist/agents/n8n/N8nUnitTesterAgent.d.ts +130 -0
- package/dist/agents/n8n/N8nUnitTesterAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nUnitTesterAgent.js +522 -0
- package/dist/agents/n8n/N8nUnitTesterAgent.js.map +1 -0
- package/dist/agents/n8n/N8nVersionComparatorAgent.d.ts +201 -0
- package/dist/agents/n8n/N8nVersionComparatorAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nVersionComparatorAgent.js +645 -0
- package/dist/agents/n8n/N8nVersionComparatorAgent.js.map +1 -0
- package/dist/agents/n8n/N8nWorkflowExecutorAgent.d.ts +120 -0
- package/dist/agents/n8n/N8nWorkflowExecutorAgent.d.ts.map +1 -0
- package/dist/agents/n8n/N8nWorkflowExecutorAgent.js +347 -0
- package/dist/agents/n8n/N8nWorkflowExecutorAgent.js.map +1 -0
- package/dist/agents/n8n/index.d.ts +119 -0
- package/dist/agents/n8n/index.d.ts.map +1 -0
- package/dist/agents/n8n/index.js +298 -0
- package/dist/agents/n8n/index.js.map +1 -0
- package/dist/agents/n8n/types.d.ts +486 -0
- package/dist/agents/n8n/types.d.ts.map +1 -0
- package/dist/agents/n8n/types.js +8 -0
- package/dist/agents/n8n/types.js.map +1 -0
- package/dist/cli/init/agents.d.ts.map +1 -1
- package/dist/cli/init/agents.js +29 -0
- package/dist/cli/init/agents.js.map +1 -1
- package/dist/cli/init/skills.d.ts.map +1 -1
- package/dist/cli/init/skills.js +7 -1
- package/dist/cli/init/skills.js.map +1 -1
- package/dist/core/memory/HNSWVectorMemory.js +1 -1
- package/dist/core/memory/RuVectorPatternStore.d.ts +90 -0
- package/dist/core/memory/RuVectorPatternStore.d.ts.map +1 -1
- package/dist/core/memory/RuVectorPatternStore.js +209 -0
- package/dist/core/memory/RuVectorPatternStore.js.map +1 -1
- package/dist/learning/FederatedManager.d.ts +232 -0
- package/dist/learning/FederatedManager.d.ts.map +1 -0
- package/dist/learning/FederatedManager.js +489 -0
- package/dist/learning/FederatedManager.js.map +1 -0
- package/dist/learning/HNSWPatternAdapter.d.ts +117 -0
- package/dist/learning/HNSWPatternAdapter.d.ts.map +1 -0
- package/dist/learning/HNSWPatternAdapter.js +262 -0
- package/dist/learning/HNSWPatternAdapter.js.map +1 -0
- package/dist/learning/LearningEngine.d.ts +27 -0
- package/dist/learning/LearningEngine.d.ts.map +1 -1
- package/dist/learning/LearningEngine.js +75 -1
- package/dist/learning/LearningEngine.js.map +1 -1
- package/dist/learning/PatternCurator.d.ts +217 -0
- package/dist/learning/PatternCurator.d.ts.map +1 -0
- package/dist/learning/PatternCurator.js +393 -0
- package/dist/learning/PatternCurator.js.map +1 -0
- package/dist/learning/index.d.ts +6 -0
- package/dist/learning/index.d.ts.map +1 -1
- package/dist/learning/index.js +16 -1
- package/dist/learning/index.js.map +1 -1
- package/dist/learning/types.d.ts +4 -0
- package/dist/learning/types.d.ts.map +1 -1
- package/dist/mcp/server-instructions.d.ts +1 -1
- package/dist/mcp/server-instructions.js +1 -1
- package/dist/memory/HNSWPatternStore.d.ts +176 -0
- package/dist/memory/HNSWPatternStore.d.ts.map +1 -0
- package/dist/memory/HNSWPatternStore.js +392 -0
- package/dist/memory/HNSWPatternStore.js.map +1 -0
- package/dist/memory/index.d.ts +8 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +13 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/providers/HybridRouter.d.ts +85 -4
- package/dist/providers/HybridRouter.d.ts.map +1 -1
- package/dist/providers/HybridRouter.js +332 -10
- package/dist/providers/HybridRouter.js.map +1 -1
- package/dist/providers/LLMBaselineTracker.d.ts +120 -0
- package/dist/providers/LLMBaselineTracker.d.ts.map +1 -0
- package/dist/providers/LLMBaselineTracker.js +305 -0
- package/dist/providers/LLMBaselineTracker.js.map +1 -0
- package/dist/providers/OpenRouterProvider.d.ts +26 -0
- package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/providers/OpenRouterProvider.js +75 -6
- package/dist/providers/OpenRouterProvider.js.map +1 -1
- package/dist/providers/RuVectorClient.d.ts +259 -0
- package/dist/providers/RuVectorClient.d.ts.map +1 -0
- package/dist/providers/RuVectorClient.js +416 -0
- package/dist/providers/RuVectorClient.js.map +1 -0
- package/dist/providers/RuvllmPatternCurator.d.ts +116 -0
- package/dist/providers/RuvllmPatternCurator.d.ts.map +1 -0
- package/dist/providers/RuvllmPatternCurator.js +323 -0
- package/dist/providers/RuvllmPatternCurator.js.map +1 -0
- package/dist/providers/RuvllmProvider.d.ts +233 -1
- package/dist/providers/RuvllmProvider.d.ts.map +1 -1
- package/dist/providers/RuvllmProvider.js +781 -11
- package/dist/providers/RuvllmProvider.js.map +1 -1
- package/dist/providers/index.d.ts +5 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +12 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/utils/ruvllm-loader.d.ts +98 -1
- package/dist/utils/ruvllm-loader.d.ts.map +1 -1
- package/dist/utils/ruvllm-loader.js.map +1 -1
- package/docs/reference/agents.md +91 -2
- package/docs/reference/skills.md +97 -2
- package/package.json +2 -2
|
@@ -0,0 +1,1187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* N8n Secrets Hygiene Auditor Agent
|
|
4
|
+
*
|
|
5
|
+
* Audits workflows for credential hygiene and secret management:
|
|
6
|
+
* - Credential scoping validation (least privilege)
|
|
7
|
+
* - Masked field detection and verification
|
|
8
|
+
* - Secret leakage into logs/outputs detection
|
|
9
|
+
* - Environment separation validation (dev/staging/prod)
|
|
10
|
+
* - Hardcoded secrets detection
|
|
11
|
+
* - Credential rotation compliance
|
|
12
|
+
* - Access pattern analysis
|
|
13
|
+
*/
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.N8nSecretsHygieneAuditorAgent = void 0;
|
|
16
|
+
const N8nBaseAgent_1 = require("./N8nBaseAgent");
|
|
17
|
+
// Sensitive field patterns by name
|
|
18
|
+
const SENSITIVE_FIELD_PATTERNS = [
|
|
19
|
+
// Authentication
|
|
20
|
+
/password/i,
|
|
21
|
+
/passwd/i,
|
|
22
|
+
/pwd/i,
|
|
23
|
+
/secret/i,
|
|
24
|
+
/token/i,
|
|
25
|
+
/apikey/i,
|
|
26
|
+
/api[_-]?key/i,
|
|
27
|
+
/auth/i,
|
|
28
|
+
/credential/i,
|
|
29
|
+
/bearer/i,
|
|
30
|
+
/jwt/i,
|
|
31
|
+
/oauth/i,
|
|
32
|
+
// Keys and certificates
|
|
33
|
+
/private[_-]?key/i,
|
|
34
|
+
/ssh[_-]?key/i,
|
|
35
|
+
/pem/i,
|
|
36
|
+
/certificate/i,
|
|
37
|
+
/cert/i,
|
|
38
|
+
/ssl/i,
|
|
39
|
+
/tls/i,
|
|
40
|
+
// Database
|
|
41
|
+
/connection[_-]?string/i,
|
|
42
|
+
/db[_-]?pass/i,
|
|
43
|
+
/database[_-]?password/i,
|
|
44
|
+
// Cloud providers
|
|
45
|
+
/aws[_-]?access/i,
|
|
46
|
+
/aws[_-]?secret/i,
|
|
47
|
+
/azure[_-]?key/i,
|
|
48
|
+
/gcp[_-]?key/i,
|
|
49
|
+
// Payment
|
|
50
|
+
/card[_-]?number/i,
|
|
51
|
+
/cvv/i,
|
|
52
|
+
/cvc/i,
|
|
53
|
+
/stripe/i,
|
|
54
|
+
/merchant/i,
|
|
55
|
+
// Personal data
|
|
56
|
+
/ssn/i,
|
|
57
|
+
/social[_-]?security/i,
|
|
58
|
+
];
|
|
59
|
+
// Hardcoded secret patterns
|
|
60
|
+
const HARDCODED_SECRET_PATTERNS = [
|
|
61
|
+
// API Keys
|
|
62
|
+
{ pattern: /sk[_-]live[_-][a-zA-Z0-9]{24,}/g, type: 'Stripe Secret Key', severity: 'critical' },
|
|
63
|
+
{ pattern: /sk[_-]test[_-][a-zA-Z0-9]{24,}/g, type: 'Stripe Test Key', severity: 'high' },
|
|
64
|
+
{ pattern: /pk[_-]live[_-][a-zA-Z0-9]{24,}/g, type: 'Stripe Publishable Key', severity: 'medium' },
|
|
65
|
+
{ pattern: /xoxb-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24}/g, type: 'Slack Bot Token', severity: 'critical' },
|
|
66
|
+
{ pattern: /xoxp-[0-9]{10,}-[0-9]{10,}-[0-9]{10,}-[a-f0-9]{32}/g, type: 'Slack User Token', severity: 'critical' },
|
|
67
|
+
{ pattern: /ghp_[a-zA-Z0-9]{36}/g, type: 'GitHub Personal Token', severity: 'critical' },
|
|
68
|
+
{ pattern: /gho_[a-zA-Z0-9]{36}/g, type: 'GitHub OAuth Token', severity: 'critical' },
|
|
69
|
+
{ pattern: /ghu_[a-zA-Z0-9]{36}/g, type: 'GitHub User Token', severity: 'critical' },
|
|
70
|
+
{ pattern: /AKIA[0-9A-Z]{16}/g, type: 'AWS Access Key ID', severity: 'critical' },
|
|
71
|
+
{ pattern: /[a-zA-Z0-9\/+=]{40}/g, type: 'Potential AWS Secret Key', severity: 'high' },
|
|
72
|
+
{ pattern: /AIza[0-9A-Za-z_-]{35}/g, type: 'Google API Key', severity: 'high' },
|
|
73
|
+
{ pattern: /ya29\.[0-9A-Za-z_-]+/g, type: 'Google OAuth Token', severity: 'high' },
|
|
74
|
+
// JWT and Bearer tokens
|
|
75
|
+
{ pattern: /eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/g, type: 'JWT Token', severity: 'high' },
|
|
76
|
+
{ pattern: /bearer\s+[a-zA-Z0-9_-]{20,}/gi, type: 'Bearer Token', severity: 'high' },
|
|
77
|
+
// Passwords and generic secrets
|
|
78
|
+
{ pattern: /password["\s:=]+["']?[a-zA-Z0-9!@#$%^&*()_+]{8,}["']?/gi, type: 'Hardcoded Password', severity: 'critical' },
|
|
79
|
+
{ pattern: /secret["\s:=]+["']?[a-zA-Z0-9!@#$%^&*()_+]{16,}["']?/gi, type: 'Hardcoded Secret', severity: 'critical' },
|
|
80
|
+
// Connection strings
|
|
81
|
+
{ pattern: /mongodb(\+srv)?:\/\/[^:\s]+:[^@\s]+@/gi, type: 'MongoDB Connection String', severity: 'critical' },
|
|
82
|
+
{ pattern: /postgres(ql)?:\/\/[^:\s]+:[^@\s]+@/gi, type: 'PostgreSQL Connection String', severity: 'critical' },
|
|
83
|
+
{ pattern: /mysql:\/\/[^:\s]+:[^@\s]+@/gi, type: 'MySQL Connection String', severity: 'critical' },
|
|
84
|
+
{ pattern: /redis:\/\/[^:\s]*:[^@\s]+@/gi, type: 'Redis Connection String', severity: 'critical' },
|
|
85
|
+
// Private keys
|
|
86
|
+
{ pattern: /-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g, type: 'Private Key', severity: 'critical' },
|
|
87
|
+
{ pattern: /-----BEGIN PGP PRIVATE KEY BLOCK-----/g, type: 'PGP Private Key', severity: 'critical' },
|
|
88
|
+
];
|
|
89
|
+
// Environment indicators
|
|
90
|
+
const ENVIRONMENT_INDICATORS = {
|
|
91
|
+
development: [
|
|
92
|
+
'localhost', '127.0.0.1', 'dev.', '.dev', '-dev', '_dev',
|
|
93
|
+
'development', 'local', 'test', 'sandbox', 'mock',
|
|
94
|
+
],
|
|
95
|
+
staging: [
|
|
96
|
+
'staging', 'stage', 'stg.', '.stg', '-stg', '_stg',
|
|
97
|
+
'preprod', 'pre-prod', 'uat', 'qa',
|
|
98
|
+
],
|
|
99
|
+
production: [
|
|
100
|
+
'prod.', '.prod', '-prod', '_prod', 'production',
|
|
101
|
+
'live', 'api.', 'www.', '.com', '.io', '.net',
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
// Log/output nodes that might leak secrets
|
|
105
|
+
const LOGGING_NODE_TYPES = [
|
|
106
|
+
'n8n-nodes-base.set',
|
|
107
|
+
'n8n-nodes-base.function',
|
|
108
|
+
'n8n-nodes-base.code',
|
|
109
|
+
'n8n-nodes-base.httpRequest',
|
|
110
|
+
'n8n-nodes-base.slack',
|
|
111
|
+
'n8n-nodes-base.discord',
|
|
112
|
+
'n8n-nodes-base.telegram',
|
|
113
|
+
'n8n-nodes-base.email',
|
|
114
|
+
'n8n-nodes-base.gmail',
|
|
115
|
+
];
|
|
116
|
+
/**
|
|
117
|
+
* N8n Secrets Hygiene Auditor Agent
|
|
118
|
+
*
|
|
119
|
+
* Comprehensive auditing of secret management and credential hygiene
|
|
120
|
+
* in n8n workflows.
|
|
121
|
+
*/
|
|
122
|
+
class N8nSecretsHygieneAuditorAgent extends N8nBaseAgent_1.N8nBaseAgent {
|
|
123
|
+
constructor(config) {
|
|
124
|
+
const capabilities = [
|
|
125
|
+
{
|
|
126
|
+
name: 'secrets-hygiene-audit',
|
|
127
|
+
version: '1.0.0',
|
|
128
|
+
description: 'Audit credential hygiene and secret management',
|
|
129
|
+
parameters: {},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'credential-scoping',
|
|
133
|
+
version: '1.0.0',
|
|
134
|
+
description: 'Validate credential scoping and least privilege',
|
|
135
|
+
parameters: {},
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: 'log-leakage-detection',
|
|
139
|
+
version: '1.0.0',
|
|
140
|
+
description: 'Detect potential secret leakage into logs',
|
|
141
|
+
parameters: {},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'environment-validation',
|
|
145
|
+
version: '1.0.0',
|
|
146
|
+
description: 'Validate environment separation',
|
|
147
|
+
parameters: {},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: 'entropy-analysis',
|
|
151
|
+
version: '1.0.0',
|
|
152
|
+
description: 'Shannon entropy analysis for secret detection',
|
|
153
|
+
parameters: {},
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
super({
|
|
157
|
+
...config,
|
|
158
|
+
type: 'n8n-secrets-hygiene-auditor',
|
|
159
|
+
capabilities: [...capabilities, ...(config.capabilities || [])],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async performTask(task) {
|
|
163
|
+
const hygieneTask = task;
|
|
164
|
+
if (hygieneTask.type !== 'secrets-hygiene-audit') {
|
|
165
|
+
throw new Error(`Unsupported task type: ${hygieneTask.type}`);
|
|
166
|
+
}
|
|
167
|
+
return this.auditSecretsHygiene(hygieneTask.target, hygieneTask.workflow, hygieneTask.options);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Run secrets hygiene audit on a workflow
|
|
171
|
+
*/
|
|
172
|
+
async auditSecretsHygiene(workflowId, providedWorkflow, options) {
|
|
173
|
+
const startTime = Date.now();
|
|
174
|
+
// Get workflow
|
|
175
|
+
let workflow;
|
|
176
|
+
if (providedWorkflow) {
|
|
177
|
+
workflow = providedWorkflow;
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
workflow = await this.getWorkflow(workflowId);
|
|
181
|
+
}
|
|
182
|
+
const opts = options || {};
|
|
183
|
+
const issues = [];
|
|
184
|
+
// Run all analyses
|
|
185
|
+
const credentialScopes = opts.checkScoping !== false
|
|
186
|
+
? await this.analyzeCredentialScopes(workflow, issues)
|
|
187
|
+
: [];
|
|
188
|
+
const maskedFields = opts.checkMasking !== false
|
|
189
|
+
? this.analyzeMaskedFields(workflow, issues)
|
|
190
|
+
: [];
|
|
191
|
+
const logLeakage = opts.checkLogLeakage !== false
|
|
192
|
+
? this.analyzeLogLeakage(workflow, issues)
|
|
193
|
+
: [];
|
|
194
|
+
const environment = opts.checkEnvironment !== false
|
|
195
|
+
? this.analyzeEnvironment(workflow, opts.targetEnvironment, issues)
|
|
196
|
+
: { detectedEnvironment: 'unknown', environmentIndicators: [], crossEnvironmentRisks: [], recommendations: [] };
|
|
197
|
+
const hardcodedSecrets = opts.checkHardcoded !== false
|
|
198
|
+
? this.findHardcodedSecrets(workflow, issues)
|
|
199
|
+
: [];
|
|
200
|
+
const credentialAccess = await this.analyzeCredentialAccess(workflow, opts.rotationPolicyDays || 90, issues);
|
|
201
|
+
// NEW: Run entropy analysis if requested
|
|
202
|
+
let entropyAnalysis;
|
|
203
|
+
if (opts.runEntropyAnalysis !== false) {
|
|
204
|
+
entropyAnalysis = this.runEntropyAnalysis(workflow, opts.entropyThreshold || 4.5, opts.minSecretLength || 16, issues);
|
|
205
|
+
}
|
|
206
|
+
// Calculate overall score and risk
|
|
207
|
+
const overallScore = this.calculateHygieneScore(issues);
|
|
208
|
+
const overallRisk = this.determineOverallRisk(issues);
|
|
209
|
+
// Generate recommendations
|
|
210
|
+
const recommendations = this.generateRecommendations(credentialScopes, maskedFields, logLeakage, environment, hardcodedSecrets, credentialAccess, issues);
|
|
211
|
+
return {
|
|
212
|
+
workflowId: workflow.id?.toString() || workflowId,
|
|
213
|
+
workflowName: workflow.name,
|
|
214
|
+
overallScore,
|
|
215
|
+
overallRisk,
|
|
216
|
+
credentialScopes,
|
|
217
|
+
maskedFields,
|
|
218
|
+
logLeakage,
|
|
219
|
+
environment,
|
|
220
|
+
hardcodedSecrets,
|
|
221
|
+
credentialAccess,
|
|
222
|
+
issues,
|
|
223
|
+
recommendations,
|
|
224
|
+
auditDuration: Date.now() - startTime,
|
|
225
|
+
entropyAnalysis,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Analyze credential scoping
|
|
230
|
+
*/
|
|
231
|
+
async analyzeCredentialScopes(workflow, issues) {
|
|
232
|
+
const results = [];
|
|
233
|
+
const credentialUsage = new Map();
|
|
234
|
+
// Collect credential usage across nodes
|
|
235
|
+
for (const node of workflow.nodes) {
|
|
236
|
+
const credentials = node.credentials;
|
|
237
|
+
if (credentials) {
|
|
238
|
+
for (const [credType, credInfo] of Object.entries(credentials)) {
|
|
239
|
+
const credId = typeof credInfo === 'object' && credInfo !== null
|
|
240
|
+
? credInfo.id || credType
|
|
241
|
+
: credType;
|
|
242
|
+
if (!credentialUsage.has(credId)) {
|
|
243
|
+
credentialUsage.set(credId, []);
|
|
244
|
+
}
|
|
245
|
+
credentialUsage.get(credId).push(node.name);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// Analyze each credential
|
|
250
|
+
for (const [credId, usedByNodes] of credentialUsage) {
|
|
251
|
+
const analysis = this.analyzeCredential(credId, usedByNodes, workflow);
|
|
252
|
+
results.push(analysis);
|
|
253
|
+
// Check for over-scoping issues
|
|
254
|
+
if (analysis.isOverscoped) {
|
|
255
|
+
issues.push({
|
|
256
|
+
id: `scope-${credId}`,
|
|
257
|
+
type: 'excessive-scope',
|
|
258
|
+
severity: 'medium',
|
|
259
|
+
description: `Credential "${analysis.credentialName}" has excessive scope`,
|
|
260
|
+
remediation: 'Reduce credential permissions to minimum required',
|
|
261
|
+
cweId: 'CWE-250', // Execution with Unnecessary Privileges
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
// Check for shared credentials
|
|
265
|
+
if (usedByNodes.length > 3) {
|
|
266
|
+
issues.push({
|
|
267
|
+
id: `shared-${credId}`,
|
|
268
|
+
type: 'shared-credential',
|
|
269
|
+
severity: 'low',
|
|
270
|
+
description: `Credential "${analysis.credentialName}" is shared across ${usedByNodes.length} nodes`,
|
|
271
|
+
remediation: 'Consider using separate credentials for different operations',
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return results;
|
|
276
|
+
}
|
|
277
|
+
analyzeCredential(credId, usedByNodes, workflow) {
|
|
278
|
+
const recommendations = [];
|
|
279
|
+
const requiredPermissions = [];
|
|
280
|
+
const actualPermissions = [];
|
|
281
|
+
// Infer required permissions from node operations
|
|
282
|
+
for (const nodeName of usedByNodes) {
|
|
283
|
+
const node = workflow.nodes.find(n => n.name === nodeName);
|
|
284
|
+
if (node) {
|
|
285
|
+
const operation = node.parameters?.operation || '';
|
|
286
|
+
const resource = node.parameters?.resource || '';
|
|
287
|
+
if (operation) {
|
|
288
|
+
requiredPermissions.push(`${resource}:${operation}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// Determine scope level based on usage pattern
|
|
293
|
+
let scopeLevel = 'workflow';
|
|
294
|
+
if (usedByNodes.length > 5) {
|
|
295
|
+
scopeLevel = 'project';
|
|
296
|
+
}
|
|
297
|
+
// Check if over-scoped (simplified check)
|
|
298
|
+
const isOverscoped = actualPermissions.length > requiredPermissions.length;
|
|
299
|
+
if (requiredPermissions.length === 1) {
|
|
300
|
+
recommendations.push('Consider creating a dedicated credential for this single operation');
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
credentialId: credId,
|
|
304
|
+
credentialName: credId, // Would need API call to get actual name
|
|
305
|
+
credentialType: 'unknown', // Would need API call
|
|
306
|
+
usedByNodes,
|
|
307
|
+
scopeLevel,
|
|
308
|
+
isOverscoped,
|
|
309
|
+
requiredPermissions: [...new Set(requiredPermissions)],
|
|
310
|
+
actualPermissions,
|
|
311
|
+
recommendations,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Analyze masked fields
|
|
316
|
+
*/
|
|
317
|
+
analyzeMaskedFields(workflow, issues) {
|
|
318
|
+
const results = [];
|
|
319
|
+
for (const node of workflow.nodes) {
|
|
320
|
+
const analysis = this.analyzeNodeMasking(node);
|
|
321
|
+
results.push(analysis);
|
|
322
|
+
// Add issues for unmasked fields
|
|
323
|
+
for (const field of analysis.unmaskedFields) {
|
|
324
|
+
issues.push({
|
|
325
|
+
id: `unmask-${node.id}-${field}`,
|
|
326
|
+
type: 'unmasked-field',
|
|
327
|
+
severity: 'high',
|
|
328
|
+
nodeId: node.id,
|
|
329
|
+
nodeName: node.name,
|
|
330
|
+
description: `Sensitive field "${field}" is not masked in ${node.name}`,
|
|
331
|
+
remediation: 'Add field to masked/sensitive fields configuration',
|
|
332
|
+
cweId: 'CWE-312', // Cleartext Storage of Sensitive Information
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return results;
|
|
337
|
+
}
|
|
338
|
+
analyzeNodeMasking(node) {
|
|
339
|
+
const sensitiveFields = [];
|
|
340
|
+
const unmaskedFields = [];
|
|
341
|
+
const recommendations = [];
|
|
342
|
+
// Analyze node parameters for sensitive fields
|
|
343
|
+
this.findSensitiveFields(node.parameters || {}, '', sensitiveFields);
|
|
344
|
+
// Check which are masked
|
|
345
|
+
for (const field of sensitiveFields) {
|
|
346
|
+
if (!field.isMasked) {
|
|
347
|
+
unmaskedFields.push(field.fieldName);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Determine masking status
|
|
351
|
+
let maskingStatus = 'complete';
|
|
352
|
+
if (unmaskedFields.length === sensitiveFields.length && sensitiveFields.length > 0) {
|
|
353
|
+
maskingStatus = 'none';
|
|
354
|
+
}
|
|
355
|
+
else if (unmaskedFields.length > 0) {
|
|
356
|
+
maskingStatus = 'partial';
|
|
357
|
+
}
|
|
358
|
+
if (unmaskedFields.length > 0) {
|
|
359
|
+
recommendations.push(`Mask ${unmaskedFields.length} sensitive field(s) in this node`);
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
nodeId: node.id,
|
|
363
|
+
nodeName: node.name,
|
|
364
|
+
sensitiveFields,
|
|
365
|
+
unmaskedFields,
|
|
366
|
+
maskingStatus,
|
|
367
|
+
recommendations,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
findSensitiveFields(obj, path, results) {
|
|
371
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
372
|
+
const fieldPath = path ? `${path}.${key}` : key;
|
|
373
|
+
// Check if field name matches sensitive patterns
|
|
374
|
+
for (const pattern of SENSITIVE_FIELD_PATTERNS) {
|
|
375
|
+
if (pattern.test(key)) {
|
|
376
|
+
results.push({
|
|
377
|
+
fieldName: key,
|
|
378
|
+
fieldPath,
|
|
379
|
+
isMasked: this.isFieldMasked(value),
|
|
380
|
+
exposureRisk: this.determineExposureRisk(key),
|
|
381
|
+
detectionMethod: 'pattern',
|
|
382
|
+
});
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// Recurse into nested objects
|
|
387
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
388
|
+
this.findSensitiveFields(value, fieldPath, results);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
isFieldMasked(value) {
|
|
393
|
+
if (typeof value !== 'string')
|
|
394
|
+
return true;
|
|
395
|
+
// Check if value looks masked (all asterisks or placeholder)
|
|
396
|
+
return (value === '********' ||
|
|
397
|
+
value === '***' ||
|
|
398
|
+
value.includes('{{') || // Expression reference
|
|
399
|
+
value.startsWith('$') // Variable reference
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
determineExposureRisk(fieldName) {
|
|
403
|
+
const lowercaseName = fieldName.toLowerCase();
|
|
404
|
+
if (lowercaseName.includes('password') ||
|
|
405
|
+
lowercaseName.includes('secret') ||
|
|
406
|
+
lowercaseName.includes('private') ||
|
|
407
|
+
lowercaseName.includes('token')) {
|
|
408
|
+
return 'high';
|
|
409
|
+
}
|
|
410
|
+
if (lowercaseName.includes('key') ||
|
|
411
|
+
lowercaseName.includes('auth')) {
|
|
412
|
+
return 'medium';
|
|
413
|
+
}
|
|
414
|
+
return 'low';
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Analyze log leakage potential
|
|
418
|
+
*/
|
|
419
|
+
analyzeLogLeakage(workflow, issues) {
|
|
420
|
+
const results = [];
|
|
421
|
+
for (const node of workflow.nodes) {
|
|
422
|
+
const analysis = this.analyzeNodeLogLeakage(node, workflow);
|
|
423
|
+
if (analysis.potentialLeaks.length > 0) {
|
|
424
|
+
results.push(analysis);
|
|
425
|
+
// Add issues
|
|
426
|
+
for (const leak of analysis.potentialLeaks) {
|
|
427
|
+
issues.push({
|
|
428
|
+
id: `leak-${node.id}-${leak.fieldPath}`,
|
|
429
|
+
type: 'log-leakage',
|
|
430
|
+
severity: leak.severity,
|
|
431
|
+
nodeId: node.id,
|
|
432
|
+
nodeName: node.name,
|
|
433
|
+
description: leak.description,
|
|
434
|
+
remediation: 'Filter or mask sensitive data before logging/output',
|
|
435
|
+
cweId: 'CWE-532', // Information Exposure Through Log Files
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return results;
|
|
441
|
+
}
|
|
442
|
+
analyzeNodeLogLeakage(node, workflow) {
|
|
443
|
+
const potentialLeaks = [];
|
|
444
|
+
const loggingNodes = [];
|
|
445
|
+
const recommendations = [];
|
|
446
|
+
const isLoggingNode = LOGGING_NODE_TYPES.some(t => node.type.toLowerCase().includes(t.replace('n8n-nodes-base.', '')));
|
|
447
|
+
if (isLoggingNode) {
|
|
448
|
+
loggingNodes.push(node.name);
|
|
449
|
+
// Check if node outputs might contain sensitive data
|
|
450
|
+
const params = node.parameters || {};
|
|
451
|
+
const paramsStr = JSON.stringify(params);
|
|
452
|
+
// Check for console.log or similar
|
|
453
|
+
if (paramsStr.includes('console.log') || paramsStr.includes('console.error')) {
|
|
454
|
+
potentialLeaks.push({
|
|
455
|
+
fieldPath: 'code',
|
|
456
|
+
leakType: 'log',
|
|
457
|
+
severity: 'high',
|
|
458
|
+
description: `Console logging in ${node.name} may expose sensitive data`,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
// Check for full object output
|
|
462
|
+
if (paramsStr.includes('$json') && !paramsStr.includes('$json.')) {
|
|
463
|
+
potentialLeaks.push({
|
|
464
|
+
fieldPath: '$json',
|
|
465
|
+
leakType: 'output',
|
|
466
|
+
severity: 'medium',
|
|
467
|
+
description: `Full JSON output in ${node.name} may contain secrets`,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
// Check for sending to external services
|
|
471
|
+
if (node.type.includes('slack') || node.type.includes('discord') || node.type.includes('telegram')) {
|
|
472
|
+
// Check if message contains potential secrets
|
|
473
|
+
const message = params.text || params.message || params.content || '';
|
|
474
|
+
if (typeof message === 'string' && message.includes('$json')) {
|
|
475
|
+
potentialLeaks.push({
|
|
476
|
+
fieldPath: 'message',
|
|
477
|
+
leakType: 'output',
|
|
478
|
+
severity: 'high',
|
|
479
|
+
description: `Message in ${node.name} may leak sensitive data to external service`,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (potentialLeaks.length > 0) {
|
|
485
|
+
recommendations.push('Review and filter sensitive data before output');
|
|
486
|
+
recommendations.push('Use allowlist approach to explicitly select safe fields');
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
nodeId: node.id,
|
|
490
|
+
nodeName: node.name,
|
|
491
|
+
hasLogging: isLoggingNode,
|
|
492
|
+
loggingNodes,
|
|
493
|
+
potentialLeaks,
|
|
494
|
+
recommendations,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Analyze environment configuration
|
|
499
|
+
*/
|
|
500
|
+
analyzeEnvironment(workflow, targetEnvironment, issues) {
|
|
501
|
+
const indicators = [];
|
|
502
|
+
const crossEnvironmentRisks = [];
|
|
503
|
+
const recommendations = [];
|
|
504
|
+
// Collect environment indicators from workflow
|
|
505
|
+
const workflowStr = JSON.stringify(workflow);
|
|
506
|
+
for (const [env, patterns] of Object.entries(ENVIRONMENT_INDICATORS)) {
|
|
507
|
+
for (const pattern of patterns) {
|
|
508
|
+
if (workflowStr.toLowerCase().includes(pattern.toLowerCase())) {
|
|
509
|
+
indicators.push({
|
|
510
|
+
source: 'workflow-config',
|
|
511
|
+
indicator: pattern,
|
|
512
|
+
suggestedEnvironment: env,
|
|
513
|
+
confidence: pattern.includes('.') ? 0.8 : 0.6,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// Determine most likely environment
|
|
519
|
+
const envCounts = {
|
|
520
|
+
development: 0,
|
|
521
|
+
staging: 0,
|
|
522
|
+
production: 0,
|
|
523
|
+
};
|
|
524
|
+
for (const indicator of indicators) {
|
|
525
|
+
const env = indicator.suggestedEnvironment;
|
|
526
|
+
if (env in envCounts) {
|
|
527
|
+
envCounts[env] += indicator.confidence;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
let detectedEnvironment = 'unknown';
|
|
531
|
+
const maxCount = Math.max(...Object.values(envCounts));
|
|
532
|
+
if (maxCount > 0) {
|
|
533
|
+
const foundEnv = Object.entries(envCounts).find(([, count]) => count === maxCount)?.[0];
|
|
534
|
+
if (foundEnv === 'development' || foundEnv === 'staging' || foundEnv === 'production') {
|
|
535
|
+
detectedEnvironment = foundEnv;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// Check for cross-environment risks
|
|
539
|
+
if (detectedEnvironment === 'production') {
|
|
540
|
+
// Check for dev indicators in production
|
|
541
|
+
const devIndicators = indicators.filter(i => i.suggestedEnvironment === 'development');
|
|
542
|
+
if (devIndicators.length > 0) {
|
|
543
|
+
crossEnvironmentRisks.push({
|
|
544
|
+
type: 'dev-in-prod',
|
|
545
|
+
description: 'Development indicators found in production workflow',
|
|
546
|
+
severity: 'high',
|
|
547
|
+
affectedResources: devIndicators.map(i => i.indicator),
|
|
548
|
+
});
|
|
549
|
+
issues.push({
|
|
550
|
+
id: 'env-dev-in-prod',
|
|
551
|
+
type: 'env-mismatch',
|
|
552
|
+
severity: 'high',
|
|
553
|
+
description: 'Development configuration detected in production workflow',
|
|
554
|
+
remediation: 'Remove development URLs and use production credentials',
|
|
555
|
+
cweId: 'CWE-489', // Active Debug Code
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
// Check for target environment mismatch
|
|
560
|
+
if (targetEnvironment && detectedEnvironment !== 'unknown' && detectedEnvironment !== targetEnvironment) {
|
|
561
|
+
crossEnvironmentRisks.push({
|
|
562
|
+
type: 'environment-mismatch',
|
|
563
|
+
description: `Expected ${targetEnvironment} but detected ${detectedEnvironment}`,
|
|
564
|
+
severity: 'medium',
|
|
565
|
+
affectedResources: [],
|
|
566
|
+
});
|
|
567
|
+
issues.push({
|
|
568
|
+
id: 'env-mismatch',
|
|
569
|
+
type: 'env-mismatch',
|
|
570
|
+
severity: 'medium',
|
|
571
|
+
description: `Environment mismatch: expected ${targetEnvironment}, detected ${detectedEnvironment}`,
|
|
572
|
+
remediation: 'Verify workflow is deployed to correct environment',
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
if (detectedEnvironment === 'unknown') {
|
|
576
|
+
recommendations.push('Add environment indicators to workflow for better traceability');
|
|
577
|
+
}
|
|
578
|
+
if (crossEnvironmentRisks.length > 0) {
|
|
579
|
+
recommendations.push('Review and fix environment-specific configurations');
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
detectedEnvironment,
|
|
583
|
+
environmentIndicators: indicators,
|
|
584
|
+
crossEnvironmentRisks,
|
|
585
|
+
recommendations,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Find hardcoded secrets
|
|
590
|
+
*/
|
|
591
|
+
findHardcodedSecrets(workflow, issues) {
|
|
592
|
+
const findings = [];
|
|
593
|
+
for (const node of workflow.nodes) {
|
|
594
|
+
const nodeFindings = this.scanNodeForSecrets(node);
|
|
595
|
+
findings.push(...nodeFindings);
|
|
596
|
+
// Add issues
|
|
597
|
+
for (const finding of nodeFindings) {
|
|
598
|
+
issues.push({
|
|
599
|
+
id: `hardcoded-${finding.nodeId}-${finding.secretType}`,
|
|
600
|
+
type: 'hardcoded-secret',
|
|
601
|
+
severity: finding.severity,
|
|
602
|
+
nodeId: finding.nodeId,
|
|
603
|
+
nodeName: finding.nodeName,
|
|
604
|
+
description: `Hardcoded ${finding.secretType} found in ${finding.nodeName}`,
|
|
605
|
+
remediation: 'Move secret to n8n credentials store or environment variable',
|
|
606
|
+
cweId: 'CWE-798', // Use of Hard-coded Credentials
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return findings;
|
|
611
|
+
}
|
|
612
|
+
scanNodeForSecrets(node) {
|
|
613
|
+
const findings = [];
|
|
614
|
+
const nodeStr = JSON.stringify(node.parameters || {});
|
|
615
|
+
for (const { pattern, type, severity } of HARDCODED_SECRET_PATTERNS) {
|
|
616
|
+
const matches = nodeStr.match(pattern);
|
|
617
|
+
if (matches) {
|
|
618
|
+
for (const match of matches) {
|
|
619
|
+
// Don't flag obvious placeholders or expressions
|
|
620
|
+
if (match.includes('{{') ||
|
|
621
|
+
match.includes('$') ||
|
|
622
|
+
match === '********' ||
|
|
623
|
+
match.includes('example') ||
|
|
624
|
+
match.includes('placeholder')) {
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
findings.push({
|
|
628
|
+
nodeId: node.id,
|
|
629
|
+
nodeName: node.name,
|
|
630
|
+
fieldPath: 'parameters',
|
|
631
|
+
secretType: type,
|
|
632
|
+
severity,
|
|
633
|
+
partialValue: this.maskSecret(match),
|
|
634
|
+
recommendation: `Move ${type} to credential store`,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
// Check for secrets in URLs
|
|
640
|
+
const urlPattern = /(https?:\/\/[^\s"']+)/gi;
|
|
641
|
+
const urls = nodeStr.match(urlPattern) || [];
|
|
642
|
+
for (const url of urls) {
|
|
643
|
+
// Check for credentials in URL
|
|
644
|
+
if (url.includes('@') && url.includes(':')) {
|
|
645
|
+
findings.push({
|
|
646
|
+
nodeId: node.id,
|
|
647
|
+
nodeName: node.name,
|
|
648
|
+
fieldPath: 'url',
|
|
649
|
+
secretType: 'Credentials in URL',
|
|
650
|
+
severity: 'critical',
|
|
651
|
+
partialValue: this.maskUrl(url),
|
|
652
|
+
recommendation: 'Use authentication parameters instead of URL credentials',
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
// Check for API key in query string
|
|
656
|
+
if (url.includes('api_key=') || url.includes('apikey=') || url.includes('key=')) {
|
|
657
|
+
findings.push({
|
|
658
|
+
nodeId: node.id,
|
|
659
|
+
nodeName: node.name,
|
|
660
|
+
fieldPath: 'url',
|
|
661
|
+
secretType: 'API Key in URL',
|
|
662
|
+
severity: 'high',
|
|
663
|
+
partialValue: this.maskUrl(url),
|
|
664
|
+
recommendation: 'Move API key to headers or credential store',
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return findings;
|
|
669
|
+
}
|
|
670
|
+
maskSecret(secret) {
|
|
671
|
+
if (secret.length <= 8) {
|
|
672
|
+
return '***';
|
|
673
|
+
}
|
|
674
|
+
return `${secret.substring(0, 4)}...${secret.substring(secret.length - 4)}`;
|
|
675
|
+
}
|
|
676
|
+
maskUrl(url) {
|
|
677
|
+
try {
|
|
678
|
+
const parsed = new URL(url);
|
|
679
|
+
if (parsed.password) {
|
|
680
|
+
parsed.password = '***';
|
|
681
|
+
}
|
|
682
|
+
if (parsed.username) {
|
|
683
|
+
parsed.username = '***';
|
|
684
|
+
}
|
|
685
|
+
// Mask query params
|
|
686
|
+
for (const [key] of parsed.searchParams) {
|
|
687
|
+
if (key.toLowerCase().includes('key') || key.toLowerCase().includes('token')) {
|
|
688
|
+
parsed.searchParams.set(key, '***');
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
return parsed.toString();
|
|
692
|
+
}
|
|
693
|
+
catch {
|
|
694
|
+
return '***masked-url***';
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Analyze credential access patterns
|
|
699
|
+
*/
|
|
700
|
+
async analyzeCredentialAccess(workflow, rotationPolicyDays, issues) {
|
|
701
|
+
const results = [];
|
|
702
|
+
const credentialUsage = new Map();
|
|
703
|
+
// Collect credential usage
|
|
704
|
+
for (const node of workflow.nodes) {
|
|
705
|
+
const credentials = node.credentials;
|
|
706
|
+
if (credentials) {
|
|
707
|
+
for (const [credType, credInfo] of Object.entries(credentials)) {
|
|
708
|
+
const credId = typeof credInfo === 'object' && credInfo !== null
|
|
709
|
+
? credInfo.id || credType
|
|
710
|
+
: credType;
|
|
711
|
+
if (!credentialUsage.has(credId)) {
|
|
712
|
+
credentialUsage.set(credId, []);
|
|
713
|
+
}
|
|
714
|
+
credentialUsage.get(credId).push(workflow.name);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
for (const [credId, accessingWorkflows] of credentialUsage) {
|
|
719
|
+
const accessFrequency = accessingWorkflows.length > 5 ? 'high' :
|
|
720
|
+
accessingWorkflows.length > 2 ? 'medium' : 'low';
|
|
721
|
+
// Rotation status would need actual credential metadata from n8n API
|
|
722
|
+
// Currently always 'unknown' until n8n exposes credential metadata
|
|
723
|
+
const rotationStatus = 'unknown';
|
|
724
|
+
// TODO: Add actual rotation check when n8n API supports credential metadata
|
|
725
|
+
// const actualStatus = await this.checkRotationStatus(credId);
|
|
726
|
+
// if (actualStatus === 'overdue') { ... }
|
|
727
|
+
const recommendations = [];
|
|
728
|
+
// Note: rotationStatus check is placeholder for future API support
|
|
729
|
+
// When n8n exposes credential metadata, uncomment and implement rotation checking
|
|
730
|
+
results.push({
|
|
731
|
+
credentialId: credId,
|
|
732
|
+
credentialName: credId,
|
|
733
|
+
accessFrequency,
|
|
734
|
+
rotationStatus,
|
|
735
|
+
accessingWorkflows,
|
|
736
|
+
recommendations,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
return results;
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Calculate hygiene score
|
|
743
|
+
*/
|
|
744
|
+
calculateHygieneScore(issues) {
|
|
745
|
+
let score = 100;
|
|
746
|
+
for (const issue of issues) {
|
|
747
|
+
switch (issue.severity) {
|
|
748
|
+
case 'critical':
|
|
749
|
+
score -= 25;
|
|
750
|
+
break;
|
|
751
|
+
case 'high':
|
|
752
|
+
score -= 15;
|
|
753
|
+
break;
|
|
754
|
+
case 'medium':
|
|
755
|
+
score -= 8;
|
|
756
|
+
break;
|
|
757
|
+
case 'low':
|
|
758
|
+
score -= 3;
|
|
759
|
+
break;
|
|
760
|
+
case 'info':
|
|
761
|
+
score -= 1;
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return Math.max(0, Math.min(100, score));
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Determine overall risk level
|
|
769
|
+
*/
|
|
770
|
+
determineOverallRisk(issues) {
|
|
771
|
+
if (issues.some(i => i.severity === 'critical')) {
|
|
772
|
+
return 'critical';
|
|
773
|
+
}
|
|
774
|
+
if (issues.some(i => i.severity === 'high')) {
|
|
775
|
+
return 'high';
|
|
776
|
+
}
|
|
777
|
+
if (issues.some(i => i.severity === 'medium')) {
|
|
778
|
+
return 'medium';
|
|
779
|
+
}
|
|
780
|
+
if (issues.some(i => i.severity === 'low')) {
|
|
781
|
+
return 'low';
|
|
782
|
+
}
|
|
783
|
+
return 'info';
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Generate recommendations
|
|
787
|
+
*/
|
|
788
|
+
generateRecommendations(credentialScopes, maskedFields, logLeakage, environment, hardcodedSecrets, credentialAccess, issues) {
|
|
789
|
+
const recommendations = [];
|
|
790
|
+
// Critical: Hardcoded secrets
|
|
791
|
+
if (hardcodedSecrets.length > 0) {
|
|
792
|
+
recommendations.push(`CRITICAL: Remove ${hardcodedSecrets.length} hardcoded secret(s) and use credential store`);
|
|
793
|
+
}
|
|
794
|
+
// High: Log leakage
|
|
795
|
+
if (logLeakage.some(l => l.potentialLeaks.length > 0)) {
|
|
796
|
+
recommendations.push('HIGH: Review logging/output nodes for potential secret leakage');
|
|
797
|
+
}
|
|
798
|
+
// High: Unmasked fields
|
|
799
|
+
const unmaskedCount = maskedFields.reduce((sum, m) => sum + m.unmaskedFields.length, 0);
|
|
800
|
+
if (unmaskedCount > 0) {
|
|
801
|
+
recommendations.push(`HIGH: Mask ${unmaskedCount} sensitive field(s) to prevent exposure`);
|
|
802
|
+
}
|
|
803
|
+
// Medium: Environment issues
|
|
804
|
+
if (environment.crossEnvironmentRisks.length > 0) {
|
|
805
|
+
recommendations.push('MEDIUM: Resolve environment configuration mismatches');
|
|
806
|
+
}
|
|
807
|
+
// Medium: Over-scoped credentials
|
|
808
|
+
const overscopedCount = credentialScopes.filter(c => c.isOverscoped).length;
|
|
809
|
+
if (overscopedCount > 0) {
|
|
810
|
+
recommendations.push(`MEDIUM: Reduce scope of ${overscopedCount} over-privileged credential(s)`);
|
|
811
|
+
}
|
|
812
|
+
// Low: Rotation
|
|
813
|
+
const rotationDue = credentialAccess.filter(c => c.rotationStatus === 'due' || c.rotationStatus === 'overdue').length;
|
|
814
|
+
if (rotationDue > 0) {
|
|
815
|
+
recommendations.push(`LOW: ${rotationDue} credential(s) need rotation`);
|
|
816
|
+
}
|
|
817
|
+
// General best practices
|
|
818
|
+
if (issues.length === 0) {
|
|
819
|
+
recommendations.push('Excellent! No secret hygiene issues detected');
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
recommendations.push(`Address ${issues.filter(i => i.severity === 'critical' || i.severity === 'high').length} high-priority issue(s) first`);
|
|
823
|
+
}
|
|
824
|
+
return recommendations;
|
|
825
|
+
}
|
|
826
|
+
// ============================================================================
|
|
827
|
+
// Shannon Entropy Analysis
|
|
828
|
+
// ============================================================================
|
|
829
|
+
/**
|
|
830
|
+
* Run entropy-based secret detection
|
|
831
|
+
* High-entropy strings are statistically likely to be secrets
|
|
832
|
+
*/
|
|
833
|
+
runEntropyAnalysis(workflow, entropyThreshold, minLength, issues) {
|
|
834
|
+
const findings = [];
|
|
835
|
+
const allEntropies = [];
|
|
836
|
+
let totalStringsScanned = 0;
|
|
837
|
+
// Scan each node for high-entropy strings
|
|
838
|
+
for (const node of workflow.nodes) {
|
|
839
|
+
const nodeFindings = this.scanNodeForHighEntropyStrings(node, entropyThreshold, minLength);
|
|
840
|
+
for (const finding of nodeFindings) {
|
|
841
|
+
findings.push(finding);
|
|
842
|
+
allEntropies.push(finding.entropy);
|
|
843
|
+
totalStringsScanned++;
|
|
844
|
+
// Add as issue if confidence is high enough
|
|
845
|
+
if (finding.confidence >= 0.7) {
|
|
846
|
+
issues.push({
|
|
847
|
+
id: `entropy-${node.id}-${finding.fieldPath}`,
|
|
848
|
+
type: 'hardcoded-secret',
|
|
849
|
+
severity: this.entropyToSeverity(finding.entropy, finding.confidence),
|
|
850
|
+
nodeId: node.id,
|
|
851
|
+
nodeName: node.name,
|
|
852
|
+
description: `High-entropy string detected (${finding.entropy.toFixed(2)} bits): likely ${finding.likelySecretType}`,
|
|
853
|
+
remediation: finding.recommendation,
|
|
854
|
+
cweId: 'CWE-798',
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
// Calculate entropy distribution
|
|
860
|
+
const entropyDistribution = this.calculateEntropyDistribution(allEntropies);
|
|
861
|
+
// Generate recommendations
|
|
862
|
+
const recommendations = [];
|
|
863
|
+
if (findings.length > 0) {
|
|
864
|
+
const veryHighCount = findings.filter(f => f.entropyRating === 'very-high').length;
|
|
865
|
+
const highCount = findings.filter(f => f.entropyRating === 'high').length;
|
|
866
|
+
if (veryHighCount > 0) {
|
|
867
|
+
recommendations.push(`CRITICAL: ${veryHighCount} string(s) with very high entropy (>5.5 bits) detected - likely secrets`);
|
|
868
|
+
}
|
|
869
|
+
if (highCount > 0) {
|
|
870
|
+
recommendations.push(`HIGH: ${highCount} string(s) with high entropy (${entropyThreshold}-5.5 bits) detected`);
|
|
871
|
+
}
|
|
872
|
+
recommendations.push('Review flagged strings and move actual secrets to credential store');
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
recommendations.push('No high-entropy strings detected');
|
|
876
|
+
}
|
|
877
|
+
// Emit event
|
|
878
|
+
this.emitEvent('secrets.entropy-analysis.completed', {
|
|
879
|
+
workflowId: workflow.id,
|
|
880
|
+
totalScanned: totalStringsScanned,
|
|
881
|
+
highEntropyCount: findings.length,
|
|
882
|
+
});
|
|
883
|
+
return {
|
|
884
|
+
totalStringsScanned,
|
|
885
|
+
highEntropyFindings: findings,
|
|
886
|
+
entropyDistribution,
|
|
887
|
+
recommendations,
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Scan a single node for high-entropy strings
|
|
892
|
+
*/
|
|
893
|
+
scanNodeForHighEntropyStrings(node, threshold, minLength) {
|
|
894
|
+
const findings = [];
|
|
895
|
+
this.extractAndAnalyzeStrings(node.parameters || {}, '', node, threshold, minLength, findings);
|
|
896
|
+
return findings;
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Recursively extract strings and analyze their entropy
|
|
900
|
+
*/
|
|
901
|
+
extractAndAnalyzeStrings(obj, path, node, threshold, minLength, findings) {
|
|
902
|
+
if (typeof obj === 'string') {
|
|
903
|
+
// Skip if too short or is a template expression
|
|
904
|
+
if (obj.length < minLength || obj.includes('{{') || obj.startsWith('$')) {
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
// Skip known non-secrets (URLs without credentials, common strings)
|
|
908
|
+
if (this.isKnownNonSecret(obj)) {
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const entropy = this.calculateShannonEntropy(obj);
|
|
912
|
+
if (entropy >= threshold) {
|
|
913
|
+
const charsetAnalysis = this.analyzeCharset(obj);
|
|
914
|
+
const likelyType = this.inferSecretType(obj, charsetAnalysis, entropy);
|
|
915
|
+
const confidence = this.calculateConfidence(obj, charsetAnalysis, entropy, path);
|
|
916
|
+
findings.push({
|
|
917
|
+
nodeId: node.id,
|
|
918
|
+
nodeName: node.name,
|
|
919
|
+
fieldPath: path,
|
|
920
|
+
entropy,
|
|
921
|
+
entropyRating: this.getEntropyRating(entropy),
|
|
922
|
+
length: obj.length,
|
|
923
|
+
charsetAnalysis,
|
|
924
|
+
likelySecretType: likelyType,
|
|
925
|
+
confidence,
|
|
926
|
+
partialValue: this.maskSecret(obj),
|
|
927
|
+
recommendation: `Move ${likelyType} to n8n credential store`,
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
else if (Array.isArray(obj)) {
|
|
932
|
+
obj.forEach((item, index) => {
|
|
933
|
+
this.extractAndAnalyzeStrings(item, `${path}[${index}]`, node, threshold, minLength, findings);
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
else if (obj && typeof obj === 'object') {
|
|
937
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
938
|
+
this.extractAndAnalyzeStrings(value, path ? `${path}.${key}` : key, node, threshold, minLength, findings);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Calculate Shannon entropy of a string
|
|
944
|
+
* Higher entropy = more randomness = more likely to be a secret
|
|
945
|
+
*/
|
|
946
|
+
calculateShannonEntropy(str) {
|
|
947
|
+
if (str.length === 0)
|
|
948
|
+
return 0;
|
|
949
|
+
// Count character frequencies
|
|
950
|
+
const frequencies = new Map();
|
|
951
|
+
for (const char of str) {
|
|
952
|
+
frequencies.set(char, (frequencies.get(char) || 0) + 1);
|
|
953
|
+
}
|
|
954
|
+
// Calculate entropy: -Σ p(x) * log2(p(x))
|
|
955
|
+
let entropy = 0;
|
|
956
|
+
const len = str.length;
|
|
957
|
+
for (const count of frequencies.values()) {
|
|
958
|
+
const probability = count / len;
|
|
959
|
+
entropy -= probability * Math.log2(probability);
|
|
960
|
+
}
|
|
961
|
+
return entropy;
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Analyze character set composition
|
|
965
|
+
*/
|
|
966
|
+
analyzeCharset(str) {
|
|
967
|
+
const hasUppercase = /[A-Z]/.test(str);
|
|
968
|
+
const hasLowercase = /[a-z]/.test(str);
|
|
969
|
+
const hasDigits = /[0-9]/.test(str);
|
|
970
|
+
const hasSpecial = /[^A-Za-z0-9]/.test(str);
|
|
971
|
+
// Calculate effective charset size
|
|
972
|
+
let charsetSize = 0;
|
|
973
|
+
if (hasUppercase)
|
|
974
|
+
charsetSize += 26;
|
|
975
|
+
if (hasLowercase)
|
|
976
|
+
charsetSize += 26;
|
|
977
|
+
if (hasDigits)
|
|
978
|
+
charsetSize += 10;
|
|
979
|
+
if (hasSpecial)
|
|
980
|
+
charsetSize += 32; // Common special chars
|
|
981
|
+
// Check if base64-like (only alphanumeric + /+=)
|
|
982
|
+
const isBase64Like = /^[A-Za-z0-9+/=]+$/.test(str) && str.length % 4 === 0;
|
|
983
|
+
// Check if hex-like
|
|
984
|
+
const isHexLike = /^[A-Fa-f0-9]+$/.test(str);
|
|
985
|
+
return {
|
|
986
|
+
hasUppercase,
|
|
987
|
+
hasLowercase,
|
|
988
|
+
hasDigits,
|
|
989
|
+
hasSpecial,
|
|
990
|
+
charsetSize,
|
|
991
|
+
isBase64Like,
|
|
992
|
+
isHexLike,
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Infer likely secret type from characteristics
|
|
997
|
+
*/
|
|
998
|
+
inferSecretType(str, charset, entropy) {
|
|
999
|
+
// Check for known prefixes
|
|
1000
|
+
if (str.startsWith('sk_live_') || str.startsWith('sk_test_')) {
|
|
1001
|
+
return 'Stripe Secret Key';
|
|
1002
|
+
}
|
|
1003
|
+
if (str.startsWith('pk_live_') || str.startsWith('pk_test_')) {
|
|
1004
|
+
return 'Stripe Publishable Key';
|
|
1005
|
+
}
|
|
1006
|
+
if (str.startsWith('ghp_') || str.startsWith('gho_')) {
|
|
1007
|
+
return 'GitHub Token';
|
|
1008
|
+
}
|
|
1009
|
+
if (str.startsWith('AKIA')) {
|
|
1010
|
+
return 'AWS Access Key';
|
|
1011
|
+
}
|
|
1012
|
+
if (str.startsWith('xoxb-') || str.startsWith('xoxp-')) {
|
|
1013
|
+
return 'Slack Token';
|
|
1014
|
+
}
|
|
1015
|
+
if (str.startsWith('eyJ')) {
|
|
1016
|
+
return 'JWT Token';
|
|
1017
|
+
}
|
|
1018
|
+
// Infer from characteristics
|
|
1019
|
+
if (charset.isBase64Like && str.length >= 32) {
|
|
1020
|
+
return 'Base64 Encoded Secret';
|
|
1021
|
+
}
|
|
1022
|
+
if (charset.isHexLike && str.length >= 32) {
|
|
1023
|
+
return 'Hex Encoded Secret/Hash';
|
|
1024
|
+
}
|
|
1025
|
+
if (charset.isHexLike && str.length === 64) {
|
|
1026
|
+
return 'SHA-256 Hash or Private Key';
|
|
1027
|
+
}
|
|
1028
|
+
if (str.length >= 40 && entropy > 5.0) {
|
|
1029
|
+
return 'API Key or Access Token';
|
|
1030
|
+
}
|
|
1031
|
+
if (str.length >= 20 && charset.hasUppercase && charset.hasLowercase && charset.hasDigits) {
|
|
1032
|
+
return 'Random Access Token';
|
|
1033
|
+
}
|
|
1034
|
+
return 'Unknown High-Entropy Secret';
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
1037
|
+
* Calculate confidence that string is actually a secret
|
|
1038
|
+
*/
|
|
1039
|
+
calculateConfidence(str, charset, entropy, path) {
|
|
1040
|
+
let confidence = 0.5; // Base confidence
|
|
1041
|
+
// Higher entropy = higher confidence
|
|
1042
|
+
if (entropy > 5.5)
|
|
1043
|
+
confidence += 0.2;
|
|
1044
|
+
else if (entropy > 5.0)
|
|
1045
|
+
confidence += 0.15;
|
|
1046
|
+
else if (entropy > 4.5)
|
|
1047
|
+
confidence += 0.1;
|
|
1048
|
+
// Longer strings more likely to be secrets
|
|
1049
|
+
if (str.length >= 64)
|
|
1050
|
+
confidence += 0.1;
|
|
1051
|
+
else if (str.length >= 32)
|
|
1052
|
+
confidence += 0.05;
|
|
1053
|
+
// Base64 or hex encoding suggests secret
|
|
1054
|
+
if (charset.isBase64Like)
|
|
1055
|
+
confidence += 0.1;
|
|
1056
|
+
if (charset.isHexLike)
|
|
1057
|
+
confidence += 0.1;
|
|
1058
|
+
// Mixed case + digits suggests generated token
|
|
1059
|
+
if (charset.hasUppercase && charset.hasLowercase && charset.hasDigits) {
|
|
1060
|
+
confidence += 0.1;
|
|
1061
|
+
}
|
|
1062
|
+
// Field name suggests secret
|
|
1063
|
+
const pathLower = path.toLowerCase();
|
|
1064
|
+
const secretFieldPatterns = ['key', 'token', 'secret', 'password', 'auth', 'credential'];
|
|
1065
|
+
if (secretFieldPatterns.some(p => pathLower.includes(p))) {
|
|
1066
|
+
confidence += 0.15;
|
|
1067
|
+
}
|
|
1068
|
+
// Reduce confidence for common false positives
|
|
1069
|
+
if (str.includes(' '))
|
|
1070
|
+
confidence -= 0.2; // Spaces suggest natural text
|
|
1071
|
+
if (/\.(com|org|net|io)/.test(str))
|
|
1072
|
+
confidence -= 0.1; // URL-like
|
|
1073
|
+
return Math.max(0, Math.min(1, confidence));
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* Get entropy rating label
|
|
1077
|
+
*/
|
|
1078
|
+
getEntropyRating(entropy) {
|
|
1079
|
+
if (entropy >= 5.5)
|
|
1080
|
+
return 'very-high';
|
|
1081
|
+
if (entropy >= 5.0)
|
|
1082
|
+
return 'high';
|
|
1083
|
+
return 'moderate';
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Convert entropy to severity
|
|
1087
|
+
*/
|
|
1088
|
+
entropyToSeverity(entropy, confidence) {
|
|
1089
|
+
if (entropy >= 5.5 && confidence >= 0.8)
|
|
1090
|
+
return 'critical';
|
|
1091
|
+
if (entropy >= 5.0 && confidence >= 0.7)
|
|
1092
|
+
return 'high';
|
|
1093
|
+
if (entropy >= 4.5 && confidence >= 0.6)
|
|
1094
|
+
return 'medium';
|
|
1095
|
+
return 'low';
|
|
1096
|
+
}
|
|
1097
|
+
/**
|
|
1098
|
+
* Check if string is a known non-secret (false positive)
|
|
1099
|
+
*/
|
|
1100
|
+
isKnownNonSecret(str) {
|
|
1101
|
+
// URLs without credentials
|
|
1102
|
+
if (/^https?:\/\/[^:@]+$/.test(str))
|
|
1103
|
+
return true;
|
|
1104
|
+
// Email addresses
|
|
1105
|
+
if (/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(str))
|
|
1106
|
+
return true;
|
|
1107
|
+
// UUIDs (not secrets, just identifiers)
|
|
1108
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str))
|
|
1109
|
+
return true;
|
|
1110
|
+
// Common placeholder values
|
|
1111
|
+
const placeholders = [
|
|
1112
|
+
'your-api-key-here',
|
|
1113
|
+
'replace-with-your-key',
|
|
1114
|
+
'xxx',
|
|
1115
|
+
'placeholder',
|
|
1116
|
+
'example',
|
|
1117
|
+
'test',
|
|
1118
|
+
'sample',
|
|
1119
|
+
];
|
|
1120
|
+
if (placeholders.some(p => str.toLowerCase().includes(p)))
|
|
1121
|
+
return true;
|
|
1122
|
+
// File paths
|
|
1123
|
+
if (str.startsWith('/') || str.includes('\\') || str.includes('://'))
|
|
1124
|
+
return true;
|
|
1125
|
+
// Natural language (multiple spaces suggest text)
|
|
1126
|
+
if ((str.match(/ /g) || []).length > 3)
|
|
1127
|
+
return true;
|
|
1128
|
+
return false;
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Calculate entropy distribution buckets
|
|
1132
|
+
*/
|
|
1133
|
+
calculateEntropyDistribution(entropies) {
|
|
1134
|
+
if (entropies.length === 0) {
|
|
1135
|
+
return [
|
|
1136
|
+
{ range: '0-2', count: 0, percentage: 0 },
|
|
1137
|
+
{ range: '2-3', count: 0, percentage: 0 },
|
|
1138
|
+
{ range: '3-4', count: 0, percentage: 0 },
|
|
1139
|
+
{ range: '4-5', count: 0, percentage: 0 },
|
|
1140
|
+
{ range: '5-6', count: 0, percentage: 0 },
|
|
1141
|
+
{ range: '6+', count: 0, percentage: 0 },
|
|
1142
|
+
];
|
|
1143
|
+
}
|
|
1144
|
+
const buckets = [
|
|
1145
|
+
{ range: '0-2', count: 0, percentage: 0 },
|
|
1146
|
+
{ range: '2-3', count: 0, percentage: 0 },
|
|
1147
|
+
{ range: '3-4', count: 0, percentage: 0 },
|
|
1148
|
+
{ range: '4-5', count: 0, percentage: 0 },
|
|
1149
|
+
{ range: '5-6', count: 0, percentage: 0 },
|
|
1150
|
+
{ range: '6+', count: 0, percentage: 0 },
|
|
1151
|
+
];
|
|
1152
|
+
for (const e of entropies) {
|
|
1153
|
+
if (e < 2)
|
|
1154
|
+
buckets[0].count++;
|
|
1155
|
+
else if (e < 3)
|
|
1156
|
+
buckets[1].count++;
|
|
1157
|
+
else if (e < 4)
|
|
1158
|
+
buckets[2].count++;
|
|
1159
|
+
else if (e < 5)
|
|
1160
|
+
buckets[3].count++;
|
|
1161
|
+
else if (e < 6)
|
|
1162
|
+
buckets[4].count++;
|
|
1163
|
+
else
|
|
1164
|
+
buckets[5].count++;
|
|
1165
|
+
}
|
|
1166
|
+
const total = entropies.length;
|
|
1167
|
+
for (const bucket of buckets) {
|
|
1168
|
+
bucket.percentage = Math.round((bucket.count / total) * 100);
|
|
1169
|
+
}
|
|
1170
|
+
return buckets;
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Quick entropy check for a single string
|
|
1174
|
+
*/
|
|
1175
|
+
quickEntropyCheck(str) {
|
|
1176
|
+
const entropy = this.calculateShannonEntropy(str);
|
|
1177
|
+
const charset = this.analyzeCharset(str);
|
|
1178
|
+
const isLikelySecret = entropy >= 4.5 && !this.isKnownNonSecret(str);
|
|
1179
|
+
return {
|
|
1180
|
+
entropy,
|
|
1181
|
+
isLikelySecret,
|
|
1182
|
+
secretType: isLikelySecret ? this.inferSecretType(str, charset, entropy) : null,
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
exports.N8nSecretsHygieneAuditorAgent = N8nSecretsHygieneAuditorAgent;
|
|
1187
|
+
//# sourceMappingURL=N8nSecretsHygieneAuditorAgent.js.map
|