xploitscan 0.6.0 → 0.8.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/README.md +79 -79
- package/dist/{api-QTNXZY4O.js → api-Z7VNGPT2.js} +2 -2
- package/dist/{chunk-47X3TUVR.js → chunk-CBDFSACC.js} +1 -8
- package/dist/{chunk-47X3TUVR.js.map → chunk-CBDFSACC.js.map} +1 -1
- package/dist/index.js +608 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- /package/dist/{api-QTNXZY4O.js.map → api-Z7VNGPT2.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
__require,
|
|
4
3
|
checkUsage,
|
|
5
4
|
clearToken,
|
|
6
5
|
getStoredToken,
|
|
@@ -9,7 +8,7 @@ import {
|
|
|
9
8
|
storeToken,
|
|
10
9
|
syncUser,
|
|
11
10
|
uploadScanResults
|
|
12
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-CBDFSACC.js";
|
|
13
12
|
|
|
14
13
|
// src/index.ts
|
|
15
14
|
import { Command } from "commander";
|
|
@@ -3166,7 +3165,42 @@ var complianceMap = {
|
|
|
3166
3165
|
VC093: { owasp: "A01:2021", cwe: "CWE-22" },
|
|
3167
3166
|
VC094: { owasp: "A03:2021", cwe: "CWE-78" },
|
|
3168
3167
|
VC095: { owasp: "A05:2021", cwe: "CWE-942" },
|
|
3169
|
-
VC096: { owasp: "A02:2021", cwe: "CWE-319" }
|
|
3168
|
+
VC096: { owasp: "A02:2021", cwe: "CWE-319" },
|
|
3169
|
+
VC097: { owasp: "A09:2021", cwe: "CWE-532" },
|
|
3170
|
+
VC098: { owasp: "A04:2021", cwe: "CWE-400" },
|
|
3171
|
+
VC099: { owasp: "A04:2021", cwe: "CWE-401" },
|
|
3172
|
+
VC100: { owasp: "A04:2021", cwe: "CWE-400" },
|
|
3173
|
+
VC101: { owasp: "A04:2021", cwe: "CWE-400" },
|
|
3174
|
+
VC102: { owasp: "A04:2021", cwe: "CWE-400" },
|
|
3175
|
+
VC103: { owasp: "A04:2021", cwe: "CWE-710" },
|
|
3176
|
+
VC104: { owasp: "A04:2021", cwe: "CWE-390" },
|
|
3177
|
+
VC105: { owasp: "A04:2021", cwe: "CWE-710" },
|
|
3178
|
+
VC106: { owasp: "A04:2021", cwe: "CWE-710" },
|
|
3179
|
+
VC107: { owasp: "A02:2021", cwe: "CWE-311" },
|
|
3180
|
+
VC108: { owasp: "A01:2021", cwe: "CWE-284" },
|
|
3181
|
+
VC109: { owasp: "A01:2021", cwe: "CWE-284" },
|
|
3182
|
+
VC110: { owasp: "A09:2021", cwe: "CWE-778" },
|
|
3183
|
+
VC111: { owasp: "A05:2021", cwe: "CWE-16" },
|
|
3184
|
+
VC112: { owasp: "A06:2021", cwe: "CWE-1104" },
|
|
3185
|
+
VC113: { owasp: "A05:2021", cwe: "CWE-200" },
|
|
3186
|
+
VC114: { owasp: "A05:2021", cwe: "CWE-16" },
|
|
3187
|
+
VC115: { owasp: "A02:2021", cwe: "CWE-311" },
|
|
3188
|
+
VC116: { owasp: "A05:2021", cwe: "CWE-770" },
|
|
3189
|
+
VC117: { owasp: "A03:2021", cwe: "CWE-22" },
|
|
3190
|
+
VC118: { owasp: "A09:2021", cwe: "CWE-532" },
|
|
3191
|
+
VC119: { owasp: "A07:2021", cwe: "CWE-798" },
|
|
3192
|
+
VC120: { owasp: "A07:2021", cwe: "CWE-352" },
|
|
3193
|
+
VC121: { owasp: "A06:2021", cwe: "CWE-1104" },
|
|
3194
|
+
VC122: { owasp: "A02:2021", cwe: "CWE-326" },
|
|
3195
|
+
VC123: { owasp: "A02:2021", cwe: "CWE-326" },
|
|
3196
|
+
VC124: { owasp: "A02:2021", cwe: "CWE-327" },
|
|
3197
|
+
VC125: { owasp: "A07:2021", cwe: "CWE-640" },
|
|
3198
|
+
VC126: { owasp: "A05:2021", cwe: "CWE-200" },
|
|
3199
|
+
VC127: { owasp: "A01:2021", cwe: "CWE-862" },
|
|
3200
|
+
VC128: { owasp: "A05:2021", cwe: "CWE-444" },
|
|
3201
|
+
VC129: { owasp: "A02:2021", cwe: "CWE-311" },
|
|
3202
|
+
VC130: { owasp: "A07:2021", cwe: "CWE-307" },
|
|
3203
|
+
VC131: { owasp: "A06:2021", cwe: "CWE-1104" }
|
|
3170
3204
|
};
|
|
3171
3205
|
var consoleLogProduction = {
|
|
3172
3206
|
id: "VC097",
|
|
@@ -3646,6 +3680,526 @@ var k8sNoResourceLimits = {
|
|
|
3646
3680
|
}];
|
|
3647
3681
|
}
|
|
3648
3682
|
};
|
|
3683
|
+
var pathTraversal = {
|
|
3684
|
+
id: "VC117",
|
|
3685
|
+
title: "Path Traversal Vulnerability",
|
|
3686
|
+
severity: "critical",
|
|
3687
|
+
category: "Injection",
|
|
3688
|
+
description: "User input is used to construct file paths without sanitization, allowing attackers to read/write arbitrary files (e.g., ../../etc/passwd).",
|
|
3689
|
+
check(content, filePath) {
|
|
3690
|
+
if (isTestFile(filePath)) return [];
|
|
3691
|
+
if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|php|java)$/)) return [];
|
|
3692
|
+
const findings = [];
|
|
3693
|
+
const patterns = [
|
|
3694
|
+
/(?:readFile|readFileSync|createReadStream|writeFile|writeFileSync|appendFile|unlink|unlinkSync|access|stat|statSync|existsSync)\s*\(\s*(?:req\.|request\.|ctx\.|params\.|query\.)/gi,
|
|
3695
|
+
/(?:path\.join|path\.resolve)\s*\([^)]*(?:req\.|request\.|ctx\.|params\.|query\.)[^)]*\)/gi,
|
|
3696
|
+
/(?:res\.sendFile|res\.download)\s*\([^)]*(?:req\.|request\.)/gi,
|
|
3697
|
+
/open\s*\(\s*(?:request\.|params\[|args\.)/gi
|
|
3698
|
+
];
|
|
3699
|
+
for (const pat of patterns) {
|
|
3700
|
+
let m;
|
|
3701
|
+
while ((m = pat.exec(content)) !== null) {
|
|
3702
|
+
if (isCommentLine(content, m.index)) continue;
|
|
3703
|
+
const surrounding = content.substring(Math.max(0, m.index - 200), m.index + 200);
|
|
3704
|
+
if (/path\.normalize|sanitize|\.replace\(\s*['"]\.\./i.test(surrounding)) continue;
|
|
3705
|
+
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3706
|
+
findings.push({
|
|
3707
|
+
rule: "VC117",
|
|
3708
|
+
title: pathTraversal.title,
|
|
3709
|
+
severity: "critical",
|
|
3710
|
+
category: "Injection",
|
|
3711
|
+
file: filePath,
|
|
3712
|
+
line: lineNum,
|
|
3713
|
+
snippet: getSnippet(content, lineNum),
|
|
3714
|
+
fix: "Sanitize file paths: use path.normalize(), reject paths containing '..', and validate against an allowed base directory."
|
|
3715
|
+
});
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
return findings;
|
|
3719
|
+
}
|
|
3720
|
+
};
|
|
3721
|
+
var piiInLogs = {
|
|
3722
|
+
id: "VC118",
|
|
3723
|
+
title: "Personally Identifiable Information in Logs",
|
|
3724
|
+
severity: "high",
|
|
3725
|
+
category: "Information Leakage",
|
|
3726
|
+
description: "Logging statements that include email addresses, passwords, SSNs, credit card numbers, or other PII can leak sensitive data to log aggregation systems.",
|
|
3727
|
+
check(content, filePath) {
|
|
3728
|
+
if (isTestFile(filePath)) return [];
|
|
3729
|
+
if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];
|
|
3730
|
+
const findings = [];
|
|
3731
|
+
const logPattern = /(?:console\.(?:log|info|warn|error|debug)|logger\.(?:info|warn|error|debug|log)|log\.(?:info|warn|error|debug)|logging\.(?:info|warn|error|debug))\s*\([^)]*(?:password|passwd|secret|ssn|social.?security|credit.?card|cardNumber|cvv|token|bearer|authorization)\b/gi;
|
|
3732
|
+
let m;
|
|
3733
|
+
while ((m = logPattern.exec(content)) !== null) {
|
|
3734
|
+
if (isCommentLine(content, m.index)) continue;
|
|
3735
|
+
if (isInsideFixMessage(content, m.index)) continue;
|
|
3736
|
+
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3737
|
+
findings.push({
|
|
3738
|
+
rule: "VC118",
|
|
3739
|
+
title: piiInLogs.title,
|
|
3740
|
+
severity: "high",
|
|
3741
|
+
category: "Information Leakage",
|
|
3742
|
+
file: filePath,
|
|
3743
|
+
line: lineNum,
|
|
3744
|
+
snippet: getSnippet(content, lineNum),
|
|
3745
|
+
fix: "Never log sensitive data. Redact or mask PII before logging: log('user login', { email: maskEmail(email) })."
|
|
3746
|
+
});
|
|
3747
|
+
}
|
|
3748
|
+
return findings;
|
|
3749
|
+
}
|
|
3750
|
+
};
|
|
3751
|
+
var hardcodedOAuthSecret = {
|
|
3752
|
+
id: "VC119",
|
|
3753
|
+
title: "Hardcoded OAuth Client Secret",
|
|
3754
|
+
severity: "critical",
|
|
3755
|
+
category: "Secrets",
|
|
3756
|
+
description: "OAuth client secrets hardcoded in source code can be extracted and used to impersonate your application.",
|
|
3757
|
+
check(content, filePath) {
|
|
3758
|
+
if (isTestFile(filePath)) return [];
|
|
3759
|
+
if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php|env|json|yaml|yml)$/)) return [];
|
|
3760
|
+
if (/package-lock|yarn\.lock|pnpm-lock|composer\.lock/i.test(filePath)) return [];
|
|
3761
|
+
const findings = [];
|
|
3762
|
+
const patterns = [
|
|
3763
|
+
/client[_-]?secret\s*[:=]\s*["'][a-zA-Z0-9_\-]{20,}["']/gi,
|
|
3764
|
+
/GOOGLE_CLIENT_SECRET\s*[:=]\s*["'][^"']{10,}["']/gi,
|
|
3765
|
+
/GITHUB_CLIENT_SECRET\s*[:=]\s*["'][^"']{10,}["']/gi,
|
|
3766
|
+
/FACEBOOK_APP_SECRET\s*[:=]\s*["'][^"']{10,}["']/gi,
|
|
3767
|
+
/AUTH0_CLIENT_SECRET\s*[:=]\s*["'][^"']{10,}["']/gi
|
|
3768
|
+
];
|
|
3769
|
+
for (const pat of patterns) {
|
|
3770
|
+
let m;
|
|
3771
|
+
while ((m = pat.exec(content)) !== null) {
|
|
3772
|
+
if (isCommentLine(content, m.index)) continue;
|
|
3773
|
+
if (/process\.env|os\.environ|ENV\[|getenv|\$\{|<your|CHANGE_ME|xxx|placeholder/i.test(m[0])) continue;
|
|
3774
|
+
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3775
|
+
findings.push({
|
|
3776
|
+
rule: "VC119",
|
|
3777
|
+
title: hardcodedOAuthSecret.title,
|
|
3778
|
+
severity: "critical",
|
|
3779
|
+
category: "Secrets",
|
|
3780
|
+
file: filePath,
|
|
3781
|
+
line: lineNum,
|
|
3782
|
+
snippet: getSnippet(content, lineNum),
|
|
3783
|
+
fix: "Move OAuth client secrets to environment variables. Never commit secrets to source control."
|
|
3784
|
+
});
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
return findings;
|
|
3788
|
+
}
|
|
3789
|
+
};
|
|
3790
|
+
var missingOAuthState = {
|
|
3791
|
+
id: "VC120",
|
|
3792
|
+
title: "OAuth Flow Missing State Parameter",
|
|
3793
|
+
severity: "high",
|
|
3794
|
+
category: "Authentication",
|
|
3795
|
+
description: "OAuth authorization requests without a state parameter are vulnerable to CSRF attacks, allowing attackers to link their account to a victim's session.",
|
|
3796
|
+
check(content, filePath) {
|
|
3797
|
+
if (isTestFile(filePath)) return [];
|
|
3798
|
+
if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];
|
|
3799
|
+
const findings = [];
|
|
3800
|
+
const oauthUrlPattern = /(?:authorize\?|\/oauth\/authorize|\/auth\?|authorization_endpoint)[^}]*(?:client_id|response_type)/gi;
|
|
3801
|
+
let m;
|
|
3802
|
+
while ((m = oauthUrlPattern.exec(content)) !== null) {
|
|
3803
|
+
if (isCommentLine(content, m.index)) continue;
|
|
3804
|
+
const surrounding = content.substring(m.index, Math.min(content.length, m.index + 500));
|
|
3805
|
+
if (/state\s*[=:]/i.test(surrounding)) continue;
|
|
3806
|
+
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3807
|
+
findings.push({
|
|
3808
|
+
rule: "VC120",
|
|
3809
|
+
title: missingOAuthState.title,
|
|
3810
|
+
severity: "high",
|
|
3811
|
+
category: "Authentication",
|
|
3812
|
+
file: filePath,
|
|
3813
|
+
line: lineNum,
|
|
3814
|
+
snippet: getSnippet(content, lineNum),
|
|
3815
|
+
fix: "Always include a cryptographically random 'state' parameter in OAuth authorization requests and validate it on callback."
|
|
3816
|
+
});
|
|
3817
|
+
}
|
|
3818
|
+
return findings;
|
|
3819
|
+
}
|
|
3820
|
+
};
|
|
3821
|
+
var unpinnedGitHubAction = {
|
|
3822
|
+
id: "VC121",
|
|
3823
|
+
title: "Unpinned GitHub Actions Version",
|
|
3824
|
+
severity: "high",
|
|
3825
|
+
category: "Supply Chain",
|
|
3826
|
+
description: "GitHub Actions using branch references (@main, @master) instead of commit SHAs can be compromised via supply-chain attacks.",
|
|
3827
|
+
check(content, filePath) {
|
|
3828
|
+
if (!filePath.match(/\.github\/workflows\/.*\.(yml|yaml)$/)) return [];
|
|
3829
|
+
const findings = [];
|
|
3830
|
+
const usesPattern = /uses\s*:\s*[\w\-]+\/[\w\-]+@(main|master|dev|develop|latest)\b/gi;
|
|
3831
|
+
let m;
|
|
3832
|
+
while ((m = usesPattern.exec(content)) !== null) {
|
|
3833
|
+
if (isCommentLine(content, m.index)) continue;
|
|
3834
|
+
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3835
|
+
findings.push({
|
|
3836
|
+
rule: "VC121",
|
|
3837
|
+
title: unpinnedGitHubAction.title,
|
|
3838
|
+
severity: "high",
|
|
3839
|
+
category: "Supply Chain",
|
|
3840
|
+
file: filePath,
|
|
3841
|
+
line: lineNum,
|
|
3842
|
+
snippet: getSnippet(content, lineNum),
|
|
3843
|
+
fix: "Pin GitHub Actions to a specific commit SHA instead of a branch name: uses: owner/action@<full-sha>"
|
|
3844
|
+
});
|
|
3845
|
+
}
|
|
3846
|
+
return findings;
|
|
3847
|
+
}
|
|
3848
|
+
};
|
|
3849
|
+
var deprecatedTLS = {
|
|
3850
|
+
id: "VC122",
|
|
3851
|
+
title: "Deprecated TLS Version Configured",
|
|
3852
|
+
severity: "high",
|
|
3853
|
+
category: "Cryptography",
|
|
3854
|
+
description: "TLS 1.0 and 1.1 are deprecated and have known vulnerabilities. Only TLS 1.2+ should be used.",
|
|
3855
|
+
check(content, filePath) {
|
|
3856
|
+
if (isTestFile(filePath)) return [];
|
|
3857
|
+
if (!filePath.match(/\.(js|ts|py|rb|go|java|yaml|yml|json|conf|cfg|xml)$/)) return [];
|
|
3858
|
+
const findings = [];
|
|
3859
|
+
const patterns = [
|
|
3860
|
+
/(?:TLSv1_METHOD|TLSv1\.0|TLSv1\.1|ssl\.PROTOCOL_TLSv1|SSLv3|SSLv2|TLS_1_0|TLS_1_1|minVersion\s*[:=]\s*["']TLSv1(?:\.1)?["'])/gi,
|
|
3861
|
+
/(?:tls\.DEFAULT_MIN_VERSION|secureProtocol)\s*[:=]\s*["'](?:TLSv1_method|TLSv1_1_method|SSLv3_method)["']/gi,
|
|
3862
|
+
/ssl_protocols\s+.*(?:TLSv1(?:\.1)?(?:\s|;))/gi
|
|
3863
|
+
];
|
|
3864
|
+
for (const pat of patterns) {
|
|
3865
|
+
let m;
|
|
3866
|
+
while ((m = pat.exec(content)) !== null) {
|
|
3867
|
+
if (isCommentLine(content, m.index)) continue;
|
|
3868
|
+
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3869
|
+
findings.push({
|
|
3870
|
+
rule: "VC122",
|
|
3871
|
+
title: deprecatedTLS.title,
|
|
3872
|
+
severity: "high",
|
|
3873
|
+
category: "Cryptography",
|
|
3874
|
+
file: filePath,
|
|
3875
|
+
line: lineNum,
|
|
3876
|
+
snippet: getSnippet(content, lineNum),
|
|
3877
|
+
fix: "Use TLS 1.2 or higher. Set minVersion to 'TLSv1.2' and remove support for TLS 1.0/1.1."
|
|
3878
|
+
});
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
return findings;
|
|
3882
|
+
}
|
|
3883
|
+
};
|
|
3884
|
+
var weakRSAKeySize = {
|
|
3885
|
+
id: "VC123",
|
|
3886
|
+
title: "Weak RSA Key Size",
|
|
3887
|
+
severity: "high",
|
|
3888
|
+
category: "Cryptography",
|
|
3889
|
+
description: "RSA keys smaller than 2048 bits are considered insecure and can be factored with modern hardware.",
|
|
3890
|
+
check(content, filePath) {
|
|
3891
|
+
if (isTestFile(filePath)) return [];
|
|
3892
|
+
if (!filePath.match(/\.(js|ts|py|rb|go|java|php)$/)) return [];
|
|
3893
|
+
const findings = [];
|
|
3894
|
+
const patterns = [
|
|
3895
|
+
/generateKeyPair\s*\(\s*["']rsa["']\s*,\s*\{[^}]*modulusLength\s*:\s*(512|768|1024)\b/gi,
|
|
3896
|
+
/RSA\.generate\s*\(\s*(512|768|1024)\b/gi,
|
|
3897
|
+
/rsa\.GenerateKey\s*\([^,]*,\s*(512|768|1024)\b/gi,
|
|
3898
|
+
/KeyPairGenerator.*initialize\s*\(\s*(512|768|1024)\b/gi
|
|
3899
|
+
];
|
|
3900
|
+
for (const pat of patterns) {
|
|
3901
|
+
let m;
|
|
3902
|
+
while ((m = pat.exec(content)) !== null) {
|
|
3903
|
+
if (isCommentLine(content, m.index)) continue;
|
|
3904
|
+
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3905
|
+
findings.push({
|
|
3906
|
+
rule: "VC123",
|
|
3907
|
+
title: weakRSAKeySize.title,
|
|
3908
|
+
severity: "high",
|
|
3909
|
+
category: "Cryptography",
|
|
3910
|
+
file: filePath,
|
|
3911
|
+
line: lineNum,
|
|
3912
|
+
snippet: getSnippet(content, lineNum),
|
|
3913
|
+
fix: "Use RSA key sizes of at least 2048 bits. 4096 bits is recommended for long-term security."
|
|
3914
|
+
});
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
return findings;
|
|
3918
|
+
}
|
|
3919
|
+
};
|
|
3920
|
+
var ecbModeEncryption = {
|
|
3921
|
+
id: "VC124",
|
|
3922
|
+
title: "Insecure ECB Mode Encryption",
|
|
3923
|
+
severity: "high",
|
|
3924
|
+
category: "Cryptography",
|
|
3925
|
+
description: "ECB (Electronic Codebook) mode encrypts identical plaintext blocks to identical ciphertext, leaking patterns in the data.",
|
|
3926
|
+
check(content, filePath) {
|
|
3927
|
+
if (isTestFile(filePath)) return [];
|
|
3928
|
+
if (!filePath.match(/\.(js|ts|py|rb|go|java|php)$/)) return [];
|
|
3929
|
+
const findings = [];
|
|
3930
|
+
const patterns = [
|
|
3931
|
+
/createCipher(?:iv)?\s*\(\s*["'](?:aes-(?:128|192|256)-ecb|des-ecb)["']/gi,
|
|
3932
|
+
/AES\.(?:new|MODE_ECB)|MODE_ECB/gi,
|
|
3933
|
+
/Cipher\.getInstance\s*\(\s*["']AES\/ECB/gi,
|
|
3934
|
+
/aes\.NewCipher|cipher\.NewECBEncrypter/gi
|
|
3935
|
+
];
|
|
3936
|
+
for (const pat of patterns) {
|
|
3937
|
+
let m;
|
|
3938
|
+
while ((m = pat.exec(content)) !== null) {
|
|
3939
|
+
if (isCommentLine(content, m.index)) continue;
|
|
3940
|
+
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3941
|
+
findings.push({
|
|
3942
|
+
rule: "VC124",
|
|
3943
|
+
title: ecbModeEncryption.title,
|
|
3944
|
+
severity: "high",
|
|
3945
|
+
category: "Cryptography",
|
|
3946
|
+
file: filePath,
|
|
3947
|
+
line: lineNum,
|
|
3948
|
+
snippet: getSnippet(content, lineNum),
|
|
3949
|
+
fix: "Use AES-GCM or AES-CBC with HMAC instead of ECB mode. GCM provides both encryption and authentication."
|
|
3950
|
+
});
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
return findings;
|
|
3954
|
+
}
|
|
3955
|
+
};
|
|
3956
|
+
var insecurePasswordReset = {
|
|
3957
|
+
id: "VC125",
|
|
3958
|
+
title: "Insecure Password Reset Implementation",
|
|
3959
|
+
severity: "high",
|
|
3960
|
+
category: "Authentication",
|
|
3961
|
+
description: "Password reset using predictable tokens, no expiration, or user-enumeration leaks can be exploited to take over accounts.",
|
|
3962
|
+
check(content, filePath) {
|
|
3963
|
+
if (isTestFile(filePath)) return [];
|
|
3964
|
+
if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];
|
|
3965
|
+
if (!/(?:reset|forgot).*(?:password|passwd)/i.test(content)) return [];
|
|
3966
|
+
const findings = [];
|
|
3967
|
+
const weakTokens = /(?:reset|forgot).*(?:token|code)\s*[:=].*(?:Date\.now|Math\.random|uuid\(\)|nanoid\(\))/gi;
|
|
3968
|
+
let m;
|
|
3969
|
+
while ((m = weakTokens.exec(content)) !== null) {
|
|
3970
|
+
if (isCommentLine(content, m.index)) continue;
|
|
3971
|
+
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3972
|
+
findings.push({
|
|
3973
|
+
rule: "VC125",
|
|
3974
|
+
title: insecurePasswordReset.title,
|
|
3975
|
+
severity: "high",
|
|
3976
|
+
category: "Authentication",
|
|
3977
|
+
file: filePath,
|
|
3978
|
+
line: lineNum,
|
|
3979
|
+
snippet: getSnippet(content, lineNum),
|
|
3980
|
+
fix: "Use crypto.randomBytes(32).toString('hex') for reset tokens. Set expiration (15-60 minutes) and single-use enforcement."
|
|
3981
|
+
});
|
|
3982
|
+
}
|
|
3983
|
+
const enumeration = /(?:user|email|account)\s*(?:not\s*found|does\s*not\s*exist|invalid)/gi;
|
|
3984
|
+
while ((m = enumeration.exec(content)) !== null) {
|
|
3985
|
+
if (isCommentLine(content, m.index)) continue;
|
|
3986
|
+
const surrounding = content.substring(Math.max(0, m.index - 500), m.index);
|
|
3987
|
+
if (!/(?:reset|forgot).*password/i.test(surrounding)) continue;
|
|
3988
|
+
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3989
|
+
findings.push({
|
|
3990
|
+
rule: "VC125",
|
|
3991
|
+
title: insecurePasswordReset.title,
|
|
3992
|
+
severity: "high",
|
|
3993
|
+
category: "Authentication",
|
|
3994
|
+
file: filePath,
|
|
3995
|
+
line: lineNum,
|
|
3996
|
+
snippet: getSnippet(content, lineNum),
|
|
3997
|
+
fix: "Always return the same response regardless of whether the email exists. Say 'If an account exists, a reset link was sent.'"
|
|
3998
|
+
});
|
|
3999
|
+
}
|
|
4000
|
+
return findings;
|
|
4001
|
+
}
|
|
4002
|
+
};
|
|
4003
|
+
var terraformStateExposed = {
|
|
4004
|
+
id: "VC126",
|
|
4005
|
+
title: "Terraform State File Committed",
|
|
4006
|
+
severity: "critical",
|
|
4007
|
+
category: "Secrets",
|
|
4008
|
+
description: "Terraform state files contain sensitive infrastructure details, secrets, and access credentials in plaintext. They must never be committed to version control.",
|
|
4009
|
+
check(content, filePath) {
|
|
4010
|
+
const findings = [];
|
|
4011
|
+
if (/terraform\.tfstate(\.backup)?$/.test(filePath)) {
|
|
4012
|
+
findings.push({
|
|
4013
|
+
rule: "VC126",
|
|
4014
|
+
title: terraformStateExposed.title,
|
|
4015
|
+
severity: "critical",
|
|
4016
|
+
category: "Secrets",
|
|
4017
|
+
file: filePath,
|
|
4018
|
+
line: 1,
|
|
4019
|
+
snippet: getSnippet(content, 1),
|
|
4020
|
+
fix: "Add '*.tfstate' and '*.tfstate.backup' to .gitignore. Use remote state backends (S3, GCS, Terraform Cloud) instead."
|
|
4021
|
+
});
|
|
4022
|
+
}
|
|
4023
|
+
if (filePath.endsWith(".gitignore") && !content.includes("tfstate")) {
|
|
4024
|
+
if (/\.tf$/.test(filePath) || content.includes(".tf")) {
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
return findings;
|
|
4028
|
+
}
|
|
4029
|
+
};
|
|
4030
|
+
var insecureHTTPMethods = {
|
|
4031
|
+
id: "VC127",
|
|
4032
|
+
title: "Dangerous HTTP Methods Without Auth",
|
|
4033
|
+
severity: "high",
|
|
4034
|
+
category: "Authorization",
|
|
4035
|
+
description: "DELETE, PUT, and PATCH endpoints without authentication checks allow unauthorized data modification or deletion.",
|
|
4036
|
+
check(content, filePath) {
|
|
4037
|
+
if (isTestFile(filePath)) return [];
|
|
4038
|
+
if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];
|
|
4039
|
+
const findings = [];
|
|
4040
|
+
const routePatterns = [
|
|
4041
|
+
/(?:app|router|server)\.(delete|put|patch)\s*\(\s*["']/gi
|
|
4042
|
+
];
|
|
4043
|
+
for (const pat of routePatterns) {
|
|
4044
|
+
let m;
|
|
4045
|
+
while ((m = pat.exec(content)) !== null) {
|
|
4046
|
+
if (isCommentLine(content, m.index)) continue;
|
|
4047
|
+
const handlerBlock = content.substring(m.index, Math.min(content.length, m.index + 500));
|
|
4048
|
+
if (/auth|authenticate|authorize|requireAuth|isAuthenticated|protect|guard|middleware|session|jwt|verify|clerk|getAuth/i.test(handlerBlock)) continue;
|
|
4049
|
+
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
4050
|
+
findings.push({
|
|
4051
|
+
rule: "VC127",
|
|
4052
|
+
title: insecureHTTPMethods.title,
|
|
4053
|
+
severity: "high",
|
|
4054
|
+
category: "Authorization",
|
|
4055
|
+
file: filePath,
|
|
4056
|
+
line: lineNum,
|
|
4057
|
+
snippet: getSnippet(content, lineNum),
|
|
4058
|
+
fix: "Add authentication middleware to DELETE, PUT, and PATCH routes. Verify the user has permission to modify the resource."
|
|
4059
|
+
});
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
return findings;
|
|
4063
|
+
}
|
|
4064
|
+
};
|
|
4065
|
+
var httpRequestSmuggling = {
|
|
4066
|
+
id: "VC128",
|
|
4067
|
+
title: "HTTP Request Smuggling Risk",
|
|
4068
|
+
severity: "high",
|
|
4069
|
+
category: "Configuration",
|
|
4070
|
+
description: "Manually parsing Content-Length or Transfer-Encoding headers can lead to request smuggling when behind a reverse proxy.",
|
|
4071
|
+
check(content, filePath) {
|
|
4072
|
+
if (isTestFile(filePath)) return [];
|
|
4073
|
+
if (!filePath.match(/\.(js|ts|py|rb|go|java|php)$/)) return [];
|
|
4074
|
+
const findings = [];
|
|
4075
|
+
const patterns = [
|
|
4076
|
+
/(?:headers?\[?|getHeader\s*\(\s*)["'](?:content-length|transfer-encoding)["']\s*\]?\s*\)/gi,
|
|
4077
|
+
/parseInt\s*\(\s*(?:req|request)\.headers?\[?\s*["']content-length["']/gi
|
|
4078
|
+
];
|
|
4079
|
+
for (const pat of patterns) {
|
|
4080
|
+
let m;
|
|
4081
|
+
while ((m = pat.exec(content)) !== null) {
|
|
4082
|
+
if (isCommentLine(content, m.index)) continue;
|
|
4083
|
+
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
4084
|
+
findings.push({
|
|
4085
|
+
rule: "VC128",
|
|
4086
|
+
title: httpRequestSmuggling.title,
|
|
4087
|
+
severity: "high",
|
|
4088
|
+
category: "Configuration",
|
|
4089
|
+
file: filePath,
|
|
4090
|
+
line: lineNum,
|
|
4091
|
+
snippet: getSnippet(content, lineNum),
|
|
4092
|
+
fix: "Let your framework handle Content-Length and Transfer-Encoding headers. Do not manually parse or trust these values."
|
|
4093
|
+
});
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
return findings;
|
|
4097
|
+
}
|
|
4098
|
+
};
|
|
4099
|
+
var unencryptedPII = {
|
|
4100
|
+
id: "VC129",
|
|
4101
|
+
title: "Sensitive Data Stored Without Encryption",
|
|
4102
|
+
severity: "high",
|
|
4103
|
+
category: "Information Leakage",
|
|
4104
|
+
description: "Database schemas storing PII (SSN, credit cards, health records) in plaintext violate compliance requirements and expose data in breaches.",
|
|
4105
|
+
check(content, filePath) {
|
|
4106
|
+
if (isTestFile(filePath)) return [];
|
|
4107
|
+
if (!filePath.match(/\.(sql|prisma|py|ts|js|rb)$/)) return [];
|
|
4108
|
+
const findings = [];
|
|
4109
|
+
const schemaPatterns = [
|
|
4110
|
+
/(?:column|field|Column|Field)\s*\(\s*["'](?:ssn|social_security|tax_id|credit_card|card_number|cvv|passport|driver_license|bank_account|routing_number)["']\s*,\s*(?:String|TEXT|VARCHAR|Text|text)/gi,
|
|
4111
|
+
/(?:ssn|social_security|tax_id|credit_card|card_number|cvv|passport_number|driver_license|bank_account|routing_number)\s+(?:TEXT|VARCHAR|String|text|character varying)/gi
|
|
4112
|
+
];
|
|
4113
|
+
for (const pat of schemaPatterns) {
|
|
4114
|
+
let m;
|
|
4115
|
+
while ((m = pat.exec(content)) !== null) {
|
|
4116
|
+
if (isCommentLine(content, m.index)) continue;
|
|
4117
|
+
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
4118
|
+
findings.push({
|
|
4119
|
+
rule: "VC129",
|
|
4120
|
+
title: unencryptedPII.title,
|
|
4121
|
+
severity: "high",
|
|
4122
|
+
category: "Information Leakage",
|
|
4123
|
+
file: filePath,
|
|
4124
|
+
line: lineNum,
|
|
4125
|
+
snippet: getSnippet(content, lineNum),
|
|
4126
|
+
fix: "Encrypt sensitive fields at the application level before storing. Use field-level encryption with a KMS for SSN, credit cards, and health data."
|
|
4127
|
+
});
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
return findings;
|
|
4131
|
+
}
|
|
4132
|
+
};
|
|
4133
|
+
var missingAuthRateLimit = {
|
|
4134
|
+
id: "VC130",
|
|
4135
|
+
title: "Authentication Endpoint Without Rate Limiting",
|
|
4136
|
+
severity: "high",
|
|
4137
|
+
category: "Authentication",
|
|
4138
|
+
description: "Login, registration, and password reset endpoints without rate limiting are vulnerable to credential stuffing and brute-force attacks.",
|
|
4139
|
+
check(content, filePath) {
|
|
4140
|
+
if (isTestFile(filePath)) return [];
|
|
4141
|
+
if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];
|
|
4142
|
+
const findings = [];
|
|
4143
|
+
const authRoutePattern = /(?:app|router|server)\.(?:post|put)\s*\(\s*["'](?:\/(?:api\/)?(?:auth\/)?(?:login|signin|sign-in|register|signup|sign-up|forgot-password|reset-password))["']/gi;
|
|
4144
|
+
let m;
|
|
4145
|
+
while ((m = authRoutePattern.exec(content)) !== null) {
|
|
4146
|
+
if (isCommentLine(content, m.index)) continue;
|
|
4147
|
+
const surrounding = content.substring(Math.max(0, m.index - 300), Math.min(content.length, m.index + 300));
|
|
4148
|
+
if (/rateLimit|rateLimiter|throttle|slowDown|express-rate-limit|rate_limit|RateLimiter|limiter/i.test(surrounding)) continue;
|
|
4149
|
+
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
4150
|
+
findings.push({
|
|
4151
|
+
rule: "VC130",
|
|
4152
|
+
title: missingAuthRateLimit.title,
|
|
4153
|
+
severity: "high",
|
|
4154
|
+
category: "Authentication",
|
|
4155
|
+
file: filePath,
|
|
4156
|
+
line: lineNum,
|
|
4157
|
+
snippet: getSnippet(content, lineNum),
|
|
4158
|
+
fix: "Add rate limiting to authentication endpoints. Limit to 5-10 attempts per minute per IP. Use express-rate-limit or similar."
|
|
4159
|
+
});
|
|
4160
|
+
}
|
|
4161
|
+
return findings;
|
|
4162
|
+
}
|
|
4163
|
+
};
|
|
4164
|
+
var vulnerableDependencies = {
|
|
4165
|
+
id: "VC131",
|
|
4166
|
+
title: "Potentially Vulnerable Dependency",
|
|
4167
|
+
severity: "high",
|
|
4168
|
+
category: "Supply Chain",
|
|
4169
|
+
description: "Dependencies with known security issues that are commonly found in AI-generated codebases.",
|
|
4170
|
+
check(content, filePath) {
|
|
4171
|
+
if (!filePath.endsWith("package.json")) return [];
|
|
4172
|
+
if (/node_modules|\.next|dist|build/.test(filePath)) return [];
|
|
4173
|
+
const findings = [];
|
|
4174
|
+
const vulnerablePackages = [
|
|
4175
|
+
[/"jsonwebtoken"\s*:\s*"[\^~]?[0-7]\./g, "jsonwebtoken < 8.x has signature bypass vulnerabilities"],
|
|
4176
|
+
[/"lodash"\s*:\s*"[\^~]?[0-3]\./g, "lodash < 4.x has prototype pollution vulnerabilities"],
|
|
4177
|
+
[/"minimist"\s*:\s*"[\^~]?[01]\.[01]\./g, "minimist < 1.2.6 has prototype pollution"],
|
|
4178
|
+
[/"node-fetch"\s*:\s*"[\^~]?[12]\./g, "node-fetch < 3.x has data exposure issues"],
|
|
4179
|
+
[/"express"\s*:\s*"[\^~]?[0-3]\./g, "express < 4.x has multiple known vulnerabilities"],
|
|
4180
|
+
[/"axios"\s*:\s*"[\^~]?0\.[0-9]\./g, "axios < 0.21 has SSRF vulnerabilities"],
|
|
4181
|
+
[/"tar"\s*:\s*"[\^~]?[0-5]\./g, "tar < 6.x has path traversal vulnerabilities"],
|
|
4182
|
+
[/"glob-parent"\s*:\s*"[\^~]?[0-4]\./g, "glob-parent < 5.1.2 has ReDoS vulnerability"]
|
|
4183
|
+
];
|
|
4184
|
+
for (const [pat, message] of vulnerablePackages) {
|
|
4185
|
+
let m;
|
|
4186
|
+
while ((m = pat.exec(content)) !== null) {
|
|
4187
|
+
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
4188
|
+
findings.push({
|
|
4189
|
+
rule: "VC131",
|
|
4190
|
+
title: vulnerableDependencies.title,
|
|
4191
|
+
severity: "high",
|
|
4192
|
+
category: "Supply Chain",
|
|
4193
|
+
file: filePath,
|
|
4194
|
+
line: lineNum,
|
|
4195
|
+
snippet: getSnippet(content, lineNum),
|
|
4196
|
+
fix: `${message}. Update to the latest version and run 'npm audit' regularly.`
|
|
4197
|
+
});
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
return findings;
|
|
4201
|
+
}
|
|
4202
|
+
};
|
|
3649
4203
|
var freeRules = [
|
|
3650
4204
|
hardcodedSecrets,
|
|
3651
4205
|
// VC001
|
|
@@ -3824,7 +4378,37 @@ var proRules = [
|
|
|
3824
4378
|
dockerCopySensitive,
|
|
3825
4379
|
dockerTooManyPorts,
|
|
3826
4380
|
k8sSecretNotEncrypted,
|
|
3827
|
-
k8sNoResourceLimits
|
|
4381
|
+
k8sNoResourceLimits,
|
|
4382
|
+
pathTraversal,
|
|
4383
|
+
// VC117
|
|
4384
|
+
piiInLogs,
|
|
4385
|
+
// VC118
|
|
4386
|
+
hardcodedOAuthSecret,
|
|
4387
|
+
// VC119
|
|
4388
|
+
missingOAuthState,
|
|
4389
|
+
// VC120
|
|
4390
|
+
unpinnedGitHubAction,
|
|
4391
|
+
// VC121
|
|
4392
|
+
deprecatedTLS,
|
|
4393
|
+
// VC122
|
|
4394
|
+
weakRSAKeySize,
|
|
4395
|
+
// VC123
|
|
4396
|
+
ecbModeEncryption,
|
|
4397
|
+
// VC124
|
|
4398
|
+
insecurePasswordReset,
|
|
4399
|
+
// VC125
|
|
4400
|
+
terraformStateExposed,
|
|
4401
|
+
// VC126
|
|
4402
|
+
insecureHTTPMethods,
|
|
4403
|
+
// VC127
|
|
4404
|
+
httpRequestSmuggling,
|
|
4405
|
+
// VC128
|
|
4406
|
+
unencryptedPII,
|
|
4407
|
+
// VC129
|
|
4408
|
+
missingAuthRateLimit,
|
|
4409
|
+
// VC130
|
|
4410
|
+
vulnerableDependencies
|
|
4411
|
+
// VC131
|
|
3828
4412
|
];
|
|
3829
4413
|
function runCustomRules(content, filePath, disabledRules = [], tier = "free") {
|
|
3830
4414
|
const findings = [];
|
|
@@ -5427,10 +6011,16 @@ async function scanCommand(directory, options) {
|
|
|
5427
6011
|
const config = await loadConfig(dir);
|
|
5428
6012
|
const useAI = (options.aiAnalysis ?? config.ai ?? true) && !!process.env.ANTHROPIC_API_KEY;
|
|
5429
6013
|
const isSilent = format !== "terminal";
|
|
6014
|
+
let tier = "free";
|
|
6015
|
+
let userPlan = "anonymous";
|
|
5430
6016
|
if (isAuthenticated()) {
|
|
5431
6017
|
const usage = await checkUsage();
|
|
6018
|
+
userPlan = usage.plan;
|
|
6019
|
+
if (usage.plan === "pro") {
|
|
6020
|
+
tier = "pro";
|
|
6021
|
+
}
|
|
5432
6022
|
if (!usage.allowed) {
|
|
5433
|
-
console.log(chalk2.red("\nDaily scan limit reached
|
|
6023
|
+
console.log(chalk2.red("\nDaily scan limit reached."));
|
|
5434
6024
|
console.log(chalk2.yellow("Upgrade to Pro for unlimited scans: ") + chalk2.bold("xploitscan upgrade"));
|
|
5435
6025
|
console.log(chalk2.gray(`Resets tomorrow. Plan: ${usage.plan}
|
|
5436
6026
|
`));
|
|
@@ -5481,7 +6071,7 @@ async function scanCommand(directory, options) {
|
|
|
5481
6071
|
fileContentsForAnalysis.push({ path: filePath, content });
|
|
5482
6072
|
const astCtx = buildASTContext(content, filePath);
|
|
5483
6073
|
if (astCtx.isScannerFile) continue;
|
|
5484
|
-
const findings = runCustomRules(content, filePath, config.disableRules);
|
|
6074
|
+
const findings = runCustomRules(content, filePath, config.disableRules, tier);
|
|
5485
6075
|
for (const f of findings) {
|
|
5486
6076
|
if (astCtx.isTestFile) {
|
|
5487
6077
|
f.confidence = "low";
|
|
@@ -5621,6 +6211,15 @@ async function scanCommand(directory, options) {
|
|
|
5621
6211
|
renderTerminalReport(result, fileContentsForAnalysis);
|
|
5622
6212
|
break;
|
|
5623
6213
|
}
|
|
6214
|
+
if (tier === "free" && !isSilent) {
|
|
6215
|
+
console.log("");
|
|
6216
|
+
if (userPlan === "anonymous") {
|
|
6217
|
+
console.log(chalk2.gray(" Scanned with 30 free rules.") + chalk2.cyan(" Log in to unlock all 131 rules \u2192") + chalk2.bold(" xploitscan auth login"));
|
|
6218
|
+
} else {
|
|
6219
|
+
console.log(chalk2.gray(" Scanned with 30 rules.") + chalk2.cyan(" Upgrade to Pro for all 131 rules \u2192") + chalk2.bold(" xploitscan upgrade"));
|
|
6220
|
+
}
|
|
6221
|
+
console.log("");
|
|
6222
|
+
}
|
|
5624
6223
|
if (isAuthenticated()) {
|
|
5625
6224
|
await Promise.allSettled([
|
|
5626
6225
|
incrementUsage(),
|
|
@@ -5833,7 +6432,7 @@ Open this URL in your browser to log in:`));
|
|
|
5833
6432
|
var program = new Command();
|
|
5834
6433
|
program.name("xploitscan").description(
|
|
5835
6434
|
"AI security scanner for vibe-coded apps. Find vulnerabilities before attackers do."
|
|
5836
|
-
).version("0.
|
|
6435
|
+
).version("0.8.0");
|
|
5837
6436
|
program.command("scan").description("Scan a directory for security vulnerabilities").argument("[directory]", "Directory to scan", ".").option("--no-ai", "Skip AI-powered analysis").option("-f, --format <format>", "Output format: terminal, json, sarif", "terminal").option("-v, --verbose", "Show detailed output", false).option("--diff [base]", "Scan only files changed vs base branch (default: main)").option("-w, --watch", "Watch for file changes and re-scan automatically", false).action(async (directory, opts) => {
|
|
5838
6437
|
await scanCommand(directory, {
|
|
5839
6438
|
directory,
|
|
@@ -5849,7 +6448,7 @@ auth.command("login").description("Log in to your XploitScan account").action(lo
|
|
|
5849
6448
|
auth.command("logout").description("Log out of your XploitScan account").action(logoutCommand);
|
|
5850
6449
|
auth.command("whoami").description("Show current logged-in user").action(whoamiCommand);
|
|
5851
6450
|
program.command("upgrade").description("Upgrade to XploitScan Pro for unlimited scans").action(async () => {
|
|
5852
|
-
const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-
|
|
6451
|
+
const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-Z7VNGPT2.js");
|
|
5853
6452
|
const chalk4 = (await import("chalk")).default;
|
|
5854
6453
|
const token = getStoredToken2();
|
|
5855
6454
|
if (!token) {
|
|
@@ -5862,7 +6461,7 @@ program.command("upgrade").description("Upgrade to XploitScan Pro for unlimited
|
|
|
5862
6461
|
console.log(chalk4.green(`
|
|
5863
6462
|
Open this URL to upgrade:`));
|
|
5864
6463
|
console.log(chalk4.bold.underline(url));
|
|
5865
|
-
const { execFile: execFile4 } =
|
|
6464
|
+
const { execFile: execFile4 } = await import("child_process");
|
|
5866
6465
|
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
5867
6466
|
execFile4(openCmd, [url], () => {
|
|
5868
6467
|
});
|