weavatrix 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/bin/weavatrix-mcp.mjs +6 -0
- package/package.json +50 -0
- package/skill/SKILL.md +56 -0
- package/src/analysis/coverage-reports.js +232 -0
- package/src/analysis/dead-check.js +136 -0
- package/src/analysis/dep-check.js +310 -0
- package/src/analysis/dep-rules.js +221 -0
- package/src/analysis/duplicates-worker.js +11 -0
- package/src/analysis/duplicates.compute.js +215 -0
- package/src/analysis/duplicates.js +15 -0
- package/src/analysis/duplicates.run.js +51 -0
- package/src/analysis/duplicates.tokenize.js +182 -0
- package/src/analysis/endpoints.js +156 -0
- package/src/analysis/findings.js +52 -0
- package/src/analysis/graph-analysis.aggregate.js +280 -0
- package/src/analysis/graph-analysis.js +7 -0
- package/src/analysis/graph-analysis.refs.js +146 -0
- package/src/analysis/graph-analysis.summaries.js +62 -0
- package/src/analysis/internal-audit.collect.js +117 -0
- package/src/analysis/internal-audit.js +9 -0
- package/src/analysis/internal-audit.reach.js +47 -0
- package/src/analysis/internal-audit.run.js +189 -0
- package/src/analysis/manifests.js +169 -0
- package/src/analysis/source-complexity.ast.js +97 -0
- package/src/analysis/source-complexity.constants.js +55 -0
- package/src/analysis/source-complexity.js +14 -0
- package/src/analysis/source-complexity.report.js +76 -0
- package/src/analysis/source-complexity.walk.js +174 -0
- package/src/build-graph.js +102 -0
- package/src/config.js +5 -0
- package/src/graph/build-worker.js +30 -0
- package/src/graph/builder/lang-csharp.js +40 -0
- package/src/graph/builder/lang-css.js +29 -0
- package/src/graph/builder/lang-go.js +53 -0
- package/src/graph/builder/lang-html.js +29 -0
- package/src/graph/builder/lang-java.js +29 -0
- package/src/graph/builder/lang-js.js +168 -0
- package/src/graph/builder/lang-python.js +78 -0
- package/src/graph/builder/lang-rust.js +45 -0
- package/src/graph/builder/spec-pkg.js +87 -0
- package/src/graph/graph-filter.js +44 -0
- package/src/graph/internal-builder.build.js +217 -0
- package/src/graph/internal-builder.js +12 -0
- package/src/graph/internal-builder.langs.js +86 -0
- package/src/graph/internal-builder.resolvers.js +116 -0
- package/src/graph/layout.js +35 -0
- package/src/infra/infra-items.js +255 -0
- package/src/infra/infra-registry.js +184 -0
- package/src/infra/infra.detect.js +119 -0
- package/src/infra/infra.js +38 -0
- package/src/infra/infra.match.js +102 -0
- package/src/infra/infra.scan.js +93 -0
- package/src/mcp/catalog.mjs +68 -0
- package/src/mcp/graph-context.mjs +276 -0
- package/src/mcp/tools-actions.mjs +132 -0
- package/src/mcp/tools-graph.mjs +274 -0
- package/src/mcp/tools-health.mjs +209 -0
- package/src/mcp/tools-impact.mjs +189 -0
- package/src/mcp-rg.mjs +51 -0
- package/src/mcp-server.mjs +171 -0
- package/src/mcp-source-tools.mjs +139 -0
- package/src/process.js +77 -0
- package/src/scan/discover.inventory.js +78 -0
- package/src/scan/discover.js +5 -0
- package/src/scan/discover.list.js +79 -0
- package/src/scan/discover.stack.js +227 -0
- package/src/scan/search.core.js +24 -0
- package/src/scan/search.git.js +102 -0
- package/src/scan/search.js +9 -0
- package/src/scan/search.node.js +83 -0
- package/src/scan/search.preview.js +49 -0
- package/src/scan/search.rg.js +145 -0
- package/src/security/advisory-store.js +177 -0
- package/src/security/installed.js +247 -0
- package/src/security/malware-file-heuristics.js +121 -0
- package/src/security/malware-heuristics.exclusions.js +134 -0
- package/src/security/malware-heuristics.js +15 -0
- package/src/security/malware-heuristics.roots.js +142 -0
- package/src/security/malware-heuristics.scan.js +87 -0
- package/src/security/malware-heuristics.sweep.js +122 -0
- package/src/security/malware-scoring.js +84 -0
- package/src/security/match.js +85 -0
- package/src/security/registry-sig.classify.js +136 -0
- package/src/security/registry-sig.js +18 -0
- package/src/security/registry-sig.rules.js +188 -0
- package/src/security/typosquat.js +72 -0
- package/src/util.js +27 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// infra-items.js — extract concrete infra items (DB tables, cache keysets, queue topics, cloud files, SQL
|
|
2
|
+
// tables, env-declared endpoints) from source text for the detected services. Split out of infra.js; it owns
|
|
3
|
+
// the shared leaf helpers (lc/safeRead/size caps) as the lower module — infra.js imports them back.
|
|
4
|
+
import { safeRead, MAX_FILE_BYTES } from "../util.js";
|
|
5
|
+
|
|
6
|
+
const IMPORT_SCAN_MAX_FILES = 2000; // cap the synchronous import-scan pass (connector attribution only) so the
|
|
7
|
+
const lc = (s) => String(s || "").toLowerCase();
|
|
8
|
+
|
|
9
|
+
export { safeRead };
|
|
10
|
+
|
|
11
|
+
const ITEM_META = {
|
|
12
|
+
db: { label: "TABLES", unit: "rows" },
|
|
13
|
+
ts: { label: "SERIES", unit: "pts" },
|
|
14
|
+
cache: { label: "KEYSETS", unit: "keys" },
|
|
15
|
+
queue: { label: "TOPICS", unit: "msg/s" },
|
|
16
|
+
cloud: { label: "FILES", unit: "KB" },
|
|
17
|
+
fs: { label: "FILES", unit: "KB" },
|
|
18
|
+
logs: { label: "STREAMS", unit: "l/s" },
|
|
19
|
+
api: { label: "ENDPOINTS", unit: "req/s" },
|
|
20
|
+
};
|
|
21
|
+
const ITEM_VALUE = { db: 42, ts: 120, cache: 700, queue: 22, cloud: 120, fs: 80, logs: 55, api: 24 };
|
|
22
|
+
const SQL_RESERVED = new Set([
|
|
23
|
+
"select", "from", "where", "join", "left", "right", "inner", "outer", "full", "cross",
|
|
24
|
+
"on", "and", "or", "by", "group", "order", "limit", "offset", "as", "with", "values",
|
|
25
|
+
"set", "returning", "true", "false", "null", "undefined", "if", "case", "when", "then",
|
|
26
|
+
"else", "end", "having", "over", "partition", "distinct", "the", "is",
|
|
27
|
+
"count", "sum", "avg", "min", "max", "uniq", "array", "date", "datetime", "string", "number",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export function itemMetaFor(service) {
|
|
31
|
+
if (service.id === "mongodb") return { label: "COLLECTIONS", unit: "docs" };
|
|
32
|
+
if (service.id === "dynamodb") return { label: "TABLES", unit: "items" };
|
|
33
|
+
if (service.id === "elasticsearch" || service.id === "solr" || service.id === "meilisearch" || service.id === "typesense") {
|
|
34
|
+
return { label: "INDEXES", unit: "docs" };
|
|
35
|
+
}
|
|
36
|
+
return ITEM_META[service.kind] || { label: "ITEMS", unit: "" };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function cleanInfraItemName(raw) {
|
|
40
|
+
const name = String(raw || "").trim().replace(/^(?:["'`]|\[)+|(?:["'`]|])+$/g, "").replace(/[;,]+$/g, "").trim();
|
|
41
|
+
if (!name || name.length < 2 || name.length > 64) return "";
|
|
42
|
+
if (name.includes("://") || name.includes("${") || name.includes("#{")) return "";
|
|
43
|
+
if (/[\s(){}+$]/.test(name)) return "";
|
|
44
|
+
if (!/^[A-Za-z0-9_./:\-*]+$/.test(name)) return "";
|
|
45
|
+
if (/^\d+$/.test(name) || SQL_RESERVED.has(name.toLowerCase())) return "";
|
|
46
|
+
return name;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function addInfraItem(map, raw, weight = 1, sourcePath = "", op = "") {
|
|
50
|
+
const name = cleanInfraItemName(raw);
|
|
51
|
+
if (!name) return;
|
|
52
|
+
const cur = map.get(name) || { name, refs: 0, files: new Set(), ops: new Set() };
|
|
53
|
+
cur.refs += Math.max(1, weight);
|
|
54
|
+
if (sourcePath) cur.files.add(sourcePath);
|
|
55
|
+
if (op) cur.ops.add(op);
|
|
56
|
+
map.set(name, cur);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// One regex-walk adder factory shared by the two matchers below — the only difference is how a
|
|
60
|
+
// captured group becomes items (a single name vs a string-array body).
|
|
61
|
+
const matchAdder = (add) => (map, text, re, group = 1, weight = 1, sourcePath = "", op = "") => {
|
|
62
|
+
for (const m of text.matchAll(re)) add(map, m[group], weight, sourcePath, op);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function addStringArrayItems(map, raw, weight = 1, sourcePath = "", op = "") {
|
|
66
|
+
const body = String(raw || "");
|
|
67
|
+
for (const m of body.matchAll(/["'`]([^"'`]+)["'`]/g)) addInfraItem(map, m[1], weight, sourcePath, op);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const addMatches = matchAdder(addInfraItem);
|
|
71
|
+
const addStructArrayMatches = matchAdder(addStringArrayItems);
|
|
72
|
+
|
|
73
|
+
function addSqlTableMatches(map, text, re, cteAliases, group = 1, weight = 1, sourcePath = "", op = "") {
|
|
74
|
+
for (const m of String(text || "").matchAll(re)) {
|
|
75
|
+
const name = String(m[group] || "");
|
|
76
|
+
if (cteAliases.has(name.toLowerCase())) continue;
|
|
77
|
+
addInfraItem(map, name, weight, sourcePath, op);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function looksLikeSqlSnippet(raw) {
|
|
82
|
+
const text = String(raw || "").replace(/\$\{[\s\S]*?\}/g, " ");
|
|
83
|
+
if (!/[A-Za-z]/.test(text)) return false;
|
|
84
|
+
const statementStart = "(?:^|[\\r\\n;])\\s*";
|
|
85
|
+
return (
|
|
86
|
+
new RegExp(`${statementStart}(?:select|with)\\b[\\s\\S]{0,600}\\bfrom\\b`, "i").test(text) ||
|
|
87
|
+
new RegExp(`${statementStart}(?:insert\\s+into|delete\\s+from|alter\\s+table|create\\s+(?:or\\s+replace\\s+)?table|truncate\\s+table|update\\s+["'\`\\[]?[A-Za-z_][\\w$]*(?:\\.[A-Za-z_][\\w$]*)?["'\`\\]]?\\s+set)\\b`, "i").test(text)
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function stringLiteralBody(match) {
|
|
92
|
+
for (let i = 1; i < match.length; i++) if (match[i] != null) return match[i];
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function extractSqlSnippets(text) {
|
|
97
|
+
const out = [];
|
|
98
|
+
const src = String(text || "");
|
|
99
|
+
const add = (body) => {
|
|
100
|
+
const snippet = String(body || "");
|
|
101
|
+
if (looksLikeSqlSnippet(snippet)) out.push(snippet);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
for (const match of src.matchAll(/"""([\s\S]*?)"""|'''([\s\S]*?)'''/g)) add(stringLiteralBody(match));
|
|
105
|
+
for (const match of src.matchAll(/`([\s\S]*?)`|"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)'/g)) add(stringLiteralBody(match));
|
|
106
|
+
return out.join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function sqlCteAliases(text) {
|
|
110
|
+
const out = new Set();
|
|
111
|
+
for (const match of String(text || "").matchAll(/(?:\bwith|,)\s+([A-Za-z_][\w$]*)\s+as\s*\(/gi)) {
|
|
112
|
+
out.add(String(match[1] || "").toLowerCase());
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function hasServiceHint(service, text) {
|
|
118
|
+
const lower = lc(text);
|
|
119
|
+
const tokens = [service.id, service.name, ...(service.imports || []), ...(service.deps || [])]
|
|
120
|
+
.map((x) => lc(x))
|
|
121
|
+
.filter((x) => x && x.length > 2);
|
|
122
|
+
return tokens.some((token) => lower.includes(token));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function finalizeInfraItems(map, kind) {
|
|
126
|
+
const base = ITEM_VALUE[kind] || 40;
|
|
127
|
+
return [...map.values()]
|
|
128
|
+
.sort((a, b) => b.refs - a.refs || a.name.localeCompare(b.name))
|
|
129
|
+
.slice(0, 24)
|
|
130
|
+
.map((item) => ({
|
|
131
|
+
name: item.name,
|
|
132
|
+
val: Math.round(Math.max(4, base * (1 + Math.log2(item.refs + 1) * 0.55))),
|
|
133
|
+
health: Math.max(0.42, Math.min(0.96, 0.94 - Math.log2(item.refs + 1) * 0.08)),
|
|
134
|
+
refs: item.refs,
|
|
135
|
+
fileCount: item.files ? item.files.size : 0,
|
|
136
|
+
files: item.files ? [...item.files].sort().slice(0, 12) : [],
|
|
137
|
+
ops: item.ops ? [...item.ops].sort().slice(0, 12) : [],
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function extractInfraItemsForService(service, text, out, sourcePath = "") {
|
|
142
|
+
const id = service.id;
|
|
143
|
+
const kind = service.kind;
|
|
144
|
+
const hit = (re, group = 1, weight = 1, op = "") => addMatches(out, text, re, group, weight, sourcePath, op);
|
|
145
|
+
const hitSql = (target, re, cteAliases, group = 1, weight = 1, op = "") => addSqlTableMatches(out, target, re, cteAliases, group, weight, sourcePath, op);
|
|
146
|
+
const hitArrays = (re, group = 1, weight = 1, op = "") => addStructArrayMatches(out, text, re, group, weight, sourcePath, op);
|
|
147
|
+
// Go/CLI services often carry their topics/keyspaces as FLAG DEFAULTS, not literals at the call
|
|
148
|
+
// site (bgp-speaker: flag.String("bgp_update_kafka_topic", "bgp_updates", …), flag.String(
|
|
149
|
+
// "redis_key_base", "bgp:mitigators", …)) — the flag NAME says what the default VALUE is.
|
|
150
|
+
const hitFlagDefaults = (nameRe, weight = 2, op = "configured") => {
|
|
151
|
+
for (const m of text.matchAll(/\b(?:flag|pflag)\.String(?:Var)?\s*\(\s*(?:&[\w.]+\s*,\s*)?"([^"]+)"\s*,\s*"([^"]+)"/g)) {
|
|
152
|
+
if (nameRe.test(m[1])) addInfraItem(out, m[2], weight, sourcePath, op);
|
|
153
|
+
}
|
|
154
|
+
for (const m of text.matchAll(/\badd_argument\s*\(\s*["']--?([^"']+)["'][^)]{0,200}?\bdefault\s*=\s*["']([^"']+)["']/g)) {
|
|
155
|
+
if (nameRe.test(m[1])) addInfraItem(out, m[2], weight, sourcePath, op);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
if (id === "mongodb") {
|
|
160
|
+
hit(/\.collection\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/gi, 1, 2);
|
|
161
|
+
hit(/\.Collection\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g, 1, 2);
|
|
162
|
+
hit(/\bgetCollection\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/gi, 1, 2);
|
|
163
|
+
hit(/\bcollection\s*:\s*["'`]([^"'`]+)["'`]/gi);
|
|
164
|
+
hit(/\b(?:mongoose\.)?model\s*\(\s*["'`]([^"'`]+)["'`]/gi);
|
|
165
|
+
hit(/\bdb\s*\[\s*["'`]([^"'`]+)["'`]\s*\]/gi);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (id === "dynamodb") {
|
|
170
|
+
hit(/\bTableName\s*:\s*["'`]([^"'`]+)["'`]/g, 1, 2);
|
|
171
|
+
hit(/\btableName\s*[:=]\s*["'`]([^"'`]+)["'`]/g, 1, 2);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (id === "elasticsearch" || id === "solr" || id === "meilisearch" || id === "typesense") {
|
|
175
|
+
hit(/\bindex(?:Name)?\s*[:=]\s*["'`]([^"'`]+)["'`]/gi, 1, 2);
|
|
176
|
+
hit(/\.index\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/gi, 1, 2);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (kind === "db" || kind === "ts") {
|
|
180
|
+
const sqlText = extractSqlSnippets(text);
|
|
181
|
+
if (sqlText) {
|
|
182
|
+
const cteAliases = sqlCteAliases(sqlText);
|
|
183
|
+
hitSql(sqlText, /\b(?:insert\s+into|delete\s+from|alter\s+table|create\s+(?:or\s+replace\s+)?table|truncate\s+table)\s+(?:["'`]|\[)?([A-Za-z_][\w$]*(?:\.[A-Za-z_][\w$]*)?)(?:["'`]|])?/gi, cteAliases, 1, 2);
|
|
184
|
+
hitSql(sqlText, /\b(?:from|join|into|update|table)\s+(?:["'`]|\[)?([A-Za-z_][\w$]*(?:\.[A-Za-z_][\w$]*)?)(?:["'`]|])?/gi, cteAliases);
|
|
185
|
+
}
|
|
186
|
+
if (id !== "clickhouse" && hasServiceHint(service, text)) {
|
|
187
|
+
hit(/\b(?:table|tableName|measurement|bucket)\s*[:=]\s*["'`]([^"'`]+)["'`]/gi);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (kind === "cache") {
|
|
192
|
+
hit(/\b(?:get|getBuffer|getdel|exists|ttl|pttl)\s*\(\s*(?:ctx\s*,\s*)?["'`]([^"'`]+)["'`]/gi, 1, 1, "read");
|
|
193
|
+
hit(/\b(?:set|setex|psetex|setnx|incr|decr|expire|persist)\s*\(\s*(?:ctx\s*,\s*)?["'`]([^"'`]+)["'`]/gi, 1, 2, "write");
|
|
194
|
+
hit(/\b(?:del|delete|unlink|expireat|pexpireat)\s*\(\s*(?:ctx\s*,\s*)?["'`]([^"'`]+)["'`]/gi, 1, 2, "delete");
|
|
195
|
+
hit(/\b(?:hget|hmget|hgetall|hexists)\s*\(\s*(?:ctx\s*,\s*)?["'`]([^"'`]+)["'`]/gi, 1, 1, "hash read");
|
|
196
|
+
hit(/\b(?:hset|hmset|hdel)\s*\(\s*(?:ctx\s*,\s*)?["'`]([^"'`]+)["'`]/gi, 1, 2, "hash write");
|
|
197
|
+
hit(/\b(?:lpush|rpush|lpop|rpop|sadd|srem|zadd|zrem)\s*\(\s*(?:ctx\s*,\s*)?["'`]([^"'`]+)["'`]/gi, 1, 2, "collection");
|
|
198
|
+
hit(/\b(?:xadd|xread|xreadgroup|xgroup)\s*\(\s*(?:ctx\s*,\s*)?["'`]([^"'`]+)["'`]/gi, 1, 2, "stream");
|
|
199
|
+
hit(/\b(?:publish|subscribe|psubscribe)\s*\(\s*(?:ctx\s*,\s*)?["'`]([^"'`]+)["'`]/gi, 1, 1, "pub/sub");
|
|
200
|
+
hit(/\b(?:cacheKey|redisKey|valkeyKey|redisPrefix|valkeyPrefix|keyPrefix|lockKey|rateLimitKey|sessionKey)\s*[:=]\s*["'`]([^"'`]+)["'`]/gi, 1, 2, "key pattern");
|
|
201
|
+
hitArrays(/\b(?:Streams|streams)\s*:\s*\[\]string\s*\{([^}]+)\}/g, 1, 2, "stream");
|
|
202
|
+
hitFlagDefaults(/key|keyspace|prefix/i, 2, "key pattern");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (kind === "queue") {
|
|
206
|
+
hit(/\b(?:topic|topics|queue|subject|stream|channel|Topic|Queue|Subject|Stream|Channel)\s*[:=]\s*["'`]([^"'`]+)["'`]/g, 1, 2, "configured");
|
|
207
|
+
hit(/\b(?:publish|produce|send|sendMessage|writeMessage|writeMessages|emit)\s*\(\s*["'`]([^"'`]+)["'`]/gi, 1, 2, "produce");
|
|
208
|
+
hit(/\b(?:subscribe|consume|consumer|listen|readMessage|readMessages)\s*\(\s*["'`]([^"'`]+)["'`]/gi, 1, 1, "consume");
|
|
209
|
+
hit(/\b(?:producer\.send|sendBatch)\s*\(\s*\{[\s\S]{0,500}?\btopic\s*:\s*["'`]([^"'`]+)["'`]/gi, 1, 2, "produce");
|
|
210
|
+
hit(/\bconsumer\.subscribe\s*\(\s*\{[\s\S]{0,320}?\btopic\s*:\s*["'`]([^"'`]+)["'`]/gi, 1, 2, "consume");
|
|
211
|
+
hit(/\b(?:KafkaConsumer|NewConsumer|NewReader|NewWriter|NewTopic)\s*\(\s*["'`]([^"'`]+)["'`]/g, 1, 2, "configured");
|
|
212
|
+
hit(/@KafkaListener\s*\([^)]*\btopics?\s*=\s*["'`]([^"'`]+)["'`]/g, 1, 2, "consume");
|
|
213
|
+
hit(/\bkafkaTemplate\.send\s*\(\s*["'`]([^"'`]+)["'`]/g, 1, 2, "produce");
|
|
214
|
+
hitArrays(/\b(?:topics|Topics|GroupTopics|SubscribeTopics|streams|Streams)\s*[:=]\s*(?:\[\]string)?\s*\{([^}]+)\}/g, 1, 2, "configured");
|
|
215
|
+
hitArrays(/\b(?:subscribe|Consume|SubscribeTopics)\s*\(\s*(?:ctx\s*,\s*)?\[\]string\s*\{([^}]+)\}/g, 1, 2, "consume");
|
|
216
|
+
hitFlagDefaults(/topic|queue|subject|stream|channel/i, 2, "configured");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (kind === "cloud" || kind === "fs") {
|
|
220
|
+
hit(/\b(?:bucket|bucketName|container|containerName)\s*[:=]\s*["'`]([^"'`]+)["'`]/gi, 1, 2);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function collectInfraItems(scan, entries, filesByService) {
|
|
225
|
+
const result = new Map();
|
|
226
|
+
const allFiles = (scan.codeFiles || []).slice(0, IMPORT_SCAN_MAX_FILES);
|
|
227
|
+
const textCache = new Map();
|
|
228
|
+
const readText = (f) => {
|
|
229
|
+
if (!f || !f.full) return "";
|
|
230
|
+
if (!textCache.has(f.full)) textCache.set(f.full, safeRead(f.full));
|
|
231
|
+
return textCache.get(f.full);
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
for (const { service } of entries) {
|
|
235
|
+
const meta = itemMetaFor(service);
|
|
236
|
+
const wanted = filesByService.get(service.id);
|
|
237
|
+
const preferred = wanted && wanted.size ? allFiles.filter((f) => wanted.has(f.path)) : [];
|
|
238
|
+
const candidateFiles = preferred.length ? [...preferred, ...allFiles.filter((f) => !wanted.has(f.path))] : allFiles;
|
|
239
|
+
const itemMap = new Map();
|
|
240
|
+
for (const f of candidateFiles) {
|
|
241
|
+
const text = readText(f);
|
|
242
|
+
if (!text) continue;
|
|
243
|
+
extractInfraItemsForService(service, text, itemMap, f.path);
|
|
244
|
+
}
|
|
245
|
+
result.set(service.id, {
|
|
246
|
+
itemLabel: meta.label,
|
|
247
|
+
unit: meta.unit,
|
|
248
|
+
items: finalizeInfraItems(itemMap, service.kind),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
export { MAX_FILE_BYTES, IMPORT_SCAN_MAX_FILES, lc };
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// Infra signature registry — generated by the infra-signature-registry workflow (parallel research across
|
|
2
|
+
// Node/Python/Go/JVM/Ruby/PHP/.NET/Rust + docker/k8s/env conventions, then adversarial false-positive +
|
|
3
|
+
// coverage + ground-truth verification, then a category-split synthesis). One row per backing service.
|
|
4
|
+
// See main/repos/infra.js for matcher semantics: deps=exact manifest dep, images=path-segment suffix,
|
|
5
|
+
// envPrefixes=KEY prefix (envWeak entries also require an infra suffix), imports=quoted source token.
|
|
6
|
+
// ORMs / AWS-SDK monoliths / instrumentation libs / generic env prefixes were deliberately EXCLUDED as
|
|
7
|
+
// non-attributing. Import tokens are normalized to bare module paths. Edit this file to tune; regenerate
|
|
8
|
+
// by re-running the workflow (.claude/.../infra-signature-registry).
|
|
9
|
+
export const INFRA_SERVICES = [
|
|
10
|
+
{ id: "postgres", name: "POSTGRES", kind: "db", color: 0x4fd1ff,
|
|
11
|
+
deps: ["pg","pg-promise","postgres","@neondatabase/serverless","psycopg2-binary","asyncpg","github.com/jackc/pgx/v5","org.postgresql:postgresql"],
|
|
12
|
+
imports: ["pg-promise","@neondatabase/serverless","psycopg2","asyncpg","github.com/lib/pq","github.com/jackc/pgx","org.postgresql","tokio_postgres"], images: ["postgres","bitnami/postgresql","postgis/postgis","supabase/postgres","pgvector/pgvector"], envPrefixes: ["POSTGRES","POSTGRESQL","PGHOST","PGUSER","PGPASSWORD","PGDATABASE","POSTGRES_HOST"], envWeak: [] },
|
|
13
|
+
{ id: "mysql", name: "MYSQL", kind: "db", color: 0x4fc6ff,
|
|
14
|
+
deps: ["mysql2","pymysql","mysqlclient","aiomysql","asyncmy","github.com/go-sql-driver/mysql","com.mysql:mysql-connector-j","MySqlConnector"],
|
|
15
|
+
imports: ["mysql2","pymysql","MySQLdb","aiomysql","asyncmy","github.com/go-sql-driver/mysql","com.mysql.cj","mysql_async"], images: ["mysql","bitnami/mysql","mysql/mysql-server","percona"], envPrefixes: ["MYSQL_HOST","MYSQL_PORT","MYSQL_USER","MYSQL_PASSWORD","MYSQL_DATABASE","MYSQL_ROOT_PASSWORD"], envWeak: [] },
|
|
16
|
+
{ id: "mariadb", name: "MARIADB", kind: "db", color: 0x4fbfe6,
|
|
17
|
+
deps: ["mariadb","org.mariadb.jdbc:mariadb-java-client","org.mariadb:r2dbc-mariadb"],
|
|
18
|
+
imports: ["mariadb","org.mariadb.jdbc","org.mariadb.r2dbc"], images: ["mariadb","bitnami/mariadb"], envPrefixes: ["MARIADB_HOST","MARIADB_USER","MARIADB_PASSWORD","MARIADB_DATABASE","MARIADB_ROOT_PASSWORD"], envWeak: [] },
|
|
19
|
+
{ id: "sqlserver", name: "SQLSERVER", kind: "db", color: 0x5fb0ff,
|
|
20
|
+
deps: ["mssql","tedious","pymssql","github.com/microsoft/go-mssqldb","com.microsoft.sqlserver:mssql-jdbc","tiberius","io.r2dbc:r2dbc-mssql"],
|
|
21
|
+
imports: ["mssql","tedious","pymssql","github.com/microsoft/go-mssqldb","com.microsoft.sqlserver.jdbc","tiberius","io.r2dbc.mssql"], images: ["mcr.microsoft.com/mssql/server","mcr.microsoft.com/azure-sql-edge"], envPrefixes: ["MSSQL_HOST","MSSQL_SA_PASSWORD","SQLSERVER_HOST","SQL_SERVER"], envWeak: [] },
|
|
22
|
+
{ id: "sqlite", name: "SQLITE", kind: "db", color: 0x6fb8ff,
|
|
23
|
+
deps: ["better-sqlite3","sqlite3","aiosqlite","@libsql/client","github.com/mattn/go-sqlite3","org.xerial:sqlite-jdbc","rusqlite","modernc.org/sqlite"],
|
|
24
|
+
imports: ["better-sqlite3","sqlite3","aiosqlite","@libsql/client","github.com/mattn/go-sqlite3","org.sqlite","rusqlite","node:sqlite"], images: [], envPrefixes: ["SQLITE_PATH","TURSO","LIBSQL"], envWeak: [] },
|
|
25
|
+
{ id: "cockroachdb", name: "COCKROACHDB", kind: "db", color: 0x4fd9d1,
|
|
26
|
+
deps: ["github.com/cockroachdb/cockroach-go/v2","sqlalchemy-cockroachdb"],
|
|
27
|
+
imports: ["github.com/cockroachdb/cockroach-go","sqlalchemy_cockroachdb"], images: ["cockroachdb/cockroach","bitnami/cockroachdb"], envPrefixes: ["COCKROACH","COCKROACHDB","CRDB_HOST","COCKROACH_URL"], envWeak: [] },
|
|
28
|
+
{ id: "spanner", name: "SPANNER", kind: "db", color: 0x4fd1e0,
|
|
29
|
+
deps: ["cloud.google.com/go/spanner","com.google.cloud:google-cloud-spanner","com.google.cloud:google-cloud-spanner-jdbc"],
|
|
30
|
+
imports: ["cloud.google.com/go/spanner","com.google.cloud.spanner"], images: ["gcr.io/cloud-spanner-emulator/emulator"], envPrefixes: ["SPANNER"], envWeak: [] },
|
|
31
|
+
{ id: "planetscale", name: "PLANETSCALE", kind: "db", color: 0x6fd1ff,
|
|
32
|
+
deps: ["@planetscale/database"],
|
|
33
|
+
imports: ["@planetscale/database"], images: ["vitess/vtgate","vitess/lite"], envPrefixes: ["PLANETSCALE","DATABASE_HOST","PSCALE"], envWeak: [] },
|
|
34
|
+
{ id: "snowflake", name: "SNOWFLAKE", kind: "db", color: 0x4fb6ff,
|
|
35
|
+
deps: ["snowflake-sdk","snowflake-connector-python","github.com/snowflakedb/gosnowflake","net.snowflake:snowflake-jdbc","Snowflake.Data"],
|
|
36
|
+
imports: ["snowflake-sdk","snowflake.connector","github.com/snowflakedb/gosnowflake","net.snowflake.client","Snowflake.Data"], images: [], envPrefixes: ["SNOWFLAKE_ACCOUNT","SNOWFLAKE_USER","SNOWFLAKE_WAREHOUSE","SNOWFLAKE_DATABASE"], envWeak: [] },
|
|
37
|
+
{ id: "bigquery", name: "BIGQUERY", kind: "db", color: 0x5fc0ff,
|
|
38
|
+
deps: ["@google-cloud/bigquery","google-cloud-bigquery","cloud.google.com/go/bigquery","com.google.cloud:google-cloud-bigquery"],
|
|
39
|
+
imports: ["@google-cloud/bigquery","google.cloud.bigquery","cloud.google.com/go/bigquery","com.google.cloud.bigquery"], images: ["ghcr.io/goccy/bigquery-emulator"], envPrefixes: ["BIGQUERY","GOOGLE_BIGQUERY"], envWeak: [] },
|
|
40
|
+
{ id: "redshift", name: "REDSHIFT", kind: "db", color: 0x4fa8ff,
|
|
41
|
+
deps: ["@aws-sdk/client-redshift","@aws-sdk/client-redshift-data","redshift-connector","com.amazon.redshift:redshift-jdbc42"],
|
|
42
|
+
imports: ["@aws-sdk/client-redshift","redshift_connector","com.amazon.redshift"], images: [], envPrefixes: ["REDSHIFT_HOST","REDSHIFT_URL","REDSHIFT_CLUSTER"], envWeak: [] },
|
|
43
|
+
{ id: "db2", name: "DB2", kind: "db", color: 0x4fcfff,
|
|
44
|
+
deps: ["com.ibm.db2:jcc","com.ibm.db2.jcc:db2jcc","ibm_db"],
|
|
45
|
+
imports: ["com.ibm.db2.jcc","ibm_db"], images: ["ibmcom/db2","icr.io/db2_community/db2"], envPrefixes: ["DB2"], envWeak: ["DB2"] },
|
|
46
|
+
{ id: "mongodb", name: "MONGODB", kind: "db", color: 0x6fe26f,
|
|
47
|
+
deps: ["mongodb","mongoose","pymongo","motor","go.mongodb.org/mongo-driver","org.mongodb:mongodb-driver-sync","org.mongodb:mongo-java-driver","MongoDB.Driver"],
|
|
48
|
+
imports: ["mongoose","pymongo","motor.motor_asyncio","go.mongodb.org/mongo-driver/mongo","com.mongodb.","mongodb","Mongo::Client"], images: ["mongo","bitnami/mongodb","percona/percona-server-mongodb","mongodb/mongodb-community-server"], envPrefixes: ["MONGO","MONGODB","MONGO_URI","MONGO_HOST","MONGODB_URI"], envWeak: [] },
|
|
49
|
+
{ id: "cassandra", name: "CASSANDRA", kind: "db", color: 0x66dd66,
|
|
50
|
+
deps: ["cassandra-driver","github.com/gocql/gocql","com.datastax.oss:java-driver-core","com.datastax.cassandra:cassandra-driver-core","CassandraCSharpDriver","scylla-driver","acsylla","cdrs-tokio"],
|
|
51
|
+
imports: ["cassandra-driver","github.com/gocql/gocql","com.datastax.oss.driver","com.datastax.driver.core","scylla"], images: ["cassandra","bitnami/cassandra","scylladb/scylla","scylladb/scylla-enterprise"], envPrefixes: ["CASSANDRA","SCYLLA","CASSANDRA_CONTACT_POINTS","CASSANDRA_KEYSPACE","SCYLLA_CONTACT_POINTS"], envWeak: [] },
|
|
52
|
+
{ id: "dynamodb", name: "DYNAMODB", kind: "db", color: 0x5fd95f,
|
|
53
|
+
deps: ["@aws-sdk/client-dynamodb","@aws-sdk/lib-dynamodb","dynamoose","electrodb","pynamodb","github.com/aws/aws-sdk-go-v2/service/dynamodb","software.amazon.awssdk:dynamodb","github.com/guregu/dynamo"],
|
|
54
|
+
imports: ["@aws-sdk/client-dynamodb","@aws-sdk/lib-dynamodb","dynamoose","pynamodb","github.com/aws/aws-sdk-go-v2/service/dynamodb","software.amazon.awssdk.services.dynamodb","Amazon.DynamoDBv2"], images: ["amazon/dynamodb-local"], envPrefixes: ["DYNAMODB","DYNAMO","DYNAMODB_TABLE","DYNAMODB_ENDPOINT","AWS_DYNAMODB"], envWeak: [] },
|
|
55
|
+
{ id: "elasticsearch", name: "ELASTICSEARCH", kind: "db", color: 0x7aea7a,
|
|
56
|
+
deps: ["@elastic/elasticsearch","@opensearch-project/opensearch","elasticsearch","elasticsearch-dsl","opensearch-py","github.com/elastic/go-elasticsearch","co.elastic.clients:elasticsearch-java","org.opensearch.client:opensearch-java"],
|
|
57
|
+
imports: ["@elastic/elasticsearch","@opensearch-project/opensearch","elasticsearch_dsl","opensearchpy","github.com/elastic/go-elasticsearch","co.elastic.clients.elasticsearch","org.opensearch.client"], images: ["elasticsearch","docker.elastic.co/elasticsearch/elasticsearch","bitnami/elasticsearch","opensearchproject/opensearch"], envPrefixes: ["ELASTICSEARCH","OPENSEARCH","ES_HOST","ES_URL","ELASTICSEARCH_URL","OPENSEARCH_URL"], envWeak: [] },
|
|
58
|
+
{ id: "solr", name: "SOLR", kind: "db", color: 0x63e063,
|
|
59
|
+
deps: ["solr-client","solr-node","pysolr","aiosolr","github.com/vanng822/go-solr","org.apache.solr:solr-solrj"],
|
|
60
|
+
imports: ["solr-client","solr-node","pysolr","org.apache.solr.client.solrj","org.springframework.data.solr"], images: ["solr","bitnami/solr"], envPrefixes: ["SOLR","SOLR_HOST","SOLR_URL","SOLR_PORT"], envWeak: [] },
|
|
61
|
+
{ id: "meilisearch", name: "MEILISEARCH", kind: "db", color: 0x72e672,
|
|
62
|
+
deps: ["meilisearch","@meilisearch/instant-meilisearch","meilisearch-python-sdk","github.com/meilisearch/meilisearch-go","com.meilisearch.sdk:meilisearch-java"],
|
|
63
|
+
imports: ["meilisearch","@meilisearch/instant-meilisearch","github.com/meilisearch/meilisearch-go","com.meilisearch.sdk"], images: ["getmeili/meilisearch"], envPrefixes: ["MEILI","MEILISEARCH","MEILI_HOST","MEILI_MASTER_KEY","MEILISEARCH_URL"], envWeak: [] },
|
|
64
|
+
{ id: "typesense", name: "TYPESENSE", kind: "db", color: 0x59d959,
|
|
65
|
+
deps: ["typesense","typesense-instantsearch-adapter","typesense-python","github.com/typesense/typesense-go"],
|
|
66
|
+
imports: ["typesense","typesense-instantsearch-adapter","github.com/typesense/typesense-go"], images: ["typesense/typesense"], envPrefixes: ["TYPESENSE","TYPESENSE_HOST","TYPESENSE_API_KEY","TYPESENSE_URL"], envWeak: [] },
|
|
67
|
+
{ id: "neo4j", name: "NEO4J", kind: "db", color: 0x6fe28a,
|
|
68
|
+
deps: ["neo4j-driver","neo4j","py2neo","neomodel","github.com/neo4j/neo4j-go-driver","org.neo4j.driver:neo4j-java-driver","neo4rs","@neo4j/graphql"],
|
|
69
|
+
imports: ["neo4j-driver","py2neo","neomodel","github.com/neo4j/neo4j-go-driver","org.neo4j.driver","neo4rs"], images: ["neo4j","bitnami/neo4j"], envPrefixes: ["NEO4J","NEO4J_URI","NEO4J_HOST","NEO4J_AUTH"], envWeak: [] },
|
|
70
|
+
{ id: "couchbase", name: "COUCHBASE", kind: "db", color: 0x6fe27a,
|
|
71
|
+
deps: ["couchbase","acouchbase","github.com/couchbase/gocb/v2","com.couchbase.client:java-client","couchbase-rs"],
|
|
72
|
+
imports: ["couchbase","acouchbase","github.com/couchbase/gocb","com.couchbase.client"], images: ["couchbase","bitnami/couchbase"], envPrefixes: ["COUCHBASE","COUCHBASE_HOST","COUCHBASE_URL","COUCHBASE_BUCKET"], envWeak: [] },
|
|
73
|
+
{ id: "qdrant", name: "QDRANT", kind: "db", color: 0x86e886,
|
|
74
|
+
deps: ["@qdrant/js-client-rest","@qdrant/qdrant-js","qdrant-client","github.com/qdrant/go-client","io.qdrant:client"],
|
|
75
|
+
imports: ["@qdrant/js-client-rest","qdrant_client","github.com/qdrant/go-client","io.qdrant.client"], images: ["qdrant/qdrant"], envPrefixes: ["QDRANT","QDRANT_URL","QDRANT_HOST","QDRANT_API_KEY"], envWeak: [] },
|
|
76
|
+
{ id: "weaviate", name: "WEAVIATE", kind: "db", color: 0x90ee90,
|
|
77
|
+
deps: ["weaviate-client","weaviate-ts-client","weaviate-python-client","github.com/weaviate/weaviate-go-client","io.weaviate:client"],
|
|
78
|
+
imports: ["weaviate-client","weaviate-ts-client","github.com/weaviate/weaviate-go-client","io.weaviate.client"], images: ["semitechnologies/weaviate","cr.weaviate.io/semitechnologies/weaviate"], envPrefixes: ["WEAVIATE","WEAVIATE_URL","WEAVIATE_HOST","WEAVIATE_API_KEY"], envWeak: [] },
|
|
79
|
+
{ id: "milvus", name: "MILVUS", kind: "db", color: 0x9cf09c,
|
|
80
|
+
deps: ["@zilliz/milvus2-sdk-node","pymilvus","github.com/milvus-io/milvus-sdk-go","io.milvus:milvus-sdk-java"],
|
|
81
|
+
imports: ["@zilliz/milvus2-sdk-node","pymilvus","github.com/milvus-io/milvus-sdk-go","io.milvus."], images: ["milvusdb/milvus","zilliz/milvus"], envPrefixes: ["MILVUS","MILVUS_HOST","MILVUS_URL","MILVUS_PORT"], envWeak: [] },
|
|
82
|
+
{ id: "clickhouse", name: "CLICKHOUSE", kind: "ts", color: 0xffe14f,
|
|
83
|
+
deps: ["@clickhouse/client","@clickhouse/client-web","clickhouse-driver","clickhouse-connect","github.com/ClickHouse/clickhouse-go/v2","com.clickhouse:clickhouse-jdbc","com.clickhouse:clickhouse-client","ClickHouse.Client","clickhouse"],
|
|
84
|
+
imports: ["@clickhouse/client","clickhouse_driver","clickhouse_connect","github.com/ClickHouse/clickhouse-go","com.clickhouse.","ru.yandex.clickhouse","clickhouse","Octonica.ClickHouseClient"], images: ["clickhouse/clickhouse-server","clickhouse/clickhouse-keeper","bitnami/clickhouse","yandex/clickhouse-server"], envPrefixes: ["CLICKHOUSE","CH_HOST","CH_PORT","CH_PASSWORD"], envWeak: [] },
|
|
85
|
+
{ id: "influxdb", name: "INFLUXDB", kind: "ts", color: 0xf5d800,
|
|
86
|
+
deps: ["@influxdata/influxdb-client","influxdb-client","github.com/influxdata/influxdb-client-go/v2","com.influxdb:influxdb-client-java","org.influxdb:influxdb-java","influxdb3-python","influxdb2","InfluxDB.Client","influx"],
|
|
87
|
+
imports: ["@influxdata/influxdb-client","influxdb_client","github.com/influxdata/influxdb-client-go","github.com/influxdata/influxdb1-client","com.influxdb.client","org.influxdb.","influx","influxdb"], images: ["influxdb","bitnami/influxdb","quay.io/influxdb/influxdb","telegraf"], envPrefixes: ["INFLUX","INFLUXDB","INFLUX_TOKEN","INFLUXDB_URL","INFLUX_ORG","INFLUX_BUCKET"], envWeak: [] },
|
|
88
|
+
{ id: "timescaledb", name: "TIMESCALEDB", kind: "ts", color: 0xffea66,
|
|
89
|
+
deps: ["@timescale/toolkit"],
|
|
90
|
+
imports: ["@timescale/toolkit"], images: ["timescale/timescaledb","timescale/timescaledb-ha","timescaledev/timescaledb-ha"], envPrefixes: ["TIMESCALE","TIMESCALEDB"], envWeak: [] },
|
|
91
|
+
{ id: "prometheus", name: "PROMETHEUS", kind: "ts", color: 0xfff04d,
|
|
92
|
+
deps: ["prometheus-query","prometheus-api-client","github.com/prometheus/client_golang/api"],
|
|
93
|
+
imports: ["prometheus-query","github.com/prometheus/client_golang/api","prometheus_api_client"], images: ["prom/prometheus","prom/pushgateway","bitnami/prometheus","prom/node-exporter"], envPrefixes: ["PROMETHEUS","PROMETHEUS_URL","PUSHGATEWAY"], envWeak: [] },
|
|
94
|
+
{ id: "victoriametrics", name: "VICTORIAMETRICS", kind: "ts", color: 0xe6d633,
|
|
95
|
+
deps: ["github.com/VictoriaMetrics/VictoriaMetrics"],
|
|
96
|
+
imports: ["github.com/VictoriaMetrics/VictoriaMetrics"], images: ["victoriametrics/victoria-metrics","victoriametrics/vmagent","victoriametrics/vminsert","victoriametrics/vmselect"], envPrefixes: ["VICTORIA_METRICS","VICTORIAMETRICS"], envWeak: [] },
|
|
97
|
+
{ id: "graphite", name: "GRAPHITE", kind: "ts", color: 0xf0e07a,
|
|
98
|
+
deps: ["graphyte","graphitesend","graphite-udp","io.micrometer:micrometer-registry-graphite","io.dropwizard.metrics:metrics-graphite"],
|
|
99
|
+
imports: ["graphyte","graphitesend","io.micrometer.graphite","com.codahale.metrics.graphite"], images: ["graphiteapp/graphite-statsd"], envPrefixes: ["GRAPHITE","CARBON"], envWeak: [] },
|
|
100
|
+
{ id: "redis", name: "REDIS", kind: "cache", color: 0xff6b6b,
|
|
101
|
+
deps: ["ioredis","redis","@redis/client","redis-om","@upstash/redis","redis-py","github.com/redis/go-redis/v9","redis.clients:jedis"],
|
|
102
|
+
imports: ["ioredis","@redis/client","redis.asyncio","github.com/redis/go-redis","github.com/gomodule/redigo/redis","redis.clients.jedis","io.lettuce.core","org.redisson"], images: ["redis","redis/redis-stack","redis/redis-stack-server","bitnami/redis","redislabs/redismod","eqalpha/keydb"], envPrefixes: ["REDIS","REDIS_HOST","REDIS_URL","REDIS_PASSWORD"], envWeak: [] },
|
|
103
|
+
{ id: "valkey", name: "VALKEY", kind: "cache", color: 0xff8585,
|
|
104
|
+
deps: ["valkey","iovalkey","valkey-glide","@valkey/valkey-glide","github.com/valkey-io/valkey-go","valkey-py"],
|
|
105
|
+
imports: ["iovalkey","valkey_glide","github.com/valkey-io/valkey-go"], images: ["valkey/valkey","bitnami/valkey"], envPrefixes: ["VALKEY","VALKEY_HOST","VALKEY_URL"], envWeak: [] },
|
|
106
|
+
{ id: "memcached", name: "MEMCACHED", kind: "cache", color: 0xff5252,
|
|
107
|
+
deps: ["memjs","pymemcache","pylibmc","github.com/bradfitz/gomemcache","net.spy:spymemcached","com.googlecode.xmemcached:xmemcached","aiomcache","memcached","memcache"],
|
|
108
|
+
imports: ["memjs","pymemcache","pylibmc","github.com/bradfitz/gomemcache/memcache","net.spy.memcached","net.rubyeye.xmemcached"], images: ["memcached","bitnami/memcached"], envPrefixes: ["MEMCACHED","MEMCACHE","MEMCACHED_HOST","MEMCACHED_SERVERS"], envWeak: [] },
|
|
109
|
+
{ id: "hazelcast", name: "HAZELCAST", kind: "cache", color: 0xff9494,
|
|
110
|
+
deps: ["com.hazelcast:hazelcast","com.hazelcast:hazelcast-spring","com.hazelcast:hazelcast-client"],
|
|
111
|
+
imports: ["com.hazelcast."], images: ["hazelcast/hazelcast","hazelcast/management-center"], envPrefixes: ["HAZELCAST"], envWeak: [] },
|
|
112
|
+
{ id: "kafka", name: "KAFKA", kind: "queue", color: 0xff7ad9,
|
|
113
|
+
deps: ["kafkajs","node-rdkafka","@confluentinc/kafka-javascript","confluent-kafka","aiokafka","github.com/segmentio/kafka-go","org.apache.kafka:kafka-clients","github.com/twmb/franz-go"],
|
|
114
|
+
imports: ["kafkajs","node-rdkafka","confluent_kafka","aiokafka","github.com/segmentio/kafka-go","github.com/Shopify/sarama","org.apache.kafka.clients","org.springframework.kafka"], images: ["confluentinc/cp-kafka","confluentinc/cp-server","bitnami/kafka","apache/kafka","wurstmeister/kafka","redpandadata/redpanda","vectorized/redpanda","confluentinc/cp-zookeeper"], envPrefixes: ["KAFKA","KAFKA_BOOTSTRAP_SERVERS","KAFKA_BROKERS","CONFLUENT","REDPANDA","SCHEMA_REGISTRY"], envWeak: [] },
|
|
115
|
+
{ id: "rabbitmq", name: "RABBITMQ", kind: "queue", color: 0xff86dd,
|
|
116
|
+
deps: ["amqplib","amqp-connection-manager","pika","aio-pika","github.com/rabbitmq/amqp091-go","com.rabbitmq:amqp-client","org.springframework.amqp:spring-rabbit","lapin"],
|
|
117
|
+
imports: ["amqplib","amqp-connection-manager","pika","aio_pika","github.com/rabbitmq/amqp091-go","com.rabbitmq.client","org.springframework.amqp","lapin"], images: ["rabbitmq","bitnami/rabbitmq","cloudamqp/lavinmq"], envPrefixes: ["RABBITMQ","RABBITMQ_HOST","RABBITMQ_URL","CLOUDAMQP"], envWeak: [] },
|
|
118
|
+
{ id: "nats", name: "NATS", kind: "queue", color: 0xff6fd3,
|
|
119
|
+
deps: ["nats","nats.ws","nats-py","github.com/nats-io/nats.go","io.nats:jnats","async-nats","node-nats-streaming","@nats-io/jetstream"],
|
|
120
|
+
imports: ["github.com/nats-io/nats.go","io.nats.client","async_nats","node-nats-streaming","nats","nats.aio","nats.ws"], images: ["nats","synadia/nats-server","bitnami/nats","natsio/nats-box"], envPrefixes: ["NATS","NATS_URL","NATS_HOST","NATS_SERVERS"], envWeak: [] },
|
|
121
|
+
{ id: "mqtt", name: "MQTT", kind: "queue", color: 0xff90e2,
|
|
122
|
+
deps: ["mqtt","async-mqtt","paho-mqtt","aiomqtt","github.com/eclipse/paho.mqtt.golang","org.eclipse.paho:org.eclipse.paho.client.mqttv3","rumqttc","MQTTnet"],
|
|
123
|
+
imports: ["async-mqtt","paho.mqtt","aiomqtt","github.com/eclipse/paho.mqtt.golang","org.eclipse.paho","rumqttc","mqtt"], images: ["eclipse-mosquitto","emqx/emqx","hivemq/hivemq-ce","vernemq/vernemq","nanomq/nanomq"], envPrefixes: ["MQTT","MQTT_HOST","MQTT_BROKER","MOSQUITTO","EMQX"], envWeak: [] },
|
|
124
|
+
{ id: "pulsar", name: "PULSAR", kind: "queue", color: 0xff7ace,
|
|
125
|
+
deps: ["pulsar-client","github.com/apache/pulsar-client-go","org.apache.pulsar:pulsar-client","org.springframework.pulsar:spring-pulsar"],
|
|
126
|
+
imports: ["pulsar-client","github.com/apache/pulsar-client-go","org.apache.pulsar.client","org.springframework.pulsar","_pulsar"], images: ["apachepulsar/pulsar","apachepulsar/pulsar-all","streamnative/pulsar","bitnami/pulsar"], envPrefixes: ["PULSAR","PULSAR_URL","PULSAR_BROKER","PULSAR_SERVICE_URL"], envWeak: [] },
|
|
127
|
+
{ id: "nsq", name: "NSQ", kind: "queue", color: 0xff64c9,
|
|
128
|
+
deps: ["github.com/nsqio/go-nsq"],
|
|
129
|
+
imports: ["github.com/nsqio/go-nsq"], images: ["nsqio/nsq"], envPrefixes: ["NSQ"], envWeak: ["NSQ"] },
|
|
130
|
+
{ id: "activemq", name: "ACTIVEMQ", kind: "queue", color: 0xff9ce6,
|
|
131
|
+
deps: ["org.apache.activemq:activemq-client","org.apache.activemq:artemis-jms-client","org.springframework.boot:spring-boot-starter-activemq","org.springframework.boot:spring-boot-starter-artemis"],
|
|
132
|
+
imports: ["org.apache.activemq","org.apache.activemq.artemis"], images: ["apache/activemq-classic","apache/activemq-artemis","rmohr/activemq","vromero/activemq-artemis"], envPrefixes: ["ACTIVEMQ","ARTEMIS"], envWeak: [] },
|
|
133
|
+
{ id: "sqs", name: "SQS", kind: "queue", color: 0xff72d0,
|
|
134
|
+
deps: ["@aws-sdk/client-sqs","sqs-consumer","sqs-producer","github.com/aws/aws-sdk-go-v2/service/sqs","software.amazon.awssdk:sqs","shoryuken"],
|
|
135
|
+
imports: ["@aws-sdk/client-sqs","sqs-consumer","sqs-producer","github.com/aws/aws-sdk-go-v2/service/sqs","software.amazon.awssdk.services.sqs","Aws::SQS"], images: ["softwaremill/elasticmq","roribio16/alpine-sqs"], envPrefixes: ["SQS","AWS_SQS","SQS_QUEUE_URL"], envWeak: ["SQS"] },
|
|
136
|
+
{ id: "sns", name: "SNS", kind: "queue", color: 0xff82d9,
|
|
137
|
+
deps: ["@aws-sdk/client-sns","github.com/aws/aws-sdk-go-v2/service/sns","software.amazon.awssdk:sns","com.amazonaws:aws-java-sdk-sns"],
|
|
138
|
+
imports: ["@aws-sdk/client-sns","github.com/aws/aws-sdk-go-v2/service/sns","software.amazon.awssdk.services.sns","Amazon.SNS"], images: [], envPrefixes: ["SNS","AWS_SNS"], envWeak: ["SNS"] },
|
|
139
|
+
{ id: "kinesis", name: "KINESIS", kind: "queue", color: 0xff6cce,
|
|
140
|
+
deps: ["@aws-sdk/client-kinesis","github.com/aws/aws-sdk-go-v2/service/kinesis","software.amazon.awssdk:kinesis","software.amazon.kinesis:amazon-kinesis-client"],
|
|
141
|
+
imports: ["@aws-sdk/client-kinesis","github.com/aws/aws-sdk-go-v2/service/kinesis","software.amazon.awssdk.services.kinesis","software.amazon.kinesis"], images: [], envPrefixes: ["KINESIS","AWS_KINESIS"], envWeak: [] },
|
|
142
|
+
{ id: "gcp-pubsub", name: "GCP_PUBSUB", kind: "queue", color: 0xff7adf,
|
|
143
|
+
deps: ["@google-cloud/pubsub","google-cloud-pubsub","cloud.google.com/go/pubsub","com.google.cloud:google-cloud-pubsub"],
|
|
144
|
+
imports: ["@google-cloud/pubsub","google.cloud.pubsub","cloud.google.com/go/pubsub","com.google.cloud.pubsub"], images: ["messagebird/gcloud-pubsub-emulator"], envPrefixes: ["PUBSUB","PUBSUB_EMULATOR_HOST","GCP_PUBSUB"], envWeak: [] },
|
|
145
|
+
{ id: "azure-servicebus", name: "AZURE_SERVICEBUS", kind: "queue", color: 0xff88e0,
|
|
146
|
+
deps: ["@azure/service-bus","azure-servicebus","com.azure:azure-messaging-servicebus","github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus"],
|
|
147
|
+
imports: ["@azure/service-bus","azure.servicebus","com.azure.messaging.servicebus","github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus"], images: [], envPrefixes: ["SERVICEBUS","AZURE_SERVICEBUS","SERVICE_BUS"], envWeak: [] },
|
|
148
|
+
{ id: "s3", name: "S3", kind: "cloud", color: 0xb06bff,
|
|
149
|
+
deps: ["@aws-sdk/client-s3","@aws-sdk/s3-request-presigner","@aws-sdk/lib-storage","aws-sdk-s3","github.com/aws/aws-sdk-go-v2/service/s3","software.amazon.awssdk:s3","com.amazonaws:aws-java-sdk-s3","s3fs"],
|
|
150
|
+
imports: ["@aws-sdk/client-s3","github.com/aws/aws-sdk-go-v2/service/s3","github.com/aws/aws-sdk-go/service/s3","software.amazon.awssdk.services.s3","com.amazonaws.services.s3","aws_sdk_s3","rusoto_s3"], images: ["adobe/s3mock","zenko/cloudserver"], envPrefixes: ["S3","AWS_S3","S3_BUCKET","S3_ENDPOINT","AWS_S3_BUCKET"], envWeak: ["S3"] },
|
|
151
|
+
{ id: "minio", name: "MINIO", kind: "cloud", color: 0xa64fff,
|
|
152
|
+
deps: ["minio","minio-py","github.com/minio/minio-go","github.com/minio/minio-go/v7","io.minio:minio"],
|
|
153
|
+
imports: ["minio","github.com/minio/minio-go","io.minio."], images: ["minio/minio","bitnami/minio","minio/mc"], envPrefixes: ["MINIO","MINIO_ENDPOINT","MINIO_ACCESS_KEY","MINIO_SECRET_KEY","MINIO_ROOT_USER","MINIO_BUCKET"], envWeak: [] },
|
|
154
|
+
{ id: "r2", name: "R2", kind: "cloud", color: 0xc08bff,
|
|
155
|
+
deps: [],
|
|
156
|
+
imports: [], images: [], envPrefixes: ["R2_BUCKET","R2_ACCESS_KEY_ID","R2_SECRET_ACCESS_KEY","R2_ACCOUNT_ID","CLOUDFLARE_R2"], envWeak: [] },
|
|
157
|
+
{ id: "gcs", name: "GCS", kind: "cloud", color: 0x9b5bff,
|
|
158
|
+
deps: ["@google-cloud/storage","google-cloud-storage","cloud.google.com/go/storage","com.google.cloud:google-cloud-storage","gcsfs","gcs-resumable-upload"],
|
|
159
|
+
imports: ["@google-cloud/storage","google.cloud.storage","cloud.google.com/go/storage","com.google.cloud.storage","gcsfs"], images: ["fsouza/fake-gcs-server"], envPrefixes: ["GCS_BUCKET","GOOGLE_CLOUD_STORAGE","GCP_BUCKET"], envWeak: [] },
|
|
160
|
+
{ id: "azure-blob", name: "AZURE_BLOB", kind: "cloud", color: 0xb87bff,
|
|
161
|
+
deps: ["@azure/storage-blob","azure-storage-blob","github.com/Azure/azure-sdk-for-go/sdk/storage/azblob","com.azure:azure-storage-blob","adlfs"],
|
|
162
|
+
imports: ["@azure/storage-blob","azure.storage.blob","github.com/Azure/azure-sdk-for-go/sdk/storage/azblob","com.azure.storage.blob"], images: ["mcr.microsoft.com/azure-storage/azurite"], envPrefixes: ["AZURE_STORAGE","AZURE_BLOB","AZURE_STORAGE_CONNECTION_STRING","AZURE_STORAGE_ACCOUNT","AZURE_CONTAINER"], envWeak: [] },
|
|
163
|
+
{ id: "keycloak", name: "KEYCLOAK", kind: "api", color: 0x66e0ff,
|
|
164
|
+
deps: ["@keycloak/keycloak-admin-client","keycloak-connect","keycloak-js","python-keycloak","github.com/Nerzal/gocloak","org.keycloak:keycloak-admin-client","org.keycloak:keycloak-spring-boot-starter","Keycloak.AuthServices.Authentication"],
|
|
165
|
+
imports: ["@keycloak/keycloak-admin-client","keycloak-connect","keycloak-js","github.com/Nerzal/gocloak","org.keycloak.","fastapi_keycloak","Keycloak.AuthServices"], images: ["quay.io/keycloak/keycloak","keycloak/keycloak","bitnami/keycloak"], envPrefixes: ["KEYCLOAK","KEYCLOAK_URL","KEYCLOAK_REALM","KEYCLOAK_CLIENT_ID","KEYCLOAK_BASE_URL"], envWeak: [] },
|
|
166
|
+
{ id: "auth0", name: "AUTH0", kind: "api", color: 0x5cd9ff,
|
|
167
|
+
deps: ["auth0","@auth0/auth0-spa-js","@auth0/auth0-react","@auth0/nextjs-auth0","auth0-python","github.com/auth0/go-auth0","com.auth0:auth0","auth0/auth0-php"],
|
|
168
|
+
imports: ["@auth0/auth0-spa-js","@auth0/auth0-react","@auth0/nextjs-auth0","github.com/auth0/go-auth0","com.auth0.","Auth0\\SDK","auth0"], images: [], envPrefixes: ["AUTH0","AUTH0_DOMAIN","AUTH0_CLIENT_ID","AUTH0_CLIENT_SECRET","AUTH0_AUDIENCE"], envWeak: [] },
|
|
169
|
+
{ id: "okta", name: "OKTA", kind: "api", color: 0x7ae6ff,
|
|
170
|
+
deps: ["@okta/okta-sdk-nodejs","@okta/okta-auth-js","@okta/okta-react","github.com/okta/okta-sdk-golang","com.okta.sdk:okta-sdk-api","okta-jwt-verifier","Okta.Sdk","okta/sdk"],
|
|
171
|
+
imports: ["@okta/okta-sdk-nodejs","@okta/okta-auth-js","github.com/okta/okta-sdk-golang","com.okta.","Okta.Sdk","okta_jwt_verifier"], images: [], envPrefixes: ["OKTA","OKTA_DOMAIN","OKTA_CLIENT_ID","OKTA_ISSUER","OKTA_ORG_URL"], envWeak: [] },
|
|
172
|
+
{ id: "cognito", name: "COGNITO", kind: "api", color: 0x66d4ff,
|
|
173
|
+
deps: ["@aws-sdk/client-cognito-identity-provider","amazon-cognito-identity-js","pycognito","github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider","software.amazon.awssdk:cognitoidentityprovider","AWSSDK.CognitoIdentityProvider"],
|
|
174
|
+
imports: ["@aws-sdk/client-cognito-identity-provider","amazon-cognito-identity-js","pycognito","github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider","software.amazon.awssdk.services.cognitoidentityprovider","aws_sdk_cognitoidentityprovider"], images: [], envPrefixes: ["COGNITO","COGNITO_USER_POOL_ID","COGNITO_CLIENT_ID","AWS_COGNITO"], envWeak: [] },
|
|
175
|
+
{ id: "firebase-auth", name: "FIREBASE_AUTH", kind: "api", color: 0x70ddff,
|
|
176
|
+
deps: ["@firebase/auth","firebase-admin","pyrebase"],
|
|
177
|
+
imports: ["@firebase/auth","firebase/auth","firebase_admin"], images: [], envPrefixes: ["FIREBASE_AUTH","FIREBASE_PROJECT_ID"], envWeak: [] },
|
|
178
|
+
{ id: "vault", name: "VAULT", kind: "api", color: 0x5ce0ff,
|
|
179
|
+
deps: ["node-vault","hvac","github.com/hashicorp/vault/api","com.bettercloud:vault-java-driver","vaultrs","hashi-vault-js","VaultSharp","org.springframework.vault:spring-vault-core"],
|
|
180
|
+
imports: ["node-vault","hvac","github.com/hashicorp/vault/api","io.github.jopenlibs.vault","vaultrs","VaultSharp"], images: ["hashicorp/vault","vault"], envPrefixes: ["VAULT_ADDR","VAULT_TOKEN","VAULT_NAMESPACE","VAULT_ROLE_ID"], envWeak: [] },
|
|
181
|
+
{ id: "oidc-generic", name: "OIDC", kind: "api", color: 0x88ecff,
|
|
182
|
+
deps: ["openid-client","passport-openidconnect","oidc-client-ts","github.com/coreos/go-oidc","github.com/coreos/go-oidc/v3"],
|
|
183
|
+
imports: ["openid-client","oidc-client-ts","passport-openidconnect","github.com/coreos/go-oidc"], images: [], envPrefixes: ["OIDC_ISSUER","OIDC_CLIENT_ID"], envWeak: [] },
|
|
184
|
+
];
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Signature detection + cached public API for infrastructure detection. Split out of infra.js
|
|
2
|
+
// (which remains the public facade); see that file's header for the pipeline overview and the
|
|
3
|
+
// registry/matching contract documented next to the INFRA_SERVICES re-export.
|
|
4
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
// Concrete infra-item extraction + the shared leaf helpers (lc / safeRead / size caps) live in infra-items.js.
|
|
7
|
+
import { collectInfraItems, itemMetaFor, safeRead, lc, IMPORT_SCAN_MAX_FILES, MAX_FILE_BYTES } from "./infra-items.js";
|
|
8
|
+
import { INFRA_SERVICES } from "./infra-registry.js";
|
|
9
|
+
import { depMatches, imageMatches, envMatches } from "./infra.match.js";
|
|
10
|
+
import { scanRepo } from "./infra.scan.js";
|
|
11
|
+
|
|
12
|
+
// ---- detection ----------------------------------------------------------------------------------
|
|
13
|
+
export function detectInfraFromScan(scan, registry = INFRA_SERVICES) {
|
|
14
|
+
const found = new Map(); // id → { service, signals:Set, sources:Set, importTokens:[] }
|
|
15
|
+
const note = (svc, signal, source) => {
|
|
16
|
+
let e = found.get(svc.id);
|
|
17
|
+
if (!e) found.set(svc.id, (e = { service: svc, signals: new Set(), sources: new Set(), importTokens: [] }));
|
|
18
|
+
if (signal) e.signals.add(signal);
|
|
19
|
+
if (source) e.sources.add(source);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
for (const svc of registry) {
|
|
23
|
+
// 1) manifest deps (highest confidence)
|
|
24
|
+
for (const tok of svc.deps || []) {
|
|
25
|
+
for (const d of scan.deps) { if (depMatches(d, tok)) { note(svc, `dep:${d}`, "manifest"); break; } }
|
|
26
|
+
}
|
|
27
|
+
// 2) container images
|
|
28
|
+
for (const tok of svc.images || []) {
|
|
29
|
+
for (const im of scan.imageSegs) { if (imageMatches(im.segs, tok)) { note(svc, `image:${im.raw}`, "image"); break; } }
|
|
30
|
+
}
|
|
31
|
+
// 3) env-var name conventions
|
|
32
|
+
const weakSet = new Set((svc.envWeak || []).map((s) => s.toUpperCase()));
|
|
33
|
+
for (const tok of svc.envPrefixes || []) {
|
|
34
|
+
for (const k of scan.envKeys) { if (envMatches(k, tok, weakSet.has(tok.toUpperCase()))) { note(svc, `env:${k}`, "env"); break; } }
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 4) source imports → attribute connector files (and a weak detection signal). Only scan if any
|
|
39
|
+
// service has import tokens; pre-filter files by a combined token regex (cheap), like apimap.
|
|
40
|
+
const importIndex = []; // { svc, token }
|
|
41
|
+
for (const svc of registry) for (const tok of svc.imports || []) importIndex.push({ svc, tok: lc(tok) });
|
|
42
|
+
const filesByService = new Map(); // id → Set(relPath)
|
|
43
|
+
if (importIndex.length && scan.codeFiles.length) {
|
|
44
|
+
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
45
|
+
const pre = new RegExp(importIndex.map((x) => esc(x.tok)).join("|"), "i");
|
|
46
|
+
// Reading every source file is the only O(repo-size) cost here and it runs synchronously, so cap it:
|
|
47
|
+
// connector-file attribution is best-effort and detection from manifests/images/env is already complete
|
|
48
|
+
// above. ~5k files keeps the cold scan well under a second even on a large monorepo (then it's cached).
|
|
49
|
+
const scanFiles = scan.codeFiles.length > IMPORT_SCAN_MAX_FILES ? scan.codeFiles.slice(0, IMPORT_SCAN_MAX_FILES) : scan.codeFiles;
|
|
50
|
+
for (const f of scanFiles) {
|
|
51
|
+
const text = safeRead(f.full);
|
|
52
|
+
if (!text || text.length > MAX_FILE_BYTES || !pre.test(text)) continue;
|
|
53
|
+
const lcText = text.toLowerCase();
|
|
54
|
+
for (const { svc, tok } of importIndex) {
|
|
55
|
+
// require a quoted/pathish occurrence so a stray word doesn't match: 'tok' "tok" `tok` tok/ tok"
|
|
56
|
+
if (lcText.includes(`"${tok}`) || lcText.includes(`'${tok}`) || lcText.includes("`" + tok) || lcText.includes(`/${tok}"`) || lcText.includes(tok + "/")) {
|
|
57
|
+
let set = filesByService.get(svc.id);
|
|
58
|
+
if (!set) filesByService.set(svc.id, (set = new Set()));
|
|
59
|
+
set.add(f.path);
|
|
60
|
+
// imports alone also count as a (weaker) detection signal
|
|
61
|
+
if (!found.has(svc.id)) note(svc, `import:${tok}`, "import");
|
|
62
|
+
else found.get(svc.id).sources.add("import");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const entries = [...found.values()];
|
|
69
|
+
const itemsByService = collectInfraItems(scan, entries, filesByService);
|
|
70
|
+
const services = entries.map(({ service, signals, sources }) => {
|
|
71
|
+
const files = [...(filesByService.get(service.id) || [])].sort().slice(0, 40);
|
|
72
|
+
const itemData = itemsByService.get(service.id) || { ...itemMetaFor(service), items: [] };
|
|
73
|
+
// confidence: a parsed manifest dep or image is hard evidence; env/import alone is softer.
|
|
74
|
+
const hard = sources.has("manifest") || sources.has("image");
|
|
75
|
+
const confidence = hard ? "high" : sources.has("env") && sources.has("import") ? "high" : sources.has("env") || sources.has("import") ? "medium" : "low";
|
|
76
|
+
return {
|
|
77
|
+
id: service.id, name: service.name, kind: service.kind, color: service.color,
|
|
78
|
+
confidence, sources: [...sources].sort(), signals: [...signals].sort().slice(0, 24), files,
|
|
79
|
+
items: itemData.items || [], itemLabel: itemData.itemLabel || itemData.label, unit: itemData.unit || "",
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
// stable, useful order: hard evidence first, then by source count, then name
|
|
83
|
+
const rank = (c) => (c === "high" ? 0 : c === "medium" ? 1 : 2);
|
|
84
|
+
services.sort((a, b) => rank(a.confidence) - rank(b.confidence) || b.signals.length - a.signals.length || a.name.localeCompare(b.name));
|
|
85
|
+
return services;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---- public API (cached by manifest/compose/env mtime) ------------------------------------------
|
|
89
|
+
const _cache = new Map(); // repoPath → { sig, result }
|
|
90
|
+
|
|
91
|
+
// cheap cache signature: newest mtime among the files that drive detection. If none change, reuse.
|
|
92
|
+
function repoSignature(repoPath) {
|
|
93
|
+
let newest = 0, seen = 0;
|
|
94
|
+
const probe = (p) => { try { const st = statSync(p); if (st.isFile()) { newest = Math.max(newest, st.mtimeMs); seen++; } } catch { /* missing — skip */ } };
|
|
95
|
+
const roots = ["package.json", "go.mod", "go.sum", "requirements.txt", "pyproject.toml", "Pipfile", "Cargo.toml", "pom.xml", "build.gradle", "build.gradle.kts", "composer.json", "Gemfile", ".env", "docker-compose.yml", "docker-compose.yaml", "compose.yml", "Dockerfile", "skaffold.yaml"];
|
|
96
|
+
for (const r of roots) probe(join(repoPath, r));
|
|
97
|
+
for (const dir of ["k8s", "kubernetes", "deploy", "manifests", "helm", "charts", ".github"]) {
|
|
98
|
+
try { for (const e of readdirSync(join(repoPath, dir), { withFileTypes: true })) if (e.isFile()) probe(join(repoPath, dir, e.name)); } catch { /* no such dir */ }
|
|
99
|
+
}
|
|
100
|
+
return `${seen}:${Math.round(newest)}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Detect the backing services a repo talks to. Cheap & cached; safe to call from repos:graph-analysis.
|
|
104
|
+
export function detectInfra(repoPath, opts = {}) {
|
|
105
|
+
if (!repoPath || !existsSync(repoPath)) return { ok: false, error: "Repo path not found", services: [] };
|
|
106
|
+
const sig = repoSignature(repoPath);
|
|
107
|
+
const cached = _cache.get(repoPath);
|
|
108
|
+
if (!opts.force && cached && cached.sig === sig) return cached.result;
|
|
109
|
+
let result;
|
|
110
|
+
try {
|
|
111
|
+
const scan = scanRepo(repoPath);
|
|
112
|
+
const services = detectInfraFromScan(scan);
|
|
113
|
+
result = { ok: true, services, scanned: { manifests: scan.manifests, codeFiles: scan.codeFiles.length, envKeys: scan.envKeys.size, images: scan.imageSegs.length } };
|
|
114
|
+
} catch (error) {
|
|
115
|
+
result = { ok: false, error: error.message, services: [] };
|
|
116
|
+
}
|
|
117
|
+
_cache.set(repoPath, { sig, result });
|
|
118
|
+
return result;
|
|
119
|
+
}
|