agentic-qe 2.5.6 → 2.5.7

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.
Files changed (134) hide show
  1. package/.claude/agents/n8n/n8n-base-agent.md +376 -0
  2. package/.claude/agents/n8n/n8n-bdd-scenario-tester.md +613 -0
  3. package/.claude/agents/n8n/n8n-chaos-tester.md +654 -0
  4. package/.claude/agents/n8n/n8n-ci-orchestrator.md +850 -0
  5. package/.claude/agents/n8n/n8n-compliance-validator.md +685 -0
  6. package/.claude/agents/n8n/n8n-expression-validator.md +560 -0
  7. package/.claude/agents/n8n/n8n-integration-test.md +602 -0
  8. package/.claude/agents/n8n/n8n-monitoring-validator.md +589 -0
  9. package/.claude/agents/n8n/n8n-node-validator.md +455 -0
  10. package/.claude/agents/n8n/n8n-performance-tester.md +630 -0
  11. package/.claude/agents/n8n/n8n-security-auditor.md +786 -0
  12. package/.claude/agents/n8n/n8n-trigger-test.md +500 -0
  13. package/.claude/agents/n8n/n8n-unit-tester.md +633 -0
  14. package/.claude/agents/n8n/n8n-version-comparator.md +567 -0
  15. package/.claude/agents/n8n/n8n-workflow-executor.md +392 -0
  16. package/.claude/skills/n8n-expression-testing/SKILL.md +434 -0
  17. package/.claude/skills/n8n-integration-testing-patterns/SKILL.md +540 -0
  18. package/.claude/skills/n8n-security-testing/SKILL.md +599 -0
  19. package/.claude/skills/n8n-trigger-testing-strategies/SKILL.md +541 -0
  20. package/.claude/skills/n8n-workflow-testing-fundamentals/SKILL.md +447 -0
  21. package/CHANGELOG.md +41 -0
  22. package/README.md +7 -4
  23. package/dist/agents/n8n/N8nAPIClient.d.ts +121 -0
  24. package/dist/agents/n8n/N8nAPIClient.d.ts.map +1 -0
  25. package/dist/agents/n8n/N8nAPIClient.js +367 -0
  26. package/dist/agents/n8n/N8nAPIClient.js.map +1 -0
  27. package/dist/agents/n8n/N8nAuditPersistence.d.ts +120 -0
  28. package/dist/agents/n8n/N8nAuditPersistence.d.ts.map +1 -0
  29. package/dist/agents/n8n/N8nAuditPersistence.js +473 -0
  30. package/dist/agents/n8n/N8nAuditPersistence.js.map +1 -0
  31. package/dist/agents/n8n/N8nBDDScenarioTesterAgent.d.ts +159 -0
  32. package/dist/agents/n8n/N8nBDDScenarioTesterAgent.d.ts.map +1 -0
  33. package/dist/agents/n8n/N8nBDDScenarioTesterAgent.js +697 -0
  34. package/dist/agents/n8n/N8nBDDScenarioTesterAgent.js.map +1 -0
  35. package/dist/agents/n8n/N8nBaseAgent.d.ts +126 -0
  36. package/dist/agents/n8n/N8nBaseAgent.d.ts.map +1 -0
  37. package/dist/agents/n8n/N8nBaseAgent.js +446 -0
  38. package/dist/agents/n8n/N8nBaseAgent.js.map +1 -0
  39. package/dist/agents/n8n/N8nCIOrchestratorAgent.d.ts +164 -0
  40. package/dist/agents/n8n/N8nCIOrchestratorAgent.d.ts.map +1 -0
  41. package/dist/agents/n8n/N8nCIOrchestratorAgent.js +610 -0
  42. package/dist/agents/n8n/N8nCIOrchestratorAgent.js.map +1 -0
  43. package/dist/agents/n8n/N8nChaosTesterAgent.d.ts +205 -0
  44. package/dist/agents/n8n/N8nChaosTesterAgent.d.ts.map +1 -0
  45. package/dist/agents/n8n/N8nChaosTesterAgent.js +729 -0
  46. package/dist/agents/n8n/N8nChaosTesterAgent.js.map +1 -0
  47. package/dist/agents/n8n/N8nComplianceValidatorAgent.d.ts +228 -0
  48. package/dist/agents/n8n/N8nComplianceValidatorAgent.d.ts.map +1 -0
  49. package/dist/agents/n8n/N8nComplianceValidatorAgent.js +986 -0
  50. package/dist/agents/n8n/N8nComplianceValidatorAgent.js.map +1 -0
  51. package/dist/agents/n8n/N8nContractTesterAgent.d.ts +213 -0
  52. package/dist/agents/n8n/N8nContractTesterAgent.d.ts.map +1 -0
  53. package/dist/agents/n8n/N8nContractTesterAgent.js +989 -0
  54. package/dist/agents/n8n/N8nContractTesterAgent.js.map +1 -0
  55. package/dist/agents/n8n/N8nExpressionValidatorAgent.d.ts +99 -0
  56. package/dist/agents/n8n/N8nExpressionValidatorAgent.d.ts.map +1 -0
  57. package/dist/agents/n8n/N8nExpressionValidatorAgent.js +632 -0
  58. package/dist/agents/n8n/N8nExpressionValidatorAgent.js.map +1 -0
  59. package/dist/agents/n8n/N8nFailureModeTesterAgent.d.ts +238 -0
  60. package/dist/agents/n8n/N8nFailureModeTesterAgent.d.ts.map +1 -0
  61. package/dist/agents/n8n/N8nFailureModeTesterAgent.js +956 -0
  62. package/dist/agents/n8n/N8nFailureModeTesterAgent.js.map +1 -0
  63. package/dist/agents/n8n/N8nIdempotencyTesterAgent.d.ts +242 -0
  64. package/dist/agents/n8n/N8nIdempotencyTesterAgent.d.ts.map +1 -0
  65. package/dist/agents/n8n/N8nIdempotencyTesterAgent.js +992 -0
  66. package/dist/agents/n8n/N8nIdempotencyTesterAgent.js.map +1 -0
  67. package/dist/agents/n8n/N8nIntegrationTestAgent.d.ts +104 -0
  68. package/dist/agents/n8n/N8nIntegrationTestAgent.d.ts.map +1 -0
  69. package/dist/agents/n8n/N8nIntegrationTestAgent.js +653 -0
  70. package/dist/agents/n8n/N8nIntegrationTestAgent.js.map +1 -0
  71. package/dist/agents/n8n/N8nMonitoringValidatorAgent.d.ts +210 -0
  72. package/dist/agents/n8n/N8nMonitoringValidatorAgent.d.ts.map +1 -0
  73. package/dist/agents/n8n/N8nMonitoringValidatorAgent.js +669 -0
  74. package/dist/agents/n8n/N8nMonitoringValidatorAgent.js.map +1 -0
  75. package/dist/agents/n8n/N8nNodeValidatorAgent.d.ts +142 -0
  76. package/dist/agents/n8n/N8nNodeValidatorAgent.d.ts.map +1 -0
  77. package/dist/agents/n8n/N8nNodeValidatorAgent.js +1090 -0
  78. package/dist/agents/n8n/N8nNodeValidatorAgent.js.map +1 -0
  79. package/dist/agents/n8n/N8nPerformanceTesterAgent.d.ts +198 -0
  80. package/dist/agents/n8n/N8nPerformanceTesterAgent.d.ts.map +1 -0
  81. package/dist/agents/n8n/N8nPerformanceTesterAgent.js +653 -0
  82. package/dist/agents/n8n/N8nPerformanceTesterAgent.js.map +1 -0
  83. package/dist/agents/n8n/N8nReplayabilityTesterAgent.d.ts +245 -0
  84. package/dist/agents/n8n/N8nReplayabilityTesterAgent.d.ts.map +1 -0
  85. package/dist/agents/n8n/N8nReplayabilityTesterAgent.js +952 -0
  86. package/dist/agents/n8n/N8nReplayabilityTesterAgent.js.map +1 -0
  87. package/dist/agents/n8n/N8nSecretsHygieneAuditorAgent.d.ts +325 -0
  88. package/dist/agents/n8n/N8nSecretsHygieneAuditorAgent.d.ts.map +1 -0
  89. package/dist/agents/n8n/N8nSecretsHygieneAuditorAgent.js +1187 -0
  90. package/dist/agents/n8n/N8nSecretsHygieneAuditorAgent.js.map +1 -0
  91. package/dist/agents/n8n/N8nSecurityAuditorAgent.d.ts +91 -0
  92. package/dist/agents/n8n/N8nSecurityAuditorAgent.d.ts.map +1 -0
  93. package/dist/agents/n8n/N8nSecurityAuditorAgent.js +825 -0
  94. package/dist/agents/n8n/N8nSecurityAuditorAgent.js.map +1 -0
  95. package/dist/agents/n8n/N8nTestHarness.d.ts +131 -0
  96. package/dist/agents/n8n/N8nTestHarness.d.ts.map +1 -0
  97. package/dist/agents/n8n/N8nTestHarness.js +456 -0
  98. package/dist/agents/n8n/N8nTestHarness.js.map +1 -0
  99. package/dist/agents/n8n/N8nTriggerTestAgent.d.ts +119 -0
  100. package/dist/agents/n8n/N8nTriggerTestAgent.d.ts.map +1 -0
  101. package/dist/agents/n8n/N8nTriggerTestAgent.js +652 -0
  102. package/dist/agents/n8n/N8nTriggerTestAgent.js.map +1 -0
  103. package/dist/agents/n8n/N8nUnitTesterAgent.d.ts +130 -0
  104. package/dist/agents/n8n/N8nUnitTesterAgent.d.ts.map +1 -0
  105. package/dist/agents/n8n/N8nUnitTesterAgent.js +522 -0
  106. package/dist/agents/n8n/N8nUnitTesterAgent.js.map +1 -0
  107. package/dist/agents/n8n/N8nVersionComparatorAgent.d.ts +201 -0
  108. package/dist/agents/n8n/N8nVersionComparatorAgent.d.ts.map +1 -0
  109. package/dist/agents/n8n/N8nVersionComparatorAgent.js +645 -0
  110. package/dist/agents/n8n/N8nVersionComparatorAgent.js.map +1 -0
  111. package/dist/agents/n8n/N8nWorkflowExecutorAgent.d.ts +120 -0
  112. package/dist/agents/n8n/N8nWorkflowExecutorAgent.d.ts.map +1 -0
  113. package/dist/agents/n8n/N8nWorkflowExecutorAgent.js +347 -0
  114. package/dist/agents/n8n/N8nWorkflowExecutorAgent.js.map +1 -0
  115. package/dist/agents/n8n/index.d.ts +119 -0
  116. package/dist/agents/n8n/index.d.ts.map +1 -0
  117. package/dist/agents/n8n/index.js +298 -0
  118. package/dist/agents/n8n/index.js.map +1 -0
  119. package/dist/agents/n8n/types.d.ts +486 -0
  120. package/dist/agents/n8n/types.d.ts.map +1 -0
  121. package/dist/agents/n8n/types.js +8 -0
  122. package/dist/agents/n8n/types.js.map +1 -0
  123. package/dist/cli/init/agents.d.ts.map +1 -1
  124. package/dist/cli/init/agents.js +29 -0
  125. package/dist/cli/init/agents.js.map +1 -1
  126. package/dist/cli/init/skills.d.ts.map +1 -1
  127. package/dist/cli/init/skills.js +7 -1
  128. package/dist/cli/init/skills.js.map +1 -1
  129. package/dist/core/memory/HNSWVectorMemory.js +1 -1
  130. package/dist/mcp/server-instructions.d.ts +1 -1
  131. package/dist/mcp/server-instructions.js +1 -1
  132. package/docs/reference/agents.md +91 -2
  133. package/docs/reference/skills.md +97 -2
  134. 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