neuro-cli 4.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 +21 -0
- package/README.md +510 -0
- package/dist/advisor/advisor.d.ts +50 -0
- package/dist/advisor/advisor.js +178 -0
- package/dist/agents/base.d.ts +62 -0
- package/dist/agents/base.js +215 -0
- package/dist/agents/orchestrator.d.ts +46 -0
- package/dist/agents/orchestrator.js +192 -0
- package/dist/agents/team.d.ts +51 -0
- package/dist/agents/team.js +210 -0
- package/dist/api/models.d.ts +23 -0
- package/dist/api/models.js +514 -0
- package/dist/api/ollama.d.ts +153 -0
- package/dist/api/ollama.js +751 -0
- package/dist/api/openrouter.d.ts +55 -0
- package/dist/api/openrouter.js +223 -0
- package/dist/commands/commands.d.ts +43 -0
- package/dist/commands/commands.js +308 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.js +311 -0
- package/dist/context/compaction.d.ts +54 -0
- package/dist/context/compaction.js +251 -0
- package/dist/context/custom-agents.d.ts +107 -0
- package/dist/context/custom-agents.js +397 -0
- package/dist/context/custom-tools.d.ts +120 -0
- package/dist/context/custom-tools.js +564 -0
- package/dist/context/git-checkpoint.d.ts +52 -0
- package/dist/context/git-checkpoint.js +240 -0
- package/dist/context/neuro-md.d.ts +48 -0
- package/dist/context/neuro-md.js +202 -0
- package/dist/context/neuroignore.d.ts +102 -0
- package/dist/context/neuroignore.js +441 -0
- package/dist/context/repo-map.d.ts +38 -0
- package/dist/context/repo-map.js +220 -0
- package/dist/context/skill-standard.d.ts +262 -0
- package/dist/context/skill-standard.js +1156 -0
- package/dist/context/skill-system.d.ts +75 -0
- package/dist/context/skill-system.js +578 -0
- package/dist/context/tree-sitter.d.ts +99 -0
- package/dist/context/tree-sitter.js +1956 -0
- package/dist/core/acp.d.ts +325 -0
- package/dist/core/acp.js +1498 -0
- package/dist/core/api-server.d.ts +143 -0
- package/dist/core/api-server.js +550 -0
- package/dist/core/approval.d.ts +81 -0
- package/dist/core/approval.js +432 -0
- package/dist/core/auto-compact.d.ts +127 -0
- package/dist/core/auto-compact.js +436 -0
- package/dist/core/auto-mode.d.ts +232 -0
- package/dist/core/auto-mode.js +831 -0
- package/dist/core/background-session.d.ts +166 -0
- package/dist/core/background-session.js +696 -0
- package/dist/core/cicd.d.ts +257 -0
- package/dist/core/cicd.js +1443 -0
- package/dist/core/cloud-sync.d.ts +156 -0
- package/dist/core/cloud-sync.js +582 -0
- package/dist/core/code-review.d.ts +132 -0
- package/dist/core/code-review.js +1191 -0
- package/dist/core/completion.d.ts +49 -0
- package/dist/core/completion.js +384 -0
- package/dist/core/context.d.ts +38 -0
- package/dist/core/context.js +144 -0
- package/dist/core/diff-preview.d.ts +35 -0
- package/dist/core/diff-preview.js +173 -0
- package/dist/core/doom-loop.d.ts +51 -0
- package/dist/core/doom-loop.js +179 -0
- package/dist/core/engine.d.ts +183 -0
- package/dist/core/engine.js +942 -0
- package/dist/core/extended-thinking.d.ts +103 -0
- package/dist/core/extended-thinking.js +269 -0
- package/dist/core/fallback.d.ts +54 -0
- package/dist/core/fallback.js +104 -0
- package/dist/core/git-worktree.d.ts +139 -0
- package/dist/core/git-worktree.js +614 -0
- package/dist/core/headless.d.ts +30 -0
- package/dist/core/headless.js +101 -0
- package/dist/core/i18n.d.ts +83 -0
- package/dist/core/i18n.js +739 -0
- package/dist/core/linting.d.ts +129 -0
- package/dist/core/linting.js +699 -0
- package/dist/core/model-router.d.ts +109 -0
- package/dist/core/model-router.js +581 -0
- package/dist/core/multi-model.d.ts +243 -0
- package/dist/core/multi-model.js +1099 -0
- package/dist/core/multi-session.d.ts +144 -0
- package/dist/core/multi-session.js +442 -0
- package/dist/core/multimodal.d.ts +125 -0
- package/dist/core/multimodal.js +286 -0
- package/dist/core/observability.d.ts +93 -0
- package/dist/core/observability.js +737 -0
- package/dist/core/os-sandbox.d.ts +122 -0
- package/dist/core/os-sandbox.js +1193 -0
- package/dist/core/outcome-grading.d.ts +228 -0
- package/dist/core/outcome-grading.js +1123 -0
- package/dist/core/output-styles.d.ts +57 -0
- package/dist/core/output-styles.js +382 -0
- package/dist/core/parallel-agents.d.ts +183 -0
- package/dist/core/parallel-agents.js +563 -0
- package/dist/core/plugin-bundle.d.ts +236 -0
- package/dist/core/plugin-bundle.js +887 -0
- package/dist/core/plugin-sdk.d.ts +139 -0
- package/dist/core/plugin-sdk.js +273 -0
- package/dist/core/prompt-cache.d.ts +163 -0
- package/dist/core/prompt-cache.js +599 -0
- package/dist/core/sandbox.d.ts +127 -0
- package/dist/core/sandbox.js +369 -0
- package/dist/core/scheduled-tasks.d.ts +151 -0
- package/dist/core/scheduled-tasks.js +502 -0
- package/dist/core/security-scanner.d.ts +160 -0
- package/dist/core/security-scanner.js +1494 -0
- package/dist/core/session.d.ts +83 -0
- package/dist/core/session.js +269 -0
- package/dist/core/shell-completion.d.ts +51 -0
- package/dist/core/shell-completion.js +674 -0
- package/dist/core/smart-monitor.d.ts +146 -0
- package/dist/core/smart-monitor.js +1199 -0
- package/dist/core/spec-driven.d.ts +233 -0
- package/dist/core/spec-driven.js +1485 -0
- package/dist/core/spending-warnings.d.ts +123 -0
- package/dist/core/spending-warnings.js +456 -0
- package/dist/core/sub-agent.d.ts +298 -0
- package/dist/core/sub-agent.js +1023 -0
- package/dist/core/telemetry.d.ts +157 -0
- package/dist/core/telemetry.js +412 -0
- package/dist/core/terminal-ux.d.ts +134 -0
- package/dist/core/terminal-ux.js +649 -0
- package/dist/core/testing.d.ts +146 -0
- package/dist/core/testing.js +1200 -0
- package/dist/core/types.d.ts +439 -0
- package/dist/core/types.js +6 -0
- package/dist/core/undo-redo.d.ts +112 -0
- package/dist/core/undo-redo.js +290 -0
- package/dist/core/updater.d.ts +159 -0
- package/dist/core/updater.js +608 -0
- package/dist/core/vim-mode.d.ts +151 -0
- package/dist/core/vim-mode.js +771 -0
- package/dist/core/voice.d.ts +137 -0
- package/dist/core/voice.js +538 -0
- package/dist/core/web-dashboard.d.ts +109 -0
- package/dist/core/web-dashboard.js +484 -0
- package/dist/hooks/hooks.d.ts +74 -0
- package/dist/hooks/hooks.js +160 -0
- package/dist/hooks/llm-evaluator.d.ts +165 -0
- package/dist/hooks/llm-evaluator.js +560 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1186 -0
- package/dist/lsp/lsp-manager.d.ts +63 -0
- package/dist/lsp/lsp-manager.js +351 -0
- package/dist/mcp/client.d.ts +133 -0
- package/dist/mcp/client.js +684 -0
- package/dist/mcp/mcp-apps.d.ts +70 -0
- package/dist/mcp/mcp-apps.js +1007 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +195 -0
- package/dist/tools/browser.d.ts +92 -0
- package/dist/tools/browser.js +1570 -0
- package/dist/tools/extended.d.ts +6 -0
- package/dist/tools/extended.js +191 -0
- package/dist/tools/file.d.ts +10 -0
- package/dist/tools/file.js +382 -0
- package/dist/tools/github.d.ts +389 -0
- package/dist/tools/github.js +759 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/memory.d.ts +6 -0
- package/dist/tools/memory.js +197 -0
- package/dist/tools/registry.d.ts +29 -0
- package/dist/tools/registry.js +64 -0
- package/dist/tools/web.d.ts +6 -0
- package/dist/tools/web.js +150 -0
- package/dist/ui/renderer.d.ts +97 -0
- package/dist/ui/renderer.js +279 -0
- package/dist/ui/theme.d.ts +27 -0
- package/dist/ui/theme.js +106 -0
- package/package.json +83 -0
|
@@ -0,0 +1,1494 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// NeuroCLI - Security Scanner
|
|
3
|
+
// Detects vulnerabilities, hardcoded secrets, and security
|
|
4
|
+
// issues across codebases using regex-based pattern matching.
|
|
5
|
+
// No external dependencies — Node.js built-in modules only.
|
|
6
|
+
// ============================================================
|
|
7
|
+
import { join, resolve, relative, extname, dirname, basename, sep, } from 'path';
|
|
8
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, } from 'fs';
|
|
9
|
+
// -----------------------------------------------------------
|
|
10
|
+
// Constants
|
|
11
|
+
// -----------------------------------------------------------
|
|
12
|
+
const IGNORE_FILE_NAME = '.neuro-security-ignore';
|
|
13
|
+
const SEVERITY_WEIGHTS = {
|
|
14
|
+
critical: 9.0,
|
|
15
|
+
high: 7.0,
|
|
16
|
+
medium: 5.0,
|
|
17
|
+
low: 3.0,
|
|
18
|
+
info: 1.0,
|
|
19
|
+
};
|
|
20
|
+
const DEFAULT_FILE_EXTENSIONS = [
|
|
21
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
22
|
+
'.py', '.rb', '.go', '.rs', '.java', '.kt',
|
|
23
|
+
'.c', '.cpp', '.h', '.hpp',
|
|
24
|
+
'.cs', '.swift', '.php', '.pl',
|
|
25
|
+
'.sh', '.bash', '.zsh', '.fish',
|
|
26
|
+
'.yaml', '.yml', '.json', '.toml', '.ini', '.cfg', '.env',
|
|
27
|
+
'.html', '.htm', '.xml', '.svg',
|
|
28
|
+
'.sql', '.graphql', '.gql',
|
|
29
|
+
'.dockerfile', '.tf', '.hcl',
|
|
30
|
+
'.md', '.rst',
|
|
31
|
+
];
|
|
32
|
+
const DEFAULT_EXCLUDE_PATTERNS = [
|
|
33
|
+
'**/node_modules/**',
|
|
34
|
+
'**/.git/**',
|
|
35
|
+
'**/dist/**',
|
|
36
|
+
'**/build/**',
|
|
37
|
+
'**/coverage/**',
|
|
38
|
+
'**/.next/**',
|
|
39
|
+
'**/vendor/**',
|
|
40
|
+
'**/__pycache__/**',
|
|
41
|
+
'**/.cache/**',
|
|
42
|
+
'**/target/**',
|
|
43
|
+
'**/*.min.js',
|
|
44
|
+
'**/*.min.css',
|
|
45
|
+
'**/*.map',
|
|
46
|
+
'**/package-lock.json',
|
|
47
|
+
'**/yarn.lock',
|
|
48
|
+
'**/bun.lock',
|
|
49
|
+
];
|
|
50
|
+
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2 MB
|
|
51
|
+
// -----------------------------------------------------------
|
|
52
|
+
// Built-in security rules (40+)
|
|
53
|
+
// -----------------------------------------------------------
|
|
54
|
+
function getDefaultRules() {
|
|
55
|
+
return [
|
|
56
|
+
// ============================================
|
|
57
|
+
// Hardcoded Secrets (rules 001-010)
|
|
58
|
+
// ============================================
|
|
59
|
+
{
|
|
60
|
+
id: 'SEC-001',
|
|
61
|
+
name: 'Hardcoded API Key',
|
|
62
|
+
category: 'hardcoded-secrets',
|
|
63
|
+
severity: 'critical',
|
|
64
|
+
pattern: '(?i)(api[_-]?key|apikey|api[_-]?secret)\\s*[:=]\\s*["\'][A-Za-z0-9_\\-]{16,}["\']',
|
|
65
|
+
description: 'An API key appears to be hardcoded in the source code. Exposing secrets in code can lead to unauthorized access.',
|
|
66
|
+
remediation: 'Store API keys in environment variables, secret managers, or encrypted configuration files. Never commit secrets to version control.',
|
|
67
|
+
cwe: 'CWE-798',
|
|
68
|
+
owasp: 'A02:2021-Cryptographic Failures',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'SEC-002',
|
|
72
|
+
name: 'Hardcoded Password',
|
|
73
|
+
category: 'hardcoded-secrets',
|
|
74
|
+
severity: 'critical',
|
|
75
|
+
pattern: '(?i)(password|passwd|pwd)\\s*[:=]\\s*["\'][^"\']{4,}["\']',
|
|
76
|
+
description: 'A password appears to be hardcoded in the source code. Hardcoded credentials are a critical security risk.',
|
|
77
|
+
remediation: 'Use environment variables or a secrets manager to handle passwords. Remove the hardcoded value and rotate the compromised credential immediately.',
|
|
78
|
+
cwe: 'CWE-798',
|
|
79
|
+
owasp: 'A07:2021-Identification and Authentication Failures',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'SEC-003',
|
|
83
|
+
name: 'Hardcoded Secret Token',
|
|
84
|
+
category: 'hardcoded-secrets',
|
|
85
|
+
severity: 'critical',
|
|
86
|
+
pattern: '(?i)(secret|auth[_-]?token|access[_-]?token|refresh[_-]?token|private[_-]?key)\\s*[:=]\\s*["\'][A-Za-z0-9_\\-+/=]{16,}["\']',
|
|
87
|
+
description: 'A secret token appears to be hardcoded in source code. Tokens should never be embedded in code.',
|
|
88
|
+
remediation: 'Move tokens to environment variables or a secure secrets management solution such as HashiCorp Vault, AWS Secrets Manager, or Doppler.',
|
|
89
|
+
cwe: 'CWE-798',
|
|
90
|
+
owasp: 'A02:2021-Cryptographic Failures',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'SEC-004',
|
|
94
|
+
name: 'Hardcoded Private Key',
|
|
95
|
+
category: 'hardcoded-secrets',
|
|
96
|
+
severity: 'critical',
|
|
97
|
+
pattern: '-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----',
|
|
98
|
+
description: 'A private key is hardcoded in the source code. Compromised private keys can lead to total system compromise.',
|
|
99
|
+
remediation: 'Never commit private keys. Store them in a secrets manager, use SSH agent forwarding, or leverage cloud-managed key services.',
|
|
100
|
+
cwe: 'CWE-798',
|
|
101
|
+
owasp: 'A02:2021-Cryptographic Failures',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'SEC-005',
|
|
105
|
+
name: 'Hardcoded JWT Secret',
|
|
106
|
+
category: 'hardcoded-secrets',
|
|
107
|
+
severity: 'critical',
|
|
108
|
+
pattern: '(?i)(jwt[_-]?secret|jwt[_-]?key|jsonwebtoken[_-]?secret)\\s*[:=]\\s*["\'][^"\']{8,}["\']',
|
|
109
|
+
description: 'A JWT signing secret appears to be hardcoded. An attacker who discovers it can forge arbitrary tokens.',
|
|
110
|
+
remediation: 'Use a strong, randomly generated secret from an environment variable or secrets manager. Rotate the secret immediately if it was ever committed.',
|
|
111
|
+
cwe: 'CWE-798',
|
|
112
|
+
owasp: 'A02:2021-Cryptographic Failures',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'SEC-006',
|
|
116
|
+
name: 'AWS Access Key ID',
|
|
117
|
+
category: 'hardcoded-secrets',
|
|
118
|
+
severity: 'critical',
|
|
119
|
+
pattern: '(?:A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}',
|
|
120
|
+
description: 'An AWS Access Key ID was detected. Exposed AWS credentials can lead to unauthorized cloud resource access.',
|
|
121
|
+
remediation: 'Remove the key from code, rotate it in the AWS console, and use IAM roles or environment variables instead.',
|
|
122
|
+
cwe: 'CWE-798',
|
|
123
|
+
owasp: 'A07:2021-Identification and Authentication Failures',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'SEC-007',
|
|
127
|
+
name: 'AWS Secret Access Key',
|
|
128
|
+
category: 'hardcoded-secrets',
|
|
129
|
+
severity: 'critical',
|
|
130
|
+
pattern: '(?i)aws[_-]?secret[_-]?access[_-]?key\\s*[:=]\\s*["\'][A-Za-z0-9/+=]{40}["\']',
|
|
131
|
+
description: 'An AWS Secret Access Key was detected. This is extremely sensitive and grants full access to the associated AWS account.',
|
|
132
|
+
remediation: 'Remove the key immediately, rotate credentials, and use IAM roles or secure credential management.',
|
|
133
|
+
cwe: 'CWE-798',
|
|
134
|
+
owasp: 'A07:2021-Identification and Authentication Failures',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'SEC-008',
|
|
138
|
+
name: 'Generic Secret Assignment',
|
|
139
|
+
category: 'hardcoded-secrets',
|
|
140
|
+
severity: 'high',
|
|
141
|
+
pattern: '(?i)(secret|token|key|credential|auth)\\s*[:=]\\s*["\'][A-Za-z0-9_\\-+/=]{24,}["\']',
|
|
142
|
+
description: 'A generic secret value appears to be assigned directly in code. This may indicate a leaked credential.',
|
|
143
|
+
remediation: 'Move the secret to an environment variable or secrets manager. Verify the pattern is not a false positive before ignoring.',
|
|
144
|
+
cwe: 'CWE-798',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'SEC-009',
|
|
148
|
+
name: 'Database Connection String with Password',
|
|
149
|
+
category: 'hardcoded-secrets',
|
|
150
|
+
severity: 'critical',
|
|
151
|
+
pattern: '(?i)(?:mysql|postgres|mongodb|redis|mssql|oracle)://[^:\\s]+:[^@\\s]+@[\\w.]+(?::\\d+)?',
|
|
152
|
+
description: 'A database connection string containing an embedded password was detected. This exposes database credentials.',
|
|
153
|
+
remediation: 'Use environment variables for connection strings, store the password separately, or use IAM authentication where available.',
|
|
154
|
+
cwe: 'CWE-798',
|
|
155
|
+
owasp: 'A07:2021-Identification and Authentication Failures',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: 'SEC-010',
|
|
159
|
+
name: 'OAuth Client Secret',
|
|
160
|
+
category: 'hardcoded-secrets',
|
|
161
|
+
severity: 'critical',
|
|
162
|
+
pattern: '(?i)(client[_-]?secret|oauth[_-]?secret)\\s*[:=]\\s*["\'][A-Za-z0-9_\\-]{16,}["\']',
|
|
163
|
+
description: 'An OAuth client secret appears to be hardcoded. Compromised OAuth secrets can allow impersonation of your application.',
|
|
164
|
+
remediation: 'Store OAuth secrets in environment variables or a secrets manager. Never commit them to version control.',
|
|
165
|
+
cwe: 'CWE-798',
|
|
166
|
+
owasp: 'A07:2021-Identification and Authentication Failures',
|
|
167
|
+
},
|
|
168
|
+
// ============================================
|
|
169
|
+
// Injection (rules 011-018)
|
|
170
|
+
// ============================================
|
|
171
|
+
{
|
|
172
|
+
id: 'SEC-011',
|
|
173
|
+
name: 'SQL Injection Risk',
|
|
174
|
+
category: 'injection',
|
|
175
|
+
severity: 'critical',
|
|
176
|
+
pattern: '(?i)(?:execute|query|raw|run)\\s*\\(\\s*["\'].*\\+|(?i)(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE)\\s+.*\\$\\{|(?i)string\\.format\\s*\\(.*(?:SELECT|INSERT|UPDATE|DELETE)',
|
|
177
|
+
description: 'Potential SQL injection vulnerability detected. Unsanitized input may be concatenated into SQL queries.',
|
|
178
|
+
remediation: 'Use parameterized queries (prepared statements) instead of string concatenation or interpolation. Validate and sanitize all user inputs.',
|
|
179
|
+
cwe: 'CWE-89',
|
|
180
|
+
owasp: 'A03:2021-Injection',
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: 'SEC-012',
|
|
184
|
+
name: 'Command Injection Risk',
|
|
185
|
+
category: 'injection',
|
|
186
|
+
severity: 'critical',
|
|
187
|
+
pattern: '(?i)(?:exec|execSync|spawn|execFile|system|popen|subprocess|os\\.system|os\\.popen|shell_exec)\\s*\\(.*(?:\\$|`|\\+|format|%)',
|
|
188
|
+
description: 'Potential command injection vulnerability. User input may be passed to a system shell command.',
|
|
189
|
+
remediation: 'Avoid passing user input to shell commands. If necessary, use strict allowlists and parameterized execution (e.g., execFile with argument arrays).',
|
|
190
|
+
cwe: 'CWE-78',
|
|
191
|
+
owasp: 'A03:2021-Injection',
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: 'SEC-013',
|
|
195
|
+
name: 'Cross-Site Scripting (XSS)',
|
|
196
|
+
category: 'injection',
|
|
197
|
+
severity: 'high',
|
|
198
|
+
pattern: '(?i)(?:innerHTML|outerHTML|document\\.write|dangerouslySetInnerHTML|v-html|\\[innerHTML\\])\\s*(?:=|\\()|(?i)\\b(?:eval|Function)\\s*\\(',
|
|
199
|
+
description: 'Potential Cross-Site Scripting (XSS) vulnerability. Unsanitized data may be inserted into the DOM as raw HTML.',
|
|
200
|
+
remediation: 'Use textContent instead of innerHTML. Sanitize HTML with a library like DOMPurify. Avoid eval() and new Function() with untrusted data.',
|
|
201
|
+
cwe: 'CWE-79',
|
|
202
|
+
owasp: 'A03:2021-Injection',
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: 'SEC-014',
|
|
206
|
+
name: 'Server-Side Request Forgery (SSRF)',
|
|
207
|
+
category: 'injection',
|
|
208
|
+
severity: 'high',
|
|
209
|
+
pattern: '(?i)(?:fetch|axios|request|http\\.get|https\\.get|urllib|requests\\.get)\\s*\\(\\s*(?:user|input|param|query|req|request)\\.',
|
|
210
|
+
description: 'Potential SSRF vulnerability. User-controlled input may be used to construct server-side requests.',
|
|
211
|
+
remediation: 'Validate and restrict URLs to an allowlist of domains. Block requests to internal IP ranges (10.x, 172.16-31.x, 192.168.x, 127.x, 169.254.x).',
|
|
212
|
+
cwe: 'CWE-918',
|
|
213
|
+
owasp: 'A10:2021-Server-Side Request Forgery',
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: 'SEC-015',
|
|
217
|
+
name: 'LDAP Injection Risk',
|
|
218
|
+
category: 'injection',
|
|
219
|
+
severity: 'high',
|
|
220
|
+
pattern: '(?i)ldap_search|ldap_bind|ldap_query|DirectorySearcher.*Filter\\s*=',
|
|
221
|
+
description: 'Potential LDAP injection vulnerability. Unsanitized input may be used in LDAP queries.',
|
|
222
|
+
remediation: 'Use parameterized LDAP queries. Escape special LDAP characters (*, (, ), \\, /, NUL) in user input before incorporating into queries.',
|
|
223
|
+
cwe: 'CWE-90',
|
|
224
|
+
owasp: 'A03:2021-Injection',
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: 'SEC-016',
|
|
228
|
+
name: 'Template Injection Risk',
|
|
229
|
+
category: 'injection',
|
|
230
|
+
severity: 'high',
|
|
231
|
+
pattern: '(?i)(?:render_template_string|jinja2\\.Template|eval\\s*\\(.*template|Template\\s*\\(.*(?:request|user|input|param))',
|
|
232
|
+
description: 'Potential server-side template injection (SSTI). User input may be interpreted as template code.',
|
|
233
|
+
remediation: 'Never pass user input directly into template rendering engines. Use sandboxed template environments and separate data from template logic.',
|
|
234
|
+
cwe: 'CWE-1336',
|
|
235
|
+
owasp: 'A03:2021-Injection',
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: 'SEC-017',
|
|
239
|
+
name: 'Path Traversal Risk',
|
|
240
|
+
category: 'injection',
|
|
241
|
+
severity: 'high',
|
|
242
|
+
pattern: '(?i)(?:readFile|writeFile|createReadStream|createWriteStream|fs\\.read|fs\\.write|open|fopen)\\s*\\(.*(?:\\.\\.\\/|\\.\\.\\\\\\\\|req\\.|params|query)',
|
|
243
|
+
description: 'Potential path traversal vulnerability. User input may be used to construct file paths without validation.',
|
|
244
|
+
remediation: 'Validate and normalize file paths. Use path.resolve() and verify the result is within the intended directory. Never trust user-provided filenames directly.',
|
|
245
|
+
cwe: 'CWE-22',
|
|
246
|
+
owasp: 'A01:2021-Broken Access Control',
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
id: 'SEC-018',
|
|
250
|
+
name: 'XML External Entity (XXE)',
|
|
251
|
+
category: 'injection',
|
|
252
|
+
severity: 'high',
|
|
253
|
+
pattern: '(?i)<!DOCTYPE\\s+\\w+\\s+SYSTEM|<!ENTITY\\s+\\w+\\s+SYSTEM|XmlParser|SAXParser|DocumentBuilder|xml\\.etree\\.ElementTree\\.parse',
|
|
254
|
+
description: 'Potential XXE vulnerability. XML parsing may process external entity references, allowing file disclosure or SSRF.',
|
|
255
|
+
remediation: 'Disable external entity processing in your XML parser. Use JSON instead of XML where possible. Configure the parser with FEATURE_SECURE_PROCESSING enabled.',
|
|
256
|
+
cwe: 'CWE-611',
|
|
257
|
+
owasp: 'A05:2021-Security Misconfiguration',
|
|
258
|
+
},
|
|
259
|
+
// ============================================
|
|
260
|
+
// Authentication (rules 019-024)
|
|
261
|
+
// ============================================
|
|
262
|
+
{
|
|
263
|
+
id: 'SEC-019',
|
|
264
|
+
name: 'Weak Cryptographic Hash (MD5)',
|
|
265
|
+
category: 'authentication',
|
|
266
|
+
severity: 'high',
|
|
267
|
+
pattern: '(?i)(?:md5|MD5Init|MD5Update|MD5Final|createHash\\s*\\(\\s*["\']md5["\'])',
|
|
268
|
+
description: 'MD5 is a weak hashing algorithm vulnerable to collision attacks. It should not be used for security purposes.',
|
|
269
|
+
remediation: 'Use SHA-256 or stronger for hashing. For password storage, use bcrypt, scrypt, or Argon2 with proper salting.',
|
|
270
|
+
cwe: 'CWE-328',
|
|
271
|
+
owasp: 'A02:2021-Cryptographic Failures',
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
id: 'SEC-020',
|
|
275
|
+
name: 'Weak Cryptographic Hash (SHA1)',
|
|
276
|
+
category: 'authentication',
|
|
277
|
+
severity: 'medium',
|
|
278
|
+
pattern: '(?i)(?:sha1|sha-1|createHash\\s*\\(\\s*["\']sha1["\']|SHA1_Init|SHA1_Update)',
|
|
279
|
+
description: 'SHA-1 is considered weak and vulnerable to collision attacks. It should not be used for security-critical purposes.',
|
|
280
|
+
remediation: 'Migrate to SHA-256 or SHA-3 for cryptographic hashing. SHA-1 may still be acceptable for non-security checksums.',
|
|
281
|
+
cwe: 'CWE-328',
|
|
282
|
+
owasp: 'A02:2021-Cryptographic Failures',
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
id: 'SEC-021',
|
|
286
|
+
name: 'Insecure Session Configuration',
|
|
287
|
+
category: 'authentication',
|
|
288
|
+
severity: 'high',
|
|
289
|
+
pattern: '(?i)(?:cookie|session)\\s*[:=].*(?:secure\\s*:\\s*false|httpOnly\\s*:\\s*false|sameSite\\s*:\\s*["\']none["\'])',
|
|
290
|
+
description: 'Session cookies may be configured without secure or httpOnly flags, or with SameSite=None, making them vulnerable to interception.',
|
|
291
|
+
remediation: 'Set Secure: true (HTTPS only), HttpOnly: true (no JS access), and SameSite: Strict or Lax on all session cookies.',
|
|
292
|
+
cwe: 'CWE-614',
|
|
293
|
+
owasp: 'A07:2021-Identification and Authentication Failures',
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
id: 'SEC-022',
|
|
297
|
+
name: 'Missing Authentication Check',
|
|
298
|
+
category: 'authentication',
|
|
299
|
+
severity: 'high',
|
|
300
|
+
pattern: '(?i)(?:app\\.(?:get|post|put|delete|patch)|router\\.(?:get|post|put|delete|patch))\\s*\\([\'"/][^)]*\\)\\s*(?:=>|\\{)(?!.*(?:auth|authenticate|verify|jwt|session|isLoggedIn|requireAuth))',
|
|
301
|
+
description: 'A route handler appears to lack authentication middleware, potentially exposing it to unauthorized access.',
|
|
302
|
+
remediation: 'Apply authentication middleware to all sensitive routes. Use a global auth middleware and explicitly mark public routes.',
|
|
303
|
+
cwe: 'CWE-306',
|
|
304
|
+
owasp: 'A07:2021-Identification and Authentication Failures',
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
id: 'SEC-023',
|
|
308
|
+
name: 'Hardcoded Credentials in URL',
|
|
309
|
+
category: 'authentication',
|
|
310
|
+
severity: 'critical',
|
|
311
|
+
pattern: '(?i)\\b(?:https?|ftp|jdbc)://[^:\\s]+:[^@\\s]+@[\\w.\\-]+',
|
|
312
|
+
description: 'A URL containing embedded credentials was detected. Credentials in URLs can be logged, cached, or leaked via referrer headers.',
|
|
313
|
+
remediation: 'Never embed credentials in URLs. Use HTTP headers, environment variables, or connection parameters to pass credentials.',
|
|
314
|
+
cwe: 'CWE-798',
|
|
315
|
+
owasp: 'A07:2021-Identification and Authentication Failures',
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
id: 'SEC-024',
|
|
319
|
+
name: 'Weak Password Hashing',
|
|
320
|
+
category: 'authentication',
|
|
321
|
+
severity: 'critical',
|
|
322
|
+
pattern: '(?i)(?:hash\\s*\\(.*password|password.*hash\\s*\\(|createHash\\s*\\(.*password|crypto\\.createHash)',
|
|
323
|
+
description: 'A password appears to be hashed using a general-purpose hash function instead of a dedicated password hashing algorithm.',
|
|
324
|
+
remediation: 'Use bcrypt, scrypt, or Argon2 for password hashing. These algorithms incorporate salting and key stretching by design.',
|
|
325
|
+
cwe: 'CWE-916',
|
|
326
|
+
owasp: 'A02:2021-Cryptographic Failures',
|
|
327
|
+
},
|
|
328
|
+
// ============================================
|
|
329
|
+
// Configuration (rules 025-031)
|
|
330
|
+
// ============================================
|
|
331
|
+
{
|
|
332
|
+
id: 'SEC-025',
|
|
333
|
+
name: 'CORS Wildcard Origin',
|
|
334
|
+
category: 'configuration',
|
|
335
|
+
severity: 'high',
|
|
336
|
+
pattern: '(?i)(?:Access-Control-Allow-Origin|cors)\\s*[:=]\\s*["\']\\*["\']|(?i)origin\\s*:\\s*["\']\\*["\']',
|
|
337
|
+
description: 'CORS is configured to allow all origins (*). This can enable cross-origin attacks from any domain.',
|
|
338
|
+
remediation: 'Specify explicit allowed origins instead of using a wildcard. Only allow trusted domains that need cross-origin access.',
|
|
339
|
+
cwe: 'CWE-942',
|
|
340
|
+
owasp: 'A05:2021-Security Misconfiguration',
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
id: 'SEC-026',
|
|
344
|
+
name: 'Debug Mode Enabled',
|
|
345
|
+
category: 'configuration',
|
|
346
|
+
severity: 'medium',
|
|
347
|
+
pattern: '(?i)(?:DEBUG|debug)\\s*[:=]\\s*(?:true|True|TRUE|1)|(?i)app\\.debug\\s*=\\s*(?:true|True)|(?i)DEBUG_MODE\\s*=\\s*(?:true|True|1)',
|
|
348
|
+
description: 'Debug mode appears to be enabled. This can expose sensitive information through error pages and debugging endpoints.',
|
|
349
|
+
remediation: 'Disable debug mode in production. Use environment-based configuration to ensure debug is only active in development.',
|
|
350
|
+
cwe: 'CWE-489',
|
|
351
|
+
owasp: 'A05:2021-Security Misconfiguration',
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
id: 'SEC-027',
|
|
355
|
+
name: 'Insecure HTTP Header Configuration',
|
|
356
|
+
category: 'configuration',
|
|
357
|
+
severity: 'medium',
|
|
358
|
+
pattern: '(?i)helmet\\s*\\(\\s*\\)\\s*;(?!)|(?i)(?:X-Frame-Options|X-Content-Type-Options|X-XSS-Protection)\\s*[:=]\\s*["\']?(?:off|disabled|0)',
|
|
359
|
+
description: 'Security headers appear to be disabled or misconfigured, potentially leaving the application vulnerable to clickjacking, MIME sniffing, or XSS.',
|
|
360
|
+
remediation: 'Enable security headers: X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Content-Security-Policy, and use Helmet.js in Node.js.',
|
|
361
|
+
cwe: 'CWE-693',
|
|
362
|
+
owasp: 'A05:2021-Security Misconfiguration',
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
id: 'SEC-028',
|
|
366
|
+
name: 'TLS/SSL Verification Disabled',
|
|
367
|
+
category: 'configuration',
|
|
368
|
+
severity: 'critical',
|
|
369
|
+
pattern: '(?i)(?:rejectUnauthorized|verify|SSL_VERIFY|CURLOPT_SSL_VERIFYPEER|checkServerIdentity)\\s*[:=]\\s*(?:false|False|FALSE|0|no)',
|
|
370
|
+
description: 'TLS/SSL certificate verification is disabled. This allows man-in-the-middle attacks on HTTPS connections.',
|
|
371
|
+
remediation: 'Never disable certificate verification in production. If needed for development, use environment-based configuration to ensure it is only disabled locally.',
|
|
372
|
+
cwe: 'CWE-295',
|
|
373
|
+
owasp: 'A02:2021-Cryptographic Failures',
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
id: 'SEC-029',
|
|
377
|
+
name: 'Hardcoded IP Address',
|
|
378
|
+
category: 'configuration',
|
|
379
|
+
severity: 'low',
|
|
380
|
+
pattern: '\\b(?:(?:25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)\\b',
|
|
381
|
+
description: 'A hardcoded IP address was detected. Hardcoded IPs reduce flexibility and may indicate internal network information disclosure.',
|
|
382
|
+
remediation: 'Use DNS hostnames instead of hardcoded IPs. Store IP addresses in configuration files or environment variables.',
|
|
383
|
+
cwe: 'CWE-778',
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
id: 'SEC-030',
|
|
387
|
+
name: 'Overly Permissive File Mode',
|
|
388
|
+
category: 'configuration',
|
|
389
|
+
severity: 'medium',
|
|
390
|
+
pattern: '(?i)(?:chmod|fs\\.chmod|os\\.chmod)\\s*\\(.*(?:0?777|0?666|"rwxrwxrwx"|rwx)|(?i)mode\\s*:\\s*0?777',
|
|
391
|
+
description: 'A file or directory is being set with overly permissive permissions (777 or 666), allowing unauthorized read/write/execute access.',
|
|
392
|
+
remediation: 'Use the principle of least privilege. Set files to 644 (owner read/write, others read) and directories to 755 by default.',
|
|
393
|
+
cwe: 'CWE-732',
|
|
394
|
+
owasp: 'A01:2021-Broken Access Control',
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
id: 'SEC-031',
|
|
398
|
+
name: 'Exposed .env File Reference',
|
|
399
|
+
category: 'configuration',
|
|
400
|
+
severity: 'medium',
|
|
401
|
+
pattern: '(?i)(?:require|import)\\s*\\(\\s*["\']dotenv["\']\\s*\\)\\s*;|(?i)dotenv\\.config\\s*\\(\\s*\\)',
|
|
402
|
+
description: 'The dotenv package is being used. While this is standard, ensure the .env file itself is in .gitignore and never committed.',
|
|
403
|
+
remediation: 'Verify .env is in .gitignore. Consider using a secrets manager for production deployments instead of .env files.',
|
|
404
|
+
cwe: 'CWE-798',
|
|
405
|
+
},
|
|
406
|
+
// ============================================
|
|
407
|
+
// Dependencies (rules 032-035)
|
|
408
|
+
// ============================================
|
|
409
|
+
{
|
|
410
|
+
id: 'SEC-032',
|
|
411
|
+
name: 'Known Vulnerable Package (event-stream)',
|
|
412
|
+
category: 'dependencies',
|
|
413
|
+
severity: 'critical',
|
|
414
|
+
pattern: '(?i)["\']event-stream["\']\\s*:\\s*["\']',
|
|
415
|
+
description: 'The event-stream package is known to contain malicious code (CVE-2018-16487 / flatmap-stream backdoor).',
|
|
416
|
+
remediation: 'Remove event-stream from your dependencies immediately. Replace with a safe alternative if needed.',
|
|
417
|
+
cwe: 'CWE-1357',
|
|
418
|
+
owasp: 'A06:2021-Vulnerable and Outdated Components',
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
id: 'SEC-033',
|
|
422
|
+
name: 'Known Vulnerable Package (lodash < 4.17.12)',
|
|
423
|
+
category: 'dependencies',
|
|
424
|
+
severity: 'high',
|
|
425
|
+
pattern: '(?i)["\']lodash["\']\\s*:\\s*["\'][0-3]\\.|4\\.1[0-6]\\.|4\\.17\\.(?:[0-9]|1[01])["\']',
|
|
426
|
+
description: 'This version of lodash is vulnerable to prototype pollution (CVE-2020-8203) and other issues.',
|
|
427
|
+
remediation: 'Upgrade lodash to version 4.17.21 or later. Run npm audit to check for other known vulnerabilities.',
|
|
428
|
+
cwe: 'CWE-1321',
|
|
429
|
+
owasp: 'A06:2021-Vulnerable and Outdated Components',
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
id: 'SEC-034',
|
|
433
|
+
name: 'Known Vulnerable Package (node-serialize)',
|
|
434
|
+
category: 'dependencies',
|
|
435
|
+
severity: 'critical',
|
|
436
|
+
pattern: '(?i)["\']node-serialize["\']\\s*:\\s*["\']',
|
|
437
|
+
description: 'The node-serialize package allows arbitrary code execution through deserialization (CVE-2017-5941).',
|
|
438
|
+
remediation: 'Remove node-serialize immediately. Use a secure serialization format like JSON with explicit type handling.',
|
|
439
|
+
cwe: 'CWE-502',
|
|
440
|
+
owasp: 'A08:2021-Software and Data Integrity Failures',
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
id: 'SEC-035',
|
|
444
|
+
name: 'Known Vulnerable Package (express < 4.0)',
|
|
445
|
+
category: 'dependencies',
|
|
446
|
+
severity: 'high',
|
|
447
|
+
pattern: '(?i)["\']express["\']\\s*:\\s*["\'][0-3]\\.',
|
|
448
|
+
description: 'An outdated version of Express.js is being used. Versions below 4.0 have known security vulnerabilities.',
|
|
449
|
+
remediation: 'Upgrade Express to the latest 4.x version or later. Review the Express security update documentation for migration steps.',
|
|
450
|
+
cwe: 'CWE-1104',
|
|
451
|
+
owasp: 'A06:2021-Vulnerable and Outdated Components',
|
|
452
|
+
},
|
|
453
|
+
// ============================================
|
|
454
|
+
// Data Exposure (rules 036-042)
|
|
455
|
+
// ============================================
|
|
456
|
+
{
|
|
457
|
+
id: 'SEC-036',
|
|
458
|
+
name: 'Sensitive Data in Console/Log',
|
|
459
|
+
category: 'data-exposure',
|
|
460
|
+
severity: 'medium',
|
|
461
|
+
pattern: '(?i)(?:console\\.log|console\\.debug|logger\\.debug|logger\\.info|print)\\s*\\(.*(?:password|token|secret|api[_-]?key|credential|ssn|social[_-]?security)',
|
|
462
|
+
description: 'Sensitive data may be written to logs. Logging secrets can expose them in log aggregation systems, monitoring tools, or log files.',
|
|
463
|
+
remediation: 'Never log sensitive data. Redact or mask passwords, tokens, and other secrets before logging. Use structured logging with field-level controls.',
|
|
464
|
+
cwe: 'CWE-532',
|
|
465
|
+
owasp: 'A09:2021-Security Logging and Monitoring Failures',
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
id: 'SEC-037',
|
|
469
|
+
name: 'Sensitive Data in Error Message',
|
|
470
|
+
category: 'data-exposure',
|
|
471
|
+
severity: 'medium',
|
|
472
|
+
pattern: '(?i)(?:throw|reject|Error|Exception)\\s*\\(.*(?:password|token|secret|api[_-]?key|credential)',
|
|
473
|
+
description: 'Sensitive data may be included in error messages or exceptions, which could be exposed to end users or in logs.',
|
|
474
|
+
remediation: 'Return generic error messages to users. Log detailed errors server-side only, ensuring secrets are not included in any error output.',
|
|
475
|
+
cwe: 'CWE-209',
|
|
476
|
+
owasp: 'A04:2021-Insecure Design',
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
id: 'SEC-038',
|
|
480
|
+
name: 'Sensitive Data in URL Query Parameter',
|
|
481
|
+
category: 'data-exposure',
|
|
482
|
+
severity: 'medium',
|
|
483
|
+
pattern: '(?i)(?:fetch|axios|request|http)\\s*\\(.*[?&](?:password|token|secret|api[_-]?key|credential)=',
|
|
484
|
+
description: 'Sensitive data may be passed in URL query parameters. URLs are logged in browser history, server logs, and referrer headers.',
|
|
485
|
+
remediation: 'Pass sensitive data in request headers or request bodies (POST) instead of URL parameters.',
|
|
486
|
+
cwe: 'CWE-598',
|
|
487
|
+
owasp: 'A02:2021-Cryptographic Failures',
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
id: 'SEC-039',
|
|
491
|
+
name: 'Stack Trace Exposure',
|
|
492
|
+
category: 'data-exposure',
|
|
493
|
+
severity: 'low',
|
|
494
|
+
pattern: '(?i)(?:app\\.use|app\\.all|middleware).*err\\.(?:stack|message).*(?:res\\.send|res\\.json|res\\.write|response\\.write)',
|
|
495
|
+
description: 'Stack traces may be exposed to end users through error handlers. This reveals internal application structure.',
|
|
496
|
+
remediation: 'Return generic error responses in production. Only include stack traces in development mode. Use environment-based error handling.',
|
|
497
|
+
cwe: 'CWE-209',
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
id: 'SEC-040',
|
|
501
|
+
name: 'Information Disclosure via Comment',
|
|
502
|
+
category: 'data-exposure',
|
|
503
|
+
severity: 'low',
|
|
504
|
+
pattern: '(?i)(?:\\/\\/|#|<!--|\\*).*\\b(?:TODO:\\s*(?:hack|security|vuln|fixme)|FIXME:\\s*(?:security|auth)|HACK:|XXX:|BUG:)',
|
|
505
|
+
description: 'A source code comment indicates a known security issue or hack. These should be tracked and resolved.',
|
|
506
|
+
remediation: 'Create tickets for all security-related TODO/FIXME comments. Resolve them before deploying to production.',
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
id: 'SEC-041',
|
|
510
|
+
name: 'PII/Sensitive Data Pattern (SSN)',
|
|
511
|
+
category: 'data-exposure',
|
|
512
|
+
severity: 'high',
|
|
513
|
+
pattern: '\\b\\d{3}-\\d{2}-\\d{4}\\b',
|
|
514
|
+
description: 'A pattern resembling a US Social Security Number (SSN) was detected. SSNs are highly sensitive PII.',
|
|
515
|
+
remediation: 'Do not store or process SSNs in code files. If necessary, encrypt at rest and in transit, and comply with applicable data protection regulations.',
|
|
516
|
+
cwe: 'CWE-359',
|
|
517
|
+
owasp: 'A01:2021-Broken Access Control',
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
id: 'SEC-042',
|
|
521
|
+
name: 'Credit Card Number Pattern',
|
|
522
|
+
category: 'data-exposure',
|
|
523
|
+
severity: 'high',
|
|
524
|
+
pattern: '\\b(?:\\d{4}[\\s-]?){3}\\d{4}\\b',
|
|
525
|
+
description: 'A pattern resembling a credit card number was detected. Payment card data is subject to PCI-DSS compliance requirements.',
|
|
526
|
+
remediation: 'Never store raw credit card numbers. Use tokenization via a payment processor and ensure PCI-DSS compliance.',
|
|
527
|
+
cwe: 'CWE-359',
|
|
528
|
+
owasp: 'A01:2021-Broken Access Control',
|
|
529
|
+
},
|
|
530
|
+
// ============================================
|
|
531
|
+
// Cryptographic (rules 043-050)
|
|
532
|
+
// ============================================
|
|
533
|
+
{
|
|
534
|
+
id: 'SEC-043',
|
|
535
|
+
name: 'Weak Encryption Algorithm (DES/3DES/RC4/Blowfish)',
|
|
536
|
+
category: 'cryptographic',
|
|
537
|
+
severity: 'high',
|
|
538
|
+
pattern: '(?i)(?:createCipher|createCipheriv|Cipher)\\s*\\(\\s*["\'](?:des|des3|tripledes|rc4|blowfish|arc4)["\']',
|
|
539
|
+
description: 'A weak or broken encryption algorithm is being used. These algorithms are vulnerable to known attacks.',
|
|
540
|
+
remediation: 'Use AES-256-GCM or ChaCha20-Poly1305 for encryption. DES, 3DES, RC4, and Blowfish are all deprecated for security use.',
|
|
541
|
+
cwe: 'CWE-327',
|
|
542
|
+
owasp: 'A02:2021-Cryptographic Failures',
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
id: 'SEC-044',
|
|
546
|
+
name: 'Hardcoded Initialization Vector (IV)',
|
|
547
|
+
category: 'cryptographic',
|
|
548
|
+
severity: 'high',
|
|
549
|
+
pattern: '(?i)(?:iv|initializationVector|nonce)\\s*[:=]\\s*(?:Buffer\\.from|Uint8Array|new byte)\\s*\\(\\s*["\'][A-Za-z0-9+/=]+["\']',
|
|
550
|
+
description: 'An initialization vector (IV) appears to be hardcoded. Reusing IVs with the same key undermines encryption security.',
|
|
551
|
+
remediation: 'Generate a unique, random IV for each encryption operation using crypto.randomBytes() or equivalent.',
|
|
552
|
+
cwe: 'CWE-329',
|
|
553
|
+
owasp: 'A02:2021-Cryptographic Failures',
|
|
554
|
+
},
|
|
555
|
+
{
|
|
556
|
+
id: 'SEC-045',
|
|
557
|
+
name: 'ECB Mode Usage',
|
|
558
|
+
category: 'cryptographic',
|
|
559
|
+
severity: 'high',
|
|
560
|
+
pattern: '(?i)(?:createCipher|createCipheriv|Cipher)\\s*\\(\\s*["\']aes-?128-?ecb["\']|(?:createCipher|createCipheriv|Cipher)\\s*\\(\\s*["\']aes-?256-?ecb["\']',
|
|
561
|
+
description: 'ECB (Electronic Codebook) mode is being used. ECB mode encrypts identical plaintext blocks into identical ciphertext blocks, leaking patterns.',
|
|
562
|
+
remediation: 'Use authenticated encryption modes like AES-GCM or AES-CBC with a proper IV. Never use ECB mode for encrypting more than one block.',
|
|
563
|
+
cwe: 'CWE-327',
|
|
564
|
+
owasp: 'A02:2021-Cryptographic Failures',
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
id: 'SEC-046',
|
|
568
|
+
name: 'Small RSA Key Size',
|
|
569
|
+
category: 'cryptographic',
|
|
570
|
+
severity: 'high',
|
|
571
|
+
pattern: '(?i)(?:generateKeyPair|RSA|rsa)\\s*.*(?:modulusLength|keySize|bits)\\s*[:=]\\s*(?:512|768|1024|2048)\\b',
|
|
572
|
+
description: 'An RSA key size of 2048 bits or less may be insufficient for long-term security. Keys below 2048 bits are considered weak.',
|
|
573
|
+
remediation: 'Use at minimum 3072-bit RSA keys (4096-bit recommended for new deployments). Consider using Ed25519 for digital signatures.',
|
|
574
|
+
cwe: 'CWE-326',
|
|
575
|
+
owasp: 'A02:2021-Cryptographic Failures',
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
id: 'SEC-047',
|
|
579
|
+
name: 'Insecure Random Number Generator',
|
|
580
|
+
category: 'cryptographic',
|
|
581
|
+
severity: 'high',
|
|
582
|
+
pattern: '(?i)(?:Math\\.random|rand\\(\\)|srand\\(\\)|random\\.random)\\s*(?:\\(\\)|).*(?:token|password|secret|key|session|nonce|salt)',
|
|
583
|
+
description: 'A non-cryptographic random number generator is being used for a security-sensitive purpose. These generators are predictable.',
|
|
584
|
+
remediation: 'Use crypto.randomBytes(), crypto.getRandomValues(), or a cryptographic PRNG for all security-sensitive operations.',
|
|
585
|
+
cwe: 'CWE-338',
|
|
586
|
+
owasp: 'A02:2021-Cryptographic Failures',
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
id: 'SEC-048',
|
|
590
|
+
name: 'Missing Integrity Check (HMAC)',
|
|
591
|
+
category: 'cryptographic',
|
|
592
|
+
severity: 'medium',
|
|
593
|
+
pattern: '(?i)(?:createCipher|createCipheriv|Cipher)\\s*\\(\\s*["\']aes-(?:128|256)-cbc["\']',
|
|
594
|
+
description: 'AES-CBC mode is used without apparent HMAC verification. Unauthenticated encryption is vulnerable to padding oracle attacks.',
|
|
595
|
+
remediation: 'Use authenticated encryption (AEAD) such as AES-256-GCM, or manually add an HMAC for integrity verification after encryption.',
|
|
596
|
+
cwe: 'CWE-353',
|
|
597
|
+
owasp: 'A02:2021-Cryptographic Failures',
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
id: 'SEC-049',
|
|
601
|
+
name: 'Deprecated Crypto API',
|
|
602
|
+
category: 'cryptographic',
|
|
603
|
+
severity: 'medium',
|
|
604
|
+
pattern: '(?i)crypto\\.createCipher\\s*\\(\\s*(?!"\')|(?i)Cipher\\s*\\(\\s*(?!.*iv)',
|
|
605
|
+
description: 'A deprecated crypto API is being used. crypto.createCipher (without IV) is deprecated in Node.js and insecure.',
|
|
606
|
+
remediation: 'Use crypto.createCipheriv() with a proper algorithm, key, and IV. The createCipher API was deprecated due to security issues.',
|
|
607
|
+
cwe: 'CWE-327',
|
|
608
|
+
owasp: 'A02:2021-Cryptographic Failures',
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
id: 'SEC-050',
|
|
612
|
+
name: 'Hardcoded Encryption Key',
|
|
613
|
+
category: 'cryptographic',
|
|
614
|
+
severity: 'critical',
|
|
615
|
+
pattern: '(?i)(?:encryption[_-]?key|encrypt[_-]?key|cipher[_-]?key|aes[_-]?key)\\s*[:=]\\s*["\'][A-Za-z0-9+/=]{16,}["\']',
|
|
616
|
+
description: 'An encryption key appears to be hardcoded. This compromises the entire encryption scheme if the code is exposed.',
|
|
617
|
+
remediation: 'Store encryption keys in a key management service (KMS) or secrets manager. Use key derivation functions (PBKDF2, HKDF) when deriving keys from passwords.',
|
|
618
|
+
cwe: 'CWE-798',
|
|
619
|
+
owasp: 'A02:2021-Cryptographic Failures',
|
|
620
|
+
},
|
|
621
|
+
];
|
|
622
|
+
}
|
|
623
|
+
// -----------------------------------------------------------
|
|
624
|
+
// SecurityScanner class
|
|
625
|
+
// -----------------------------------------------------------
|
|
626
|
+
export class SecurityScanner {
|
|
627
|
+
rules;
|
|
628
|
+
vulnerabilities;
|
|
629
|
+
ignoredRules;
|
|
630
|
+
ignoredFiles;
|
|
631
|
+
ignoreEntries;
|
|
632
|
+
config;
|
|
633
|
+
vulnCounter;
|
|
634
|
+
rootDir;
|
|
635
|
+
constructor(rootDir, config) {
|
|
636
|
+
this.rootDir = rootDir || process.cwd();
|
|
637
|
+
this.rules = getDefaultRules();
|
|
638
|
+
this.vulnerabilities = [];
|
|
639
|
+
this.ignoredRules = new Set();
|
|
640
|
+
this.ignoredFiles = new Set();
|
|
641
|
+
this.ignoreEntries = [];
|
|
642
|
+
this.vulnCounter = 0;
|
|
643
|
+
this.config = {
|
|
644
|
+
enabled: true,
|
|
645
|
+
autoScanOnChange: false,
|
|
646
|
+
failOnSeverity: 'critical',
|
|
647
|
+
excludePatterns: [...DEFAULT_EXCLUDE_PATTERNS],
|
|
648
|
+
customRules: [],
|
|
649
|
+
...config,
|
|
650
|
+
};
|
|
651
|
+
// Add custom rules if provided
|
|
652
|
+
if (this.config.customRules.length > 0) {
|
|
653
|
+
this.rules.push(...this.config.customRules);
|
|
654
|
+
}
|
|
655
|
+
// Load ignore file if it exists
|
|
656
|
+
this.loadIgnoreFile();
|
|
657
|
+
}
|
|
658
|
+
// ---------------------------------------------------------
|
|
659
|
+
// Public API
|
|
660
|
+
// ---------------------------------------------------------
|
|
661
|
+
/**
|
|
662
|
+
* Scan a single file for security issues.
|
|
663
|
+
*/
|
|
664
|
+
scanFile(filePath) {
|
|
665
|
+
const absolutePath = resolve(filePath);
|
|
666
|
+
if (!existsSync(absolutePath)) {
|
|
667
|
+
return [];
|
|
668
|
+
}
|
|
669
|
+
const stat = statSync(absolutePath);
|
|
670
|
+
if (stat.isDirectory()) {
|
|
671
|
+
return [];
|
|
672
|
+
}
|
|
673
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
674
|
+
return [];
|
|
675
|
+
}
|
|
676
|
+
// Check if file is ignored
|
|
677
|
+
if (this.isFilePathIgnored(absolutePath)) {
|
|
678
|
+
return [];
|
|
679
|
+
}
|
|
680
|
+
let content;
|
|
681
|
+
try {
|
|
682
|
+
content = readFileSync(absolutePath, 'utf-8');
|
|
683
|
+
}
|
|
684
|
+
catch {
|
|
685
|
+
return [];
|
|
686
|
+
}
|
|
687
|
+
const findings = this.scanContent(content, absolutePath);
|
|
688
|
+
this.vulnerabilities.push(...findings);
|
|
689
|
+
return findings;
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Scan an entire directory for security issues.
|
|
693
|
+
*/
|
|
694
|
+
scanDirectory(dirPath, options) {
|
|
695
|
+
const startTime = Date.now();
|
|
696
|
+
const absoluteDir = resolve(dirPath);
|
|
697
|
+
const allFindings = [];
|
|
698
|
+
let filesScanned = 0;
|
|
699
|
+
const effectiveOptions = {
|
|
700
|
+
recursive: true,
|
|
701
|
+
excludePatterns: [
|
|
702
|
+
...this.config.excludePatterns,
|
|
703
|
+
...(options?.excludePatterns || []),
|
|
704
|
+
],
|
|
705
|
+
maxFileSize: options?.maxFileSize || MAX_FILE_SIZE,
|
|
706
|
+
fileExtensions: options?.fileExtensions || DEFAULT_FILE_EXTENSIONS,
|
|
707
|
+
};
|
|
708
|
+
// Reset state for a fresh scan
|
|
709
|
+
this.vulnerabilities = [];
|
|
710
|
+
this.vulnCounter = 0;
|
|
711
|
+
this.walkDirectory(absoluteDir, allFindings, effectiveOptions, (count) => {
|
|
712
|
+
filesScanned = count;
|
|
713
|
+
});
|
|
714
|
+
const duration = Date.now() - startTime;
|
|
715
|
+
return {
|
|
716
|
+
totalVulnerabilities: allFindings.length,
|
|
717
|
+
critical: allFindings.filter((v) => v.severity === 'critical').length,
|
|
718
|
+
high: allFindings.filter((v) => v.severity === 'high').length,
|
|
719
|
+
medium: allFindings.filter((v) => v.severity === 'medium').length,
|
|
720
|
+
low: allFindings.filter((v) => v.severity === 'low').length,
|
|
721
|
+
info: allFindings.filter((v) => v.severity === 'info').length,
|
|
722
|
+
files: filesScanned,
|
|
723
|
+
duration,
|
|
724
|
+
vulnerabilities: allFindings,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
/**
|
|
728
|
+
* Scan a unified diff for security issues.
|
|
729
|
+
*/
|
|
730
|
+
scanDiff(diff) {
|
|
731
|
+
const hunks = this.parseDiff(diff);
|
|
732
|
+
const allFindings = [];
|
|
733
|
+
for (const hunk of hunks) {
|
|
734
|
+
if (hunk.changeType === 'remove')
|
|
735
|
+
continue; // only scan added lines
|
|
736
|
+
const findings = this.scanContent(hunk.content, hunk.file);
|
|
737
|
+
// Adjust line numbers to the diff's line
|
|
738
|
+
for (const finding of findings) {
|
|
739
|
+
finding.line = hunk.line;
|
|
740
|
+
finding.file = hunk.file;
|
|
741
|
+
}
|
|
742
|
+
allFindings.push(...findings);
|
|
743
|
+
}
|
|
744
|
+
this.vulnerabilities.push(...allFindings);
|
|
745
|
+
return allFindings;
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Get all found vulnerabilities.
|
|
749
|
+
*/
|
|
750
|
+
getVulnerabilities() {
|
|
751
|
+
return [...this.vulnerabilities];
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Get vulnerability counts by severity.
|
|
755
|
+
*/
|
|
756
|
+
getSeverityCounts() {
|
|
757
|
+
return {
|
|
758
|
+
critical: this.vulnerabilities.filter((v) => v.severity === 'critical')
|
|
759
|
+
.length,
|
|
760
|
+
high: this.vulnerabilities.filter((v) => v.severity === 'high').length,
|
|
761
|
+
medium: this.vulnerabilities.filter((v) => v.severity === 'medium')
|
|
762
|
+
.length,
|
|
763
|
+
low: this.vulnerabilities.filter((v) => v.severity === 'low').length,
|
|
764
|
+
info: this.vulnerabilities.filter((v) => v.severity === 'info').length,
|
|
765
|
+
total: this.vulnerabilities.length,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Generate a report in the specified format.
|
|
770
|
+
*/
|
|
771
|
+
generateReport(format = 'json') {
|
|
772
|
+
switch (format) {
|
|
773
|
+
case 'json':
|
|
774
|
+
return this.generateJsonReport();
|
|
775
|
+
case 'markdown':
|
|
776
|
+
return this.generateMarkdownReport();
|
|
777
|
+
case 'sarif':
|
|
778
|
+
return this.generateSarifReport();
|
|
779
|
+
default:
|
|
780
|
+
return this.generateJsonReport();
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Configure custom rules. Replaces existing custom rules.
|
|
785
|
+
*/
|
|
786
|
+
setRules(rules) {
|
|
787
|
+
// Remove previous custom rules
|
|
788
|
+
this.rules = getDefaultRules();
|
|
789
|
+
// Add the new custom rules
|
|
790
|
+
this.rules.push(...rules);
|
|
791
|
+
this.config.customRules = rules;
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Ignore a specific rule by its ID.
|
|
795
|
+
*/
|
|
796
|
+
ignoreRule(ruleId) {
|
|
797
|
+
this.ignoredRules.add(ruleId);
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Ignore findings in a specific file.
|
|
801
|
+
*/
|
|
802
|
+
ignoreFile(filePath) {
|
|
803
|
+
const absolutePath = resolve(filePath);
|
|
804
|
+
this.ignoredFiles.add(absolutePath);
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Get remediation suggestions for a vulnerability.
|
|
808
|
+
*/
|
|
809
|
+
getRemediation(vulnId) {
|
|
810
|
+
const vuln = this.vulnerabilities.find((v) => v.id === vulnId);
|
|
811
|
+
if (!vuln)
|
|
812
|
+
return null;
|
|
813
|
+
const rule = this.rules.find((r) => r.id === vuln.ruleId);
|
|
814
|
+
if (!rule)
|
|
815
|
+
return vuln.remediation;
|
|
816
|
+
return [
|
|
817
|
+
`## ${rule.name}`,
|
|
818
|
+
``,
|
|
819
|
+
`**Rule ID:** ${rule.id}`,
|
|
820
|
+
`**Severity:** ${vuln.severity}`,
|
|
821
|
+
`**Category:** ${rule.category}`,
|
|
822
|
+
vuln.cwe ? `**CWE:** ${vuln.cwe}` : '',
|
|
823
|
+
vuln.owasp ? `**OWASP:** ${vuln.owasp}` : '',
|
|
824
|
+
``,
|
|
825
|
+
`### Description`,
|
|
826
|
+
rule.description,
|
|
827
|
+
``,
|
|
828
|
+
`### Remediation`,
|
|
829
|
+
rule.remediation,
|
|
830
|
+
``,
|
|
831
|
+
`### Location`,
|
|
832
|
+
`- **File:** ${vuln.file}`,
|
|
833
|
+
`- **Line:** ${vuln.line}`,
|
|
834
|
+
`- **Column:** ${vuln.column}`,
|
|
835
|
+
`- **Confidence:** ${vuln.confidence}`,
|
|
836
|
+
]
|
|
837
|
+
.filter(Boolean)
|
|
838
|
+
.join('\n');
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Compute a CVSS-like score for the current set of vulnerabilities.
|
|
842
|
+
*/
|
|
843
|
+
getCVSSLikeScore() {
|
|
844
|
+
if (this.vulnerabilities.length === 0)
|
|
845
|
+
return 0;
|
|
846
|
+
const counts = this.getSeverityCounts();
|
|
847
|
+
let weightedSum = 0;
|
|
848
|
+
let maxSeverity = 0;
|
|
849
|
+
for (const [severity, weight] of Object.entries(SEVERITY_WEIGHTS)) {
|
|
850
|
+
const count = counts[severity] || 0;
|
|
851
|
+
weightedSum += weight * count;
|
|
852
|
+
if (count > 0 && weight > maxSeverity) {
|
|
853
|
+
maxSeverity = weight;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
// Base score: weighted average, capped at 10
|
|
857
|
+
const avgScore = weightedSum / this.vulnerabilities.length;
|
|
858
|
+
// Factor in the worst finding
|
|
859
|
+
const combinedScore = avgScore * 0.4 + maxSeverity * 0.6;
|
|
860
|
+
return Math.min(10, Math.round(combinedScore * 10) / 10);
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Clear all found vulnerabilities.
|
|
864
|
+
*/
|
|
865
|
+
clear() {
|
|
866
|
+
this.vulnerabilities = [];
|
|
867
|
+
this.vulnCounter = 0;
|
|
868
|
+
}
|
|
869
|
+
/**
|
|
870
|
+
* Get all active rules.
|
|
871
|
+
*/
|
|
872
|
+
getRules() {
|
|
873
|
+
return [...this.rules];
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Get the current configuration.
|
|
877
|
+
*/
|
|
878
|
+
getConfig() {
|
|
879
|
+
return { ...this.config };
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Update configuration.
|
|
883
|
+
*/
|
|
884
|
+
updateConfig(partial) {
|
|
885
|
+
Object.assign(this.config, partial);
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Export findings to a file.
|
|
889
|
+
*/
|
|
890
|
+
exportReport(outputPath, format = 'json') {
|
|
891
|
+
const report = this.generateReport(format);
|
|
892
|
+
const dir = dirname(outputPath);
|
|
893
|
+
if (!existsSync(dir)) {
|
|
894
|
+
mkdirSync(dir, { recursive: true });
|
|
895
|
+
}
|
|
896
|
+
writeFileSync(outputPath, report, 'utf-8');
|
|
897
|
+
}
|
|
898
|
+
// ---------------------------------------------------------
|
|
899
|
+
// Private: scanning internals
|
|
900
|
+
// ---------------------------------------------------------
|
|
901
|
+
scanContent(content, filePath) {
|
|
902
|
+
const lines = content.split('\n');
|
|
903
|
+
const findings = [];
|
|
904
|
+
for (const rule of this.rules) {
|
|
905
|
+
// Skip ignored rules
|
|
906
|
+
if (this.ignoredRules.has(rule.id))
|
|
907
|
+
continue;
|
|
908
|
+
let regex;
|
|
909
|
+
try {
|
|
910
|
+
regex = new RegExp(rule.pattern, 'gi');
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
// Invalid regex pattern — skip this rule
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
|
|
917
|
+
const line = lines[lineIndex];
|
|
918
|
+
regex.lastIndex = 0; // Reset for each line
|
|
919
|
+
let match;
|
|
920
|
+
try {
|
|
921
|
+
match = regex.exec(line);
|
|
922
|
+
}
|
|
923
|
+
catch {
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
if (match) {
|
|
927
|
+
const column = match.index + 1;
|
|
928
|
+
const vulnId = this.generateVulnId();
|
|
929
|
+
// Determine confidence based on context
|
|
930
|
+
const confidence = this.assessConfidence(rule, line, filePath);
|
|
931
|
+
// Check if this specific finding is ignored
|
|
932
|
+
if (this.isFindingIgnored(rule.id, filePath, lineIndex + 1)) {
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
findings.push({
|
|
936
|
+
id: vulnId,
|
|
937
|
+
ruleId: rule.id,
|
|
938
|
+
file: filePath,
|
|
939
|
+
line: lineIndex + 1,
|
|
940
|
+
column,
|
|
941
|
+
severity: rule.severity,
|
|
942
|
+
category: rule.category,
|
|
943
|
+
title: rule.name,
|
|
944
|
+
description: rule.description,
|
|
945
|
+
remediation: rule.remediation,
|
|
946
|
+
cwe: rule.cwe,
|
|
947
|
+
owasp: rule.owasp,
|
|
948
|
+
confidence,
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return findings;
|
|
954
|
+
}
|
|
955
|
+
walkDirectory(dir, findings, options, onFileCount, currentCount = 0) {
|
|
956
|
+
let fileCount = currentCount;
|
|
957
|
+
if (!existsSync(dir))
|
|
958
|
+
return fileCount;
|
|
959
|
+
let entries;
|
|
960
|
+
try {
|
|
961
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
962
|
+
}
|
|
963
|
+
catch {
|
|
964
|
+
return fileCount;
|
|
965
|
+
}
|
|
966
|
+
for (const entry of entries) {
|
|
967
|
+
const fullPath = join(dir, entry.name);
|
|
968
|
+
if (entry.isDirectory()) {
|
|
969
|
+
if (!options.recursive)
|
|
970
|
+
continue;
|
|
971
|
+
if (this.isPathExcluded(fullPath, options.excludePatterns || []))
|
|
972
|
+
continue;
|
|
973
|
+
fileCount = this.walkDirectory(fullPath, findings, options, onFileCount, fileCount);
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
if (entry.isFile()) {
|
|
977
|
+
// Check extension filter
|
|
978
|
+
const ext = extname(entry.name).toLowerCase();
|
|
979
|
+
if (options.fileExtensions &&
|
|
980
|
+
options.fileExtensions.length > 0 &&
|
|
981
|
+
!options.fileExtensions.includes(ext)) {
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
// Check exclude patterns
|
|
985
|
+
if (this.isPathExcluded(fullPath, options.excludePatterns || []))
|
|
986
|
+
continue;
|
|
987
|
+
// Check ignored files
|
|
988
|
+
if (this.isFilePathIgnored(fullPath))
|
|
989
|
+
continue;
|
|
990
|
+
// Check file size
|
|
991
|
+
try {
|
|
992
|
+
const stat = statSync(fullPath);
|
|
993
|
+
if (stat.size > (options.maxFileSize || MAX_FILE_SIZE))
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
catch {
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
// Scan the file
|
|
1000
|
+
try {
|
|
1001
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
1002
|
+
const fileFindings = this.scanContent(content, fullPath);
|
|
1003
|
+
findings.push(...fileFindings);
|
|
1004
|
+
}
|
|
1005
|
+
catch {
|
|
1006
|
+
// Skip files that cannot be read as text
|
|
1007
|
+
}
|
|
1008
|
+
fileCount++;
|
|
1009
|
+
onFileCount(fileCount);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
return fileCount;
|
|
1013
|
+
}
|
|
1014
|
+
// ---------------------------------------------------------
|
|
1015
|
+
// Private: diff parsing
|
|
1016
|
+
// ---------------------------------------------------------
|
|
1017
|
+
parseDiff(diff) {
|
|
1018
|
+
const hunks = [];
|
|
1019
|
+
const lines = diff.split('\n');
|
|
1020
|
+
let currentFile = 'unknown';
|
|
1021
|
+
let currentLine = 0;
|
|
1022
|
+
for (const line of lines) {
|
|
1023
|
+
// Detect file path from diff headers
|
|
1024
|
+
const fileMatch = line.match(/^diff --git a\/(.+?) b\/(.+?)$|^--- (?:a\/)?(.+?)$|^\+\+\+ (?:b\/)?(.+?)$/);
|
|
1025
|
+
if (fileMatch) {
|
|
1026
|
+
const newFile = fileMatch[4] || fileMatch[3] || fileMatch[2] || fileMatch[1];
|
|
1027
|
+
if (newFile && newFile !== '/dev/null') {
|
|
1028
|
+
currentFile = newFile;
|
|
1029
|
+
}
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
// Detect hunk header: @@ -a,b +c,d @@
|
|
1033
|
+
const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
1034
|
+
if (hunkMatch) {
|
|
1035
|
+
currentLine = parseInt(hunkMatch[1], 10);
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
// Added line
|
|
1039
|
+
if (line.startsWith('+') && !line.startsWith('++')) {
|
|
1040
|
+
hunks.push({
|
|
1041
|
+
file: currentFile,
|
|
1042
|
+
line: currentLine,
|
|
1043
|
+
content: line.substring(1),
|
|
1044
|
+
changeType: 'add',
|
|
1045
|
+
});
|
|
1046
|
+
currentLine++;
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
// Removed line
|
|
1050
|
+
if (line.startsWith('-') && !line.startsWith('--')) {
|
|
1051
|
+
hunks.push({
|
|
1052
|
+
file: currentFile,
|
|
1053
|
+
line: currentLine,
|
|
1054
|
+
content: line.substring(1),
|
|
1055
|
+
changeType: 'remove',
|
|
1056
|
+
});
|
|
1057
|
+
continue;
|
|
1058
|
+
}
|
|
1059
|
+
// Context line
|
|
1060
|
+
if (line.startsWith(' ') || line.match(/^\d+/)) {
|
|
1061
|
+
currentLine++;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return hunks;
|
|
1065
|
+
}
|
|
1066
|
+
// ---------------------------------------------------------
|
|
1067
|
+
// Private: ignore file handling
|
|
1068
|
+
// ---------------------------------------------------------
|
|
1069
|
+
loadIgnoreFile() {
|
|
1070
|
+
const ignorePath = join(this.rootDir, IGNORE_FILE_NAME);
|
|
1071
|
+
if (!existsSync(ignorePath))
|
|
1072
|
+
return;
|
|
1073
|
+
try {
|
|
1074
|
+
const content = readFileSync(ignorePath, 'utf-8');
|
|
1075
|
+
const lines = content.split('\n');
|
|
1076
|
+
for (const line of lines) {
|
|
1077
|
+
const trimmed = line.trim();
|
|
1078
|
+
if (!trimmed || trimmed.startsWith('#'))
|
|
1079
|
+
continue;
|
|
1080
|
+
// Format: ruleId [file[:line]] [# reason]
|
|
1081
|
+
// Examples:
|
|
1082
|
+
// SEC-029
|
|
1083
|
+
// SEC-029 src/config.ts
|
|
1084
|
+
// SEC-029 src/config.ts:42
|
|
1085
|
+
// SEC-029 src/config.ts:42 # False positive for localhost
|
|
1086
|
+
const parts = trimmed.split('#');
|
|
1087
|
+
const mainPart = (parts[0] || '').trim();
|
|
1088
|
+
const reason = (parts[1] || '').trim();
|
|
1089
|
+
const tokens = mainPart.split(/\s+/);
|
|
1090
|
+
const ruleId = tokens[0];
|
|
1091
|
+
const fileRef = tokens[1] || undefined;
|
|
1092
|
+
const entry = { ruleId, reason: reason || undefined };
|
|
1093
|
+
if (fileRef) {
|
|
1094
|
+
if (fileRef.includes(':')) {
|
|
1095
|
+
const [file, lineStr] = fileRef.split(':');
|
|
1096
|
+
entry.file = file;
|
|
1097
|
+
entry.line = parseInt(lineStr, 10) || undefined;
|
|
1098
|
+
}
|
|
1099
|
+
else {
|
|
1100
|
+
entry.file = fileRef;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
this.ignoreEntries.push(entry);
|
|
1104
|
+
// Also add to quick-lookup sets
|
|
1105
|
+
if (ruleId && !fileRef) {
|
|
1106
|
+
this.ignoredRules.add(ruleId);
|
|
1107
|
+
}
|
|
1108
|
+
if (entry.file && !ruleId?.includes('SEC-')) {
|
|
1109
|
+
this.ignoredFiles.add(resolve(entry.file));
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
catch {
|
|
1114
|
+
// Ignore file read errors
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
isFilePathIgnored(absolutePath) {
|
|
1118
|
+
const ignoredList = Array.from(this.ignoredFiles);
|
|
1119
|
+
for (const ignored of ignoredList) {
|
|
1120
|
+
if (absolutePath === ignored || absolutePath.startsWith(ignored + sep)) {
|
|
1121
|
+
return true;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
// Check ignore entries with file patterns
|
|
1125
|
+
for (const entry of this.ignoreEntries) {
|
|
1126
|
+
if (entry.file && !entry.ruleId) {
|
|
1127
|
+
const ignoredPath = resolve(entry.file);
|
|
1128
|
+
if (absolutePath === ignoredPath ||
|
|
1129
|
+
absolutePath.startsWith(ignoredPath + sep)) {
|
|
1130
|
+
return true;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
return false;
|
|
1135
|
+
}
|
|
1136
|
+
isFindingIgnored(ruleId, filePath, line) {
|
|
1137
|
+
if (this.ignoredRules.has(ruleId))
|
|
1138
|
+
return true;
|
|
1139
|
+
for (const entry of this.ignoreEntries) {
|
|
1140
|
+
if (entry.ruleId !== ruleId)
|
|
1141
|
+
continue;
|
|
1142
|
+
if (!entry.file)
|
|
1143
|
+
return true; // Rule globally ignored
|
|
1144
|
+
const normalizedEntryFile = resolve(entry.file);
|
|
1145
|
+
const normalizedFindingFile = resolve(filePath);
|
|
1146
|
+
if (normalizedFindingFile !== normalizedEntryFile)
|
|
1147
|
+
continue;
|
|
1148
|
+
if (!entry.line)
|
|
1149
|
+
return true; // Rule ignored for this file
|
|
1150
|
+
if (entry.line === line)
|
|
1151
|
+
return true; // Rule ignored on this specific line
|
|
1152
|
+
}
|
|
1153
|
+
return false;
|
|
1154
|
+
}
|
|
1155
|
+
// ---------------------------------------------------------
|
|
1156
|
+
// Private: path exclusion
|
|
1157
|
+
// ---------------------------------------------------------
|
|
1158
|
+
isPathExcluded(filePath, patterns) {
|
|
1159
|
+
const relativePath = relative(this.rootDir, filePath);
|
|
1160
|
+
const normalizedPath = relativePath.replace(/\\/g, '/');
|
|
1161
|
+
for (const pattern of patterns) {
|
|
1162
|
+
const normalizedPattern = pattern.replace(/\\/g, '/');
|
|
1163
|
+
// Convert simple glob patterns to regex
|
|
1164
|
+
const regexPattern = normalizedPattern
|
|
1165
|
+
.replace(/\./g, '\\.')
|
|
1166
|
+
.replace(/\*\*/g, '§§') // Temporarily mark **
|
|
1167
|
+
.replace(/\*/g, '[^/]*')
|
|
1168
|
+
.replace(/§§/g, '.*')
|
|
1169
|
+
.replace(/\?/g, '[^/]');
|
|
1170
|
+
try {
|
|
1171
|
+
const regex = new RegExp(regexPattern, 'i');
|
|
1172
|
+
if (regex.test(normalizedPath) || regex.test(filePath.replace(/\\/g, '/'))) {
|
|
1173
|
+
return true;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
catch {
|
|
1177
|
+
// Skip invalid patterns
|
|
1178
|
+
}
|
|
1179
|
+
// Also check direct segment match
|
|
1180
|
+
const segments = normalizedPattern.split('/');
|
|
1181
|
+
for (const segment of segments) {
|
|
1182
|
+
if (segment &&
|
|
1183
|
+
!segment.includes('*') &&
|
|
1184
|
+
normalizedPath.includes(segment)) {
|
|
1185
|
+
// Verify it's a real segment match, not a substring
|
|
1186
|
+
const pathParts = normalizedPath.split('/');
|
|
1187
|
+
if (pathParts.includes(segment)) {
|
|
1188
|
+
return true;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
// ---------------------------------------------------------
|
|
1196
|
+
// Private: confidence assessment
|
|
1197
|
+
// ---------------------------------------------------------
|
|
1198
|
+
assessConfidence(rule, line, filePath) {
|
|
1199
|
+
const trimmedLine = line.trim();
|
|
1200
|
+
// Comments are low confidence
|
|
1201
|
+
if (trimmedLine.startsWith('//') ||
|
|
1202
|
+
trimmedLine.startsWith('#') ||
|
|
1203
|
+
trimmedLine.startsWith('/*') ||
|
|
1204
|
+
trimmedLine.startsWith('*') ||
|
|
1205
|
+
trimmedLine.startsWith('--') ||
|
|
1206
|
+
trimmedLine.startsWith('<!--')) {
|
|
1207
|
+
return 'low';
|
|
1208
|
+
}
|
|
1209
|
+
// String literals in test files are lower confidence
|
|
1210
|
+
const ext = extname(filePath);
|
|
1211
|
+
const baseName = basename(filePath).toLowerCase();
|
|
1212
|
+
const isTestFile = baseName.includes('.test.') ||
|
|
1213
|
+
baseName.includes('.spec.') ||
|
|
1214
|
+
baseName.includes('_test.') ||
|
|
1215
|
+
baseName.includes('test_') ||
|
|
1216
|
+
baseName.includes('.e2e.') ||
|
|
1217
|
+
baseName.includes('mock') ||
|
|
1218
|
+
baseName.includes('fixture') ||
|
|
1219
|
+
baseName.includes('stub');
|
|
1220
|
+
if (isTestFile) {
|
|
1221
|
+
return 'low';
|
|
1222
|
+
}
|
|
1223
|
+
// Example/doc files are lower confidence
|
|
1224
|
+
const isDocFile = baseName.includes('readme') ||
|
|
1225
|
+
baseName.includes('example') ||
|
|
1226
|
+
baseName.includes('demo') ||
|
|
1227
|
+
ext === '.md';
|
|
1228
|
+
if (isDocFile) {
|
|
1229
|
+
return 'low';
|
|
1230
|
+
}
|
|
1231
|
+
// Configuration files often have false positives
|
|
1232
|
+
const isConfigFile = ext === '.json' ||
|
|
1233
|
+
ext === '.yaml' ||
|
|
1234
|
+
ext === '.yml' ||
|
|
1235
|
+
ext === '.toml' ||
|
|
1236
|
+
ext === '.ini' ||
|
|
1237
|
+
ext === '.cfg' ||
|
|
1238
|
+
ext === '.env';
|
|
1239
|
+
if (isConfigFile && rule.category === 'hardcoded-secrets') {
|
|
1240
|
+
return 'medium';
|
|
1241
|
+
}
|
|
1242
|
+
// Variable assignment patterns are high confidence for secrets
|
|
1243
|
+
if (rule.category === 'hardcoded-secrets' &&
|
|
1244
|
+
(trimmedLine.includes('=') || trimmedLine.includes(':'))) {
|
|
1245
|
+
// Check if it's an actual assignment (not a comparison)
|
|
1246
|
+
const hasSingleEquals = trimmedLine.includes('=') && !trimmedLine.includes('==');
|
|
1247
|
+
const hasColon = trimmedLine.includes(':');
|
|
1248
|
+
if (hasSingleEquals || hasColon) {
|
|
1249
|
+
return 'high';
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
// Injection patterns with user input references are high confidence
|
|
1253
|
+
if (rule.category === 'injection' &&
|
|
1254
|
+
(trimmedLine.includes('req.') ||
|
|
1255
|
+
trimmedLine.includes('request.') ||
|
|
1256
|
+
trimmedLine.includes('params.') ||
|
|
1257
|
+
trimmedLine.includes('query.') ||
|
|
1258
|
+
trimmedLine.includes('input') ||
|
|
1259
|
+
trimmedLine.includes('user'))) {
|
|
1260
|
+
return 'high';
|
|
1261
|
+
}
|
|
1262
|
+
// Default to medium
|
|
1263
|
+
return 'medium';
|
|
1264
|
+
}
|
|
1265
|
+
// ---------------------------------------------------------
|
|
1266
|
+
// Private: ID generation
|
|
1267
|
+
// ---------------------------------------------------------
|
|
1268
|
+
generateVulnId() {
|
|
1269
|
+
this.vulnCounter++;
|
|
1270
|
+
const timestamp = Date.now().toString(36);
|
|
1271
|
+
const counter = this.vulnCounter.toString(36);
|
|
1272
|
+
const random = Math.random().toString(36).substring(2, 6);
|
|
1273
|
+
return `VUL-${timestamp}-${counter}-${random}`;
|
|
1274
|
+
}
|
|
1275
|
+
// ---------------------------------------------------------
|
|
1276
|
+
// Private: report generation
|
|
1277
|
+
// ---------------------------------------------------------
|
|
1278
|
+
generateJsonReport() {
|
|
1279
|
+
const result = {
|
|
1280
|
+
totalVulnerabilities: this.vulnerabilities.length,
|
|
1281
|
+
critical: this.vulnerabilities.filter((v) => v.severity === 'critical')
|
|
1282
|
+
.length,
|
|
1283
|
+
high: this.vulnerabilities.filter((v) => v.severity === 'high').length,
|
|
1284
|
+
medium: this.vulnerabilities.filter((v) => v.severity === 'medium')
|
|
1285
|
+
.length,
|
|
1286
|
+
low: this.vulnerabilities.filter((v) => v.severity === 'low').length,
|
|
1287
|
+
info: this.vulnerabilities.filter((v) => v.severity === 'info').length,
|
|
1288
|
+
files: new Set(this.vulnerabilities.map((v) => v.file)).size,
|
|
1289
|
+
duration: 0,
|
|
1290
|
+
vulnerabilities: this.vulnerabilities,
|
|
1291
|
+
};
|
|
1292
|
+
return JSON.stringify({
|
|
1293
|
+
scanner: 'NeuroCLI Security Scanner',
|
|
1294
|
+
version: '3.0.0',
|
|
1295
|
+
timestamp: new Date().toISOString(),
|
|
1296
|
+
cvssLikeScore: this.getCVSSLikeScore(),
|
|
1297
|
+
result,
|
|
1298
|
+
}, null, 2);
|
|
1299
|
+
}
|
|
1300
|
+
generateMarkdownReport() {
|
|
1301
|
+
const counts = this.getSeverityCounts();
|
|
1302
|
+
const score = this.getCVSSLikeScore();
|
|
1303
|
+
const lines = [];
|
|
1304
|
+
lines.push('# NeuroCLI Security Scan Report');
|
|
1305
|
+
lines.push('');
|
|
1306
|
+
lines.push(`**Generated:** ${new Date().toISOString()}`);
|
|
1307
|
+
lines.push(`**Scanner:** NeuroCLI Security Scanner v3.0.0`);
|
|
1308
|
+
lines.push(`**CVSS-like Score:** ${score}/10`);
|
|
1309
|
+
lines.push('');
|
|
1310
|
+
// Summary table
|
|
1311
|
+
lines.push('## Summary');
|
|
1312
|
+
lines.push('');
|
|
1313
|
+
lines.push('| Severity | Count |');
|
|
1314
|
+
lines.push('|----------|-------|');
|
|
1315
|
+
lines.push(`| 🔴 Critical | ${counts.critical} |`);
|
|
1316
|
+
lines.push(`| 🟠 High | ${counts.high} |`);
|
|
1317
|
+
lines.push(`| 🟡 Medium | ${counts.medium} |`);
|
|
1318
|
+
lines.push(`| 🔵 Low | ${counts.low} |`);
|
|
1319
|
+
lines.push(`| ⚪ Info | ${counts.info} |`);
|
|
1320
|
+
lines.push(`| **Total** | **${counts.total}** |`);
|
|
1321
|
+
lines.push('');
|
|
1322
|
+
// Group by severity
|
|
1323
|
+
const severities = ['critical', 'high', 'medium', 'low', 'info'];
|
|
1324
|
+
const severityEmoji = {
|
|
1325
|
+
critical: '🔴',
|
|
1326
|
+
high: '🟠',
|
|
1327
|
+
medium: '🟡',
|
|
1328
|
+
low: '🔵',
|
|
1329
|
+
info: '⚪',
|
|
1330
|
+
};
|
|
1331
|
+
for (const severity of severities) {
|
|
1332
|
+
const vulns = this.vulnerabilities.filter((v) => v.severity === severity);
|
|
1333
|
+
if (vulns.length === 0)
|
|
1334
|
+
continue;
|
|
1335
|
+
lines.push(`## ${severityEmoji[severity]} ${severity.charAt(0).toUpperCase() + severity.slice(1)} Severity`);
|
|
1336
|
+
lines.push('');
|
|
1337
|
+
// Group by category
|
|
1338
|
+
const byCategory = new Map();
|
|
1339
|
+
for (const vuln of vulns) {
|
|
1340
|
+
const existing = byCategory.get(vuln.category) || [];
|
|
1341
|
+
existing.push(vuln);
|
|
1342
|
+
byCategory.set(vuln.category, existing);
|
|
1343
|
+
}
|
|
1344
|
+
for (const [category, categoryVulns] of Array.from(byCategory.entries())) {
|
|
1345
|
+
lines.push(`### ${category.charAt(0).toUpperCase() + category.slice(1).replace(/-/g, ' ')}`);
|
|
1346
|
+
lines.push('');
|
|
1347
|
+
for (const vuln of categoryVulns) {
|
|
1348
|
+
lines.push(`#### \`${vuln.ruleId}\` - ${vuln.title}`);
|
|
1349
|
+
lines.push('');
|
|
1350
|
+
lines.push(`- **File:** \`${vuln.file}\``);
|
|
1351
|
+
lines.push(`- **Line:** ${vuln.line}, Column ${vuln.column}`);
|
|
1352
|
+
lines.push(`- **Confidence:** ${vuln.confidence}`);
|
|
1353
|
+
if (vuln.cwe)
|
|
1354
|
+
lines.push(`- **CWE:** ${vuln.cwe}`);
|
|
1355
|
+
if (vuln.owasp)
|
|
1356
|
+
lines.push(`- **OWASP:** ${vuln.owasp}`);
|
|
1357
|
+
lines.push('');
|
|
1358
|
+
lines.push(vuln.description);
|
|
1359
|
+
lines.push('');
|
|
1360
|
+
lines.push('**Remediation:**');
|
|
1361
|
+
lines.push(vuln.remediation);
|
|
1362
|
+
lines.push('');
|
|
1363
|
+
lines.push('---');
|
|
1364
|
+
lines.push('');
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
if (this.vulnerabilities.length === 0) {
|
|
1369
|
+
lines.push('## ✅ No vulnerabilities found');
|
|
1370
|
+
lines.push('');
|
|
1371
|
+
lines.push('Your codebase appears to be free of the security issues checked by the scanner.');
|
|
1372
|
+
}
|
|
1373
|
+
return lines.join('\n');
|
|
1374
|
+
}
|
|
1375
|
+
generateSarifReport() {
|
|
1376
|
+
const sarif = {
|
|
1377
|
+
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json',
|
|
1378
|
+
version: '2.1.0',
|
|
1379
|
+
runs: [
|
|
1380
|
+
{
|
|
1381
|
+
tool: {
|
|
1382
|
+
driver: {
|
|
1383
|
+
name: 'NeuroCLI Security Scanner',
|
|
1384
|
+
version: '3.0.0',
|
|
1385
|
+
informationUri: 'https://neurocli.dev/docs/security',
|
|
1386
|
+
rules: this.rules.map((rule) => ({
|
|
1387
|
+
id: rule.id,
|
|
1388
|
+
name: rule.name,
|
|
1389
|
+
shortDescription: {
|
|
1390
|
+
text: rule.description,
|
|
1391
|
+
},
|
|
1392
|
+
fullDescription: {
|
|
1393
|
+
text: rule.description,
|
|
1394
|
+
},
|
|
1395
|
+
helpUri: rule.cwe
|
|
1396
|
+
? `https://cwe.mitre.org/data/definitions/${rule.cwe.replace('CWE-', '')}.html`
|
|
1397
|
+
: undefined,
|
|
1398
|
+
properties: {
|
|
1399
|
+
category: rule.category,
|
|
1400
|
+
severity: rule.severity,
|
|
1401
|
+
'security-severity': SEVERITY_WEIGHTS[rule.severity].toString(),
|
|
1402
|
+
cwe: rule.cwe,
|
|
1403
|
+
owasp: rule.owasp,
|
|
1404
|
+
},
|
|
1405
|
+
defaultConfiguration: {
|
|
1406
|
+
level: this.severityToSarifLevel(rule.severity),
|
|
1407
|
+
},
|
|
1408
|
+
})),
|
|
1409
|
+
},
|
|
1410
|
+
},
|
|
1411
|
+
results: this.vulnerabilities.map((vuln) => ({
|
|
1412
|
+
ruleId: vuln.ruleId,
|
|
1413
|
+
level: this.severityToSarifLevel(vuln.severity),
|
|
1414
|
+
message: {
|
|
1415
|
+
text: vuln.description,
|
|
1416
|
+
},
|
|
1417
|
+
locations: [
|
|
1418
|
+
{
|
|
1419
|
+
physicalLocation: {
|
|
1420
|
+
artifactLocation: {
|
|
1421
|
+
uri: vuln.file.replace(/\\/g, '/'),
|
|
1422
|
+
},
|
|
1423
|
+
region: {
|
|
1424
|
+
startLine: vuln.line,
|
|
1425
|
+
startColumn: vuln.column,
|
|
1426
|
+
},
|
|
1427
|
+
},
|
|
1428
|
+
},
|
|
1429
|
+
],
|
|
1430
|
+
properties: {
|
|
1431
|
+
confidence: vuln.confidence,
|
|
1432
|
+
remediation: vuln.remediation,
|
|
1433
|
+
cwe: vuln.cwe,
|
|
1434
|
+
owasp: vuln.owasp,
|
|
1435
|
+
},
|
|
1436
|
+
})),
|
|
1437
|
+
},
|
|
1438
|
+
],
|
|
1439
|
+
};
|
|
1440
|
+
return JSON.stringify(sarif, null, 2);
|
|
1441
|
+
}
|
|
1442
|
+
severityToSarifLevel(severity) {
|
|
1443
|
+
switch (severity) {
|
|
1444
|
+
case 'critical':
|
|
1445
|
+
case 'high':
|
|
1446
|
+
return 'error';
|
|
1447
|
+
case 'medium':
|
|
1448
|
+
return 'warning';
|
|
1449
|
+
case 'low':
|
|
1450
|
+
return 'note';
|
|
1451
|
+
case 'info':
|
|
1452
|
+
return 'none';
|
|
1453
|
+
default:
|
|
1454
|
+
return 'warning';
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
// -----------------------------------------------------------
|
|
1459
|
+
// Convenience factory
|
|
1460
|
+
// -----------------------------------------------------------
|
|
1461
|
+
/**
|
|
1462
|
+
* Create a pre-configured SecurityScanner instance.
|
|
1463
|
+
*/
|
|
1464
|
+
export function createSecurityScanner(rootDir, config) {
|
|
1465
|
+
return new SecurityScanner(rootDir, config);
|
|
1466
|
+
}
|
|
1467
|
+
/**
|
|
1468
|
+
* Quick-scan a single file and return findings.
|
|
1469
|
+
*/
|
|
1470
|
+
export function quickScanFile(filePath) {
|
|
1471
|
+
const scanner = new SecurityScanner();
|
|
1472
|
+
return scanner.scanFile(filePath);
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* Quick-scan a directory and return a full result.
|
|
1476
|
+
*/
|
|
1477
|
+
export function quickScanDirectory(dirPath, options) {
|
|
1478
|
+
const scanner = new SecurityScanner(dirPath);
|
|
1479
|
+
return scanner.scanDirectory(dirPath, options);
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* Quick-scan a diff and return findings.
|
|
1483
|
+
*/
|
|
1484
|
+
export function quickScanDiff(diff) {
|
|
1485
|
+
const scanner = new SecurityScanner();
|
|
1486
|
+
return scanner.scanDiff(diff);
|
|
1487
|
+
}
|
|
1488
|
+
/**
|
|
1489
|
+
* Get the default set of security rules.
|
|
1490
|
+
*/
|
|
1491
|
+
export function getDefaultSecurityRules() {
|
|
1492
|
+
return getDefaultRules();
|
|
1493
|
+
}
|
|
1494
|
+
//# sourceMappingURL=security-scanner.js.map
|