@datahogo/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 +661 -0
- package/README.md +17 -0
- package/dist/engines/config.d.ts +3 -0
- package/dist/engines/config.d.ts.map +1 -0
- package/dist/engines/config.js +433 -0
- package/dist/engines/config.js.map +1 -0
- package/dist/engines/dast.d.ts +3 -0
- package/dist/engines/dast.d.ts.map +1 -0
- package/dist/engines/dast.js +167 -0
- package/dist/engines/dast.js.map +1 -0
- package/dist/engines/db-rules.d.ts +3 -0
- package/dist/engines/db-rules.d.ts.map +1 -0
- package/dist/engines/db-rules.js +166 -0
- package/dist/engines/db-rules.js.map +1 -0
- package/dist/engines/dependencies.d.ts +3 -0
- package/dist/engines/dependencies.d.ts.map +1 -0
- package/dist/engines/dependencies.js +167 -0
- package/dist/engines/dependencies.js.map +1 -0
- package/dist/engines/github-actions.d.ts +3 -0
- package/dist/engines/github-actions.d.ts.map +1 -0
- package/dist/engines/github-actions.js +215 -0
- package/dist/engines/github-actions.js.map +1 -0
- package/dist/engines/gitleaks.d.ts +3 -0
- package/dist/engines/gitleaks.d.ts.map +1 -0
- package/dist/engines/gitleaks.js +107 -0
- package/dist/engines/gitleaks.js.map +1 -0
- package/dist/engines/npm-audit.d.ts +3 -0
- package/dist/engines/npm-audit.d.ts.map +1 -0
- package/dist/engines/npm-audit.js +113 -0
- package/dist/engines/npm-audit.js.map +1 -0
- package/dist/engines/patterns.d.ts +3 -0
- package/dist/engines/patterns.d.ts.map +1 -0
- package/dist/engines/patterns.js +1330 -0
- package/dist/engines/patterns.js.map +1 -0
- package/dist/engines/secrets.d.ts +4 -0
- package/dist/engines/secrets.d.ts.map +1 -0
- package/dist/engines/secrets.js +260 -0
- package/dist/engines/secrets.js.map +1 -0
- package/dist/engines/semgrep.d.ts +3 -0
- package/dist/engines/semgrep.d.ts.map +1 -0
- package/dist/engines/semgrep.js +173 -0
- package/dist/engines/semgrep.js.map +1 -0
- package/dist/engines/types.d.ts +30 -0
- package/dist/engines/types.d.ts.map +1 -0
- package/dist/engines/types.js +3 -0
- package/dist/engines/types.js.map +1 -0
- package/dist/engines/url-scanner.d.ts +3 -0
- package/dist/engines/url-scanner.d.ts.map +1 -0
- package/dist/engines/url-scanner.js +469 -0
- package/dist/engines/url-scanner.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestrator.d.ts +32 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +237 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/rules/.gitkeep +0 -0
- package/dist/rules/ai-llm-security.yaml +199 -0
- package/dist/rules/api-security.yaml +489 -0
- package/dist/rules/auth-patterns.yaml +406 -0
- package/dist/rules/crypto-patterns.yaml +227 -0
- package/dist/rules/database-cloud-security.yaml +169 -0
- package/dist/rules/general-security.yaml +588 -0
- package/dist/rules/injection-patterns.yaml +318 -0
- package/dist/rules/nextjs-security.yaml +532 -0
- package/dist/rules/node-security.yaml +380 -0
- package/dist/rules/rules/.gitkeep +0 -0
- package/dist/rules/rules/ai-llm-security.yaml +199 -0
- package/dist/rules/rules/api-security.yaml +489 -0
- package/dist/rules/rules/auth-patterns.yaml +406 -0
- package/dist/rules/rules/crypto-patterns.yaml +227 -0
- package/dist/rules/rules/database-cloud-security.yaml +169 -0
- package/dist/rules/rules/general-security.yaml +588 -0
- package/dist/rules/rules/injection-patterns.yaml +318 -0
- package/dist/rules/rules/nextjs-security.yaml +532 -0
- package/dist/rules/rules/node-security.yaml +380 -0
- package/dist/rules/rules/supabase-security.yaml +387 -0
- package/dist/rules/supabase-security.yaml +387 -0
- package/dist/scanning/agents/csharp-agent.d.ts +12 -0
- package/dist/scanning/agents/csharp-agent.d.ts.map +1 -0
- package/dist/scanning/agents/csharp-agent.js +592 -0
- package/dist/scanning/agents/csharp-agent.js.map +1 -0
- package/dist/scanning/agents/go-agent.d.ts +14 -0
- package/dist/scanning/agents/go-agent.d.ts.map +1 -0
- package/dist/scanning/agents/go-agent.js +641 -0
- package/dist/scanning/agents/go-agent.js.map +1 -0
- package/dist/scanning/agents/java-agent.d.ts +8 -0
- package/dist/scanning/agents/java-agent.d.ts.map +1 -0
- package/dist/scanning/agents/java-agent.js +807 -0
- package/dist/scanning/agents/java-agent.js.map +1 -0
- package/dist/scanning/agents/javascript-agent.d.ts +8 -0
- package/dist/scanning/agents/javascript-agent.d.ts.map +1 -0
- package/dist/scanning/agents/javascript-agent.js +77 -0
- package/dist/scanning/agents/javascript-agent.js.map +1 -0
- package/dist/scanning/agents/mobile-agent.d.ts +8 -0
- package/dist/scanning/agents/mobile-agent.d.ts.map +1 -0
- package/dist/scanning/agents/mobile-agent.js +916 -0
- package/dist/scanning/agents/mobile-agent.js.map +1 -0
- package/dist/scanning/agents/php-agent.d.ts +8 -0
- package/dist/scanning/agents/php-agent.d.ts.map +1 -0
- package/dist/scanning/agents/php-agent.js +679 -0
- package/dist/scanning/agents/php-agent.js.map +1 -0
- package/dist/scanning/agents/python-agent.d.ts +8 -0
- package/dist/scanning/agents/python-agent.d.ts.map +1 -0
- package/dist/scanning/agents/python-agent.js +701 -0
- package/dist/scanning/agents/python-agent.js.map +1 -0
- package/dist/scanning/agents/supabase-agent.d.ts +8 -0
- package/dist/scanning/agents/supabase-agent.d.ts.map +1 -0
- package/dist/scanning/agents/supabase-agent.js +484 -0
- package/dist/scanning/agents/supabase-agent.js.map +1 -0
- package/dist/scanning/orchestrator.d.ts +29 -0
- package/dist/scanning/orchestrator.d.ts.map +1 -0
- package/dist/scanning/orchestrator.js +186 -0
- package/dist/scanning/orchestrator.js.map +1 -0
- package/dist/scanning/tech-detector.d.ts +12 -0
- package/dist/scanning/tech-detector.d.ts.map +1 -0
- package/dist/scanning/tech-detector.js +252 -0
- package/dist/scanning/tech-detector.js.map +1 -0
- package/dist/scanning/types.d.ts +87 -0
- package/dist/scanning/types.d.ts.map +1 -0
- package/dist/scanning/types.js +5 -0
- package/dist/scanning/types.js.map +1 -0
- package/dist/utils/child-process.d.ts +2 -0
- package/dist/utils/child-process.d.ts.map +1 -0
- package/dist/utils/child-process.js +4 -0
- package/dist/utils/child-process.js.map +1 -0
- package/dist/utils/context-classifier.d.ts +11 -0
- package/dist/utils/context-classifier.d.ts.map +1 -0
- package/dist/utils/context-classifier.js +79 -0
- package/dist/utils/context-classifier.js.map +1 -0
- package/dist/utils/exec.d.ts +18 -0
- package/dist/utils/exec.d.ts.map +1 -0
- package/dist/utils/exec.js +51 -0
- package/dist/utils/exec.js.map +1 -0
- package/dist/utils/file-filter.d.ts +4 -0
- package/dist/utils/file-filter.d.ts.map +1 -0
- package/dist/utils/file-filter.js +68 -0
- package/dist/utils/file-filter.js.map +1 -0
- package/dist/utils/fs.d.ts +2 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +5 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +27 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/post-processor.d.ts +10 -0
- package/dist/utils/post-processor.d.ts.map +1 -0
- package/dist/utils/post-processor.js +183 -0
- package/dist/utils/post-processor.js.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,807 @@
|
|
|
1
|
+
// Java ecosystem security scanner agent.
|
|
2
|
+
// Detects Java/Kotlin projects (Maven, Gradle) and scans for Spring
|
|
3
|
+
// misconfigurations, deserialization issues, injection vectors,
|
|
4
|
+
// weak cryptography, and common Spring Security pitfalls.
|
|
5
|
+
export class JavaScanAgent {
|
|
6
|
+
async detect(files) {
|
|
7
|
+
for (const filePath of files.keys()) {
|
|
8
|
+
if (filePath === "pom.xml" ||
|
|
9
|
+
filePath === "build.gradle" ||
|
|
10
|
+
filePath === "build.gradle.kts" ||
|
|
11
|
+
filePath.endsWith("/pom.xml") ||
|
|
12
|
+
filePath.endsWith("/build.gradle") ||
|
|
13
|
+
filePath.endsWith("/build.gradle.kts")) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
async scan(files) {
|
|
20
|
+
const results = [];
|
|
21
|
+
// Detect project-level security infrastructure once and pass to checks
|
|
22
|
+
// that may want to downgrade confidence based on it.
|
|
23
|
+
const projectSecurity = detectProjectLevelSecurity(files);
|
|
24
|
+
results.push(...checkActuatorExposed(files));
|
|
25
|
+
results.push(...checkLog4Shell(files));
|
|
26
|
+
results.push(...checkUnsafeDeserialization(files));
|
|
27
|
+
results.push(...checkSqlInjectionQuery(files));
|
|
28
|
+
results.push(...checkCredentialsInConfig(files));
|
|
29
|
+
results.push(...checkCsrfDisabled(files));
|
|
30
|
+
results.push(...checkXxeVulnerability(files));
|
|
31
|
+
results.push(...checkRequestMappingWithoutMethod(files));
|
|
32
|
+
results.push(...checkPermitAllOnSensitiveRoutes(files));
|
|
33
|
+
results.push(...checkWeakHashAlgorithm(files));
|
|
34
|
+
results.push(...checkCommandInjection(files));
|
|
35
|
+
results.push(...checkCorsWildcard(files));
|
|
36
|
+
results.push(...checkHardcodedJwtSecret(files));
|
|
37
|
+
results.push(...checkStackTraceInResponse(files));
|
|
38
|
+
results.push(...checkSpringSecurityDebug(files));
|
|
39
|
+
results.push(...checkInsecureRandom(files));
|
|
40
|
+
results.push(...checkMassAssignment(files));
|
|
41
|
+
// If project-wide Spring Security is active, downgrade auth-absence
|
|
42
|
+
// findings to "low" confidence (the framework may handle auth globally).
|
|
43
|
+
if (projectSecurity.hasSpringSecurityConfig) {
|
|
44
|
+
for (const result of results) {
|
|
45
|
+
if (result.checkId === "java:permit-all-sensitive" ||
|
|
46
|
+
result.checkId === "java:request-mapping-no-method") {
|
|
47
|
+
result.confidence = "low";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// If project has rate-limiting libraries, suppress rate-limiting findings
|
|
52
|
+
// (bucket4j / resilience4j / Spring @RateLimiter).
|
|
53
|
+
// (No explicit rate-limit check yet, but leaving the hook for future checks.)
|
|
54
|
+
return results;
|
|
55
|
+
}
|
|
56
|
+
getMetadata() {
|
|
57
|
+
return {
|
|
58
|
+
name: "java-agent",
|
|
59
|
+
version: "1.0.0",
|
|
60
|
+
technologies: ["java", "spring", "kotlin"],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
getChecks() {
|
|
64
|
+
return [
|
|
65
|
+
{ id: "java:actuator-exposed", name: "Spring Actuator endpoints exposed", severity: "HIGH" },
|
|
66
|
+
{ id: "java:log4shell", name: "Log4Shell JNDI injection", severity: "CRITICAL" },
|
|
67
|
+
{ id: "java:unsafe-deserialization", name: "Unsafe Java deserialization", severity: "CRITICAL" },
|
|
68
|
+
{ id: "java:sql-injection-query", name: "SQL injection via @Query", severity: "CRITICAL" },
|
|
69
|
+
{ id: "java:credentials-in-config", name: "Hardcoded credentials in config", severity: "CRITICAL" },
|
|
70
|
+
{ id: "java:csrf-disabled", name: "CSRF protection disabled", severity: "HIGH" },
|
|
71
|
+
{ id: "java:xxe", name: "XML External Entity (XXE) vulnerability", severity: "HIGH" },
|
|
72
|
+
{ id: "java:request-mapping-no-method", name: "@RequestMapping without HTTP method restriction", severity: "MEDIUM" },
|
|
73
|
+
{ id: "java:permit-all-sensitive", name: "Sensitive route accessible without authentication", severity: "HIGH" },
|
|
74
|
+
{ id: "java:weak-hash", name: "Weak hash algorithm (MD5/SHA-1)", severity: "HIGH" },
|
|
75
|
+
{ id: "java:command-injection", name: "Command injection via Runtime.exec()", severity: "CRITICAL" },
|
|
76
|
+
{ id: "java:cors-wildcard", name: "CORS wildcard allows all origins", severity: "MEDIUM" },
|
|
77
|
+
{ id: "java:hardcoded-jwt-secret", name: "Hardcoded JWT secret", severity: "CRITICAL" },
|
|
78
|
+
{ id: "java:stack-trace-exposure", name: "Stack trace exposed via printStackTrace()", severity: "MEDIUM" },
|
|
79
|
+
{ id: "java:spring-security-debug", name: "Spring Security debug mode enabled", severity: "HIGH" },
|
|
80
|
+
{ id: "java:insecure-random", name: "java.util.Random used for security-sensitive value", severity: "HIGH" },
|
|
81
|
+
{ id: "java:mass-assignment", name: "Mass assignment via unvalidated model binding", severity: "MEDIUM" },
|
|
82
|
+
];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// --- Helpers ---
|
|
86
|
+
function isJavaFile(filePath) {
|
|
87
|
+
return filePath.endsWith(".java");
|
|
88
|
+
}
|
|
89
|
+
function isConfigFile(filePath) {
|
|
90
|
+
return (filePath.endsWith(".properties") ||
|
|
91
|
+
filePath.endsWith(".yml") ||
|
|
92
|
+
filePath.endsWith(".yaml"));
|
|
93
|
+
}
|
|
94
|
+
function detectProjectLevelSecurity(files) {
|
|
95
|
+
let hasSpringSecurityConfig = false;
|
|
96
|
+
let hasRateLimiting = false;
|
|
97
|
+
for (const [filePath, content] of files) {
|
|
98
|
+
if (!isJavaFile(filePath))
|
|
99
|
+
continue;
|
|
100
|
+
if (/@EnableWebSecurity/.test(content) ||
|
|
101
|
+
/@EnableGlobalMethodSecurity/.test(content) ||
|
|
102
|
+
/@EnableMethodSecurity/.test(content)) {
|
|
103
|
+
hasSpringSecurityConfig = true;
|
|
104
|
+
}
|
|
105
|
+
if (/@RateLimiter/.test(content) ||
|
|
106
|
+
/bucket4j/.test(content) ||
|
|
107
|
+
/resilience4j/.test(content)) {
|
|
108
|
+
hasRateLimiting = true;
|
|
109
|
+
}
|
|
110
|
+
if (hasSpringSecurityConfig && hasRateLimiting)
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
return { hasSpringSecurityConfig, hasRateLimiting };
|
|
114
|
+
}
|
|
115
|
+
// --- Check 1: Spring Actuator exposed ---
|
|
116
|
+
// HIGH, CWE-200
|
|
117
|
+
// Detects management.endpoints.web.exposure.include with dangerous values.
|
|
118
|
+
function checkActuatorExposed(files) {
|
|
119
|
+
const results = [];
|
|
120
|
+
for (const [filePath, content] of files) {
|
|
121
|
+
if (!isConfigFile(filePath))
|
|
122
|
+
continue;
|
|
123
|
+
const lines = content.split("\n");
|
|
124
|
+
for (let i = 0; i < lines.length; i++) {
|
|
125
|
+
const line = lines[i];
|
|
126
|
+
// --- Properties file format: single line contains full dotted key ---
|
|
127
|
+
if (/management\.endpoints\.web\.exposure\.include/.test(line)) {
|
|
128
|
+
// Handle both properties format (=*) and inline YAML (: * or : "*")
|
|
129
|
+
if (/[=:]\s*["']?\*["']?/.test(line) || /[=:]\s*.*(?:env|configprops|heapdump)/.test(line)) {
|
|
130
|
+
results.push(buildActuatorFinding(filePath, i + 1));
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
// --- YAML multi-line format: look for `include:` key with dangerous value
|
|
135
|
+
// when `exposure:` appeared in the preceding few lines ---
|
|
136
|
+
if (/^\s+include\s*:/.test(line)) {
|
|
137
|
+
const value = line.split(":").slice(1).join(":").trim().replace(/["']/g, "");
|
|
138
|
+
const isDangerous = value === "*" ||
|
|
139
|
+
/(?:env|configprops|heapdump)/.test(value);
|
|
140
|
+
if (isDangerous) {
|
|
141
|
+
// Confirm we are in an actuator exposure context by scanning back
|
|
142
|
+
const lookBehind = lines.slice(Math.max(0, i - 5), i).join("\n");
|
|
143
|
+
if (/exposure\s*:/.test(lookBehind) && /endpoints\s*:/.test(lookBehind)) {
|
|
144
|
+
results.push(buildActuatorFinding(filePath, i + 1));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return results;
|
|
151
|
+
}
|
|
152
|
+
function buildActuatorFinding(filePath, line) {
|
|
153
|
+
return {
|
|
154
|
+
checkId: "java:actuator-exposed",
|
|
155
|
+
title: "Spring Actuator endpoints exposed",
|
|
156
|
+
severity: "HIGH",
|
|
157
|
+
confidence: "high",
|
|
158
|
+
file: filePath,
|
|
159
|
+
line,
|
|
160
|
+
description: "Actuator endpoints are configured to expose sensitive information (*, env, configprops, or heapdump). " +
|
|
161
|
+
"These endpoints can leak environment variables, configuration properties, memory heap dumps, and other " +
|
|
162
|
+
"sensitive runtime data to anyone who can reach them.",
|
|
163
|
+
fix: "Restrict actuator endpoints: management.endpoints.web.exposure.include=health,info",
|
|
164
|
+
cwe: "CWE-200",
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// --- Check 2: Log4j / Log4Shell ---
|
|
168
|
+
// CRITICAL, CWE-917
|
|
169
|
+
// Detects the literal JNDI lookup string in Java source files.
|
|
170
|
+
function checkLog4Shell(files) {
|
|
171
|
+
const results = [];
|
|
172
|
+
for (const [filePath, content] of files) {
|
|
173
|
+
if (!isJavaFile(filePath))
|
|
174
|
+
continue;
|
|
175
|
+
const lines = content.split("\n");
|
|
176
|
+
for (let i = 0; i < lines.length; i++) {
|
|
177
|
+
const line = lines[i];
|
|
178
|
+
if (line.includes("${jndi:")) {
|
|
179
|
+
results.push({
|
|
180
|
+
checkId: "java:log4shell",
|
|
181
|
+
title: "Log4Shell JNDI lookup string detected",
|
|
182
|
+
severity: "CRITICAL",
|
|
183
|
+
confidence: "high",
|
|
184
|
+
file: filePath,
|
|
185
|
+
line: i + 1,
|
|
186
|
+
description: "A JNDI lookup string '${jndi:' was found in the source code. If this string is logged via Log4j 2.x " +
|
|
187
|
+
"before version 2.17.1, it triggers the Log4Shell vulnerability (CVE-2021-44228), enabling remote code " +
|
|
188
|
+
"execution by an attacker who can control any logged value.",
|
|
189
|
+
fix: "Update Log4j to 2.17.1+. Set log4j2.formatMsgNoLookups=true.",
|
|
190
|
+
cwe: "CWE-917",
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return results;
|
|
196
|
+
}
|
|
197
|
+
// --- Check 3: Unsafe deserialization ---
|
|
198
|
+
// CRITICAL, CWE-502
|
|
199
|
+
// Detects ObjectInputStream instantiation in Java source.
|
|
200
|
+
function checkUnsafeDeserialization(files) {
|
|
201
|
+
const results = [];
|
|
202
|
+
for (const [filePath, content] of files) {
|
|
203
|
+
if (!isJavaFile(filePath))
|
|
204
|
+
continue;
|
|
205
|
+
const lines = content.split("\n");
|
|
206
|
+
for (let i = 0; i < lines.length; i++) {
|
|
207
|
+
const line = lines[i];
|
|
208
|
+
const trimmed = line.trim();
|
|
209
|
+
// Skip comments
|
|
210
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*"))
|
|
211
|
+
continue;
|
|
212
|
+
if (/new\s+ObjectInputStream\s*\(/.test(line)) {
|
|
213
|
+
results.push({
|
|
214
|
+
checkId: "java:unsafe-deserialization",
|
|
215
|
+
title: "Unsafe Java deserialization via ObjectInputStream",
|
|
216
|
+
severity: "CRITICAL",
|
|
217
|
+
confidence: "medium",
|
|
218
|
+
file: filePath,
|
|
219
|
+
line: i + 1,
|
|
220
|
+
description: "ObjectInputStream.readObject() can execute arbitrary code when deserializing untrusted data. " +
|
|
221
|
+
"Attackers who control the serialized input can trigger gadget chains leading to remote code execution, " +
|
|
222
|
+
"arbitrary file write, or SSRF.",
|
|
223
|
+
fix: "Use JSON/Protocol Buffers instead. If ObjectInputStream is needed, use a whitelist filter.",
|
|
224
|
+
cwe: "CWE-502",
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return results;
|
|
230
|
+
}
|
|
231
|
+
// --- Check 4: SQL injection via @Query with string concatenation ---
|
|
232
|
+
// CRITICAL, CWE-89
|
|
233
|
+
// Detects @Query annotations that build queries via string concatenation
|
|
234
|
+
// but DO NOT use positional (?1, ?2) or named (:paramName) parameter markers.
|
|
235
|
+
function checkSqlInjectionQuery(files) {
|
|
236
|
+
const results = [];
|
|
237
|
+
for (const [filePath, content] of files) {
|
|
238
|
+
if (!isJavaFile(filePath))
|
|
239
|
+
continue;
|
|
240
|
+
const lines = content.split("\n");
|
|
241
|
+
for (let i = 0; i < lines.length; i++) {
|
|
242
|
+
const line = lines[i];
|
|
243
|
+
if (!/@Query\s*\(/.test(line))
|
|
244
|
+
continue;
|
|
245
|
+
// Look at the current line and the next few lines for string concatenation
|
|
246
|
+
const snippet = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
247
|
+
// Only flag actual string concatenation (the + operator after a closing quote)
|
|
248
|
+
if (!/"\s*\+/.test(snippet))
|
|
249
|
+
continue;
|
|
250
|
+
// Do NOT flag if the snippet contains named (:param) or positional (?1) markers.
|
|
251
|
+
// These indicate the query is parameterized even though there is a + in the
|
|
252
|
+
// annotation string (e.g., multi-line string with a compile-time constant).
|
|
253
|
+
if (/\?[0-9]+/.test(snippet) || /:[a-zA-Z][a-zA-Z0-9]*/.test(snippet))
|
|
254
|
+
continue;
|
|
255
|
+
results.push({
|
|
256
|
+
checkId: "java:sql-injection-query",
|
|
257
|
+
title: "SQL injection via @Query with string concatenation",
|
|
258
|
+
severity: "CRITICAL",
|
|
259
|
+
confidence: "medium",
|
|
260
|
+
file: filePath,
|
|
261
|
+
line: i + 1,
|
|
262
|
+
description: "The @Query annotation uses string concatenation (+) to build the query. " +
|
|
263
|
+
"Concatenating user-controlled values into JPQL or native SQL queries allows attackers " +
|
|
264
|
+
"to manipulate the query structure and access or modify unauthorized data.",
|
|
265
|
+
fix: 'Use named parameters: @Query("SELECT u FROM User u WHERE u.name = :name")',
|
|
266
|
+
cwe: "CWE-89",
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return results;
|
|
271
|
+
}
|
|
272
|
+
// --- Check 5: Credentials in config files ---
|
|
273
|
+
// CRITICAL, CWE-798
|
|
274
|
+
// Detects hardcoded passwords, secrets, keys, or tokens in .properties/.yml files.
|
|
275
|
+
function checkCredentialsInConfig(files) {
|
|
276
|
+
const results = [];
|
|
277
|
+
// Matches: password=secret123, spring.datasource.password: mypassword, etc.
|
|
278
|
+
// Skips: values that are property references like ${DB_PASSWORD}
|
|
279
|
+
const credentialPattern = /(?:password|secret|key|token)\s*[=:]\s*([^\s${}]{4,})/i;
|
|
280
|
+
for (const [filePath, content] of files) {
|
|
281
|
+
if (!isConfigFile(filePath))
|
|
282
|
+
continue;
|
|
283
|
+
const lines = content.split("\n");
|
|
284
|
+
for (let i = 0; i < lines.length; i++) {
|
|
285
|
+
const line = lines[i];
|
|
286
|
+
const trimmed = line.trim();
|
|
287
|
+
// Skip blank lines and comments
|
|
288
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("//"))
|
|
289
|
+
continue;
|
|
290
|
+
// Skip lines that are YAML keys with no value (e.g., "password:")
|
|
291
|
+
// Skip lines referencing env vars (${...})
|
|
292
|
+
if (trimmed.includes("${"))
|
|
293
|
+
continue;
|
|
294
|
+
const match = credentialPattern.exec(line);
|
|
295
|
+
if (match) {
|
|
296
|
+
const value = match[1];
|
|
297
|
+
// Skip obvious placeholders
|
|
298
|
+
if (/^(?:your[_-]|change[_-]me|placeholder|example|here|todo|xxx|n\/a|null|true|false|\d+)$/i.test(value)) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
results.push({
|
|
302
|
+
checkId: "java:credentials-in-config",
|
|
303
|
+
title: "Hardcoded credential in configuration file",
|
|
304
|
+
severity: "CRITICAL",
|
|
305
|
+
confidence: "high",
|
|
306
|
+
file: filePath,
|
|
307
|
+
line: i + 1,
|
|
308
|
+
description: "A password, secret, key, or token appears to be hardcoded directly in a configuration file. " +
|
|
309
|
+
"Committing credentials to source control exposes them to anyone with repository access " +
|
|
310
|
+
"and they persist in git history even after removal.",
|
|
311
|
+
fix: "Use environment variables or a vault: spring.datasource.password=${DB_PASSWORD}",
|
|
312
|
+
cwe: "CWE-798",
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return results;
|
|
318
|
+
}
|
|
319
|
+
// --- Check 6: CSRF disabled ---
|
|
320
|
+
// HIGH, CWE-352
|
|
321
|
+
// Detects Spring Security configuration that explicitly disables CSRF protection.
|
|
322
|
+
function checkCsrfDisabled(files) {
|
|
323
|
+
const results = [];
|
|
324
|
+
// Patterns for different Spring Security versions
|
|
325
|
+
const csrfDisablePatterns = [
|
|
326
|
+
/\.csrf\s*\(\s*\)\s*\.disable\s*\(\s*\)/,
|
|
327
|
+
/csrf\s*\(\s*csrf\s*->\s*csrf\.disable\s*\(\s*\)/,
|
|
328
|
+
/\.csrf\s*\(\s*AbstractHttpConfigurer\s*::\s*disable\s*\)/,
|
|
329
|
+
];
|
|
330
|
+
for (const [filePath, content] of files) {
|
|
331
|
+
if (!isJavaFile(filePath))
|
|
332
|
+
continue;
|
|
333
|
+
const lines = content.split("\n");
|
|
334
|
+
for (let i = 0; i < lines.length; i++) {
|
|
335
|
+
const line = lines[i];
|
|
336
|
+
const trimmed = line.trim();
|
|
337
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*"))
|
|
338
|
+
continue;
|
|
339
|
+
if (csrfDisablePatterns.some((pattern) => pattern.test(line))) {
|
|
340
|
+
results.push({
|
|
341
|
+
checkId: "java:csrf-disabled",
|
|
342
|
+
title: "CSRF protection disabled",
|
|
343
|
+
severity: "HIGH",
|
|
344
|
+
confidence: "high",
|
|
345
|
+
file: filePath,
|
|
346
|
+
line: i + 1,
|
|
347
|
+
description: "Spring Security's CSRF protection has been explicitly disabled. " +
|
|
348
|
+
"CSRF allows attackers to trick authenticated users into performing unintended actions " +
|
|
349
|
+
"(state-changing requests) on your application.",
|
|
350
|
+
fix: "Enable CSRF protection for browser-facing endpoints.",
|
|
351
|
+
cwe: "CWE-352",
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return results;
|
|
357
|
+
}
|
|
358
|
+
// --- Check 7: XXE vulnerability ---
|
|
359
|
+
// HIGH, CWE-611
|
|
360
|
+
// Detects DocumentBuilderFactory usage without disabling DTD features.
|
|
361
|
+
// Uses a 10-line lookahead (extended from 5) to reduce false positives on
|
|
362
|
+
// multi-statement builder setup patterns.
|
|
363
|
+
function checkXxeVulnerability(files) {
|
|
364
|
+
const results = [];
|
|
365
|
+
for (const [filePath, content] of files) {
|
|
366
|
+
if (!isJavaFile(filePath))
|
|
367
|
+
continue;
|
|
368
|
+
const lines = content.split("\n");
|
|
369
|
+
for (let i = 0; i < lines.length; i++) {
|
|
370
|
+
const line = lines[i];
|
|
371
|
+
if (!/DocumentBuilderFactory\.newInstance\s*\(\s*\)/.test(line))
|
|
372
|
+
continue;
|
|
373
|
+
// Look within 10 lines after this call for .setFeature (extended from 5)
|
|
374
|
+
const lookAhead = lines.slice(i, Math.min(i + 11, lines.length)).join("\n");
|
|
375
|
+
if (!lookAhead.includes(".setFeature")) {
|
|
376
|
+
results.push({
|
|
377
|
+
checkId: "java:xxe",
|
|
378
|
+
title: "XML External Entity (XXE) vulnerability",
|
|
379
|
+
severity: "HIGH",
|
|
380
|
+
confidence: "medium",
|
|
381
|
+
file: filePath,
|
|
382
|
+
line: i + 1,
|
|
383
|
+
description: "DocumentBuilderFactory is instantiated without disabling DTD processing. " +
|
|
384
|
+
"An attacker who can control XML input can exploit this to read local files, " +
|
|
385
|
+
"perform SSRF attacks, or cause denial of service via entity expansion.",
|
|
386
|
+
fix: 'Disable DTDs: factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)',
|
|
387
|
+
cwe: "CWE-611",
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return results;
|
|
393
|
+
}
|
|
394
|
+
// --- Check 8: @RequestMapping without HTTP method restriction ---
|
|
395
|
+
// MEDIUM, CWE-749
|
|
396
|
+
// Detects @RequestMapping annotations that omit a method= attribute.
|
|
397
|
+
// NOT flagged when the file has a class-level @Secured or @PreAuthorize
|
|
398
|
+
// combined with @RestController, which indicates the class is already
|
|
399
|
+
// protected via method security — making the HTTP method ambiguity lower risk.
|
|
400
|
+
function checkRequestMappingWithoutMethod(files) {
|
|
401
|
+
const results = [];
|
|
402
|
+
for (const [filePath, content] of files) {
|
|
403
|
+
if (!isJavaFile(filePath))
|
|
404
|
+
continue;
|
|
405
|
+
// Check file-wide for class-level security annotations alongside @RestController.
|
|
406
|
+
// If both are present, the developer is using method-security; skip this file.
|
|
407
|
+
const hasRestController = /@RestController/.test(content);
|
|
408
|
+
const hasClassLevelSecurity = /@Secured\s*\(/.test(content) || /@PreAuthorize\s*\(/.test(content);
|
|
409
|
+
if (hasRestController && hasClassLevelSecurity)
|
|
410
|
+
continue;
|
|
411
|
+
const lines = content.split("\n");
|
|
412
|
+
for (let i = 0; i < lines.length; i++) {
|
|
413
|
+
const line = lines[i];
|
|
414
|
+
// Match @RequestMapping with a path string but no method= attribute
|
|
415
|
+
if (!/@RequestMapping\s*\(/.test(line))
|
|
416
|
+
continue;
|
|
417
|
+
if (/method\s*=/.test(line))
|
|
418
|
+
continue;
|
|
419
|
+
// Only flag if it has a value/path (not bare @RequestMapping on a class without value)
|
|
420
|
+
if (!/@RequestMapping\s*\(\s*(?:value\s*=\s*)?["']/.test(line))
|
|
421
|
+
continue;
|
|
422
|
+
results.push({
|
|
423
|
+
checkId: "java:request-mapping-no-method",
|
|
424
|
+
title: "@RequestMapping without HTTP method restriction",
|
|
425
|
+
severity: "MEDIUM",
|
|
426
|
+
confidence: "low",
|
|
427
|
+
file: filePath,
|
|
428
|
+
line: i + 1,
|
|
429
|
+
description: "@RequestMapping without a method= attribute accepts all HTTP methods (GET, POST, PUT, DELETE, PATCH, etc.). " +
|
|
430
|
+
"This widens the attack surface unnecessarily. A CSRF-unprotected endpoint might inadvertently accept POST " +
|
|
431
|
+
"requests, or a read-only endpoint might accept state-changing methods.",
|
|
432
|
+
fix: "Specify HTTP method: @GetMapping, @PostMapping, or @RequestMapping(method = RequestMethod.GET)",
|
|
433
|
+
cwe: "CWE-749",
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return results;
|
|
438
|
+
}
|
|
439
|
+
// --- Check 9: permitAll on sensitive routes ---
|
|
440
|
+
// HIGH, CWE-306
|
|
441
|
+
// Heuristic: only flags `permitAll()` when it appears on the SAME LINE as a
|
|
442
|
+
// sensitive path pattern. We do NOT attempt to parse the Spring Security DSL
|
|
443
|
+
// across multiple lines because the DSL ordering matters (rules are evaluated
|
|
444
|
+
// in declaration order) and multi-line regex is too error-prone.
|
|
445
|
+
// Confidence is "low" for the same reason: a later `.authenticated()` rule
|
|
446
|
+
// could override this for some paths.
|
|
447
|
+
function checkPermitAllOnSensitiveRoutes(files) {
|
|
448
|
+
const results = [];
|
|
449
|
+
// Sensitive path fragments that should require authentication
|
|
450
|
+
const sensitivePaths = ["/admin", "/api/", "/manage", "/config", "/users", "/settings", "/account"];
|
|
451
|
+
for (const [filePath, content] of files) {
|
|
452
|
+
if (!isJavaFile(filePath))
|
|
453
|
+
continue;
|
|
454
|
+
const lines = content.split("\n");
|
|
455
|
+
for (let i = 0; i < lines.length; i++) {
|
|
456
|
+
const line = lines[i];
|
|
457
|
+
const trimmed = line.trim();
|
|
458
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*"))
|
|
459
|
+
continue;
|
|
460
|
+
// Only examine lines that contain BOTH permitAll() AND a sensitive path.
|
|
461
|
+
// Multi-line DSL chaining is intentionally ignored — too ambiguous.
|
|
462
|
+
if (!line.includes("permitAll"))
|
|
463
|
+
continue;
|
|
464
|
+
const hasSensitivePath = sensitivePaths.some((path) => line.includes(path));
|
|
465
|
+
if (hasSensitivePath) {
|
|
466
|
+
results.push({
|
|
467
|
+
checkId: "java:permit-all-sensitive",
|
|
468
|
+
title: "Sensitive route accessible without authentication",
|
|
469
|
+
severity: "HIGH",
|
|
470
|
+
confidence: "low",
|
|
471
|
+
file: filePath,
|
|
472
|
+
line: i + 1,
|
|
473
|
+
description: "A sensitive path (/admin, /api/, /manage, /config, /users, /settings, or /account) appears on the " +
|
|
474
|
+
"same line as permitAll(), suggesting it may be accessible without authentication. " +
|
|
475
|
+
"Spring Security DSL ordering matters — verify this rule is not overridden by a later authenticated() call.",
|
|
476
|
+
fix: "Require authentication for sensitive endpoints.",
|
|
477
|
+
cwe: "CWE-306",
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return results;
|
|
483
|
+
}
|
|
484
|
+
// --- Check 10: Weak hash algorithm ---
|
|
485
|
+
// HIGH, CWE-328
|
|
486
|
+
// Detects MessageDigest.getInstance calls using MD5 or SHA-1.
|
|
487
|
+
function checkWeakHashAlgorithm(files) {
|
|
488
|
+
const results = [];
|
|
489
|
+
const weakAlgorithmPattern = /MessageDigest\.getInstance\s*\(\s*["'](MD5|SHA-1|SHA1)["']\s*\)/;
|
|
490
|
+
for (const [filePath, content] of files) {
|
|
491
|
+
if (!isJavaFile(filePath))
|
|
492
|
+
continue;
|
|
493
|
+
const lines = content.split("\n");
|
|
494
|
+
for (let i = 0; i < lines.length; i++) {
|
|
495
|
+
const line = lines[i];
|
|
496
|
+
const match = weakAlgorithmPattern.exec(line);
|
|
497
|
+
if (match) {
|
|
498
|
+
results.push({
|
|
499
|
+
checkId: "java:weak-hash",
|
|
500
|
+
title: `Weak hash algorithm: ${match[1]}`,
|
|
501
|
+
severity: "HIGH",
|
|
502
|
+
confidence: "high",
|
|
503
|
+
file: filePath,
|
|
504
|
+
line: i + 1,
|
|
505
|
+
description: `${match[1]} is a cryptographically broken hash algorithm. MD5 and SHA-1 are vulnerable to collision ` +
|
|
506
|
+
"attacks and should not be used for security-sensitive purposes such as password hashing, " +
|
|
507
|
+
"digital signatures, or integrity verification.",
|
|
508
|
+
fix: 'Use SHA-256 or SHA-512: MessageDigest.getInstance("SHA-256")',
|
|
509
|
+
cwe: "CWE-328",
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return results;
|
|
515
|
+
}
|
|
516
|
+
// --- Check 11: Command injection ---
|
|
517
|
+
// CRITICAL, CWE-78
|
|
518
|
+
// Only flags Runtime.exec() or ProcessBuilder when the command is built via
|
|
519
|
+
// string concatenation (e.g. exec("cmd " + userInput)).
|
|
520
|
+
// exec(new String[]{"cmd", arg}) passes separate arguments to the OS and
|
|
521
|
+
// does NOT invoke a shell, so it is NOT flagged.
|
|
522
|
+
function checkCommandInjection(files) {
|
|
523
|
+
const results = [];
|
|
524
|
+
for (const [filePath, content] of files) {
|
|
525
|
+
if (!isJavaFile(filePath))
|
|
526
|
+
continue;
|
|
527
|
+
const lines = content.split("\n");
|
|
528
|
+
for (let i = 0; i < lines.length; i++) {
|
|
529
|
+
const line = lines[i];
|
|
530
|
+
const trimmed = line.trim();
|
|
531
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*"))
|
|
532
|
+
continue;
|
|
533
|
+
// Runtime.exec() with string concatenation — dangerous because a single
|
|
534
|
+
// concatenated string is passed to a shell and can contain metacharacters.
|
|
535
|
+
// exec(new String[]{...}) is safe; we skip those by checking that the
|
|
536
|
+
// argument is NOT an array literal (no "new String[" on the same line).
|
|
537
|
+
const hasRuntimeExec = /Runtime\.getRuntime\s*\(\s*\)\.exec\s*\(/.test(line);
|
|
538
|
+
const hasProcessBuilder = /new\s+ProcessBuilder\s*\(/.test(line);
|
|
539
|
+
if (!hasRuntimeExec && !hasProcessBuilder)
|
|
540
|
+
continue;
|
|
541
|
+
// Skip array-based exec calls — they do not invoke a shell.
|
|
542
|
+
if (/new\s+String\s*\[/.test(line))
|
|
543
|
+
continue;
|
|
544
|
+
// Only flag when string concatenation is present in the same statement.
|
|
545
|
+
if (line.includes("+")) {
|
|
546
|
+
results.push({
|
|
547
|
+
checkId: "java:command-injection",
|
|
548
|
+
title: "Command injection via Runtime.exec() with dynamic input",
|
|
549
|
+
severity: "CRITICAL",
|
|
550
|
+
confidence: "high",
|
|
551
|
+
file: filePath,
|
|
552
|
+
line: i + 1,
|
|
553
|
+
description: "Runtime.getRuntime().exec() or ProcessBuilder is called with a dynamically constructed command " +
|
|
554
|
+
"(string concatenation with +). If any part of the command string comes from user input, an attacker " +
|
|
555
|
+
"can inject shell metacharacters to execute arbitrary commands on the server.",
|
|
556
|
+
fix: "Avoid Runtime.exec(). If necessary, use ProcessBuilder with a pre-validated, fixed array of arguments.",
|
|
557
|
+
cwe: "CWE-78",
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return results;
|
|
563
|
+
}
|
|
564
|
+
// --- Check 12: CORS wildcard ---
|
|
565
|
+
// MEDIUM, CWE-942
|
|
566
|
+
// Detects Spring @CrossOrigin with wildcard origins or allowedOrigins("*").
|
|
567
|
+
function checkCorsWildcard(files) {
|
|
568
|
+
const results = [];
|
|
569
|
+
const corsPatterns = [
|
|
570
|
+
// @CrossOrigin(origins = "*")
|
|
571
|
+
/@CrossOrigin\s*\([^)]*origins\s*=\s*["']\*["']/,
|
|
572
|
+
// @CrossOrigin with no arguments (defaults to all origins)
|
|
573
|
+
/@CrossOrigin\s*(?:\(\s*\))?\s*$/,
|
|
574
|
+
// allowedOrigins("*")
|
|
575
|
+
/allowedOrigins\s*\(\s*["']\*["']\s*\)/,
|
|
576
|
+
];
|
|
577
|
+
for (const [filePath, content] of files) {
|
|
578
|
+
if (!isJavaFile(filePath))
|
|
579
|
+
continue;
|
|
580
|
+
const lines = content.split("\n");
|
|
581
|
+
for (let i = 0; i < lines.length; i++) {
|
|
582
|
+
const line = lines[i];
|
|
583
|
+
const trimmed = line.trim();
|
|
584
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*"))
|
|
585
|
+
continue;
|
|
586
|
+
if (corsPatterns.some((pattern) => pattern.test(trimmed))) {
|
|
587
|
+
results.push({
|
|
588
|
+
checkId: "java:cors-wildcard",
|
|
589
|
+
title: "CORS wildcard allows all origins",
|
|
590
|
+
severity: "MEDIUM",
|
|
591
|
+
confidence: "high",
|
|
592
|
+
file: filePath,
|
|
593
|
+
line: i + 1,
|
|
594
|
+
description: "CORS is configured to allow requests from any origin (*). " +
|
|
595
|
+
"This allows any website to make authenticated cross-origin requests to your API, " +
|
|
596
|
+
"potentially leaking user data or enabling cross-site request forgery.",
|
|
597
|
+
fix: 'Restrict CORS: @CrossOrigin(origins = "https://yourdomain.com")',
|
|
598
|
+
cwe: "CWE-942",
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return results;
|
|
604
|
+
}
|
|
605
|
+
// --- Check 13: Hardcoded JWT secret ---
|
|
606
|
+
// CRITICAL, CWE-798
|
|
607
|
+
// Detects JWT secret/signing keys hardcoded as string literals.
|
|
608
|
+
// Confidence is "high" only when the literal value is present on the same
|
|
609
|
+
// line as the jwt/JWT keyword AND is not a Spring @Value injection.
|
|
610
|
+
function checkHardcodedJwtSecret(files) {
|
|
611
|
+
const results = [];
|
|
612
|
+
// Matches: secret = "...", key = "...", signing = "..." in JWT-adjacent context
|
|
613
|
+
const jwtSecretPattern = /(?:secret|key|signing)\s*=\s*["']([^"']{8,})["']/i;
|
|
614
|
+
for (const [filePath, content] of files) {
|
|
615
|
+
if (!isJavaFile(filePath))
|
|
616
|
+
continue;
|
|
617
|
+
const lines = content.split("\n");
|
|
618
|
+
for (let i = 0; i < lines.length; i++) {
|
|
619
|
+
const line = lines[i];
|
|
620
|
+
const trimmed = line.trim();
|
|
621
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*"))
|
|
622
|
+
continue;
|
|
623
|
+
// Only flag lines in JWT-relevant context
|
|
624
|
+
if (!/jwt/i.test(line) && !/JWT/i.test(line))
|
|
625
|
+
continue;
|
|
626
|
+
// Skip @Value-injected fields — those read from environment/config
|
|
627
|
+
if (/@Value\s*\(/.test(line))
|
|
628
|
+
continue;
|
|
629
|
+
const match = jwtSecretPattern.exec(line);
|
|
630
|
+
if (match) {
|
|
631
|
+
const value = match[1];
|
|
632
|
+
// Skip obvious placeholders
|
|
633
|
+
if (/^(?:your[_-]|change[_-]me|placeholder|example|here|xxx)$/i.test(value))
|
|
634
|
+
continue;
|
|
635
|
+
results.push({
|
|
636
|
+
checkId: "java:hardcoded-jwt-secret",
|
|
637
|
+
title: "Hardcoded JWT secret in source code",
|
|
638
|
+
severity: "CRITICAL",
|
|
639
|
+
confidence: "high",
|
|
640
|
+
file: filePath,
|
|
641
|
+
line: i + 1,
|
|
642
|
+
description: "A JWT secret or signing key is hardcoded as a string literal. " +
|
|
643
|
+
"Anyone with access to the source code can forge valid JWT tokens for any user, " +
|
|
644
|
+
"including administrator accounts.",
|
|
645
|
+
fix: "Store JWT secrets in environment variables or a secrets manager.",
|
|
646
|
+
cwe: "CWE-798",
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return results;
|
|
652
|
+
}
|
|
653
|
+
// --- Check 14: Stack trace in response ---
|
|
654
|
+
// MEDIUM, CWE-209
|
|
655
|
+
// Detects printStackTrace() only when the surrounding catch block also contains
|
|
656
|
+
// a Response-related type or a return/throw statement. A bare printStackTrace()
|
|
657
|
+
// in a catch block that only logs is not flagged, because the stack trace never
|
|
658
|
+
// reaches the client in that case.
|
|
659
|
+
function checkStackTraceInResponse(files) {
|
|
660
|
+
const results = [];
|
|
661
|
+
for (const [filePath, content] of files) {
|
|
662
|
+
if (!isJavaFile(filePath))
|
|
663
|
+
continue;
|
|
664
|
+
// Skip test files — stack traces in tests are acceptable
|
|
665
|
+
if (filePath.includes("/test/") ||
|
|
666
|
+
filePath.includes("Test.java") ||
|
|
667
|
+
filePath.endsWith("Tests.java")) {
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
const lines = content.split("\n");
|
|
671
|
+
for (let i = 0; i < lines.length; i++) {
|
|
672
|
+
const line = lines[i];
|
|
673
|
+
const trimmed = line.trim();
|
|
674
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*"))
|
|
675
|
+
continue;
|
|
676
|
+
if (!/\.printStackTrace\s*\(\s*\)/.test(line))
|
|
677
|
+
continue;
|
|
678
|
+
// Scan a window of ±8 lines to find the enclosing catch block context.
|
|
679
|
+
// If the block contains Response/return/throw, the stack trace could
|
|
680
|
+
// reach the client; flag it.
|
|
681
|
+
const windowStart = Math.max(0, i - 4);
|
|
682
|
+
const windowEnd = Math.min(lines.length, i + 9);
|
|
683
|
+
const catchWindow = lines.slice(windowStart, windowEnd).join("\n");
|
|
684
|
+
const couldLeakToClient = /Response/.test(catchWindow) ||
|
|
685
|
+
/\breturn\b/.test(catchWindow) ||
|
|
686
|
+
/\bthrow\b/.test(catchWindow);
|
|
687
|
+
if (!couldLeakToClient)
|
|
688
|
+
continue;
|
|
689
|
+
results.push({
|
|
690
|
+
checkId: "java:stack-trace-exposure",
|
|
691
|
+
title: "Stack trace exposed via printStackTrace()",
|
|
692
|
+
severity: "MEDIUM",
|
|
693
|
+
confidence: "low",
|
|
694
|
+
file: filePath,
|
|
695
|
+
line: i + 1,
|
|
696
|
+
description: "printStackTrace() outputs the full exception stack trace, which may be captured in HTTP " +
|
|
697
|
+
"responses, logs visible to users, or error pages. This leaks internal implementation details " +
|
|
698
|
+
"(class names, method signatures, library versions) that help attackers map the attack surface.",
|
|
699
|
+
fix: "Log errors server-side. Return generic error messages to clients.",
|
|
700
|
+
cwe: "CWE-209",
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return results;
|
|
705
|
+
}
|
|
706
|
+
// --- Check 15: Spring Security debug mode ---
|
|
707
|
+
// HIGH, CWE-215
|
|
708
|
+
// Detects @EnableWebSecurity(debug = true).
|
|
709
|
+
function checkSpringSecurityDebug(files) {
|
|
710
|
+
const results = [];
|
|
711
|
+
const debugPattern = /@EnableWebSecurity\s*\([^)]*debug\s*=\s*true[^)]*\)/;
|
|
712
|
+
for (const [filePath, content] of files) {
|
|
713
|
+
if (!isJavaFile(filePath))
|
|
714
|
+
continue;
|
|
715
|
+
const lines = content.split("\n");
|
|
716
|
+
for (let i = 0; i < lines.length; i++) {
|
|
717
|
+
const line = lines[i];
|
|
718
|
+
const trimmed = line.trim();
|
|
719
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*"))
|
|
720
|
+
continue;
|
|
721
|
+
if (debugPattern.test(line)) {
|
|
722
|
+
results.push({
|
|
723
|
+
checkId: "java:spring-security-debug",
|
|
724
|
+
title: "Spring Security debug mode enabled",
|
|
725
|
+
severity: "HIGH",
|
|
726
|
+
confidence: "high",
|
|
727
|
+
file: filePath,
|
|
728
|
+
line: i + 1,
|
|
729
|
+
description: "@EnableWebSecurity(debug = true) logs every HTTP request including security-relevant information " +
|
|
730
|
+
"such as headers, request parameters, and filter chain decisions. In production this generates " +
|
|
731
|
+
"verbose logs that may expose sensitive data and slow down the application.",
|
|
732
|
+
fix: "Set debug=false in production: @EnableWebSecurity(debug = false)",
|
|
733
|
+
cwe: "CWE-215",
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return results;
|
|
739
|
+
}
|
|
740
|
+
// --- Check 16: Insecure randomness ---
|
|
741
|
+
// HIGH, CWE-330
|
|
742
|
+
// Detects java.util.Random used near security-sensitive variable names.
|
|
743
|
+
function checkInsecureRandom(files) {
|
|
744
|
+
const results = [];
|
|
745
|
+
for (const [filePath, content] of files) {
|
|
746
|
+
if (!isJavaFile(filePath))
|
|
747
|
+
continue;
|
|
748
|
+
const lines = content.split("\n");
|
|
749
|
+
for (let i = 0; i < lines.length; i++) {
|
|
750
|
+
const line = lines[i];
|
|
751
|
+
if (!/new\s+Random\s*\(|ThreadLocalRandom\.current\(\)/.test(line))
|
|
752
|
+
continue;
|
|
753
|
+
const contextStart = Math.max(0, i - 3);
|
|
754
|
+
const contextEnd = Math.min(lines.length, i + 3);
|
|
755
|
+
const context = lines.slice(contextStart, contextEnd).join("\n").toLowerCase();
|
|
756
|
+
if (/token|secret|key|session|nonce|salt|otp|password|csrf/.test(context)) {
|
|
757
|
+
results.push({
|
|
758
|
+
checkId: "java:insecure-random",
|
|
759
|
+
title: "java.util.Random used for security-sensitive value",
|
|
760
|
+
severity: "HIGH",
|
|
761
|
+
confidence: "medium",
|
|
762
|
+
file: filePath,
|
|
763
|
+
line: i + 1,
|
|
764
|
+
description: "java.util.Random is not cryptographically secure. For tokens, secrets, or session IDs, use java.security.SecureRandom instead.",
|
|
765
|
+
fix: "SecureRandom random = new SecureRandom(); byte[] bytes = new byte[32]; random.nextBytes(bytes);",
|
|
766
|
+
cwe: "CWE-330",
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
return results;
|
|
772
|
+
}
|
|
773
|
+
// --- Check 17: Mass assignment ---
|
|
774
|
+
// MEDIUM, CWE-915
|
|
775
|
+
// Detects @RequestBody without @Valid on the same method signature,
|
|
776
|
+
// or @ModelAttribute passed directly to repository.save().
|
|
777
|
+
function checkMassAssignment(files) {
|
|
778
|
+
const results = [];
|
|
779
|
+
for (const [filePath, content] of files) {
|
|
780
|
+
if (!isJavaFile(filePath))
|
|
781
|
+
continue;
|
|
782
|
+
const lines = content.split("\n");
|
|
783
|
+
for (let i = 0; i < lines.length; i++) {
|
|
784
|
+
const line = lines[i];
|
|
785
|
+
// @RequestBody without @Valid — spreads entire JSON body into object
|
|
786
|
+
if (/@RequestBody\s+(?!@Valid)/.test(line)) {
|
|
787
|
+
// Look ahead for repository.save() without field filtering
|
|
788
|
+
const ahead = lines.slice(i, Math.min(i + 10, lines.length)).join("\n");
|
|
789
|
+
if (/\.save\s*\(|\.saveAndFlush\s*\(|\.persist\s*\(/.test(ahead)) {
|
|
790
|
+
results.push({
|
|
791
|
+
checkId: "java:mass-assignment",
|
|
792
|
+
title: "Mass assignment — @RequestBody without @Valid to repository.save()",
|
|
793
|
+
severity: "MEDIUM",
|
|
794
|
+
confidence: "low",
|
|
795
|
+
file: filePath,
|
|
796
|
+
line: i + 1,
|
|
797
|
+
description: "The request body is bound directly to an entity and saved without validation. An attacker can submit extra fields (role, isAdmin, price) to modify unauthorized properties.",
|
|
798
|
+
fix: "Use a DTO with only the allowed fields, add @Valid for validation, and map explicitly to the entity.",
|
|
799
|
+
cwe: "CWE-915",
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return results;
|
|
806
|
+
}
|
|
807
|
+
//# sourceMappingURL=java-agent.js.map
|