@massu/core 1.5.7 → 1.6.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/dist/adapter.d.ts +76 -0
- package/dist/adapter.js +431 -0
- package/dist/cli.js +818 -356
- package/dist/detect/adapters/.bundle-shasums.json +12 -0
- package/dist/detect/adapters/aspnet.js +577 -0
- package/dist/detect/adapters/go-chi.js +561 -0
- package/dist/detect/adapters/parse-guard.d.ts +69 -0
- package/dist/detect/adapters/parse-guard.js +54 -0
- package/dist/detect/adapters/phoenix.js +556 -0
- package/dist/detect/adapters/query-helpers.d.ts +60 -0
- package/dist/detect/adapters/query-helpers.js +85 -0
- package/dist/detect/adapters/rails.js +567 -0
- package/dist/detect/adapters/spring.js +582 -0
- package/dist/detect/adapters/tree-sitter-loader.d.ts +102 -0
- package/dist/detect/adapters/tree-sitter-loader.js +317 -0
- package/dist/detect/adapters/types.d.ts +151 -0
- package/dist/detect/adapters/types.js +0 -0
- package/dist/hooks/session-start.js +570 -5224
- package/package.json +17 -5
- package/src/adapter.ts +31 -0
- package/src/detect/adapters/aspnet.ts +4 -293
- package/src/detect/adapters/go-chi.ts +4 -261
- package/src/detect/adapters/phoenix.ts +4 -277
- package/src/detect/adapters/rails.ts +4 -279
- package/src/detect/adapters/spring.ts +4 -284
- package/src/security/registry-pubkey.generated.ts +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"@massu/core/adapter": "c6967280a498fc95b0f22a3e083a7dda509b5a3f5d249b94397c654e241dda2b",
|
|
3
|
+
"query-helpers": "f1372ef965450109258ad5bd300a1b3655660149e0b1b69ad30e1cc4acedcb2e",
|
|
4
|
+
"parse-guard": "1bed6c2298b40c07f0bd9641d975bf00e4d656fb4f52ab42a28666390d47ec6d",
|
|
5
|
+
"tree-sitter-loader": "6e26e2c81385ce39f6edc2d7bc6537e4f3a048693a62e191ff9928ebfdddd14e",
|
|
6
|
+
"types": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
7
|
+
"rails": "a58db04f6f232a859f41714229b0680db3e2960213df9dab1ef95cd9d6670ede",
|
|
8
|
+
"phoenix": "3902e42393f4edbbe17c0058f9fc09c20b80ece1b08b56305cca6e9b972a8821",
|
|
9
|
+
"aspnet": "a04de27b4bf31c626375c376b613e2851d420a8c94494d9a222bd01c76f505ad",
|
|
10
|
+
"spring": "101ed8ab391b4769383dec6c1fee7429c2ee1f57efdb7d391f391d69e5dcbe1b",
|
|
11
|
+
"go-chi": "03be4cb66102ae172ec443f154ce18f0d5280e5e32cf408112539f06dad347c9"
|
|
12
|
+
}
|
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
// ../adapter-aspnet/dist/index.js
|
|
2
|
+
import { Parser as Parser2 } from "web-tree-sitter";
|
|
3
|
+
|
|
4
|
+
// src/detect/adapters/query-helpers.ts
|
|
5
|
+
import { Query } from "web-tree-sitter";
|
|
6
|
+
var InvalidQueryError = class extends Error {
|
|
7
|
+
queryName;
|
|
8
|
+
querySource;
|
|
9
|
+
cause;
|
|
10
|
+
constructor(queryName, querySource, cause) {
|
|
11
|
+
const causeMsg = cause instanceof Error ? cause.message : String(cause);
|
|
12
|
+
super(
|
|
13
|
+
`[query-helpers] Invalid Tree-sitter query "${queryName}": ${causeMsg}
|
|
14
|
+
Query source:
|
|
15
|
+
${querySource}`
|
|
16
|
+
);
|
|
17
|
+
this.name = "InvalidQueryError";
|
|
18
|
+
this.queryName = queryName;
|
|
19
|
+
this.querySource = querySource;
|
|
20
|
+
this.cause = cause;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
var queryCache = /* @__PURE__ */ new WeakMap();
|
|
24
|
+
function compileQuery(language, source, queryName) {
|
|
25
|
+
let perLang = queryCache.get(language);
|
|
26
|
+
if (!perLang) {
|
|
27
|
+
perLang = /* @__PURE__ */ new Map();
|
|
28
|
+
queryCache.set(language, perLang);
|
|
29
|
+
}
|
|
30
|
+
const cached = perLang.get(source);
|
|
31
|
+
if (cached) return cached;
|
|
32
|
+
let q;
|
|
33
|
+
try {
|
|
34
|
+
q = new Query(language, source);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
throw new InvalidQueryError(queryName, source, e);
|
|
37
|
+
}
|
|
38
|
+
perLang.set(source, q);
|
|
39
|
+
return q;
|
|
40
|
+
}
|
|
41
|
+
function runQuery(parser, source, queryText, queryName, filePath) {
|
|
42
|
+
const language = parser.language;
|
|
43
|
+
if (!language) {
|
|
44
|
+
throw new InvalidQueryError(
|
|
45
|
+
queryName,
|
|
46
|
+
queryText,
|
|
47
|
+
new Error("Parser has no language assigned")
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const query = compileQuery(language, queryText, queryName);
|
|
51
|
+
const tree = parser.parse(source);
|
|
52
|
+
if (!tree) return [];
|
|
53
|
+
let matches;
|
|
54
|
+
try {
|
|
55
|
+
matches = query.matches(tree.rootNode);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
throw new InvalidQueryError(queryName, queryText, e);
|
|
58
|
+
}
|
|
59
|
+
const out = [];
|
|
60
|
+
for (const match of matches) {
|
|
61
|
+
if (!match.captures || match.captures.length === 0) continue;
|
|
62
|
+
const captures = {};
|
|
63
|
+
let earliestLine = Number.POSITIVE_INFINITY;
|
|
64
|
+
for (const cap of match.captures) {
|
|
65
|
+
const node = cap.node;
|
|
66
|
+
captures[cap.name] = node.text;
|
|
67
|
+
if (node.startPosition.row + 1 < earliestLine) {
|
|
68
|
+
earliestLine = node.startPosition.row + 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
out.push({
|
|
72
|
+
captures,
|
|
73
|
+
file: filePath,
|
|
74
|
+
line: Number.isFinite(earliestLine) ? earliestLine : 1,
|
|
75
|
+
queryName
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
tree.delete();
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/detect/adapters/tree-sitter-loader.ts
|
|
86
|
+
import { createHash } from "crypto";
|
|
87
|
+
import {
|
|
88
|
+
mkdirSync,
|
|
89
|
+
readdirSync,
|
|
90
|
+
readFileSync,
|
|
91
|
+
writeFileSync,
|
|
92
|
+
renameSync,
|
|
93
|
+
unlinkSync,
|
|
94
|
+
lstatSync,
|
|
95
|
+
chmodSync,
|
|
96
|
+
utimesSync
|
|
97
|
+
} from "fs";
|
|
98
|
+
import { homedir } from "os";
|
|
99
|
+
import { dirname, join } from "path";
|
|
100
|
+
import { Language, Parser } from "web-tree-sitter";
|
|
101
|
+
var GrammarSHAMismatchError = class extends Error {
|
|
102
|
+
language;
|
|
103
|
+
expected;
|
|
104
|
+
actual;
|
|
105
|
+
constructor(language, expected, actual) {
|
|
106
|
+
super(
|
|
107
|
+
`[tree-sitter-loader] SHA-256 mismatch for grammar "${language}". Expected ${expected}, got ${actual}. REFUSING to load \u2014 see Phase 3.5 audit attack vector #3.`
|
|
108
|
+
);
|
|
109
|
+
this.name = "GrammarSHAMismatchError";
|
|
110
|
+
this.language = language;
|
|
111
|
+
this.expected = expected;
|
|
112
|
+
this.actual = actual;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var GrammarUnavailableError = class extends Error {
|
|
116
|
+
language;
|
|
117
|
+
cause;
|
|
118
|
+
constructor(language, cause) {
|
|
119
|
+
const causeMsg = cause instanceof Error ? cause.message : cause ? String(cause) : "no cached grammar and download failed";
|
|
120
|
+
super(
|
|
121
|
+
`[tree-sitter-loader] Grammar for "${language}" is unavailable: ${causeMsg}. Falling back to regex introspection for files in ${language}.`
|
|
122
|
+
);
|
|
123
|
+
this.name = "GrammarUnavailableError";
|
|
124
|
+
this.language = language;
|
|
125
|
+
this.cause = cause;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
var GrammarCacheSymlinkError = class extends Error {
|
|
129
|
+
cachePath;
|
|
130
|
+
constructor(cachePath) {
|
|
131
|
+
super(
|
|
132
|
+
`[tree-sitter-loader] Refusing to load grammar \u2014 cache path "${cachePath}" is a symlink or non-regular file. (Phase 3.5 finding #3 \u2014 symlink attack vector.)`
|
|
133
|
+
);
|
|
134
|
+
this.name = "GrammarCacheSymlinkError";
|
|
135
|
+
this.cachePath = cachePath;
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
var GrammarUrlNotHttpsError = class extends Error {
|
|
139
|
+
url;
|
|
140
|
+
constructor(url) {
|
|
141
|
+
super(
|
|
142
|
+
`[tree-sitter-loader] Refusing to download grammar from non-HTTPS URL: ${url}. Only https:// URLs are accepted. (Phase 3.5 finding #3.)`
|
|
143
|
+
);
|
|
144
|
+
this.name = "GrammarUrlNotHttpsError";
|
|
145
|
+
this.url = url;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
var GRAMMAR_MANIFEST = {
|
|
149
|
+
python: {
|
|
150
|
+
url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-python.wasm",
|
|
151
|
+
sha256: "9056d0fb0c337810d019fae350e8167786119da98f0f282aceae7ab89ee8253b",
|
|
152
|
+
version: "0.1.13"
|
|
153
|
+
},
|
|
154
|
+
typescript: {
|
|
155
|
+
url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-typescript.wasm",
|
|
156
|
+
sha256: "8515404dceed38e1ed86aa34b09fcf3379fff1b4ff9dd3967bcd6d1eb5ac3d8f",
|
|
157
|
+
version: "0.1.13"
|
|
158
|
+
},
|
|
159
|
+
javascript: {
|
|
160
|
+
url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-javascript.wasm",
|
|
161
|
+
sha256: "63812b9e275d26851264734868d27a1656bd44a2ef6eb3e85e6b03728c595ab5",
|
|
162
|
+
version: "0.1.13"
|
|
163
|
+
},
|
|
164
|
+
swift: {
|
|
165
|
+
url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-swift.wasm",
|
|
166
|
+
sha256: "41c4fdb2249a3aa6d87eed0d383081ff09725c2248b4977043a43825980ffcc7",
|
|
167
|
+
version: "0.1.13"
|
|
168
|
+
},
|
|
169
|
+
// ----------------------------------------------------------------
|
|
170
|
+
// Plan 3c Phase 7 expansion (2026-05-07):
|
|
171
|
+
//
|
|
172
|
+
// Six additional grammars to support the registry-verified framework
|
|
173
|
+
// adapters (go-chi, rails, aspnet, spring, ktor, phoenix) plus the
|
|
174
|
+
// bundled adapters in the same language families (gin/echo/fiber,
|
|
175
|
+
// sinatra, etc.). All entries use the SAME pinned tree-sitter-wasms
|
|
176
|
+
// version (0.1.13) as the v1 four to keep the dependency surface
|
|
177
|
+
// single-source.
|
|
178
|
+
//
|
|
179
|
+
// SHA-256s computed 2026-05-07 via:
|
|
180
|
+
// curl -fsSL <url> | shasum -a 256
|
|
181
|
+
//
|
|
182
|
+
// The unpkg filename for C# uses an underscore (`c_sharp`) while the
|
|
183
|
+
// TreeSitterLanguage identifier uses no separator (`csharp`); the map
|
|
184
|
+
// key is the type identifier, the URL is the storage path — they do
|
|
185
|
+
// NOT need to match, the same as how `python` maps to `tree-sitter-
|
|
186
|
+
// python.wasm`. This is intentional and validated by the manifest
|
|
187
|
+
// shape test in tree-sitter-loader-manifest.test.ts.
|
|
188
|
+
// ----------------------------------------------------------------
|
|
189
|
+
go: {
|
|
190
|
+
url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-go.wasm",
|
|
191
|
+
sha256: "9963ca89b616eaf04b08a43bc1fb0f07b85395bec313330851f1f1ead2f755b6",
|
|
192
|
+
version: "0.1.13"
|
|
193
|
+
},
|
|
194
|
+
ruby: {
|
|
195
|
+
url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-ruby.wasm",
|
|
196
|
+
sha256: "93a5022855314cdb45458c7bb026a24a0ebc3a5ff6439e542e881f14dfa13a39",
|
|
197
|
+
version: "0.1.13"
|
|
198
|
+
},
|
|
199
|
+
csharp: {
|
|
200
|
+
url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-c_sharp.wasm",
|
|
201
|
+
sha256: "6266a7e32d68a3459104d994dc848df15d5672b0ea8e86d327274b694f8e6991",
|
|
202
|
+
version: "0.1.13"
|
|
203
|
+
},
|
|
204
|
+
java: {
|
|
205
|
+
url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-java.wasm",
|
|
206
|
+
sha256: "637aac4415fb39a211a4f4292d63c66b5ce9c32fa2cd35464af4f681d91b9a1f",
|
|
207
|
+
version: "0.1.13"
|
|
208
|
+
},
|
|
209
|
+
kotlin: {
|
|
210
|
+
url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-kotlin.wasm",
|
|
211
|
+
sha256: "b5cb00c8d06ed0f10f1dbe497205b437809d7e87db1f638721a8cfb30e044449",
|
|
212
|
+
version: "0.1.13"
|
|
213
|
+
},
|
|
214
|
+
elixir: {
|
|
215
|
+
url: "https://unpkg.com/tree-sitter-wasms@0.1.13/out/tree-sitter-elixir.wasm",
|
|
216
|
+
sha256: "82e91b9759ddca30d8978ebbfa8e347b4451b64c931f9ae62112e6db9b8fac20",
|
|
217
|
+
version: "0.1.13"
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
function getCacheDir() {
|
|
221
|
+
return process.env.MASSU_WASM_CACHE_DIR ?? join(homedir(), ".massu", "wasm-cache");
|
|
222
|
+
}
|
|
223
|
+
function getCachedPath(language, sha) {
|
|
224
|
+
return join(getCacheDir(), `${language}-${sha}.wasm`);
|
|
225
|
+
}
|
|
226
|
+
var DEFAULT_CACHE_RETAIN_COUNT = 16;
|
|
227
|
+
function getCacheRetainCount() {
|
|
228
|
+
const env = process.env.MASSU_WASM_CACHE_RETAIN;
|
|
229
|
+
if (env) {
|
|
230
|
+
const n = Number(env);
|
|
231
|
+
if (Number.isFinite(n) && n >= 1 && n <= 1024) return Math.floor(n);
|
|
232
|
+
}
|
|
233
|
+
return DEFAULT_CACHE_RETAIN_COUNT;
|
|
234
|
+
}
|
|
235
|
+
function touchCacheFile(path) {
|
|
236
|
+
try {
|
|
237
|
+
const now = /* @__PURE__ */ new Date();
|
|
238
|
+
utimesSync(path, now, now);
|
|
239
|
+
} catch {
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function evictBeyondRetainCount(retain = getCacheRetainCount()) {
|
|
243
|
+
const dir = getCacheDir();
|
|
244
|
+
let entries;
|
|
245
|
+
try {
|
|
246
|
+
entries = readdirSync(dir);
|
|
247
|
+
} catch {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const candidates = [];
|
|
251
|
+
for (const name of entries) {
|
|
252
|
+
if (!name.endsWith(".wasm")) continue;
|
|
253
|
+
const path = join(dir, name);
|
|
254
|
+
let stat;
|
|
255
|
+
try {
|
|
256
|
+
stat = lstatSync(path);
|
|
257
|
+
} catch {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
261
|
+
console.error(
|
|
262
|
+
`[tree-sitter-loader] cache eviction skipped non-regular file: ${path} (possible symlink attack \u2014 see Phase 3.5 finding F-008).`
|
|
263
|
+
);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
candidates.push({ path, mtimeMs: stat.mtimeMs });
|
|
267
|
+
}
|
|
268
|
+
if (candidates.length <= retain) return;
|
|
269
|
+
candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
270
|
+
for (const victim of candidates.slice(retain)) {
|
|
271
|
+
try {
|
|
272
|
+
unlinkSync(victim.path);
|
|
273
|
+
} catch {
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function sha256(bytes) {
|
|
278
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
279
|
+
}
|
|
280
|
+
var parserInitPromise = null;
|
|
281
|
+
async function ensureParserInitialized() {
|
|
282
|
+
if (parserInitPromise) return parserInitPromise;
|
|
283
|
+
parserInitPromise = Parser.init();
|
|
284
|
+
return parserInitPromise;
|
|
285
|
+
}
|
|
286
|
+
var loadedGrammars = /* @__PURE__ */ new Map();
|
|
287
|
+
async function loadGrammar(language, options = {}) {
|
|
288
|
+
await ensureParserInitialized();
|
|
289
|
+
const cached = loadedGrammars.get(language);
|
|
290
|
+
if (cached) return cached;
|
|
291
|
+
const manifest = options.manifestOverride?.[language] ?? GRAMMAR_MANIFEST[language];
|
|
292
|
+
if (!manifest) {
|
|
293
|
+
throw new GrammarUnavailableError(
|
|
294
|
+
language,
|
|
295
|
+
new Error(`No manifest entry for language "${language}". v1 supports: ${Object.keys(GRAMMAR_MANIFEST).join(", ")}.`)
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
const cachePath = getCachedPath(language, manifest.sha256);
|
|
299
|
+
let cacheLstat;
|
|
300
|
+
try {
|
|
301
|
+
cacheLstat = lstatSync(cachePath);
|
|
302
|
+
} catch {
|
|
303
|
+
cacheLstat = null;
|
|
304
|
+
}
|
|
305
|
+
if (cacheLstat) {
|
|
306
|
+
if (cacheLstat.isSymbolicLink() || !cacheLstat.isFile()) {
|
|
307
|
+
throw new GrammarCacheSymlinkError(cachePath);
|
|
308
|
+
}
|
|
309
|
+
let bytes;
|
|
310
|
+
try {
|
|
311
|
+
bytes = readFileSync(cachePath);
|
|
312
|
+
} catch (e) {
|
|
313
|
+
bytes = new Uint8Array(0);
|
|
314
|
+
}
|
|
315
|
+
if (bytes.byteLength > 0) {
|
|
316
|
+
const actualSha = sha256(bytes);
|
|
317
|
+
if (actualSha !== manifest.sha256) {
|
|
318
|
+
throw new GrammarSHAMismatchError(language, manifest.sha256, actualSha);
|
|
319
|
+
}
|
|
320
|
+
const lang2 = await Language.load(bytes);
|
|
321
|
+
loadedGrammars.set(language, lang2);
|
|
322
|
+
touchCacheFile(cachePath);
|
|
323
|
+
return lang2;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (!/^https:\/\//i.test(manifest.url)) {
|
|
327
|
+
throw new GrammarUrlNotHttpsError(manifest.url);
|
|
328
|
+
}
|
|
329
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
330
|
+
if (!fetchImpl) {
|
|
331
|
+
throw new GrammarUnavailableError(
|
|
332
|
+
language,
|
|
333
|
+
new Error("No fetch implementation available (Node < 18?)")
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
let body;
|
|
337
|
+
try {
|
|
338
|
+
const res = await fetchImpl(manifest.url);
|
|
339
|
+
if (!res.ok) {
|
|
340
|
+
throw new Error(`HTTP ${res.status ?? "unknown"} from ${manifest.url}`);
|
|
341
|
+
}
|
|
342
|
+
body = new Uint8Array(await res.arrayBuffer());
|
|
343
|
+
} catch (e) {
|
|
344
|
+
throw new GrammarUnavailableError(language, e);
|
|
345
|
+
}
|
|
346
|
+
const downloadedSha = sha256(body);
|
|
347
|
+
if (downloadedSha !== manifest.sha256) {
|
|
348
|
+
throw new GrammarSHAMismatchError(language, manifest.sha256, downloadedSha);
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
mkdirSync(dirname(cachePath), { recursive: true, mode: 448 });
|
|
352
|
+
try {
|
|
353
|
+
chmodSync(dirname(cachePath), 448);
|
|
354
|
+
} catch {
|
|
355
|
+
}
|
|
356
|
+
const tmpPath = `${cachePath}.tmp.${process.pid}`;
|
|
357
|
+
writeFileSync(tmpPath, body, { mode: 384 });
|
|
358
|
+
try {
|
|
359
|
+
chmodSync(tmpPath, 384);
|
|
360
|
+
} catch {
|
|
361
|
+
}
|
|
362
|
+
try {
|
|
363
|
+
renameSync(tmpPath, cachePath);
|
|
364
|
+
try {
|
|
365
|
+
chmodSync(cachePath, 384);
|
|
366
|
+
} catch {
|
|
367
|
+
}
|
|
368
|
+
} catch (e) {
|
|
369
|
+
try {
|
|
370
|
+
unlinkSync(tmpPath);
|
|
371
|
+
} catch {
|
|
372
|
+
}
|
|
373
|
+
throw e;
|
|
374
|
+
}
|
|
375
|
+
evictBeyondRetainCount();
|
|
376
|
+
} catch (e) {
|
|
377
|
+
console.error(
|
|
378
|
+
`[tree-sitter-loader] cache write failed for ${language}: ${e instanceof Error ? e.message : String(e)} \u2014 loading directly from memory.`
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
const lang = await Language.load(body);
|
|
382
|
+
loadedGrammars.set(language, lang);
|
|
383
|
+
return lang;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/detect/adapters/parse-guard.ts
|
|
387
|
+
var MAX_AST_FILE_BYTES = 1 * 1024 * 1024;
|
|
388
|
+
var MAX_AST_PARSE_DEPTH = 5e3;
|
|
389
|
+
function isParsableSource(source, sizeBytes) {
|
|
390
|
+
const bytes = sizeBytes ?? Buffer.byteLength(source, "utf-8");
|
|
391
|
+
if (bytes > MAX_AST_FILE_BYTES) {
|
|
392
|
+
return {
|
|
393
|
+
reason: "size-cap",
|
|
394
|
+
detail: `${bytes} bytes > ${MAX_AST_FILE_BYTES} cap`
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
let depth = 0;
|
|
398
|
+
let maxDepth = 0;
|
|
399
|
+
for (let i = 0; i < source.length; i++) {
|
|
400
|
+
const c = source.charCodeAt(i);
|
|
401
|
+
if (c === 0) {
|
|
402
|
+
return { reason: "control-bytes", detail: "NUL byte at offset " + i };
|
|
403
|
+
}
|
|
404
|
+
if (c === 40 || c === 91 || c === 123) {
|
|
405
|
+
depth++;
|
|
406
|
+
if (depth > maxDepth) maxDepth = depth;
|
|
407
|
+
if (depth > MAX_AST_PARSE_DEPTH) {
|
|
408
|
+
return {
|
|
409
|
+
reason: "depth-cap",
|
|
410
|
+
detail: `nesting depth exceeded ${MAX_AST_PARSE_DEPTH}`
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
} else if (c === 41 || c === 93 || c === 125) {
|
|
414
|
+
depth = depth > 0 ? depth - 1 : 0;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ../adapter-aspnet/dist/index.js
|
|
421
|
+
var MAP_VERB_QUERY = `
|
|
422
|
+
(invocation_expression
|
|
423
|
+
function: (member_access_expression
|
|
424
|
+
name: (identifier) @method (#match? @method "^Map(Get|Post|Put|Patch|Delete|Head|Options)$"))
|
|
425
|
+
arguments: (argument_list
|
|
426
|
+
.
|
|
427
|
+
(argument (string_literal) @route_path)))
|
|
428
|
+
`;
|
|
429
|
+
var HTTP_ATTR_QUERY = `
|
|
430
|
+
(attribute
|
|
431
|
+
name: (identifier) @attr_name (#match? @attr_name "^Http(Get|Post|Put|Patch|Delete|Head|Options)$"))
|
|
432
|
+
`;
|
|
433
|
+
var ROUTE_ATTR_QUERY = `
|
|
434
|
+
(attribute
|
|
435
|
+
name: (identifier) @_attr_name (#eq? @_attr_name "Route")
|
|
436
|
+
(attribute_argument_list
|
|
437
|
+
(attribute_argument (string_literal) @route_template)))
|
|
438
|
+
`;
|
|
439
|
+
var CONTROLLER_CLASS_QUERY = `
|
|
440
|
+
(class_declaration
|
|
441
|
+
name: (identifier) @class_name (#match? @class_name "Controller$"))
|
|
442
|
+
`;
|
|
443
|
+
var aspnetAdapter = {
|
|
444
|
+
id: "aspnet",
|
|
445
|
+
languages: ["csharp"],
|
|
446
|
+
matches(signals) {
|
|
447
|
+
if (!signals.csproj)
|
|
448
|
+
return false;
|
|
449
|
+
if (/Sdk\s*=\s*["']Microsoft\.NET\.Sdk\.Web["']/i.test(signals.csproj))
|
|
450
|
+
return true;
|
|
451
|
+
if (/Microsoft\.AspNetCore\.App/i.test(signals.csproj))
|
|
452
|
+
return true;
|
|
453
|
+
return false;
|
|
454
|
+
},
|
|
455
|
+
async introspect(files, _rootDir) {
|
|
456
|
+
if (files.length === 0) {
|
|
457
|
+
return { conventions: {}, provenance: [], confidence: "none" };
|
|
458
|
+
}
|
|
459
|
+
let language;
|
|
460
|
+
try {
|
|
461
|
+
language = await loadGrammar("csharp");
|
|
462
|
+
} catch (e) {
|
|
463
|
+
return { conventions: {}, provenance: [], confidence: "none" };
|
|
464
|
+
}
|
|
465
|
+
const parser = new Parser2();
|
|
466
|
+
parser.setLanguage(language);
|
|
467
|
+
const routeMethods = /* @__PURE__ */ new Map();
|
|
468
|
+
const prefixBases = /* @__PURE__ */ new Map();
|
|
469
|
+
const controllerClasses = /* @__PURE__ */ new Map();
|
|
470
|
+
try {
|
|
471
|
+
for (const file of files) {
|
|
472
|
+
const skip = isParsableSource(file.content, file.size);
|
|
473
|
+
if (skip) {
|
|
474
|
+
process.stderr.write(`[massu/ast] WARN: aspnet skipping ${file.path}: ${skip.reason} (${skip.detail}). Cap=${MAX_AST_FILE_BYTES}. (Phase 3.5 mitigation)
|
|
475
|
+
`);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
for (const hit of runQuery(parser, file.content, MAP_VERB_QUERY, "aspnet-map-verb", file.path)) {
|
|
480
|
+
const methodRaw = hit.captures.method;
|
|
481
|
+
if (!methodRaw)
|
|
482
|
+
continue;
|
|
483
|
+
const verb = methodRaw.replace(/^Map/, "");
|
|
484
|
+
if (!routeMethods.has(verb)) {
|
|
485
|
+
routeMethods.set(verb, { line: hit.line, file: file.path });
|
|
486
|
+
}
|
|
487
|
+
const pathRaw = hit.captures.route_path;
|
|
488
|
+
if (pathRaw) {
|
|
489
|
+
const literal = pathRaw.replace(/^["']/, "").replace(/["']$/, "");
|
|
490
|
+
const base = extractPrefixBase(literal);
|
|
491
|
+
if (base && !prefixBases.has(base)) {
|
|
492
|
+
prefixBases.set(base, { line: hit.line, file: file.path });
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
for (const hit of runQuery(parser, file.content, HTTP_ATTR_QUERY, "aspnet-http-attr", file.path)) {
|
|
497
|
+
const attrRaw = hit.captures.attr_name;
|
|
498
|
+
if (!attrRaw)
|
|
499
|
+
continue;
|
|
500
|
+
const verb = attrRaw.replace(/^Http/, "");
|
|
501
|
+
if (!routeMethods.has(verb)) {
|
|
502
|
+
routeMethods.set(verb, { line: hit.line, file: file.path });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
for (const hit of runQuery(parser, file.content, ROUTE_ATTR_QUERY, "aspnet-route-attr", file.path)) {
|
|
506
|
+
const tplRaw = hit.captures.route_template;
|
|
507
|
+
if (!tplRaw)
|
|
508
|
+
continue;
|
|
509
|
+
const literal = tplRaw.replace(/^["']/, "").replace(/["']$/, "");
|
|
510
|
+
const base = extractPrefixBase(literal);
|
|
511
|
+
if (base && !prefixBases.has(base)) {
|
|
512
|
+
prefixBases.set(base, { line: hit.line, file: file.path });
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
for (const hit of runQuery(parser, file.content, CONTROLLER_CLASS_QUERY, "aspnet-controller-class", file.path)) {
|
|
516
|
+
const name = hit.captures.class_name;
|
|
517
|
+
if (name && !controllerClasses.has(name)) {
|
|
518
|
+
controllerClasses.set(name, { line: hit.line, file: file.path });
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} catch (e) {
|
|
522
|
+
if (e instanceof InvalidQueryError) {
|
|
523
|
+
throw e;
|
|
524
|
+
}
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
} finally {
|
|
529
|
+
try {
|
|
530
|
+
parser.delete();
|
|
531
|
+
} catch {
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const conventions = {};
|
|
535
|
+
const provenance = [];
|
|
536
|
+
if (routeMethods.size === 1) {
|
|
537
|
+
const [name, { line, file }] = routeMethods.entries().next().value;
|
|
538
|
+
conventions.route_method = name;
|
|
539
|
+
provenance.push({ field: "route_method", sourceFile: file, line, query: "aspnet-map-verb" });
|
|
540
|
+
} else if (routeMethods.size >= 2) {
|
|
541
|
+
const [name, { line, file }] = routeMethods.entries().next().value;
|
|
542
|
+
conventions.route_method = name;
|
|
543
|
+
provenance.push({ field: "route_method", sourceFile: file, line, query: "aspnet-map-verb" });
|
|
544
|
+
}
|
|
545
|
+
if (prefixBases.size >= 1) {
|
|
546
|
+
const [base, { line, file }] = prefixBases.entries().next().value;
|
|
547
|
+
conventions.route_prefix_base = base;
|
|
548
|
+
provenance.push({ field: "route_prefix_base", sourceFile: file, line, query: "aspnet-route-prefix" });
|
|
549
|
+
}
|
|
550
|
+
if (controllerClasses.size >= 1) {
|
|
551
|
+
const [name, { line, file }] = controllerClasses.entries().next().value;
|
|
552
|
+
conventions.controller_class = name;
|
|
553
|
+
provenance.push({ field: "controller_class", sourceFile: file, line, query: "aspnet-controller-class" });
|
|
554
|
+
}
|
|
555
|
+
let confidence;
|
|
556
|
+
if (Object.keys(conventions).length === 0) {
|
|
557
|
+
confidence = "none";
|
|
558
|
+
} else if (routeMethods.size === 1) {
|
|
559
|
+
confidence = "high";
|
|
560
|
+
} else if (routeMethods.size >= 2) {
|
|
561
|
+
confidence = "low";
|
|
562
|
+
} else {
|
|
563
|
+
confidence = "medium";
|
|
564
|
+
}
|
|
565
|
+
return { conventions, provenance, confidence };
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
function extractPrefixBase(prefix) {
|
|
569
|
+
const stripped = prefix.replace(/^\/+/, "");
|
|
570
|
+
const firstSeg = stripped.split("/")[0];
|
|
571
|
+
if (!firstSeg)
|
|
572
|
+
return null;
|
|
573
|
+
return "/" + firstSeg;
|
|
574
|
+
}
|
|
575
|
+
export {
|
|
576
|
+
aspnetAdapter
|
|
577
|
+
};
|