ghostpatch 1.0.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.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +213 -0
  3. package/__tests__/detectors.test.ts +224 -0
  4. package/__tests__/rules.test.ts +117 -0
  5. package/__tests__/scanner.test.ts +222 -0
  6. package/dist/ai/anthropic.d.ts +11 -0
  7. package/dist/ai/anthropic.d.ts.map +1 -0
  8. package/dist/ai/anthropic.js +76 -0
  9. package/dist/ai/anthropic.js.map +1 -0
  10. package/dist/ai/huggingface.d.ts +12 -0
  11. package/dist/ai/huggingface.d.ts.map +1 -0
  12. package/dist/ai/huggingface.js +95 -0
  13. package/dist/ai/huggingface.js.map +1 -0
  14. package/dist/ai/openai.d.ts +11 -0
  15. package/dist/ai/openai.d.ts.map +1 -0
  16. package/dist/ai/openai.js +71 -0
  17. package/dist/ai/openai.js.map +1 -0
  18. package/dist/ai/prompts.d.ts +5 -0
  19. package/dist/ai/prompts.d.ts.map +1 -0
  20. package/dist/ai/prompts.js +101 -0
  21. package/dist/ai/prompts.js.map +1 -0
  22. package/dist/ai/provider.d.ts +9 -0
  23. package/dist/ai/provider.d.ts.map +1 -0
  24. package/dist/ai/provider.js +66 -0
  25. package/dist/ai/provider.js.map +1 -0
  26. package/dist/cli/index.d.ts +3 -0
  27. package/dist/cli/index.d.ts.map +1 -0
  28. package/dist/cli/index.js +318 -0
  29. package/dist/cli/index.js.map +1 -0
  30. package/dist/core/reporter.d.ts +7 -0
  31. package/dist/core/reporter.d.ts.map +1 -0
  32. package/dist/core/reporter.js +366 -0
  33. package/dist/core/reporter.js.map +1 -0
  34. package/dist/core/rules.d.ts +8 -0
  35. package/dist/core/rules.d.ts.map +1 -0
  36. package/dist/core/rules.js +1077 -0
  37. package/dist/core/rules.js.map +1 -0
  38. package/dist/core/scanner.d.ts +6 -0
  39. package/dist/core/scanner.d.ts.map +1 -0
  40. package/dist/core/scanner.js +217 -0
  41. package/dist/core/scanner.js.map +1 -0
  42. package/dist/core/severity.d.ts +100 -0
  43. package/dist/core/severity.d.ts.map +1 -0
  44. package/dist/core/severity.js +52 -0
  45. package/dist/core/severity.js.map +1 -0
  46. package/dist/detectors/auth.d.ts +3 -0
  47. package/dist/detectors/auth.d.ts.map +1 -0
  48. package/dist/detectors/auth.js +138 -0
  49. package/dist/detectors/auth.js.map +1 -0
  50. package/dist/detectors/crypto.d.ts +3 -0
  51. package/dist/detectors/crypto.d.ts.map +1 -0
  52. package/dist/detectors/crypto.js +128 -0
  53. package/dist/detectors/crypto.js.map +1 -0
  54. package/dist/detectors/dependency.d.ts +4 -0
  55. package/dist/detectors/dependency.d.ts.map +1 -0
  56. package/dist/detectors/dependency.js +267 -0
  57. package/dist/detectors/dependency.js.map +1 -0
  58. package/dist/detectors/deserialize.d.ts +3 -0
  59. package/dist/detectors/deserialize.d.ts.map +1 -0
  60. package/dist/detectors/deserialize.js +107 -0
  61. package/dist/detectors/deserialize.js.map +1 -0
  62. package/dist/detectors/injection.d.ts +3 -0
  63. package/dist/detectors/injection.d.ts.map +1 -0
  64. package/dist/detectors/injection.js +158 -0
  65. package/dist/detectors/injection.js.map +1 -0
  66. package/dist/detectors/misconfig.d.ts +3 -0
  67. package/dist/detectors/misconfig.d.ts.map +1 -0
  68. package/dist/detectors/misconfig.js +153 -0
  69. package/dist/detectors/misconfig.js.map +1 -0
  70. package/dist/detectors/pathtraversal.d.ts +3 -0
  71. package/dist/detectors/pathtraversal.d.ts.map +1 -0
  72. package/dist/detectors/pathtraversal.js +90 -0
  73. package/dist/detectors/pathtraversal.js.map +1 -0
  74. package/dist/detectors/prototype.d.ts +3 -0
  75. package/dist/detectors/prototype.d.ts.map +1 -0
  76. package/dist/detectors/prototype.js +79 -0
  77. package/dist/detectors/prototype.js.map +1 -0
  78. package/dist/detectors/secrets.d.ts +4 -0
  79. package/dist/detectors/secrets.d.ts.map +1 -0
  80. package/dist/detectors/secrets.js +137 -0
  81. package/dist/detectors/secrets.js.map +1 -0
  82. package/dist/detectors/ssrf.d.ts +3 -0
  83. package/dist/detectors/ssrf.d.ts.map +1 -0
  84. package/dist/detectors/ssrf.js +78 -0
  85. package/dist/detectors/ssrf.js.map +1 -0
  86. package/dist/detectors/zeroday.d.ts +9 -0
  87. package/dist/detectors/zeroday.d.ts.map +1 -0
  88. package/dist/detectors/zeroday.js +77 -0
  89. package/dist/detectors/zeroday.js.map +1 -0
  90. package/dist/index.d.ts +10 -0
  91. package/dist/index.d.ts.map +1 -0
  92. package/dist/index.js +42 -0
  93. package/dist/index.js.map +1 -0
  94. package/dist/mcp/server.d.ts +2 -0
  95. package/dist/mcp/server.d.ts.map +1 -0
  96. package/dist/mcp/server.js +358 -0
  97. package/dist/mcp/server.js.map +1 -0
  98. package/dist/utils/config.d.ts +4 -0
  99. package/dist/utils/config.d.ts.map +1 -0
  100. package/dist/utils/config.js +97 -0
  101. package/dist/utils/config.js.map +1 -0
  102. package/dist/utils/fingerprint.d.ts +5 -0
  103. package/dist/utils/fingerprint.d.ts.map +1 -0
  104. package/dist/utils/fingerprint.js +55 -0
  105. package/dist/utils/fingerprint.js.map +1 -0
  106. package/dist/utils/languages.d.ts +8 -0
  107. package/dist/utils/languages.d.ts.map +1 -0
  108. package/dist/utils/languages.js +128 -0
  109. package/dist/utils/languages.js.map +1 -0
  110. package/package.json +53 -0
  111. package/src/ai/anthropic.ts +82 -0
  112. package/src/ai/huggingface.ts +111 -0
  113. package/src/ai/openai.ts +75 -0
  114. package/src/ai/prompts.ts +100 -0
  115. package/src/ai/provider.ts +68 -0
  116. package/src/cli/index.ts +314 -0
  117. package/src/core/reporter.ts +356 -0
  118. package/src/core/rules.ts +1089 -0
  119. package/src/core/scanner.ts +201 -0
  120. package/src/core/severity.ts +140 -0
  121. package/src/detectors/auth.ts +152 -0
  122. package/src/detectors/crypto.ts +128 -0
  123. package/src/detectors/dependency.ts +240 -0
  124. package/src/detectors/deserialize.ts +106 -0
  125. package/src/detectors/injection.ts +172 -0
  126. package/src/detectors/misconfig.ts +152 -0
  127. package/src/detectors/pathtraversal.ts +89 -0
  128. package/src/detectors/prototype.ts +77 -0
  129. package/src/detectors/secrets.ts +138 -0
  130. package/src/detectors/ssrf.ts +77 -0
  131. package/src/detectors/zeroday.ts +93 -0
  132. package/src/index.ts +24 -0
  133. package/src/mcp/server.ts +379 -0
  134. package/src/utils/config.ts +64 -0
  135. package/src/utils/fingerprint.ts +21 -0
  136. package/src/utils/languages.ts +95 -0
  137. package/tsconfig.json +20 -0
  138. package/vitest.config.ts +8 -0
