security-mcp 1.1.2 → 1.1.4
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/README.md +145 -18
- package/defaults/control-catalog.json +200 -0
- package/dist/cli/index.js +82 -5
- package/dist/cli/install.js +36 -6
- package/dist/cli/onboarding.js +6 -0
- package/dist/gate/checks/auth-deep.js +231 -0
- package/dist/gate/checks/injection-deep.js +205 -0
- package/dist/gate/policy.js +5 -1
- package/dist/mcp/server.js +271 -41
- package/package.json +1 -1
- package/prompts/SECURITY_PROMPT.md +73 -0
- package/skills/appsec-code-auditor/SKILL.md +35 -0
- package/skills/auth-session-hacker/SKILL.md +26 -0
- package/skills/injection-specialist/SKILL.md +29 -0
- package/skills/pentest-infra/SKILL.md +43 -0
- package/skills/pentest-team/SKILL.md +47 -0
- package/skills/pentest-web-api/SKILL.md +53 -0
- package/skills/senior-security-engineer/SKILL.md +203 -2
package/dist/cli/install.js
CHANGED
|
@@ -29,6 +29,7 @@ function getEditorTargets(opts) {
|
|
|
29
29
|
const cursorGlobalPath = resolveHome("~/.cursor/mcp.json");
|
|
30
30
|
const cursorLocalPath = ".cursor/mcp.json";
|
|
31
31
|
const vscodePath = getVsCodeSettingsPath();
|
|
32
|
+
const windsurfPath = resolveHome("~/.windsurf/mcp.json");
|
|
32
33
|
const all = [
|
|
33
34
|
{
|
|
34
35
|
name: "Claude Code",
|
|
@@ -53,6 +54,12 @@ function getEditorTargets(opts) {
|
|
|
53
54
|
configPath: vscodePath,
|
|
54
55
|
type: "vscode-settings",
|
|
55
56
|
detected: existsSync(vscodePath)
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: "Windsurf",
|
|
60
|
+
configPath: windsurfPath,
|
|
61
|
+
type: "mcp-servers-json",
|
|
62
|
+
detected: existsSync(resolveHome("~/.windsurf"))
|
|
56
63
|
}
|
|
57
64
|
];
|
|
58
65
|
if (opts.all) {
|
|
@@ -166,11 +173,24 @@ function installSkill(dryRun) {
|
|
|
166
173
|
*/
|
|
167
174
|
// CWE-22: only alphanumeric, hyphens, and dots allowed in skill names
|
|
168
175
|
const SAFE_SKILL_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/;
|
|
176
|
+
// CWE-918: allowlist for skill download hosts — no user-controlled URLs reach the network
|
|
177
|
+
const ALLOWED_SKILL_HOSTS = new Set(["raw.githubusercontent.com", "github.com"]);
|
|
169
178
|
export async function downloadSkill(skillName, url, dryRun = false) {
|
|
170
179
|
if (!SAFE_SKILL_NAME_RE.test(skillName)) {
|
|
171
180
|
process.stdout.write(` [error] invalid skill name "${skillName}" — skipping download\n`);
|
|
172
181
|
return;
|
|
173
182
|
}
|
|
183
|
+
try {
|
|
184
|
+
const { hostname } = new URL(url);
|
|
185
|
+
if (!ALLOWED_SKILL_HOSTS.has(hostname)) {
|
|
186
|
+
process.stdout.write(` [error] blocked skill download from unauthorized host "${hostname}"\n`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
process.stdout.write(` [error] invalid skill URL "${url}" — skipping download\n`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
174
194
|
const skillDest = resolveHome(`~/.claude/skills/${skillName}/SKILL.md`);
|
|
175
195
|
if (dryRun) {
|
|
176
196
|
process.stdout.write(` [dry-run] would download skill "${skillName}" from ${url} → ${skillDest}\n`);
|
|
@@ -271,12 +291,22 @@ export async function runInstall(opts) {
|
|
|
271
291
|
process.stdout.write("\nInstalling security policy...\n");
|
|
272
292
|
installPolicy(dryRun);
|
|
273
293
|
process.stdout.write("\n");
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
294
|
+
if (dryRun) {
|
|
295
|
+
process.stdout.write("Dry-run complete. Re-run without --dry-run to apply.\n\n");
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
process.stdout.write("Installation complete!\n");
|
|
277
299
|
process.stdout.write(`Install mode: ${opts.useGlobalBinary ? "global binary (security-mcp serve)" : "npx (npx -y security-mcp@latest serve)"}\n`);
|
|
278
300
|
process.stdout.write("\nNext steps:\n");
|
|
279
|
-
process.stdout.write(" 1. Restart your editor.\n");
|
|
280
|
-
process.stdout.write(
|
|
281
|
-
process.stdout.write(
|
|
301
|
+
process.stdout.write(" 1. Restart your editor (fully quit and reopen — not just reload window).\n");
|
|
302
|
+
process.stdout.write(" 2. Verify the server loaded:\n");
|
|
303
|
+
process.stdout.write(" Claude Code: type /mcp and confirm security-mcp is listed as Connected.\n");
|
|
304
|
+
process.stdout.write(" Cursor: Settings > MCP > security-mcp should show as active.\n");
|
|
305
|
+
process.stdout.write(" 3. Run your first security review:\n");
|
|
306
|
+
process.stdout.write(" /senior-security-engineer\n");
|
|
307
|
+
process.stdout.write(" The agent will ask you to choose a scan scope (recent changes / full codebase / specific files).\n");
|
|
308
|
+
process.stdout.write(" 4. For a deep 39-agent audit before a release:\n");
|
|
309
|
+
process.stdout.write(" /ciso-orchestrator\n");
|
|
310
|
+
process.stdout.write("\nVerify this install at any time:\n");
|
|
311
|
+
process.stdout.write(" npx -y security-mcp@latest --version\n\n");
|
|
282
312
|
}
|
package/dist/cli/onboarding.js
CHANGED
|
@@ -275,8 +275,14 @@ async function fetchLatestRelease(repo) {
|
|
|
275
275
|
function pickAsset(assets, pattern) {
|
|
276
276
|
return assets.find((a) => a.name.toLowerCase().includes(pattern.toLowerCase()))?.browser_download_url;
|
|
277
277
|
}
|
|
278
|
+
const ALLOWED_BINARY_HOSTS = new Set([
|
|
279
|
+
"github.com", "objects.githubusercontent.com", "github-releases.githubusercontent.com"
|
|
280
|
+
]);
|
|
278
281
|
async function downloadBinary(url, dest) {
|
|
279
282
|
try {
|
|
283
|
+
const { hostname } = new URL(url);
|
|
284
|
+
if (!ALLOWED_BINARY_HOSTS.has(hostname))
|
|
285
|
+
return false;
|
|
280
286
|
const res = await fetch(url);
|
|
281
287
|
if (!res.ok || !res.body)
|
|
282
288
|
return false;
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep authentication and session enforcement — covers JWT, OAuth, session, and cookie
|
|
3
|
+
* attack classes not detected by existing checks.
|
|
4
|
+
* CWE references per MITRE CWE catalog; ATT&CK techniques per MITRE ATT&CK v14.
|
|
5
|
+
*/
|
|
6
|
+
import { sanitizeErrorMessage } from "../result.js";
|
|
7
|
+
import { searchRepo } from "../../repo/search.js";
|
|
8
|
+
const NON_CODE_RE = /\.(?:md|json|yaml|yml|txt|rst|toml|lock)$/i;
|
|
9
|
+
export async function checkAuthDeep(_opts) {
|
|
10
|
+
const findings = [];
|
|
11
|
+
const codeSearch = async (query) => (await searchRepo({ query, isRegex: true, maxMatches: 200 })).filter(h => !NON_CODE_RE.test(h.file));
|
|
12
|
+
try {
|
|
13
|
+
// 1. JWT verify without explicit algorithms array (algorithm confusion / none-attack)
|
|
14
|
+
const jwtVerifyHits = await codeSearch(String.raw `jwt\.verify\s*\(`);
|
|
15
|
+
const jwtAlgSafeRe = /algorithms\s*:\s*\[/;
|
|
16
|
+
const jwtAlgUnsafe = jwtVerifyHits.filter((h) => !jwtAlgSafeRe.test(h.preview));
|
|
17
|
+
if (jwtAlgUnsafe.length > 0) {
|
|
18
|
+
findings.push({
|
|
19
|
+
id: "JWT_ALG_NONE_ACCEPTED",
|
|
20
|
+
title: "jwt.verify() called without explicit algorithms array — algorithm confusion attack possible (CWE-327)",
|
|
21
|
+
severity: "CRITICAL",
|
|
22
|
+
evidence: jwtAlgUnsafe.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
23
|
+
files: [...new Set(jwtAlgUnsafe.slice(0, 10).map((m) => m.file))],
|
|
24
|
+
requiredActions: [
|
|
25
|
+
"Always pass algorithms: ['RS256'] (or your actual algorithm) to jwt.verify().",
|
|
26
|
+
"CWE-327 — without algorithms pin, attacker can forge tokens using alg:none or switch RS256→HS256 using the public key as secret.",
|
|
27
|
+
"Fix: jwt.verify(token, publicKey, { algorithms: ['RS256'] })"
|
|
28
|
+
]
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// 2. Session not regenerated after login (session fixation)
|
|
32
|
+
const loginHandlerHits = await codeSearch(String.raw `(?:req\.session\.user|req\.session\.userId|req\.session\.account|req\.session\.authenticated)\s*=`);
|
|
33
|
+
const sessionRegenerateRe = /req\.session\.regenerate|session\.regenerate\s*\(/;
|
|
34
|
+
const sessionFixationRisk = loginHandlerHits.filter((h) => !sessionRegenerateRe.test(h.preview));
|
|
35
|
+
if (sessionFixationRisk.length > 0) {
|
|
36
|
+
findings.push({
|
|
37
|
+
id: "SESSION_FIXATION",
|
|
38
|
+
title: "Session identity set without session regeneration — session fixation risk (CWE-384)",
|
|
39
|
+
severity: "HIGH",
|
|
40
|
+
evidence: sessionFixationRisk.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
41
|
+
files: [...new Set(sessionFixationRisk.slice(0, 10).map((m) => m.file))],
|
|
42
|
+
requiredActions: [
|
|
43
|
+
"Call req.session.regenerate() before setting session identity after authentication.",
|
|
44
|
+
"CWE-384 — an attacker who fixes the session ID before login can hijack the authenticated session.",
|
|
45
|
+
"Fix: req.session.regenerate((err) => { req.session.userId = user.id; res.json({ ok: true }); });"
|
|
46
|
+
]
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
// 3. OAuth authorize endpoint without state parameter generation
|
|
50
|
+
const oauthAuthHits = await codeSearch(String.raw `(?:authorizationUrl|oauth\.authorize|passport\.authenticate\s*\(\s*['"]oauth|\.redirect\s*\(\s*['"]https:\/\/[^'"]*\/oauth\/authorize|\/oauth\/callback|\/auth\/callback)`);
|
|
51
|
+
const oauthStateSafeRe = /state\s*[:=]|generateState|crypto\.randomBytes|randomUUID|nonce/;
|
|
52
|
+
const oauthStateUnsafe = oauthAuthHits.filter((h) => !oauthStateSafeRe.test(h.preview));
|
|
53
|
+
if (oauthStateUnsafe.length > 0) {
|
|
54
|
+
findings.push({
|
|
55
|
+
id: "OAUTH_MISSING_STATE",
|
|
56
|
+
title: "OAuth flow without state parameter — CSRF on authorization callback (CWE-352)",
|
|
57
|
+
severity: "HIGH",
|
|
58
|
+
evidence: oauthStateUnsafe.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
59
|
+
files: [...new Set(oauthStateUnsafe.slice(0, 10).map((m) => m.file))],
|
|
60
|
+
requiredActions: [
|
|
61
|
+
"Generate a cryptographically random state parameter and verify it on the callback.",
|
|
62
|
+
"CWE-352 — without state, an attacker can inject their own authorization code into the victim's session.",
|
|
63
|
+
"Fix: const state = crypto.randomBytes(32).toString('hex'); session.oauthState = state; // verify on callback"
|
|
64
|
+
]
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// 4. OAuth redirect_uri validated with includes/startsWith (too broad)
|
|
68
|
+
const redirectUriHits = await codeSearch(String.raw `redirect_uri.*(?:\.includes\s*\(|\.startsWith\s*\(|\.match\s*\(|indexOf\s*\()|(?:\.includes\s*\(|\.startsWith\s*\().*redirect_uri`);
|
|
69
|
+
if (redirectUriHits.length > 0) {
|
|
70
|
+
findings.push({
|
|
71
|
+
id: "OAUTH_OPEN_REDIRECT_URI",
|
|
72
|
+
title: "OAuth redirect_uri validated with includes/startsWith — open redirect via subdomain (CWE-601)",
|
|
73
|
+
severity: "HIGH",
|
|
74
|
+
evidence: redirectUriHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
75
|
+
files: [...new Set(redirectUriHits.slice(0, 10).map((m) => m.file))],
|
|
76
|
+
requiredActions: [
|
|
77
|
+
"Validate redirect_uri with exact string equality against a pre-registered allowlist.",
|
|
78
|
+
"CWE-601 — startsWith('https://example.com') allows https://example.com.evil.com/.",
|
|
79
|
+
"Fix: if (redirectUri !== REGISTERED_REDIRECT_URI) throw new Error('Invalid redirect_uri');"
|
|
80
|
+
]
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// 5. PKCE not enforced — OAuth/OIDC flow without code_challenge
|
|
84
|
+
const pkceHits = await codeSearch(String.raw `(?:authorization_code|grant_type.*authorization_code|code.*exchange|token.*endpoint.*code\b)`);
|
|
85
|
+
const pkceSafeRe = /code_challenge|code_verifier|pkce|PKCE/;
|
|
86
|
+
const pkceUnsafe = pkceHits.filter((h) => !pkceSafeRe.test(h.preview));
|
|
87
|
+
if (pkceUnsafe.length > 0) {
|
|
88
|
+
findings.push({
|
|
89
|
+
id: "PKCE_NOT_ENFORCED",
|
|
90
|
+
title: "OAuth authorization code flow without PKCE — code interception attack (RFC 7636)",
|
|
91
|
+
severity: "HIGH",
|
|
92
|
+
evidence: pkceUnsafe.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
93
|
+
files: [...new Set(pkceUnsafe.slice(0, 10).map((m) => m.file))],
|
|
94
|
+
requiredActions: [
|
|
95
|
+
"Require PKCE (code_challenge_method=S256) for all public clients and SPAs.",
|
|
96
|
+
"RFC 7636 / ATT&CK T1528 — without PKCE, a stolen authorization code can be exchanged for tokens.",
|
|
97
|
+
"Fix: enforce code_challenge in the /authorize handler and verify code_verifier in /token exchange."
|
|
98
|
+
]
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// 6. Hardcoded JWT secret (short literal string)
|
|
102
|
+
const hardcodedJwtHits = await codeSearch(String.raw `jwt\.sign\s*\([^,]+,\s*['"][a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]{1,32}['"]|jwt\.verify\s*\([^,]+,\s*['"][a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]{1,32}['"]`);
|
|
103
|
+
if (hardcodedJwtHits.length > 0) {
|
|
104
|
+
findings.push({
|
|
105
|
+
id: "HARDCODED_JWT_SECRET",
|
|
106
|
+
title: "Hardcoded JWT secret literal — secret exposed in source code (CWE-798)",
|
|
107
|
+
severity: "CRITICAL",
|
|
108
|
+
evidence: hardcodedJwtHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
109
|
+
files: [...new Set(hardcodedJwtHits.slice(0, 10).map((m) => m.file))],
|
|
110
|
+
requiredActions: [
|
|
111
|
+
"Move JWT secrets to environment variables or a secrets manager; never commit them to source.",
|
|
112
|
+
"CWE-798 — hardcoded secrets are trivially extracted from git history and Docker images.",
|
|
113
|
+
"Fix: jwt.sign(payload, process.env.JWT_SECRET!, { algorithms: ['RS256'] })"
|
|
114
|
+
]
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
// 7. Login/auth/token endpoints without rate limiting middleware
|
|
118
|
+
const loginRouteHits = await codeSearch(String.raw `(?:router|app)\.post\s*\(\s*['"][^'"]*(?:\/login|\/signin|\/auth|\/token|\/session)['"]\s*,`);
|
|
119
|
+
const rateLimitRe = /rateLimit|rateLimiter|rate_limit|limiter|throttle|slowDown|expressRateLimit/;
|
|
120
|
+
const rateLimitMissing = loginRouteHits.filter((h) => !rateLimitRe.test(h.preview));
|
|
121
|
+
if (rateLimitMissing.length > 0) {
|
|
122
|
+
findings.push({
|
|
123
|
+
id: "MISSING_RATE_LIMIT_LOGIN",
|
|
124
|
+
title: "Authentication endpoint without rate limiting — brute force attack surface (CWE-307)",
|
|
125
|
+
severity: "HIGH",
|
|
126
|
+
evidence: rateLimitMissing.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
127
|
+
files: [...new Set(rateLimitMissing.slice(0, 10).map((m) => m.file))],
|
|
128
|
+
requiredActions: [
|
|
129
|
+
"Apply express-rate-limit or equivalent middleware to all authentication endpoints.",
|
|
130
|
+
"CWE-307 — without rate limiting, brute force or credential stuffing attacks are unrestricted.",
|
|
131
|
+
"Fix: app.post('/login', loginRateLimiter, authHandler); // max: 5 attempts per 15 minutes"
|
|
132
|
+
]
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// 8. Plaintext password comparison (timing oracle / no hashing)
|
|
136
|
+
const passwordCompareHits = await codeSearch(String.raw `password\s*===\s*(?:req\.|user\.|stored|db\.|record\.)|(?:req\.|body\.)password\s*===\s*|password\s*==\s*(?:req\.|user\.|stored|db\.)|compareSync\s*\(\s*(?:req\.|body\.)`);
|
|
137
|
+
const passwordSafeRe = /bcrypt|argon2|scrypt|pbkdf2|timingSafeEqual|compare\s*\(/i;
|
|
138
|
+
const passwordUnsafe = passwordCompareHits.filter((h) => !passwordSafeRe.test(h.preview));
|
|
139
|
+
if (passwordUnsafe.length > 0) {
|
|
140
|
+
findings.push({
|
|
141
|
+
id: "PASSWORD_PLAIN_COMPARE",
|
|
142
|
+
title: "Plaintext password comparison — no hashing or timing oracle (CWE-256)",
|
|
143
|
+
severity: "CRITICAL",
|
|
144
|
+
evidence: passwordUnsafe.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
145
|
+
files: [...new Set(passwordUnsafe.slice(0, 10).map((m) => m.file))],
|
|
146
|
+
requiredActions: [
|
|
147
|
+
"Use bcrypt.compare() or argon2.verify() for password verification — never === comparison.",
|
|
148
|
+
"CWE-256 — plaintext comparison leaks timing information and stores passwords without hashing.",
|
|
149
|
+
"Fix: const valid = await bcrypt.compare(password, user.passwordHash); if (!valid) throw new Error('Unauthorized');"
|
|
150
|
+
]
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// 9. SAML signature validation disabled
|
|
154
|
+
const samlHits = await codeSearch(String.raw `(?:new\s+saml\.Strategy|passport-saml|samlify|node-saml|SAMLResponse|validateSignature\s*:\s*false|wantAssertionsSigned\s*:\s*false|signatureAlgorithm\s*:\s*['"]none['"])`);
|
|
155
|
+
const samlUnsafeRe = /validateSignature\s*:\s*false|wantAssertionsSigned\s*:\s*false|signatureAlgorithm\s*:\s*['"]none['"]/;
|
|
156
|
+
const samlUnsafe = samlHits.filter((h) => samlUnsafeRe.test(h.preview));
|
|
157
|
+
if (samlUnsafe.length > 0) {
|
|
158
|
+
findings.push({
|
|
159
|
+
id: "SAML_SIGNATURE_NOT_ENFORCED",
|
|
160
|
+
title: "SAML signature validation disabled — SAML response forgery (CWE-347)",
|
|
161
|
+
severity: "CRITICAL",
|
|
162
|
+
evidence: samlUnsafe.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
163
|
+
files: [...new Set(samlUnsafe.slice(0, 10).map((m) => m.file))],
|
|
164
|
+
requiredActions: [
|
|
165
|
+
"Set validateSignature:true and wantAssertionsSigned:true in all SAML strategy configurations.",
|
|
166
|
+
"CWE-347 — unsigned SAML responses allow any user to craft an assertion claiming to be any other user.",
|
|
167
|
+
"Fix: new SamlStrategy({ validateSignature: true, wantAssertionsSigned: true, cert: IDP_CERT }, ...)"
|
|
168
|
+
]
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
// 10. Cookies without httpOnly/secure/sameSite flags
|
|
172
|
+
const cookieHits = await codeSearch(String.raw `res\.cookie\s*\(\s*['"][^'"]+['"]`);
|
|
173
|
+
const cookieHttpOnlyRe = /httpOnly\s*:\s*true/;
|
|
174
|
+
const cookieSecureRe = /secure\s*:\s*true/;
|
|
175
|
+
const cookieUnsafe = cookieHits.filter((h) => !cookieHttpOnlyRe.test(h.preview) || !cookieSecureRe.test(h.preview));
|
|
176
|
+
if (cookieUnsafe.length > 0) {
|
|
177
|
+
findings.push({
|
|
178
|
+
id: "COOKIE_MISSING_SECURE_FLAGS",
|
|
179
|
+
title: "Cookie set without httpOnly and/or secure flags (CWE-1004 / CWE-614)",
|
|
180
|
+
severity: "HIGH",
|
|
181
|
+
evidence: cookieUnsafe.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
182
|
+
files: [...new Set(cookieUnsafe.slice(0, 10).map((m) => m.file))],
|
|
183
|
+
requiredActions: [
|
|
184
|
+
"Set httpOnly:true, secure:true, and sameSite:'Strict' on all authentication and session cookies.",
|
|
185
|
+
"CWE-1004/CWE-614 — missing httpOnly enables XSS cookie theft; missing secure sends cookie over HTTP.",
|
|
186
|
+
"Fix: res.cookie('session', token, { httpOnly: true, secure: true, sameSite: 'Strict', maxAge: 3600000 });"
|
|
187
|
+
]
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
// 11. Refresh token issued but old token not invalidated (token rotation missing)
|
|
191
|
+
const refreshTokenHits = await codeSearch(String.raw `(?:refresh_token|refreshToken)\s*[:=](?:.*jwt\.sign|.*generateToken|.*createToken|.*sign\s*\()|(?:grantType|grant_type)\s*[:=]\s*['"]refresh_token['"]`);
|
|
192
|
+
const refreshRotateRe = /delete|revoke|invalidate|blacklist|rotateToken|revokeToken|tokenFamily|REFRESH_TOKEN_FAMILY/;
|
|
193
|
+
const refreshUnsafe = refreshTokenHits.filter((h) => !refreshRotateRe.test(h.preview));
|
|
194
|
+
if (refreshUnsafe.length > 0) {
|
|
195
|
+
findings.push({
|
|
196
|
+
id: "REFRESH_TOKEN_NOT_ROTATED",
|
|
197
|
+
title: "Refresh token issued without revoking previous token — replay attack surface (CWE-613)",
|
|
198
|
+
severity: "HIGH",
|
|
199
|
+
evidence: refreshUnsafe.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
200
|
+
files: [...new Set(refreshUnsafe.slice(0, 10).map((m) => m.file))],
|
|
201
|
+
requiredActions: [
|
|
202
|
+
"Implement refresh token rotation: invalidate the old token before issuing the new one.",
|
|
203
|
+
"CWE-613 — without rotation, a stolen refresh token remains valid indefinitely.",
|
|
204
|
+
"Fix: await db.refreshTokens.delete(oldToken); const newToken = issueRefreshToken(user); // token family detection for theft detection"
|
|
205
|
+
]
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
// 12. JWT HS/RS confusion — jwt.verify called without algorithm pin on RS256 context
|
|
209
|
+
const jwtHsRsHits = await codeSearch(String.raw `jwt\.verify\s*\(\s*[^,]+,\s*(?:publicKey|PUBLIC_KEY|pub_key|process\.env\.[A-Z_]*PUBLIC|fs\.readFileSync[^)]*\.pem)`);
|
|
210
|
+
const jwtHsRsSafeRe = /algorithms\s*:\s*\[\s*['"](?:RS|ES|PS)/;
|
|
211
|
+
const jwtHsRsUnsafe = jwtHsRsHits.filter((h) => !jwtHsRsSafeRe.test(h.preview));
|
|
212
|
+
if (jwtHsRsUnsafe.length > 0) {
|
|
213
|
+
findings.push({
|
|
214
|
+
id: "JWT_HS_RS_CONFUSION",
|
|
215
|
+
title: "JWT verified with public key without algorithm pin — HS/RS confusion attack (CVE-2015-9235 pattern)",
|
|
216
|
+
severity: "CRITICAL",
|
|
217
|
+
evidence: jwtHsRsUnsafe.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
218
|
+
files: [...new Set(jwtHsRsUnsafe.slice(0, 10).map((m) => m.file))],
|
|
219
|
+
requiredActions: [
|
|
220
|
+
"Pin the algorithm to RS256/ES256 explicitly: jwt.verify(token, publicKey, { algorithms: ['RS256'] }).",
|
|
221
|
+
"Without algorithm pin: attacker takes RS256 public key, signs token with HS256 using that key as the HMAC secret — library accepts it.",
|
|
222
|
+
"This is CVE-2015-9235 — still exploitable in jsonwebtoken < 9.0 without the algorithms option."
|
|
223
|
+
]
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
console.warn("[checkAuthDeep] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
|
|
229
|
+
}
|
|
230
|
+
return findings;
|
|
231
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep injection class enforcement — covers attack vectors not detected by existing checks.
|
|
3
|
+
* CWE references per MITRE CWE catalog; ATT&CK techniques per MITRE ATT&CK v14.
|
|
4
|
+
*/
|
|
5
|
+
import { sanitizeErrorMessage } from "../result.js";
|
|
6
|
+
import { searchRepo } from "../../repo/search.js";
|
|
7
|
+
const NON_CODE_RE = /\.(?:md|json|yaml|yml|txt|rst|toml|lock)$/i;
|
|
8
|
+
export async function checkInjectionDeep(_opts) {
|
|
9
|
+
const findings = [];
|
|
10
|
+
const codeSearch = async (query) => (await searchRepo({ query, isRegex: true, maxMatches: 200 })).filter(h => !NON_CODE_RE.test(h.file));
|
|
11
|
+
try {
|
|
12
|
+
// 1. XXE — XML entity parsing without entity disabling
|
|
13
|
+
const xxeHits = await codeSearch(String.raw `(?:new\s+(?:DOMParser|SAXParser|XMLParser|fxp\.XMLParser)|xml2js\.parseString|fast-xml-parser|libxmljs\.parseXml|parseXML)\s*\(`);
|
|
14
|
+
const xxeSafeRe = /entityExpansion\s*:\s*false|processEntities\s*:\s*false|resolveEntities\s*:\s*false|FEATURE_EXTERNAL_GENERAL_ENTITIES|XMLConstants\.FEATURE_SECURE_PROCESSING/;
|
|
15
|
+
const xxeUnsafe = xxeHits.filter((h) => !xxeSafeRe.test(h.preview));
|
|
16
|
+
if (xxeUnsafe.length > 0) {
|
|
17
|
+
findings.push({
|
|
18
|
+
id: "XXE_ENTITY_PARSING",
|
|
19
|
+
title: "XML parser may process external entities (XXE — CWE-611)",
|
|
20
|
+
severity: "HIGH",
|
|
21
|
+
evidence: xxeUnsafe.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
22
|
+
files: [...new Set(xxeUnsafe.slice(0, 10).map((m) => m.file))],
|
|
23
|
+
requiredActions: [
|
|
24
|
+
"Disable external entity processing: set processEntities:false (fast-xml-parser) or resolveEntities:false (xml2js).",
|
|
25
|
+
"CWE-611 / ATT&CK T1190 — XXE can leak files, SSRF, or RCE via server-side request.",
|
|
26
|
+
"Example fix (fast-xml-parser): new XMLParser({ processEntities: false, ignoreAttributes: false })"
|
|
27
|
+
]
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
// 2. SSTI — server-side template injection via user-controlled compile
|
|
31
|
+
const sstiHits = await codeSearch(String.raw `(?:Handlebars\.compile|ejs\.render|ejs\.compile|nunjucks\.renderString|pug\.compile|pug\.render|\.template\s*\(|Mustache\.render)\s*\(\s*(?:req\.|body\.|params\.|query\.|user\.|input|template|src)`);
|
|
32
|
+
if (sstiHits.length > 0) {
|
|
33
|
+
findings.push({
|
|
34
|
+
id: "SSTI_TEMPLATE_COMPILE",
|
|
35
|
+
title: "Server-side template compiled from user input (SSTI — CWE-94)",
|
|
36
|
+
severity: "CRITICAL",
|
|
37
|
+
evidence: sstiHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
38
|
+
files: [...new Set(sstiHits.slice(0, 10).map((m) => m.file))],
|
|
39
|
+
requiredActions: [
|
|
40
|
+
"Never compile templates from user input — only render with user-controlled data as context variables.",
|
|
41
|
+
"CWE-94 / ATT&CK T1059 — SSTI achieves RCE via template engine expression evaluation.",
|
|
42
|
+
"Fix: precompile templates at build time; pass untrusted data only as template context, never as template source."
|
|
43
|
+
]
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
// 3. Prototype pollution — unsafe merge of user-controlled data into plain objects
|
|
47
|
+
const ppHits = await codeSearch(String.raw `(?:_\.merge|Object\.assign|deepmerge|lodash\.merge|merge\s*\()\s*\(\s*(?:\{\}|obj|target|options|config|settings|result)\s*,\s*(?:req\.|body\.|params\.|query\.|user\.|payload\.|data\.)`);
|
|
48
|
+
if (ppHits.length > 0) {
|
|
49
|
+
findings.push({
|
|
50
|
+
id: "PROTOTYPE_POLLUTION",
|
|
51
|
+
title: "Unsafe merge of user-controlled data into plain object — prototype pollution risk (CWE-1321)",
|
|
52
|
+
severity: "HIGH",
|
|
53
|
+
evidence: ppHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
54
|
+
files: [...new Set(ppHits.slice(0, 10).map((m) => m.file))],
|
|
55
|
+
requiredActions: [
|
|
56
|
+
"Validate with Zod/Joi schema before merging; use Object.create(null) as the merge target.",
|
|
57
|
+
"CWE-1321 / ATT&CK T1548 — payload {\"__proto__\":{\"isAdmin\":true}} can pollute all objects in the process.",
|
|
58
|
+
"Fix: const safe = schema.parse(req.body); Object.assign(Object.create(null), defaults, safe);"
|
|
59
|
+
]
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// 4. Open redirect — res.redirect with unvalidated user input
|
|
63
|
+
const openRedirectHits = await codeSearch(String.raw `res\.redirect\s*\(\s*(?:req\.|body\.|params\.|query\.|headers\.|url\b|redirect|returnUrl|next|target|destination)`);
|
|
64
|
+
const redirectAllowlistRe = /allowlist|allowedHosts|isAllowed|REDIRECT_WHITELIST|validateRedirect|isSafeUrl|startsWith\s*\(['"]\/\b/;
|
|
65
|
+
const openRedirectUnsafe = openRedirectHits.filter((h) => !redirectAllowlistRe.test(h.preview));
|
|
66
|
+
if (openRedirectUnsafe.length > 0) {
|
|
67
|
+
findings.push({
|
|
68
|
+
id: "OPEN_REDIRECT",
|
|
69
|
+
title: "Open redirect — user-controlled URL in res.redirect() without allowlist (CWE-601)",
|
|
70
|
+
severity: "HIGH",
|
|
71
|
+
evidence: openRedirectUnsafe.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
72
|
+
files: [...new Set(openRedirectUnsafe.slice(0, 10).map((m) => m.file))],
|
|
73
|
+
requiredActions: [
|
|
74
|
+
"Validate redirect targets against an allowlist of trusted hosts or enforce relative-only redirects.",
|
|
75
|
+
"CWE-601 / ATT&CK T1598 — open redirects are used in phishing chains and OAuth token theft.",
|
|
76
|
+
"Fix: if (!url.startsWith('/') || url.startsWith('//')) throw new Error('Invalid redirect');"
|
|
77
|
+
]
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// 5. NoSQL operator injection — MongoDB query built from req.body directly
|
|
81
|
+
const nosqlHits = await codeSearch(String.raw `(?:\.find|\.findOne|\.findOneAndUpdate|\.updateOne|\.deleteOne|\.aggregate)\s*\(\s*(?:req\.body|body\.|params\.|query\.)\b`);
|
|
82
|
+
if (nosqlHits.length > 0) {
|
|
83
|
+
findings.push({
|
|
84
|
+
id: "NOSQL_OPERATOR_INJECTION",
|
|
85
|
+
title: "NoSQL query built from user input without operator stripping (CWE-943)",
|
|
86
|
+
severity: "HIGH",
|
|
87
|
+
evidence: nosqlHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
88
|
+
files: [...new Set(nosqlHits.slice(0, 10).map((m) => m.file))],
|
|
89
|
+
requiredActions: [
|
|
90
|
+
"Never pass req.body directly into MongoDB queries — extract and validate each field individually.",
|
|
91
|
+
"CWE-943 — payload {\"$gt\":\"\"} bypasses equality checks; {\"$where\":\"sleep(5000)\"} achieves DoS.",
|
|
92
|
+
"Fix: const { username } = z.object({ username: z.string() }).parse(req.body); User.findOne({ username });"
|
|
93
|
+
]
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// 6. CRLF injection — user value in res.setHeader without sanitization
|
|
97
|
+
const crlfHits = await codeSearch(String.raw `res\.setHeader\s*\(\s*[^,]+,\s*(?:req\.|body\.|params\.|query\.|user\.|headers\.)`);
|
|
98
|
+
const crlfSafeRe = /replace\s*\(.*\\r|replace\s*\(.*\\n|sanitize|encodeURIComponent/;
|
|
99
|
+
const crlfUnsafe = crlfHits.filter((h) => !crlfSafeRe.test(h.preview));
|
|
100
|
+
if (crlfUnsafe.length > 0) {
|
|
101
|
+
findings.push({
|
|
102
|
+
id: "CRLF_INJECTION",
|
|
103
|
+
title: "CRLF injection risk — user value written to HTTP response header (CWE-113)",
|
|
104
|
+
severity: "HIGH",
|
|
105
|
+
evidence: crlfUnsafe.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
106
|
+
files: [...new Set(crlfUnsafe.slice(0, 10).map((m) => m.file))],
|
|
107
|
+
requiredActions: [
|
|
108
|
+
String.raw `Strip \r and \n from any user-controlled value before writing to response headers.`,
|
|
109
|
+
"CWE-113 — CRLF injection enables HTTP response splitting, header injection, session fixation.",
|
|
110
|
+
String.raw `Fix: const safe = value.replace(/[\r\n]/g, ''); res.setHeader('X-Header', safe);`
|
|
111
|
+
]
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
// 7. YAML unsafe load (js-yaml v3 default)
|
|
115
|
+
const yamlHits = await codeSearch(String.raw `yaml\.load\s*\((?!.*FAILSAFE_SCHEMA)(?!.*JSON_SCHEMA)(?!.*CORE_SCHEMA)|jsYaml\.load\s*\((?!.*schema)|require\s*\(['"]js-yaml['"]\)\.load\s*\(`);
|
|
116
|
+
if (yamlHits.length > 0) {
|
|
117
|
+
findings.push({
|
|
118
|
+
id: "YAML_UNSAFE_LOAD",
|
|
119
|
+
title: "js-yaml load() without safe schema — arbitrary code execution risk (CWE-502)",
|
|
120
|
+
severity: "CRITICAL",
|
|
121
|
+
evidence: yamlHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
122
|
+
files: [...new Set(yamlHits.slice(0, 10).map((m) => m.file))],
|
|
123
|
+
requiredActions: [
|
|
124
|
+
"Use yaml.load(str, { schema: yaml.FAILSAFE_SCHEMA }) or yaml.safeLoad() (js-yaml v3).",
|
|
125
|
+
"CWE-502 — js-yaml default schema executes JS functions embedded in YAML (!!js/function).",
|
|
126
|
+
"For js-yaml v4+: safeLoad was removed; use load() which is safe by default — verify version."
|
|
127
|
+
]
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
// 8. Unsafe deserialization
|
|
131
|
+
const deserializeHits = await codeSearch(String.raw `(?:node-serialize\.unserialize|serialize\.unserialize|unserialize\s*\(|new\s+Function\s*\(\s*(?:req\.|body\.|params\.|data\.|input)|eval\s*\(\s*(?:req\.|body\.|params\.|data\.|Buffer\.from|atob\())`);
|
|
132
|
+
if (deserializeHits.length > 0) {
|
|
133
|
+
findings.push({
|
|
134
|
+
id: "DESERIALIZE_UNSAFE",
|
|
135
|
+
title: "Unsafe deserialization of user input (CWE-502)",
|
|
136
|
+
severity: "CRITICAL",
|
|
137
|
+
evidence: deserializeHits.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
138
|
+
files: [...new Set(deserializeHits.slice(0, 10).map((m) => m.file))],
|
|
139
|
+
requiredActions: [
|
|
140
|
+
"Never deserialize untrusted data with node-serialize, eval(), or new Function().",
|
|
141
|
+
"CWE-502 / ATT&CK T1059 — deserialization gadget chains achieve RCE without user interaction.",
|
|
142
|
+
"Fix: use JSON.parse() with a Zod schema for structured data; for binary formats use a safe decoder with a strict schema."
|
|
143
|
+
]
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
// 9. Path traversal — path.join with user-controlled segment without normalization check
|
|
147
|
+
const pathTraversalHits = await codeSearch(String.raw `path\.(?:join|resolve)\s*\([^)]*(?:req\.|body\.|params\.|query\.|filename|filepath|file_path|filePath|fileName)[^)]*\)`);
|
|
148
|
+
const pathSafeRe = /normalize|startsWith|indexOf\s*\(base|resolve.*startsWith|\.includes\s*\(['"]\.\.['"]|path\.sep/;
|
|
149
|
+
const pathUnsafe = pathTraversalHits.filter((h) => !pathSafeRe.test(h.preview));
|
|
150
|
+
if (pathUnsafe.length > 0) {
|
|
151
|
+
findings.push({
|
|
152
|
+
id: "PATH_TRAVERSAL_JOIN",
|
|
153
|
+
title: "Path traversal — path.join() with user input without prefix verification (CWE-22)",
|
|
154
|
+
severity: "HIGH",
|
|
155
|
+
evidence: pathUnsafe.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
156
|
+
files: [...new Set(pathUnsafe.slice(0, 10).map((m) => m.file))],
|
|
157
|
+
requiredActions: [
|
|
158
|
+
"After path.join(), verify the resolved path starts with the intended base directory.",
|
|
159
|
+
"CWE-22 / ATT&CK T1083 — ../../etc/passwd reads arbitrary files on the server.",
|
|
160
|
+
"Fix: const full = path.resolve(BASE_DIR, userFilename); if (!full.startsWith(BASE_DIR + path.sep)) throw new Error('Invalid path');"
|
|
161
|
+
]
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// 10. Log injection — user-controlled strings logged without newline stripping
|
|
165
|
+
const logInjectionHits = await codeSearch(String.raw `(?:console\.(?:log|warn|error|info)|logger\.(?:log|warn|error|info|debug)|log\.(?:info|warn|error|debug))\s*\([^)]*(?:req\.|body\.|params\.|query\.|headers\.|user\.|username|email|ip\b)`);
|
|
166
|
+
const logSafeRe = /replace\s*\(.*\\n|replace\s*\(.*\\r|sanitize|JSON\.stringify|inspect\s*\(/;
|
|
167
|
+
const logUnsafe = logInjectionHits.filter((h) => !logSafeRe.test(h.preview));
|
|
168
|
+
if (logUnsafe.length > 0) {
|
|
169
|
+
findings.push({
|
|
170
|
+
id: "LOG_INJECTION",
|
|
171
|
+
title: "Log injection — user-controlled string written to logs without newline sanitization (CWE-117)",
|
|
172
|
+
severity: "MEDIUM",
|
|
173
|
+
evidence: logUnsafe.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
174
|
+
files: [...new Set(logUnsafe.slice(0, 10).map((m) => m.file))],
|
|
175
|
+
requiredActions: [
|
|
176
|
+
String.raw `Strip or encode \n and \r from user-controlled values before logging.`,
|
|
177
|
+
"CWE-117 — log injection forges log entries, erasing evidence of attacks or injecting false audit trails.",
|
|
178
|
+
String.raw `Fix: logger.info('Login attempt', { username: username.replace(/[\r\n]/g, '_') });`
|
|
179
|
+
]
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
// 11. SSRF via user-controlled URL in HTTP request
|
|
183
|
+
const ssrfHits = await codeSearch(String.raw `(?:fetch|axios\.(?:get|post|put|delete|request)|https?\.(?:get|request)|got\s*\(|needle\.(?:get|post)|superagent\.(?:get|post))\s*\(\s*(?:req\.|body\.|params\.|query\.|url\b|webhook|endpoint|target|callback|proxy)`);
|
|
184
|
+
const ssrfSafeRe = /allowedHosts|SSRF_GUARD|validateUrl|isAllowedUrl|new URL.*hostname|URL_ALLOWLIST/;
|
|
185
|
+
const ssrfUnsafe = ssrfHits.filter((h) => !ssrfSafeRe.test(h.preview));
|
|
186
|
+
if (ssrfUnsafe.length > 0) {
|
|
187
|
+
findings.push({
|
|
188
|
+
id: "SSRF_USER_URL",
|
|
189
|
+
title: "SSRF — HTTP request to user-controlled URL without allowlist (CWE-918)",
|
|
190
|
+
severity: "CRITICAL",
|
|
191
|
+
evidence: ssrfUnsafe.slice(0, 10).map((m) => `${m.file}:${m.line}:${m.preview}`),
|
|
192
|
+
files: [...new Set(ssrfUnsafe.slice(0, 10).map((m) => m.file))],
|
|
193
|
+
requiredActions: [
|
|
194
|
+
"Validate the URL hostname against an explicit allowlist before making server-side HTTP requests.",
|
|
195
|
+
"CWE-918 / ATT&CK T1090 — SSRF reaches 169.254.169.254 for cloud metadata, internal services, and localhost.",
|
|
196
|
+
"Fix: const { hostname } = new URL(userUrl); if (!ALLOWED_HOSTS.includes(hostname)) throw new Error('Blocked');"
|
|
197
|
+
]
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
console.warn("[checkInjectionDeep] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
|
|
203
|
+
}
|
|
204
|
+
return findings;
|
|
205
|
+
}
|
package/dist/gate/policy.js
CHANGED
|
@@ -28,6 +28,8 @@ import { runRuntimeChecks } from "./checks/runtime.js";
|
|
|
28
28
|
import { runCiPipelineChecks } from "./checks/ci-pipeline.js";
|
|
29
29
|
import { runNucleiChecks } from "./checks/nuclei.js";
|
|
30
30
|
import { getCommitHash, loadBaseline, saveBaseline, compareBaseline } from "./baseline.js";
|
|
31
|
+
import { checkInjectionDeep } from "./checks/injection-deep.js";
|
|
32
|
+
import { checkAuthDeep } from "./checks/auth-deep.js";
|
|
31
33
|
import { randomUUID } from "node:crypto";
|
|
32
34
|
const PolicySchema = z.object({
|
|
33
35
|
name: z.string(),
|
|
@@ -170,7 +172,9 @@ export async function runPrGate(opts) {
|
|
|
170
172
|
surfaces.ai ? runAiRedteamChecks({ changedFiles }) : Promise.resolve([]),
|
|
171
173
|
process.env["SECURITY_STAGING_URL"] ? runRuntimeChecks({ targets, changedFiles }) : Promise.resolve([]),
|
|
172
174
|
runCiPipelineChecks({ changedFiles }),
|
|
173
|
-
process.env["SECURITY_STAGING_URL"] ? runNucleiChecks({ changedFiles }) : Promise.resolve([])
|
|
175
|
+
process.env["SECURITY_STAGING_URL"] ? runNucleiChecks({ changedFiles }) : Promise.resolve([]),
|
|
176
|
+
(surfaces.api || surfaces.web) ? checkInjectionDeep({ changedFiles }) : Promise.resolve([]),
|
|
177
|
+
(surfaces.api || surfaces.web) ? checkAuthDeep({ changedFiles }) : Promise.resolve([])
|
|
174
178
|
]);
|
|
175
179
|
rawFindings = [];
|
|
176
180
|
for (const result of checkResults) {
|