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
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const path = require("node:path");
|
|
2
|
+
const {
|
|
3
|
+
recordFreeFixUsage: defaultRecordFreeFixUsage,
|
|
4
|
+
verifyFixPermission: defaultVerifyFixPermission
|
|
5
|
+
} = require("../licensing/licenseManager");
|
|
6
|
+
|
|
7
|
+
async function startMcpServer(options = {}) {
|
|
8
|
+
const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
|
|
9
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
10
|
+
const { z } = await import("zod");
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
applyScanFixes,
|
|
14
|
+
auditDependencies,
|
|
15
|
+
cwd = process.cwd(),
|
|
16
|
+
loadPreflightPolicy,
|
|
17
|
+
renderAuditReport,
|
|
18
|
+
renderReport,
|
|
19
|
+
scanProject,
|
|
20
|
+
scanProjectDiff,
|
|
21
|
+
transport = new StdioServerTransport(),
|
|
22
|
+
version = "0.0.0"
|
|
23
|
+
} = options;
|
|
24
|
+
|
|
25
|
+
if (!applyScanFixes || !auditDependencies || !loadPreflightPolicy || !renderAuditReport || !renderReport || !scanProject || !scanProjectDiff) {
|
|
26
|
+
throw new Error("PreFlight MCP server requires scanner dependencies.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const server = new McpServer({
|
|
30
|
+
name: "preflight-pro",
|
|
31
|
+
version
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
registerMcpTools(server, {
|
|
35
|
+
applyScanFixes,
|
|
36
|
+
auditDependencies,
|
|
37
|
+
cwd,
|
|
38
|
+
loadPreflightPolicy,
|
|
39
|
+
renderAuditReport,
|
|
40
|
+
renderReport,
|
|
41
|
+
scanProject,
|
|
42
|
+
scanProjectDiff,
|
|
43
|
+
z
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await server.connect(transport);
|
|
47
|
+
return server;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function makeScanProjectSchema(z) {
|
|
51
|
+
return z
|
|
52
|
+
? z.object({
|
|
53
|
+
directory: z.string().optional().describe("Project directory to scan. Defaults to the current working directory."),
|
|
54
|
+
diff: z.boolean().optional().describe("Scan only changed Git files."),
|
|
55
|
+
format: z.enum(["text", "json"]).optional().describe("Response format.")
|
|
56
|
+
})
|
|
57
|
+
: undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function makeAuditDependenciesSchema(z) {
|
|
61
|
+
return z
|
|
62
|
+
? z.object({
|
|
63
|
+
directory: z.string().optional().describe("Project directory to audit. Defaults to the current working directory."),
|
|
64
|
+
format: z.enum(["text", "json"]).optional().describe("Response format.")
|
|
65
|
+
})
|
|
66
|
+
: undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function makePreflightFixSchema(z) {
|
|
70
|
+
return z
|
|
71
|
+
? z.object({
|
|
72
|
+
directory: z.string().optional().describe("Project directory to fix. Defaults to the current working directory."),
|
|
73
|
+
diff: z.boolean().optional().describe("Fix only changed Git files.")
|
|
74
|
+
})
|
|
75
|
+
: undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function registerMcpTools(server, options = {}) {
|
|
79
|
+
const {
|
|
80
|
+
applyScanFixes,
|
|
81
|
+
auditDependencies,
|
|
82
|
+
cwd = process.cwd(),
|
|
83
|
+
loadPreflightPolicy,
|
|
84
|
+
recordFreeFixUsage = defaultRecordFreeFixUsage,
|
|
85
|
+
renderAuditReport,
|
|
86
|
+
renderReport,
|
|
87
|
+
scanProject,
|
|
88
|
+
scanProjectDiff,
|
|
89
|
+
verifyFixPermission = defaultVerifyFixPermission,
|
|
90
|
+
z
|
|
91
|
+
} = options;
|
|
92
|
+
|
|
93
|
+
server.registerTool(
|
|
94
|
+
"scan_project",
|
|
95
|
+
{
|
|
96
|
+
title: "Scan Project",
|
|
97
|
+
description: "Run local AST parsing and SQL injection detection only. This tool never runs npm audit or network dependency checks.",
|
|
98
|
+
inputSchema: makeScanProjectSchema(z)
|
|
99
|
+
},
|
|
100
|
+
async ({ directory, diff = false, format = "text" }) => {
|
|
101
|
+
const rootDir = path.resolve(cwd, directory || ".");
|
|
102
|
+
const policy = await loadPreflightPolicy(cwd);
|
|
103
|
+
const findings = diff ? await scanProjectDiff(rootDir, { policy }) : await scanProject(rootDir, { policy });
|
|
104
|
+
const text = format === "json" ? JSON.stringify(findings, null, 2) : renderReport(findings, { color: false });
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
content: [{ type: "text", text }]
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
server.registerTool(
|
|
113
|
+
"preflight_fix",
|
|
114
|
+
{
|
|
115
|
+
title: "PreFlight Fix",
|
|
116
|
+
description: "Apply supported local PreFlight fixes. This tool is freemium-gated; scan_project remains free.",
|
|
117
|
+
inputSchema: makePreflightFixSchema(z)
|
|
118
|
+
},
|
|
119
|
+
async ({ directory, diff = false }) => {
|
|
120
|
+
const permission = await verifyFixPermission();
|
|
121
|
+
if (!permission.allowed) {
|
|
122
|
+
return {
|
|
123
|
+
isError: true,
|
|
124
|
+
content: [{ type: "text", text: permission.message }]
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const rootDir = path.resolve(cwd, directory || ".");
|
|
129
|
+
const policy = await loadPreflightPolicy(cwd);
|
|
130
|
+
const findings = diff ? await scanProjectDiff(rootDir, { policy }) : await scanProject(rootDir, { policy });
|
|
131
|
+
const fixResult = await applyScanFixes(findings, { ask: async () => "y" });
|
|
132
|
+
|
|
133
|
+
if (permission.tier === "free") {
|
|
134
|
+
await recordFreeFixUsage();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
content: [
|
|
139
|
+
{
|
|
140
|
+
type: "text",
|
|
141
|
+
text:
|
|
142
|
+
`PreFlight remediation attempted ${fixResult?.attempted || 0} fix(es): ` +
|
|
143
|
+
`${fixResult?.applied || 0} applied, ${fixResult?.skipped || 0} skipped, ${fixResult?.unsupported || 0} unsupported.\n`
|
|
144
|
+
}
|
|
145
|
+
]
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
server.registerTool(
|
|
151
|
+
"audit_dependencies",
|
|
152
|
+
{
|
|
153
|
+
title: "Audit Dependencies",
|
|
154
|
+
description: "Explicitly run dependency auditing through npm audit. This is separate from scan_project.",
|
|
155
|
+
inputSchema: makeAuditDependenciesSchema(z)
|
|
156
|
+
},
|
|
157
|
+
async ({ directory, format = "text" }) => {
|
|
158
|
+
const rootDir = path.resolve(cwd, directory || ".");
|
|
159
|
+
const result = await auditDependencies(rootDir);
|
|
160
|
+
const text = format === "json" ? JSON.stringify(result, null, 2) : renderAuditReport(result, { color: false });
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
content: [{ type: "text", text }]
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
registerMcpTools,
|
|
171
|
+
startMcpServer
|
|
172
|
+
};
|
package/taintTracker.js
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
const fs = require("node:fs");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const ParserBinding = require("web-tree-sitter");
|
|
4
|
+
|
|
5
|
+
const Parser = ParserBinding.Parser || ParserBinding.default?.Parser || ParserBinding.default || ParserBinding;
|
|
6
|
+
const Language = ParserBinding.Language || ParserBinding.default?.Language;
|
|
7
|
+
const TAINT_NAME_PATTERN = /(?:SECRET|KEY|TOKEN|URI)/i;
|
|
8
|
+
const SOURCE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"];
|
|
9
|
+
|
|
10
|
+
let parserReady;
|
|
11
|
+
let javascriptLanguage;
|
|
12
|
+
|
|
13
|
+
async function initializeParser() {
|
|
14
|
+
if (!parserReady) {
|
|
15
|
+
parserReady = Parser.init?.();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (parserReady) {
|
|
19
|
+
await parserReady;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!javascriptLanguage) {
|
|
23
|
+
const wasmPath = require.resolve("tree-sitter-javascript/tree-sitter-javascript.wasm");
|
|
24
|
+
javascriptLanguage = await Language.load(wasmPath);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function parseJavaScript(sourceCode) {
|
|
29
|
+
await initializeParser();
|
|
30
|
+
const parser = new Parser();
|
|
31
|
+
parser.setLanguage(javascriptLanguage);
|
|
32
|
+
return parser.parse(sourceCode);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getNodeText(node, sourceCode) {
|
|
36
|
+
return sourceCode.slice(node.startIndex, node.endIndex);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function unquote(value) {
|
|
40
|
+
return value.trim().replace(/;$/, "").trim().replace(/^['"`]|['"`]$/g, "");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function childByFieldName(node, fieldName) {
|
|
44
|
+
return typeof node.childForFieldName === "function" ? node.childForFieldName(fieldName) : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function walk(node, visitor) {
|
|
48
|
+
if (!node) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
visitor(node);
|
|
53
|
+
for (let index = 0; index < node.childCount; index += 1) {
|
|
54
|
+
walk(node.child(index), visitor);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isClientComponent(rootNode, sourceCode) {
|
|
59
|
+
for (let index = 0; index < rootNode.namedChildCount; index += 1) {
|
|
60
|
+
const child = rootNode.namedChild(index);
|
|
61
|
+
if (child.type !== "expression_statement") {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (unquote(getNodeText(child, sourceCode)) === "use client") {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getDeclaratorName(node, sourceCode) {
|
|
74
|
+
const nameNode = childByFieldName(node, "name");
|
|
75
|
+
return nameNode ? getNodeText(nameNode, sourceCode) : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getDeclaratorValue(node) {
|
|
79
|
+
return childByFieldName(node, "value");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function matchesAnyCredentialRegex(value, credentialRegexes = []) {
|
|
83
|
+
return credentialRegexes.some((regex) => {
|
|
84
|
+
if (!(regex instanceof RegExp)) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
regex.lastIndex = 0;
|
|
89
|
+
return regex.test(value);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function findTaintSources(rootNode, sourceCode, credentialRegexes = []) {
|
|
94
|
+
const taintedSources = new Set();
|
|
95
|
+
|
|
96
|
+
walk(rootNode, (node) => {
|
|
97
|
+
if (node.type !== "variable_declarator") {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const variableName = getDeclaratorName(node, sourceCode);
|
|
102
|
+
if (!variableName) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const valueNode = getDeclaratorValue(node);
|
|
107
|
+
const value = valueNode ? getNodeText(valueNode, sourceCode) : "";
|
|
108
|
+
if (TAINT_NAME_PATTERN.test(variableName) || matchesAnyCredentialRegex(value, credentialRegexes)) {
|
|
109
|
+
taintedSources.add(variableName);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return taintedSources;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function collectIdentifiers(node, sourceCode, names = new Set()) {
|
|
117
|
+
if (!node) {
|
|
118
|
+
return names;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (node.type === "identifier") {
|
|
122
|
+
names.add(getNodeText(node, sourceCode));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (let index = 0; index < node.namedChildCount; index += 1) {
|
|
126
|
+
collectIdentifiers(node.namedChild(index), sourceCode, names);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return names;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getImportSource(node, sourceCode) {
|
|
133
|
+
for (let index = 0; index < node.namedChildCount; index += 1) {
|
|
134
|
+
const child = node.namedChild(index);
|
|
135
|
+
if (child.type === "string") {
|
|
136
|
+
return unquote(getNodeText(child, sourceCode));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function collectImportClause(node, sourceCode, source, imports) {
|
|
144
|
+
if (!node || node.type === "string") {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (node.type === "identifier") {
|
|
149
|
+
imports.push({ imported: "default", local: getNodeText(node, sourceCode), source });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (node.type === "import_specifier") {
|
|
154
|
+
const nameNode = childByFieldName(node, "name");
|
|
155
|
+
const aliasNode = childByFieldName(node, "alias");
|
|
156
|
+
if (nameNode) {
|
|
157
|
+
imports.push({
|
|
158
|
+
imported: getNodeText(nameNode, sourceCode),
|
|
159
|
+
local: getNodeText(aliasNode || nameNode, sourceCode),
|
|
160
|
+
source
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (node.type === "namespace_import") {
|
|
167
|
+
const name = [...collectIdentifiers(node, sourceCode)][0];
|
|
168
|
+
if (name) {
|
|
169
|
+
imports.push({ imported: "*", local: name, source });
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (let index = 0; index < node.namedChildCount; index += 1) {
|
|
175
|
+
collectImportClause(node.namedChild(index), sourceCode, source, imports);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function collectExportNames(node, sourceCode, exports) {
|
|
180
|
+
if (!node) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (node.type === "variable_declarator") {
|
|
185
|
+
const name = getDeclaratorName(node, sourceCode);
|
|
186
|
+
if (name) {
|
|
187
|
+
exports.add(name);
|
|
188
|
+
}
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (node.type === "export_specifier") {
|
|
193
|
+
const aliasNode = childByFieldName(node, "alias");
|
|
194
|
+
const nameNode = childByFieldName(node, "name");
|
|
195
|
+
const name = aliasNode || nameNode;
|
|
196
|
+
if (name) {
|
|
197
|
+
exports.add(getNodeText(name, sourceCode));
|
|
198
|
+
}
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (node.type === "function_declaration" || node.type === "class_declaration") {
|
|
203
|
+
const name = childByFieldName(node, "name");
|
|
204
|
+
if (name) {
|
|
205
|
+
exports.add(getNodeText(name, sourceCode));
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (let index = 0; index < node.namedChildCount; index += 1) {
|
|
211
|
+
collectExportNames(node.namedChild(index), sourceCode, exports);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function collectReExportSpecifiers(node, sourceCode, source, reExports) {
|
|
216
|
+
if (!node) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (node.type === "export_specifier") {
|
|
221
|
+
const nameNode = childByFieldName(node, "name");
|
|
222
|
+
const aliasNode = childByFieldName(node, "alias");
|
|
223
|
+
if (nameNode) {
|
|
224
|
+
const imported = getNodeText(nameNode, sourceCode);
|
|
225
|
+
reExports.push({
|
|
226
|
+
imported,
|
|
227
|
+
exported: getNodeText(aliasNode || nameNode, sourceCode),
|
|
228
|
+
source
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for (let index = 0; index < node.namedChildCount; index += 1) {
|
|
235
|
+
collectReExportSpecifiers(node.namedChild(index), sourceCode, source, reExports);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function parseModuleBoundaries(rootNode, sourceCode) {
|
|
240
|
+
const imports = [];
|
|
241
|
+
const exports = new Set();
|
|
242
|
+
const reExports = [];
|
|
243
|
+
|
|
244
|
+
for (let index = 0; index < rootNode.namedChildCount; index += 1) {
|
|
245
|
+
const node = rootNode.namedChild(index);
|
|
246
|
+
|
|
247
|
+
if (node.type === "import_statement") {
|
|
248
|
+
const source = getImportSource(node, sourceCode);
|
|
249
|
+
if (source) {
|
|
250
|
+
for (let childIndex = 0; childIndex < node.namedChildCount; childIndex += 1) {
|
|
251
|
+
collectImportClause(node.namedChild(childIndex), sourceCode, source, imports);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (node.type === "export_statement") {
|
|
257
|
+
const source = getImportSource(node, sourceCode);
|
|
258
|
+
if (source) {
|
|
259
|
+
const text = getNodeText(node, sourceCode);
|
|
260
|
+
if (/^\s*export\s+\*/.test(text)) {
|
|
261
|
+
reExports.push({ imported: "*", exported: "*", source });
|
|
262
|
+
} else {
|
|
263
|
+
collectReExportSpecifiers(node, sourceCode, source, reExports);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
collectExportNames(node, sourceCode, exports);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { imports, exports, reExports };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function fileExists(filePath) {
|
|
274
|
+
try {
|
|
275
|
+
return fs.statSync(filePath).isFile();
|
|
276
|
+
} catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function resolveImportPath(fromFile, importSource) {
|
|
282
|
+
if (!importSource.startsWith(".")) {
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const basePath = path.resolve(path.dirname(fromFile), importSource);
|
|
287
|
+
const extension = path.extname(basePath);
|
|
288
|
+
const candidates = extension
|
|
289
|
+
? [basePath]
|
|
290
|
+
: [
|
|
291
|
+
...SOURCE_EXTENSIONS.map((ext) => `${basePath}${ext}`),
|
|
292
|
+
...SOURCE_EXTENSIONS.map((ext) => path.join(basePath, `index${ext}`))
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
return candidates.find(fileExists) || null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function resolveGraphImport(sourceFile, imported) {
|
|
299
|
+
if (imported.source && path.isAbsolute(imported.source)) {
|
|
300
|
+
return imported.source;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (imported.source && imported.source.startsWith(".")) {
|
|
304
|
+
return resolveImportPath(sourceFile, imported.source);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function analyzeTaintGraph(projectGraph) {
|
|
311
|
+
const violations = [];
|
|
312
|
+
let changed = true;
|
|
313
|
+
|
|
314
|
+
while (changed) {
|
|
315
|
+
changed = false;
|
|
316
|
+
|
|
317
|
+
for (const [filePath, fileNode] of Object.entries(projectGraph)) {
|
|
318
|
+
for (const reExport of fileNode.reExports || []) {
|
|
319
|
+
const sourceFile = resolveGraphImport(filePath, reExport);
|
|
320
|
+
const sourceNode = sourceFile ? projectGraph[sourceFile] : null;
|
|
321
|
+
if (!sourceNode) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (reExport.imported === "*") {
|
|
326
|
+
for (const taintedName of sourceNode.taintedSources || []) {
|
|
327
|
+
if (!fileNode.taintedSources.has(taintedName)) {
|
|
328
|
+
fileNode.taintedSources.add(taintedName);
|
|
329
|
+
fileNode.exports?.add?.(taintedName);
|
|
330
|
+
changed = true;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (sourceNode.taintedSources?.has(reExport.imported) && !fileNode.taintedSources.has(reExport.exported)) {
|
|
337
|
+
fileNode.taintedSources.add(reExport.exported);
|
|
338
|
+
fileNode.exports?.add?.(reExport.exported);
|
|
339
|
+
changed = true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
for (const imported of fileNode.imports || []) {
|
|
344
|
+
const sourceFile = resolveGraphImport(filePath, imported);
|
|
345
|
+
const sourceNode = sourceFile ? projectGraph[sourceFile] : null;
|
|
346
|
+
if (!sourceNode) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const sourceName = imported.imported === "default" || imported.imported === "*" ? imported.local : imported.imported;
|
|
351
|
+
if (sourceNode.taintedSources?.has(sourceName) && !fileNode.taintedSources.has(imported.local)) {
|
|
352
|
+
fileNode.taintedSources.add(imported.local);
|
|
353
|
+
changed = true;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
for (const [filePath, fileNode] of Object.entries(projectGraph)) {
|
|
360
|
+
if (!fileNode.isClient) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
for (const imported of fileNode.imports || []) {
|
|
365
|
+
const sourceFile = resolveGraphImport(filePath, imported);
|
|
366
|
+
const sourceNode = sourceFile ? projectGraph[sourceFile] : null;
|
|
367
|
+
if (!sourceNode) {
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const sourceName = imported.imported === "default" || imported.imported === "*" ? imported.local : imported.imported;
|
|
372
|
+
if (sourceNode.taintedSources?.has(sourceName)) {
|
|
373
|
+
violations.push({
|
|
374
|
+
status: "VIOLATION",
|
|
375
|
+
variable: imported.local,
|
|
376
|
+
sourceFile,
|
|
377
|
+
leakedFile: filePath
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return violations;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
module.exports = {
|
|
387
|
+
analyzeTaintGraph,
|
|
388
|
+
findTaintSources,
|
|
389
|
+
isClientComponent,
|
|
390
|
+
parseJavaScript,
|
|
391
|
+
parseModuleBoundaries,
|
|
392
|
+
resolveImportPath
|
|
393
|
+
};
|
|
Binary file
|
|
Binary file
|
|
Binary file
|