@realmonsters/scan 0.0.0-stage → 0.0.2

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 CHANGED
@@ -1,3 +1,23 @@
1
- # Temporary Holding Version
1
+ # @realmonsters/scan
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Builds an inventory of what a repository depends on: packages, runtimes, container images, GitHub Actions, LLM model IDs and public vendor API hosts, each with file:line evidence and the places in code that use them.
4
+
5
+ ```sh
6
+ npx @realmonsters/scan . --out inventory.json
7
+ ```
8
+
9
+ ## What it does not do
10
+
11
+ - **No network.** The scanner imports no network or process modules. You can prove it by running it under Node's permission model, which denies network and child processes unless explicitly allowed:
12
+
13
+ ```sh
14
+ node --permission --allow-fs-read="$(pwd)/*" --allow-fs-read="$(npm root -g)/*" \
15
+ --allow-fs-write="$(pwd)/inventory.json" \
16
+ "$(npm root -g)/@realmonsters/scan/dist/cli.js" . --out inventory.json
17
+ ```
18
+
19
+ - **No source code.** The output contains file paths, line numbers, package names, versions, model IDs, imported symbol names and public vendor hostnames. It never contains a line of your code.
20
+ - **No secrets.** Real `.env*` files, `.envrc`, `.npmrc`, `.netrc` and key files are never opened. Anything credential-shaped is withheld, and every output string must pass a strict allow-list.
21
+ - **No internal hostnames.** Only known public vendors are named (e.g. `api.anthropic.com`, `zendesk.com`). Internal hosts, IP addresses, private registries and custom CI runner names are counted, never named.
22
+
23
+ Read `inventory.json` before you send it anywhere.
@@ -0,0 +1,864 @@
1
+ // src/scan.ts
2
+ import { readFile, readdir, lstat, open } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ // ../core/src/index.ts
6
+ var INVENTORY_SCHEMA = "realmonsters.inventory/0.1";
7
+
8
+ // src/redact.ts
9
+ var KW = "(?:password|passwd|pwd|secret|token|api[_-]?key|apikey|access[_-]?key|secret[_-]?key|client[_-]?secret|private[_-]?key|auth[_-]?token|credentials?|dsn|sas)";
10
+ var PATTERNS = [
11
+ // cloud
12
+ /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|ANPA)[0-9A-Z]{16}\b/,
13
+ // AWS key ids
14
+ /aws.{0,24}secret.{0,24}[:=]\s*['"]?[A-Za-z0-9/+=]{40}/i,
15
+ // AWS secret access key in context
16
+ /AccountKey=[^;\s'"]+/i,
17
+ /SharedAccessKey=[^;\s'"]+/i,
18
+ /[?&]sig=[A-Za-z0-9%+/=]{16,}/i,
19
+ // Azure
20
+ /"private_key(?:_id)?"\s*:/,
21
+ /"type"\s*:\s*"service_account"/,
22
+ // GCP service-account JSON
23
+ /\bAIza[0-9A-Za-z_-]{30,}/,
24
+ // Google API key
25
+ // source control and registries
26
+ /\bgh[pousr]_[A-Za-z0-9_]{20,}/,
27
+ /\bgithub_pat_[A-Za-z0-9_]{20,}/,
28
+ /\bglpat-[A-Za-z0-9_-]{20,}/,
29
+ /\bgldt-[A-Za-z0-9_-]{20,}/,
30
+ /\bnpm_[A-Za-z0-9]{36}\b/,
31
+ /\bpypi-[A-Za-z0-9_-]{16,}/,
32
+ /\/\/[^\s/]*:_authToken=/,
33
+ // .npmrc
34
+ // AI providers
35
+ /\bsk-ant-[A-Za-z0-9_-]{20,}/,
36
+ /\bsk-(?:proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}/,
37
+ // payments, messaging, SaaS
38
+ /\b[rsp]k_(?:live|test)_[A-Za-z0-9]{16,}/,
39
+ /\bwhsec_[A-Za-z0-9]{16,}/,
40
+ // Stripe
41
+ /\bSK[0-9a-fA-F]{32}\b/,
42
+ /\bAC[0-9a-fA-F]{32}\b/,
43
+ // Twilio
44
+ /\bSG\.[\w-]{16,}\.[\w-]{16,}/,
45
+ // SendGrid
46
+ /\bxox[abprse]-[A-Za-z0-9-]{10,}/,
47
+ /hooks\.slack\.com\/services\/[A-Z0-9/]+/i,
48
+ /discord(?:app)?\.com\/api\/webhooks\/\d+\/[\w-]+/i,
49
+ /\bntn_[A-Za-z0-9]{30,}/,
50
+ /\bsecret_[A-Za-z0-9]{30,}/,
51
+ // Notion
52
+ /\bhv[sbr]\.[A-Za-z0-9_-]{20,}/,
53
+ // HashiCorp Vault
54
+ /\blin_api_[A-Za-z0-9]{30,}/,
55
+ // Linear
56
+ /\bshp(?:at|ca|pa|ss)_[a-fA-F0-9]{32}/,
57
+ // Shopify
58
+ /\bdapi[a-f0-9]{32}/,
59
+ // Databricks
60
+ /\b(?:dd|datadog)[_-]?(?:api|app)[_-]?key\s*[:=]/i,
61
+ // Datadog in context
62
+ // structure
63
+ /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/,
64
+ // JWT
65
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
66
+ /-----BEGIN OPENSSH PRIVATE KEY-----/,
67
+ /\b[a-z][a-z0-9+.-]*:\/\/[^\s/:@'"]*:[^\s/@'"]+@/i,
68
+ // basic-auth in any scheme
69
+ /[?&](?:token|key|api[_-]?key|apikey|access_token|auth|signature|secret|password|pwd)=[^&\s'"]{6,}/i,
70
+ new RegExp(`(?<![A-Za-z0-9])[A-Za-z0-9_]*${KW}[A-Za-z0-9_]*\\s*[:=]\\s*['"]?[^\\s'",;]{8,}`, "i")
71
+ ];
72
+ function looksSecret(s) {
73
+ return PATTERNS.some((re) => re.test(s));
74
+ }
75
+ function lineLooksSecret(line) {
76
+ return looksSecret(line);
77
+ }
78
+ var SAFE = /^[A-Za-z0-9@._/:^~<>=!*+,| -]{1,120}$/;
79
+ var BLOB = /[A-Za-z0-9+/]{32,}={0,2}/g;
80
+ function hasBlob(s) {
81
+ for (const m of s.matchAll(BLOB)) if (!/^[0-9a-f]+$/.test(m[0])) return true;
82
+ return false;
83
+ }
84
+ function safeToken(raw) {
85
+ if (raw === void 0) return { ok: false, reason: "unsafe" };
86
+ const s = raw.trim();
87
+ if (looksSecret(s)) return { ok: false, reason: "secret" };
88
+ if (!SAFE.test(s)) return { ok: false, reason: "unsafe" };
89
+ if (hasBlob(s)) return { ok: false, reason: "secret" };
90
+ return { ok: true, value: s };
91
+ }
92
+
93
+ // src/hosts.ts
94
+ var VENDOR_DOMAINS = [
95
+ // AI
96
+ "api.anthropic.com",
97
+ "api.openai.com",
98
+ "generativelanguage.googleapis.com",
99
+ "aiplatform.googleapis.com",
100
+ "api.mistral.ai",
101
+ "api.cohere.com",
102
+ "api.cohere.ai",
103
+ "api.groq.com",
104
+ "api.together.xyz",
105
+ "api.fireworks.ai",
106
+ "api.deepseek.com",
107
+ "api.x.ai",
108
+ "api.perplexity.ai",
109
+ "api.replicate.com",
110
+ "api-inference.huggingface.co",
111
+ "huggingface.co",
112
+ "api.elevenlabs.io",
113
+ "api.deepgram.com",
114
+ "api.assemblyai.com",
115
+ "openai.azure.com",
116
+ "bedrock-runtime.amazonaws.com",
117
+ "api.voyageai.com",
118
+ "api.pinecone.io",
119
+ // dev platforms
120
+ "api.github.com",
121
+ "gitlab.com",
122
+ "api.bitbucket.org",
123
+ "api.vercel.com",
124
+ "api.netlify.com",
125
+ "api.cloudflare.com",
126
+ "api.render.com",
127
+ "api.fly.io",
128
+ "api.heroku.com",
129
+ "api.digitalocean.com",
130
+ "supabase.co",
131
+ "firebaseio.com",
132
+ "api.linear.app",
133
+ "sentry.io",
134
+ "api.datadoghq.com",
135
+ "api.datadoghq.eu",
136
+ "api.pagerduty.com",
137
+ "api.honeycomb.io",
138
+ "api.launchdarkly.com",
139
+ "api.statsig.com",
140
+ "api.segment.io",
141
+ "api.mixpanel.com",
142
+ "api.amplitude.com",
143
+ "app.posthog.com",
144
+ "us.i.posthog.com",
145
+ "eu.i.posthog.com",
146
+ // collaboration
147
+ "api.notion.com",
148
+ "slack.com",
149
+ "api.slack.com",
150
+ "graph.microsoft.com",
151
+ "login.microsoftonline.com",
152
+ "atlassian.net",
153
+ "api.atlassian.com",
154
+ "api.asana.com",
155
+ "api.monday.com",
156
+ "api.clickup.com",
157
+ "api.airtable.com",
158
+ "api.figma.com",
159
+ "api.zoom.us",
160
+ "www.googleapis.com",
161
+ "sheets.googleapis.com",
162
+ "gmail.googleapis.com",
163
+ "calendar.googleapis.com",
164
+ "drive.googleapis.com",
165
+ "discord.com",
166
+ "api.telegram.org",
167
+ "graph.facebook.com",
168
+ // business
169
+ "api.stripe.com",
170
+ "api.paypal.com",
171
+ "api-m.paypal.com",
172
+ "connect.squareup.com",
173
+ "api.adyen.com",
174
+ "plaid.com",
175
+ "api.wise.com",
176
+ "salesforce.com",
177
+ "force.com",
178
+ "api.hubapi.com",
179
+ "zendesk.com",
180
+ "api.intercom.io",
181
+ "myshopify.com",
182
+ "api.shopify.com",
183
+ "api.twilio.com",
184
+ "api.sendgrid.com",
185
+ "api.mailgun.net",
186
+ "api.postmarkapp.com",
187
+ "api.resend.com",
188
+ "api.brevo.com",
189
+ "api.docusign.net",
190
+ "api.xero.com",
191
+ "quickbooks.api.intuit.com",
192
+ "api.chargebee.com",
193
+ "api.paddle.com",
194
+ "api.lemonsqueezy.com",
195
+ // identity / security / maps
196
+ "okta.com",
197
+ "auth0.com",
198
+ "api.workos.com",
199
+ "api.clerk.com",
200
+ "maps.googleapis.com",
201
+ "api.mapbox.com",
202
+ "acme-v02.api.letsencrypt.org"
203
+ ];
204
+ var IPV4 = /^\d{1,3}(?:\.\d{1,3}){3}$/;
205
+ var PRIVATE_TLD = /\.(?:internal|local|localhost|lan|corp|home|intranet|private|test|invalid|example)$/i;
206
+ function publicVendorHost(host) {
207
+ const h = host.toLowerCase().replace(/\.$/, "");
208
+ if (IPV4.test(h) || h.includes(":") || PRIVATE_TLD.test(h)) return null;
209
+ if (/^bedrock-runtime\.[a-z0-9-]+\.amazonaws\.com$/.test(h)) return "bedrock-runtime.amazonaws.com";
210
+ for (const d of VENDOR_DOMAINS) if (h === d || h.endsWith("." + d)) return d;
211
+ return null;
212
+ }
213
+
214
+ // src/callsites.ts
215
+ import ts from "typescript";
216
+ var JS_EXT = /\.(?:[cm]?[jt]sx?)$/;
217
+ function packageOf(spec) {
218
+ if (!spec || spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("node:") || spec.startsWith("#")) return null;
219
+ const parts = spec.split("/");
220
+ if (spec.startsWith("@")) return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
221
+ return parts[0];
222
+ }
223
+ function jsImports(file, text) {
224
+ if (!JS_EXT.test(file)) return [];
225
+ const kind = /\.tsx$|\.jsx$/.test(file) ? ts.ScriptKind.TSX : /\.ts$|\.mts$|\.cts$/.test(file) ? ts.ScriptKind.TS : ts.ScriptKind.JS;
226
+ const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, false, kind);
227
+ const hits = [];
228
+ const at = (node) => sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
229
+ const push = (spec, node, symbol) => {
230
+ const pkg = packageOf(spec);
231
+ if (pkg) hits.push({ pkg, site: { file, line: at(node), ...symbol ? { symbol } : {} } });
232
+ };
233
+ const visit = (node) => {
234
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
235
+ const spec = node.moduleSpecifier.text;
236
+ const c = node.importClause;
237
+ const names = [];
238
+ if (c?.name) names.push(c.name.text);
239
+ if (c?.namedBindings) {
240
+ if (ts.isNamespaceImport(c.namedBindings)) names.push(`* as ${c.namedBindings.name.text}`);
241
+ else for (const el of c.namedBindings.elements) names.push((el.propertyName ?? el.name).text);
242
+ }
243
+ push(spec, node, names.length ? names.join(", ") : void 0);
244
+ } else if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
245
+ push(node.moduleSpecifier.text, node, "re-export");
246
+ } else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference) && ts.isStringLiteral(node.moduleReference.expression)) {
247
+ push(node.moduleReference.expression.text, node, node.name.text);
248
+ } else if (ts.isCallExpression(node) && node.arguments.length >= 1 && ts.isStringLiteralLike(node.arguments[0])) {
249
+ const callee = node.expression;
250
+ if (callee.kind === ts.SyntaxKind.ImportKeyword) push(node.arguments[0].text, node, "dynamic import");
251
+ else if (ts.isIdentifier(callee) && callee.text === "require") push(node.arguments[0].text, node, "require");
252
+ }
253
+ ts.forEachChild(node, visit);
254
+ };
255
+ visit(sf);
256
+ return hits;
257
+ }
258
+ var PY_DIST = {
259
+ sklearn: "scikit-learn",
260
+ cv2: "opencv-python",
261
+ PIL: "pillow",
262
+ yaml: "pyyaml",
263
+ bs4: "beautifulsoup4",
264
+ google: "google-cloud",
265
+ dateutil: "python-dateutil",
266
+ jwt: "pyjwt",
267
+ dotenv: "python-dotenv",
268
+ psycopg2: "psycopg2-binary"
269
+ };
270
+ function pyImports(file, text) {
271
+ if (!file.endsWith(".py")) return [];
272
+ const hits = [];
273
+ text.split(/\r?\n/).forEach((raw, i) => {
274
+ const line = raw.trim();
275
+ let m = /^from\s+([A-Za-z_][\w]*)(?:\.[\w.]*)?\s+import\s+(.+)$/.exec(line);
276
+ if (m && m[1] !== "__future__") {
277
+ hits.push({ pkg: (PY_DIST[m[1]] ?? m[1]).toLowerCase().replace(/_/g, "-"), site: { file, line: i + 1, symbol: m[2].replace(/[()]/g, "").split("#")[0].trim().slice(0, 80) } });
278
+ return;
279
+ }
280
+ m = /^import\s+(.+)$/.exec(line);
281
+ if (m) for (const part of m[1].split("#")[0].split(",")) {
282
+ const mod = part.trim().split(/\s+as\s+/)[0].split(".")[0];
283
+ if (/^[A-Za-z_]\w*$/.test(mod)) hits.push({ pkg: (PY_DIST[mod] ?? mod).toLowerCase().replace(/_/g, "-"), site: { file, line: i + 1, symbol: part.trim().slice(0, 80) } });
284
+ }
285
+ });
286
+ return hits;
287
+ }
288
+
289
+ // src/lockfiles.ts
290
+ var isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
291
+ function keyLines(text) {
292
+ const m = /* @__PURE__ */ new Map();
293
+ const lines = text.split(/\r?\n/);
294
+ for (let i = 0; i < lines.length; i++) {
295
+ const k = /^\s*"([^"]+)"\s*:/.exec(lines[i]);
296
+ if (k && !m.has(k[1])) m.set(k[1], i + 1);
297
+ }
298
+ return m;
299
+ }
300
+ function npmPackageLock(text) {
301
+ let doc;
302
+ try {
303
+ doc = JSON.parse(text);
304
+ } catch {
305
+ return [];
306
+ }
307
+ if (!isObj(doc)) return [];
308
+ const lines = keyLines(text);
309
+ const out = [];
310
+ if (isObj(doc.packages)) {
311
+ for (const [key, v] of Object.entries(doc.packages)) {
312
+ const m = /^node_modules\/((?:@[^/]+\/)?[^/]+)$/.exec(key);
313
+ if (m && isObj(v) && typeof v.version === "string") out.push({ ecosystem: "npm", name: m[1], version: v.version, line: lines.get(key) ?? 1 });
314
+ }
315
+ } else if (isObj(doc.dependencies)) {
316
+ for (const [name, v] of Object.entries(doc.dependencies)) {
317
+ if (isObj(v) && typeof v.version === "string") out.push({ ecosystem: "npm", name, version: v.version, line: lines.get(name) ?? 1 });
318
+ }
319
+ }
320
+ return out;
321
+ }
322
+ function pnpmLock(text) {
323
+ const out = [];
324
+ const lines = text.split(/\r?\n/);
325
+ const hasImporters = lines.some((l) => /^importers:/.test(l));
326
+ const depIndent = hasImporters ? 4 : 0;
327
+ let inRoot = !hasImporters, inDeps = false, current = null;
328
+ const ind = (n) => " ".repeat(n);
329
+ for (let i = 0; i < lines.length; i++) {
330
+ const raw = lines[i];
331
+ if (hasImporters) {
332
+ if (/^importers:/.test(raw)) {
333
+ inRoot = false;
334
+ continue;
335
+ }
336
+ if (/^ \.:\s*$/.test(raw)) {
337
+ inRoot = true;
338
+ continue;
339
+ }
340
+ if (/^ \S/.test(raw) || /^\S/.test(raw)) {
341
+ inRoot = false;
342
+ inDeps = false;
343
+ }
344
+ } else if (/^\S/.test(raw)) {
345
+ inDeps = /^(dependencies|devDependencies|optionalDependencies):/.test(raw);
346
+ continue;
347
+ }
348
+ if (!inRoot) continue;
349
+ if (hasImporters && new RegExp(`^${ind(depIndent)}(dependencies|devDependencies|optionalDependencies):`).test(raw)) {
350
+ inDeps = true;
351
+ continue;
352
+ }
353
+ if (hasImporters && new RegExp(`^${ind(depIndent)}\\S`).test(raw)) {
354
+ inDeps = false;
355
+ continue;
356
+ }
357
+ if (!inDeps) continue;
358
+ const nameRe = new RegExp(`^${ind(depIndent + 2)}'?((?:@[^/'\\s]+/)?[^:'\\s]+)'?:\\s*(.*)$`);
359
+ const n = nameRe.exec(raw);
360
+ if (n) {
361
+ const inline = /^'?([0-9][^('\s]*)/.exec(n[2]);
362
+ if (inline) {
363
+ out.push({ ecosystem: "npm", name: n[1], version: inline[1], line: i + 1 });
364
+ current = null;
365
+ } else current = n[1];
366
+ continue;
367
+ }
368
+ const v = new RegExp(`^${ind(depIndent + 4)}version:\\s*'?([0-9][^('\\s]*)`).exec(raw);
369
+ if (v && current) {
370
+ out.push({ ecosystem: "npm", name: current, version: v[1], line: i + 1 });
371
+ current = null;
372
+ }
373
+ }
374
+ return out;
375
+ }
376
+ function yarnLock(text) {
377
+ const out = [];
378
+ const lines = text.split(/\r?\n/);
379
+ let name = null, line = 0;
380
+ const seen = /* @__PURE__ */ new Set();
381
+ for (let i = 0; i < lines.length; i++) {
382
+ const raw = lines[i];
383
+ if (!raw.startsWith(" ") && raw.trim() && !raw.startsWith("#") && !raw.startsWith("__metadata")) {
384
+ const h = /^"?((?:@[^@/"]+\/)?[^@"\s]+)@/.exec(raw);
385
+ name = h ? h[1] : null;
386
+ line = i + 1;
387
+ continue;
388
+ }
389
+ const v = /^\s+version:?\s+"?([0-9][^"\s]*)"?/.exec(raw);
390
+ if (v && name && !seen.has(name)) {
391
+ out.push({ ecosystem: "npm", name, version: v[1], line });
392
+ seen.add(name);
393
+ name = null;
394
+ }
395
+ }
396
+ return out;
397
+ }
398
+ function tomlPackageLock(text) {
399
+ const out = [];
400
+ const lines = text.split(/\r?\n/);
401
+ let name = null, line = 0;
402
+ for (let i = 0; i < lines.length; i++) {
403
+ const raw = lines[i];
404
+ if (/^\[\[package\]\]/.test(raw)) {
405
+ name = null;
406
+ continue;
407
+ }
408
+ const n = /^name\s*=\s*"([^"]+)"/.exec(raw);
409
+ if (n) {
410
+ name = n[1].toLowerCase().replace(/_/g, "-");
411
+ line = i + 1;
412
+ continue;
413
+ }
414
+ const v = /^version\s*=\s*"([^"]+)"/.exec(raw);
415
+ if (v && name) {
416
+ out.push({ ecosystem: "pypi", name, version: v[1], line });
417
+ name = null;
418
+ }
419
+ }
420
+ return out;
421
+ }
422
+
423
+ // src/scan.ts
424
+ var SCANNER = { name: "@realmonsters/scan", version: "0.0.2" };
425
+ var IGNORE_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".yarn", ".pnpm-store", ".gradle", ".m2", "bower_components", "Pods", ".serverless", ".aws-sam", ".vercel", ".netlify", "dist", "build", "out", ".next", ".nuxt", ".turbo", ".cache", "coverage", ".venv", "venv", "__pycache__", ".terraform", ".tox", ".mypy_cache", ".pytest_cache", "vendor", "target", ".idea", ".vscode"]);
426
+ var SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".py", ".json", ".yaml", ".yml", ".toml", ".tf"]);
427
+ var LOCKFILES = /* @__PURE__ */ new Set(["package-lock.json", "npm-shrinkwrap.json", "pnpm-lock.yaml", "yarn.lock", "poetry.lock", "uv.lock"]);
428
+ var MANIFESTS = /* @__PURE__ */ new Set(["package.json", "pyproject.toml", "requirements.txt", "Pipfile"]);
429
+ var SECRET_FILES = /* @__PURE__ */ new Set([".envrc", ".npmrc", ".pypirc", ".netrc", ".git-credentials", "credentials", "id_rsa", "id_ed25519"]);
430
+ var MAX_SOURCE_BYTES = 1e6;
431
+ var MAX_LOCK_BYTES = 5e7;
432
+ var MAX_FILES = 2e5;
433
+ var SKIP_EXT = /\.(?:png|jpe?g|gif|webp|ico|svg|pdf|zip|tgz|gz|bz2|xz|7z|rar|jar|war|whl|so|dylib|dll|exe|bin|wasm|woff2?|ttf|otf|eot|mp[34]|mov|avi|webm|wav|csv|tsv|parquet|sqlite|db|snap|map|min\.js)$/i;
434
+ var MODEL_CORE = "claude-(?:[a-z0-9]+-)*[a-z0-9.]+(?:@\\d{8})?|(?:chatgpt-)?gpt-(?:4o|4\\.1|4\\.5|4|5(?:\\.\\d+)?|6(?:\\.\\d+)?|3\\.5|realtime|audio|image)[a-z0-9.-]*|chatgpt-(?:4o|image)[a-z0-9.-]*|o[134](?:-mini|-pro|-preview)?(?:-deep-research)?(?:-\\d{4}-\\d{2}-\\d{2})?|tts-1(?:-hd)?(?:-\\d{4})?|whisper-1|dall-e-[23]|sora-2[a-z0-9.-]*|computer-use-preview[a-z0-9.-]*|codex-mini-latest|text-moderation-[a-z0-9-]+|text-(?:davinci|curie|babbage|ada)-(?:edit-)?\\d{3}|(?:babbage|davinci)-002|code-(?:davinci|cushman)-(?:edit-)?\\d{3}|(?:text|code)-(?:similarity|search)-[a-z0-9-]+|gemini-(?:\\d+(?:\\.\\d+)?|embedding|nano|omni|robotics|live|pro|flash|exp)[a-z0-9.-]*|embedding-(?:gecko-)?001|imagen-[0-9a-z.-]+|veo-\\d[a-z0-9.-]*|lyria-[a-z0-9.-]+|(?:open-)?mistral-[a-z0-9.-]+|(?:codestral|pixtral|ministral)-[a-z0-9.-]+|(?:meta-)?llama-?\\d(?:\\.\\d+)?-[0-9]+b[a-z0-9.-]*|text-embedding-[a-z0-9-]+|command-r[a-z0-9.+-]*";
435
+ var MODEL_RE = new RegExp(`(?<=["'\`=:]\\s?)(?:us\\.|eu\\.|apac\\.)?(?:anthropic\\.|models/)?(${MODEL_CORE})(?=["'\`\\s,;)]|$|-v\\d)`, "g");
436
+ var HOST_RE = /https?:\/\/([a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+)/gi;
437
+ var PUBLIC_REGISTRIES = /* @__PURE__ */ new Set(["docker.io", "index.docker.io", "ghcr.io", "public.ecr.aws", "gcr.io", "mcr.microsoft.com", "quay.io", "registry.k8s.io"]);
438
+ var GH_HOSTED_RUNNER = /^(?:ubuntu|windows|macos)-(?:latest|\d[\w.-]*)$/;
439
+ var BASE_RUNTIME = { node: "node", nodejs: "node", python: "python", postgres: "postgres", postgresql: "postgres", "aurora-postgresql": "postgres", mysql: "mysql", "aurora-mysql": "mysql", mariadb: "mariadb", redis: "redis", nginx: "nginx", ubuntu: "ubuntu", debian: "debian", alpine: "alpine" };
440
+ function modelEcosystem(id) {
441
+ if (id.startsWith("claude")) return "anthropic";
442
+ if (/^(?:gemini|embedding-|imagen-|veo-|lyria-)|^text-embedding-00\d/.test(id)) return "google";
443
+ if (/^(?:chatgpt-|gpt-|o[134]|tts-|whisper-|dall-e|sora-|computer-use|codex-|text-moderation|text-(?:davinci|curie|babbage|ada|similarity|search)|babbage-|davinci-|code-(?:davinci|cushman|search)|text-embedding)/.test(id)) return "openai";
444
+ if (/mistral|codestral|pixtral|ministral/.test(id)) return "mistral";
445
+ if (id.includes("llama")) return "meta";
446
+ return "other";
447
+ }
448
+ function normalizeVersion(raw) {
449
+ if (typeof raw !== "string") return void 0;
450
+ const v = raw.trim();
451
+ if (!v) return void 0;
452
+ if (/^(?:git\+|git:|github:|gitlab:|bitbucket:)|\.git(?:#|$)/i.test(v) || /^[\w.-]+\/[\w.-]+(?:#.*)?$/.test(v)) return "git";
453
+ if (/^https?:/i.test(v)) return "url";
454
+ if (/^(?:file|link|portal):/i.test(v) || v.startsWith(".") || v.startsWith("/") || v.startsWith("~/")) return "local";
455
+ if (/^workspace:/i.test(v)) return "workspace";
456
+ if (/^npm:/i.test(v)) return "alias";
457
+ if (/^catalog:/i.test(v)) return "catalog";
458
+ if (/^[a-z][a-z0-9-]{0,20}$/i.test(v)) return v.toLowerCase();
459
+ if (/^[\d^~<>=!*xX.|, +a-z-]{1,40}$/i.test(v)) return v.replace(/\s+/g, " ");
460
+ return "other";
461
+ }
462
+ var Collector = class {
463
+ constructor(stats) {
464
+ this.stats = stats;
465
+ }
466
+ map = /* @__PURE__ */ new Map();
467
+ add(kind, ecosystem, name, file, line, token, version, versionSource) {
468
+ const n = safeToken(name), t = safeToken(token);
469
+ if (!n.ok || !t.ok) {
470
+ if (!n.ok && n.reason === "secret" || !t.ok && t.reason === "secret") this.stats.redactions++;
471
+ else this.stats.tokensDropped++;
472
+ return;
473
+ }
474
+ let ver;
475
+ if (version !== void 0) {
476
+ const sv = safeToken(version);
477
+ if (sv.ok) ver = sv.value;
478
+ else this.stats.tokensDropped++;
479
+ }
480
+ const id = `${kind}:${ecosystem}/${n.value}`;
481
+ let u = this.map.get(id);
482
+ if (!u) {
483
+ u = { id, kind, ecosystem, name: n.value, evidence: [], callSites: [], versions: [], versionConflict: false };
484
+ this.map.set(id, u);
485
+ }
486
+ if (ver) {
487
+ if (!u.versions.includes(ver)) u.versions.push(ver);
488
+ if (!u.version) {
489
+ u.version = ver;
490
+ u.versionSource = versionSource;
491
+ }
492
+ }
493
+ if (!u.evidence.some((e) => e.file === file && e.line === line)) u.evidence.push({ file, line, token: t.value, ...ver ? { version: ver } : {} });
494
+ }
495
+ setLockVersion(id, version, file, line, token) {
496
+ const u = this.map.get(id);
497
+ const sv = safeToken(version), t = safeToken(token);
498
+ if (!u || !sv.ok || !t.ok) return;
499
+ u.version = sv.value;
500
+ u.versionSource = "lockfile";
501
+ if (!u.versions.includes(sv.value)) u.versions.push(sv.value);
502
+ if (!u.evidence.some((e) => e.file === file && e.line === line)) u.evidence.push({ file, line, token: t.value, version: sv.value });
503
+ }
504
+ addCallSite(id, site) {
505
+ const u = this.map.get(id);
506
+ if (!u) return;
507
+ if (site.symbol !== void 0) {
508
+ const s = safeToken(site.symbol);
509
+ site = s.ok ? { ...site, symbol: s.value } : { file: site.file, line: site.line };
510
+ }
511
+ if (!u.callSites.some((c) => c.file === site.file && c.line === site.line)) u.callSites.push(site);
512
+ }
513
+ all() {
514
+ const cmp = (a, b) => a < b ? -1 : a > b ? 1 : 0;
515
+ return [...this.map.values()].map((u) => {
516
+ u.versions.sort(cmp);
517
+ u.versionConflict = conflicting(u);
518
+ u.evidence.sort((a, b) => cmp(a.file, b.file) || a.line - b.line);
519
+ u.callSites.sort((a, b) => cmp(a.file, b.file) || a.line - b.line);
520
+ return u;
521
+ }).sort((a, b) => cmp(a.id, b.id));
522
+ }
523
+ };
524
+ function conflicting(u) {
525
+ const declared = u.evidence.filter((e) => e.version && !/^[\^~<>=]/.test(e.version)).map((e) => /(\d+)/.exec(e.version)?.[1]);
526
+ return new Set(declared.filter(Boolean)).size > 1;
527
+ }
528
+ async function walk(root, stats) {
529
+ const out = [];
530
+ const stack = [""];
531
+ while (stack.length && out.length < MAX_FILES) {
532
+ const rel = stack.pop();
533
+ let entries;
534
+ try {
535
+ entries = await readdir(path.join(root, rel), { withFileTypes: true });
536
+ } catch {
537
+ stats.filesSkipped++;
538
+ continue;
539
+ }
540
+ for (const e of entries) {
541
+ const r = rel ? `${rel}/${e.name}` : e.name;
542
+ if (e.isSymbolicLink()) {
543
+ stats.filesSkipped++;
544
+ continue;
545
+ }
546
+ if (e.isDirectory()) {
547
+ if (!IGNORE_DIRS.has(e.name)) stack.push(r);
548
+ continue;
549
+ }
550
+ if (e.isFile()) out.push(r);
551
+ }
552
+ }
553
+ return out.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
554
+ }
555
+ async function readText(abs, max) {
556
+ const s = await lstat(abs);
557
+ if (!s.isFile() || s.size > max) return null;
558
+ const fh = await open(abs, "r");
559
+ try {
560
+ const head = Buffer.alloc(Math.min(8192, s.size));
561
+ await fh.read(head, 0, head.length, 0);
562
+ if (head.includes(0)) return null;
563
+ } finally {
564
+ await fh.close();
565
+ }
566
+ return (await readFile(abs)).toString("utf8");
567
+ }
568
+ async function scan(rootDir) {
569
+ const root = path.resolve(rootDir);
570
+ const rs = await lstat(root).catch(() => null);
571
+ if (!rs || !rs.isDirectory()) throw new Error(`not a directory: ${path.basename(rootDir) || rootDir}`);
572
+ const stats = { filesScanned: 0, filesSkipped: 0, redactions: 0, tokensDropped: 0, hostsWithheld: 0 };
573
+ const col = new Collector(stats);
574
+ const imports = [];
575
+ const resolved = [];
576
+ for (const rel of await walk(root, stats)) {
577
+ const base = path.posix.basename(rel);
578
+ const isEnvExample = /^\.env\.(?:example|sample|template)$/.test(base);
579
+ if (base.startsWith(".env") && !isEnvExample || SECRET_FILES.has(base) || /\.(?:pem|key|p12|pfx|jks)$/i.test(base)) {
580
+ stats.filesSkipped++;
581
+ continue;
582
+ }
583
+ if (SKIP_EXT.test(base)) {
584
+ stats.filesSkipped++;
585
+ continue;
586
+ }
587
+ const ext = path.posix.extname(base);
588
+ const isLock = LOCKFILES.has(base);
589
+ let text;
590
+ try {
591
+ text = await readText(path.join(root, rel), isLock ? MAX_LOCK_BYTES : MAX_SOURCE_BYTES);
592
+ } catch {
593
+ text = null;
594
+ }
595
+ if (text === null) {
596
+ stats.filesSkipped++;
597
+ continue;
598
+ }
599
+ if (ext === ".json" && /"schema"\s*:\s*"realmonsters\./.test(text.slice(0, 400))) {
600
+ stats.filesSkipped++;
601
+ continue;
602
+ }
603
+ try {
604
+ const lines = text.split(/\r?\n/);
605
+ if (isLock) {
606
+ const list = base === "pnpm-lock.yaml" ? pnpmLock(text) : base === "yarn.lock" ? yarnLock(text) : base.endsWith(".lock") ? tomlPackageLock(text) : npmPackageLock(text);
607
+ resolved.push(...list.map((r) => ({ ...r, file: rel })));
608
+ stats.filesScanned++;
609
+ continue;
610
+ }
611
+ const fileImports = [...jsImports(rel, text), ...pyImports(rel, text)];
612
+ if (base === "package.json") detectPackageJson(rel, text, col);
613
+ else if (base === "requirements.txt" || /^requirements[-._].*\.txt$/.test(base) || /(?:^|\/)requirements\/[^/]+\.txt$/.test(rel)) detectRequirements(rel, lines, col);
614
+ else if (base === "pyproject.toml") detectPyproject(rel, lines, col);
615
+ else if (base === ".nvmrc" || base === ".node-version") addRuntimeFile(rel, lines[0], "node", col);
616
+ else if (base === ".python-version") addRuntimeFile(rel, lines[0], "python", col);
617
+ else if (base === ".tool-versions") detectToolVersions(rel, lines, col);
618
+ else if (/^dockerfile(?:\.|$)|\.dockerfile$/i.test(base)) detectDockerfile(rel, lines, col);
619
+ else if (/^\.github\/workflows\/[^/]+\.ya?ml$/.test(rel)) detectWorkflow(rel, lines, col);
620
+ else if (ext === ".tf") detectTerraform(rel, lines, col);
621
+ if ((SOURCE_EXT.has(ext) || isEnvExample) && !MANIFESTS.has(base)) {
622
+ for (let i = 0; i < lines.length; i++) {
623
+ const line = lines[i];
624
+ if (line.length > 4e3) continue;
625
+ if (lineLooksSecret(line)) {
626
+ stats.redactions++;
627
+ continue;
628
+ }
629
+ for (const m of line.matchAll(MODEL_RE)) {
630
+ if (/^o[134]/.test(m[1]) && !/["'`]/.test(line[(m.index ?? 0) - 1] ?? "")) continue;
631
+ const id = m[1].replace(/@(\d{8})$/, "-$1");
632
+ col.add("llm-model", modelEcosystem(id), id, rel, i + 1, id);
633
+ }
634
+ if (isEnvExample) continue;
635
+ for (const m of line.matchAll(HOST_RE)) {
636
+ const vendor = publicVendorHost(m[1]);
637
+ if (vendor) col.add("api-host", "other", vendor, rel, i + 1, vendor);
638
+ else stats.hostsWithheld++;
639
+ }
640
+ }
641
+ }
642
+ imports.push(...fileImports);
643
+ stats.filesScanned++;
644
+ } catch {
645
+ stats.filesSkipped++;
646
+ }
647
+ }
648
+ for (const r of resolved) col.setLockVersion(`package:${r.ecosystem}/${r.name}`, r.version, r.file, r.line, `${r.name}@${r.version}`);
649
+ for (const h of imports) for (const eco of ["npm", "pypi"]) col.addCallSite(`package:${eco}/${h.pkg}`, h.site);
650
+ for (const u of col.all()) if (u.kind === "llm-model" || u.kind === "api-host") for (const e of u.evidence) col.addCallSite(u.id, { file: e.file, line: e.line, symbol: e.token });
651
+ const upstreams = col.all();
652
+ const byKind = {};
653
+ for (const u of upstreams) byKind[u.kind] = (byKind[u.kind] ?? 0) + 1;
654
+ return { schema: INVENTORY_SCHEMA, generatedAt: (/* @__PURE__ */ new Date()).toISOString(), root: path.basename(root), scanner: SCANNER, upstreams, stats: { ...stats, byKind } };
655
+ }
656
+ function lineIndex(text) {
657
+ const lines = text.split(/\r?\n/);
658
+ return (needle) => {
659
+ const i = lines.findIndex((l) => l.includes(needle));
660
+ return i < 0 ? 1 : i + 1;
661
+ };
662
+ }
663
+ function detectPackageJson(rel, text, col) {
664
+ let pkg;
665
+ try {
666
+ pkg = JSON.parse(text);
667
+ } catch {
668
+ return;
669
+ }
670
+ if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return;
671
+ const p = pkg;
672
+ const at = lineIndex(text);
673
+ for (const field of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
674
+ const deps = p[field];
675
+ if (!deps || typeof deps !== "object" || Array.isArray(deps)) continue;
676
+ const blockStart = at(`"${field}"`);
677
+ const lines = text.split(/\r?\n/);
678
+ for (const [name, range] of Object.entries(deps)) {
679
+ const v = normalizeVersion(range);
680
+ let line = blockStart;
681
+ for (let i = blockStart; i < lines.length; i++) if (lines[i].includes(`"${name}"`)) {
682
+ line = i + 1;
683
+ break;
684
+ }
685
+ col.add("package", "npm", name, rel, line, v ? `${name}@${v}` : name, v, "manifest");
686
+ }
687
+ }
688
+ const node = p.engines && typeof p.engines === "object" ? normalizeVersion(p.engines.node) : void 0;
689
+ if (node) col.add("runtime", "other", "node", rel, at('"node"'), `node ${node}`, node, "manifest");
690
+ if (typeof p.packageManager === "string") {
691
+ const m = /^([a-z]+)@([\d.]+)/.exec(p.packageManager);
692
+ if (m) col.add("package", "npm", m[1], rel, at('"packageManager"'), `${m[1]}@${m[2]}`, m[2], "manifest");
693
+ }
694
+ }
695
+ var PY_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
696
+ var PY_SPEC = /^(?:===|==|>=|<=|~=|!=|>|<)\s*[A-Za-z0-9.*+!-]+(?:\s*,\s*(?:===|==|>=|<=|~=|!=|>|<)\s*[A-Za-z0-9.*+!-]+)*$/;
697
+ function pyDep(raw) {
698
+ const s = raw.split(";")[0].split("#")[0].trim();
699
+ if (!s || s.startsWith("-") || s.includes("://") || s.includes(" @ ") || s.startsWith(".") || s.startsWith("/")) return null;
700
+ const m = /^([A-Za-z0-9][A-Za-z0-9._-]*)(?:\[[^\]]*\])?\s*(.*)$/.exec(s);
701
+ if (!m || !PY_NAME.test(m[1])) return null;
702
+ const spec = m[2].trim().replace(/^\(|\)$/g, "");
703
+ return { name: m[1].toLowerCase().replace(/_/g, "-"), spec: spec && PY_SPEC.test(spec) ? spec.replace(/\s+/g, "") : void 0 };
704
+ }
705
+ function pyVersion(spec) {
706
+ if (!spec) return void 0;
707
+ return spec.startsWith("==") && !spec.includes(",") ? spec.slice(2) : spec;
708
+ }
709
+ function detectRequirements(rel, lines, col) {
710
+ lines.forEach((raw, i) => {
711
+ const d = pyDep(raw);
712
+ if (d) col.add("package", "pypi", d.name, rel, i + 1, d.spec ? `${d.name}${d.spec}` : d.name, pyVersion(d.spec), "manifest");
713
+ });
714
+ }
715
+ function detectPyproject(rel, lines, col) {
716
+ let section = "", inArray = false;
717
+ lines.forEach((raw, i) => {
718
+ const line = raw.trim();
719
+ const sec = /^\[([^\]]+)\]$/.exec(line);
720
+ if (sec) {
721
+ section = sec[1];
722
+ inArray = false;
723
+ return;
724
+ }
725
+ if (section === "project" && /^requires-python\s*=/.test(line)) {
726
+ const v = normalizeVersion(line.split("=").slice(1).join("=").trim().replace(/^["']|["']$/g, ""));
727
+ if (v) col.add("runtime", "other", "python", rel, i + 1, `python ${v}`, v, "manifest");
728
+ }
729
+ const arrayStart = section === "project" && /^dependencies\s*=\s*\[/.test(line) || (section === "project.optional-dependencies" || section === "dependency-groups") && /^[\w.-]+\s*=\s*\[/.test(line);
730
+ if (arrayStart || inArray) {
731
+ for (const m of line.matchAll(/["']([^"']+)["']/g)) {
732
+ const d = pyDep(m[1]);
733
+ if (d) col.add("package", "pypi", d.name, rel, i + 1, d.spec ? `${d.name}${d.spec}` : d.name, pyVersion(d.spec), "manifest");
734
+ }
735
+ inArray = !line.includes("]");
736
+ return;
737
+ }
738
+ if (/^tool\.poetry(?:\.group\.[\w-]+)?\.(?:dev-)?dependencies$/.test(section)) {
739
+ const m = /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*=\s*(?:"([^"]*)"|\{.*?\bversion\s*=\s*"([^"]*)".*\}|\{.*\})/.exec(line);
740
+ if (!m) return;
741
+ const name = m[1].toLowerCase().replace(/_/g, "-");
742
+ const v = normalizeVersion(m[2] ?? m[3]);
743
+ if (name === "python") {
744
+ if (v) col.add("runtime", "other", "python", rel, i + 1, `python ${v}`, v, "manifest");
745
+ return;
746
+ }
747
+ col.add("package", "pypi", name, rel, i + 1, v ? `${name} ${v}` : name, v, "manifest");
748
+ }
749
+ });
750
+ }
751
+ function addRuntimeFile(rel, first, name, col) {
752
+ const v = /^v?(\d[\w.]*)/.exec((first ?? "").trim())?.[1];
753
+ if (v) col.add("runtime", "other", name, rel, 1, `${name} ${v}`, v, "runtime-file");
754
+ }
755
+ function detectToolVersions(rel, lines, col) {
756
+ lines.forEach((l, i) => {
757
+ const m = /^(nodejs|node|python|postgres|terraform|golang|ruby)\s+v?(\d[\w.]*)/.exec(l.trim());
758
+ if (!m) return;
759
+ const name = m[1] === "nodejs" ? "node" : m[1];
760
+ col.add("runtime", "other", name, rel, i + 1, `${name} ${m[2]}`, m[2], "runtime-file");
761
+ });
762
+ }
763
+ function parseImageRef(ref) {
764
+ if (ref.includes("$")) return null;
765
+ const noDigest = ref.split("@")[0];
766
+ const slash = noDigest.lastIndexOf("/");
767
+ const colon = noDigest.lastIndexOf(":");
768
+ const name = colon > slash ? noDigest.slice(0, colon) : noDigest;
769
+ const tag = colon > slash ? noDigest.slice(colon + 1) : void 0;
770
+ const parts = name.split("/");
771
+ const first = parts[0];
772
+ const hasRegistry = parts.length > 1 && (first.includes(".") || first.includes(":") || first === "localhost");
773
+ if (hasRegistry && !PUBLIC_REGISTRIES.has(first.toLowerCase())) return { image: `private-registry/${parts[parts.length - 1]}`, tag };
774
+ const image = hasRegistry ? parts.slice(1).join("/") : name;
775
+ return { image: image.replace(/^library\//, ""), tag };
776
+ }
777
+ function detectDockerfile(rel, lines, col) {
778
+ const stages = /* @__PURE__ */ new Set();
779
+ lines.forEach((raw, i) => {
780
+ const m = /^\s*FROM\s+(?:--platform=\S+\s+)?(\S+)(?:\s+AS\s+(\S+))?/i.exec(raw);
781
+ if (!m) return;
782
+ const ref = m[1];
783
+ const isStageRef = stages.has(ref.toLowerCase());
784
+ if (m[2]) stages.add(m[2].toLowerCase());
785
+ if (ref === "scratch" || isStageRef) return;
786
+ const p = parseImageRef(ref);
787
+ if (!p) return;
788
+ col.add("container-image", "docker", p.image, rel, i + 1, p.tag ? `${p.image}:${p.tag}` : p.image, p.tag, "dockerfile");
789
+ const rt = BASE_RUNTIME[p.image.split("/").pop() ?? p.image];
790
+ const v = p.tag?.split("-")[0];
791
+ if (rt && v && /^\d/.test(v)) col.add("runtime", "other", rt, rel, i + 1, `${p.image}:${p.tag}`, v, "inferred");
792
+ });
793
+ }
794
+ function detectWorkflow(rel, lines, col) {
795
+ lines.forEach((raw, i) => {
796
+ const u = /^\s*-?\s*uses:\s*["']?([^\s"'#]+)/.exec(raw);
797
+ if (u && !u[1].startsWith("./") && !u[1].startsWith("docker://") && !u[1].includes("${{")) {
798
+ const [action, ref] = u[1].split("@");
799
+ col.add("github-action", "github-actions", action, rel, i + 1, ref ? `${action}@${ref}` : action, ref, "manifest");
800
+ }
801
+ const r = /^\s*runs-on:\s*(.+)$/.exec(raw);
802
+ if (r) {
803
+ const val = r[1].split("#")[0].trim().replace(/^["']|["']$/g, "");
804
+ if (!val.includes("${{")) {
805
+ const labels = val.replace(/^\[|\]$/g, "").split(",").map((s) => s.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
806
+ const name = labels.find((l) => GH_HOSTED_RUNNER.test(l)) ?? "self-hosted";
807
+ col.add("github-runner", "github-actions", name, rel, i + 1, name);
808
+ }
809
+ }
810
+ const n = /^\s*node-version:\s*["']?(\d[\w.]*)/.exec(raw);
811
+ if (n) col.add("runtime", "other", "node", rel, i + 1, `node ${n[1]}`, n[1], "manifest");
812
+ const p = /^\s*python-version:\s*["']?(\d[\w.]*)/.exec(raw);
813
+ if (p) col.add("runtime", "other", "python", rel, i + 1, `python ${p[1]}`, p[1], "manifest");
814
+ });
815
+ }
816
+ function detectTerraform(rel, lines, col) {
817
+ let depth = 0;
818
+ let block = {};
819
+ const flush = () => {
820
+ if (block.engine) {
821
+ const name = BASE_RUNTIME[block.engine] ?? block.engine;
822
+ col.add("runtime", "other", name, rel, block.versionLine ?? block.engineLine ?? 1, block.version ? `${name} ${block.version}` : name, block.version, "terraform");
823
+ }
824
+ block = {};
825
+ };
826
+ lines.forEach((raw, i) => {
827
+ const line = raw.split("#")[0];
828
+ const rt = /^\s*runtime\s*=\s*"([a-z]+?)(\d+(?:\.\d+)*)(?:\.x)?"/.exec(line);
829
+ if (rt) {
830
+ const name = BASE_RUNTIME[rt[1]] ?? rt[1];
831
+ col.add("runtime", "other", name, rel, i + 1, `${rt[1]}${rt[2]}`, rt[2], "terraform");
832
+ }
833
+ const en = /^\s*engine\s*=\s*"([a-z0-9-]+)"/.exec(line);
834
+ if (en) {
835
+ block.engine = en[1];
836
+ block.engineLine = i + 1;
837
+ }
838
+ const ev = /^\s*engine_version\s*=\s*"([\d.]+)"/.exec(line);
839
+ if (ev) {
840
+ block.version = ev[1];
841
+ block.versionLine = i + 1;
842
+ }
843
+ const im = /^\s*image\s*=\s*"([^"]+)"/.exec(line);
844
+ if (im) {
845
+ const p = parseImageRef(im[1]);
846
+ if (p) col.add("container-image", "docker", p.image, rel, i + 1, p.tag ? `${p.image}:${p.tag}` : p.image, p.tag, "terraform");
847
+ }
848
+ for (const ch of line) {
849
+ if (ch === "{") depth++;
850
+ else if (ch === "}") {
851
+ depth--;
852
+ if (depth === 0) flush();
853
+ }
854
+ }
855
+ });
856
+ flush();
857
+ }
858
+
859
+ export {
860
+ SCANNER,
861
+ normalizeVersion,
862
+ scan,
863
+ parseImageRef
864
+ };
package/dist/cli.js ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ SCANNER,
4
+ scan
5
+ } from "./chunk-SXEJJ3CT.js";
6
+
7
+ // src/cli.ts
8
+ import { writeFile } from "node:fs/promises";
9
+ var HELP = `${SCANNER.name} ${SCANNER.version}
10
+ Usage: realmonsters-scan <dir> [--out <file>] [--quiet]
11
+
12
+ Builds inventory.json for a repository: packages, runtimes, container images,
13
+ GitHub Actions, LLM model IDs and public vendor API hosts, each with file:line evidence.
14
+ Runs entirely offline. Never copies source lines; withholds anything that looks like a
15
+ secret and any hostname that is not a known public vendor. Read the output before you
16
+ send it anywhere.
17
+ `;
18
+ function parse(argv) {
19
+ const r = { quiet: false, help: false };
20
+ for (let i = 0; i < argv.length; i++) {
21
+ const a = argv[i];
22
+ if (a === "--help" || a === "-h") r.help = true;
23
+ else if (a === "--quiet" || a === "-q") r.quiet = true;
24
+ else if (a === "--out" || a === "-o") {
25
+ r.out = argv[++i];
26
+ if (!r.out) r.error = "--out needs a file name";
27
+ } else if (a.startsWith("-")) r.error = `unknown option ${a}`;
28
+ else if (r.dir) r.error = `unexpected argument ${a}`;
29
+ else r.dir = a;
30
+ }
31
+ return r;
32
+ }
33
+ var args = parse(process.argv.slice(2));
34
+ if (args.help || !args.dir && !args.error) {
35
+ process.stdout.write(HELP);
36
+ process.exit(args.help ? 0 : 2);
37
+ }
38
+ if (args.error) {
39
+ process.stderr.write(`realmonsters-scan: ${args.error}
40
+ `);
41
+ process.exit(2);
42
+ }
43
+ try {
44
+ const inv = await scan(args.dir);
45
+ const json = JSON.stringify(inv, null, 2) + "\n";
46
+ if (args.out) await writeFile(args.out, json);
47
+ else process.stdout.write(json);
48
+ if (!args.quiet) {
49
+ const s = inv.stats;
50
+ process.stderr.write(
51
+ `${SCANNER.name}: ${inv.upstreams.length} watched upstreams in ${inv.root} (${Object.entries(s.byKind).map(([a, b]) => `${b} ${a}`).join(", ")}); ${s.filesScanned} files scanned, ${s.filesSkipped} skipped, ${s.redactions} redacted, ${s.hostsWithheld} non-vendor hosts withheld` + (args.out ? ` \u2192 ${args.out}` : "") + "\n"
52
+ );
53
+ }
54
+ } catch (e) {
55
+ process.stderr.write(`realmonsters-scan: ${e instanceof Error ? e.message : "scan failed"}
56
+ `);
57
+ process.exit(1);
58
+ }
package/dist/scan.js ADDED
@@ -0,0 +1,12 @@
1
+ import {
2
+ SCANNER,
3
+ normalizeVersion,
4
+ parseImageRef,
5
+ scan
6
+ } from "./chunk-SXEJJ3CT.js";
7
+ export {
8
+ SCANNER,
9
+ normalizeVersion,
10
+ parseImageRef,
11
+ scan
12
+ };
package/package.json CHANGED
@@ -1,6 +1,44 @@
1
1
  {
2
2
  "name": "@realmonsters/scan",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.2",
4
+ "description": "realMonsters local inventory scanner. Runs offline; writes inventory.json (paths, lines, symbols, versions). Never source, never secrets.",
5
+ "type": "module",
6
+ "bin": {
7
+ "realmonsters-scan": "dist/cli.js"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "types": "./src/scan.ts",
12
+ "default": "./dist/scan.js"
13
+ }
14
+ },
15
+ "license": "UNLICENSED",
16
+ "dependencies": {
17
+ "typescript": "5.9.3"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "engines": {
24
+ "node": ">=22"
25
+ },
26
+ "scripts": {
27
+ "build": "node build.mjs"
28
+ },
29
+ "devDependencies": {
30
+ "@realmonsters/core": "0.0.1"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/realmonsters-hq/realmonsters.git"
35
+ },
36
+ "homepage": "https://realmonsters.dev",
37
+ "keywords": [
38
+ "inventory",
39
+ "dependencies",
40
+ "deprecation",
41
+ "eol",
42
+ "realmonsters"
43
+ ]
44
+ }