@@ -0,0 +1,89 @@
1
+ import { Finding, Severity } from '../core/severity';
2
+ import { generateFingerprint } from '../utils/fingerprint';
3
+
4
+ const PATTERNS = [
5
+ {
6
+ id: 'PATH-TRAVERSAL-FS', name: 'Path Traversal (File System)', severity: Severity.HIGH, confidence: 'medium' as const,
7
+ cwe: 'CWE-22',
8
+ pattern: /(?:readFile|readFileSync|createReadStream|writeFile|writeFileSync|appendFile|unlink|unlinkSync|open|openSync|access|accessSync)\s*\(\s*(?:req\.|request\.|input|user|param|query|body|path\.join\s*\([^)]*req)/i,
9
+ description: 'File operation with user-controlled path — directory traversal risk.',
10
+ remediation: 'Validate paths. Use path.resolve() and verify against a base directory.',
11
+ },
12
+ {
13
+ id: 'PATH-TRAVERSAL-PY', name: 'Path Traversal (Python)', severity: Severity.HIGH, confidence: 'medium' as const,
14
+ cwe: 'CWE-22',
15
+ pattern: /(?:open|os\.path\.join|pathlib\.Path|shutil\.copy|os\.rename)\s*\(\s*(?:request\.|input|user_|flask\.request|args\.get|form\.get)/i,
16
+ description: 'File operation with user-controlled path in Python.',
17
+ remediation: 'Use os.path.realpath() and verify against base directory.',
18
+ },
19
+ {
20
+ id: 'PATH-TRAVERSAL-JAVA', name: 'Path Traversal (Java)', severity: Severity.HIGH, confidence: 'medium' as const,
21
+ cwe: 'CWE-22',
22
+ pattern: /(?:new\s+File|Paths\.get|FileInputStream|FileOutputStream)\s*\(\s*(?:request\.getParameter|servletRequest|param|input)/i,
23
+ description: 'File operation with user-controlled path in Java.',
24
+ remediation: 'Canonicalize path and verify it starts with the expected base directory.',
25
+ },
26
+ {
27
+ id: 'PATH-ZIP-SLIP', name: 'Zip Slip Vulnerability', severity: Severity.HIGH, confidence: 'medium' as const,
28
+ cwe: 'CWE-22',
29
+ pattern: /(?:extractAll|extract\s*\(|unzip|ZipFile|tar\.extractall|tar\.extract|decompress|gunzip)\s*\(/i,
30
+ antiPattern: /(?:validatePath|sanitize|startsWith|normalize|realpath|abspath|canonical)/i,
31
+ description: 'Archive extraction without path validation — Zip Slip vulnerability.',
32
+ remediation: 'Validate extracted paths stay within intended directory.',
33
+ },
34
+ {
35
+ id: 'PATH-DOT-DOT', name: 'Directory Traversal Sequence', severity: Severity.HIGH, confidence: 'medium' as const,
36
+ cwe: 'CWE-22',
37
+ pattern: /(?:\.\.\/|\.\.\\|%2e%2e%2f|%2e%2e\/|\.\.%2f|%2e%2e%5c)/i,
38
+ antiPattern: /(?:import|require|from\s+['"]|test|spec|relative)/,
39
+ description: 'Directory traversal sequence detected.',
40
+ remediation: 'Reject input containing ".." path sequences.',
41
+ },
42
+ {
43
+ id: 'PATH-SEND-FILE', name: 'Unsafe File Serving', severity: Severity.HIGH, confidence: 'medium' as const,
44
+ cwe: 'CWE-22',
45
+ pattern: /(?:sendFile|send_file|download|serveFile)\s*\(\s*(?:req\.|request\.|path\.join\s*\([^)]*(?:req|param|query))/i,
46
+ description: 'File sent to client based on user input.',
47
+ remediation: 'Validate file paths and restrict to safe directories.',
48
+ },
49
+ ];
50
+
51
+ export function detect(content: string, filePath: string, language: string): Finding[] {
52
+ const findings: Finding[] = [];
53
+ const lines = content.split('\n');
54
+
55
+ for (const pat of PATTERNS) {
56
+ for (let i = 0; i < lines.length; i++) {
57
+ const line = lines[i];
58
+ if (pat.pattern.test(line)) {
59
+ if (pat.antiPattern) {
60
+ const cs = Math.max(0, i - 3);
61
+ const ce = Math.min(lines.length, i + 4);
62
+ if (pat.antiPattern.test(lines.slice(cs, ce).join('\n'))) continue;
63
+ }
64
+ findings.push({
65
+ id: `${pat.id}-${filePath}:${i + 1}`,
66
+ ruleId: pat.id, title: pat.name, description: pat.description,
67
+ severity: pat.severity, confidence: pat.confidence,
68
+ filePath, line: i + 1,
69
+ codeSnippet: getSnippet(lines, i),
70
+ cwe: pat.cwe, owasp: 'A01',
71
+ remediation: pat.remediation,
72
+ fingerprint: generateFingerprint(pat.id, filePath, line.trim()),
73
+ });
74
+ }
75
+ }
76
+ }
77
+ return findings;
78
+ }
79
+
80
+ function getSnippet(lines: string[], index: number, context = 2): string {
81
+ const start = Math.max(0, index - context);
82
+ const end = Math.min(lines.length, index + context + 1);
83
+ return lines.slice(start, end)
84
+ .map((l, i) => {
85
+ const lineNum = start + i + 1;
86
+ const marker = (start + i === index) ? '>' : ' ';
87
+ return `${marker} ${lineNum} | ${l}`;
88
+ }).join('\n');
89
+ }
@@ -0,0 +1,77 @@
1
+ import { Finding, Severity } from '../core/severity';
2
+ import { generateFingerprint } from '../utils/fingerprint';
3
+
4
+ const PATTERNS = [
5
+ {
6
+ id: 'PROTO-MERGE', name: 'Prototype Pollution via Deep Merge', severity: Severity.HIGH, confidence: 'medium' as const,
7
+ cwe: 'CWE-1321',
8
+ pattern: /(?:Object\.assign|_\.merge|_\.extend|_\.defaultsDeep|deepmerge|deep-extend|merge-deep|lodash\.merge)\s*\(/,
9
+ antiPattern: /(?:Object\.create\(null\)|hasOwnProperty|__proto__|prototype|constructor.*check)/i,
10
+ description: 'Deep merge may allow prototype pollution.',
11
+ remediation: 'Validate input keys. Reject __proto__, constructor, and prototype.',
12
+ },
13
+ {
14
+ id: 'PROTO-BRACKET', name: 'Dynamic Property Assignment', severity: Severity.MEDIUM, confidence: 'low' as const,
15
+ cwe: 'CWE-1321',
16
+ pattern: /\w+\s*\[\s*(?:key|prop|name|field|attr|k|p|property)\s*\]\s*=\s*(?!undefined|null|false|true|0|''|"")/,
17
+ antiPattern: /(?:hasOwnProperty|Object\.keys|Object\.entries|whitelist|allowlist|sanitize|__proto__|prototype|constructor)/i,
18
+ description: 'Dynamic property assignment without prototype pollution guard.',
19
+ remediation: 'Check key is not __proto__, constructor, or prototype before assignment.',
20
+ },
21
+ {
22
+ id: 'PROTO-JSON-PARSE', name: 'JSON.parse Without Prototype Check', severity: Severity.LOW, confidence: 'low' as const,
23
+ cwe: 'CWE-1321',
24
+ pattern: /JSON\.parse\s*\(\s*(?:req\.|request\.|body|input|user|data)/i,
25
+ antiPattern: /(?:reviver|filter|sanitize|validate|schema)/i,
26
+ description: 'JSON.parse of user input may contain __proto__ keys.',
27
+ remediation: 'Use a JSON reviver to strip __proto__ keys, or validate input schema.',
28
+ },
29
+ {
30
+ id: 'PROTO-RECURSIVE', name: 'Recursive Object Copy', severity: Severity.MEDIUM, confidence: 'low' as const,
31
+ cwe: 'CWE-1321',
32
+ pattern: /function\s+(?:deep[Cc]opy|deep[Cc]lone|deep[Mm]erge|extend|assign[Dd]eep)\s*\(/,
33
+ antiPattern: /(?:__proto__|prototype|constructor|hasOwnProperty|Object\.create\(null\))/i,
34
+ description: 'Custom deep copy/merge may be vulnerable to prototype pollution.',
35
+ remediation: 'Add checks for __proto__, constructor, and prototype in recursive operations.',
36
+ },
37
+ ];
38
+
39
+ export function detect(content: string, filePath: string, language: string): Finding[] {
40
+ const findings: Finding[] = [];
41
+ if (!['javascript', 'typescript'].includes(language)) return findings;
42
+
43
+ const lines = content.split('\n');
44
+ for (const pat of PATTERNS) {
45
+ for (let i = 0; i < lines.length; i++) {
46
+ const line = lines[i];
47
+ if (pat.pattern.test(line)) {
48
+ if (pat.antiPattern) {
49
+ const cs = Math.max(0, i - 5);
50
+ const ce = Math.min(lines.length, i + 6);
51
+ if (pat.antiPattern.test(lines.slice(cs, ce).join('\n'))) continue;
52
+ }
53
+ findings.push({
54
+ id: `${pat.id}-${filePath}:${i + 1}`,
55
+ ruleId: pat.id, title: pat.name, description: pat.description,
56
+ severity: pat.severity, confidence: pat.confidence,
57
+ filePath, line: i + 1,
58
+ codeSnippet: getSnippet(lines, i),
59
+ cwe: pat.cwe, owasp: 'A03',
60
+ remediation: pat.remediation,
61
+ fingerprint: generateFingerprint(pat.id, filePath, line.trim()),
62
+ });
63
+ }
64
+ }
65
+ }
66
+ return findings;
67
+ }
68
+
69
+ function getSnippet(lines: string[], index: number, context = 2): string {
70
+ const start = Math.max(0, index - context);
71
+ const end = Math.min(lines.length, index + context + 1);
72
+ return lines.slice(start, end).map((l, i) => {
73
+ const lineNum = start + i + 1;
74
+ const marker = (start + i === index) ? '>' : ' ';
75
+ return `${marker} ${lineNum} | ${l}`;
76
+ }).join('\n');
77
+ }
@@ -0,0 +1,138 @@
1
+ import { Finding, Severity } from '../core/severity';
2
+ import { generateFingerprint } from '../utils/fingerprint';
3
+
4
+ const PATTERNS = [
5
+ {
6
+ id: 'SEC-AWS-KEY', name: 'AWS Access Key', severity: Severity.CRITICAL, confidence: 'high' as const,
7
+ cwe: 'CWE-798', pattern: /(?:AKIA|ASIA)[A-Z0-9]{16}/,
8
+ antiPattern: /(?:example|sample|test|fake|dummy|placeholder|xxx|EXAMPLE)/i,
9
+ description: 'AWS access key ID found.', remediation: 'Rotate key immediately. Use IAM roles or env vars.',
10
+ },
11
+ {
12
+ id: 'SEC-AWS-SECRET', name: 'AWS Secret Key', severity: Severity.CRITICAL, confidence: 'high' as const,
13
+ cwe: 'CWE-798', pattern: /(?:aws_secret|AWS_SECRET)\w*\s*[:=]\s*['"][A-Za-z0-9/+=]{40}['"]/,
14
+ description: 'AWS secret access key found.', remediation: 'Rotate immediately. Use AWS Secrets Manager.',
15
+ },
16
+ {
17
+ id: 'SEC-GITHUB-TOKEN', name: 'GitHub Token', severity: Severity.CRITICAL, confidence: 'high' as const,
18
+ cwe: 'CWE-798', pattern: /(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}/,
19
+ description: 'GitHub personal access token found.', remediation: 'Rotate immediately. Use env vars.',
20
+ },
21
+ {
22
+ id: 'SEC-GOOGLE-KEY', name: 'Google API Key', severity: Severity.HIGH, confidence: 'high' as const,
23
+ cwe: 'CWE-798', pattern: /AIza[A-Za-z0-9_\\-]{35}/,
24
+ description: 'Google API key found.', remediation: 'Rotate and restrict key scope. Use env vars.',
25
+ },
26
+ {
27
+ id: 'SEC-SLACK-TOKEN', name: 'Slack Token', severity: Severity.CRITICAL, confidence: 'high' as const,
28
+ cwe: 'CWE-798', pattern: /xox[bpors]-[A-Za-z0-9\-]{10,}/,
29
+ description: 'Slack API token found.', remediation: 'Rotate immediately. Use env vars.',
30
+ },
31
+ {
32
+ id: 'SEC-STRIPE-KEY', name: 'Stripe API Key', severity: Severity.CRITICAL, confidence: 'high' as const,
33
+ cwe: 'CWE-798', pattern: /(?:sk|pk)_(?:live|test)_[A-Za-z0-9]{20,}/,
34
+ description: 'Stripe API key found.', remediation: 'Rotate immediately. Use env vars.',
35
+ },
36
+ {
37
+ id: 'SEC-PRIVATE-KEY', name: 'Private Key', severity: Severity.CRITICAL, confidence: 'high' as const,
38
+ cwe: 'CWE-321', pattern: /-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----/,
39
+ description: 'Private key embedded in source.', remediation: 'Remove from source. Use key management service.',
40
+ },
41
+ {
42
+ id: 'SEC-GENERIC-API-KEY', name: 'Generic API Key', severity: Severity.HIGH, confidence: 'medium' as const,
43
+ cwe: 'CWE-798', pattern: /(?:api[-_]?key|apikey|API[-_]?KEY)\s*[:=]\s*['"][a-zA-Z0-9_\-]{20,}['"]/i,
44
+ antiPattern: /(?:example|sample|test|fake|dummy|placeholder|xxx|your_|process\.env|os\.environ|config\.|env\[)/i,
45
+ description: 'Hardcoded API key found.', remediation: 'Store API keys in env vars or secrets manager.',
46
+ },
47
+ {
48
+ id: 'SEC-GENERIC-SECRET', name: 'Generic Secret/Password', severity: Severity.HIGH, confidence: 'medium' as const,
49
+ cwe: 'CWE-798', pattern: /(?:secret|token|password|passwd|credentials?)\s*[:=]\s*['"][a-zA-Z0-9!@#$%^&*()\-_+=]{12,}['"]/i,
50
+ antiPattern: /(?:example|sample|test|fake|dummy|placeholder|xxx|your_|process\.env|os\.environ|config\.|env\[|<|TODO|CHANGE|REPLACE|type|interface|const\s+\w+:\s*string)/i,
51
+ description: 'Potential hardcoded secret.', remediation: 'Use env vars or secrets manager.',
52
+ },
53
+ {
54
+ id: 'SEC-DB-CONN-STRING', name: 'Database Connection String', severity: Severity.HIGH, confidence: 'high' as const,
55
+ cwe: 'CWE-798', pattern: /['"](?:mongodb(?:\+srv)?|postgres(?:ql)?|mysql|mssql|redis|amqp):\/\/[^:]+:[^@]+@[^'"]+['"]/i,
56
+ antiPattern: /(?:localhost|127\.0\.0\.1|example\.com|process\.env|os\.environ)/i,
57
+ description: 'Database connection string with credentials.', remediation: 'Use env vars for connection strings.',
58
+ },
59
+ {
60
+ id: 'SEC-SENDGRID', name: 'SendGrid API Key', severity: Severity.HIGH, confidence: 'high' as const,
61
+ cwe: 'CWE-798', pattern: /SG\.[A-Za-z0-9_\-]{22}\.[A-Za-z0-9_\-]{43}/,
62
+ description: 'SendGrid API key found.', remediation: 'Rotate immediately. Use env vars.',
63
+ },
64
+ {
65
+ id: 'SEC-TWILIO', name: 'Twilio Credentials', severity: Severity.HIGH, confidence: 'medium' as const,
66
+ cwe: 'CWE-798', pattern: /(?:AC[a-z0-9]{32}|SK[a-z0-9]{32})/,
67
+ description: 'Twilio Account SID or API key found.', remediation: 'Rotate immediately. Use env vars.',
68
+ },
69
+ {
70
+ id: 'SEC-FIREBASE', name: 'Firebase Config Exposed', severity: Severity.MEDIUM, confidence: 'medium' as const,
71
+ cwe: 'CWE-798', pattern: /(?:firebase|FIREBASE).*(?:apiKey|authDomain|databaseURL)\s*[:=]\s*['"]/i,
72
+ description: 'Firebase configuration in source.', remediation: 'Use env vars. Secure with Firebase Security Rules.',
73
+ },
74
+ {
75
+ id: 'SEC-HARDCODED-DB-PASS', name: 'Hardcoded Database Password', severity: Severity.CRITICAL, confidence: 'high' as const,
76
+ cwe: 'CWE-798', pattern: /(?:(?:db|database|mysql|postgres|mongo|redis)[-_.]?(?:password|passwd|pass|pwd))\s*[:=]\s*['"][^'"]{4,}['"]/i,
77
+ antiPattern: /(?:process\.env|os\.environ|config\.|env\[|example|sample|test|your_|placeholder)/i,
78
+ description: 'Database password hardcoded.', remediation: 'Use env vars or secrets manager.',
79
+ },
80
+ {
81
+ id: 'SEC-OPENAI-KEY', name: 'OpenAI API Key', severity: Severity.CRITICAL, confidence: 'high' as const,
82
+ cwe: 'CWE-798', pattern: /sk-[A-Za-z0-9]{32,}/,
83
+ antiPattern: /(?:example|sample|test|fake|placeholder|xxx)/i,
84
+ description: 'OpenAI API key found.', remediation: 'Rotate immediately. Use env vars.',
85
+ },
86
+ {
87
+ id: 'SEC-ANTHROPIC-KEY', name: 'Anthropic API Key', severity: Severity.CRITICAL, confidence: 'high' as const,
88
+ cwe: 'CWE-798', pattern: /sk-ant-[A-Za-z0-9\-]{32,}/,
89
+ description: 'Anthropic API key found.', remediation: 'Rotate immediately. Use env vars.',
90
+ },
91
+ ];
92
+
93
+ // All languages — secrets can appear anywhere
94
+ const ALL_LANGS = ['javascript', 'typescript', 'python', 'java', 'go', 'rust', 'c', 'cpp', 'csharp', 'php', 'ruby', 'swift', 'kotlin', 'shell', 'sql'];
95
+
96
+ export function detect(content: string, filePath: string, _language: string): Finding[] {
97
+ const findings: Finding[] = [];
98
+ const lines = content.split('\n');
99
+
100
+ for (const pat of PATTERNS) {
101
+ for (let i = 0; i < lines.length; i++) {
102
+ const line = lines[i];
103
+ if (pat.pattern.test(line)) {
104
+ if (pat.antiPattern && pat.antiPattern.test(line)) continue;
105
+
106
+ findings.push({
107
+ id: `${pat.id}-${filePath}:${i + 1}`,
108
+ ruleId: pat.id,
109
+ title: pat.name,
110
+ description: pat.description,
111
+ severity: pat.severity,
112
+ confidence: pat.confidence,
113
+ filePath, line: i + 1,
114
+ codeSnippet: getSnippet(lines, i),
115
+ cwe: pat.cwe, owasp: 'A02',
116
+ remediation: pat.remediation,
117
+ fingerprint: generateFingerprint(pat.id, filePath, line.trim()),
118
+ });
119
+ }
120
+ }
121
+ }
122
+ return findings;
123
+ }
124
+
125
+ export function detectSecretsOnly(content: string, filePath: string): Finding[] {
126
+ return detect(content, filePath, 'generic');
127
+ }
128
+
129
+ function getSnippet(lines: string[], index: number, context = 2): string {
130
+ const start = Math.max(0, index - context);
131
+ const end = Math.min(lines.length, index + context + 1);
132
+ return lines.slice(start, end)
133
+ .map((l, i) => {
134
+ const lineNum = start + i + 1;
135
+ const marker = (start + i === index) ? '>' : ' ';
136
+ return `${marker} ${lineNum} | ${l}`;
137
+ }).join('\n');
138
+ }
@@ -0,0 +1,77 @@
1
+ import { Finding, Severity } from '../core/severity';
2
+ import { generateFingerprint } from '../utils/fingerprint';
3
+
4
+ const PATTERNS = [
5
+ {
6
+ id: 'SSRF-USER-URL', name: 'SSRF via User-Controlled URL', severity: Severity.HIGH, confidence: 'medium' as const,
7
+ cwe: 'CWE-918',
8
+ pattern: /(?:fetch|axios|http\.get|https\.get|request|urllib|requests\.(?:get|post|put|delete|head)|HttpClient|http\.NewRequest)\s*\(\s*(?:req\.|request\.|input|user|param|query|body|args)/i,
9
+ description: 'HTTP request URL from user input — SSRF risk.',
10
+ remediation: 'Validate URLs against an allowlist. Block internal/private IP ranges.',
11
+ },
12
+ {
13
+ id: 'SSRF-URL-PARAM', name: 'URL From Query Parameter', severity: Severity.HIGH, confidence: 'medium' as const,
14
+ cwe: 'CWE-918',
15
+ pattern: /(?:url|uri|endpoint|target|destination|redirect|callback|webhook)\s*[:=]\s*(?:req\.query|req\.params|request\.args|request\.GET|params\[)/i,
16
+ description: 'URL taken from query parameter.',
17
+ remediation: 'Validate URL scheme, host, and resolved IP. Use allowlist.',
18
+ },
19
+ {
20
+ id: 'SSRF-IMAGE-FETCH', name: 'Image/File Fetch from URL', severity: Severity.MEDIUM, confidence: 'medium' as const,
21
+ cwe: 'CWE-918',
22
+ pattern: /(?:download|fetch|grab|load|import)(?:Image|File|URL|Resource|Content)\s*\(\s*(?:url|uri|src|href|link)/i,
23
+ antiPattern: /(?:allowlist|whitelist|validateUrl|isAllowed|checkUrl|blockPrivate|isExternal)/i,
24
+ description: 'File/image download from variable URL.',
25
+ remediation: 'Validate URL and block internal IP ranges before fetching.',
26
+ },
27
+ {
28
+ id: 'SSRF-WEBHOOK', name: 'Webhook URL from User', severity: Severity.HIGH, confidence: 'medium' as const,
29
+ cwe: 'CWE-918',
30
+ pattern: /(?:webhook|callback|notify)(?:Url|URL|_url|Uri|URI)\s*[:=]\s*(?:req\.|request\.|body\.|input|user)/i,
31
+ description: 'Webhook URL from user input — can probe internal services.',
32
+ remediation: 'Validate webhook URLs. Block private IP ranges and localhost.',
33
+ },
34
+ ];
35
+
36
+ export function detect(content: string, filePath: string, language: string): Finding[] {
37
+ const findings: Finding[] = [];
38
+ const lines = content.split('\n');
39
+ const backendLangs = ['javascript', 'typescript', 'python', 'java', 'go', 'php', 'ruby', 'csharp', 'kotlin'];
40
+
41
+ if (!backendLangs.includes(language)) return findings;
42
+
43
+ for (const pat of PATTERNS) {
44
+ for (let i = 0; i < lines.length; i++) {
45
+ const line = lines[i];
46
+ if (pat.pattern.test(line)) {
47
+ if (pat.antiPattern) {
48
+ const cs = Math.max(0, i - 3);
49
+ const ce = Math.min(lines.length, i + 4);
50
+ if (pat.antiPattern.test(lines.slice(cs, ce).join('\n'))) continue;
51
+ }
52
+ findings.push({
53
+ id: `${pat.id}-${filePath}:${i + 1}`,
54
+ ruleId: pat.id, title: pat.name, description: pat.description,
55
+ severity: pat.severity, confidence: pat.confidence,
56
+ filePath, line: i + 1,
57
+ codeSnippet: getSnippet(lines, i),
58
+ cwe: pat.cwe, owasp: 'A10',
59
+ remediation: pat.remediation,
60
+ fingerprint: generateFingerprint(pat.id, filePath, line.trim()),
61
+ });
62
+ }
63
+ }
64
+ }
65
+ return findings;
66
+ }
67
+
68
+ function getSnippet(lines: string[], index: number, context = 2): string {
69
+ const start = Math.max(0, index - context);
70
+ const end = Math.min(lines.length, index + context + 1);
71
+ return lines.slice(start, end)
72
+ .map((l, i) => {
73
+ const lineNum = start + i + 1;
74
+ const marker = (start + i === index) ? '>' : ' ';
75
+ return `${marker} ${lineNum} | ${l}`;
76
+ }).join('\n');
77
+ }
@@ -0,0 +1,93 @@
1
+ import { Finding, Severity, AIFinding } from '../core/severity';
2
+ import { generateFingerprint } from '../utils/fingerprint';
3
+
4
+ export interface AIProvider {
5
+ name: string;
6
+ analyze(code: string, context: string): Promise<AIFinding[]>;
7
+ isAvailable(): boolean;
8
+ }
9
+
10
+ const SUSPICIOUS_PATTERNS = [
11
+ { pattern: /(?:setTimeout|setInterval)\s*\(\s*\w+\s*,\s*0\s*\)/, reason: 'Potential race condition with zero-delay timer' },
12
+ { pattern: /(?:await|async).*(?:parallel|all|race).*(?:db|database|write|update|delete|remove)/i, reason: 'Concurrent database operations may cause race conditions' },
13
+ { pattern: /if\s*\(\s*!?\s*(?:req|request)\.(?:user|session|auth)\s*\)\s*\{?\s*(?:return|throw|next)?[^}]*\}?\s*(?:\/\/|$)/i, reason: 'Authentication check may have bypass logic' },
14
+ { pattern: /(?:try\s*\{[^}]*(?:throw|error|reject)[^}]*\}\s*catch\s*\(\s*\w+\s*\)\s*\{[^}]*\})/i, reason: 'Error handling may silently swallow security exceptions' },
15
+ { pattern: /(?:\.then\s*\([^)]*\)\s*\.catch\s*\(\s*(?:\(\s*\)\s*=>|function\s*\(\s*\))\s*\{?\s*\}?\s*\))/i, reason: 'Empty catch handler silences errors' },
16
+ { pattern: /(?:Object\.keys|for\s*\(\s*(?:let|var|const)\s+\w+\s+(?:in|of)\s+).*(?:req\.|request\.|body|query|params)/i, reason: 'Iterating over user input keys without validation' },
17
+ { pattern: /(?:async\s+function|=>\s*\{)(?:(?!(?:try|catch|finally)).)*(?:await\s+)(?:(?!(?:try|catch|finally)).)*$/im, reason: 'Async function without error handling' },
18
+ { pattern: /(?:password|secret|token|key).*(?:===?|!==?|==).*(?:undefined|null|''|"")/i, reason: 'Null/empty check on credential may allow bypass' },
19
+ ];
20
+
21
+ export function detectSuspiciousPatterns(content: string, filePath: string, language: string): Finding[] {
22
+ const findings: Finding[] = [];
23
+ const lines = content.split('\n');
24
+
25
+ for (const { pattern, reason } of SUSPICIOUS_PATTERNS) {
26
+ for (let i = 0; i < lines.length; i++) {
27
+ if (pattern.test(lines[i])) {
28
+ findings.push({
29
+ id: `ZDAY-PATTERN-${filePath}:${i + 1}`,
30
+ ruleId: 'ZDAY-SUSPICIOUS',
31
+ title: 'Suspicious Pattern (AI Analysis Recommended)',
32
+ description: reason,
33
+ severity: Severity.LOW,
34
+ confidence: 'low',
35
+ filePath, line: i + 1,
36
+ codeSnippet: getSnippet(lines, i),
37
+ cwe: 'CWE-691',
38
+ remediation: 'Enable AI analysis (--ai) for deeper investigation of this pattern.',
39
+ fingerprint: generateFingerprint('ZDAY', filePath, lines[i].trim()),
40
+ });
41
+ }
42
+ }
43
+ }
44
+
45
+ return findings;
46
+ }
47
+
48
+ export async function analyzeWithAI(
49
+ code: string,
50
+ filePath: string,
51
+ language: string,
52
+ provider: AIProvider
53
+ ): Promise<Finding[]> {
54
+ if (!provider.isAvailable()) return [];
55
+
56
+ try {
57
+ const context = `File: ${filePath}\nLanguage: ${language}\n\nAnalyze this code for security vulnerabilities including:\n- Logic bugs that could lead to authorization bypass\n- Race conditions in concurrent operations\n- Business logic vulnerabilities\n- Novel attack vectors not caught by pattern matching\n- Time-of-check-to-time-of-use (TOCTOU) issues\n- Integer overflow/underflow\n- Null pointer dereference\n- Information leakage`;
58
+
59
+ const aiFindings = await provider.analyze(code, context);
60
+
61
+ return aiFindings.map((af, index) => ({
62
+ id: `ZDAY-AI-${filePath}:${index}`,
63
+ ruleId: 'ZDAY-AI',
64
+ title: af.title,
65
+ description: af.description,
66
+ severity: af.severity,
67
+ confidence: af.confidence,
68
+ filePath,
69
+ line: af.line || 1,
70
+ codeSnippet: code.split('\n').slice(
71
+ Math.max(0, (af.line || 1) - 3),
72
+ (af.line || 1) + 2
73
+ ).join('\n'),
74
+ cwe: af.cwe,
75
+ owasp: 'A04',
76
+ remediation: af.remediation,
77
+ aiEnhanced: true,
78
+ fingerprint: generateFingerprint('ZDAY-AI', filePath, af.title),
79
+ }));
80
+ } catch {
81
+ return [];
82
+ }
83
+ }
84
+
85
+ function getSnippet(lines: string[], index: number, context = 2): string {
86
+ const start = Math.max(0, index - context);
87
+ const end = Math.min(lines.length, index + context + 1);
88
+ return lines.slice(start, end).map((l, i) => {
89
+ const lineNum = start + i + 1;
90
+ const marker = (start + i === index) ? '>' : ' ';
91
+ return `${marker} ${lineNum} | ${l}`;
92
+ }).join('\n');
93
+ }
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ // GhostPatch — AI-Powered Security Vulnerability Scanner
2
+ // Library API
3
+
4
+ export { scan, scanFile, scanWithAI } from './core/scanner';
5
+ export { generateReport, reportJSON, reportSARIF, reportHTML, reportTerminal } from './core/reporter';
6
+ export {
7
+ Finding,
8
+ AIFinding,
9
+ ScanResult,
10
+ ScanSummary,
11
+ ScanOptions,
12
+ Rule,
13
+ GhostPatchConfig,
14
+ Severity,
15
+ SEVERITY_ORDER,
16
+ meetsMinSeverity,
17
+ severityFromString,
18
+ } from './core/severity';
19
+ export { ALL_RULES, getRulesForLanguage, getRuleById, getRulesByOwasp, getRulesBySeverity, getEnabledRules } from './core/rules';
20
+ export { getAvailableProvider, AIProvider } from './ai/provider';
21
+ export { loadConfig, getDefaultConfig } from './utils/config';
22
+ export { detectLanguage, isSupportedFile, SUPPORTED_LANGUAGES } from './utils/languages';
23
+ export { generateFingerprint, deduplicateFindings } from './utils/fingerprint';
24
+ export { startMCPServer } from './mcp/server';