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.
- package/LICENSE +21 -0
- package/README.md +213 -0
- package/__tests__/detectors.test.ts +224 -0
- package/__tests__/rules.test.ts +117 -0
- package/__tests__/scanner.test.ts +222 -0
- package/dist/ai/anthropic.d.ts +11 -0
- package/dist/ai/anthropic.d.ts.map +1 -0
- package/dist/ai/anthropic.js +76 -0
- package/dist/ai/anthropic.js.map +1 -0
- package/dist/ai/huggingface.d.ts +12 -0
- package/dist/ai/huggingface.d.ts.map +1 -0
- package/dist/ai/huggingface.js +95 -0
- package/dist/ai/huggingface.js.map +1 -0
- package/dist/ai/openai.d.ts +11 -0
- package/dist/ai/openai.d.ts.map +1 -0
- package/dist/ai/openai.js +71 -0
- package/dist/ai/openai.js.map +1 -0
- package/dist/ai/prompts.d.ts +5 -0
- package/dist/ai/prompts.d.ts.map +1 -0
- package/dist/ai/prompts.js +101 -0
- package/dist/ai/prompts.js.map +1 -0
- package/dist/ai/provider.d.ts +9 -0
- package/dist/ai/provider.d.ts.map +1 -0
- package/dist/ai/provider.js +66 -0
- package/dist/ai/provider.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +318 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/reporter.d.ts +7 -0
- package/dist/core/reporter.d.ts.map +1 -0
- package/dist/core/reporter.js +366 -0
- package/dist/core/reporter.js.map +1 -0
- package/dist/core/rules.d.ts +8 -0
- package/dist/core/rules.d.ts.map +1 -0
- package/dist/core/rules.js +1077 -0
- package/dist/core/rules.js.map +1 -0
- package/dist/core/scanner.d.ts +6 -0
- package/dist/core/scanner.d.ts.map +1 -0
- package/dist/core/scanner.js +217 -0
- package/dist/core/scanner.js.map +1 -0
- package/dist/core/severity.d.ts +100 -0
- package/dist/core/severity.d.ts.map +1 -0
- package/dist/core/severity.js +52 -0
- package/dist/core/severity.js.map +1 -0
- package/dist/detectors/auth.d.ts +3 -0
- package/dist/detectors/auth.d.ts.map +1 -0
- package/dist/detectors/auth.js +138 -0
- package/dist/detectors/auth.js.map +1 -0
- package/dist/detectors/crypto.d.ts +3 -0
- package/dist/detectors/crypto.d.ts.map +1 -0
- package/dist/detectors/crypto.js +128 -0
- package/dist/detectors/crypto.js.map +1 -0
- package/dist/detectors/dependency.d.ts +4 -0
- package/dist/detectors/dependency.d.ts.map +1 -0
- package/dist/detectors/dependency.js +267 -0
- package/dist/detectors/dependency.js.map +1 -0
- package/dist/detectors/deserialize.d.ts +3 -0
- package/dist/detectors/deserialize.d.ts.map +1 -0
- package/dist/detectors/deserialize.js +107 -0
- package/dist/detectors/deserialize.js.map +1 -0
- package/dist/detectors/injection.d.ts +3 -0
- package/dist/detectors/injection.d.ts.map +1 -0
- package/dist/detectors/injection.js +158 -0
- package/dist/detectors/injection.js.map +1 -0
- package/dist/detectors/misconfig.d.ts +3 -0
- package/dist/detectors/misconfig.d.ts.map +1 -0
- package/dist/detectors/misconfig.js +153 -0
- package/dist/detectors/misconfig.js.map +1 -0
- package/dist/detectors/pathtraversal.d.ts +3 -0
- package/dist/detectors/pathtraversal.d.ts.map +1 -0
- package/dist/detectors/pathtraversal.js +90 -0
- package/dist/detectors/pathtraversal.js.map +1 -0
- package/dist/detectors/prototype.d.ts +3 -0
- package/dist/detectors/prototype.d.ts.map +1 -0
- package/dist/detectors/prototype.js +79 -0
- package/dist/detectors/prototype.js.map +1 -0
- package/dist/detectors/secrets.d.ts +4 -0
- package/dist/detectors/secrets.d.ts.map +1 -0
- package/dist/detectors/secrets.js +137 -0
- package/dist/detectors/secrets.js.map +1 -0
- package/dist/detectors/ssrf.d.ts +3 -0
- package/dist/detectors/ssrf.d.ts.map +1 -0
- package/dist/detectors/ssrf.js +78 -0
- package/dist/detectors/ssrf.js.map +1 -0
- package/dist/detectors/zeroday.d.ts +9 -0
- package/dist/detectors/zeroday.d.ts.map +1 -0
- package/dist/detectors/zeroday.js +77 -0
- package/dist/detectors/zeroday.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +358 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/utils/config.d.ts +4 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +97 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/fingerprint.d.ts +5 -0
- package/dist/utils/fingerprint.d.ts.map +1 -0
- package/dist/utils/fingerprint.js +55 -0
- package/dist/utils/fingerprint.js.map +1 -0
- package/dist/utils/languages.d.ts +8 -0
- package/dist/utils/languages.d.ts.map +1 -0
- package/dist/utils/languages.js +128 -0
- package/dist/utils/languages.js.map +1 -0
- package/package.json +53 -0
- package/src/ai/anthropic.ts +82 -0
- package/src/ai/huggingface.ts +111 -0
- package/src/ai/openai.ts +75 -0
- package/src/ai/prompts.ts +100 -0
- package/src/ai/provider.ts +68 -0
- package/src/cli/index.ts +314 -0
- package/src/core/reporter.ts +356 -0
- package/src/core/rules.ts +1089 -0
- package/src/core/scanner.ts +201 -0
- package/src/core/severity.ts +140 -0
- package/src/detectors/auth.ts +152 -0
- package/src/detectors/crypto.ts +128 -0
- package/src/detectors/dependency.ts +240 -0
- package/src/detectors/deserialize.ts +106 -0
- package/src/detectors/injection.ts +172 -0
- package/src/detectors/misconfig.ts +152 -0
- package/src/detectors/pathtraversal.ts +89 -0
- package/src/detectors/prototype.ts +77 -0
- package/src/detectors/secrets.ts +138 -0
- package/src/detectors/ssrf.ts +77 -0
- package/src/detectors/zeroday.ts +93 -0
- package/src/index.ts +24 -0
- package/src/mcp/server.ts +379 -0
- package/src/utils/config.ts +64 -0
- package/src/utils/fingerprint.ts +21 -0
- package/src/utils/languages.ts +95 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CONFIG_FILES = exports.DEFAULT_EXCLUDE = exports.SUPPORTED_LANGUAGES = exports.LANGUAGE_MAP = void 0;
|
|
37
|
+
exports.detectLanguage = detectLanguage;
|
|
38
|
+
exports.isSupportedFile = isSupportedFile;
|
|
39
|
+
exports.isConfigFile = isConfigFile;
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
exports.LANGUAGE_MAP = {
|
|
42
|
+
'.js': 'javascript',
|
|
43
|
+
'.jsx': 'javascript',
|
|
44
|
+
'.mjs': 'javascript',
|
|
45
|
+
'.cjs': 'javascript',
|
|
46
|
+
'.ts': 'typescript',
|
|
47
|
+
'.tsx': 'typescript',
|
|
48
|
+
'.mts': 'typescript',
|
|
49
|
+
'.cts': 'typescript',
|
|
50
|
+
'.py': 'python',
|
|
51
|
+
'.pyw': 'python',
|
|
52
|
+
'.java': 'java',
|
|
53
|
+
'.go': 'go',
|
|
54
|
+
'.rs': 'rust',
|
|
55
|
+
'.c': 'c',
|
|
56
|
+
'.h': 'c',
|
|
57
|
+
'.cpp': 'cpp',
|
|
58
|
+
'.cc': 'cpp',
|
|
59
|
+
'.cxx': 'cpp',
|
|
60
|
+
'.hpp': 'cpp',
|
|
61
|
+
'.hxx': 'cpp',
|
|
62
|
+
'.cs': 'csharp',
|
|
63
|
+
'.php': 'php',
|
|
64
|
+
'.rb': 'ruby',
|
|
65
|
+
'.swift': 'swift',
|
|
66
|
+
'.kt': 'kotlin',
|
|
67
|
+
'.kts': 'kotlin',
|
|
68
|
+
'.sh': 'shell',
|
|
69
|
+
'.bash': 'shell',
|
|
70
|
+
'.zsh': 'shell',
|
|
71
|
+
'.sql': 'sql',
|
|
72
|
+
'.html': 'html',
|
|
73
|
+
'.htm': 'html',
|
|
74
|
+
'.vue': 'javascript',
|
|
75
|
+
'.svelte': 'javascript',
|
|
76
|
+
};
|
|
77
|
+
exports.SUPPORTED_LANGUAGES = [
|
|
78
|
+
'typescript', 'javascript', 'python', 'java', 'go', 'rust',
|
|
79
|
+
'c', 'cpp', 'csharp', 'php', 'ruby', 'swift', 'kotlin',
|
|
80
|
+
'shell', 'sql',
|
|
81
|
+
];
|
|
82
|
+
function detectLanguage(filePath) {
|
|
83
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
84
|
+
return exports.LANGUAGE_MAP[ext] || null;
|
|
85
|
+
}
|
|
86
|
+
function isSupportedFile(filePath) {
|
|
87
|
+
return detectLanguage(filePath) !== null;
|
|
88
|
+
}
|
|
89
|
+
exports.DEFAULT_EXCLUDE = [
|
|
90
|
+
'node_modules/**',
|
|
91
|
+
'dist/**',
|
|
92
|
+
'build/**',
|
|
93
|
+
'out/**',
|
|
94
|
+
'.git/**',
|
|
95
|
+
'.next/**',
|
|
96
|
+
'.nuxt/**',
|
|
97
|
+
'vendor/**',
|
|
98
|
+
'__pycache__/**',
|
|
99
|
+
'*.min.js',
|
|
100
|
+
'*.min.css',
|
|
101
|
+
'*.map',
|
|
102
|
+
'*.lock',
|
|
103
|
+
'package-lock.json',
|
|
104
|
+
'yarn.lock',
|
|
105
|
+
'pnpm-lock.yaml',
|
|
106
|
+
'.env',
|
|
107
|
+
'.env.*',
|
|
108
|
+
'coverage/**',
|
|
109
|
+
'.nyc_output/**',
|
|
110
|
+
'*.d.ts',
|
|
111
|
+
'*.bundle.js',
|
|
112
|
+
'*.chunk.js',
|
|
113
|
+
];
|
|
114
|
+
exports.CONFIG_FILES = [
|
|
115
|
+
'package.json',
|
|
116
|
+
'requirements.txt',
|
|
117
|
+
'Pipfile',
|
|
118
|
+
'pom.xml',
|
|
119
|
+
'go.mod',
|
|
120
|
+
'Gemfile',
|
|
121
|
+
'Cargo.toml',
|
|
122
|
+
'composer.json',
|
|
123
|
+
];
|
|
124
|
+
function isConfigFile(filePath) {
|
|
125
|
+
const basename = path.basename(filePath);
|
|
126
|
+
return exports.CONFIG_FILES.includes(basename);
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=languages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"languages.js","sourceRoot":"","sources":["../../src/utils/languages.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CA,wCAGC;AAED,0CAEC;AAuCD,oCAGC;AA9FD,2CAA6B;AAEhB,QAAA,YAAY,GAA2B;IAClD,KAAK,EAAE,YAAY;IACnB,MAAM,EAAE,YAAY;IACpB,MAAM,EAAE,YAAY;IACpB,MAAM,EAAE,YAAY;IACpB,KAAK,EAAE,YAAY;IACnB,MAAM,EAAE,YAAY;IACpB,MAAM,EAAE,YAAY;IACpB,MAAM,EAAE,YAAY;IACpB,KAAK,EAAE,QAAQ;IACf,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,MAAM;IACf,KAAK,EAAE,IAAI;IACX,KAAK,EAAE,MAAM;IACb,IAAI,EAAE,GAAG;IACT,IAAI,EAAE,GAAG;IACT,MAAM,EAAE,KAAK;IACb,KAAK,EAAE,KAAK;IACZ,MAAM,EAAE,KAAK;IACb,MAAM,EAAE,KAAK;IACb,MAAM,EAAE,KAAK;IACb,KAAK,EAAE,QAAQ;IACf,MAAM,EAAE,KAAK;IACb,KAAK,EAAE,MAAM;IACb,QAAQ,EAAE,OAAO;IACjB,KAAK,EAAE,QAAQ;IACf,MAAM,EAAE,QAAQ;IAChB,KAAK,EAAE,OAAO;IACd,OAAO,EAAE,OAAO;IAChB,MAAM,EAAE,OAAO;IACf,MAAM,EAAE,KAAK;IACb,OAAO,EAAE,MAAM;IACf,MAAM,EAAE,MAAM;IACd,MAAM,EAAE,YAAY;IACpB,SAAS,EAAE,YAAY;CACxB,CAAC;AAEW,QAAA,mBAAmB,GAAG;IACjC,YAAY,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;IAC1D,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ;IACtD,OAAO,EAAE,KAAK;CACf,CAAC;AAEF,SAAgB,cAAc,CAAC,QAAgB;IAC7C,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;IACjD,OAAO,oBAAY,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC;AACnC,CAAC;AAED,SAAgB,eAAe,CAAC,QAAgB;IAC9C,OAAO,cAAc,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC;AAC3C,CAAC;AAEY,QAAA,eAAe,GAAG;IAC7B,iBAAiB;IACjB,SAAS;IACT,UAAU;IACV,QAAQ;IACR,SAAS;IACT,UAAU;IACV,UAAU;IACV,WAAW;IACX,gBAAgB;IAChB,UAAU;IACV,WAAW;IACX,OAAO;IACP,QAAQ;IACR,mBAAmB;IACnB,WAAW;IACX,gBAAgB;IAChB,MAAM;IACN,QAAQ;IACR,aAAa;IACb,gBAAgB;IAChB,QAAQ;IACR,aAAa;IACb,YAAY;CACb,CAAC;AAEW,QAAA,YAAY,GAAG;IAC1B,cAAc;IACd,kBAAkB;IAClB,SAAS;IACT,SAAS;IACT,QAAQ;IACR,SAAS;IACT,YAAY;IACZ,eAAe;CAChB,CAAC;AAEF,SAAgB,YAAY,CAAC,QAAgB;IAC3C,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACzC,OAAO,oBAAY,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AACzC,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ghostpatch",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI-powered security vulnerability scanner — like SonarQube but runs locally via npm with zero infrastructure. Uses free HuggingFace models by default.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ghostpatch": "dist/cli/index.js",
|
|
9
|
+
"gp": "dist/cli/index.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc",
|
|
13
|
+
"dev": "tsc --watch",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"start": "node dist/cli/index.js",
|
|
17
|
+
"scan": "node dist/cli/index.js scan",
|
|
18
|
+
"serve": "node dist/cli/index.js serve",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"security",
|
|
23
|
+
"scanner",
|
|
24
|
+
"vulnerability",
|
|
25
|
+
"sast",
|
|
26
|
+
"ai",
|
|
27
|
+
"sonarqube",
|
|
28
|
+
"owasp",
|
|
29
|
+
"mcp",
|
|
30
|
+
"static-analysis"
|
|
31
|
+
],
|
|
32
|
+
"author": "",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18.0.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"commander": "^14.0.3",
|
|
39
|
+
"glob": "^13.0.1",
|
|
40
|
+
"micromatch": "^4.0.8",
|
|
41
|
+
"chokidar": "^4.0.3"
|
|
42
|
+
},
|
|
43
|
+
"optionalDependencies": {
|
|
44
|
+
"@anthropic-ai/sdk": "^0.39.0",
|
|
45
|
+
"openai": "^4.80.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/micromatch": "^4.0.9",
|
|
49
|
+
"@types/node": "^22.12.0",
|
|
50
|
+
"typescript": "^5.7.3",
|
|
51
|
+
"vitest": "^3.2.4"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { AIFinding } from '../core/severity';
|
|
2
|
+
import { AIProvider, parseAIResponse } from './provider';
|
|
3
|
+
import { buildAnalysisPrompt } from './prompts';
|
|
4
|
+
|
|
5
|
+
export class AnthropicProvider implements AIProvider {
|
|
6
|
+
name = 'anthropic';
|
|
7
|
+
private apiKey: string | undefined;
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
this.apiKey = process.env.ANTHROPIC_API_KEY;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
isAvailable(): boolean {
|
|
14
|
+
return !!this.apiKey;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async analyze(code: string, context: string): Promise<AIFinding[]> {
|
|
18
|
+
if (!this.apiKey) return [];
|
|
19
|
+
|
|
20
|
+
const prompt = buildAnalysisPrompt(code, context, 'security');
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
// Try to use the SDK if available
|
|
24
|
+
const Anthropic = require('@anthropic-ai/sdk');
|
|
25
|
+
const client = new Anthropic({ apiKey: this.apiKey });
|
|
26
|
+
|
|
27
|
+
const response = await client.messages.create({
|
|
28
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
29
|
+
max_tokens: 4096,
|
|
30
|
+
messages: [{
|
|
31
|
+
role: 'user',
|
|
32
|
+
content: prompt,
|
|
33
|
+
}],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const text = response.content
|
|
37
|
+
.filter((c: any) => c.type === 'text')
|
|
38
|
+
.map((c: any) => c.text)
|
|
39
|
+
.join('');
|
|
40
|
+
|
|
41
|
+
return parseAIResponse(text);
|
|
42
|
+
} catch (sdkError) {
|
|
43
|
+
// Fallback to direct API call
|
|
44
|
+
try {
|
|
45
|
+
return await this.callDirectAPI(prompt);
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private async callDirectAPI(prompt: string): Promise<AIFinding[]> {
|
|
53
|
+
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
'x-api-key': this.apiKey!,
|
|
58
|
+
'anthropic-version': '2023-06-01',
|
|
59
|
+
},
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
model: 'claude-sonnet-4-5-20250929',
|
|
62
|
+
max_tokens: 4096,
|
|
63
|
+
messages: [{
|
|
64
|
+
role: 'user',
|
|
65
|
+
content: prompt,
|
|
66
|
+
}],
|
|
67
|
+
}),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
throw new Error(`Anthropic API error: ${response.status}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const data: any = await response.json();
|
|
75
|
+
const text = data.content
|
|
76
|
+
?.filter((c: any) => c.type === 'text')
|
|
77
|
+
?.map((c: any) => c.text)
|
|
78
|
+
?.join('') || '';
|
|
79
|
+
|
|
80
|
+
return parseAIResponse(text);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { AIFinding } from '../core/severity';
|
|
2
|
+
import { AIProvider, parseAIResponse } from './provider';
|
|
3
|
+
import { buildAnalysisPrompt } from './prompts';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_MODEL = 'Qwen/Qwen2.5-Coder-32B-Instruct';
|
|
6
|
+
const FALLBACK_MODEL = 'bigcode/starcoder2-15b';
|
|
7
|
+
const HF_API_URL = 'https://api-inference.huggingface.co/models';
|
|
8
|
+
|
|
9
|
+
export class HuggingFaceProvider implements AIProvider {
|
|
10
|
+
name = 'huggingface';
|
|
11
|
+
private token: string | undefined;
|
|
12
|
+
private model: string;
|
|
13
|
+
|
|
14
|
+
constructor(model?: string) {
|
|
15
|
+
this.token = process.env.HF_TOKEN || process.env.HUGGINGFACE_TOKEN;
|
|
16
|
+
this.model = model || DEFAULT_MODEL;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
isAvailable(): boolean {
|
|
20
|
+
return true; // HuggingFace free tier doesn't require a token for some models
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async analyze(code: string, context: string): Promise<AIFinding[]> {
|
|
24
|
+
const prompt = buildAnalysisPrompt(code, context, 'security');
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const response = await this.callAPI(this.model, prompt);
|
|
28
|
+
return parseAIResponse(response);
|
|
29
|
+
} catch {
|
|
30
|
+
try {
|
|
31
|
+
// Fallback to secondary model
|
|
32
|
+
const response = await this.callAPI(FALLBACK_MODEL, prompt);
|
|
33
|
+
return parseAIResponse(response);
|
|
34
|
+
} catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private async callAPI(model: string, prompt: string): Promise<string> {
|
|
41
|
+
const headers: Record<string, string> = {
|
|
42
|
+
'Content-Type': 'application/json',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
if (this.token) {
|
|
46
|
+
headers['Authorization'] = `Bearer ${this.token}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const body = JSON.stringify({
|
|
50
|
+
inputs: prompt,
|
|
51
|
+
parameters: {
|
|
52
|
+
max_new_tokens: 2048,
|
|
53
|
+
temperature: 0.1,
|
|
54
|
+
return_full_text: false,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const response = await fetchWithRetry(`${HF_API_URL}/${model}`, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers,
|
|
61
|
+
body,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
throw new Error(`HuggingFace API error: ${response.status}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
|
|
70
|
+
if (Array.isArray(data) && data[0]?.generated_text) {
|
|
71
|
+
return data[0].generated_text;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return JSON.stringify(data);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function fetchWithRetry(
|
|
79
|
+
url: string,
|
|
80
|
+
options: RequestInit,
|
|
81
|
+
retries: number = 3,
|
|
82
|
+
delay: number = 1000,
|
|
83
|
+
): Promise<Response> {
|
|
84
|
+
for (let i = 0; i < retries; i++) {
|
|
85
|
+
try {
|
|
86
|
+
const response = await fetch(url, options);
|
|
87
|
+
|
|
88
|
+
// If model is loading, wait and retry
|
|
89
|
+
if (response.status === 503) {
|
|
90
|
+
const body = await response.json().catch(() => ({}));
|
|
91
|
+
const estimatedTime = (body as any)?.estimated_time || 20;
|
|
92
|
+
const waitTime = Math.min(estimatedTime * 1000, 30000);
|
|
93
|
+
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Rate limited
|
|
98
|
+
if (response.status === 429) {
|
|
99
|
+
await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return response;
|
|
104
|
+
} catch (err) {
|
|
105
|
+
if (i === retries - 1) throw err;
|
|
106
|
+
await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
throw new Error('Max retries exceeded');
|
|
111
|
+
}
|
package/src/ai/openai.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { AIFinding } from '../core/severity';
|
|
2
|
+
import { AIProvider, parseAIResponse } from './provider';
|
|
3
|
+
import { buildAnalysisPrompt } from './prompts';
|
|
4
|
+
|
|
5
|
+
export class OpenAIProvider implements AIProvider {
|
|
6
|
+
name = 'openai';
|
|
7
|
+
private apiKey: string | undefined;
|
|
8
|
+
|
|
9
|
+
constructor() {
|
|
10
|
+
this.apiKey = process.env.OPENAI_API_KEY;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
isAvailable(): boolean {
|
|
14
|
+
return !!this.apiKey;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async analyze(code: string, context: string): Promise<AIFinding[]> {
|
|
18
|
+
if (!this.apiKey) return [];
|
|
19
|
+
|
|
20
|
+
const prompt = buildAnalysisPrompt(code, context, 'security');
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
// Try to use the SDK if available
|
|
24
|
+
const OpenAI = require('openai');
|
|
25
|
+
const client = new OpenAI({ apiKey: this.apiKey });
|
|
26
|
+
|
|
27
|
+
const response = await client.chat.completions.create({
|
|
28
|
+
model: 'gpt-4o',
|
|
29
|
+
messages: [{
|
|
30
|
+
role: 'user',
|
|
31
|
+
content: prompt,
|
|
32
|
+
}],
|
|
33
|
+
temperature: 0.1,
|
|
34
|
+
max_tokens: 4096,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const text = response.choices[0]?.message?.content || '';
|
|
38
|
+
return parseAIResponse(text);
|
|
39
|
+
} catch (sdkError) {
|
|
40
|
+
// Fallback to direct API call
|
|
41
|
+
try {
|
|
42
|
+
return await this.callDirectAPI(prompt);
|
|
43
|
+
} catch {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private async callDirectAPI(prompt: string): Promise<AIFinding[]> {
|
|
50
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: {
|
|
53
|
+
'Content-Type': 'application/json',
|
|
54
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
model: 'gpt-4o',
|
|
58
|
+
messages: [{
|
|
59
|
+
role: 'user',
|
|
60
|
+
content: prompt,
|
|
61
|
+
}],
|
|
62
|
+
temperature: 0.1,
|
|
63
|
+
max_tokens: 4096,
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
throw new Error(`OpenAI API error: ${response.status}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const data: any = await response.json();
|
|
72
|
+
const text = data.choices?.[0]?.message?.content || '';
|
|
73
|
+
return parseAIResponse(text);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export const SECURITY_ANALYSIS_PROMPT = `You are a senior application security engineer performing a code review. Analyze the following code for security vulnerabilities.
|
|
2
|
+
|
|
3
|
+
Look for:
|
|
4
|
+
1. **Injection flaws** — SQL injection, command injection, XSS, LDAP injection, template injection
|
|
5
|
+
2. **Authentication/Authorization bugs** — bypass conditions, privilege escalation, session issues
|
|
6
|
+
3. **Cryptographic weaknesses** — weak algorithms, hardcoded keys, insecure randomness
|
|
7
|
+
4. **Business logic vulnerabilities** — race conditions, TOCTOU, integer overflow, logic bypasses
|
|
8
|
+
5. **Data exposure** — sensitive data in logs, responses, URLs, or error messages
|
|
9
|
+
6. **Insecure configurations** — debug modes, permissive CORS, missing security headers
|
|
10
|
+
7. **Novel attack vectors** — unusual patterns that could be exploited
|
|
11
|
+
|
|
12
|
+
For each vulnerability found, respond in this exact JSON format:
|
|
13
|
+
{
|
|
14
|
+
"findings": [
|
|
15
|
+
{
|
|
16
|
+
"title": "Brief vulnerability title",
|
|
17
|
+
"description": "Detailed explanation of the vulnerability and its impact",
|
|
18
|
+
"severity": "critical|high|medium|low|info",
|
|
19
|
+
"confidence": "high|medium|low",
|
|
20
|
+
"line": <line_number_or_null>,
|
|
21
|
+
"cwe": "CWE-XXX",
|
|
22
|
+
"remediation": "How to fix the issue"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
If no vulnerabilities are found, return: { "findings": [] }
|
|
28
|
+
|
|
29
|
+
Be precise and avoid false positives. Only report genuine security concerns.`;
|
|
30
|
+
|
|
31
|
+
export const SECRETS_ANALYSIS_PROMPT = `You are a secrets detection specialist. Analyze the following code for hardcoded secrets, credentials, API keys, tokens, and sensitive configuration.
|
|
32
|
+
|
|
33
|
+
Look for:
|
|
34
|
+
- API keys (AWS, GCP, Azure, Stripe, SendGrid, Twilio, etc.)
|
|
35
|
+
- Passwords and credentials
|
|
36
|
+
- Private keys and certificates
|
|
37
|
+
- Database connection strings with credentials
|
|
38
|
+
- OAuth client secrets
|
|
39
|
+
- JWT secrets
|
|
40
|
+
- Encryption keys
|
|
41
|
+
- Webhook URLs with tokens
|
|
42
|
+
|
|
43
|
+
Respond in JSON format:
|
|
44
|
+
{
|
|
45
|
+
"findings": [
|
|
46
|
+
{
|
|
47
|
+
"title": "Type of secret found",
|
|
48
|
+
"description": "What was found and why it's a risk",
|
|
49
|
+
"severity": "critical|high|medium",
|
|
50
|
+
"confidence": "high|medium|low",
|
|
51
|
+
"line": <line_number_or_null>,
|
|
52
|
+
"cwe": "CWE-798",
|
|
53
|
+
"remediation": "How to properly handle this secret"
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}`;
|
|
57
|
+
|
|
58
|
+
export const ZERO_DAY_PROMPT = `You are an elite security researcher analyzing code for novel, zero-day class vulnerabilities that standard scanners would miss.
|
|
59
|
+
|
|
60
|
+
Focus on:
|
|
61
|
+
1. **Logic bugs** — Subtle flaws in business logic, authentication flows, or authorization checks
|
|
62
|
+
2. **Race conditions** — Time-of-check to time-of-use bugs, concurrent access issues
|
|
63
|
+
3. **Type confusion** — Unexpected type coercion leading to security bypass
|
|
64
|
+
4. **Integer issues** — Overflow, underflow, truncation leading to security impacts
|
|
65
|
+
5. **State management** — Inconsistent state that could be exploited
|
|
66
|
+
6. **Error handling** — Errors that leak info or bypass security checks
|
|
67
|
+
7. **Deserialization** — Unsafe data handling leading to code execution
|
|
68
|
+
8. **Side channels** — Timing attacks, cache-based attacks
|
|
69
|
+
|
|
70
|
+
Only report findings you have HIGH confidence in. These should be genuine, exploitable issues.
|
|
71
|
+
|
|
72
|
+
Respond in JSON format:
|
|
73
|
+
{
|
|
74
|
+
"findings": [
|
|
75
|
+
{
|
|
76
|
+
"title": "Vulnerability title",
|
|
77
|
+
"description": "Detailed technical explanation",
|
|
78
|
+
"severity": "critical|high|medium",
|
|
79
|
+
"confidence": "high|medium",
|
|
80
|
+
"line": <line_number_or_null>,
|
|
81
|
+
"cwe": "CWE-XXX",
|
|
82
|
+
"remediation": "Specific fix"
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}`;
|
|
86
|
+
|
|
87
|
+
export function buildAnalysisPrompt(code: string, context: string, promptType: 'security' | 'secrets' | 'zeroday' = 'security'): string {
|
|
88
|
+
const prompt = promptType === 'secrets' ? SECRETS_ANALYSIS_PROMPT
|
|
89
|
+
: promptType === 'zeroday' ? ZERO_DAY_PROMPT
|
|
90
|
+
: SECURITY_ANALYSIS_PROMPT;
|
|
91
|
+
|
|
92
|
+
return `${prompt}
|
|
93
|
+
|
|
94
|
+
Context: ${context}
|
|
95
|
+
|
|
96
|
+
Code to analyze:
|
|
97
|
+
\`\`\`
|
|
98
|
+
${code}
|
|
99
|
+
\`\`\``;
|
|
100
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { AIFinding, Severity, severityFromString } from '../core/severity';
|
|
2
|
+
|
|
3
|
+
export interface AIProvider {
|
|
4
|
+
name: string;
|
|
5
|
+
analyze(code: string, context: string): Promise<AIFinding[]>;
|
|
6
|
+
isAvailable(): boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getAvailableProvider(preferred?: string): AIProvider | null {
|
|
10
|
+
// Import providers lazily
|
|
11
|
+
const { HuggingFaceProvider } = require('./huggingface');
|
|
12
|
+
const { AnthropicProvider } = require('./anthropic');
|
|
13
|
+
const { OpenAIProvider } = require('./openai');
|
|
14
|
+
|
|
15
|
+
if (preferred) {
|
|
16
|
+
switch (preferred) {
|
|
17
|
+
case 'anthropic': {
|
|
18
|
+
const p = new AnthropicProvider();
|
|
19
|
+
if (p.isAvailable()) return p;
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
case 'openai': {
|
|
23
|
+
const p = new OpenAIProvider();
|
|
24
|
+
if (p.isAvailable()) return p;
|
|
25
|
+
break;
|
|
26
|
+
}
|
|
27
|
+
case 'huggingface': {
|
|
28
|
+
const p = new HuggingFaceProvider();
|
|
29
|
+
return p;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Auto-detect: prefer Anthropic > OpenAI > HuggingFace
|
|
35
|
+
const anthropic = new AnthropicProvider();
|
|
36
|
+
if (anthropic.isAvailable()) return anthropic;
|
|
37
|
+
|
|
38
|
+
const openai = new OpenAIProvider();
|
|
39
|
+
if (openai.isAvailable()) return openai;
|
|
40
|
+
|
|
41
|
+
// HuggingFace is always available (free tier)
|
|
42
|
+
return new HuggingFaceProvider();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function parseAIResponse(response: string): AIFinding[] {
|
|
46
|
+
try {
|
|
47
|
+
// Try to extract JSON from the response
|
|
48
|
+
const jsonMatch = response.match(/\{[\s\S]*"findings"[\s\S]*\}/);
|
|
49
|
+
if (!jsonMatch) return [];
|
|
50
|
+
|
|
51
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
52
|
+
if (!Array.isArray(parsed.findings)) return [];
|
|
53
|
+
|
|
54
|
+
return parsed.findings
|
|
55
|
+
.filter((f: any) => f.title && f.description)
|
|
56
|
+
.map((f: any) => ({
|
|
57
|
+
title: String(f.title),
|
|
58
|
+
description: String(f.description),
|
|
59
|
+
severity: severityFromString(f.severity || 'medium'),
|
|
60
|
+
confidence: (['high', 'medium', 'low'].includes(f.confidence) ? f.confidence : 'medium') as 'high' | 'medium' | 'low',
|
|
61
|
+
line: typeof f.line === 'number' ? f.line : undefined,
|
|
62
|
+
cwe: f.cwe ? String(f.cwe) : undefined,
|
|
63
|
+
remediation: f.remediation ? String(f.remediation) : undefined,
|
|
64
|
+
}));
|
|
65
|
+
} catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|