preflight-scavenger 0.2.0-beta.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 -0
- package/index.js +2320 -0
- package/logger.js +64 -0
- package/mcp-instructions.md +65 -0
- package/package.json +81 -0
- package/remediationEngine.js +243 -0
- package/scaffoldEngine.js +420 -0
- package/src/licensing/licenseManager.js +248 -0
- package/src/mcp/server.js +172 -0
- package/taintTracker.js +393 -0
- package/wasm/tree-sitter-javascript.wasm +0 -0
- package/wasm/tree-sitter-tsx.wasm +0 -0
- package/wasm/tree-sitter-typescript.wasm +0 -0
package/logger.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const chalk = require("chalk");
|
|
2
|
+
|
|
3
|
+
function shouldUseColor(options = {}) {
|
|
4
|
+
if (options.noColor === true || options.color === false) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const stream = options.stream || process.stdout;
|
|
9
|
+
return Boolean(stream && stream.isTTY);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getChalk(options = {}) {
|
|
13
|
+
return shouldUseColor(options) ? new chalk.Instance({ level: 1 }) : null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function colorize(level, message, options = {}) {
|
|
17
|
+
const c = getChalk(options);
|
|
18
|
+
if (!c) {
|
|
19
|
+
return message;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const normalizedLevel = String(level || "").toLowerCase();
|
|
23
|
+
if (normalizedLevel === "critical" || normalizedLevel === "error" || normalizedLevel === "high") {
|
|
24
|
+
return c.red.bold(message);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (normalizedLevel === "warning" || normalizedLevel === "warn" || normalizedLevel === "moderate" || normalizedLevel === "low") {
|
|
28
|
+
return c.yellow(message);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (normalizedLevel === "success") {
|
|
32
|
+
return c.green.bold(message);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return message;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createLogger(options = {}) {
|
|
39
|
+
const stdout = options.stdout || process.stdout;
|
|
40
|
+
const stderr = options.stderr || process.stderr;
|
|
41
|
+
const color = options.color;
|
|
42
|
+
const noColor = options.noColor;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
error(message) {
|
|
46
|
+
stderr.write(`${colorize("error", message, { color, noColor, stream: stderr })}\n`);
|
|
47
|
+
},
|
|
48
|
+
log(message) {
|
|
49
|
+
stdout.write(`${message}\n`);
|
|
50
|
+
},
|
|
51
|
+
success(message) {
|
|
52
|
+
stdout.write(`${colorize("success", message, { color, noColor, stream: stdout })}\n`);
|
|
53
|
+
},
|
|
54
|
+
warn(message) {
|
|
55
|
+
stderr.write(`${colorize("warning", message, { color, noColor, stream: stderr })}\n`);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
colorize,
|
|
62
|
+
createLogger,
|
|
63
|
+
shouldUseColor
|
|
64
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# PreFlight MCP Integration
|
|
2
|
+
|
|
3
|
+
Use this guide to connect `mcp-server.js` to Claude Desktop or Cursor while keeping PreFlight local.
|
|
4
|
+
|
|
5
|
+
## 1. Install PreFlight dependencies
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 2. Confirm the MCP server path
|
|
12
|
+
|
|
13
|
+
Use the absolute path to your local server file:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
C:\ABSOLUTE\PATH\TO\PreFlight\mcp-server.js
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## 3. Configure Claude Desktop
|
|
20
|
+
|
|
21
|
+
Open `claude_desktop_config.json` and add this server entry:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"preflight": {
|
|
27
|
+
"command": "node",
|
|
28
|
+
"args": ["C:\\ABSOLUTE\\PATH\\TO\\PreFlight\\mcp-server.js"],
|
|
29
|
+
"env": {
|
|
30
|
+
"OPENAI_API_KEY": "YOUR_OPENAI_API_KEY"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Restart Claude Desktop after saving the file.
|
|
38
|
+
|
|
39
|
+
## 4. Configure Cursor
|
|
40
|
+
|
|
41
|
+
In Cursor, open MCP settings and add a new server using the same command, args, and env values:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"command": "node",
|
|
46
|
+
"args": ["C:\\ABSOLUTE\\PATH\\TO\\PreFlight\\mcp-server.js"],
|
|
47
|
+
"env": {
|
|
48
|
+
"OPENAI_API_KEY": "YOUR_OPENAI_API_KEY"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 5. Verify locally
|
|
54
|
+
|
|
55
|
+
Run a scan from your project before relying on the MCP workflow:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
preflight scan .
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
For LLM SQL remediation, pass the OpenAI key through the MCP `env` block above, a local `.env` file, or the CLI flag:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
preflight scan . --fix --openai-key=YOUR_OPENAI_API_KEY
|
|
65
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "preflight-scavenger",
|
|
3
|
+
"version": "0.2.0-beta.0",
|
|
4
|
+
"description": "The local security gate for AI-generated code.",
|
|
5
|
+
"license": "BUSL-1.1",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"homepage": "https://github.com/av29nassh-sketch/PreFlight#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/av29nassh-sketch/PreFlight.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/av29nassh-sketch/PreFlight/issues"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public",
|
|
17
|
+
"tag": "beta",
|
|
18
|
+
"registry": "https://registry.npmjs.org/"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"preflight",
|
|
22
|
+
"security",
|
|
23
|
+
"ai-code",
|
|
24
|
+
"nextjs",
|
|
25
|
+
"supabase",
|
|
26
|
+
"secret-scanner",
|
|
27
|
+
"mcp",
|
|
28
|
+
"cli"
|
|
29
|
+
],
|
|
30
|
+
"files": [
|
|
31
|
+
"index.js",
|
|
32
|
+
"logger.js",
|
|
33
|
+
"remediationEngine.js",
|
|
34
|
+
"scaffoldEngine.js",
|
|
35
|
+
"taintTracker.js",
|
|
36
|
+
"src/**/*.js",
|
|
37
|
+
"wasm/*.wasm",
|
|
38
|
+
"mcp-instructions.md",
|
|
39
|
+
"README.md"
|
|
40
|
+
],
|
|
41
|
+
"bin": {
|
|
42
|
+
"preflight": "index.js",
|
|
43
|
+
"scavenger": "index.js"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"test": "vitest run --globals",
|
|
47
|
+
"scan": "node index.js",
|
|
48
|
+
"build:bin": "pkg . --out-path dist",
|
|
49
|
+
"build:bin:mac": "pkg . --targets node18-macos-x64,node18-macos-arm64 --no-bytecode --public --public-packages \"*\" --out-path dist",
|
|
50
|
+
"build:bin:win": "pkg . --targets node18-win-x64 --out-path dist"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
54
|
+
"chalk": "^4.1.2",
|
|
55
|
+
"commander": "^12.1.0",
|
|
56
|
+
"dotenv": "^17.4.2",
|
|
57
|
+
"fast-glob": "^3.3.3",
|
|
58
|
+
"openai": "^6.41.0",
|
|
59
|
+
"pgsql-ast-parser": "^12.0.1",
|
|
60
|
+
"picocolors": "^1.1.1",
|
|
61
|
+
"tree-sitter-javascript": "^0.25.0",
|
|
62
|
+
"tree-sitter-typescript": "^0.23.2",
|
|
63
|
+
"web-tree-sitter": "^0.26.9",
|
|
64
|
+
"zod": "^4.4.3"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"pkg": "^5.8.1",
|
|
68
|
+
"vitest": "^4.0.15"
|
|
69
|
+
},
|
|
70
|
+
"engines": {
|
|
71
|
+
"node": ">=18.0.0"
|
|
72
|
+
},
|
|
73
|
+
"pkg": {
|
|
74
|
+
"targets": [
|
|
75
|
+
"node18-macos-x64",
|
|
76
|
+
"node18-macos-arm64",
|
|
77
|
+
"node18-win-x64"
|
|
78
|
+
],
|
|
79
|
+
"outputPath": "dist"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
const path = require("node:path");
|
|
2
|
+
const ParserBinding = require("web-tree-sitter");
|
|
3
|
+
const OpenAIImport = require("openai");
|
|
4
|
+
|
|
5
|
+
const Parser = ParserBinding.Parser || ParserBinding.default?.Parser || ParserBinding.default || ParserBinding;
|
|
6
|
+
const Language = ParserBinding.Language || ParserBinding.default?.Language;
|
|
7
|
+
const OpenAI = OpenAIImport.default || OpenAIImport;
|
|
8
|
+
|
|
9
|
+
const SQL_KEYWORD_PATTERN = /\b(?:SELECT|INSERT|UPDATE|DELETE)\b/i;
|
|
10
|
+
const SURGICAL_LLM_SYSTEM_PROMPT =
|
|
11
|
+
"You are a specialized code refactoring utility. Convert the provided insecure JavaScript/TypeScript string concatenation into a completely secure, parameterized query format using standard placeholder symbols ($1, $2, etc.). Return ONLY the executable, corrected code fragment. Do not output markdown code blocks, backticks, or text explanations.";
|
|
12
|
+
const DEFAULT_GEMINI_MODEL = "gemini-1.5-flash";
|
|
13
|
+
const DEFAULT_OPENAI_MODEL = "gpt-4o-mini";
|
|
14
|
+
const DEFAULT_OPENROUTER_MODEL = "qwen/qwen3-coder:free";
|
|
15
|
+
const GEMINI_OPENAI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/";
|
|
16
|
+
const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
|
17
|
+
const FREE_SQL_REMEDIATION_MESSAGE = [
|
|
18
|
+
"=========================================",
|
|
19
|
+
"💡 SQL Remediation is available for FREE!",
|
|
20
|
+
"=========================================",
|
|
21
|
+
"To automatically fix SQL injections, get a free API key:",
|
|
22
|
+
"1. Go to Google AI Studio (https://aistudio.google.com/)",
|
|
23
|
+
"2. Generate a free API key.",
|
|
24
|
+
"3. Add it to your IDE/Environment as: GEMINI_API_KEY",
|
|
25
|
+
"=========================================",
|
|
26
|
+
"[SKIP] Skipping LLM SQL remediation for this run."
|
|
27
|
+
].join("\n");
|
|
28
|
+
|
|
29
|
+
let parserReady;
|
|
30
|
+
let javascriptLanguage;
|
|
31
|
+
|
|
32
|
+
async function initializeParser() {
|
|
33
|
+
if (!parserReady) {
|
|
34
|
+
parserReady = Parser.init?.();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (parserReady) {
|
|
38
|
+
await parserReady;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!javascriptLanguage) {
|
|
42
|
+
const wasmPath = require.resolve("tree-sitter-javascript/tree-sitter-javascript.wasm");
|
|
43
|
+
javascriptLanguage = await Language.load(wasmPath);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function parseJavaScript(sourceCode) {
|
|
48
|
+
await initializeParser();
|
|
49
|
+
const parser = new Parser();
|
|
50
|
+
parser.setLanguage(javascriptLanguage);
|
|
51
|
+
return parser.parse(sourceCode);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getNodeText(node, sourceCode) {
|
|
55
|
+
return sourceCode.slice(node.startIndex, node.endIndex);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function toByteIndex(sourceCode, stringIndex) {
|
|
59
|
+
return Buffer.byteLength(sourceCode.slice(0, stringIndex), "utf8");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getOperator(node, sourceCode) {
|
|
63
|
+
for (let index = 0; index < node.childCount; index += 1) {
|
|
64
|
+
const child = node.child(index);
|
|
65
|
+
if (!child.isNamed && getNodeText(child, sourceCode).trim() === "+") {
|
|
66
|
+
return "+";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getFieldNode(node, fieldName) {
|
|
74
|
+
if (typeof node.childForFieldName === "function") {
|
|
75
|
+
return node.childForFieldName(fieldName);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function nodeContainsSqlKeyword(node, sourceCode) {
|
|
82
|
+
return Boolean(node && SQL_KEYWORD_PATTERN.test(getNodeText(node, sourceCode)));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function makeMatch(node, sourceCode) {
|
|
86
|
+
const rawSnippet = getNodeText(node, sourceCode);
|
|
87
|
+
const startIndex = toByteIndex(sourceCode, node.startIndex);
|
|
88
|
+
return {
|
|
89
|
+
startIndex,
|
|
90
|
+
endIndex: startIndex + Buffer.byteLength(rawSnippet, "utf8"),
|
|
91
|
+
rawSnippet
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function findSqlConcatenations(node, sourceCode, matches = []) {
|
|
96
|
+
if (!node) {
|
|
97
|
+
return matches;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (node.type === "binary_expression" && getOperator(node, sourceCode) === "+") {
|
|
101
|
+
const left = getFieldNode(node, "left");
|
|
102
|
+
const right = getFieldNode(node, "right");
|
|
103
|
+
|
|
104
|
+
if (nodeContainsSqlKeyword(left, sourceCode) || nodeContainsSqlKeyword(right, sourceCode)) {
|
|
105
|
+
matches.push(makeMatch(node, sourceCode));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (let index = 0; index < node.namedChildCount; index += 1) {
|
|
110
|
+
findSqlConcatenations(node.namedChild(index), sourceCode, matches);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return matches;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function treeContainsUnsafeNode(node) {
|
|
117
|
+
if (!node) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const isMissing = typeof node.isMissing === "function" ? node.isMissing() : node.isMissing === true;
|
|
122
|
+
if (node.type === "ERROR" || node.type === "MISSING" || isMissing) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (let index = 0; index < node.childCount; index += 1) {
|
|
127
|
+
if (treeContainsUnsafeNode(node.child(index))) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function verifySyntaxSafety(proposedFix) {
|
|
136
|
+
const tree = await parseJavaScript(proposedFix);
|
|
137
|
+
try {
|
|
138
|
+
if (treeContainsUnsafeNode(tree.rootNode)) {
|
|
139
|
+
throw new Error("Remediation Syntax Violation");
|
|
140
|
+
}
|
|
141
|
+
} finally {
|
|
142
|
+
tree.delete?.();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function resolveLlmProvider(env = process.env, options = {}) {
|
|
149
|
+
const modelOverride = options.model || env.MODEL_NAME;
|
|
150
|
+
|
|
151
|
+
if (env.GEMINI_API_KEY) {
|
|
152
|
+
return {
|
|
153
|
+
apiKey: env.GEMINI_API_KEY,
|
|
154
|
+
baseURL: GEMINI_OPENAI_BASE_URL,
|
|
155
|
+
model: modelOverride || DEFAULT_GEMINI_MODEL,
|
|
156
|
+
provider: "gemini"
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (env.OPENROUTER_API_KEY) {
|
|
161
|
+
return {
|
|
162
|
+
apiKey: env.OPENROUTER_API_KEY,
|
|
163
|
+
baseURL: OPENROUTER_BASE_URL,
|
|
164
|
+
model: modelOverride || DEFAULT_OPENROUTER_MODEL,
|
|
165
|
+
provider: "openrouter"
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (env.OPENAI_API_KEY) {
|
|
170
|
+
return {
|
|
171
|
+
apiKey: env.OPENAI_API_KEY,
|
|
172
|
+
baseURL: undefined,
|
|
173
|
+
model: modelOverride || DEFAULT_OPENAI_MODEL,
|
|
174
|
+
provider: "openai"
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function extractChatCompletionText(response) {
|
|
182
|
+
return (response.choices?.[0]?.message?.content || "").trim();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function logTokenUsage(response, log) {
|
|
186
|
+
const usage = response.usage || {};
|
|
187
|
+
const promptTokens = usage.prompt_tokens || 0;
|
|
188
|
+
const completionTokens = usage.completion_tokens || 0;
|
|
189
|
+
const totalTokens = usage.total_tokens || promptTokens + completionTokens;
|
|
190
|
+
log(`\u001b[36m[LLM] Fix completed. Tokens used: ${totalTokens} (Prompt: ${promptTokens}, Completion: ${completionTokens})\u001b[0m`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function generateParameterizedFix(rawSnippet, options = {}) {
|
|
194
|
+
const warn =
|
|
195
|
+
options.warn ||
|
|
196
|
+
((message) => {
|
|
197
|
+
console.warn(message);
|
|
198
|
+
});
|
|
199
|
+
const log =
|
|
200
|
+
options.log ||
|
|
201
|
+
((message) => {
|
|
202
|
+
console.log(message);
|
|
203
|
+
});
|
|
204
|
+
const provider = options.client
|
|
205
|
+
? { client: options.client, model: options.model || process.env.MODEL_NAME || DEFAULT_OPENAI_MODEL }
|
|
206
|
+
: resolveLlmProvider(process.env, options);
|
|
207
|
+
|
|
208
|
+
if (!provider) {
|
|
209
|
+
warn(FREE_SQL_REMEDIATION_MESSAGE);
|
|
210
|
+
return rawSnippet;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const client =
|
|
214
|
+
provider.client ||
|
|
215
|
+
new OpenAI({
|
|
216
|
+
apiKey: provider.apiKey,
|
|
217
|
+
...(provider.baseURL ? { baseURL: provider.baseURL } : {})
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const response = await client.chat.completions.create({
|
|
221
|
+
model: provider.model,
|
|
222
|
+
messages: [
|
|
223
|
+
{ role: "system", content: SURGICAL_LLM_SYSTEM_PROMPT },
|
|
224
|
+
{ role: "user", content: rawSnippet }
|
|
225
|
+
],
|
|
226
|
+
temperature: 0
|
|
227
|
+
});
|
|
228
|
+
logTokenUsage(response, log);
|
|
229
|
+
const proposedFix = extractChatCompletionText(response);
|
|
230
|
+
|
|
231
|
+
await verifySyntaxSafety(proposedFix);
|
|
232
|
+
return proposedFix;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
FREE_SQL_REMEDIATION_MESSAGE,
|
|
237
|
+
findSqlConcatenations,
|
|
238
|
+
generateParameterizedFix,
|
|
239
|
+
parseJavaScript,
|
|
240
|
+
resolveLlmProvider,
|
|
241
|
+
SURGICAL_LLM_SYSTEM_PROMPT,
|
|
242
|
+
verifySyntaxSafety
|
|
243
|
+
};
|