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.
Files changed (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/bin/weavatrix-mcp.mjs +6 -0
  4. package/package.json +50 -0
  5. package/skill/SKILL.md +56 -0
  6. package/src/analysis/coverage-reports.js +232 -0
  7. package/src/analysis/dead-check.js +136 -0
  8. package/src/analysis/dep-check.js +310 -0
  9. package/src/analysis/dep-rules.js +221 -0
  10. package/src/analysis/duplicates-worker.js +11 -0
  11. package/src/analysis/duplicates.compute.js +215 -0
  12. package/src/analysis/duplicates.js +15 -0
  13. package/src/analysis/duplicates.run.js +51 -0
  14. package/src/analysis/duplicates.tokenize.js +182 -0
  15. package/src/analysis/endpoints.js +156 -0
  16. package/src/analysis/findings.js +52 -0
  17. package/src/analysis/graph-analysis.aggregate.js +280 -0
  18. package/src/analysis/graph-analysis.js +7 -0
  19. package/src/analysis/graph-analysis.refs.js +146 -0
  20. package/src/analysis/graph-analysis.summaries.js +62 -0
  21. package/src/analysis/internal-audit.collect.js +117 -0
  22. package/src/analysis/internal-audit.js +9 -0
  23. package/src/analysis/internal-audit.reach.js +47 -0
  24. package/src/analysis/internal-audit.run.js +189 -0
  25. package/src/analysis/manifests.js +169 -0
  26. package/src/analysis/source-complexity.ast.js +97 -0
  27. package/src/analysis/source-complexity.constants.js +55 -0
  28. package/src/analysis/source-complexity.js +14 -0
  29. package/src/analysis/source-complexity.report.js +76 -0
  30. package/src/analysis/source-complexity.walk.js +174 -0
  31. package/src/build-graph.js +102 -0
  32. package/src/config.js +5 -0
  33. package/src/graph/build-worker.js +30 -0
  34. package/src/graph/builder/lang-csharp.js +40 -0
  35. package/src/graph/builder/lang-css.js +29 -0
  36. package/src/graph/builder/lang-go.js +53 -0
  37. package/src/graph/builder/lang-html.js +29 -0
  38. package/src/graph/builder/lang-java.js +29 -0
  39. package/src/graph/builder/lang-js.js +168 -0
  40. package/src/graph/builder/lang-python.js +78 -0
  41. package/src/graph/builder/lang-rust.js +45 -0
  42. package/src/graph/builder/spec-pkg.js +87 -0
  43. package/src/graph/graph-filter.js +44 -0
  44. package/src/graph/internal-builder.build.js +217 -0
  45. package/src/graph/internal-builder.js +12 -0
  46. package/src/graph/internal-builder.langs.js +86 -0
  47. package/src/graph/internal-builder.resolvers.js +116 -0
  48. package/src/graph/layout.js +35 -0
  49. package/src/infra/infra-items.js +255 -0
  50. package/src/infra/infra-registry.js +184 -0
  51. package/src/infra/infra.detect.js +119 -0
  52. package/src/infra/infra.js +38 -0
  53. package/src/infra/infra.match.js +102 -0
  54. package/src/infra/infra.scan.js +93 -0
  55. package/src/mcp/catalog.mjs +68 -0
  56. package/src/mcp/graph-context.mjs +276 -0
  57. package/src/mcp/tools-actions.mjs +132 -0
  58. package/src/mcp/tools-graph.mjs +274 -0
  59. package/src/mcp/tools-health.mjs +209 -0
  60. package/src/mcp/tools-impact.mjs +189 -0
  61. package/src/mcp-rg.mjs +51 -0
  62. package/src/mcp-server.mjs +171 -0
  63. package/src/mcp-source-tools.mjs +139 -0
  64. package/src/process.js +77 -0
  65. package/src/scan/discover.inventory.js +78 -0
  66. package/src/scan/discover.js +5 -0
  67. package/src/scan/discover.list.js +79 -0
  68. package/src/scan/discover.stack.js +227 -0
  69. package/src/scan/search.core.js +24 -0
  70. package/src/scan/search.git.js +102 -0
  71. package/src/scan/search.js +9 -0
  72. package/src/scan/search.node.js +83 -0
  73. package/src/scan/search.preview.js +49 -0
  74. package/src/scan/search.rg.js +145 -0
  75. package/src/security/advisory-store.js +177 -0
  76. package/src/security/installed.js +247 -0
  77. package/src/security/malware-file-heuristics.js +121 -0
  78. package/src/security/malware-heuristics.exclusions.js +134 -0
  79. package/src/security/malware-heuristics.js +15 -0
  80. package/src/security/malware-heuristics.roots.js +142 -0
  81. package/src/security/malware-heuristics.scan.js +87 -0
  82. package/src/security/malware-heuristics.sweep.js +122 -0
  83. package/src/security/malware-scoring.js +84 -0
  84. package/src/security/match.js +85 -0
  85. package/src/security/registry-sig.classify.js +136 -0
  86. package/src/security/registry-sig.js +18 -0
  87. package/src/security/registry-sig.rules.js +188 -0
  88. package/src/security/typosquat.js +72 -0
  89. 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
+ }