@yuaone/core 0.1.0
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/LICENSE +663 -0
- package/README.md +15 -0
- package/dist/__tests__/context-manager.test.d.ts +6 -0
- package/dist/__tests__/context-manager.test.d.ts.map +1 -0
- package/dist/__tests__/context-manager.test.js +220 -0
- package/dist/__tests__/context-manager.test.js.map +1 -0
- package/dist/__tests__/governor.test.d.ts +6 -0
- package/dist/__tests__/governor.test.d.ts.map +1 -0
- package/dist/__tests__/governor.test.js +210 -0
- package/dist/__tests__/governor.test.js.map +1 -0
- package/dist/__tests__/model-router.test.d.ts +6 -0
- package/dist/__tests__/model-router.test.d.ts.map +1 -0
- package/dist/__tests__/model-router.test.js +329 -0
- package/dist/__tests__/model-router.test.js.map +1 -0
- package/dist/agent-logger.d.ts +384 -0
- package/dist/agent-logger.d.ts.map +1 -0
- package/dist/agent-logger.js +820 -0
- package/dist/agent-logger.js.map +1 -0
- package/dist/agent-loop.d.ts +163 -0
- package/dist/agent-loop.d.ts.map +1 -0
- package/dist/agent-loop.js +609 -0
- package/dist/agent-loop.js.map +1 -0
- package/dist/agent-modes.d.ts +85 -0
- package/dist/agent-modes.d.ts.map +1 -0
- package/dist/agent-modes.js +418 -0
- package/dist/agent-modes.js.map +1 -0
- package/dist/approval.d.ts +137 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +299 -0
- package/dist/approval.js.map +1 -0
- package/dist/async-completion-queue.d.ts +56 -0
- package/dist/async-completion-queue.d.ts.map +1 -0
- package/dist/async-completion-queue.js +77 -0
- package/dist/async-completion-queue.js.map +1 -0
- package/dist/auto-fix.d.ts +174 -0
- package/dist/auto-fix.d.ts.map +1 -0
- package/dist/auto-fix.js +319 -0
- package/dist/auto-fix.js.map +1 -0
- package/dist/codebase-context.d.ts +396 -0
- package/dist/codebase-context.d.ts.map +1 -0
- package/dist/codebase-context.js +1260 -0
- package/dist/codebase-context.js.map +1 -0
- package/dist/conflict-resolver.d.ts +191 -0
- package/dist/conflict-resolver.d.ts.map +1 -0
- package/dist/conflict-resolver.js +524 -0
- package/dist/conflict-resolver.js.map +1 -0
- package/dist/constants.d.ts +52 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +141 -0
- package/dist/constants.js.map +1 -0
- package/dist/context-budget.d.ts +435 -0
- package/dist/context-budget.d.ts.map +1 -0
- package/dist/context-budget.js +903 -0
- package/dist/context-budget.js.map +1 -0
- package/dist/context-compressor.d.ts +143 -0
- package/dist/context-compressor.d.ts.map +1 -0
- package/dist/context-compressor.js +511 -0
- package/dist/context-compressor.js.map +1 -0
- package/dist/context-manager.d.ts +112 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +247 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/continuous-reflection.d.ts +267 -0
- package/dist/continuous-reflection.d.ts.map +1 -0
- package/dist/continuous-reflection.js +338 -0
- package/dist/continuous-reflection.js.map +1 -0
- package/dist/cross-file-refactor.d.ts +352 -0
- package/dist/cross-file-refactor.d.ts.map +1 -0
- package/dist/cross-file-refactor.js +1544 -0
- package/dist/cross-file-refactor.js.map +1 -0
- package/dist/dag-orchestrator.d.ts +138 -0
- package/dist/dag-orchestrator.d.ts.map +1 -0
- package/dist/dag-orchestrator.js +379 -0
- package/dist/dag-orchestrator.js.map +1 -0
- package/dist/debate-orchestrator.d.ts +301 -0
- package/dist/debate-orchestrator.d.ts.map +1 -0
- package/dist/debate-orchestrator.js +719 -0
- package/dist/debate-orchestrator.js.map +1 -0
- package/dist/dependency-analyzer.d.ts +113 -0
- package/dist/dependency-analyzer.d.ts.map +1 -0
- package/dist/dependency-analyzer.js +444 -0
- package/dist/dependency-analyzer.js.map +1 -0
- package/dist/design-loop.d.ts +59 -0
- package/dist/design-loop.d.ts.map +1 -0
- package/dist/design-loop.js +344 -0
- package/dist/design-loop.js.map +1 -0
- package/dist/doc-intelligence.d.ts +383 -0
- package/dist/doc-intelligence.d.ts.map +1 -0
- package/dist/doc-intelligence.js +1307 -0
- package/dist/doc-intelligence.js.map +1 -0
- package/dist/dynamic-role-generator.d.ts +76 -0
- package/dist/dynamic-role-generator.d.ts.map +1 -0
- package/dist/dynamic-role-generator.js +194 -0
- package/dist/dynamic-role-generator.js.map +1 -0
- package/dist/errors.d.ts +69 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +102 -0
- package/dist/errors.js.map +1 -0
- package/dist/event-bus.d.ts +159 -0
- package/dist/event-bus.d.ts.map +1 -0
- package/dist/event-bus.js +305 -0
- package/dist/event-bus.js.map +1 -0
- package/dist/execution-engine.d.ts +425 -0
- package/dist/execution-engine.d.ts.map +1 -0
- package/dist/execution-engine.js +1555 -0
- package/dist/execution-engine.js.map +1 -0
- package/dist/git-intelligence.d.ts +306 -0
- package/dist/git-intelligence.d.ts.map +1 -0
- package/dist/git-intelligence.js +1099 -0
- package/dist/git-intelligence.js.map +1 -0
- package/dist/governor.d.ts +77 -0
- package/dist/governor.d.ts.map +1 -0
- package/dist/governor.js +161 -0
- package/dist/governor.js.map +1 -0
- package/dist/hierarchical-planner.d.ts +313 -0
- package/dist/hierarchical-planner.d.ts.map +1 -0
- package/dist/hierarchical-planner.js +981 -0
- package/dist/hierarchical-planner.js.map +1 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +123 -0
- package/dist/index.js.map +1 -0
- package/dist/intent-inference.d.ts +103 -0
- package/dist/intent-inference.d.ts.map +1 -0
- package/dist/intent-inference.js +605 -0
- package/dist/intent-inference.js.map +1 -0
- package/dist/interrupt-manager.d.ts +143 -0
- package/dist/interrupt-manager.d.ts.map +1 -0
- package/dist/interrupt-manager.js +196 -0
- package/dist/interrupt-manager.js.map +1 -0
- package/dist/kernel.d.ts +564 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +1419 -0
- package/dist/kernel.js.map +1 -0
- package/dist/language-support.d.ts +232 -0
- package/dist/language-support.d.ts.map +1 -0
- package/dist/language-support.js +1134 -0
- package/dist/language-support.js.map +1 -0
- package/dist/llm-client.d.ts +82 -0
- package/dist/llm-client.d.ts.map +1 -0
- package/dist/llm-client.js +475 -0
- package/dist/llm-client.js.map +1 -0
- package/dist/mcp-client.d.ts +232 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +718 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/memory-manager.d.ts +200 -0
- package/dist/memory-manager.d.ts.map +1 -0
- package/dist/memory-manager.js +568 -0
- package/dist/memory-manager.js.map +1 -0
- package/dist/memory.d.ts +87 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +341 -0
- package/dist/memory.js.map +1 -0
- package/dist/model-router.d.ts +245 -0
- package/dist/model-router.d.ts.map +1 -0
- package/dist/model-router.js +632 -0
- package/dist/model-router.js.map +1 -0
- package/dist/parallel-executor.d.ts +125 -0
- package/dist/parallel-executor.d.ts.map +1 -0
- package/dist/parallel-executor.js +201 -0
- package/dist/parallel-executor.js.map +1 -0
- package/dist/perf-optimizer.d.ts +212 -0
- package/dist/perf-optimizer.d.ts.map +1 -0
- package/dist/perf-optimizer.js +721 -0
- package/dist/perf-optimizer.js.map +1 -0
- package/dist/persona.d.ts +305 -0
- package/dist/persona.d.ts.map +1 -0
- package/dist/persona.js +887 -0
- package/dist/persona.js.map +1 -0
- package/dist/planner.d.ts +70 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +264 -0
- package/dist/planner.js.map +1 -0
- package/dist/qa-pipeline.d.ts +365 -0
- package/dist/qa-pipeline.d.ts.map +1 -0
- package/dist/qa-pipeline.js +1352 -0
- package/dist/qa-pipeline.js.map +1 -0
- package/dist/reasoning-adapter.d.ts +116 -0
- package/dist/reasoning-adapter.d.ts.map +1 -0
- package/dist/reasoning-adapter.js +187 -0
- package/dist/reasoning-adapter.js.map +1 -0
- package/dist/role-registry.d.ts +55 -0
- package/dist/role-registry.d.ts.map +1 -0
- package/dist/role-registry.js +192 -0
- package/dist/role-registry.js.map +1 -0
- package/dist/sandbox-tiers.d.ts +327 -0
- package/dist/sandbox-tiers.d.ts.map +1 -0
- package/dist/sandbox-tiers.js +928 -0
- package/dist/sandbox-tiers.js.map +1 -0
- package/dist/security-scanner.d.ts +222 -0
- package/dist/security-scanner.d.ts.map +1 -0
- package/dist/security-scanner.js +1129 -0
- package/dist/security-scanner.js.map +1 -0
- package/dist/security.d.ts +93 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +393 -0
- package/dist/security.js.map +1 -0
- package/dist/self-reflection.d.ts +397 -0
- package/dist/self-reflection.d.ts.map +1 -0
- package/dist/self-reflection.js +908 -0
- package/dist/self-reflection.js.map +1 -0
- package/dist/session-persistence.d.ts +191 -0
- package/dist/session-persistence.d.ts.map +1 -0
- package/dist/session-persistence.js +395 -0
- package/dist/session-persistence.js.map +1 -0
- package/dist/speculative-executor.d.ts +210 -0
- package/dist/speculative-executor.d.ts.map +1 -0
- package/dist/speculative-executor.js +618 -0
- package/dist/speculative-executor.js.map +1 -0
- package/dist/state-machine.d.ts +289 -0
- package/dist/state-machine.d.ts.map +1 -0
- package/dist/state-machine.js +695 -0
- package/dist/state-machine.js.map +1 -0
- package/dist/sub-agent.d.ts +177 -0
- package/dist/sub-agent.d.ts.map +1 -0
- package/dist/sub-agent.js +303 -0
- package/dist/sub-agent.js.map +1 -0
- package/dist/system-prompt.d.ts +26 -0
- package/dist/system-prompt.d.ts.map +1 -0
- package/dist/system-prompt.js +84 -0
- package/dist/system-prompt.js.map +1 -0
- package/dist/test-intelligence.d.ts +439 -0
- package/dist/test-intelligence.d.ts.map +1 -0
- package/dist/test-intelligence.js +1165 -0
- package/dist/test-intelligence.js.map +1 -0
- package/dist/types.d.ts +632 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/vector-index.d.ts +314 -0
- package/dist/vector-index.d.ts.map +1 -0
- package/dist/vector-index.js +618 -0
- package/dist/vector-index.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,1129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module security-scanner
|
|
3
|
+
* @description YUAN Security DAST (Dynamic Application Security Testing) module.
|
|
4
|
+
*
|
|
5
|
+
* Performs dynamic security analysis on code changes:
|
|
6
|
+
* 1. Dependency Vulnerability Scanning — known CVE patterns in package.json
|
|
7
|
+
* 2. Secret Detection — API keys, tokens, passwords, private keys
|
|
8
|
+
* 3. Code Security Patterns — injection, XSS, SSRF, traversal, crypto
|
|
9
|
+
* 4. Configuration Security — tsconfig, CSP, CORS, headers
|
|
10
|
+
* 5. Report Generation — severity-based findings with pass/fail decision
|
|
11
|
+
*
|
|
12
|
+
* No external dependencies — Node builtins only.
|
|
13
|
+
* All secrets in evidence are masked.
|
|
14
|
+
*/
|
|
15
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
16
|
+
import { join, relative, extname } from "node:path";
|
|
17
|
+
import { randomUUID } from "node:crypto";
|
|
18
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
19
|
+
// Constants — Severity ordering
|
|
20
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
21
|
+
const SEVERITY_ORDER = {
|
|
22
|
+
critical: 5,
|
|
23
|
+
high: 4,
|
|
24
|
+
medium: 3,
|
|
25
|
+
low: 2,
|
|
26
|
+
info: 1,
|
|
27
|
+
};
|
|
28
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
29
|
+
// Built-in Patterns
|
|
30
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
31
|
+
/** Secret detection patterns */
|
|
32
|
+
const SECRET_PATTERNS = [
|
|
33
|
+
{
|
|
34
|
+
name: "aws-access-key",
|
|
35
|
+
regex: /\bAKIA[0-9A-Z]{16}\b/,
|
|
36
|
+
severity: "critical",
|
|
37
|
+
category: "secret",
|
|
38
|
+
message: "AWS Access Key ID detected",
|
|
39
|
+
suggestion: "Remove the key and rotate it immediately. Use environment variables or AWS IAM roles.",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "aws-secret-key",
|
|
43
|
+
regex: /(?<![A-Za-z0-9/+=])[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=])/,
|
|
44
|
+
severity: "medium",
|
|
45
|
+
category: "secret",
|
|
46
|
+
message: "Possible AWS Secret Access Key detected (40-char base64 string)",
|
|
47
|
+
suggestion: "Verify if this is a secret key. Use environment variables instead of hardcoding.",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "github-token",
|
|
51
|
+
regex: /\b(ghp_[a-zA-Z0-9]{36}|gho_[a-zA-Z0-9]{36}|ghs_[a-zA-Z0-9]{36}|ghr_[a-zA-Z0-9]{36}|github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59})\b/,
|
|
52
|
+
severity: "critical",
|
|
53
|
+
category: "secret",
|
|
54
|
+
message: "GitHub token detected",
|
|
55
|
+
suggestion: "Remove the token and revoke it. Use GITHUB_TOKEN environment variable.",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "generic-api-key",
|
|
59
|
+
regex: /(?:api[_-]?key|apikey|api[_-]?secret)\s*[:=]\s*["']([a-zA-Z0-9_\-]{20,})["']/i,
|
|
60
|
+
severity: "high",
|
|
61
|
+
category: "secret",
|
|
62
|
+
message: "Hardcoded API key detected",
|
|
63
|
+
suggestion: "Move API keys to environment variables or a secrets manager.",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "generic-password",
|
|
67
|
+
regex: /(?:password|passwd|pwd|secret)\s*[:=]\s*["']([^"']{8,})["']/i,
|
|
68
|
+
severity: "high",
|
|
69
|
+
category: "secret",
|
|
70
|
+
message: "Hardcoded password or secret detected",
|
|
71
|
+
suggestion: "Never hardcode passwords. Use environment variables or a secrets manager.",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "private-key",
|
|
75
|
+
regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/,
|
|
76
|
+
severity: "critical",
|
|
77
|
+
category: "secret",
|
|
78
|
+
message: "Private key detected in source code",
|
|
79
|
+
suggestion: "Remove the private key immediately. Store keys in a secure vault.",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "jwt-token",
|
|
83
|
+
regex: /\beyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\b/,
|
|
84
|
+
severity: "high",
|
|
85
|
+
category: "secret",
|
|
86
|
+
message: "JWT token detected in source code",
|
|
87
|
+
suggestion: "Remove hardcoded JWT tokens. Generate them at runtime.",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "slack-token",
|
|
91
|
+
regex: /\bxox[bpors]-[a-zA-Z0-9-]{10,}\b/,
|
|
92
|
+
severity: "critical",
|
|
93
|
+
category: "secret",
|
|
94
|
+
message: "Slack token detected",
|
|
95
|
+
suggestion: "Remove the token and revoke it. Use environment variables.",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: "stripe-key",
|
|
99
|
+
regex: /\b[sr]k_(live|test)_[a-zA-Z0-9]{20,}\b/,
|
|
100
|
+
severity: "critical",
|
|
101
|
+
category: "secret",
|
|
102
|
+
message: "Stripe API key detected",
|
|
103
|
+
suggestion: "Remove the key and rotate it. Use environment variables.",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "google-api-key",
|
|
107
|
+
regex: /\bAIza[a-zA-Z0-9_-]{35}\b/,
|
|
108
|
+
severity: "high",
|
|
109
|
+
category: "secret",
|
|
110
|
+
message: "Google API key detected",
|
|
111
|
+
suggestion: "Remove the key and restrict it. Use environment variables.",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "env-file-reference",
|
|
115
|
+
regex: /\.env(?:\.local|\.production|\.staging|\.development)?$/,
|
|
116
|
+
severity: "medium",
|
|
117
|
+
category: "secret",
|
|
118
|
+
message: ".env file should not be committed to version control",
|
|
119
|
+
suggestion: "Add .env files to .gitignore. Use .env.example for templates.",
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "base64-secret",
|
|
123
|
+
regex: /(?:secret|token|key|password|credential)\s*[:=]\s*["'](?:[A-Za-z0-9+/]{32,}={0,2})["']/i,
|
|
124
|
+
severity: "medium",
|
|
125
|
+
category: "secret",
|
|
126
|
+
message: "Possible base64-encoded secret detected",
|
|
127
|
+
suggestion: "Verify if this is a secret. Use environment variables for sensitive data.",
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "connection-string",
|
|
131
|
+
regex: /(?:mongodb|postgres|mysql|redis|amqp):\/\/[^"'\s]+:[^"'\s]+@/i,
|
|
132
|
+
severity: "high",
|
|
133
|
+
category: "secret",
|
|
134
|
+
message: "Database connection string with credentials detected",
|
|
135
|
+
suggestion: "Use environment variables for connection strings. Never hardcode credentials.",
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
/** SQL injection patterns */
|
|
139
|
+
const INJECTION_PATTERNS = [
|
|
140
|
+
{
|
|
141
|
+
name: "sql-string-concat",
|
|
142
|
+
regex: /(?:query|execute|exec|raw)\s*\(\s*(?:["'`].*?\b(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE)\b.*?["'`]\s*\+|`[^`]*\$\{)/i,
|
|
143
|
+
severity: "high",
|
|
144
|
+
category: "injection",
|
|
145
|
+
message: "Possible SQL injection via string concatenation or template literal",
|
|
146
|
+
suggestion: "Use parameterized queries or an ORM. Never concatenate user input into SQL.",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: "sql-template-literal",
|
|
150
|
+
regex: /\b(?:query|execute|exec|raw)\s*\(\s*`[^`]*\$\{[^}]+\}[^`]*(?:WHERE|AND|OR|SET|VALUES)\b/i,
|
|
151
|
+
severity: "high",
|
|
152
|
+
category: "injection",
|
|
153
|
+
message: "SQL query with template literal interpolation",
|
|
154
|
+
suggestion: "Use parameterized queries. Template literals in SQL are injection vectors.",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: "command-injection-exec",
|
|
158
|
+
regex: /\b(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:.*?\+|`[^`]*\$\{)/,
|
|
159
|
+
severity: "critical",
|
|
160
|
+
category: "injection",
|
|
161
|
+
message: "Possible command injection via exec/spawn with dynamic input",
|
|
162
|
+
suggestion: "Use execFile with argument arrays. Never pass user input to exec().",
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "command-injection-shell",
|
|
166
|
+
regex: /\bchild_process\b.*?\bexec\s*\(/,
|
|
167
|
+
severity: "high",
|
|
168
|
+
category: "injection",
|
|
169
|
+
message: "Use of child_process.exec which runs in a shell",
|
|
170
|
+
suggestion: "Prefer execFile() over exec() to avoid shell interpretation.",
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: "eval-usage",
|
|
174
|
+
regex: /\beval\s*\(\s*(?!["'`])/,
|
|
175
|
+
severity: "critical",
|
|
176
|
+
category: "injection",
|
|
177
|
+
message: "Use of eval() with potentially dynamic input",
|
|
178
|
+
suggestion: "Avoid eval(). Use JSON.parse() for data, or Function constructor with caution.",
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: "new-function",
|
|
182
|
+
regex: /new\s+Function\s*\(\s*(?:.*?\+|`[^`]*\$\{)/,
|
|
183
|
+
severity: "high",
|
|
184
|
+
category: "injection",
|
|
185
|
+
message: "Dynamic Function constructor with interpolated input",
|
|
186
|
+
suggestion: "Avoid new Function() with dynamic input. Use safer alternatives.",
|
|
187
|
+
},
|
|
188
|
+
];
|
|
189
|
+
/** XSS patterns */
|
|
190
|
+
const XSS_PATTERNS = [
|
|
191
|
+
{
|
|
192
|
+
name: "innerhtml-assignment",
|
|
193
|
+
regex: /\.innerHTML\s*=\s*(?!["'`]\s*$)/,
|
|
194
|
+
severity: "high",
|
|
195
|
+
category: "xss",
|
|
196
|
+
message: "Direct innerHTML assignment — potential XSS vector",
|
|
197
|
+
suggestion: "Use textContent for text or a sanitization library (DOMPurify) for HTML.",
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: "dangerously-set-innerhtml",
|
|
201
|
+
regex: /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:\s*(?!.*sanitize|.*DOMPurify|.*purify)/i,
|
|
202
|
+
severity: "high",
|
|
203
|
+
category: "xss",
|
|
204
|
+
message: "dangerouslySetInnerHTML without apparent sanitization",
|
|
205
|
+
suggestion: "Always sanitize HTML with DOMPurify before using dangerouslySetInnerHTML.",
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: "document-write",
|
|
209
|
+
regex: /document\.write\s*\(/,
|
|
210
|
+
severity: "medium",
|
|
211
|
+
category: "xss",
|
|
212
|
+
message: "document.write() usage — potential XSS and performance issues",
|
|
213
|
+
suggestion: "Use DOM manipulation methods instead of document.write().",
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: "outerhtml-assignment",
|
|
217
|
+
regex: /\.outerHTML\s*=/,
|
|
218
|
+
severity: "medium",
|
|
219
|
+
category: "xss",
|
|
220
|
+
message: "outerHTML assignment — potential XSS vector",
|
|
221
|
+
suggestion: "Use DOM manipulation methods. Sanitize input if HTML is required.",
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: "jquery-html",
|
|
225
|
+
regex: /\$\s*\([^)]*\)\s*\.html\s*\(\s*(?!["'`]\s*\))/,
|
|
226
|
+
severity: "medium",
|
|
227
|
+
category: "xss",
|
|
228
|
+
message: "jQuery .html() with potentially unsanitized input",
|
|
229
|
+
suggestion: "Use .text() for text content or sanitize HTML input.",
|
|
230
|
+
},
|
|
231
|
+
];
|
|
232
|
+
/** Path traversal patterns */
|
|
233
|
+
const TRAVERSAL_PATTERNS = [
|
|
234
|
+
{
|
|
235
|
+
name: "path-traversal-user-input",
|
|
236
|
+
regex: /(?:readFile|readFileSync|createReadStream|writeFile|writeFileSync|unlink|unlinkSync|stat|statSync|access|accessSync)\s*\(\s*(?:.*?\+|`[^`]*\$\{|.*?req\.|.*?params\.|.*?query\.|.*?body\.)/,
|
|
237
|
+
severity: "high",
|
|
238
|
+
category: "traversal",
|
|
239
|
+
message: "File operation with potentially user-controlled path",
|
|
240
|
+
suggestion: "Validate and sanitize file paths. Use path.resolve() and verify the result stays within allowed directories.",
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: "path-join-user-input",
|
|
244
|
+
regex: /path\.(?:join|resolve)\s*\([^)]*(?:req\.|params\.|query\.|body\.)/,
|
|
245
|
+
severity: "medium",
|
|
246
|
+
category: "traversal",
|
|
247
|
+
message: "path.join/resolve with user-controlled input",
|
|
248
|
+
suggestion: "Validate that resolved paths stay within expected directories. Check for '..' sequences.",
|
|
249
|
+
},
|
|
250
|
+
];
|
|
251
|
+
/** Insecure crypto patterns */
|
|
252
|
+
const CRYPTO_PATTERNS = [
|
|
253
|
+
{
|
|
254
|
+
name: "md5-usage",
|
|
255
|
+
regex: /\bcreateHash\s*\(\s*["']md5["']\s*\)/,
|
|
256
|
+
severity: "medium",
|
|
257
|
+
category: "crypto",
|
|
258
|
+
message: "MD5 hash detected — cryptographically weak",
|
|
259
|
+
suggestion: "Use SHA-256 or SHA-3 for security purposes. MD5 is only suitable for checksums.",
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: "sha1-usage",
|
|
263
|
+
regex: /\bcreateHash\s*\(\s*["']sha1["']\s*\)/,
|
|
264
|
+
severity: "medium",
|
|
265
|
+
category: "crypto",
|
|
266
|
+
message: "SHA-1 hash detected — deprecated for security",
|
|
267
|
+
suggestion: "Use SHA-256 or SHA-3. SHA-1 is deprecated for security purposes.",
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
name: "weak-random",
|
|
271
|
+
regex: /Math\.random\s*\(\s*\)/,
|
|
272
|
+
severity: "low",
|
|
273
|
+
category: "crypto",
|
|
274
|
+
message: "Math.random() is not cryptographically secure",
|
|
275
|
+
suggestion: "Use crypto.randomBytes() or crypto.randomUUID() for security-sensitive random values.",
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: "hardcoded-iv",
|
|
279
|
+
regex: /createCipheriv\s*\([^,]+,\s*[^,]+,\s*(?:Buffer\.from\s*\(\s*["']|["'])/,
|
|
280
|
+
severity: "high",
|
|
281
|
+
category: "crypto",
|
|
282
|
+
message: "Hardcoded initialization vector (IV) in cipher",
|
|
283
|
+
suggestion: "Generate a random IV with crypto.randomBytes() for each encryption operation.",
|
|
284
|
+
},
|
|
285
|
+
];
|
|
286
|
+
/** SSRF patterns */
|
|
287
|
+
const SSRF_PATTERNS = [
|
|
288
|
+
{
|
|
289
|
+
name: "ssrf-fetch",
|
|
290
|
+
regex: /\bfetch\s*\(\s*(?:.*?req\.|.*?params\.|.*?query\.|.*?body\.|.*?\+|`[^`]*\$\{)/,
|
|
291
|
+
severity: "high",
|
|
292
|
+
category: "ssrf",
|
|
293
|
+
message: "fetch() with potentially user-controlled URL — SSRF risk",
|
|
294
|
+
suggestion: "Validate and whitelist URLs before fetching. Block internal/private IP ranges.",
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
name: "ssrf-http-request",
|
|
298
|
+
regex: /\b(?:http|https)\.(?:get|request)\s*\(\s*(?:.*?req\.|.*?params\.|.*?query\.|.*?body\.|.*?\+|`[^`]*\$\{)/,
|
|
299
|
+
severity: "high",
|
|
300
|
+
category: "ssrf",
|
|
301
|
+
message: "HTTP request with potentially user-controlled URL — SSRF risk",
|
|
302
|
+
suggestion: "Validate and whitelist URLs. Block requests to internal networks (127.0.0.1, 10.x, 172.16.x, 192.168.x).",
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
name: "ssrf-axios",
|
|
306
|
+
regex: /\baxios\s*\.?\s*(?:get|post|put|patch|delete|request)\s*\(\s*(?:.*?req\.|.*?params\.|.*?query\.|.*?body\.|.*?\+|`[^`]*\$\{)/,
|
|
307
|
+
severity: "high",
|
|
308
|
+
category: "ssrf",
|
|
309
|
+
message: "Axios request with potentially user-controlled URL — SSRF risk",
|
|
310
|
+
suggestion: "Validate and whitelist URLs before making requests.",
|
|
311
|
+
},
|
|
312
|
+
];
|
|
313
|
+
/** Prototype pollution patterns */
|
|
314
|
+
const POLLUTION_PATTERNS = [
|
|
315
|
+
{
|
|
316
|
+
name: "prototype-pollution-merge",
|
|
317
|
+
regex: /(?:Object\.assign|_\.merge|_\.defaultsDeep|_\.set|_\.setWith)\s*\(\s*(?:.*?req\.|.*?body\.|.*?params\.)/,
|
|
318
|
+
severity: "high",
|
|
319
|
+
category: "injection",
|
|
320
|
+
message: "Object merge with user input — prototype pollution risk",
|
|
321
|
+
suggestion: "Validate keys before merging. Block '__proto__', 'constructor', and 'prototype' keys.",
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: "bracket-notation-user-input",
|
|
325
|
+
regex: /\[[^\]]*(?:req\.|params\.|query\.|body\.)[^\]]*\]\s*=/,
|
|
326
|
+
severity: "medium",
|
|
327
|
+
category: "injection",
|
|
328
|
+
message: "Bracket notation assignment with user input — prototype pollution risk",
|
|
329
|
+
suggestion: "Validate property names. Block '__proto__', 'constructor', and 'prototype'.",
|
|
330
|
+
},
|
|
331
|
+
];
|
|
332
|
+
/** Known vulnerable package patterns (subset of well-known CVEs) */
|
|
333
|
+
const VULNERABLE_PACKAGES = [
|
|
334
|
+
{
|
|
335
|
+
name: "lodash",
|
|
336
|
+
vulnerableVersions: "<4.17.21",
|
|
337
|
+
severity: "high",
|
|
338
|
+
cve: "CVE-2021-23337",
|
|
339
|
+
message: "lodash < 4.17.21 has prototype pollution vulnerabilities",
|
|
340
|
+
suggestion: "Upgrade lodash to >= 4.17.21.",
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: "minimist",
|
|
344
|
+
vulnerableVersions: "<1.2.6",
|
|
345
|
+
severity: "high",
|
|
346
|
+
cve: "CVE-2021-44906",
|
|
347
|
+
message: "minimist < 1.2.6 has prototype pollution vulnerability",
|
|
348
|
+
suggestion: "Upgrade minimist to >= 1.2.6.",
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: "node-forge",
|
|
352
|
+
vulnerableVersions: "<1.3.0",
|
|
353
|
+
severity: "high",
|
|
354
|
+
cve: "CVE-2022-24771",
|
|
355
|
+
message: "node-forge < 1.3.0 has signature verification bypass",
|
|
356
|
+
suggestion: "Upgrade node-forge to >= 1.3.0.",
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
name: "jsonwebtoken",
|
|
360
|
+
vulnerableVersions: "<9.0.0",
|
|
361
|
+
severity: "high",
|
|
362
|
+
cve: "CVE-2022-23529",
|
|
363
|
+
message: "jsonwebtoken < 9.0.0 has insecure key retrieval vulnerability",
|
|
364
|
+
suggestion: "Upgrade jsonwebtoken to >= 9.0.0.",
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: "express",
|
|
368
|
+
vulnerableVersions: "<4.19.2",
|
|
369
|
+
severity: "medium",
|
|
370
|
+
cve: "CVE-2024-29041",
|
|
371
|
+
message: "express < 4.19.2 has open redirect vulnerability",
|
|
372
|
+
suggestion: "Upgrade express to >= 4.19.2.",
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: "axios",
|
|
376
|
+
vulnerableVersions: "<1.6.0",
|
|
377
|
+
severity: "medium",
|
|
378
|
+
cve: "CVE-2023-45857",
|
|
379
|
+
message: "axios < 1.6.0 has CSRF vulnerability via cookie exposure",
|
|
380
|
+
suggestion: "Upgrade axios to >= 1.6.0.",
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
name: "semver",
|
|
384
|
+
vulnerableVersions: "<7.5.2",
|
|
385
|
+
severity: "medium",
|
|
386
|
+
cve: "CVE-2022-25883",
|
|
387
|
+
message: "semver < 7.5.2 has ReDoS vulnerability",
|
|
388
|
+
suggestion: "Upgrade semver to >= 7.5.2.",
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: "tar",
|
|
392
|
+
vulnerableVersions: "<6.2.1",
|
|
393
|
+
severity: "high",
|
|
394
|
+
cve: "CVE-2024-28863",
|
|
395
|
+
message: "tar < 6.2.1 has denial of service vulnerability",
|
|
396
|
+
suggestion: "Upgrade tar to >= 6.2.1.",
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
name: "xml2js",
|
|
400
|
+
vulnerableVersions: "<0.5.0",
|
|
401
|
+
severity: "medium",
|
|
402
|
+
cve: "CVE-2023-0842",
|
|
403
|
+
message: "xml2js < 0.5.0 has prototype pollution vulnerability",
|
|
404
|
+
suggestion: "Upgrade xml2js to >= 0.5.0.",
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
name: "tough-cookie",
|
|
408
|
+
vulnerableVersions: "<4.1.3",
|
|
409
|
+
severity: "medium",
|
|
410
|
+
cve: "CVE-2023-26136",
|
|
411
|
+
message: "tough-cookie < 4.1.3 has prototype pollution vulnerability",
|
|
412
|
+
suggestion: "Upgrade tough-cookie to >= 4.1.3.",
|
|
413
|
+
},
|
|
414
|
+
];
|
|
415
|
+
/** Deprecated/unmaintained packages */
|
|
416
|
+
const DEPRECATED_PACKAGES = [
|
|
417
|
+
{
|
|
418
|
+
name: "request",
|
|
419
|
+
severity: "low",
|
|
420
|
+
message: "'request' package is deprecated and unmaintained",
|
|
421
|
+
suggestion: "Migrate to 'node-fetch', 'axios', or native fetch().",
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: "querystring",
|
|
425
|
+
severity: "info",
|
|
426
|
+
message: "'querystring' module is deprecated in Node.js",
|
|
427
|
+
suggestion: "Use URLSearchParams or the 'qs' package.",
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
name: "uuid",
|
|
431
|
+
vulnerableVersions: "<9.0.0",
|
|
432
|
+
severity: "info",
|
|
433
|
+
message: "Consider using Node.js built-in crypto.randomUUID() instead of uuid package",
|
|
434
|
+
suggestion: "Use crypto.randomUUID() (Node 19+) or keep uuid >= 9.0.0.",
|
|
435
|
+
},
|
|
436
|
+
];
|
|
437
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
438
|
+
// File extensions to scan
|
|
439
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
440
|
+
const SCANNABLE_EXTENSIONS = new Set([
|
|
441
|
+
".ts",
|
|
442
|
+
".tsx",
|
|
443
|
+
".js",
|
|
444
|
+
".jsx",
|
|
445
|
+
".mjs",
|
|
446
|
+
".cjs",
|
|
447
|
+
".json",
|
|
448
|
+
".yaml",
|
|
449
|
+
".yml",
|
|
450
|
+
".toml",
|
|
451
|
+
".env",
|
|
452
|
+
".sh",
|
|
453
|
+
".bash",
|
|
454
|
+
".py",
|
|
455
|
+
".go",
|
|
456
|
+
".rs",
|
|
457
|
+
".java",
|
|
458
|
+
".rb",
|
|
459
|
+
".php",
|
|
460
|
+
".vue",
|
|
461
|
+
".svelte",
|
|
462
|
+
]);
|
|
463
|
+
const DEFAULT_IGNORE_PATHS = [
|
|
464
|
+
"node_modules",
|
|
465
|
+
".git",
|
|
466
|
+
"dist",
|
|
467
|
+
"build",
|
|
468
|
+
"coverage",
|
|
469
|
+
".next",
|
|
470
|
+
".nuxt",
|
|
471
|
+
"__pycache__",
|
|
472
|
+
".cache",
|
|
473
|
+
"vendor",
|
|
474
|
+
];
|
|
475
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
476
|
+
// SecurityScanner
|
|
477
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
478
|
+
/**
|
|
479
|
+
* DAST Security Scanner — performs dynamic security analysis on code changes.
|
|
480
|
+
*
|
|
481
|
+
* Scans for secrets, injection vulnerabilities, XSS, SSRF, insecure crypto,
|
|
482
|
+
* dependency vulnerabilities, and configuration issues.
|
|
483
|
+
*
|
|
484
|
+
* @example
|
|
485
|
+
* ```typescript
|
|
486
|
+
* const scanner = new SecurityScanner({
|
|
487
|
+
* projectPath: '/path/to/project',
|
|
488
|
+
* scanSecrets: true,
|
|
489
|
+
* scanCode: true,
|
|
490
|
+
* });
|
|
491
|
+
* const result = await scanner.scan();
|
|
492
|
+
* console.log(result.summary);
|
|
493
|
+
* ```
|
|
494
|
+
*/
|
|
495
|
+
export class SecurityScanner {
|
|
496
|
+
config;
|
|
497
|
+
constructor(config) {
|
|
498
|
+
this.config = {
|
|
499
|
+
projectPath: config.projectPath,
|
|
500
|
+
scanSecrets: config.scanSecrets ?? true,
|
|
501
|
+
scanDependencies: config.scanDependencies ?? true,
|
|
502
|
+
scanCode: config.scanCode ?? true,
|
|
503
|
+
scanConfig: config.scanConfig ?? true,
|
|
504
|
+
severityThreshold: config.severityThreshold ?? "low",
|
|
505
|
+
customPatterns: config.customPatterns ?? [],
|
|
506
|
+
ignorePaths: config.ignorePaths ?? [],
|
|
507
|
+
ignoreRules: config.ignoreRules ?? [],
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
// ────────────────────────────────────────────────────────────────────
|
|
511
|
+
// Full scan
|
|
512
|
+
// ────────────────────────────────────────────────────────────────────
|
|
513
|
+
/**
|
|
514
|
+
* Run a full security scan on the project.
|
|
515
|
+
*
|
|
516
|
+
* @param files Optional list of specific file paths to scan (relative to projectPath).
|
|
517
|
+
* If omitted, discovers files recursively.
|
|
518
|
+
* @returns Scan result with findings, summary, and pass/fail decision.
|
|
519
|
+
*/
|
|
520
|
+
async scan(files) {
|
|
521
|
+
const start = Date.now();
|
|
522
|
+
const findings = [];
|
|
523
|
+
let filesScanned = 0;
|
|
524
|
+
// Discover or use provided files
|
|
525
|
+
const filePaths = files ?? (await this.discoverFiles(this.config.projectPath));
|
|
526
|
+
// Collect file contents
|
|
527
|
+
const fileContents = new Map();
|
|
528
|
+
for (const fp of filePaths) {
|
|
529
|
+
if (this.isIgnored(fp))
|
|
530
|
+
continue;
|
|
531
|
+
try {
|
|
532
|
+
const absPath = fp.startsWith("/") ? fp : join(this.config.projectPath, fp);
|
|
533
|
+
const content = await readFile(absPath, "utf-8");
|
|
534
|
+
const relPath = fp.startsWith("/")
|
|
535
|
+
? relative(this.config.projectPath, fp)
|
|
536
|
+
: fp;
|
|
537
|
+
fileContents.set(relPath, content);
|
|
538
|
+
filesScanned++;
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
// Skip unreadable files
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// Run scans
|
|
545
|
+
for (const [filePath, content] of fileContents) {
|
|
546
|
+
if (this.config.scanSecrets) {
|
|
547
|
+
findings.push(...this.scanFileForSecrets(content, filePath));
|
|
548
|
+
}
|
|
549
|
+
if (this.config.scanCode) {
|
|
550
|
+
findings.push(...this.scanFileForInjection(content, filePath));
|
|
551
|
+
findings.push(...this.scanFileForXSS(content, filePath));
|
|
552
|
+
findings.push(...this.scanFileForTraversal(content, filePath));
|
|
553
|
+
findings.push(...this.scanFileForCrypto(content, filePath));
|
|
554
|
+
findings.push(...this.scanFileForSSRF(content, filePath));
|
|
555
|
+
findings.push(...this.scanFileForPrototypePollution(content, filePath));
|
|
556
|
+
findings.push(...this.scanFileWithCustomPatterns(content, filePath));
|
|
557
|
+
}
|
|
558
|
+
// Dependency scan for package.json files
|
|
559
|
+
if (this.config.scanDependencies && filePath.endsWith("package.json")) {
|
|
560
|
+
findings.push(...this.scanDependencies(content));
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
// Config scan
|
|
564
|
+
if (this.config.scanConfig) {
|
|
565
|
+
findings.push(...this.scanConfig(fileContents));
|
|
566
|
+
}
|
|
567
|
+
// Filter by threshold and ignored rules
|
|
568
|
+
const filtered = findings.filter((f) => {
|
|
569
|
+
if (this.config.ignoreRules.includes(f.rule))
|
|
570
|
+
return false;
|
|
571
|
+
return SEVERITY_ORDER[f.severity] >= SEVERITY_ORDER[this.config.severityThreshold];
|
|
572
|
+
});
|
|
573
|
+
// Build summary
|
|
574
|
+
const summary = {
|
|
575
|
+
critical: 0,
|
|
576
|
+
high: 0,
|
|
577
|
+
medium: 0,
|
|
578
|
+
low: 0,
|
|
579
|
+
info: 0,
|
|
580
|
+
total: filtered.length,
|
|
581
|
+
};
|
|
582
|
+
for (const f of filtered) {
|
|
583
|
+
summary[f.severity]++;
|
|
584
|
+
}
|
|
585
|
+
// Pass/fail: no critical or high findings
|
|
586
|
+
const passed = summary.critical === 0 && summary.high === 0;
|
|
587
|
+
return {
|
|
588
|
+
findings: filtered,
|
|
589
|
+
summary,
|
|
590
|
+
passed,
|
|
591
|
+
scanDuration: Date.now() - start,
|
|
592
|
+
filesScanned,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
// ────────────────────────────────────────────────────────────────────
|
|
596
|
+
// Individual scans — Secrets
|
|
597
|
+
// ────────────────────────────────────────────────────────────────────
|
|
598
|
+
/**
|
|
599
|
+
* Scan file contents for hardcoded secrets, API keys, and tokens.
|
|
600
|
+
*
|
|
601
|
+
* @param content File content to scan
|
|
602
|
+
* @param filePath Relative file path for reporting
|
|
603
|
+
* @returns Array of findings
|
|
604
|
+
*/
|
|
605
|
+
scanFileForSecrets(content, filePath) {
|
|
606
|
+
// Check if this is a .env file being scanned (the file itself is a finding)
|
|
607
|
+
const findings = [];
|
|
608
|
+
if (/\.env(?:\.|$)/.test(filePath)) {
|
|
609
|
+
findings.push({
|
|
610
|
+
id: randomUUID(),
|
|
611
|
+
severity: "medium",
|
|
612
|
+
category: "secret",
|
|
613
|
+
rule: "env-file-committed",
|
|
614
|
+
file: filePath,
|
|
615
|
+
line: 1,
|
|
616
|
+
message: ".env file detected — should not be committed to version control",
|
|
617
|
+
evidence: filePath,
|
|
618
|
+
suggestion: "Add .env files to .gitignore. Use .env.example for templates.",
|
|
619
|
+
confidence: 1.0,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
return [
|
|
623
|
+
...findings,
|
|
624
|
+
...this.scanWithPatterns(content, filePath, SECRET_PATTERNS),
|
|
625
|
+
];
|
|
626
|
+
}
|
|
627
|
+
// ────────────────────────────────────────────────────────────────────
|
|
628
|
+
// Individual scans — Injection
|
|
629
|
+
// ────────────────────────────────────────────────────────────────────
|
|
630
|
+
/**
|
|
631
|
+
* Scan file contents for injection vulnerabilities (SQL, command, eval).
|
|
632
|
+
*
|
|
633
|
+
* @param content File content to scan
|
|
634
|
+
* @param filePath Relative file path for reporting
|
|
635
|
+
* @returns Array of findings
|
|
636
|
+
*/
|
|
637
|
+
scanFileForInjection(content, filePath) {
|
|
638
|
+
return this.scanWithPatterns(content, filePath, INJECTION_PATTERNS);
|
|
639
|
+
}
|
|
640
|
+
// ────────────────────────────────────────────────────────────────────
|
|
641
|
+
// Individual scans — XSS
|
|
642
|
+
// ────────────────────────────────────────────────────────────────────
|
|
643
|
+
/**
|
|
644
|
+
* Scan file contents for XSS vulnerabilities.
|
|
645
|
+
*
|
|
646
|
+
* @param content File content to scan
|
|
647
|
+
* @param filePath Relative file path for reporting
|
|
648
|
+
* @returns Array of findings
|
|
649
|
+
*/
|
|
650
|
+
scanFileForXSS(content, filePath) {
|
|
651
|
+
return this.scanWithPatterns(content, filePath, XSS_PATTERNS);
|
|
652
|
+
}
|
|
653
|
+
// ────────────────────────────────────────────────────────────────────
|
|
654
|
+
// Individual scans — Dependencies
|
|
655
|
+
// ────────────────────────────────────────────────────────────────────
|
|
656
|
+
/**
|
|
657
|
+
* Scan package.json content for known vulnerable and deprecated dependencies.
|
|
658
|
+
*
|
|
659
|
+
* @param packageJsonContent Raw package.json content string
|
|
660
|
+
* @returns Array of findings
|
|
661
|
+
*/
|
|
662
|
+
scanDependencies(packageJsonContent) {
|
|
663
|
+
const findings = [];
|
|
664
|
+
let pkg;
|
|
665
|
+
try {
|
|
666
|
+
pkg = JSON.parse(packageJsonContent);
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
return findings;
|
|
670
|
+
}
|
|
671
|
+
const allDeps = {
|
|
672
|
+
...pkg.dependencies,
|
|
673
|
+
...pkg.devDependencies,
|
|
674
|
+
};
|
|
675
|
+
// Check vulnerable packages
|
|
676
|
+
for (const vuln of VULNERABLE_PACKAGES) {
|
|
677
|
+
if (allDeps[vuln.name]) {
|
|
678
|
+
const version = allDeps[vuln.name].replace(/^[\^~>=<]*/g, "");
|
|
679
|
+
if (this.isVersionVulnerable(version, vuln.vulnerableVersions)) {
|
|
680
|
+
findings.push({
|
|
681
|
+
id: randomUUID(),
|
|
682
|
+
severity: vuln.severity,
|
|
683
|
+
category: "dependency",
|
|
684
|
+
rule: `vuln-${vuln.name}`,
|
|
685
|
+
file: "package.json",
|
|
686
|
+
line: this.findDepLine(packageJsonContent, vuln.name),
|
|
687
|
+
message: `${vuln.message}${vuln.cve ? ` (${vuln.cve})` : ""}`,
|
|
688
|
+
evidence: `${vuln.name}@${allDeps[vuln.name]}`,
|
|
689
|
+
suggestion: vuln.suggestion,
|
|
690
|
+
confidence: 0.9,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
// Check deprecated packages
|
|
696
|
+
for (const dep of DEPRECATED_PACKAGES) {
|
|
697
|
+
if (allDeps[dep.name]) {
|
|
698
|
+
findings.push({
|
|
699
|
+
id: randomUUID(),
|
|
700
|
+
severity: dep.severity,
|
|
701
|
+
category: "dependency",
|
|
702
|
+
rule: `deprecated-${dep.name}`,
|
|
703
|
+
file: "package.json",
|
|
704
|
+
line: this.findDepLine(packageJsonContent, dep.name),
|
|
705
|
+
message: dep.message,
|
|
706
|
+
evidence: `${dep.name}@${allDeps[dep.name]}`,
|
|
707
|
+
suggestion: dep.suggestion,
|
|
708
|
+
confidence: 0.8,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return findings;
|
|
713
|
+
}
|
|
714
|
+
// ────────────────────────────────────────────────────────────────────
|
|
715
|
+
// Individual scans — Config
|
|
716
|
+
// ────────────────────────────────────────────────────────────────────
|
|
717
|
+
/**
|
|
718
|
+
* Scan configuration files for security issues.
|
|
719
|
+
*
|
|
720
|
+
* @param files Map of relative file paths to their contents
|
|
721
|
+
* @returns Array of findings
|
|
722
|
+
*/
|
|
723
|
+
scanConfig(files) {
|
|
724
|
+
const findings = [];
|
|
725
|
+
for (const [filePath, content] of files) {
|
|
726
|
+
// tsconfig strict mode check
|
|
727
|
+
if (filePath.endsWith("tsconfig.json") || filePath.endsWith("tsconfig.base.json")) {
|
|
728
|
+
findings.push(...this.scanTsConfig(content, filePath));
|
|
729
|
+
}
|
|
730
|
+
// Check for permissive CORS
|
|
731
|
+
if (this.isCodeFile(filePath)) {
|
|
732
|
+
findings.push(...this.scanForCorsIssues(content, filePath));
|
|
733
|
+
findings.push(...this.scanForMissingSecurityHeaders(content, filePath));
|
|
734
|
+
findings.push(...this.scanForUnsafeCSP(content, filePath));
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return findings;
|
|
738
|
+
}
|
|
739
|
+
// ────────────────────────────────────────────────────────────────────
|
|
740
|
+
// Diff-based scan
|
|
741
|
+
// ────────────────────────────────────────────────────────────────────
|
|
742
|
+
/**
|
|
743
|
+
* Scan git diff output for security issues in newly added lines.
|
|
744
|
+
* Only scans lines that start with '+' (additions).
|
|
745
|
+
*
|
|
746
|
+
* @param diffContent Raw git diff output
|
|
747
|
+
* @returns Array of findings for newly added code
|
|
748
|
+
*/
|
|
749
|
+
scanDiff(diffContent) {
|
|
750
|
+
const findings = [];
|
|
751
|
+
const lines = diffContent.split("\n");
|
|
752
|
+
let currentFile = "";
|
|
753
|
+
let currentLine = 0;
|
|
754
|
+
for (const line of lines) {
|
|
755
|
+
// Track current file from diff headers
|
|
756
|
+
const fileMatch = /^\+\+\+ b\/(.+)$/.exec(line);
|
|
757
|
+
if (fileMatch) {
|
|
758
|
+
currentFile = fileMatch[1];
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
// Track line numbers from hunk headers
|
|
762
|
+
const hunkMatch = /^@@ -\d+(?:,\d+)? \+(\d+)/.exec(line);
|
|
763
|
+
if (hunkMatch) {
|
|
764
|
+
currentLine = parseInt(hunkMatch[1], 10);
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
// Only scan added lines
|
|
768
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
769
|
+
const addedContent = line.slice(1);
|
|
770
|
+
const allPatterns = [
|
|
771
|
+
...SECRET_PATTERNS,
|
|
772
|
+
...INJECTION_PATTERNS,
|
|
773
|
+
...XSS_PATTERNS,
|
|
774
|
+
...TRAVERSAL_PATTERNS,
|
|
775
|
+
...CRYPTO_PATTERNS,
|
|
776
|
+
...SSRF_PATTERNS,
|
|
777
|
+
...POLLUTION_PATTERNS,
|
|
778
|
+
...this.config.customPatterns,
|
|
779
|
+
];
|
|
780
|
+
for (const pattern of allPatterns) {
|
|
781
|
+
if (this.config.ignoreRules.includes(pattern.name))
|
|
782
|
+
continue;
|
|
783
|
+
const match = pattern.regex.exec(addedContent);
|
|
784
|
+
if (match) {
|
|
785
|
+
findings.push({
|
|
786
|
+
id: randomUUID(),
|
|
787
|
+
severity: pattern.severity,
|
|
788
|
+
category: pattern.category,
|
|
789
|
+
rule: pattern.name,
|
|
790
|
+
file: currentFile,
|
|
791
|
+
line: currentLine,
|
|
792
|
+
column: match.index + 1,
|
|
793
|
+
message: pattern.message,
|
|
794
|
+
evidence: this.maskSecret(match[0]),
|
|
795
|
+
suggestion: pattern.suggestion,
|
|
796
|
+
confidence: this.patternConfidence(pattern),
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
currentLine++;
|
|
801
|
+
}
|
|
802
|
+
else if (!line.startsWith("-")) {
|
|
803
|
+
// Context line (no prefix) — increment line counter
|
|
804
|
+
currentLine++;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return findings;
|
|
808
|
+
}
|
|
809
|
+
// ────────────────────────────────────────────────────────────────────
|
|
810
|
+
// Utility methods
|
|
811
|
+
// ────────────────────────────────────────────────────────────────────
|
|
812
|
+
/**
|
|
813
|
+
* Mask a secret value for safe display.
|
|
814
|
+
* Shows at most the first 4 and last 2 characters.
|
|
815
|
+
*
|
|
816
|
+
* @param value The secret value to mask
|
|
817
|
+
* @returns Masked string (e.g., "AKIA****xy")
|
|
818
|
+
*/
|
|
819
|
+
maskSecret(value) {
|
|
820
|
+
if (value.length <= 6)
|
|
821
|
+
return "****";
|
|
822
|
+
if (value.length <= 10) {
|
|
823
|
+
return value.slice(0, 2) + "****" + value.slice(-2);
|
|
824
|
+
}
|
|
825
|
+
return value.slice(0, 4) + "****" + value.slice(-2);
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Check whether a file path should be ignored based on config.
|
|
829
|
+
*
|
|
830
|
+
* @param filePath File path to check (relative or absolute)
|
|
831
|
+
* @returns true if the path should be ignored
|
|
832
|
+
*/
|
|
833
|
+
isIgnored(filePath) {
|
|
834
|
+
const relPath = filePath.startsWith("/")
|
|
835
|
+
? relative(this.config.projectPath, filePath)
|
|
836
|
+
: filePath;
|
|
837
|
+
const allIgnore = [...DEFAULT_IGNORE_PATHS, ...this.config.ignorePaths];
|
|
838
|
+
for (const pattern of allIgnore) {
|
|
839
|
+
if (relPath.includes(pattern))
|
|
840
|
+
return true;
|
|
841
|
+
}
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
// ────────────────────────────────────────────────────────────────────
|
|
845
|
+
// Private helpers
|
|
846
|
+
// ────────────────────────────────────────────────────────────────────
|
|
847
|
+
/**
|
|
848
|
+
* Scan content with a set of patterns and return findings.
|
|
849
|
+
*/
|
|
850
|
+
scanWithPatterns(content, filePath, patterns) {
|
|
851
|
+
const findings = [];
|
|
852
|
+
const lines = content.split("\n");
|
|
853
|
+
for (let i = 0; i < lines.length; i++) {
|
|
854
|
+
const line = lines[i];
|
|
855
|
+
for (const pattern of patterns) {
|
|
856
|
+
if (this.config.ignoreRules.includes(pattern.name))
|
|
857
|
+
continue;
|
|
858
|
+
// Reset regex lastIndex for safety
|
|
859
|
+
const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
|
|
860
|
+
const match = regex.exec(line);
|
|
861
|
+
if (match) {
|
|
862
|
+
findings.push({
|
|
863
|
+
id: randomUUID(),
|
|
864
|
+
severity: pattern.severity,
|
|
865
|
+
category: pattern.category,
|
|
866
|
+
rule: pattern.name,
|
|
867
|
+
file: filePath,
|
|
868
|
+
line: i + 1,
|
|
869
|
+
column: match.index + 1,
|
|
870
|
+
message: pattern.message,
|
|
871
|
+
evidence: this.maskSecret(match[0]),
|
|
872
|
+
suggestion: pattern.suggestion,
|
|
873
|
+
confidence: this.patternConfidence(pattern),
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return findings;
|
|
879
|
+
}
|
|
880
|
+
/** Scan for path traversal patterns */
|
|
881
|
+
scanFileForTraversal(content, filePath) {
|
|
882
|
+
return this.scanWithPatterns(content, filePath, TRAVERSAL_PATTERNS);
|
|
883
|
+
}
|
|
884
|
+
/** Scan for insecure crypto patterns */
|
|
885
|
+
scanFileForCrypto(content, filePath) {
|
|
886
|
+
return this.scanWithPatterns(content, filePath, CRYPTO_PATTERNS);
|
|
887
|
+
}
|
|
888
|
+
/** Scan for SSRF patterns */
|
|
889
|
+
scanFileForSSRF(content, filePath) {
|
|
890
|
+
return this.scanWithPatterns(content, filePath, SSRF_PATTERNS);
|
|
891
|
+
}
|
|
892
|
+
/** Scan for prototype pollution patterns */
|
|
893
|
+
scanFileForPrototypePollution(content, filePath) {
|
|
894
|
+
return this.scanWithPatterns(content, filePath, POLLUTION_PATTERNS);
|
|
895
|
+
}
|
|
896
|
+
/** Scan with custom patterns */
|
|
897
|
+
scanFileWithCustomPatterns(content, filePath) {
|
|
898
|
+
if (this.config.customPatterns.length === 0)
|
|
899
|
+
return [];
|
|
900
|
+
return this.scanWithPatterns(content, filePath, this.config.customPatterns);
|
|
901
|
+
}
|
|
902
|
+
/** Check tsconfig for strict mode */
|
|
903
|
+
scanTsConfig(content, filePath) {
|
|
904
|
+
const findings = [];
|
|
905
|
+
try {
|
|
906
|
+
const config = JSON.parse(content);
|
|
907
|
+
if (config.compilerOptions && config.compilerOptions.strict !== true) {
|
|
908
|
+
// Check if it's not extending a base that has strict
|
|
909
|
+
if (!content.includes('"extends"')) {
|
|
910
|
+
findings.push({
|
|
911
|
+
id: randomUUID(),
|
|
912
|
+
severity: "medium",
|
|
913
|
+
category: "config",
|
|
914
|
+
rule: "tsconfig-no-strict",
|
|
915
|
+
file: filePath,
|
|
916
|
+
line: 1,
|
|
917
|
+
message: "TypeScript strict mode is not enabled",
|
|
918
|
+
evidence: '"strict": false or missing',
|
|
919
|
+
suggestion: 'Enable "strict": true in compilerOptions for stronger type safety.',
|
|
920
|
+
confidence: 0.8,
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
catch {
|
|
926
|
+
// Skip unparseable tsconfig
|
|
927
|
+
}
|
|
928
|
+
return findings;
|
|
929
|
+
}
|
|
930
|
+
/** Check for permissive CORS configuration */
|
|
931
|
+
scanForCorsIssues(content, filePath) {
|
|
932
|
+
const findings = [];
|
|
933
|
+
const lines = content.split("\n");
|
|
934
|
+
for (let i = 0; i < lines.length; i++) {
|
|
935
|
+
const line = lines[i];
|
|
936
|
+
// origin: "*" or origin: '*'
|
|
937
|
+
if (/origin\s*:\s*["']\*["']/.test(line)) {
|
|
938
|
+
findings.push({
|
|
939
|
+
id: randomUUID(),
|
|
940
|
+
severity: "medium",
|
|
941
|
+
category: "config",
|
|
942
|
+
rule: "cors-wildcard-origin",
|
|
943
|
+
file: filePath,
|
|
944
|
+
line: i + 1,
|
|
945
|
+
message: "Wildcard CORS origin detected — allows any domain",
|
|
946
|
+
evidence: 'origin: "*"',
|
|
947
|
+
suggestion: "Restrict CORS to specific allowed origins. Use a whitelist in production.",
|
|
948
|
+
confidence: 0.9,
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
// Access-Control-Allow-Origin: *
|
|
952
|
+
if (/Access-Control-Allow-Origin["']\s*,\s*["']\*["']/.test(line) ||
|
|
953
|
+
/["']Access-Control-Allow-Origin["'].*["']\*["']/.test(line)) {
|
|
954
|
+
findings.push({
|
|
955
|
+
id: randomUUID(),
|
|
956
|
+
severity: "medium",
|
|
957
|
+
category: "config",
|
|
958
|
+
rule: "cors-header-wildcard",
|
|
959
|
+
file: filePath,
|
|
960
|
+
line: i + 1,
|
|
961
|
+
message: "Wildcard Access-Control-Allow-Origin header",
|
|
962
|
+
evidence: "Access-Control-Allow-Origin: *",
|
|
963
|
+
suggestion: "Restrict to specific origins in production.",
|
|
964
|
+
confidence: 0.9,
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
return findings;
|
|
969
|
+
}
|
|
970
|
+
/** Check for missing security headers */
|
|
971
|
+
scanForMissingSecurityHeaders(content, filePath) {
|
|
972
|
+
const findings = [];
|
|
973
|
+
// Only check files that look like server configuration
|
|
974
|
+
if (!/(express|helmet|next\.config|server\.(ts|js)|app\.(ts|js)|middleware\.(ts|js))/.test(filePath)) {
|
|
975
|
+
return findings;
|
|
976
|
+
}
|
|
977
|
+
// Check if it's an Express/server file without helmet
|
|
978
|
+
if (/\bexpress\s*\(/.test(content) && !/helmet/.test(content)) {
|
|
979
|
+
findings.push({
|
|
980
|
+
id: randomUUID(),
|
|
981
|
+
severity: "medium",
|
|
982
|
+
category: "config",
|
|
983
|
+
rule: "missing-helmet",
|
|
984
|
+
file: filePath,
|
|
985
|
+
line: 1,
|
|
986
|
+
message: "Express app without helmet — missing security headers",
|
|
987
|
+
evidence: "express() without helmet()",
|
|
988
|
+
suggestion: "Install and use 'helmet' middleware for secure HTTP headers.",
|
|
989
|
+
confidence: 0.7,
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
return findings;
|
|
993
|
+
}
|
|
994
|
+
/** Check for unsafe CSP directives */
|
|
995
|
+
scanForUnsafeCSP(content, filePath) {
|
|
996
|
+
const findings = [];
|
|
997
|
+
const lines = content.split("\n");
|
|
998
|
+
for (let i = 0; i < lines.length; i++) {
|
|
999
|
+
const line = lines[i];
|
|
1000
|
+
if (/['"]unsafe-inline['"]/.test(line) && /script-src/.test(line)) {
|
|
1001
|
+
findings.push({
|
|
1002
|
+
id: randomUUID(),
|
|
1003
|
+
severity: "medium",
|
|
1004
|
+
category: "config",
|
|
1005
|
+
rule: "csp-unsafe-inline",
|
|
1006
|
+
file: filePath,
|
|
1007
|
+
line: i + 1,
|
|
1008
|
+
message: "CSP allows 'unsafe-inline' for scripts — weakens XSS protection",
|
|
1009
|
+
evidence: "script-src 'unsafe-inline'",
|
|
1010
|
+
suggestion: "Use nonces or hashes instead of 'unsafe-inline' in script-src.",
|
|
1011
|
+
confidence: 0.8,
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
if (/['"]unsafe-eval['"]/.test(line) && /script-src/.test(line)) {
|
|
1015
|
+
findings.push({
|
|
1016
|
+
id: randomUUID(),
|
|
1017
|
+
severity: "high",
|
|
1018
|
+
category: "config",
|
|
1019
|
+
rule: "csp-unsafe-eval",
|
|
1020
|
+
file: filePath,
|
|
1021
|
+
line: i + 1,
|
|
1022
|
+
message: "CSP allows 'unsafe-eval' for scripts — enables eval() attacks",
|
|
1023
|
+
evidence: "script-src 'unsafe-eval'",
|
|
1024
|
+
suggestion: "Remove 'unsafe-eval' from CSP. Refactor code to avoid eval().",
|
|
1025
|
+
confidence: 0.9,
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
return findings;
|
|
1030
|
+
}
|
|
1031
|
+
/** Recursively discover files to scan */
|
|
1032
|
+
async discoverFiles(dir, basePath) {
|
|
1033
|
+
const root = basePath ?? dir;
|
|
1034
|
+
const files = [];
|
|
1035
|
+
try {
|
|
1036
|
+
const entries = await readdir(dir);
|
|
1037
|
+
for (const entry of entries) {
|
|
1038
|
+
const fullPath = join(dir, entry);
|
|
1039
|
+
const relPath = relative(root, fullPath);
|
|
1040
|
+
if (this.isIgnored(relPath))
|
|
1041
|
+
continue;
|
|
1042
|
+
try {
|
|
1043
|
+
const st = await stat(fullPath);
|
|
1044
|
+
if (st.isDirectory()) {
|
|
1045
|
+
files.push(...(await this.discoverFiles(fullPath, root)));
|
|
1046
|
+
}
|
|
1047
|
+
else if (st.isFile()) {
|
|
1048
|
+
const ext = extname(entry).toLowerCase();
|
|
1049
|
+
if (SCANNABLE_EXTENSIONS.has(ext) || entry === "package.json" || entry.startsWith(".env")) {
|
|
1050
|
+
files.push(relPath);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
catch {
|
|
1055
|
+
// Skip inaccessible entries
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
catch {
|
|
1060
|
+
// Skip unreadable directories
|
|
1061
|
+
}
|
|
1062
|
+
return files;
|
|
1063
|
+
}
|
|
1064
|
+
/** Simple semver-like check: is the given version within a vulnerable range */
|
|
1065
|
+
isVersionVulnerable(version, vulnerableRange) {
|
|
1066
|
+
// Parse "<X.Y.Z" pattern
|
|
1067
|
+
const ltMatch = /^<(\d+)\.(\d+)\.(\d+)$/.exec(vulnerableRange);
|
|
1068
|
+
if (!ltMatch)
|
|
1069
|
+
return false;
|
|
1070
|
+
const parts = version.split(".").map((p) => parseInt(p, 10));
|
|
1071
|
+
const vulnParts = [parseInt(ltMatch[1], 10), parseInt(ltMatch[2], 10), parseInt(ltMatch[3], 10)];
|
|
1072
|
+
// version < vulnVersion
|
|
1073
|
+
if (isNaN(parts[0]) || isNaN(parts[1]) || isNaN(parts[2]))
|
|
1074
|
+
return false;
|
|
1075
|
+
if (parts[0] < vulnParts[0])
|
|
1076
|
+
return true;
|
|
1077
|
+
if (parts[0] > vulnParts[0])
|
|
1078
|
+
return false;
|
|
1079
|
+
if (parts[1] < vulnParts[1])
|
|
1080
|
+
return true;
|
|
1081
|
+
if (parts[1] > vulnParts[1])
|
|
1082
|
+
return false;
|
|
1083
|
+
return parts[2] < vulnParts[2];
|
|
1084
|
+
}
|
|
1085
|
+
/** Find the line number of a dependency in package.json */
|
|
1086
|
+
findDepLine(content, depName) {
|
|
1087
|
+
const lines = content.split("\n");
|
|
1088
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1089
|
+
if (lines[i].includes(`"${depName}"`))
|
|
1090
|
+
return i + 1;
|
|
1091
|
+
}
|
|
1092
|
+
return 1;
|
|
1093
|
+
}
|
|
1094
|
+
/** Check if a file is a code file worth scanning */
|
|
1095
|
+
isCodeFile(filePath) {
|
|
1096
|
+
const ext = extname(filePath).toLowerCase();
|
|
1097
|
+
return [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(ext);
|
|
1098
|
+
}
|
|
1099
|
+
/** Get confidence score based on pattern category */
|
|
1100
|
+
patternConfidence(pattern) {
|
|
1101
|
+
switch (pattern.category) {
|
|
1102
|
+
case "secret":
|
|
1103
|
+
// Specific token patterns get higher confidence
|
|
1104
|
+
if (pattern.name.includes("aws-access") || pattern.name.includes("github-token") ||
|
|
1105
|
+
pattern.name.includes("stripe") || pattern.name.includes("slack") ||
|
|
1106
|
+
pattern.name.includes("private-key")) {
|
|
1107
|
+
return 0.95;
|
|
1108
|
+
}
|
|
1109
|
+
return 0.7;
|
|
1110
|
+
case "injection":
|
|
1111
|
+
return pattern.name.includes("eval") ? 0.9 : 0.75;
|
|
1112
|
+
case "xss":
|
|
1113
|
+
return 0.8;
|
|
1114
|
+
case "dependency":
|
|
1115
|
+
return 0.9;
|
|
1116
|
+
case "config":
|
|
1117
|
+
return 0.8;
|
|
1118
|
+
case "crypto":
|
|
1119
|
+
return pattern.name === "weak-random" ? 0.5 : 0.85;
|
|
1120
|
+
case "traversal":
|
|
1121
|
+
return 0.7;
|
|
1122
|
+
case "ssrf":
|
|
1123
|
+
return 0.7;
|
|
1124
|
+
default:
|
|
1125
|
+
return 0.6;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
//# sourceMappingURL=security-scanner.js.map
|