@wrongstack/techstack 0.289.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/dist/adapters/cpp.d.ts +16 -0
- package/dist/adapters/cpp.d.ts.map +1 -0
- package/dist/adapters/dart.d.ts +20 -0
- package/dist/adapters/dart.d.ts.map +1 -0
- package/dist/adapters/dotnet.d.ts +19 -0
- package/dist/adapters/dotnet.d.ts.map +1 -0
- package/dist/adapters/elixir.d.ts +16 -0
- package/dist/adapters/elixir.d.ts.map +1 -0
- package/dist/adapters/go.d.ts +20 -0
- package/dist/adapters/go.d.ts.map +1 -0
- package/dist/adapters/interface.d.ts +104 -0
- package/dist/adapters/interface.d.ts.map +1 -0
- package/dist/adapters/maven.d.ts +17 -0
- package/dist/adapters/maven.d.ts.map +1 -0
- package/dist/adapters/npm.d.ts +21 -0
- package/dist/adapters/npm.d.ts.map +1 -0
- package/dist/adapters/paths.d.ts +37 -0
- package/dist/adapters/paths.d.ts.map +1 -0
- package/dist/adapters/php.d.ts +20 -0
- package/dist/adapters/php.d.ts.map +1 -0
- package/dist/adapters/python.d.ts +26 -0
- package/dist/adapters/python.d.ts.map +1 -0
- package/dist/adapters/ruby.d.ts +16 -0
- package/dist/adapters/ruby.d.ts.map +1 -0
- package/dist/adapters/rust.d.ts +20 -0
- package/dist/adapters/rust.d.ts.map +1 -0
- package/dist/advisory/native-audit.d.ts +61 -0
- package/dist/advisory/native-audit.d.ts.map +1 -0
- package/dist/advisory/osv.d.ts +41 -0
- package/dist/advisory/osv.d.ts.map +1 -0
- package/dist/delivery/coordinator.d.ts +46 -0
- package/dist/delivery/coordinator.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +58 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/workspace.d.ts +49 -0
- package/dist/discovery/workspace.d.ts.map +1 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4287 -0
- package/dist/index.js.map +7 -0
- package/dist/policy/status.d.ts +60 -0
- package/dist/policy/status.d.ts.map +1 -0
- package/dist/registry/client.d.ts +61 -0
- package/dist/registry/client.d.ts.map +1 -0
- package/dist/registry/purl.d.ts +106 -0
- package/dist/registry/purl.d.ts.map +1 -0
- package/dist/remediation.d.ts +54 -0
- package/dist/remediation.d.ts.map +1 -0
- package/dist/research/index.d.ts +11 -0
- package/dist/research/index.d.ts.map +1 -0
- package/dist/research/llm.d.ts +37 -0
- package/dist/research/llm.d.ts.map +1 -0
- package/dist/research/researcher.d.ts +31 -0
- package/dist/research/researcher.d.ts.map +1 -0
- package/dist/research/search.d.ts +19 -0
- package/dist/research/search.d.ts.map +1 -0
- package/dist/research/triage.d.ts +33 -0
- package/dist/research/triage.d.ts.map +1 -0
- package/dist/research/types.d.ts +77 -0
- package/dist/research/types.d.ts.map +1 -0
- package/dist/sbom.d.ts +52 -0
- package/dist/sbom.d.ts.map +1 -0
- package/dist/service.d.ts +110 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/snapshot-diff.d.ts +27 -0
- package/dist/snapshot-diff.d.ts.map +1 -0
- package/dist/store/schema.d.ts +20 -0
- package/dist/store/schema.d.ts.map +1 -0
- package/dist/store/sqlite.d.ts +57 -0
- package/dist/store/sqlite.d.ts.map +1 -0
- package/dist/types.d.ts +123 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +45 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4287 @@
|
|
|
1
|
+
// src/registry/purl.ts
|
|
2
|
+
var ECOSYSTEM_TO_PURL_TYPE = {
|
|
3
|
+
npm: "npm",
|
|
4
|
+
python: "pypi",
|
|
5
|
+
rust: "cargo",
|
|
6
|
+
go: "golang",
|
|
7
|
+
dotnet: "nuget",
|
|
8
|
+
php: "composer",
|
|
9
|
+
dart: "pub",
|
|
10
|
+
maven: "maven",
|
|
11
|
+
gradle: "maven",
|
|
12
|
+
ruby: "gem",
|
|
13
|
+
swift: "swift",
|
|
14
|
+
elixir: "hex",
|
|
15
|
+
cpp: "conan"
|
|
16
|
+
};
|
|
17
|
+
var PURL_TYPE_TO_ECOSYSTEM = {
|
|
18
|
+
npm: "npm",
|
|
19
|
+
pypi: "python",
|
|
20
|
+
cargo: "rust",
|
|
21
|
+
golang: "go",
|
|
22
|
+
nuget: "dotnet",
|
|
23
|
+
composer: "php",
|
|
24
|
+
pub: "dart",
|
|
25
|
+
maven: "maven",
|
|
26
|
+
gem: "ruby",
|
|
27
|
+
swift: "swift",
|
|
28
|
+
hex: "elixir",
|
|
29
|
+
conan: "cpp"
|
|
30
|
+
};
|
|
31
|
+
function buildPurl(parts) {
|
|
32
|
+
const segments = ["pkg:", parts.type, "/"];
|
|
33
|
+
if (parts.namespace) {
|
|
34
|
+
segments.push(encodePurlSegment(parts.namespace), "/");
|
|
35
|
+
}
|
|
36
|
+
segments.push(encodePurlSegment(parts.name));
|
|
37
|
+
if (parts.version) {
|
|
38
|
+
segments.push("@", encodePurlSegment(parts.version));
|
|
39
|
+
}
|
|
40
|
+
if (parts.qualifiers && parts.qualifiers.size > 0) {
|
|
41
|
+
const qs = [...parts.qualifiers.entries()].map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
42
|
+
segments.push("?", qs);
|
|
43
|
+
}
|
|
44
|
+
if (parts.subpath) {
|
|
45
|
+
segments.push("#", encodePurlSegment(parts.subpath));
|
|
46
|
+
}
|
|
47
|
+
return segments.join("");
|
|
48
|
+
}
|
|
49
|
+
function parsePurl(purl) {
|
|
50
|
+
if (!purl.startsWith("pkg:")) return void 0;
|
|
51
|
+
const withoutPrefix = purl.slice(4);
|
|
52
|
+
let main = withoutPrefix;
|
|
53
|
+
let subpath;
|
|
54
|
+
const hashIdx = main.indexOf("#");
|
|
55
|
+
if (hashIdx >= 0) {
|
|
56
|
+
subpath = decodePurlSegment(main.slice(hashIdx + 1));
|
|
57
|
+
main = main.slice(0, hashIdx);
|
|
58
|
+
}
|
|
59
|
+
let qualifiers;
|
|
60
|
+
const qIdx = main.indexOf("?");
|
|
61
|
+
if (qIdx >= 0) {
|
|
62
|
+
const qs = main.slice(qIdx + 1);
|
|
63
|
+
main = main.slice(0, qIdx);
|
|
64
|
+
qualifiers = /* @__PURE__ */ new Map();
|
|
65
|
+
for (const pair of qs.split("&")) {
|
|
66
|
+
const eqIdx = pair.indexOf("=");
|
|
67
|
+
if (eqIdx > 0) {
|
|
68
|
+
qualifiers.set(
|
|
69
|
+
decodeURIComponent(pair.slice(0, eqIdx)),
|
|
70
|
+
decodeURIComponent(pair.slice(eqIdx + 1))
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
let version;
|
|
76
|
+
const atIdx = main.lastIndexOf("@");
|
|
77
|
+
if (atIdx > 0) {
|
|
78
|
+
version = decodePurlSegment(main.slice(atIdx + 1));
|
|
79
|
+
main = main.slice(0, atIdx);
|
|
80
|
+
}
|
|
81
|
+
const slashIdx = main.indexOf("/");
|
|
82
|
+
if (slashIdx < 0) return void 0;
|
|
83
|
+
const type = main.slice(0, slashIdx);
|
|
84
|
+
let remainder = main.slice(slashIdx + 1);
|
|
85
|
+
let namespace;
|
|
86
|
+
let name;
|
|
87
|
+
const nsSlashIdx = remainder.indexOf("/");
|
|
88
|
+
if (nsSlashIdx >= 0 && type !== "npm") {
|
|
89
|
+
namespace = decodePurlSegment(remainder.slice(0, nsSlashIdx));
|
|
90
|
+
name = decodePurlSegment(remainder.slice(nsSlashIdx + 1));
|
|
91
|
+
} else if (nsSlashIdx >= 0 && type === "npm" && remainder.startsWith("%40")) {
|
|
92
|
+
namespace = decodePurlSegment(remainder.slice(0, nsSlashIdx));
|
|
93
|
+
name = decodePurlSegment(remainder.slice(nsSlashIdx + 1));
|
|
94
|
+
} else {
|
|
95
|
+
name = decodePurlSegment(remainder);
|
|
96
|
+
}
|
|
97
|
+
if (!type || !name) return void 0;
|
|
98
|
+
return {
|
|
99
|
+
type,
|
|
100
|
+
...namespace ? { namespace } : {},
|
|
101
|
+
name,
|
|
102
|
+
...version ? { version } : {},
|
|
103
|
+
...qualifiers && qualifiers.size > 0 ? { qualifiers } : {},
|
|
104
|
+
...subpath ? { subpath } : {}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function purlTypeForEcosystem(ecosystem) {
|
|
108
|
+
return ECOSYSTEM_TO_PURL_TYPE[ecosystem];
|
|
109
|
+
}
|
|
110
|
+
function ecosystemForPurlType(type) {
|
|
111
|
+
return PURL_TYPE_TO_ECOSYSTEM[type];
|
|
112
|
+
}
|
|
113
|
+
function constructPurl(ecosystem, name, version) {
|
|
114
|
+
const type = purlTypeForEcosystem(ecosystem);
|
|
115
|
+
if (ecosystem === "go") {
|
|
116
|
+
const versionSuffix = version !== void 0 ? `@${encodePurlSegment(version)}` : "";
|
|
117
|
+
return `pkg:${type}/${name}${versionSuffix}`;
|
|
118
|
+
}
|
|
119
|
+
const slashIdx = name.indexOf("/");
|
|
120
|
+
if (slashIdx > 0) {
|
|
121
|
+
const namespace = name.slice(0, slashIdx);
|
|
122
|
+
const pkgName = name.slice(slashIdx + 1);
|
|
123
|
+
if (pkgName.length > 0) {
|
|
124
|
+
return buildPurl({
|
|
125
|
+
type,
|
|
126
|
+
namespace,
|
|
127
|
+
name: pkgName,
|
|
128
|
+
...version !== void 0 ? { version } : {}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return buildPurl({
|
|
133
|
+
type,
|
|
134
|
+
name,
|
|
135
|
+
...version !== void 0 ? { version } : {}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function parsePurlEcosystem(purl) {
|
|
139
|
+
const parts = parsePurl(purl);
|
|
140
|
+
if (!parts) return void 0;
|
|
141
|
+
const ecosystem = ecosystemForPurlType(parts.type);
|
|
142
|
+
if (!ecosystem) return void 0;
|
|
143
|
+
const namespace = parts.namespace;
|
|
144
|
+
const name = namespace ? `${namespace}/${parts.name}` : parts.name;
|
|
145
|
+
return {
|
|
146
|
+
ecosystem,
|
|
147
|
+
name,
|
|
148
|
+
...parts.version !== void 0 ? { version: parts.version } : {}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function encodePurlSegment(segment) {
|
|
152
|
+
return segment.replace(/%/g, "%25").replace(/@/g, "%40").replace(/\//g, "%2F");
|
|
153
|
+
}
|
|
154
|
+
function decodePurlSegment(segment) {
|
|
155
|
+
try {
|
|
156
|
+
return decodeURIComponent(segment);
|
|
157
|
+
} catch {
|
|
158
|
+
return segment;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/discovery/workspace.ts
|
|
163
|
+
import { detectLanguageWorkspaces } from "@wrongstack/tools/languages";
|
|
164
|
+
var STATIC_LANGUAGE_TO_ECOSYSTEM = {
|
|
165
|
+
typescript: "npm",
|
|
166
|
+
javascript: "npm",
|
|
167
|
+
deno: void 0,
|
|
168
|
+
python: "python",
|
|
169
|
+
go: "go",
|
|
170
|
+
rust: "rust",
|
|
171
|
+
csharp: "dotnet",
|
|
172
|
+
php: "php",
|
|
173
|
+
ruby: "ruby",
|
|
174
|
+
swift: "swift",
|
|
175
|
+
dart: "dart",
|
|
176
|
+
elixir: "elixir",
|
|
177
|
+
c: "cpp",
|
|
178
|
+
cpp: "cpp",
|
|
179
|
+
java: "maven",
|
|
180
|
+
// overridden by `resolveJavaEcosystem` when gradle evidence is present
|
|
181
|
+
shell: void 0
|
|
182
|
+
};
|
|
183
|
+
var ECOSYSTEM_TIER = {
|
|
184
|
+
npm: "full",
|
|
185
|
+
python: "full",
|
|
186
|
+
rust: "full",
|
|
187
|
+
go: "full",
|
|
188
|
+
dotnet: "full",
|
|
189
|
+
php: "full",
|
|
190
|
+
dart: "full",
|
|
191
|
+
maven: "partial",
|
|
192
|
+
gradle: "partial",
|
|
193
|
+
ruby: "partial",
|
|
194
|
+
swift: "partial",
|
|
195
|
+
elixir: "partial",
|
|
196
|
+
cpp: "unsupported"
|
|
197
|
+
};
|
|
198
|
+
function resolveJavaEcosystem(evidence) {
|
|
199
|
+
for (const item of evidence) {
|
|
200
|
+
if (item.kind === "lockfile" && item.value === "gradle.lockfile") return "gradle";
|
|
201
|
+
if (item.kind === "manifest" && (item.value === "build.gradle" || item.value === "build.gradle.kts" || item.value === "settings.gradle")) {
|
|
202
|
+
return "gradle";
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return "maven";
|
|
206
|
+
}
|
|
207
|
+
function ecosystemForWorkspace(detected) {
|
|
208
|
+
if (detected.language === "java") {
|
|
209
|
+
return resolveJavaEcosystem(detected.evidence);
|
|
210
|
+
}
|
|
211
|
+
return STATIC_LANGUAGE_TO_ECOSYSTEM[detected.language];
|
|
212
|
+
}
|
|
213
|
+
function extractLockfiles(evidence) {
|
|
214
|
+
const seen = /* @__PURE__ */ new Set();
|
|
215
|
+
const out = [];
|
|
216
|
+
for (const item of evidence) {
|
|
217
|
+
if (item.kind !== "lockfile") continue;
|
|
218
|
+
if (seen.has(item.path)) continue;
|
|
219
|
+
seen.add(item.path);
|
|
220
|
+
out.push(item.path);
|
|
221
|
+
}
|
|
222
|
+
return out.sort();
|
|
223
|
+
}
|
|
224
|
+
function mapDetectedWorkspace(detected, projectRoot) {
|
|
225
|
+
const ecosystem = ecosystemForWorkspace(detected);
|
|
226
|
+
if (!ecosystem) return void 0;
|
|
227
|
+
const relativeRoot = detected.root === projectRoot ? "." : detected.root.startsWith(`${projectRoot}/`) || detected.root.startsWith(`${projectRoot}\\`) ? detected.root.slice(projectRoot.length + 1) : detected.root;
|
|
228
|
+
const lockfiles = extractLockfiles(detected.evidence);
|
|
229
|
+
return {
|
|
230
|
+
id: detected.id,
|
|
231
|
+
relativeRoot,
|
|
232
|
+
ecosystem,
|
|
233
|
+
...detected.packageManager ? { packageManager: detected.packageManager } : {},
|
|
234
|
+
manifests: [...detected.manifests].sort(),
|
|
235
|
+
lockfiles,
|
|
236
|
+
confidence: Math.max(0, Math.min(1, detected.confidence)),
|
|
237
|
+
coverage: ECOSYSTEM_TIER[ecosystem]
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
var TEST_FIXTURE_DIRECTORIES = [
|
|
241
|
+
"fixtures",
|
|
242
|
+
"__fixtures__",
|
|
243
|
+
"test-fixtures",
|
|
244
|
+
"testdata",
|
|
245
|
+
"__mocks__"
|
|
246
|
+
];
|
|
247
|
+
async function discoverWorkspaces(projectRoot, options) {
|
|
248
|
+
const result = await detectLanguageWorkspaces({
|
|
249
|
+
...options ?? {},
|
|
250
|
+
projectRoot,
|
|
251
|
+
ignoredDirectories: [...TEST_FIXTURE_DIRECTORIES, ...options?.ignoredDirectories ?? []]
|
|
252
|
+
});
|
|
253
|
+
const mapped = [];
|
|
254
|
+
for (const detected of result.workspaces) {
|
|
255
|
+
const workspace = mapDetectedWorkspace(detected, result.projectRoot);
|
|
256
|
+
if (workspace) mapped.push(workspace);
|
|
257
|
+
}
|
|
258
|
+
mapped.sort((a, b) => {
|
|
259
|
+
return a.ecosystem.localeCompare(b.ecosystem) || a.relativeRoot.localeCompare(b.relativeRoot) || a.id.localeCompare(b.id);
|
|
260
|
+
});
|
|
261
|
+
return mapped;
|
|
262
|
+
}
|
|
263
|
+
function coverageForEcosystem(ecosystem) {
|
|
264
|
+
return ECOSYSTEM_TIER[ecosystem];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/adapters/npm.ts
|
|
268
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
269
|
+
import { dirname, join, relative, resolve as resolve2 } from "node:path";
|
|
270
|
+
|
|
271
|
+
// src/adapters/paths.ts
|
|
272
|
+
import { resolve } from "node:path";
|
|
273
|
+
function workspaceRoot(workspace, options) {
|
|
274
|
+
const relative2 = workspace.relativeRoot || ".";
|
|
275
|
+
return options.projectRoot ? resolve(options.projectRoot, relative2) : resolve(relative2);
|
|
276
|
+
}
|
|
277
|
+
function resolveIn(root, candidate) {
|
|
278
|
+
return resolve(root, candidate);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/adapters/npm.ts
|
|
282
|
+
function detectLockfile(workspaceDir, stopAt) {
|
|
283
|
+
const candidates = [
|
|
284
|
+
{ file: "pnpm-lock.yaml", kind: "pnpm" },
|
|
285
|
+
{ file: "package-lock.json", kind: "npm" },
|
|
286
|
+
{ file: "yarn.lock", kind: "yarn" },
|
|
287
|
+
{ file: "bun.lockb", kind: "bun" }
|
|
288
|
+
];
|
|
289
|
+
const ceiling = stopAt ? resolve2(stopAt) : void 0;
|
|
290
|
+
let dir = resolve2(workspaceDir);
|
|
291
|
+
for (; ; ) {
|
|
292
|
+
for (const c of candidates) {
|
|
293
|
+
const candidate = join(dir, c.file);
|
|
294
|
+
if (existsSync(candidate)) return { kind: c.kind, path: candidate };
|
|
295
|
+
}
|
|
296
|
+
if (ceiling && dir === ceiling) break;
|
|
297
|
+
const parent = dirname(dir);
|
|
298
|
+
if (parent === dir) break;
|
|
299
|
+
if (!ceiling) break;
|
|
300
|
+
dir = parent;
|
|
301
|
+
}
|
|
302
|
+
return { kind: "none", path: "" };
|
|
303
|
+
}
|
|
304
|
+
function stripPeerSuffix(version) {
|
|
305
|
+
const paren = version.indexOf("(");
|
|
306
|
+
return (paren === -1 ? version : version.slice(0, paren)).trim();
|
|
307
|
+
}
|
|
308
|
+
function parsePnpmImporterVersions(lockContent, importerPath) {
|
|
309
|
+
const versions = /* @__PURE__ */ new Map();
|
|
310
|
+
const lines = lockContent.split(/\r?\n/);
|
|
311
|
+
let inImporters = false;
|
|
312
|
+
let inTargetImporter = false;
|
|
313
|
+
let currentPackage;
|
|
314
|
+
for (const raw of lines) {
|
|
315
|
+
if (raw.trim() === "" || raw.trimStart().startsWith("#")) continue;
|
|
316
|
+
if (!/^\s/.test(raw)) {
|
|
317
|
+
if (inImporters) break;
|
|
318
|
+
inImporters = raw.startsWith("importers:");
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (!inImporters) continue;
|
|
322
|
+
const indent = raw.length - raw.trimStart().length;
|
|
323
|
+
const line = raw.trim();
|
|
324
|
+
if (indent === 2) {
|
|
325
|
+
const key = line.endsWith(":") ? unquote(line.slice(0, -1)) : void 0;
|
|
326
|
+
inTargetImporter = key === importerPath;
|
|
327
|
+
currentPackage = void 0;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (!inTargetImporter) continue;
|
|
331
|
+
if (indent === 4) {
|
|
332
|
+
currentPackage = void 0;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (indent === 6 && line.endsWith(":")) {
|
|
336
|
+
currentPackage = unquote(line.slice(0, -1));
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (indent >= 8 && currentPackage && line.startsWith("version:")) {
|
|
340
|
+
const version = stripPeerSuffix(unquote(line.slice("version:".length).trim()));
|
|
341
|
+
if (version && !version.startsWith("link:") && !version.startsWith("file:")) {
|
|
342
|
+
versions.set(currentPackage, version);
|
|
343
|
+
}
|
|
344
|
+
currentPackage = void 0;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return versions;
|
|
348
|
+
}
|
|
349
|
+
function unquote(value) {
|
|
350
|
+
const trimmed = value.trim();
|
|
351
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'") || trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
352
|
+
return trimmed.slice(1, -1);
|
|
353
|
+
}
|
|
354
|
+
return trimmed;
|
|
355
|
+
}
|
|
356
|
+
function parseNpmLockVersions(lockContent) {
|
|
357
|
+
const versions = /* @__PURE__ */ new Map();
|
|
358
|
+
try {
|
|
359
|
+
const lock = JSON.parse(lockContent);
|
|
360
|
+
const deps = lock.dependencies ?? {};
|
|
361
|
+
for (const [name, info] of Object.entries(deps)) {
|
|
362
|
+
const depInfo = info;
|
|
363
|
+
if (depInfo.version) {
|
|
364
|
+
const cleanVersion = depInfo.version.replace(/^[^0-9]+/, "");
|
|
365
|
+
versions.set(name, cleanVersion);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const packages = lock.packages ?? {};
|
|
369
|
+
for (const key of Object.keys(packages)) {
|
|
370
|
+
const pkgInfo = packages[key];
|
|
371
|
+
if (pkgInfo.version) {
|
|
372
|
+
const name = key.replace(/^node_modules\//, "");
|
|
373
|
+
if (!versions.has(name)) {
|
|
374
|
+
versions.set(name, pkgInfo.version);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
} catch {
|
|
379
|
+
}
|
|
380
|
+
return versions;
|
|
381
|
+
}
|
|
382
|
+
function manifestEvidence(path) {
|
|
383
|
+
return {
|
|
384
|
+
kind: "manifest",
|
|
385
|
+
source: path,
|
|
386
|
+
retrievedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function lockfileEvidence(path) {
|
|
390
|
+
return {
|
|
391
|
+
kind: "lockfile",
|
|
392
|
+
source: path,
|
|
393
|
+
retrievedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
function scopeForSection(section) {
|
|
397
|
+
switch (section) {
|
|
398
|
+
case "dependencies":
|
|
399
|
+
return "runtime";
|
|
400
|
+
case "devDependencies":
|
|
401
|
+
return "development";
|
|
402
|
+
case "peerDependencies":
|
|
403
|
+
return "peer";
|
|
404
|
+
case "optionalDependencies":
|
|
405
|
+
return "optional";
|
|
406
|
+
default:
|
|
407
|
+
return "runtime";
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function statusForSpec(spec) {
|
|
411
|
+
if (spec.startsWith("file:") || spec.startsWith("link:") || spec.startsWith("workspace:")) {
|
|
412
|
+
return "local_path";
|
|
413
|
+
}
|
|
414
|
+
if (spec.startsWith("git+") || spec.startsWith("github:") || spec.startsWith("git:")) {
|
|
415
|
+
return "git_dependency";
|
|
416
|
+
}
|
|
417
|
+
return "current";
|
|
418
|
+
}
|
|
419
|
+
function isRegistrySpec(spec) {
|
|
420
|
+
return !spec.startsWith("file:") && !spec.startsWith("link:") && !spec.startsWith("workspace:") && !spec.startsWith("git+") && !spec.startsWith("github:") && !spec.startsWith("git:");
|
|
421
|
+
}
|
|
422
|
+
var NpmAdapter = class {
|
|
423
|
+
ecosystem = "npm";
|
|
424
|
+
async inventory(workspace, options) {
|
|
425
|
+
const observations = [];
|
|
426
|
+
const root = workspaceRoot(workspace, options);
|
|
427
|
+
const manifestPath = resolveIn(
|
|
428
|
+
root,
|
|
429
|
+
workspace.manifests.find((m) => m.endsWith("package.json")) ?? "package.json"
|
|
430
|
+
);
|
|
431
|
+
let pkg;
|
|
432
|
+
let manifestContent;
|
|
433
|
+
try {
|
|
434
|
+
manifestContent = readFileSync(manifestPath, "utf-8");
|
|
435
|
+
pkg = JSON.parse(manifestContent);
|
|
436
|
+
} catch {
|
|
437
|
+
return [];
|
|
438
|
+
}
|
|
439
|
+
const manifestEv = manifestEvidence(manifestPath);
|
|
440
|
+
const lockInfo = detectLockfile(root, options.projectRoot);
|
|
441
|
+
const resolvedVersions = /* @__PURE__ */ new Map();
|
|
442
|
+
let lockEv;
|
|
443
|
+
if (lockInfo.kind === "pnpm") {
|
|
444
|
+
try {
|
|
445
|
+
const lockContent = readFileSync(lockInfo.path, "utf-8");
|
|
446
|
+
const importerPath = relative(dirname(lockInfo.path), root).split(/[/\\]/).filter(Boolean).join("/") || ".";
|
|
447
|
+
const parsed = parsePnpmImporterVersions(lockContent, importerPath);
|
|
448
|
+
for (const [k, v] of parsed) resolvedVersions.set(k, v);
|
|
449
|
+
if (parsed.size > 0) lockEv = lockfileEvidence(lockInfo.path);
|
|
450
|
+
} catch {
|
|
451
|
+
}
|
|
452
|
+
} else if (lockInfo.kind === "npm") {
|
|
453
|
+
try {
|
|
454
|
+
const lockContent = readFileSync(lockInfo.path, "utf-8");
|
|
455
|
+
const parsed = parseNpmLockVersions(lockContent);
|
|
456
|
+
for (const [k, v] of parsed) resolvedVersions.set(k, v);
|
|
457
|
+
lockEv = lockfileEvidence(lockInfo.path);
|
|
458
|
+
} catch {
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const sections = [
|
|
462
|
+
{ name: "dependencies", deps: pkg.dependencies },
|
|
463
|
+
{ name: "devDependencies", deps: pkg.devDependencies },
|
|
464
|
+
{ name: "peerDependencies", deps: pkg.peerDependencies },
|
|
465
|
+
{ name: "optionalDependencies", deps: pkg.optionalDependencies }
|
|
466
|
+
];
|
|
467
|
+
const seen = /* @__PURE__ */ new Set();
|
|
468
|
+
for (const section of sections) {
|
|
469
|
+
if (!section.deps) continue;
|
|
470
|
+
const scope = scopeForSection(section.name);
|
|
471
|
+
for (const [name, requested] of Object.entries(section.deps)) {
|
|
472
|
+
const dedupKey2 = `${name}`;
|
|
473
|
+
if (seen.has(dedupKey2)) continue;
|
|
474
|
+
seen.add(dedupKey2);
|
|
475
|
+
const isRegistry = isRegistrySpec(requested);
|
|
476
|
+
const status = statusForSpec(requested);
|
|
477
|
+
const locked = resolvedVersions.get(name);
|
|
478
|
+
const purl = isRegistry && locked ? buildPurl({ type: "npm", name, version: locked }) : isRegistry ? buildPurl({ type: "npm", name }) : void 0;
|
|
479
|
+
const evidence = [manifestEv];
|
|
480
|
+
if (lockEv && locked) evidence.push(lockEv);
|
|
481
|
+
observations.push({
|
|
482
|
+
id: `dep-${workspace.id}-${name}`,
|
|
483
|
+
workspaceId: workspace.id,
|
|
484
|
+
...purl ? { purl } : {},
|
|
485
|
+
ecosystem: "npm",
|
|
486
|
+
name,
|
|
487
|
+
sourceType: isRegistry ? "registry" : status === "local_path" ? "path" : "git",
|
|
488
|
+
direct: true,
|
|
489
|
+
scope,
|
|
490
|
+
requested,
|
|
491
|
+
...locked ? { locked } : {},
|
|
492
|
+
status,
|
|
493
|
+
evidence
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return observations;
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
var npmAdapter = new NpmAdapter();
|
|
501
|
+
|
|
502
|
+
// src/adapters/python.ts
|
|
503
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
504
|
+
import { join as join2 } from "node:path";
|
|
505
|
+
function manifestEvidence2(path) {
|
|
506
|
+
return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
507
|
+
}
|
|
508
|
+
function lockfileEvidence2(path) {
|
|
509
|
+
return { kind: "lockfile", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
510
|
+
}
|
|
511
|
+
function parseTomlSections(content) {
|
|
512
|
+
const sections = [];
|
|
513
|
+
let currentSection = "__header__";
|
|
514
|
+
let currentLines = [];
|
|
515
|
+
for (const raw of content.split("\n")) {
|
|
516
|
+
const line = raw.trim();
|
|
517
|
+
if (line.startsWith("#") || line === "") continue;
|
|
518
|
+
const sectionMatch = line.match(/^\[([^\]]+)\]$/);
|
|
519
|
+
if (sectionMatch) {
|
|
520
|
+
if (currentLines.length > 0) sections.push({ name: currentSection, lines: currentLines });
|
|
521
|
+
currentSection = sectionMatch[1];
|
|
522
|
+
currentLines = [];
|
|
523
|
+
} else {
|
|
524
|
+
currentLines.push(raw);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (currentLines.length > 0) sections.push({ name: currentSection, lines: currentLines });
|
|
528
|
+
return sections;
|
|
529
|
+
}
|
|
530
|
+
function extractTomlArray(sectionLines, key) {
|
|
531
|
+
const result = [];
|
|
532
|
+
let inArray = false;
|
|
533
|
+
for (const line of sectionLines) {
|
|
534
|
+
const trimmed = line.trim();
|
|
535
|
+
if (!inArray) {
|
|
536
|
+
const match = trimmed.match(new RegExp(`^${key}\\s*=\\s*\\[`));
|
|
537
|
+
if (match) {
|
|
538
|
+
inArray = true;
|
|
539
|
+
const rest = trimmed.slice(match[0].length);
|
|
540
|
+
if (rest.includes("]")) {
|
|
541
|
+
const items = rest.replace(/\]\s*,?\s*$/, "").trim();
|
|
542
|
+
for (const item of items.split(",")) {
|
|
543
|
+
const cleaned = item.trim().replace(/^"|"$/g, "").trim();
|
|
544
|
+
if (cleaned) result.push(cleaned);
|
|
545
|
+
}
|
|
546
|
+
inArray = false;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
} else {
|
|
550
|
+
const closeIdx = trimmed.indexOf("]");
|
|
551
|
+
if (closeIdx >= 0) {
|
|
552
|
+
const items = trimmed.slice(0, closeIdx).trim();
|
|
553
|
+
for (const item of items.split(",")) {
|
|
554
|
+
const cleaned = item.trim().replace(/^"|"$/g, "").trim();
|
|
555
|
+
if (cleaned) result.push(cleaned);
|
|
556
|
+
}
|
|
557
|
+
inArray = false;
|
|
558
|
+
} else {
|
|
559
|
+
const cleaned = trimmed.replace(/,$/, "").trim().replace(/^"|"$/g, "").trim();
|
|
560
|
+
if (cleaned) result.push(cleaned);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return result;
|
|
565
|
+
}
|
|
566
|
+
function parsePep508(spec) {
|
|
567
|
+
let s = spec.trim();
|
|
568
|
+
s = s.replace(/\[.*?\]/g, "");
|
|
569
|
+
const match = s.match(/^([a-zA-Z0-9][a-zA-Z0-9._-]*)\s*(.*)$/);
|
|
570
|
+
if (!match) return { name: s, constraint: void 0 };
|
|
571
|
+
return { name: match[1], constraint: match[2]?.trim() || void 0 };
|
|
572
|
+
}
|
|
573
|
+
function parsePyprojectDeps(content) {
|
|
574
|
+
const deps = [];
|
|
575
|
+
const sections = parseTomlSections(content);
|
|
576
|
+
const projectSection = sections.find((s) => s.name === "project");
|
|
577
|
+
if (projectSection) {
|
|
578
|
+
const depSpecs = extractTomlArray(projectSection.lines, "dependencies");
|
|
579
|
+
for (const spec of depSpecs) {
|
|
580
|
+
const { name, constraint } = parsePep508(spec);
|
|
581
|
+
if (name) deps.push({ name, constraint, scope: "runtime" });
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
for (const section of sections) {
|
|
585
|
+
if (section.name === "project.optional-dependencies") {
|
|
586
|
+
for (const line of section.lines) {
|
|
587
|
+
const trimmed = line.trim();
|
|
588
|
+
const groupMatch = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=\s*\[/);
|
|
589
|
+
if (groupMatch) {
|
|
590
|
+
const depSpecs = extractTomlArray(section.lines, groupMatch[1]);
|
|
591
|
+
for (const spec of depSpecs) {
|
|
592
|
+
const { name, constraint } = parsePep508(spec);
|
|
593
|
+
if (name) deps.push({ name, constraint, scope: "optional" });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return deps;
|
|
600
|
+
}
|
|
601
|
+
function parseRequirementsTxt(content) {
|
|
602
|
+
const deps = [];
|
|
603
|
+
for (const raw of content.split("\n")) {
|
|
604
|
+
const line = raw.trim();
|
|
605
|
+
if (!line || line.startsWith("#") || line.startsWith("-")) continue;
|
|
606
|
+
const { name, constraint } = parsePep508(line);
|
|
607
|
+
if (name) deps.push({ name, constraint });
|
|
608
|
+
}
|
|
609
|
+
return deps;
|
|
610
|
+
}
|
|
611
|
+
function parsePipfileDeps(content) {
|
|
612
|
+
const deps = [];
|
|
613
|
+
const sections = parseTomlSections(content);
|
|
614
|
+
for (const section of sections) {
|
|
615
|
+
const scope = section.name === "dev-packages" ? "development" : "runtime";
|
|
616
|
+
for (const line of section.lines) {
|
|
617
|
+
const trimmed = line.trim();
|
|
618
|
+
if (trimmed.startsWith("#")) continue;
|
|
619
|
+
const match = trimmed.match(/^([a-zA-Z0-9][a-zA-Z0-9._-]*)\s*=\s*"([^"]*)"$/);
|
|
620
|
+
if (match) {
|
|
621
|
+
const constraint = match[2] === "*" ? void 0 : match[2];
|
|
622
|
+
deps.push({ name: match[1], constraint, scope });
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return deps;
|
|
627
|
+
}
|
|
628
|
+
function parseRequirementsLockVersions(content) {
|
|
629
|
+
const versions = /* @__PURE__ */ new Map();
|
|
630
|
+
for (const raw of content.split("\n")) {
|
|
631
|
+
const line = raw.trim();
|
|
632
|
+
if (!line || line.startsWith("#") || line.startsWith("-")) continue;
|
|
633
|
+
const match = line.match(/^([a-zA-Z0-9][a-zA-Z0-9._-]*)\s*==\s*([^\s;]+)/);
|
|
634
|
+
if (match) versions.set(match[1], match[2]);
|
|
635
|
+
}
|
|
636
|
+
return versions;
|
|
637
|
+
}
|
|
638
|
+
var PythonAdapter = class {
|
|
639
|
+
ecosystem = "python";
|
|
640
|
+
async inventory(workspace, options) {
|
|
641
|
+
const observations = [];
|
|
642
|
+
const root = workspaceRoot(workspace, options);
|
|
643
|
+
const seen = /* @__PURE__ */ new Set();
|
|
644
|
+
const hasPyproject = workspace.manifests.some((m) => m.includes("pyproject.toml")) || this.fileExists(join2(root, "pyproject.toml"));
|
|
645
|
+
const hasRequirements = workspace.manifests.some((m) => m.includes("requirements.txt")) || this.fileExists(join2(root, "requirements.txt"));
|
|
646
|
+
const hasPipfile = workspace.manifests.some((m) => m.includes("Pipfile")) || this.fileExists(join2(root, "Pipfile"));
|
|
647
|
+
const lockfilePath = this.detectLockfile(root);
|
|
648
|
+
let allDeps = [];
|
|
649
|
+
let pyprojectEv;
|
|
650
|
+
if (hasPyproject) {
|
|
651
|
+
try {
|
|
652
|
+
const content = readFileSync2(join2(root, "pyproject.toml"), "utf-8");
|
|
653
|
+
pyprojectEv = manifestEvidence2(join2(root, "pyproject.toml"));
|
|
654
|
+
const parsed = parsePyprojectDeps(content);
|
|
655
|
+
for (const d of parsed) allDeps.push({ ...d, source: "pyproject.toml" });
|
|
656
|
+
} catch {
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
let reqLockVersions = /* @__PURE__ */ new Map();
|
|
660
|
+
let requirementsEv;
|
|
661
|
+
if (hasRequirements) {
|
|
662
|
+
try {
|
|
663
|
+
const content = readFileSync2(join2(root, "requirements.txt"), "utf-8");
|
|
664
|
+
requirementsEv = manifestEvidence2(join2(root, "requirements.txt"));
|
|
665
|
+
const parsed = parseRequirementsTxt(content);
|
|
666
|
+
for (const d of parsed) {
|
|
667
|
+
if (!allDeps.some((existing) => existing.name === d.name)) {
|
|
668
|
+
allDeps.push({ ...d, scope: "runtime", source: "requirements.txt" });
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
reqLockVersions = parseRequirementsLockVersions(content);
|
|
672
|
+
} catch {
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (hasPipfile) {
|
|
676
|
+
try {
|
|
677
|
+
const content = readFileSync2(join2(root, "Pipfile"), "utf-8");
|
|
678
|
+
if (!pyprojectEv) pyprojectEv = manifestEvidence2(join2(root, "Pipfile"));
|
|
679
|
+
const parsed = parsePipfileDeps(content);
|
|
680
|
+
for (const d of parsed) {
|
|
681
|
+
if (!allDeps.some((existing) => existing.name === d.name)) {
|
|
682
|
+
allDeps.push({ ...d, source: "Pipfile" });
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
} catch {
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
let lockEv;
|
|
689
|
+
if (lockfilePath) {
|
|
690
|
+
try {
|
|
691
|
+
readFileSync2(lockfilePath, "utf-8");
|
|
692
|
+
lockEv = lockfileEvidence2(lockfilePath);
|
|
693
|
+
} catch {
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const manifestEv = pyprojectEv || requirementsEv;
|
|
697
|
+
for (const dep of allDeps) {
|
|
698
|
+
if (seen.has(dep.name)) continue;
|
|
699
|
+
seen.add(dep.name);
|
|
700
|
+
const locked = reqLockVersions.get(dep.name) || void 0;
|
|
701
|
+
const isRegistry = !dep.constraint || !dep.constraint.startsWith("file:") && !dep.constraint.startsWith("git+") && !dep.constraint.startsWith("-e");
|
|
702
|
+
const purl = isRegistry && locked ? buildPurl({ type: "python", name: dep.name, version: locked }) : isRegistry ? buildPurl({ type: "python", name: dep.name }) : void 0;
|
|
703
|
+
const evidence = [];
|
|
704
|
+
if (manifestEv) evidence.push(manifestEv);
|
|
705
|
+
if (lockEv && locked) evidence.push(lockEv);
|
|
706
|
+
if (evidence.length === 0) {
|
|
707
|
+
evidence.push({ kind: "manifest", source: dep.source, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
708
|
+
}
|
|
709
|
+
const status = dep.constraint && (dep.constraint.startsWith("file:") || dep.constraint.startsWith("-e")) ? "local_path" : dep.constraint?.startsWith("git+") ? "git_dependency" : "current";
|
|
710
|
+
observations.push({
|
|
711
|
+
id: `dep-${workspace.id}-${dep.name}`,
|
|
712
|
+
workspaceId: workspace.id,
|
|
713
|
+
...purl ? { purl } : {},
|
|
714
|
+
ecosystem: "python",
|
|
715
|
+
name: dep.name,
|
|
716
|
+
sourceType: isRegistry ? "registry" : status === "local_path" ? "path" : "git",
|
|
717
|
+
direct: true,
|
|
718
|
+
scope: dep.scope,
|
|
719
|
+
...dep.constraint ? { requested: dep.constraint } : {},
|
|
720
|
+
...locked ? { locked } : {},
|
|
721
|
+
status,
|
|
722
|
+
evidence
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
return observations;
|
|
726
|
+
}
|
|
727
|
+
fileExists(filePath) {
|
|
728
|
+
try {
|
|
729
|
+
readFileSync2(filePath, "utf-8");
|
|
730
|
+
return true;
|
|
731
|
+
} catch {
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
detectLockfile(workspaceRoot2) {
|
|
736
|
+
for (const file of ["Pipfile.lock", "poetry.lock", "uv.lock"]) {
|
|
737
|
+
try {
|
|
738
|
+
readFileSync2(join2(workspaceRoot2, file), "utf-8");
|
|
739
|
+
return join2(workspaceRoot2, file);
|
|
740
|
+
} catch {
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return void 0;
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
var pythonAdapter = new PythonAdapter();
|
|
747
|
+
|
|
748
|
+
// src/adapters/rust.ts
|
|
749
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
750
|
+
function manifestEvidence3(path) {
|
|
751
|
+
return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
752
|
+
}
|
|
753
|
+
function lockfileEvidence3(path) {
|
|
754
|
+
return { kind: "lockfile", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
755
|
+
}
|
|
756
|
+
function parseTomlSections2(content) {
|
|
757
|
+
const sections = [];
|
|
758
|
+
let currentSection = "__header__";
|
|
759
|
+
let currentLines = [];
|
|
760
|
+
for (const raw of content.split("\n")) {
|
|
761
|
+
const line = raw.trim();
|
|
762
|
+
if (line.startsWith("#") || line === "") continue;
|
|
763
|
+
const sectionMatch = line.match(/^\[([^\]]+)\]$/);
|
|
764
|
+
if (sectionMatch) {
|
|
765
|
+
if (currentLines.length > 0) sections.push({ name: currentSection, lines: currentLines });
|
|
766
|
+
currentSection = sectionMatch[1];
|
|
767
|
+
currentLines = [];
|
|
768
|
+
} else {
|
|
769
|
+
currentLines.push(raw);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (currentLines.length > 0) sections.push({ name: currentSection, lines: currentLines });
|
|
773
|
+
return sections;
|
|
774
|
+
}
|
|
775
|
+
function parseTomlKeyValue(line) {
|
|
776
|
+
const trimmed = line.trim();
|
|
777
|
+
if (trimmed.startsWith("#")) return void 0;
|
|
778
|
+
const match = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=\s*(.+)$/);
|
|
779
|
+
if (!match) return void 0;
|
|
780
|
+
return { key: match[1], value: match[2].trim() };
|
|
781
|
+
}
|
|
782
|
+
function extractTomlDeps(sectionLines) {
|
|
783
|
+
const deps = [];
|
|
784
|
+
for (const raw of sectionLines) {
|
|
785
|
+
const line = raw.trim();
|
|
786
|
+
if (line.startsWith("#") || line === "") continue;
|
|
787
|
+
const tableMatch = line.match(/^([a-zA-Z0-9_-]+)\s*=\s*\{\s*(.*?)\s*\}$/);
|
|
788
|
+
if (tableMatch) {
|
|
789
|
+
const name = tableMatch[1];
|
|
790
|
+
const inner = tableMatch[2];
|
|
791
|
+
const versionMatch = inner.match(/version\s*=\s*"([^"]+)"/);
|
|
792
|
+
deps.push({ name, version: versionMatch ? versionMatch[1] : void 0 });
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
const simpleMatch = line.match(/^([a-zA-Z0-9_-]+)\s*=\s*"([^"]*)"$/);
|
|
796
|
+
if (simpleMatch) {
|
|
797
|
+
deps.push({ name: simpleMatch[1], version: simpleMatch[2] || void 0 });
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
const partialMatch = parseTomlKeyValue(line);
|
|
801
|
+
if (partialMatch && !partialMatch.value.startsWith("{") && !partialMatch.value.startsWith('"')) {
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return deps;
|
|
805
|
+
}
|
|
806
|
+
function parseCargoLock(content) {
|
|
807
|
+
const versions = /* @__PURE__ */ new Map();
|
|
808
|
+
const lines = content.split("\n");
|
|
809
|
+
let currentName;
|
|
810
|
+
let currentVersion;
|
|
811
|
+
let inPackage = false;
|
|
812
|
+
for (const raw of lines) {
|
|
813
|
+
const line = raw.trim();
|
|
814
|
+
if (line.startsWith("#") || line === "") continue;
|
|
815
|
+
if (line.startsWith("[[") && line.includes("package")) {
|
|
816
|
+
if (inPackage && currentName && currentVersion) {
|
|
817
|
+
versions.set(currentName, currentVersion);
|
|
818
|
+
}
|
|
819
|
+
currentName = void 0;
|
|
820
|
+
currentVersion = void 0;
|
|
821
|
+
inPackage = true;
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
if (inPackage) {
|
|
825
|
+
if (line.startsWith("name")) {
|
|
826
|
+
const m = line.match(/^name\s*=\s*"([^"]+)"/);
|
|
827
|
+
if (m) currentName = m[1];
|
|
828
|
+
} else if (line.startsWith("version")) {
|
|
829
|
+
const m = line.match(/^version\s*=\s*"([^"]+)"/);
|
|
830
|
+
if (m) currentVersion = m[1];
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
if (inPackage && currentName && currentVersion) {
|
|
835
|
+
versions.set(currentName, currentVersion);
|
|
836
|
+
}
|
|
837
|
+
return versions;
|
|
838
|
+
}
|
|
839
|
+
function scopeForCargoSection(section) {
|
|
840
|
+
switch (section) {
|
|
841
|
+
case "dependencies":
|
|
842
|
+
return "runtime";
|
|
843
|
+
case "dev-dependencies":
|
|
844
|
+
return "development";
|
|
845
|
+
case "build-dependencies":
|
|
846
|
+
return "build";
|
|
847
|
+
default:
|
|
848
|
+
return "runtime";
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
var RustAdapter = class {
|
|
852
|
+
ecosystem = "rust";
|
|
853
|
+
async inventory(workspace, options) {
|
|
854
|
+
const observations = [];
|
|
855
|
+
const root = workspaceRoot(workspace, options);
|
|
856
|
+
const seen = /* @__PURE__ */ new Set();
|
|
857
|
+
const cargoTomlPath = workspace.manifests.find((m) => m.includes("Cargo.toml")) || (this.fileExists(resolveIn(root, "Cargo.toml")) ? "Cargo.toml" : void 0);
|
|
858
|
+
if (!cargoTomlPath) return [];
|
|
859
|
+
const fullManifestPath = resolveIn(root, cargoTomlPath);
|
|
860
|
+
let cargoContent;
|
|
861
|
+
try {
|
|
862
|
+
cargoContent = readFileSync3(fullManifestPath, "utf-8");
|
|
863
|
+
} catch {
|
|
864
|
+
return [];
|
|
865
|
+
}
|
|
866
|
+
const manifestEv = manifestEvidence3(fullManifestPath);
|
|
867
|
+
const cargoLockPath = resolveIn(root, "Cargo.lock");
|
|
868
|
+
let lockVersions = /* @__PURE__ */ new Map();
|
|
869
|
+
let lockEv;
|
|
870
|
+
try {
|
|
871
|
+
const lockContent = readFileSync3(cargoLockPath, "utf-8");
|
|
872
|
+
lockVersions = parseCargoLock(lockContent);
|
|
873
|
+
lockEv = lockfileEvidence3(cargoLockPath);
|
|
874
|
+
} catch {
|
|
875
|
+
}
|
|
876
|
+
const sections = parseTomlSections2(cargoContent);
|
|
877
|
+
const depSections = ["dependencies", "dev-dependencies", "build-dependencies"];
|
|
878
|
+
for (const section of sections) {
|
|
879
|
+
const sectionName = section.name;
|
|
880
|
+
let matchedScope;
|
|
881
|
+
for (const depSec of depSections) {
|
|
882
|
+
if (sectionName === depSec || sectionName.endsWith(`.${depSec}`)) {
|
|
883
|
+
matchedScope = depSec;
|
|
884
|
+
break;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (!matchedScope) continue;
|
|
888
|
+
const scope = scopeForCargoSection(matchedScope);
|
|
889
|
+
const deps = extractTomlDeps(section.lines);
|
|
890
|
+
for (const dep of deps) {
|
|
891
|
+
if (seen.has(dep.name)) continue;
|
|
892
|
+
seen.add(dep.name);
|
|
893
|
+
const locked = lockVersions.get(dep.name) || dep.version;
|
|
894
|
+
const isRegistry = !dep.version || !dep.version.startsWith("path=") && !dep.version.startsWith("git=") && !dep.version.startsWith("../");
|
|
895
|
+
const purl = isRegistry && locked ? buildPurl({ type: "rust", name: dep.name, version: locked }) : isRegistry ? buildPurl({ type: "rust", name: dep.name }) : void 0;
|
|
896
|
+
const evidence = [manifestEv];
|
|
897
|
+
if (lockEv && locked && lockVersions.has(dep.name)) evidence.push(lockEv);
|
|
898
|
+
const status = dep.version && (dep.version.startsWith("path=") || dep.version.startsWith("git=")) ? dep.version.startsWith("git=") ? "git_dependency" : "local_path" : "current";
|
|
899
|
+
observations.push({
|
|
900
|
+
id: `dep-${workspace.id}-${dep.name}`,
|
|
901
|
+
workspaceId: workspace.id,
|
|
902
|
+
...purl ? { purl } : {},
|
|
903
|
+
ecosystem: "rust",
|
|
904
|
+
name: dep.name,
|
|
905
|
+
sourceType: isRegistry ? "registry" : status === "local_path" ? "path" : "git",
|
|
906
|
+
direct: true,
|
|
907
|
+
scope,
|
|
908
|
+
...dep.version ? { requested: dep.version } : {},
|
|
909
|
+
...locked ? { locked } : {},
|
|
910
|
+
status,
|
|
911
|
+
evidence
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return observations;
|
|
916
|
+
}
|
|
917
|
+
fileExists(filePath) {
|
|
918
|
+
try {
|
|
919
|
+
readFileSync3(filePath, "utf-8");
|
|
920
|
+
return true;
|
|
921
|
+
} catch {
|
|
922
|
+
return false;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
var rustAdapter = new RustAdapter();
|
|
927
|
+
|
|
928
|
+
// src/adapters/go.ts
|
|
929
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
930
|
+
function manifestEvidence4(path) {
|
|
931
|
+
return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
932
|
+
}
|
|
933
|
+
function lockfileEvidence4(path) {
|
|
934
|
+
return { kind: "lockfile", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
935
|
+
}
|
|
936
|
+
function cleanGoVersion(v) {
|
|
937
|
+
return v.replace(/^v/i, "");
|
|
938
|
+
}
|
|
939
|
+
function parseGoMod(content) {
|
|
940
|
+
const deps = [];
|
|
941
|
+
const lines = content.split("\n");
|
|
942
|
+
let inRequireBlock = false;
|
|
943
|
+
for (const raw of lines) {
|
|
944
|
+
const line = raw.trim();
|
|
945
|
+
if (line === "" || line.startsWith("//")) continue;
|
|
946
|
+
if (line.startsWith("require (") && line.endsWith("(")) {
|
|
947
|
+
inRequireBlock = true;
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
if (line.startsWith("require ") && !line.includes("(")) {
|
|
951
|
+
const m = line.match(/^require\s+(\S+)\s+(\S+)/);
|
|
952
|
+
if (m) {
|
|
953
|
+
const indirect = raw.includes("// indirect");
|
|
954
|
+
deps.push({ modulePath: m[1], version: cleanGoVersion(m[2]), indirect });
|
|
955
|
+
}
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
if (inRequireBlock) {
|
|
959
|
+
if (line === ")") {
|
|
960
|
+
inRequireBlock = false;
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
const m = line.match(/^(\S+)\s+(\S+)/);
|
|
964
|
+
if (m) {
|
|
965
|
+
const indirect = raw.includes("// indirect");
|
|
966
|
+
deps.push({ modulePath: m[1], version: cleanGoVersion(m[2]), indirect });
|
|
967
|
+
}
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
if (line.startsWith("exclude") || line.startsWith("replace") || line.startsWith("retract")) {
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return deps;
|
|
975
|
+
}
|
|
976
|
+
function parseGoSum(content) {
|
|
977
|
+
const versions = /* @__PURE__ */ new Map();
|
|
978
|
+
for (const raw of content.split("\n")) {
|
|
979
|
+
const line = raw.trim();
|
|
980
|
+
if (!line) continue;
|
|
981
|
+
const m = line.match(/^(\S+)\s+(\S+)\s+\S+/);
|
|
982
|
+
if (m) {
|
|
983
|
+
const modulePath = m[1];
|
|
984
|
+
const version = cleanGoVersion(m[2]);
|
|
985
|
+
if (!versions.has(modulePath)) {
|
|
986
|
+
versions.set(modulePath, version);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return versions;
|
|
991
|
+
}
|
|
992
|
+
function parseGoModuleName(content) {
|
|
993
|
+
for (const raw of content.split("\n")) {
|
|
994
|
+
const line = raw.trim();
|
|
995
|
+
const m = line.match(/^module\s+(\S+)/);
|
|
996
|
+
if (m) return m[1];
|
|
997
|
+
}
|
|
998
|
+
return void 0;
|
|
999
|
+
}
|
|
1000
|
+
var GoAdapter = class {
|
|
1001
|
+
ecosystem = "go";
|
|
1002
|
+
async inventory(workspace, options) {
|
|
1003
|
+
const observations = [];
|
|
1004
|
+
const root = workspaceRoot(workspace, options);
|
|
1005
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1006
|
+
const goModPath = workspace.manifests.find((m) => m.includes("go.mod")) || (this.fileExists(resolveIn(root, "go.mod")) ? "go.mod" : void 0);
|
|
1007
|
+
if (!goModPath) return [];
|
|
1008
|
+
const fullManifestPath = resolveIn(root, goModPath);
|
|
1009
|
+
let goModContent;
|
|
1010
|
+
try {
|
|
1011
|
+
goModContent = readFileSync4(fullManifestPath, "utf-8");
|
|
1012
|
+
} catch {
|
|
1013
|
+
return [];
|
|
1014
|
+
}
|
|
1015
|
+
const manifestEv = manifestEvidence4(fullManifestPath);
|
|
1016
|
+
const requires = parseGoMod(goModContent);
|
|
1017
|
+
const modName = parseGoModuleName(goModContent);
|
|
1018
|
+
const goSumPath = resolveIn(root, "go.sum");
|
|
1019
|
+
let lockVersions = /* @__PURE__ */ new Map();
|
|
1020
|
+
let lockEv;
|
|
1021
|
+
try {
|
|
1022
|
+
const sumContent = readFileSync4(goSumPath, "utf-8");
|
|
1023
|
+
lockVersions = parseGoSum(sumContent);
|
|
1024
|
+
lockEv = lockfileEvidence4(goSumPath);
|
|
1025
|
+
} catch {
|
|
1026
|
+
}
|
|
1027
|
+
for (const req of requires) {
|
|
1028
|
+
if (seen.has(req.modulePath)) continue;
|
|
1029
|
+
seen.add(req.modulePath);
|
|
1030
|
+
if (req.modulePath === modName) continue;
|
|
1031
|
+
const scope = req.indirect ? "transitive" : "runtime";
|
|
1032
|
+
const direct = !req.indirect;
|
|
1033
|
+
const locked = lockVersions.get(req.modulePath) || req.version;
|
|
1034
|
+
const purl = buildPurl({ type: "go", name: req.modulePath, version: locked });
|
|
1035
|
+
const evidence = [manifestEv];
|
|
1036
|
+
if (lockEv && lockVersions.has(req.modulePath)) evidence.push(lockEv);
|
|
1037
|
+
observations.push({
|
|
1038
|
+
id: `dep-${workspace.id}-${req.modulePath}`,
|
|
1039
|
+
workspaceId: workspace.id,
|
|
1040
|
+
purl,
|
|
1041
|
+
ecosystem: "go",
|
|
1042
|
+
name: req.modulePath,
|
|
1043
|
+
sourceType: "registry",
|
|
1044
|
+
direct,
|
|
1045
|
+
scope,
|
|
1046
|
+
requested: req.version,
|
|
1047
|
+
...locked ? { locked } : {},
|
|
1048
|
+
status: "current",
|
|
1049
|
+
evidence
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
return observations;
|
|
1053
|
+
}
|
|
1054
|
+
fileExists(filePath) {
|
|
1055
|
+
try {
|
|
1056
|
+
readFileSync4(filePath, "utf-8");
|
|
1057
|
+
return true;
|
|
1058
|
+
} catch {
|
|
1059
|
+
return false;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
var goAdapter = new GoAdapter();
|
|
1064
|
+
|
|
1065
|
+
// src/adapters/dotnet.ts
|
|
1066
|
+
import { readFileSync as readFileSync5, readdirSync } from "node:fs";
|
|
1067
|
+
import { join as join3 } from "node:path";
|
|
1068
|
+
function manifestEvidence5(path) {
|
|
1069
|
+
return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1070
|
+
}
|
|
1071
|
+
function lockfileEvidence5(path) {
|
|
1072
|
+
return { kind: "lockfile", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1073
|
+
}
|
|
1074
|
+
function parseCsproj(content) {
|
|
1075
|
+
const refs = [];
|
|
1076
|
+
const regex = /<PackageReference\s+Include\s*=\s*"([^"]+)"\s*(?:Version\s*=\s*"([^"]*)")?\s*\/?\s*>/g;
|
|
1077
|
+
let match;
|
|
1078
|
+
while ((match = regex.exec(content)) !== null) {
|
|
1079
|
+
const name = match[1];
|
|
1080
|
+
const version = match[2] || void 0;
|
|
1081
|
+
refs.push({ name, version });
|
|
1082
|
+
}
|
|
1083
|
+
return refs;
|
|
1084
|
+
}
|
|
1085
|
+
function parseProjectAssetsJson(content) {
|
|
1086
|
+
const versions = /* @__PURE__ */ new Map();
|
|
1087
|
+
try {
|
|
1088
|
+
const json = JSON.parse(content);
|
|
1089
|
+
if (json.libraries) {
|
|
1090
|
+
for (const key of Object.keys(json.libraries)) {
|
|
1091
|
+
const sepIndex = key.lastIndexOf("/");
|
|
1092
|
+
if (sepIndex >= 0) {
|
|
1093
|
+
const name = key.slice(0, sepIndex);
|
|
1094
|
+
const version = key.slice(sepIndex + 1);
|
|
1095
|
+
if (name && version) {
|
|
1096
|
+
versions.set(name, version);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
} catch {
|
|
1102
|
+
}
|
|
1103
|
+
return versions;
|
|
1104
|
+
}
|
|
1105
|
+
var DotNetAdapter = class {
|
|
1106
|
+
ecosystem = "dotnet";
|
|
1107
|
+
async inventory(workspace, options) {
|
|
1108
|
+
const observations = [];
|
|
1109
|
+
const root = workspaceRoot(workspace, options);
|
|
1110
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1111
|
+
let csprojPath;
|
|
1112
|
+
try {
|
|
1113
|
+
const files = readdirSync(root);
|
|
1114
|
+
const csproj = files.find((f) => f.endsWith(".csproj"));
|
|
1115
|
+
if (csproj) csprojPath = join3(root, csproj);
|
|
1116
|
+
} catch {
|
|
1117
|
+
}
|
|
1118
|
+
if (!csprojPath) return [];
|
|
1119
|
+
let csprojContent;
|
|
1120
|
+
try {
|
|
1121
|
+
csprojContent = readFileSync5(csprojPath, "utf-8");
|
|
1122
|
+
} catch {
|
|
1123
|
+
return [];
|
|
1124
|
+
}
|
|
1125
|
+
const manifestEv = manifestEvidence5(csprojPath);
|
|
1126
|
+
const refs = parseCsproj(csprojContent);
|
|
1127
|
+
const assetsPath = join3(root, "project.assets.json");
|
|
1128
|
+
let lockVersions = /* @__PURE__ */ new Map();
|
|
1129
|
+
let lockEv;
|
|
1130
|
+
try {
|
|
1131
|
+
const assetsContent = readFileSync5(assetsPath, "utf-8");
|
|
1132
|
+
lockVersions = parseProjectAssetsJson(assetsContent);
|
|
1133
|
+
lockEv = lockfileEvidence5(assetsPath);
|
|
1134
|
+
} catch {
|
|
1135
|
+
}
|
|
1136
|
+
for (const ref of refs) {
|
|
1137
|
+
if (seen.has(ref.name)) continue;
|
|
1138
|
+
seen.add(ref.name);
|
|
1139
|
+
const locked = lockVersions.get(ref.name) || ref.version;
|
|
1140
|
+
const purl = locked ? buildPurl({ type: "dotnet", name: ref.name, version: locked }) : buildPurl({ type: "dotnet", name: ref.name });
|
|
1141
|
+
const evidence = [manifestEv];
|
|
1142
|
+
if (lockEv && lockVersions.has(ref.name)) evidence.push(lockEv);
|
|
1143
|
+
observations.push({
|
|
1144
|
+
id: `dep-${workspace.id}-${ref.name}`,
|
|
1145
|
+
workspaceId: workspace.id,
|
|
1146
|
+
purl,
|
|
1147
|
+
ecosystem: "dotnet",
|
|
1148
|
+
name: ref.name,
|
|
1149
|
+
sourceType: "registry",
|
|
1150
|
+
direct: true,
|
|
1151
|
+
scope: "runtime",
|
|
1152
|
+
...ref.version ? { requested: ref.version } : {},
|
|
1153
|
+
...locked ? { locked } : {},
|
|
1154
|
+
status: "current",
|
|
1155
|
+
evidence
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
return observations;
|
|
1159
|
+
}
|
|
1160
|
+
};
|
|
1161
|
+
var dotNetAdapter = new DotNetAdapter();
|
|
1162
|
+
|
|
1163
|
+
// src/adapters/php.ts
|
|
1164
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
1165
|
+
import { join as join4 } from "node:path";
|
|
1166
|
+
function manifestEvidence6(path) {
|
|
1167
|
+
return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1168
|
+
}
|
|
1169
|
+
function lockfileEvidence6(path) {
|
|
1170
|
+
return { kind: "lockfile", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1171
|
+
}
|
|
1172
|
+
function parseComposerLock(content) {
|
|
1173
|
+
const versions = /* @__PURE__ */ new Map();
|
|
1174
|
+
try {
|
|
1175
|
+
const lock = JSON.parse(content);
|
|
1176
|
+
for (const pkg of [...lock.packages ?? [], ...lock["packages-dev"] ?? []]) {
|
|
1177
|
+
versions.set(pkg.name, pkg.version);
|
|
1178
|
+
}
|
|
1179
|
+
} catch {
|
|
1180
|
+
}
|
|
1181
|
+
return versions;
|
|
1182
|
+
}
|
|
1183
|
+
function statusForComposerSpec(spec) {
|
|
1184
|
+
if (spec.startsWith("file:") || spec.startsWith("path:")) return "local_path";
|
|
1185
|
+
if (spec.startsWith("git@") || spec.startsWith("git:") || spec.startsWith("http")) return "git_dependency";
|
|
1186
|
+
return "current";
|
|
1187
|
+
}
|
|
1188
|
+
function sourceTypeForComposerSpec(spec) {
|
|
1189
|
+
if (spec.startsWith("file:") || spec.startsWith("path:")) return "path";
|
|
1190
|
+
if (spec.startsWith("git@") || spec.startsWith("git:") || spec.startsWith("http")) return "git";
|
|
1191
|
+
return "registry";
|
|
1192
|
+
}
|
|
1193
|
+
var PhpAdapter = class {
|
|
1194
|
+
ecosystem = "php";
|
|
1195
|
+
async inventory(workspace, options) {
|
|
1196
|
+
const observations = [];
|
|
1197
|
+
const root = workspaceRoot(workspace, options);
|
|
1198
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1199
|
+
const composerJsonPath = workspace.manifests.find((m) => m.includes("composer.json")) || (this.fileExists(join4(root, "composer.json")) ? join4(root, "composer.json") : void 0);
|
|
1200
|
+
if (!composerJsonPath) return [];
|
|
1201
|
+
let content;
|
|
1202
|
+
try {
|
|
1203
|
+
content = readFileSync6(composerJsonPath, "utf-8");
|
|
1204
|
+
} catch {
|
|
1205
|
+
return [];
|
|
1206
|
+
}
|
|
1207
|
+
const manifestEv = manifestEvidence6(composerJsonPath);
|
|
1208
|
+
let composerJson;
|
|
1209
|
+
try {
|
|
1210
|
+
composerJson = JSON.parse(content);
|
|
1211
|
+
} catch {
|
|
1212
|
+
return [];
|
|
1213
|
+
}
|
|
1214
|
+
const lockPath = join4(root, "composer.lock");
|
|
1215
|
+
let lockVersions = /* @__PURE__ */ new Map();
|
|
1216
|
+
let lockEv;
|
|
1217
|
+
try {
|
|
1218
|
+
const lockContent = readFileSync6(lockPath, "utf-8");
|
|
1219
|
+
lockVersions = parseComposerLock(lockContent);
|
|
1220
|
+
lockEv = lockfileEvidence6(lockPath);
|
|
1221
|
+
} catch {
|
|
1222
|
+
}
|
|
1223
|
+
const sections = [
|
|
1224
|
+
{ deps: composerJson.require, scope: "runtime" },
|
|
1225
|
+
{ deps: composerJson["require-dev"], scope: "development" }
|
|
1226
|
+
];
|
|
1227
|
+
for (const { deps, scope } of sections) {
|
|
1228
|
+
if (!deps) continue;
|
|
1229
|
+
for (const [name, constraint] of Object.entries(deps)) {
|
|
1230
|
+
if (seen.has(name)) continue;
|
|
1231
|
+
seen.add(name);
|
|
1232
|
+
const locked = lockVersions.get(name);
|
|
1233
|
+
const status = statusForComposerSpec(constraint);
|
|
1234
|
+
const sourceType = sourceTypeForComposerSpec(constraint);
|
|
1235
|
+
const isRegistry = sourceType === "registry";
|
|
1236
|
+
const purl = isRegistry && locked ? buildPurl({ type: "php", name, version: locked }) : isRegistry ? buildPurl({ type: "php", name }) : void 0;
|
|
1237
|
+
const evidence = [manifestEv];
|
|
1238
|
+
if (lockEv && locked) evidence.push(lockEv);
|
|
1239
|
+
observations.push({
|
|
1240
|
+
id: `dep-${workspace.id}-${name}`,
|
|
1241
|
+
workspaceId: workspace.id,
|
|
1242
|
+
...purl ? { purl } : {},
|
|
1243
|
+
ecosystem: "php",
|
|
1244
|
+
name,
|
|
1245
|
+
sourceType,
|
|
1246
|
+
direct: true,
|
|
1247
|
+
scope,
|
|
1248
|
+
requested: constraint,
|
|
1249
|
+
...locked ? { locked } : {},
|
|
1250
|
+
status,
|
|
1251
|
+
evidence
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return observations;
|
|
1256
|
+
}
|
|
1257
|
+
fileExists(filePath) {
|
|
1258
|
+
try {
|
|
1259
|
+
readFileSync6(filePath, "utf-8");
|
|
1260
|
+
return true;
|
|
1261
|
+
} catch {
|
|
1262
|
+
return false;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
var phpAdapter = new PhpAdapter();
|
|
1267
|
+
|
|
1268
|
+
// src/adapters/dart.ts
|
|
1269
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
1270
|
+
import { join as join5 } from "node:path";
|
|
1271
|
+
function manifestEvidence7(path) {
|
|
1272
|
+
return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1273
|
+
}
|
|
1274
|
+
function lockfileEvidence7(path) {
|
|
1275
|
+
return { kind: "lockfile", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1276
|
+
}
|
|
1277
|
+
function parsePubspecYaml(content) {
|
|
1278
|
+
const sections = /* @__PURE__ */ new Map();
|
|
1279
|
+
let currentSection;
|
|
1280
|
+
let currentName;
|
|
1281
|
+
for (const raw of content.split("\n")) {
|
|
1282
|
+
const line = raw.trimEnd();
|
|
1283
|
+
const trimmed = line.trim();
|
|
1284
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
1285
|
+
const sectionMatch = trimmed.match(/^(\w[\w-]*):\s*$/);
|
|
1286
|
+
if (sectionMatch && line.startsWith(sectionMatch[1])) {
|
|
1287
|
+
currentSection = sectionMatch[1];
|
|
1288
|
+
currentName = void 0;
|
|
1289
|
+
if (!sections.has(currentSection)) {
|
|
1290
|
+
sections.set(currentSection, /* @__PURE__ */ new Map());
|
|
1291
|
+
}
|
|
1292
|
+
continue;
|
|
1293
|
+
}
|
|
1294
|
+
if (!currentSection) continue;
|
|
1295
|
+
const depMatch = trimmed.match(/^(\S[^:]*?):\s*(.*)$/);
|
|
1296
|
+
if (depMatch && line.startsWith(" ") && !line.startsWith(" ")) {
|
|
1297
|
+
currentName = depMatch[1].trim();
|
|
1298
|
+
let constraint = depMatch[2].trim();
|
|
1299
|
+
if (!constraint || constraint.startsWith("{")) {
|
|
1300
|
+
constraint = "*";
|
|
1301
|
+
}
|
|
1302
|
+
const sec = sections.get(currentSection);
|
|
1303
|
+
sec.set(currentName, constraint);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
return sections;
|
|
1307
|
+
}
|
|
1308
|
+
function parsePubspecLock(content) {
|
|
1309
|
+
const versions = /* @__PURE__ */ new Map();
|
|
1310
|
+
const lines = content.split("\n");
|
|
1311
|
+
let currentPackage;
|
|
1312
|
+
let inPackages = false;
|
|
1313
|
+
for (const raw of lines) {
|
|
1314
|
+
const trimmed = raw.trim();
|
|
1315
|
+
if (trimmed === "") continue;
|
|
1316
|
+
if (trimmed === "packages:") {
|
|
1317
|
+
inPackages = true;
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
if (!inPackages) continue;
|
|
1321
|
+
const pkgMatch = trimmed.match(/^(\S[^:]*):\s*$/);
|
|
1322
|
+
if (pkgMatch && raw.startsWith(" ") && !raw.startsWith(" ")) {
|
|
1323
|
+
currentPackage = pkgMatch[1].trim();
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
if (currentPackage) {
|
|
1327
|
+
const verMatch = trimmed.match(/^version:\s*"?([^"\s]+)"?\s*$/);
|
|
1328
|
+
if (verMatch && raw.startsWith(" ")) {
|
|
1329
|
+
versions.set(currentPackage, verMatch[1]);
|
|
1330
|
+
currentPackage = void 0;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
return versions;
|
|
1335
|
+
}
|
|
1336
|
+
var DartAdapter = class {
|
|
1337
|
+
ecosystem = "dart";
|
|
1338
|
+
async inventory(workspace, options) {
|
|
1339
|
+
const observations = [];
|
|
1340
|
+
const root = workspaceRoot(workspace, options);
|
|
1341
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1342
|
+
const pubspecPath = workspace.manifests.find((m) => m.includes("pubspec.yaml")) || (this.fileExists(join5(root, "pubspec.yaml")) ? join5(root, "pubspec.yaml") : void 0);
|
|
1343
|
+
if (!pubspecPath) return [];
|
|
1344
|
+
let content;
|
|
1345
|
+
try {
|
|
1346
|
+
content = readFileSync7(pubspecPath, "utf-8");
|
|
1347
|
+
} catch {
|
|
1348
|
+
return [];
|
|
1349
|
+
}
|
|
1350
|
+
const manifestEv = manifestEvidence7(pubspecPath);
|
|
1351
|
+
const sections = parsePubspecYaml(content);
|
|
1352
|
+
const lockPath = join5(root, "pubspec.lock");
|
|
1353
|
+
let lockVersions = /* @__PURE__ */ new Map();
|
|
1354
|
+
let lockEv;
|
|
1355
|
+
try {
|
|
1356
|
+
const lockContent = readFileSync7(lockPath, "utf-8");
|
|
1357
|
+
lockVersions = parsePubspecLock(lockContent);
|
|
1358
|
+
lockEv = lockfileEvidence7(lockPath);
|
|
1359
|
+
} catch {
|
|
1360
|
+
}
|
|
1361
|
+
const sectionMapping = [
|
|
1362
|
+
{ yamlSection: "dependencies", scope: "runtime" },
|
|
1363
|
+
{ yamlSection: "dev_dependencies", scope: "development" },
|
|
1364
|
+
{ yamlSection: "dependency_overrides", scope: "runtime" }
|
|
1365
|
+
];
|
|
1366
|
+
for (const { yamlSection, scope } of sectionMapping) {
|
|
1367
|
+
const deps = sections.get(yamlSection);
|
|
1368
|
+
if (!deps) continue;
|
|
1369
|
+
for (const [name, constraint] of deps) {
|
|
1370
|
+
if (seen.has(name)) continue;
|
|
1371
|
+
seen.add(name);
|
|
1372
|
+
if (constraint === "*" || constraint.startsWith("{")) continue;
|
|
1373
|
+
const locked = lockVersions.get(name);
|
|
1374
|
+
let status = "current";
|
|
1375
|
+
let sourceType = "registry";
|
|
1376
|
+
if (constraint.startsWith("path:")) {
|
|
1377
|
+
status = "local_path";
|
|
1378
|
+
sourceType = "path";
|
|
1379
|
+
} else if (constraint.startsWith("git:")) {
|
|
1380
|
+
status = "git_dependency";
|
|
1381
|
+
sourceType = "git";
|
|
1382
|
+
} else if (constraint.startsWith("{")) {
|
|
1383
|
+
status = "local_path";
|
|
1384
|
+
sourceType = "path";
|
|
1385
|
+
}
|
|
1386
|
+
const isRegistry = sourceType === "registry";
|
|
1387
|
+
const purl = isRegistry && (locked || constraint) ? buildPurl({ type: "dart", name, version: locked || constraint.replace(/^[\^~>=<\s]+/, "") }) : isRegistry ? buildPurl({ type: "dart", name }) : void 0;
|
|
1388
|
+
const evidence = [manifestEv];
|
|
1389
|
+
if (lockEv && locked) evidence.push(lockEv);
|
|
1390
|
+
observations.push({
|
|
1391
|
+
id: `dep-${workspace.id}-${name}`,
|
|
1392
|
+
workspaceId: workspace.id,
|
|
1393
|
+
...purl ? { purl } : {},
|
|
1394
|
+
ecosystem: "dart",
|
|
1395
|
+
name,
|
|
1396
|
+
sourceType,
|
|
1397
|
+
direct: true,
|
|
1398
|
+
scope,
|
|
1399
|
+
...constraint && constraint !== "*" ? { requested: constraint } : {},
|
|
1400
|
+
...locked ? { locked } : {},
|
|
1401
|
+
status,
|
|
1402
|
+
evidence
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
return observations;
|
|
1407
|
+
}
|
|
1408
|
+
fileExists(filePath) {
|
|
1409
|
+
try {
|
|
1410
|
+
readFileSync7(filePath, "utf-8");
|
|
1411
|
+
return true;
|
|
1412
|
+
} catch {
|
|
1413
|
+
return false;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
var dartAdapter = new DartAdapter();
|
|
1418
|
+
|
|
1419
|
+
// src/adapters/maven.ts
|
|
1420
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
1421
|
+
function manifestEvidence8(path) {
|
|
1422
|
+
return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1423
|
+
}
|
|
1424
|
+
function parsePomDependencies(xml) {
|
|
1425
|
+
const deps = [];
|
|
1426
|
+
const depRegex = /<dependency>\s*([\s\S]*?)<\/dependency>/g;
|
|
1427
|
+
let match;
|
|
1428
|
+
while ((match = depRegex.exec(xml)) !== null) {
|
|
1429
|
+
const block = match[1];
|
|
1430
|
+
const groupId = block.match(/<groupId>([^<]+)<\/groupId>/)?.[1]?.trim();
|
|
1431
|
+
const artifactId = block.match(/<artifactId>([^<]+)<\/artifactId>/)?.[1]?.trim();
|
|
1432
|
+
const version = block.match(/<version>([^<]+)<\/version>/)?.[1]?.trim();
|
|
1433
|
+
const scope = block.match(/<scope>([^<]+)<\/scope>/)?.[1]?.trim();
|
|
1434
|
+
if (groupId && artifactId) {
|
|
1435
|
+
deps.push({ groupId, artifactId, version, scope });
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
return deps;
|
|
1439
|
+
}
|
|
1440
|
+
function mavenScopeToScope(scope) {
|
|
1441
|
+
switch (scope) {
|
|
1442
|
+
case "test":
|
|
1443
|
+
return "development";
|
|
1444
|
+
case "provided":
|
|
1445
|
+
return "optional";
|
|
1446
|
+
case "runtime":
|
|
1447
|
+
return "runtime";
|
|
1448
|
+
case "compile":
|
|
1449
|
+
return "runtime";
|
|
1450
|
+
default:
|
|
1451
|
+
return "runtime";
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
var MavenAdapter = class {
|
|
1455
|
+
ecosystem = "maven";
|
|
1456
|
+
async inventory(workspace, _options) {
|
|
1457
|
+
const observations = [];
|
|
1458
|
+
const pomPath = workspace.manifests.find((m) => m.includes("pom.xml"));
|
|
1459
|
+
if (!pomPath) return [];
|
|
1460
|
+
let content;
|
|
1461
|
+
try {
|
|
1462
|
+
content = readFileSync8(pomPath, "utf-8");
|
|
1463
|
+
} catch {
|
|
1464
|
+
return [];
|
|
1465
|
+
}
|
|
1466
|
+
const manifestEv = manifestEvidence8(pomPath);
|
|
1467
|
+
const deps = parsePomDependencies(content);
|
|
1468
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1469
|
+
for (const dep of deps) {
|
|
1470
|
+
const name = `${dep.groupId}:${dep.artifactId}`;
|
|
1471
|
+
if (seen.has(name)) continue;
|
|
1472
|
+
seen.add(name);
|
|
1473
|
+
const purl = dep.version ? buildPurl({ type: "maven", name, version: dep.version }) : buildPurl({ type: "maven", name });
|
|
1474
|
+
observations.push({
|
|
1475
|
+
id: `dep-${workspace.id}-${name}`,
|
|
1476
|
+
workspaceId: workspace.id,
|
|
1477
|
+
purl,
|
|
1478
|
+
ecosystem: "maven",
|
|
1479
|
+
name,
|
|
1480
|
+
sourceType: "registry",
|
|
1481
|
+
direct: true,
|
|
1482
|
+
scope: mavenScopeToScope(dep.scope),
|
|
1483
|
+
...dep.version ? { requested: dep.version } : {},
|
|
1484
|
+
status: "current",
|
|
1485
|
+
evidence: [manifestEv]
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
return observations;
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
var mavenAdapter = new MavenAdapter();
|
|
1492
|
+
|
|
1493
|
+
// src/adapters/ruby.ts
|
|
1494
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
1495
|
+
function manifestEvidence9(path) {
|
|
1496
|
+
return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1497
|
+
}
|
|
1498
|
+
function lockfileEvidence8(path) {
|
|
1499
|
+
return { kind: "lockfile", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1500
|
+
}
|
|
1501
|
+
function parseGemfile(content) {
|
|
1502
|
+
const gems = [];
|
|
1503
|
+
const gemRegex = /gem\s+['"]([^'"]+)['"](?:\s*,\s*['"]([^'"]+)['"])?/g;
|
|
1504
|
+
let match;
|
|
1505
|
+
while ((match = gemRegex.exec(content)) !== null) {
|
|
1506
|
+
const name = match[1];
|
|
1507
|
+
if (name === "rails" || name === "ruby") continue;
|
|
1508
|
+
gems.push({ name, version: match[2] });
|
|
1509
|
+
}
|
|
1510
|
+
return gems;
|
|
1511
|
+
}
|
|
1512
|
+
function parseGemfileLock(content) {
|
|
1513
|
+
const versions = /* @__PURE__ */ new Map();
|
|
1514
|
+
const lines = content.split("\n");
|
|
1515
|
+
let inSpecs = false;
|
|
1516
|
+
for (const line of lines) {
|
|
1517
|
+
if (line.startsWith("GEM")) {
|
|
1518
|
+
inSpecs = true;
|
|
1519
|
+
continue;
|
|
1520
|
+
}
|
|
1521
|
+
if (inSpecs && /^[A-Z]/.test(line) && !line.startsWith(" ")) {
|
|
1522
|
+
inSpecs = false;
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
if (!inSpecs) continue;
|
|
1526
|
+
const match = /^\s{4,}([\w-]+)\s+\(([^)]+)\)/.exec(line);
|
|
1527
|
+
if (match) {
|
|
1528
|
+
const version = match[2].split(" ")[0] ?? match[2];
|
|
1529
|
+
versions.set(match[1], version);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
return versions;
|
|
1533
|
+
}
|
|
1534
|
+
var RubyAdapter = class {
|
|
1535
|
+
ecosystem = "ruby";
|
|
1536
|
+
async inventory(workspace, _options) {
|
|
1537
|
+
const observations = [];
|
|
1538
|
+
const gemfilePath = workspace.manifests.find((m) => m.includes("Gemfile"));
|
|
1539
|
+
if (!gemfilePath) return [];
|
|
1540
|
+
let content;
|
|
1541
|
+
try {
|
|
1542
|
+
content = readFileSync9(gemfilePath, "utf-8");
|
|
1543
|
+
} catch {
|
|
1544
|
+
return [];
|
|
1545
|
+
}
|
|
1546
|
+
const manifestEv = manifestEvidence9(gemfilePath);
|
|
1547
|
+
const gems = parseGemfile(content);
|
|
1548
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1549
|
+
const lockfilePath = workspace.lockfiles.find((l) => l.includes("Gemfile.lock"));
|
|
1550
|
+
let lockVersions = /* @__PURE__ */ new Map();
|
|
1551
|
+
let lockEv;
|
|
1552
|
+
if (lockfilePath) {
|
|
1553
|
+
try {
|
|
1554
|
+
const lockContent = readFileSync9(lockfilePath, "utf-8");
|
|
1555
|
+
lockVersions = parseGemfileLock(lockContent);
|
|
1556
|
+
lockEv = lockfileEvidence8(lockfilePath);
|
|
1557
|
+
} catch {
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
for (const gem of gems) {
|
|
1561
|
+
if (seen.has(gem.name)) continue;
|
|
1562
|
+
seen.add(gem.name);
|
|
1563
|
+
const locked = lockVersions.get(gem.name);
|
|
1564
|
+
const version = locked ?? gem.version;
|
|
1565
|
+
const purl = version ? buildPurl({ type: "gem", name: gem.name, version }) : buildPurl({ type: "gem", name: gem.name });
|
|
1566
|
+
const evidence = [manifestEv];
|
|
1567
|
+
if (lockEv && locked) evidence.push(lockEv);
|
|
1568
|
+
observations.push({
|
|
1569
|
+
id: `dep-${workspace.id}-${gem.name}`,
|
|
1570
|
+
workspaceId: workspace.id,
|
|
1571
|
+
purl,
|
|
1572
|
+
ecosystem: "ruby",
|
|
1573
|
+
name: gem.name,
|
|
1574
|
+
sourceType: "registry",
|
|
1575
|
+
direct: true,
|
|
1576
|
+
scope: "runtime",
|
|
1577
|
+
...gem.version ? { requested: gem.version } : {},
|
|
1578
|
+
...locked ? { locked } : {},
|
|
1579
|
+
status: "current",
|
|
1580
|
+
evidence
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
return observations;
|
|
1584
|
+
}
|
|
1585
|
+
};
|
|
1586
|
+
var rubyAdapter = new RubyAdapter();
|
|
1587
|
+
|
|
1588
|
+
// src/adapters/elixir.ts
|
|
1589
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
1590
|
+
function manifestEvidence10(path) {
|
|
1591
|
+
return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1592
|
+
}
|
|
1593
|
+
function lockfileEvidence9(path) {
|
|
1594
|
+
return { kind: "lockfile", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1595
|
+
}
|
|
1596
|
+
function parseMixExsDeps(content) {
|
|
1597
|
+
const deps = [];
|
|
1598
|
+
const depRegex = /\{:(\w+),\s*["']([^"']+)["']\}/g;
|
|
1599
|
+
let match;
|
|
1600
|
+
while ((match = depRegex.exec(content)) !== null) {
|
|
1601
|
+
deps.push({ name: match[1], version: match[2] });
|
|
1602
|
+
}
|
|
1603
|
+
return deps;
|
|
1604
|
+
}
|
|
1605
|
+
function parseMixLock(content) {
|
|
1606
|
+
const versions = /* @__PURE__ */ new Map();
|
|
1607
|
+
const lockRegex = /\{:"(\w+)",\s*hex: "[^"]*",\s*"([^"]+)"/g;
|
|
1608
|
+
let match;
|
|
1609
|
+
while ((match = lockRegex.exec(content)) !== null) {
|
|
1610
|
+
versions.set(match[1], match[2]);
|
|
1611
|
+
}
|
|
1612
|
+
return versions;
|
|
1613
|
+
}
|
|
1614
|
+
var ElixirAdapter = class {
|
|
1615
|
+
ecosystem = "elixir";
|
|
1616
|
+
async inventory(workspace, _options) {
|
|
1617
|
+
const observations = [];
|
|
1618
|
+
const mixExsPath = workspace.manifests.find((m) => m.includes("mix.exs"));
|
|
1619
|
+
if (!mixExsPath) return [];
|
|
1620
|
+
let content;
|
|
1621
|
+
try {
|
|
1622
|
+
content = readFileSync10(mixExsPath, "utf-8");
|
|
1623
|
+
} catch {
|
|
1624
|
+
return [];
|
|
1625
|
+
}
|
|
1626
|
+
const manifestEv = manifestEvidence10(mixExsPath);
|
|
1627
|
+
const deps = parseMixExsDeps(content);
|
|
1628
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1629
|
+
const lockfilePath = workspace.lockfiles.find((l) => l.includes("mix.lock"));
|
|
1630
|
+
let lockVersions = /* @__PURE__ */ new Map();
|
|
1631
|
+
let lockEv;
|
|
1632
|
+
if (lockfilePath) {
|
|
1633
|
+
try {
|
|
1634
|
+
const lockContent = readFileSync10(lockfilePath, "utf-8");
|
|
1635
|
+
lockVersions = parseMixLock(lockContent);
|
|
1636
|
+
lockEv = lockfileEvidence9(lockfilePath);
|
|
1637
|
+
} catch {
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
for (const dep of deps) {
|
|
1641
|
+
if (seen.has(dep.name)) continue;
|
|
1642
|
+
seen.add(dep.name);
|
|
1643
|
+
const locked = lockVersions.get(dep.name);
|
|
1644
|
+
const version = locked ?? dep.version;
|
|
1645
|
+
const purl = version ? buildPurl({ type: "hex", name: dep.name, version }) : buildPurl({ type: "hex", name: dep.name });
|
|
1646
|
+
const evidence = [manifestEv];
|
|
1647
|
+
if (lockEv && locked) evidence.push(lockEv);
|
|
1648
|
+
observations.push({
|
|
1649
|
+
id: `dep-${workspace.id}-${dep.name}`,
|
|
1650
|
+
workspaceId: workspace.id,
|
|
1651
|
+
purl,
|
|
1652
|
+
ecosystem: "elixir",
|
|
1653
|
+
name: dep.name,
|
|
1654
|
+
sourceType: "registry",
|
|
1655
|
+
direct: true,
|
|
1656
|
+
scope: "runtime",
|
|
1657
|
+
...dep.version ? { requested: dep.version } : {},
|
|
1658
|
+
...locked ? { locked } : {},
|
|
1659
|
+
status: "current",
|
|
1660
|
+
evidence
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
return observations;
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
var elixirAdapter = new ElixirAdapter();
|
|
1667
|
+
|
|
1668
|
+
// src/adapters/cpp.ts
|
|
1669
|
+
import { readFileSync as readFileSync11 } from "node:fs";
|
|
1670
|
+
function manifestEvidence11(path) {
|
|
1671
|
+
return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1672
|
+
}
|
|
1673
|
+
function parseConanTxt(content) {
|
|
1674
|
+
const deps = [];
|
|
1675
|
+
const requiresMatch = /\[requires\]\s*\n([\s\S]*?)(?:\[|$)/;
|
|
1676
|
+
const block = requiresMatch.exec(content)?.[1];
|
|
1677
|
+
if (!block) return deps;
|
|
1678
|
+
for (const line of block.split("\n")) {
|
|
1679
|
+
const trimmed = line.trim();
|
|
1680
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1681
|
+
const parts = trimmed.split("/");
|
|
1682
|
+
if (parts.length >= 2) {
|
|
1683
|
+
deps.push({ name: parts[0], version: parts[1] });
|
|
1684
|
+
} else {
|
|
1685
|
+
deps.push({ name: trimmed });
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
return deps;
|
|
1689
|
+
}
|
|
1690
|
+
function parseVcpkgJson(content) {
|
|
1691
|
+
const deps = [];
|
|
1692
|
+
try {
|
|
1693
|
+
const json = JSON.parse(content);
|
|
1694
|
+
for (const dep of json.dependencies ?? []) {
|
|
1695
|
+
if (typeof dep === "string") {
|
|
1696
|
+
deps.push({ name: dep });
|
|
1697
|
+
} else {
|
|
1698
|
+
deps.push({ name: dep.name, version: dep.version });
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
} catch {
|
|
1702
|
+
}
|
|
1703
|
+
return deps;
|
|
1704
|
+
}
|
|
1705
|
+
var CppAdapter = class {
|
|
1706
|
+
ecosystem = "cpp";
|
|
1707
|
+
async inventory(workspace, _options) {
|
|
1708
|
+
const observations = [];
|
|
1709
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1710
|
+
for (const manifestPath of workspace.manifests) {
|
|
1711
|
+
let content;
|
|
1712
|
+
try {
|
|
1713
|
+
content = readFileSync11(manifestPath, "utf-8");
|
|
1714
|
+
} catch {
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
const manifestEv = manifestEvidence11(manifestPath);
|
|
1718
|
+
let deps = [];
|
|
1719
|
+
if (manifestPath.includes("conanfile")) {
|
|
1720
|
+
deps = parseConanTxt(content);
|
|
1721
|
+
} else if (manifestPath.includes("vcpkg.json")) {
|
|
1722
|
+
deps = parseVcpkgJson(content);
|
|
1723
|
+
} else {
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
for (const dep of deps) {
|
|
1727
|
+
if (seen.has(dep.name)) continue;
|
|
1728
|
+
seen.add(dep.name);
|
|
1729
|
+
const purl = dep.version ? buildPurl({ type: "conan", name: dep.name, version: dep.version }) : buildPurl({ type: "conan", name: dep.name });
|
|
1730
|
+
observations.push({
|
|
1731
|
+
id: `dep-${workspace.id}-${dep.name}`,
|
|
1732
|
+
workspaceId: workspace.id,
|
|
1733
|
+
purl,
|
|
1734
|
+
ecosystem: "cpp",
|
|
1735
|
+
name: dep.name,
|
|
1736
|
+
sourceType: "registry",
|
|
1737
|
+
direct: true,
|
|
1738
|
+
scope: "runtime",
|
|
1739
|
+
...dep.version ? { requested: dep.version } : {},
|
|
1740
|
+
// Tier C — we cannot verify current/version status
|
|
1741
|
+
status: "unknown",
|
|
1742
|
+
evidence: [manifestEv]
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
return observations;
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
var cppAdapter = new CppAdapter();
|
|
1750
|
+
|
|
1751
|
+
// src/snapshot-diff.ts
|
|
1752
|
+
function diffSnapshots(oldSnapshot, newSnapshot) {
|
|
1753
|
+
const oldByKey = /* @__PURE__ */ new Map();
|
|
1754
|
+
for (const dep of oldSnapshot.dependencies) {
|
|
1755
|
+
oldByKey.set(`${dep.ecosystem}:${dep.name}`, dep);
|
|
1756
|
+
}
|
|
1757
|
+
const newByKey = /* @__PURE__ */ new Map();
|
|
1758
|
+
for (const dep of newSnapshot.dependencies) {
|
|
1759
|
+
newByKey.set(`${dep.ecosystem}:${dep.name}`, dep);
|
|
1760
|
+
}
|
|
1761
|
+
const added = [];
|
|
1762
|
+
const removed = [];
|
|
1763
|
+
const changed = [];
|
|
1764
|
+
for (const [key, newDep] of newByKey) {
|
|
1765
|
+
const oldDep = oldByKey.get(key);
|
|
1766
|
+
if (!oldDep) {
|
|
1767
|
+
added.push(newDep);
|
|
1768
|
+
continue;
|
|
1769
|
+
}
|
|
1770
|
+
const fields = ["locked", "requested", "status", "latestStable"];
|
|
1771
|
+
for (const field of fields) {
|
|
1772
|
+
const oldVal = String(oldDep[field] ?? "");
|
|
1773
|
+
const newVal = String(newDep[field] ?? "");
|
|
1774
|
+
if (oldVal !== newVal) {
|
|
1775
|
+
changed.push({
|
|
1776
|
+
name: newDep.name,
|
|
1777
|
+
ecosystem: newDep.ecosystem,
|
|
1778
|
+
field: String(field),
|
|
1779
|
+
from: oldVal,
|
|
1780
|
+
to: newVal
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
for (const [key, oldDep] of oldByKey) {
|
|
1786
|
+
if (!newByKey.has(key)) {
|
|
1787
|
+
removed.push(oldDep);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
return { added, removed, changed };
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// src/sbom.ts
|
|
1794
|
+
function toSpdx(snapshot) {
|
|
1795
|
+
const created = snapshot.createdAt;
|
|
1796
|
+
return {
|
|
1797
|
+
spdxVersion: "SPDX-2.3",
|
|
1798
|
+
dataLicense: "CC0-1.0",
|
|
1799
|
+
SPDXID: "SPDXRef-DOCUMENT",
|
|
1800
|
+
name: `TechStack-SBOM-${snapshot.projectId}`,
|
|
1801
|
+
documentNamespace: `https://wrongstack.dev/spdx/${snapshot.id}`,
|
|
1802
|
+
creationInfo: {
|
|
1803
|
+
created,
|
|
1804
|
+
creators: ["Tool: WrongStack TechStack Engine"]
|
|
1805
|
+
},
|
|
1806
|
+
packages: snapshot.dependencies.map((dep, index) => ({
|
|
1807
|
+
name: dep.name,
|
|
1808
|
+
SPDXID: `SPDXRef-Package-${index}`,
|
|
1809
|
+
versionInfo: dep.locked ?? dep.requested,
|
|
1810
|
+
downloadLocation: dep.purl ? `https://purl.io/${dep.purl}` : "NOASSERTION",
|
|
1811
|
+
licenseConcluded: dep.license ?? "NOASSERTION"
|
|
1812
|
+
}))
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
function toCycloneDX(snapshot) {
|
|
1816
|
+
return {
|
|
1817
|
+
bomFormat: "CycloneDX",
|
|
1818
|
+
specVersion: "1.5",
|
|
1819
|
+
version: 1,
|
|
1820
|
+
metadata: {
|
|
1821
|
+
timestamp: snapshot.createdAt,
|
|
1822
|
+
tools: [{ name: "WrongStack TechStack Engine", version: snapshot.adapterVersion }]
|
|
1823
|
+
},
|
|
1824
|
+
components: snapshot.dependencies.map((dep) => ({
|
|
1825
|
+
type: "library",
|
|
1826
|
+
name: dep.name,
|
|
1827
|
+
version: dep.locked ?? dep.requested,
|
|
1828
|
+
...dep.purl ? { purl: dep.purl } : {},
|
|
1829
|
+
...dep.license ? { licenses: [{ license: { id: dep.license } }] } : {}
|
|
1830
|
+
}))
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// src/remediation.ts
|
|
1835
|
+
function suggestCommand(ecosystem, name, action, targetVersion) {
|
|
1836
|
+
const ver = targetVersion ? `@${targetVersion}` : "@latest";
|
|
1837
|
+
switch (ecosystem) {
|
|
1838
|
+
case "npm":
|
|
1839
|
+
if (action === "remove") return `npm uninstall ${name}`;
|
|
1840
|
+
return `npm install ${name}${ver}`;
|
|
1841
|
+
case "python":
|
|
1842
|
+
if (action === "remove") return `pip uninstall ${name}`;
|
|
1843
|
+
return `pip install ${name}${ver}`;
|
|
1844
|
+
case "rust":
|
|
1845
|
+
if (action === "remove") return `cargo remove ${name}`;
|
|
1846
|
+
return `cargo add ${name}@${targetVersion ?? "latest"}`;
|
|
1847
|
+
case "go":
|
|
1848
|
+
if (action === "remove") return `go get ${name}@none`;
|
|
1849
|
+
return `go get ${name}@${targetVersion ?? "latest"}`;
|
|
1850
|
+
case "php":
|
|
1851
|
+
if (action === "remove") return `composer remove ${name}`;
|
|
1852
|
+
return `composer require ${name}:${targetVersion ?? "latest"}`;
|
|
1853
|
+
case "dotnet":
|
|
1854
|
+
if (action === "remove") return `dotnet remove package ${name}`;
|
|
1855
|
+
return `dotnet add package ${name}`;
|
|
1856
|
+
default:
|
|
1857
|
+
return void 0;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
function generateUpgradePlan(snapshot) {
|
|
1861
|
+
const findings = snapshot.findings;
|
|
1862
|
+
const items = [];
|
|
1863
|
+
for (const finding of findings) {
|
|
1864
|
+
if (finding.action === "none") continue;
|
|
1865
|
+
const dep = snapshot.dependencies.find(
|
|
1866
|
+
(d) => d.id === finding.dependencyId
|
|
1867
|
+
);
|
|
1868
|
+
if (!dep) continue;
|
|
1869
|
+
const targetVersion = dep.latestStable ?? dep.resolvable ?? dep.wanted;
|
|
1870
|
+
items.push({
|
|
1871
|
+
dependencyName: dep.name,
|
|
1872
|
+
ecosystem: dep.ecosystem,
|
|
1873
|
+
workspaceId: dep.workspaceId,
|
|
1874
|
+
currentVersion: dep.locked ?? dep.installed ?? dep.requested,
|
|
1875
|
+
targetVersion,
|
|
1876
|
+
action: finding.action,
|
|
1877
|
+
severity: finding.severity,
|
|
1878
|
+
rationale: finding.rationale,
|
|
1879
|
+
breakingRisk: finding.breakingRisk,
|
|
1880
|
+
suggestedCommand: suggestCommand(dep.ecosystem, dep.name, finding.action, targetVersion)
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
const severityOrder = /* @__PURE__ */ new Map([
|
|
1884
|
+
["critical", 0],
|
|
1885
|
+
["high", 1],
|
|
1886
|
+
["medium", 2],
|
|
1887
|
+
["low", 3],
|
|
1888
|
+
["info", 4]
|
|
1889
|
+
]);
|
|
1890
|
+
items.sort((a, b) => {
|
|
1891
|
+
const sa = severityOrder.get(a.severity) ?? 5;
|
|
1892
|
+
const sb = severityOrder.get(b.severity) ?? 5;
|
|
1893
|
+
return sa - sb;
|
|
1894
|
+
});
|
|
1895
|
+
const summary = {
|
|
1896
|
+
total: items.length,
|
|
1897
|
+
patch: items.filter((i) => i.action === "upgrade_patch").length,
|
|
1898
|
+
minor: items.filter((i) => i.action === "upgrade_minor").length,
|
|
1899
|
+
major: items.filter((i) => i.action === "upgrade_major").length,
|
|
1900
|
+
replace: items.filter((i) => i.action === "replace").length,
|
|
1901
|
+
remove: items.filter((i) => i.action === "remove").length,
|
|
1902
|
+
investigate: items.filter((i) => i.action === "investigate").length
|
|
1903
|
+
};
|
|
1904
|
+
return {
|
|
1905
|
+
snapshotId: snapshot.id,
|
|
1906
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1907
|
+
items,
|
|
1908
|
+
summary,
|
|
1909
|
+
warning: "This plan is read-only. No dependency files will be modified unless you explicitly approve and execute each item."
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
function renderPlanMarkdown(plan) {
|
|
1913
|
+
const lines = [
|
|
1914
|
+
"# TechStack Remediation Plan",
|
|
1915
|
+
"",
|
|
1916
|
+
`**Generated:** ${plan.generatedAt}`,
|
|
1917
|
+
`**Snapshot:** ${plan.snapshotId}`,
|
|
1918
|
+
`**Total items:** ${plan.summary.total}`,
|
|
1919
|
+
"",
|
|
1920
|
+
`> \u26A0\uFE0F ${plan.warning}`,
|
|
1921
|
+
""
|
|
1922
|
+
];
|
|
1923
|
+
if (plan.items.length === 0) {
|
|
1924
|
+
lines.push("_No remediation actions needed \u2014 all dependencies are current._");
|
|
1925
|
+
return lines.join("\n");
|
|
1926
|
+
}
|
|
1927
|
+
lines.push("## Summary", "");
|
|
1928
|
+
lines.push("| Action | Count |");
|
|
1929
|
+
lines.push("|---|---|");
|
|
1930
|
+
lines.push(`| Patch upgrade | ${plan.summary.patch} |`);
|
|
1931
|
+
lines.push(`| Minor upgrade | ${plan.summary.minor} |`);
|
|
1932
|
+
lines.push(`| Major upgrade | ${plan.summary.major} |`);
|
|
1933
|
+
lines.push(`| Replace | ${plan.summary.replace} |`);
|
|
1934
|
+
lines.push(`| Remove | ${plan.summary.remove} |`);
|
|
1935
|
+
lines.push(`| Investigate | ${plan.summary.investigate} |`);
|
|
1936
|
+
lines.push("");
|
|
1937
|
+
lines.push("## Items", "");
|
|
1938
|
+
for (const item of plan.items) {
|
|
1939
|
+
const icon = item.severity === "critical" ? "\u{1F534}" : item.severity === "high" ? "\u{1F7E0}" : item.severity === "medium" ? "\u{1F7E1}" : item.severity === "low" ? "\u{1F535}" : "\u2139\uFE0F";
|
|
1940
|
+
lines.push(`### ${icon} ${item.dependencyName} (${item.ecosystem})`, "");
|
|
1941
|
+
lines.push(`- **Action:** ${item.action}`);
|
|
1942
|
+
lines.push(`- **Current:** ${item.currentVersion ?? "unknown"}`);
|
|
1943
|
+
lines.push(`- **Target:** ${item.targetVersion ?? "latest"}`);
|
|
1944
|
+
lines.push(`- **Severity:** ${item.severity}`);
|
|
1945
|
+
lines.push(`- **Rationale:** ${item.rationale}`);
|
|
1946
|
+
if (item.breakingRisk) lines.push(`- **Breaking risk:** ${item.breakingRisk}`);
|
|
1947
|
+
if (item.suggestedCommand) {
|
|
1948
|
+
lines.push(`- **Suggested command:** \`${item.suggestedCommand}\``);
|
|
1949
|
+
}
|
|
1950
|
+
lines.push("");
|
|
1951
|
+
}
|
|
1952
|
+
return lines.join("\n");
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// src/registry/client.ts
|
|
1956
|
+
import { get as httpsGet } from "node:https";
|
|
1957
|
+
import { get as httpGet } from "node:http";
|
|
1958
|
+
var DEFAULT_TTL_MS = 10 * 60 * 1e3;
|
|
1959
|
+
var MAX_CONCURRENCY_PER_HOST = 3;
|
|
1960
|
+
var MAX_RETRIES = 3;
|
|
1961
|
+
var BASE_BACKOFF_MS = 1e3;
|
|
1962
|
+
var registryCache = /* @__PURE__ */ new Map();
|
|
1963
|
+
var hostConcurrency = /* @__PURE__ */ new Map();
|
|
1964
|
+
function getCacheKey(host, path) {
|
|
1965
|
+
return `${host}${path}`;
|
|
1966
|
+
}
|
|
1967
|
+
function getCached(key) {
|
|
1968
|
+
const entry = registryCache.get(key);
|
|
1969
|
+
if (!entry) return void 0;
|
|
1970
|
+
if (Date.now() > entry.expiresAt) {
|
|
1971
|
+
registryCache.delete(key);
|
|
1972
|
+
return void 0;
|
|
1973
|
+
}
|
|
1974
|
+
return entry.data;
|
|
1975
|
+
}
|
|
1976
|
+
function setCache(key, data, etag, ttlMs = DEFAULT_TTL_MS) {
|
|
1977
|
+
registryCache.set(key, {
|
|
1978
|
+
data,
|
|
1979
|
+
etag,
|
|
1980
|
+
expiresAt: Date.now() + ttlMs
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1983
|
+
function acquireHostSlot(host) {
|
|
1984
|
+
let concurrency = hostConcurrency.get(host);
|
|
1985
|
+
if (!concurrency) {
|
|
1986
|
+
concurrency = { active: 0, queue: [] };
|
|
1987
|
+
hostConcurrency.set(host, concurrency);
|
|
1988
|
+
}
|
|
1989
|
+
if (concurrency.active < MAX_CONCURRENCY_PER_HOST) {
|
|
1990
|
+
concurrency.active++;
|
|
1991
|
+
return Promise.resolve();
|
|
1992
|
+
}
|
|
1993
|
+
return new Promise((resolve3) => {
|
|
1994
|
+
concurrency.queue.push(resolve3);
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
function releaseHostSlot(host) {
|
|
1998
|
+
const concurrency = hostConcurrency.get(host);
|
|
1999
|
+
if (!concurrency) return;
|
|
2000
|
+
concurrency.active--;
|
|
2001
|
+
if (concurrency.queue.length > 0) {
|
|
2002
|
+
const next = concurrency.queue.shift();
|
|
2003
|
+
if (next) {
|
|
2004
|
+
concurrency.active++;
|
|
2005
|
+
next();
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
function sleep(ms) {
|
|
2010
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
2011
|
+
}
|
|
2012
|
+
function computeBackoff(attempt, statusCode) {
|
|
2013
|
+
const base = statusCode === 429 ? BASE_BACKOFF_MS * 2 : BASE_BACKOFF_MS;
|
|
2014
|
+
return base * Math.pow(2, attempt) + Math.random() * 500;
|
|
2015
|
+
}
|
|
2016
|
+
function httpsFetch(hostname, path, etag, signal) {
|
|
2017
|
+
return new Promise((resolve3, reject) => {
|
|
2018
|
+
const options = {
|
|
2019
|
+
hostname,
|
|
2020
|
+
path,
|
|
2021
|
+
method: "GET",
|
|
2022
|
+
headers: {
|
|
2023
|
+
Accept: "application/json",
|
|
2024
|
+
"User-Agent": "WrongStack-TechStack/1.0",
|
|
2025
|
+
...etag ? { "If-None-Match": etag } : {}
|
|
2026
|
+
},
|
|
2027
|
+
signal,
|
|
2028
|
+
timeout: 15e3
|
|
2029
|
+
};
|
|
2030
|
+
const mod = hostname === "localhost" || hostname === "127.0.0.1" ? httpGet : httpsGet;
|
|
2031
|
+
const req = mod(options, (res) => {
|
|
2032
|
+
const statusCode = res.statusCode ?? 0;
|
|
2033
|
+
const responseHeaders = res.headers;
|
|
2034
|
+
let body = "";
|
|
2035
|
+
res.on("data", (chunk) => {
|
|
2036
|
+
body += chunk;
|
|
2037
|
+
});
|
|
2038
|
+
res.on("end", () => {
|
|
2039
|
+
resolve3({
|
|
2040
|
+
statusCode,
|
|
2041
|
+
headers: responseHeaders,
|
|
2042
|
+
body,
|
|
2043
|
+
isFromCache: false
|
|
2044
|
+
});
|
|
2045
|
+
});
|
|
2046
|
+
});
|
|
2047
|
+
req.on("error", (err) => {
|
|
2048
|
+
reject(err);
|
|
2049
|
+
});
|
|
2050
|
+
req.on("timeout", () => {
|
|
2051
|
+
req.destroy();
|
|
2052
|
+
reject(new Error(`Request timeout for ${hostname}${path}`));
|
|
2053
|
+
});
|
|
2054
|
+
req.end();
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
function parseNpmPackument(json, name) {
|
|
2058
|
+
const latestVersion = json["dist-tags"]?.["latest"];
|
|
2059
|
+
let deprecated;
|
|
2060
|
+
if (latestVersion && json.versions && typeof json.versions === "object") {
|
|
2061
|
+
const versions = json.versions;
|
|
2062
|
+
deprecated = versions[latestVersion]?.deprecated ? true : void 0;
|
|
2063
|
+
}
|
|
2064
|
+
return {
|
|
2065
|
+
latestStable: latestVersion,
|
|
2066
|
+
license: json.license ?? void 0,
|
|
2067
|
+
deprecated: deprecated ?? void 0,
|
|
2068
|
+
yanked: void 0,
|
|
2069
|
+
retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2070
|
+
source: `https://registry.npmjs.org/${name}`
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
var ECOSYSTEM_FETCHERS = {
|
|
2074
|
+
npm: {
|
|
2075
|
+
host: "registry.npmjs.org",
|
|
2076
|
+
path: (name) => {
|
|
2077
|
+
const encoded = name.startsWith("@") ? name.replace("/", "%2F") : name;
|
|
2078
|
+
return `/${encoded}`;
|
|
2079
|
+
},
|
|
2080
|
+
parser: parseNpmPackument
|
|
2081
|
+
},
|
|
2082
|
+
python: {
|
|
2083
|
+
host: "pypi.org",
|
|
2084
|
+
path: (name) => `/pypi/${name}/json`,
|
|
2085
|
+
parser: (json) => {
|
|
2086
|
+
const info = json.info;
|
|
2087
|
+
return {
|
|
2088
|
+
latestStable: info?.version ?? void 0,
|
|
2089
|
+
license: info?.license ?? void 0,
|
|
2090
|
+
deprecated: info?.deprecated ?? void 0,
|
|
2091
|
+
yanked: void 0,
|
|
2092
|
+
retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2093
|
+
source: `https://pypi.org/pypi/${info?.name ?? ""}/json`
|
|
2094
|
+
};
|
|
2095
|
+
}
|
|
2096
|
+
},
|
|
2097
|
+
cargo: {
|
|
2098
|
+
host: "crates.io",
|
|
2099
|
+
path: (name) => `/api/v1/crates/${name}`,
|
|
2100
|
+
parser: (json) => {
|
|
2101
|
+
const crate = json.crate;
|
|
2102
|
+
return {
|
|
2103
|
+
latestStable: crate?.max_stable_version ?? crate?.max_version ?? void 0,
|
|
2104
|
+
license: crate?.license ?? void 0,
|
|
2105
|
+
deprecated: void 0,
|
|
2106
|
+
// crates.io doesn't have deprecation
|
|
2107
|
+
yanked: void 0,
|
|
2108
|
+
retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2109
|
+
source: `https://crates.io/api/v1/crates/${crate?.name ?? ""}`
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
},
|
|
2113
|
+
golang: {
|
|
2114
|
+
host: "proxy.golang.org",
|
|
2115
|
+
path: (module) => `/${module}/@latest`,
|
|
2116
|
+
parser: (json, module) => {
|
|
2117
|
+
return {
|
|
2118
|
+
latestStable: json.Version ?? void 0,
|
|
2119
|
+
license: void 0,
|
|
2120
|
+
// Go proxy doesn't provide license
|
|
2121
|
+
deprecated: void 0,
|
|
2122
|
+
yanked: void 0,
|
|
2123
|
+
retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2124
|
+
source: `https://proxy.golang.org/${module}/@latest`
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
},
|
|
2128
|
+
nuget: {
|
|
2129
|
+
host: "api.nuget.org",
|
|
2130
|
+
path: (name) => {
|
|
2131
|
+
const lower = name.toLowerCase();
|
|
2132
|
+
return `/v3/registration5-semver1/${lower}/index.json`;
|
|
2133
|
+
},
|
|
2134
|
+
parser: (json, name) => {
|
|
2135
|
+
const items = json.items;
|
|
2136
|
+
let latestStable;
|
|
2137
|
+
if (items && items.length > 0) {
|
|
2138
|
+
for (const item of items) {
|
|
2139
|
+
const itemItems = item.items;
|
|
2140
|
+
if (itemItems && Array.isArray(itemItems)) {
|
|
2141
|
+
for (const entry of itemItems) {
|
|
2142
|
+
const catalogEntry = entry.catalogEntry;
|
|
2143
|
+
if (catalogEntry?.version) {
|
|
2144
|
+
const ver = catalogEntry.version;
|
|
2145
|
+
if (!latestStable || !ver.includes("-") && latestStable.includes("-")) {
|
|
2146
|
+
latestStable = ver;
|
|
2147
|
+
} else if (!ver.includes("-") && !latestStable.includes("-")) {
|
|
2148
|
+
if (ver > latestStable) latestStable = ver;
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
return {
|
|
2156
|
+
latestStable,
|
|
2157
|
+
license: void 0,
|
|
2158
|
+
// License requires per-version catalog entry
|
|
2159
|
+
deprecated: void 0,
|
|
2160
|
+
yanked: void 0,
|
|
2161
|
+
retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2162
|
+
source: `https://api.nuget.org/v3/registration5-semver1/${name.toLowerCase()}/index.json`
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
},
|
|
2166
|
+
composer: {
|
|
2167
|
+
host: "repo.packagist.org",
|
|
2168
|
+
path: (name) => `/p2/${name}.json`,
|
|
2169
|
+
parser: (json, name) => {
|
|
2170
|
+
const packages = json.packages;
|
|
2171
|
+
const versions = packages?.[name];
|
|
2172
|
+
if (!versions || versions.length === 0) {
|
|
2173
|
+
return { retrievedAt: (/* @__PURE__ */ new Date()).toISOString(), source: "packagist" };
|
|
2174
|
+
}
|
|
2175
|
+
let latestStable;
|
|
2176
|
+
for (const ver of versions) {
|
|
2177
|
+
const version = ver.version;
|
|
2178
|
+
if (version && !version.includes("dev") && !version.includes("alpha") && !version.includes("beta") && !version.includes("RC") && !version.includes("rc")) {
|
|
2179
|
+
if (!latestStable || version > latestStable) {
|
|
2180
|
+
latestStable = version;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
const latest = versions[0];
|
|
2185
|
+
return {
|
|
2186
|
+
latestStable,
|
|
2187
|
+
license: latest.license ?? void 0,
|
|
2188
|
+
deprecated: latest.deprecated ?? void 0,
|
|
2189
|
+
yanked: latest.abandoned ?? void 0,
|
|
2190
|
+
retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2191
|
+
source: `https://repo.packagist.org/p2/${name}.json`
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
},
|
|
2195
|
+
pub: {
|
|
2196
|
+
host: "pub.dev",
|
|
2197
|
+
path: (name) => `/api/packages/${name}`,
|
|
2198
|
+
parser: (json) => {
|
|
2199
|
+
const latest = json.latest;
|
|
2200
|
+
return {
|
|
2201
|
+
latestStable: json.latestVersion ?? latest?.version ?? json.version ?? void 0,
|
|
2202
|
+
license: latest?.license ?? void 0,
|
|
2203
|
+
deprecated: json.isDiscontinued ?? void 0,
|
|
2204
|
+
yanked: json.isRetracted ?? void 0,
|
|
2205
|
+
retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2206
|
+
source: `https://pub.dev/api/packages/${json.name ?? ""}`
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
};
|
|
2211
|
+
async function lookupRegistry(ecosystem, name, options = {}) {
|
|
2212
|
+
const fetcher = ECOSYSTEM_FETCHERS[ecosystem];
|
|
2213
|
+
if (!fetcher) {
|
|
2214
|
+
throw new Error(`Unsupported ecosystem for registry lookup: ${ecosystem}`);
|
|
2215
|
+
}
|
|
2216
|
+
const path = fetcher.path(name);
|
|
2217
|
+
const cacheKey = getCacheKey(fetcher.host, path);
|
|
2218
|
+
if (!options.force) {
|
|
2219
|
+
const cached = getCached(cacheKey);
|
|
2220
|
+
if (cached) return cached;
|
|
2221
|
+
}
|
|
2222
|
+
await acquireHostSlot(fetcher.host);
|
|
2223
|
+
try {
|
|
2224
|
+
const existingEntry = registryCache.get(cacheKey);
|
|
2225
|
+
const etag = existingEntry?.etag;
|
|
2226
|
+
let lastError;
|
|
2227
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
2228
|
+
try {
|
|
2229
|
+
const response = await httpsFetch(fetcher.host, path, etag, options.signal);
|
|
2230
|
+
if (response.statusCode === 304 && existingEntry) {
|
|
2231
|
+
setCache(cacheKey, existingEntry.data, existingEntry.etag, DEFAULT_TTL_MS);
|
|
2232
|
+
return existingEntry.data;
|
|
2233
|
+
}
|
|
2234
|
+
if (response.statusCode === 401 || response.statusCode === 403 || response.statusCode === 404) {
|
|
2235
|
+
return void 0;
|
|
2236
|
+
}
|
|
2237
|
+
if (response.statusCode === 429 || response.statusCode >= 500) {
|
|
2238
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
2239
|
+
const backoff = computeBackoff(attempt, response.statusCode);
|
|
2240
|
+
await sleep(backoff);
|
|
2241
|
+
continue;
|
|
2242
|
+
}
|
|
2243
|
+
throw new Error(`Registry ${fetcher.host} returned ${response.statusCode} after ${MAX_RETRIES} attempts`);
|
|
2244
|
+
}
|
|
2245
|
+
let json;
|
|
2246
|
+
try {
|
|
2247
|
+
json = JSON.parse(response.body);
|
|
2248
|
+
} catch {
|
|
2249
|
+
throw new Error(`Invalid JSON response from ${fetcher.host}${path}`);
|
|
2250
|
+
}
|
|
2251
|
+
const parsed = fetcher.parser(json, name, ecosystem);
|
|
2252
|
+
if (!parsed) {
|
|
2253
|
+
return void 0;
|
|
2254
|
+
}
|
|
2255
|
+
const responseEtag = response.headers["etag"];
|
|
2256
|
+
setCache(cacheKey, parsed, responseEtag, DEFAULT_TTL_MS);
|
|
2257
|
+
return parsed;
|
|
2258
|
+
} catch (err) {
|
|
2259
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2260
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
2261
|
+
const isRateLimit = err instanceof Error && err.message.includes("429");
|
|
2262
|
+
const backoff = computeBackoff(attempt, isRateLimit ? 429 : 500);
|
|
2263
|
+
await sleep(backoff);
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
throw lastError ?? new Error(`Failed to look up ${ecosystem}:${name}`);
|
|
2268
|
+
} finally {
|
|
2269
|
+
releaseHostSlot(fetcher.host);
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
async function lookupRegistryBatch(ecosystem, names, options = {}) {
|
|
2273
|
+
const results = /* @__PURE__ */ new Map();
|
|
2274
|
+
const entries = await Promise.all(
|
|
2275
|
+
names.map(async (name) => {
|
|
2276
|
+
try {
|
|
2277
|
+
const entry = await lookupRegistry(ecosystem, name, options);
|
|
2278
|
+
return { name, entry };
|
|
2279
|
+
} catch {
|
|
2280
|
+
return { name, entry: void 0 };
|
|
2281
|
+
}
|
|
2282
|
+
})
|
|
2283
|
+
);
|
|
2284
|
+
for (const { name, entry } of entries) {
|
|
2285
|
+
results.set(name, entry);
|
|
2286
|
+
}
|
|
2287
|
+
return results;
|
|
2288
|
+
}
|
|
2289
|
+
function supportedRegistryEcosystems() {
|
|
2290
|
+
return Object.keys(ECOSYSTEM_FETCHERS);
|
|
2291
|
+
}
|
|
2292
|
+
function clearRegistryCache() {
|
|
2293
|
+
registryCache.clear();
|
|
2294
|
+
hostConcurrency.clear();
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
// src/advisory/osv.ts
|
|
2298
|
+
import { get as httpsGet2 } from "node:https";
|
|
2299
|
+
var OSV_API_BASE = "api.osv.dev";
|
|
2300
|
+
var OSV_QUERY_BATCH_PATH = "/v1/querybatch";
|
|
2301
|
+
var MAX_BATCH_SIZE = 500;
|
|
2302
|
+
var MAX_RETRIES2 = 3;
|
|
2303
|
+
var BASE_BACKOFF_MS2 = 1e3;
|
|
2304
|
+
function mapSeverity(osvSeverity, databaseSeverity) {
|
|
2305
|
+
if (osvSeverity && osvSeverity.length > 0) {
|
|
2306
|
+
for (const s of osvSeverity) {
|
|
2307
|
+
if (s.type === "CVSS_V3" || s.type === "CVSS_V2") {
|
|
2308
|
+
const score = parseFloat(s.score);
|
|
2309
|
+
if (score >= 9) return "critical";
|
|
2310
|
+
if (score >= 7) return "high";
|
|
2311
|
+
if (score >= 4) return "medium";
|
|
2312
|
+
if (score >= 0.1) return "low";
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
if (databaseSeverity) {
|
|
2317
|
+
const ds = databaseSeverity.toLowerCase();
|
|
2318
|
+
if (ds === "critical") return "critical";
|
|
2319
|
+
if (ds === "high") return "high";
|
|
2320
|
+
if (ds === "medium" || ds === "moderate") return "medium";
|
|
2321
|
+
if (ds === "low") return "low";
|
|
2322
|
+
}
|
|
2323
|
+
return "info";
|
|
2324
|
+
}
|
|
2325
|
+
function osvPostRequest(body, signal) {
|
|
2326
|
+
return new Promise((resolve3, reject) => {
|
|
2327
|
+
const options = {
|
|
2328
|
+
hostname: OSV_API_BASE,
|
|
2329
|
+
path: OSV_QUERY_BATCH_PATH,
|
|
2330
|
+
method: "POST",
|
|
2331
|
+
headers: {
|
|
2332
|
+
"Content-Type": "application/json",
|
|
2333
|
+
"Content-Length": Buffer.byteLength(body).toString(),
|
|
2334
|
+
"User-Agent": "WrongStack-TechStack/1.0"
|
|
2335
|
+
},
|
|
2336
|
+
signal,
|
|
2337
|
+
timeout: 3e4
|
|
2338
|
+
};
|
|
2339
|
+
const req = httpsGet2(options, (res) => {
|
|
2340
|
+
const statusCode = res.statusCode ?? 0;
|
|
2341
|
+
let responseBody = "";
|
|
2342
|
+
res.on("data", (chunk) => {
|
|
2343
|
+
responseBody += chunk;
|
|
2344
|
+
});
|
|
2345
|
+
res.on("end", () => {
|
|
2346
|
+
resolve3({ statusCode, body: responseBody });
|
|
2347
|
+
});
|
|
2348
|
+
});
|
|
2349
|
+
req.on("error", (err) => {
|
|
2350
|
+
reject(err);
|
|
2351
|
+
});
|
|
2352
|
+
req.on("timeout", () => {
|
|
2353
|
+
req.destroy();
|
|
2354
|
+
reject(new Error("OSV API request timeout"));
|
|
2355
|
+
});
|
|
2356
|
+
req.write(body);
|
|
2357
|
+
req.end();
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
function sleep2(ms) {
|
|
2361
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
2362
|
+
}
|
|
2363
|
+
async function queryOsvBatch(purls, options = {}) {
|
|
2364
|
+
const advisories = /* @__PURE__ */ new Map();
|
|
2365
|
+
for (const purl of purls) {
|
|
2366
|
+
advisories.set(purl, []);
|
|
2367
|
+
}
|
|
2368
|
+
const batches = [];
|
|
2369
|
+
for (let i = 0; i < purls.length; i += MAX_BATCH_SIZE) {
|
|
2370
|
+
batches.push(purls.slice(i, i + MAX_BATCH_SIZE));
|
|
2371
|
+
}
|
|
2372
|
+
let lastError;
|
|
2373
|
+
for (const batch of batches) {
|
|
2374
|
+
const requestBody = {
|
|
2375
|
+
queries: batch.map((purl) => ({
|
|
2376
|
+
package: { purl }
|
|
2377
|
+
}))
|
|
2378
|
+
};
|
|
2379
|
+
const jsonBody = JSON.stringify(requestBody);
|
|
2380
|
+
let success = false;
|
|
2381
|
+
for (let attempt = 0; attempt < MAX_RETRIES2 && !success; attempt++) {
|
|
2382
|
+
try {
|
|
2383
|
+
const response = await osvPostRequest(jsonBody, options.signal);
|
|
2384
|
+
if (response.statusCode === 200) {
|
|
2385
|
+
const result = JSON.parse(response.body);
|
|
2386
|
+
if (result.results && Array.isArray(result.results)) {
|
|
2387
|
+
for (let i = 0; i < result.results.length; i++) {
|
|
2388
|
+
const purl = batch[i];
|
|
2389
|
+
if (!purl) continue;
|
|
2390
|
+
const vulns = result.results[i]?.vulns;
|
|
2391
|
+
if (!vulns || vulns.length === 0) continue;
|
|
2392
|
+
const parsed = [];
|
|
2393
|
+
for (const vuln of vulns) {
|
|
2394
|
+
const dbSpecific = vuln.database_specific;
|
|
2395
|
+
const affectedDbSpecific = vuln.affected?.[0]?.database_specific;
|
|
2396
|
+
const severitySource = dbSpecific?.severity ?? affectedDbSpecific?.severity;
|
|
2397
|
+
parsed.push({
|
|
2398
|
+
id: vuln.id,
|
|
2399
|
+
summary: vuln.summary ?? vuln.details ?? "No summary available",
|
|
2400
|
+
severity: mapSeverity(vuln.severity, severitySource),
|
|
2401
|
+
aliases: vuln.aliases ?? []
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
advisories.set(purl, parsed);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
success = true;
|
|
2408
|
+
} else if (response.statusCode === 429 || response.statusCode >= 500) {
|
|
2409
|
+
if (attempt < MAX_RETRIES2 - 1) {
|
|
2410
|
+
const backoff = BASE_BACKOFF_MS2 * Math.pow(2, attempt) + Math.random() * 500;
|
|
2411
|
+
await sleep2(backoff);
|
|
2412
|
+
} else {
|
|
2413
|
+
throw new Error(`OSV API returned ${response.statusCode} after ${MAX_RETRIES2} attempts: ${response.body}`);
|
|
2414
|
+
}
|
|
2415
|
+
} else {
|
|
2416
|
+
throw new Error(`OSV API returned ${response.statusCode}: ${response.body}`);
|
|
2417
|
+
}
|
|
2418
|
+
} catch (err) {
|
|
2419
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
2420
|
+
if (attempt < MAX_RETRIES2 - 1) {
|
|
2421
|
+
const backoff = BASE_BACKOFF_MS2 * Math.pow(2, attempt) + Math.random() * 500;
|
|
2422
|
+
await sleep2(backoff);
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
if (!success && lastError) {
|
|
2427
|
+
throw lastError;
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
const evidence = {
|
|
2431
|
+
kind: "osv",
|
|
2432
|
+
source: "https://api.osv.dev/v1/querybatch",
|
|
2433
|
+
retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2434
|
+
detail: `Queried ${purls.length} packages in ${batches.length} batch(es)`
|
|
2435
|
+
};
|
|
2436
|
+
return { advisories, evidence };
|
|
2437
|
+
}
|
|
2438
|
+
async function queryOsvSingle(purl, options = {}) {
|
|
2439
|
+
const result = await queryOsvBatch([purl], options);
|
|
2440
|
+
return result.advisories.get(purl) ?? [];
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
// src/advisory/native-audit.ts
|
|
2444
|
+
import { spawnSync } from "node:child_process";
|
|
2445
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
2446
|
+
import { join as join6 } from "node:path";
|
|
2447
|
+
function npmSeverity(s) {
|
|
2448
|
+
switch (s.toLowerCase()) {
|
|
2449
|
+
case "critical":
|
|
2450
|
+
return "critical";
|
|
2451
|
+
case "high":
|
|
2452
|
+
return "high";
|
|
2453
|
+
case "moderate":
|
|
2454
|
+
case "medium":
|
|
2455
|
+
return "medium";
|
|
2456
|
+
case "low":
|
|
2457
|
+
return "low";
|
|
2458
|
+
default:
|
|
2459
|
+
return "info";
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
function cargoSeverity(s) {
|
|
2463
|
+
switch (s.toLowerCase()) {
|
|
2464
|
+
case "critical":
|
|
2465
|
+
return "critical";
|
|
2466
|
+
case "high":
|
|
2467
|
+
return "high";
|
|
2468
|
+
case "medium":
|
|
2469
|
+
return "medium";
|
|
2470
|
+
case "low":
|
|
2471
|
+
return "low";
|
|
2472
|
+
default:
|
|
2473
|
+
return "info";
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
function runNpmAudit(workspaceRoot2) {
|
|
2477
|
+
const result = runAuditCommand("npm", ["audit", "--json"], workspaceRoot2);
|
|
2478
|
+
const advisories = [];
|
|
2479
|
+
let detailLines = [];
|
|
2480
|
+
if (result.status === 0 || result.status === 1) {
|
|
2481
|
+
try {
|
|
2482
|
+
const json = JSON.parse(result.stdout || "{}");
|
|
2483
|
+
const vulnerabilities = json.vulnerabilities;
|
|
2484
|
+
if (vulnerabilities) {
|
|
2485
|
+
for (const [pkg, info] of Object.entries(vulnerabilities)) {
|
|
2486
|
+
const via = info.via;
|
|
2487
|
+
if (!via) continue;
|
|
2488
|
+
for (const advisory of via) {
|
|
2489
|
+
if (typeof advisory === "string") continue;
|
|
2490
|
+
const source = advisory.source;
|
|
2491
|
+
const name = advisory.name;
|
|
2492
|
+
if (typeof source === "number") continue;
|
|
2493
|
+
advisories.push({
|
|
2494
|
+
id: advisory.cve ?? advisory.ghsa ?? `npm-${pkg}-${name ?? "unknown"}`,
|
|
2495
|
+
packageName: pkg,
|
|
2496
|
+
severity: npmSeverity(info.severity ?? "info"),
|
|
2497
|
+
summary: advisory.title ?? (name ?? "No summary"),
|
|
2498
|
+
fixVersion: info.fixAvailable ?? void 0,
|
|
2499
|
+
url: advisory.url ?? void 0,
|
|
2500
|
+
aliases: advisory.cve ? [advisory.cve] : []
|
|
2501
|
+
});
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
const metadata = json.metadata;
|
|
2506
|
+
if (metadata) {
|
|
2507
|
+
detailLines = [
|
|
2508
|
+
`Total vulnerabilities: ${metadata.vulnerabilities ?? "unknown"}`,
|
|
2509
|
+
`Total dependencies: ${metadata.totalDependencies ?? "unknown"}`
|
|
2510
|
+
];
|
|
2511
|
+
}
|
|
2512
|
+
} catch {
|
|
2513
|
+
detailLines = ["Failed to parse npm audit JSON output"];
|
|
2514
|
+
}
|
|
2515
|
+
} else {
|
|
2516
|
+
detailLines = [`npm audit exited with code ${result.status}`];
|
|
2517
|
+
}
|
|
2518
|
+
const evidence = {
|
|
2519
|
+
kind: "audit",
|
|
2520
|
+
source: "npm audit --json",
|
|
2521
|
+
retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2522
|
+
detail: detailLines.join("\n") || `Found ${advisories.length} advisories`
|
|
2523
|
+
};
|
|
2524
|
+
return { advisories, evidence };
|
|
2525
|
+
}
|
|
2526
|
+
function runPipAudit(workspaceRoot2) {
|
|
2527
|
+
const reqFiles = ["requirements.txt", "requirements-dev.txt"];
|
|
2528
|
+
let reqFlag = "";
|
|
2529
|
+
for (const f of reqFiles) {
|
|
2530
|
+
if (existsSync2(join6(workspaceRoot2, f))) {
|
|
2531
|
+
reqFlag = `--requirement ${f}`;
|
|
2532
|
+
break;
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
const args = ["audit", "--format", "json"];
|
|
2536
|
+
if (reqFlag) {
|
|
2537
|
+
args.push(...reqFlag.split(" "));
|
|
2538
|
+
}
|
|
2539
|
+
const result = runAuditCommand("pip-audit" in process.env ? "pip-audit" : "pip-audit", args, workspaceRoot2);
|
|
2540
|
+
return parsePipAuditOutput(result);
|
|
2541
|
+
}
|
|
2542
|
+
function parsePipAuditOutput(result) {
|
|
2543
|
+
const advisories = [];
|
|
2544
|
+
let detailLines = [];
|
|
2545
|
+
if (result.status === 0) {
|
|
2546
|
+
try {
|
|
2547
|
+
const json = JSON.parse(result.stdout || "[]");
|
|
2548
|
+
for (const entry of json) {
|
|
2549
|
+
advisories.push({
|
|
2550
|
+
id: entry.id ?? entry.vulnerability_id ?? "unknown",
|
|
2551
|
+
packageName: entry.name ?? "",
|
|
2552
|
+
severity: npmSeverity(entry.severity ?? "info"),
|
|
2553
|
+
summary: entry.description ?? entry.vulnerability_id ?? "No summary",
|
|
2554
|
+
fixVersion: entry.fix_version ?? void 0,
|
|
2555
|
+
url: entry.advisory_url ?? void 0,
|
|
2556
|
+
aliases: entry.aliases ?? []
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2559
|
+
} catch {
|
|
2560
|
+
detailLines = ["Failed to parse pip-audit JSON output"];
|
|
2561
|
+
}
|
|
2562
|
+
} else {
|
|
2563
|
+
detailLines = [`pip-audit exited with code ${result.status}: ${result.stderr}`];
|
|
2564
|
+
}
|
|
2565
|
+
const evidence = {
|
|
2566
|
+
kind: "audit",
|
|
2567
|
+
source: "pip-audit --format json",
|
|
2568
|
+
retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2569
|
+
detail: detailLines.join("\n") || `Found ${advisories.length} advisories`
|
|
2570
|
+
};
|
|
2571
|
+
return { advisories, evidence };
|
|
2572
|
+
}
|
|
2573
|
+
function runCargoAudit(workspaceRoot2) {
|
|
2574
|
+
const result = runAuditCommand("cargo", ["audit", "--json"], workspaceRoot2);
|
|
2575
|
+
return parseCargoAuditOutput(result);
|
|
2576
|
+
}
|
|
2577
|
+
function parseCargoAuditOutput(result) {
|
|
2578
|
+
const advisories = [];
|
|
2579
|
+
let detailLines = [];
|
|
2580
|
+
if (result.status === 0) {
|
|
2581
|
+
try {
|
|
2582
|
+
const json = JSON.parse(result.stdout || "{}");
|
|
2583
|
+
const vulnerabilities = json.vulnerabilities;
|
|
2584
|
+
const advisoriesList = vulnerabilities?.list;
|
|
2585
|
+
if (advisoriesList) {
|
|
2586
|
+
for (const adv of advisoriesList) {
|
|
2587
|
+
const advisory = adv.advisory;
|
|
2588
|
+
const pkg = adv.package;
|
|
2589
|
+
if (!advisory) continue;
|
|
2590
|
+
advisories.push({
|
|
2591
|
+
id: advisory.id ?? "unknown",
|
|
2592
|
+
packageName: pkg?.name ?? "",
|
|
2593
|
+
severity: cargoSeverity((advisory.cvss ?? "").split("/")?.[0] ?? "info"),
|
|
2594
|
+
summary: advisory.title ?? advisory.description ?? "No summary",
|
|
2595
|
+
fixVersion: advisory.patched_versions ?? void 0,
|
|
2596
|
+
url: advisory.url ?? void 0,
|
|
2597
|
+
aliases: advisory.aliases ?? []
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
} catch {
|
|
2602
|
+
detailLines = ["Failed to parse cargo audit JSON output"];
|
|
2603
|
+
}
|
|
2604
|
+
} else {
|
|
2605
|
+
detailLines = [`cargo audit exited with code ${result.status}: ${result.stderr}`];
|
|
2606
|
+
}
|
|
2607
|
+
const evidence = {
|
|
2608
|
+
kind: "audit",
|
|
2609
|
+
source: "cargo audit --json",
|
|
2610
|
+
retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2611
|
+
detail: detailLines.join("\n") || `Found ${advisories.length} advisories`
|
|
2612
|
+
};
|
|
2613
|
+
return { advisories, evidence };
|
|
2614
|
+
}
|
|
2615
|
+
function runGoVulncheck(workspaceRoot2) {
|
|
2616
|
+
const result = runAuditCommand("govulncheck", ["-json"], workspaceRoot2);
|
|
2617
|
+
const advisories = [];
|
|
2618
|
+
let detailLines = [];
|
|
2619
|
+
if (result.status === 0 || result.status === 3) {
|
|
2620
|
+
try {
|
|
2621
|
+
const json = JSON.parse(result.stdout || "{}");
|
|
2622
|
+
const vulns = json.vulns;
|
|
2623
|
+
if (vulns) {
|
|
2624
|
+
for (const v of vulns) {
|
|
2625
|
+
const osv = v.osv;
|
|
2626
|
+
advisories.push({
|
|
2627
|
+
id: v.id ?? osv ?? "unknown",
|
|
2628
|
+
packageName: v.package ?? v.module_path ?? "",
|
|
2629
|
+
severity: "high",
|
|
2630
|
+
// govulncheck doesn't provide CVSS — default to high
|
|
2631
|
+
summary: v.details ?? v.description ?? osv ?? "No summary",
|
|
2632
|
+
fixVersion: v.fixed_version ?? void 0,
|
|
2633
|
+
url: v.url ?? void 0,
|
|
2634
|
+
aliases: osv ? [osv] : []
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
} catch {
|
|
2639
|
+
detailLines = ["Failed to parse govulncheck JSON output"];
|
|
2640
|
+
}
|
|
2641
|
+
} else if (result.status === 1) {
|
|
2642
|
+
detailLines = ["govulncheck: no vulnerabilities found"];
|
|
2643
|
+
} else {
|
|
2644
|
+
detailLines = [`govulncheck exited with code ${result.status}: ${result.stderr}`];
|
|
2645
|
+
}
|
|
2646
|
+
const evidence = {
|
|
2647
|
+
kind: "audit",
|
|
2648
|
+
source: "govulncheck -json",
|
|
2649
|
+
retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2650
|
+
detail: detailLines.join("\n") || `Found ${advisories.length} advisories`
|
|
2651
|
+
};
|
|
2652
|
+
return { advisories, evidence };
|
|
2653
|
+
}
|
|
2654
|
+
function runComposerAudit(workspaceRoot2) {
|
|
2655
|
+
const result = runAuditCommand("composer", ["audit", "--format=json"], workspaceRoot2);
|
|
2656
|
+
const advisories = [];
|
|
2657
|
+
let detailLines = [];
|
|
2658
|
+
if (result.status === 0) {
|
|
2659
|
+
try {
|
|
2660
|
+
const json = JSON.parse(result.stdout || "{}");
|
|
2661
|
+
const advisoriesJson = json.advisories;
|
|
2662
|
+
if (advisoriesJson) {
|
|
2663
|
+
for (const [pkg, advs] of Object.entries(advisoriesJson)) {
|
|
2664
|
+
for (const adv of advs) {
|
|
2665
|
+
advisories.push({
|
|
2666
|
+
id: adv.cve ?? adv.reference ?? `composer-${pkg}`,
|
|
2667
|
+
packageName: pkg,
|
|
2668
|
+
severity: npmSeverity(adv.severity ?? "medium"),
|
|
2669
|
+
summary: adv.title ?? adv.description ?? "No summary",
|
|
2670
|
+
fixVersion: adv.link ? adv.link.split("/").pop() : void 0,
|
|
2671
|
+
url: adv.link ?? void 0,
|
|
2672
|
+
aliases: adv.cve ? [adv.cve] : []
|
|
2673
|
+
});
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
} catch {
|
|
2678
|
+
detailLines = ["Failed to parse composer audit JSON output"];
|
|
2679
|
+
}
|
|
2680
|
+
} else {
|
|
2681
|
+
detailLines = [`composer audit exited with code ${result.status}: ${result.stderr}`];
|
|
2682
|
+
}
|
|
2683
|
+
const evidence = {
|
|
2684
|
+
kind: "audit",
|
|
2685
|
+
source: "composer audit --format=json",
|
|
2686
|
+
retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2687
|
+
detail: detailLines.join("\n") || `Found ${advisories.length} advisories`
|
|
2688
|
+
};
|
|
2689
|
+
return { advisories, evidence };
|
|
2690
|
+
}
|
|
2691
|
+
function runDotnetAudit(workspaceRoot2) {
|
|
2692
|
+
const result = runAuditCommand("dotnet", ["package", "audit", "--format", "json"], workspaceRoot2);
|
|
2693
|
+
const advisories = [];
|
|
2694
|
+
let detailLines = [];
|
|
2695
|
+
if (result.status === 0) {
|
|
2696
|
+
try {
|
|
2697
|
+
const json = JSON.parse(result.stdout || "{}");
|
|
2698
|
+
const vulnerabilities = json.vulnerabilities;
|
|
2699
|
+
const packages = json.packages;
|
|
2700
|
+
if (vulnerabilities) {
|
|
2701
|
+
const vulnList = Array.isArray(vulnerabilities) ? vulnerabilities : [];
|
|
2702
|
+
for (const v of vulnList) {
|
|
2703
|
+
advisories.push({
|
|
2704
|
+
id: v.advisoryId ?? v.id ?? "unknown",
|
|
2705
|
+
packageName: v.packageName ?? "",
|
|
2706
|
+
severity: npmSeverity(v.severity ?? "info"),
|
|
2707
|
+
summary: v.description ?? v.title ?? "No summary",
|
|
2708
|
+
fixVersion: v.fixedVersion ?? v.patchedVersion ?? void 0,
|
|
2709
|
+
url: v.advisoryUrl ?? v.url ?? void 0,
|
|
2710
|
+
aliases: v.aliases ?? []
|
|
2711
|
+
});
|
|
2712
|
+
}
|
|
2713
|
+
} else if (packages) {
|
|
2714
|
+
for (const [pkg, entries] of Object.entries(packages)) {
|
|
2715
|
+
for (const entry of entries) {
|
|
2716
|
+
advisories.push({
|
|
2717
|
+
id: entry.advisoryId ?? entry.id ?? "unknown",
|
|
2718
|
+
packageName: pkg,
|
|
2719
|
+
severity: npmSeverity(entry.severity ?? "info"),
|
|
2720
|
+
summary: entry.description ?? entry.title ?? "No summary",
|
|
2721
|
+
fixVersion: entry.fixedVersion ?? entry.patchedVersion ?? void 0,
|
|
2722
|
+
url: entry.advisoryUrl ?? entry.url ?? void 0,
|
|
2723
|
+
aliases: entry.aliases ?? []
|
|
2724
|
+
});
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
} catch {
|
|
2729
|
+
detailLines = ["Failed to parse dotnet package audit JSON output"];
|
|
2730
|
+
}
|
|
2731
|
+
} else {
|
|
2732
|
+
detailLines = [`dotnet package audit exited with code ${result.status}: ${result.stderr}`];
|
|
2733
|
+
}
|
|
2734
|
+
const evidence = {
|
|
2735
|
+
kind: "audit",
|
|
2736
|
+
source: "dotnet package audit --format json",
|
|
2737
|
+
retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2738
|
+
detail: detailLines.join("\n") || `Found ${advisories.length} advisories`
|
|
2739
|
+
};
|
|
2740
|
+
return { advisories, evidence };
|
|
2741
|
+
}
|
|
2742
|
+
function runAuditCommand(command, args, cwd) {
|
|
2743
|
+
try {
|
|
2744
|
+
const options = {
|
|
2745
|
+
cwd,
|
|
2746
|
+
encoding: "utf-8",
|
|
2747
|
+
timeout: 6e4,
|
|
2748
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
2749
|
+
// 10MB
|
|
2750
|
+
windowsHide: true
|
|
2751
|
+
};
|
|
2752
|
+
const result = spawnSync(command, args, options);
|
|
2753
|
+
return {
|
|
2754
|
+
status: result.status,
|
|
2755
|
+
stdout: result.stdout?.toString() ?? "",
|
|
2756
|
+
stderr: result.stderr?.toString() ?? ""
|
|
2757
|
+
};
|
|
2758
|
+
} catch (err) {
|
|
2759
|
+
return {
|
|
2760
|
+
status: null,
|
|
2761
|
+
stdout: "",
|
|
2762
|
+
stderr: err instanceof Error ? err.message : String(err)
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
function runNativeAudit(ecosystem, workspaceRoot2) {
|
|
2767
|
+
switch (ecosystem) {
|
|
2768
|
+
case "npm":
|
|
2769
|
+
return runNpmAudit(workspaceRoot2);
|
|
2770
|
+
case "python":
|
|
2771
|
+
return runPipAudit(workspaceRoot2);
|
|
2772
|
+
case "rust":
|
|
2773
|
+
return runCargoAudit(workspaceRoot2);
|
|
2774
|
+
case "go":
|
|
2775
|
+
return runGoVulncheck(workspaceRoot2);
|
|
2776
|
+
case "php":
|
|
2777
|
+
return runComposerAudit(workspaceRoot2);
|
|
2778
|
+
case "dotnet":
|
|
2779
|
+
return runDotnetAudit(workspaceRoot2);
|
|
2780
|
+
// dart/pub doesn't have a standard audit command — use OSV instead
|
|
2781
|
+
default:
|
|
2782
|
+
return {
|
|
2783
|
+
advisories: [],
|
|
2784
|
+
evidence: {
|
|
2785
|
+
kind: "audit",
|
|
2786
|
+
source: `native-audit:${ecosystem}`,
|
|
2787
|
+
retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2788
|
+
detail: `No native audit tool for ecosystem: ${ecosystem}`
|
|
2789
|
+
}
|
|
2790
|
+
};
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
function isNativeAuditAvailable(ecosystem) {
|
|
2794
|
+
const result = runAuditCommand(
|
|
2795
|
+
ecosystem === "npm" ? "npm" : ecosystem === "python" ? "pip-audit" : ecosystem === "rust" ? "cargo" : ecosystem === "go" ? "govulncheck" : ecosystem === "php" ? "composer" : ecosystem === "dotnet" ? "dotnet" : "",
|
|
2796
|
+
["--version"],
|
|
2797
|
+
process.cwd()
|
|
2798
|
+
);
|
|
2799
|
+
return result.status === 0;
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
// src/policy/status.ts
|
|
2803
|
+
function isValidSemver(version) {
|
|
2804
|
+
return /^\d+\.\d+\.\d+/.test(version);
|
|
2805
|
+
}
|
|
2806
|
+
function compareVersions(a, b) {
|
|
2807
|
+
const aMatch = a.match(/^([^-]+)(?:-(.+))?$/);
|
|
2808
|
+
const bMatch = b.match(/^([^-]+)(?:-(.+))?$/);
|
|
2809
|
+
const aBase = aMatch?.[1] ?? a;
|
|
2810
|
+
const aPre = aMatch?.[2];
|
|
2811
|
+
const bBase = bMatch?.[1] ?? b;
|
|
2812
|
+
const bPre = bMatch?.[2];
|
|
2813
|
+
const aBaseParts = aBase.split(".").map(Number);
|
|
2814
|
+
const bBaseParts = bBase.split(".").map(Number);
|
|
2815
|
+
for (let i = 0; i < Math.max(aBaseParts.length, bBaseParts.length); i++) {
|
|
2816
|
+
const aNum = aBaseParts[i] ?? 0;
|
|
2817
|
+
const bNum = bBaseParts[i] ?? 0;
|
|
2818
|
+
if (aNum > bNum) return 1;
|
|
2819
|
+
if (aNum < bNum) return -1;
|
|
2820
|
+
}
|
|
2821
|
+
if (aPre === bPre) return 0;
|
|
2822
|
+
if (aPre === void 0) return 1;
|
|
2823
|
+
if (bPre === void 0) return -1;
|
|
2824
|
+
const aPreParts = aPre.split(".");
|
|
2825
|
+
const bPreParts = bPre.split(".");
|
|
2826
|
+
for (let i = 0; i < Math.max(aPreParts.length, bPreParts.length); i++) {
|
|
2827
|
+
const aId = aPreParts[i] ?? "";
|
|
2828
|
+
const bId = bPreParts[i] ?? "";
|
|
2829
|
+
if (aId === bId) continue;
|
|
2830
|
+
const aNum = Number(aId);
|
|
2831
|
+
const bNum = Number(bId);
|
|
2832
|
+
if (!Number.isNaN(aNum) && !Number.isNaN(bNum) && Number.isFinite(aNum) && Number.isFinite(bNum)) {
|
|
2833
|
+
if (aNum > bNum) return 1;
|
|
2834
|
+
if (aNum < bNum) return -1;
|
|
2835
|
+
} else {
|
|
2836
|
+
if (aId > bId) return 1;
|
|
2837
|
+
if (aId < bId) return -1;
|
|
2838
|
+
}
|
|
2839
|
+
return aId > bId ? 1 : -1;
|
|
2840
|
+
}
|
|
2841
|
+
return 0;
|
|
2842
|
+
}
|
|
2843
|
+
function isSimpleConstraint(constraint) {
|
|
2844
|
+
return constraint.startsWith("^") || constraint.startsWith("~") || constraint.startsWith(">=") || constraint.startsWith(">") || isValidSemver(constraint);
|
|
2845
|
+
}
|
|
2846
|
+
function isBreakingUpgrade(locked, latestStable, constraint) {
|
|
2847
|
+
const constraintNorm = constraint?.trim() ?? "";
|
|
2848
|
+
const lockedMajor = locked.split(".")[0];
|
|
2849
|
+
const latestMajor = latestStable.split(".")[0];
|
|
2850
|
+
if (!lockedMajor || !latestMajor) return true;
|
|
2851
|
+
if (constraintNorm.startsWith("^")) {
|
|
2852
|
+
return lockedMajor !== latestMajor;
|
|
2853
|
+
}
|
|
2854
|
+
if (constraintNorm.startsWith("~")) {
|
|
2855
|
+
const lockedMinor = locked.split(".")[1];
|
|
2856
|
+
const latestMinor = latestStable.split(".")[1];
|
|
2857
|
+
if (lockedMinor && latestMinor && lockedMajor !== latestMajor) return true;
|
|
2858
|
+
if (lockedMinor && latestMinor && lockedMinor !== latestMinor) return true;
|
|
2859
|
+
return false;
|
|
2860
|
+
}
|
|
2861
|
+
if (constraintNorm.startsWith(">=") || constraintNorm.startsWith(">")) {
|
|
2862
|
+
return lockedMajor !== latestMajor;
|
|
2863
|
+
}
|
|
2864
|
+
if (isValidSemver(constraintNorm)) {
|
|
2865
|
+
return lockedMajor !== latestMajor;
|
|
2866
|
+
}
|
|
2867
|
+
return lockedMajor !== latestMajor;
|
|
2868
|
+
}
|
|
2869
|
+
function classifyStatus(dep, registryData, advisoryData) {
|
|
2870
|
+
if (dep.sourceType === "path") {
|
|
2871
|
+
return "local_path";
|
|
2872
|
+
}
|
|
2873
|
+
if (dep.sourceType === "git") {
|
|
2874
|
+
return "git_dependency";
|
|
2875
|
+
}
|
|
2876
|
+
if (registryData?.privateOrUnresolved) {
|
|
2877
|
+
return "private_or_unresolved";
|
|
2878
|
+
}
|
|
2879
|
+
if (registryData?.lookupFailed) {
|
|
2880
|
+
return "unknown";
|
|
2881
|
+
}
|
|
2882
|
+
if (registryData?.deprecated) {
|
|
2883
|
+
return "deprecated";
|
|
2884
|
+
}
|
|
2885
|
+
if (registryData?.yanked) {
|
|
2886
|
+
return "yanked";
|
|
2887
|
+
}
|
|
2888
|
+
if (advisoryData?.hasAdvisory) {
|
|
2889
|
+
return "vulnerable";
|
|
2890
|
+
}
|
|
2891
|
+
const locked = dep.locked;
|
|
2892
|
+
const latestStable = registryData?.latestStable;
|
|
2893
|
+
if (locked && latestStable) {
|
|
2894
|
+
if (locked === latestStable) {
|
|
2895
|
+
return "current";
|
|
2896
|
+
}
|
|
2897
|
+
try {
|
|
2898
|
+
const cmp = compareVersions(locked, latestStable);
|
|
2899
|
+
if (cmp < 0) {
|
|
2900
|
+
const constraint = dep.requested;
|
|
2901
|
+
if (constraint && isSimpleConstraint(constraint)) {
|
|
2902
|
+
const breaking = isBreakingUpgrade(locked, latestStable, constraint);
|
|
2903
|
+
return breaking ? "update_available_breaking" : "update_available_safe";
|
|
2904
|
+
}
|
|
2905
|
+
return "update_available_safe";
|
|
2906
|
+
}
|
|
2907
|
+
return "current";
|
|
2908
|
+
} catch {
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
if (dep.status === "local_path" || dep.status === "git_dependency") {
|
|
2912
|
+
return dep.status;
|
|
2913
|
+
}
|
|
2914
|
+
return dep.status ?? "current";
|
|
2915
|
+
}
|
|
2916
|
+
function privateOrUnresolvedStatus(source, detail) {
|
|
2917
|
+
return {
|
|
2918
|
+
privateOrUnresolved: true,
|
|
2919
|
+
evidence: [
|
|
2920
|
+
{
|
|
2921
|
+
kind: "registry",
|
|
2922
|
+
source,
|
|
2923
|
+
retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2924
|
+
detail: detail ?? "Package returned 404/401 \u2014 private or unresolved"
|
|
2925
|
+
}
|
|
2926
|
+
]
|
|
2927
|
+
};
|
|
2928
|
+
}
|
|
2929
|
+
function failedLookupStatus(source, error) {
|
|
2930
|
+
return {
|
|
2931
|
+
lookupFailed: true,
|
|
2932
|
+
evidence: [
|
|
2933
|
+
{
|
|
2934
|
+
kind: "registry",
|
|
2935
|
+
source,
|
|
2936
|
+
retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2937
|
+
detail: error ?? "Registry lookup failed \u2014 network error or timeout"
|
|
2938
|
+
}
|
|
2939
|
+
]
|
|
2940
|
+
};
|
|
2941
|
+
}
|
|
2942
|
+
|
|
2943
|
+
// src/service.ts
|
|
2944
|
+
import { randomUUID } from "node:crypto";
|
|
2945
|
+
|
|
2946
|
+
// src/research/triage.ts
|
|
2947
|
+
var DEFAULT_TRIAGE_LIMIT = 40;
|
|
2948
|
+
var CLUSTER_BY_STATUS = {
|
|
2949
|
+
vulnerable: "vulnerability",
|
|
2950
|
+
yanked: "replacement",
|
|
2951
|
+
deprecated: "replacement",
|
|
2952
|
+
unmaintained_suspected: "replacement",
|
|
2953
|
+
update_available_breaking: "breaking_change"
|
|
2954
|
+
};
|
|
2955
|
+
var PRIORITY_BY_STATUS = {
|
|
2956
|
+
vulnerable: 100,
|
|
2957
|
+
yanked: 80,
|
|
2958
|
+
deprecated: 60,
|
|
2959
|
+
update_available_breaking: 40,
|
|
2960
|
+
unmaintained_suspected: 30
|
|
2961
|
+
};
|
|
2962
|
+
var DIRECT_BONUS = 10;
|
|
2963
|
+
var RUNTIME_BONUS = 5;
|
|
2964
|
+
function priorityFor(dep) {
|
|
2965
|
+
const base = PRIORITY_BY_STATUS[dep.status] ?? 0;
|
|
2966
|
+
const direct = dep.direct ? DIRECT_BONUS : 0;
|
|
2967
|
+
const runtime = dep.scope === "runtime" ? RUNTIME_BONUS : 0;
|
|
2968
|
+
return base + direct + runtime;
|
|
2969
|
+
}
|
|
2970
|
+
function dedupKey(dep) {
|
|
2971
|
+
return `${dep.ecosystem}\0${dep.name}\0${dep.locked ?? dep.requested ?? ""}`;
|
|
2972
|
+
}
|
|
2973
|
+
function triageCandidates(dependencies, options = {}) {
|
|
2974
|
+
const limit = Math.max(0, options.limit ?? DEFAULT_TRIAGE_LIMIT);
|
|
2975
|
+
if (limit === 0) return [];
|
|
2976
|
+
const best = /* @__PURE__ */ new Map();
|
|
2977
|
+
for (const dependency of dependencies) {
|
|
2978
|
+
const cluster = CLUSTER_BY_STATUS[dependency.status];
|
|
2979
|
+
if (!cluster) continue;
|
|
2980
|
+
if (dependency.sourceType === "path" || dependency.sourceType === "git") continue;
|
|
2981
|
+
const candidate = {
|
|
2982
|
+
dependency,
|
|
2983
|
+
cluster,
|
|
2984
|
+
priority: priorityFor(dependency)
|
|
2985
|
+
};
|
|
2986
|
+
const key = dedupKey(dependency);
|
|
2987
|
+
const existing = best.get(key);
|
|
2988
|
+
if (!existing || candidate.priority > existing.priority) {
|
|
2989
|
+
best.set(key, candidate);
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
return [...best.values()].sort(
|
|
2993
|
+
(a, b) => b.priority - a.priority || a.dependency.name.localeCompare(b.dependency.name) || (a.dependency.locked ?? "").localeCompare(b.dependency.locked ?? "")
|
|
2994
|
+
).slice(0, limit);
|
|
2995
|
+
}
|
|
2996
|
+
function clusterCandidates(candidates) {
|
|
2997
|
+
const out = /* @__PURE__ */ new Map();
|
|
2998
|
+
for (const candidate of candidates) {
|
|
2999
|
+
const list = out.get(candidate.cluster);
|
|
3000
|
+
if (list) list.push(candidate);
|
|
3001
|
+
else out.set(candidate.cluster, [candidate]);
|
|
3002
|
+
}
|
|
3003
|
+
return out;
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
// src/service.ts
|
|
3007
|
+
function getAdapter(ecosystem) {
|
|
3008
|
+
switch (ecosystem) {
|
|
3009
|
+
case "npm":
|
|
3010
|
+
return npmAdapter;
|
|
3011
|
+
case "python":
|
|
3012
|
+
return pythonAdapter;
|
|
3013
|
+
case "rust":
|
|
3014
|
+
return rustAdapter;
|
|
3015
|
+
case "go":
|
|
3016
|
+
return goAdapter;
|
|
3017
|
+
case "dotnet":
|
|
3018
|
+
return dotNetAdapter;
|
|
3019
|
+
case "php":
|
|
3020
|
+
return phpAdapter;
|
|
3021
|
+
case "dart":
|
|
3022
|
+
return dartAdapter;
|
|
3023
|
+
// Tier B — partial support
|
|
3024
|
+
case "maven":
|
|
3025
|
+
return mavenAdapter;
|
|
3026
|
+
case "gradle":
|
|
3027
|
+
return mavenAdapter;
|
|
3028
|
+
// reuse Maven adapter (same manifest family)
|
|
3029
|
+
case "ruby":
|
|
3030
|
+
return rubyAdapter;
|
|
3031
|
+
case "swift":
|
|
3032
|
+
return void 0;
|
|
3033
|
+
// no adapter yet
|
|
3034
|
+
case "elixir":
|
|
3035
|
+
return elixirAdapter;
|
|
3036
|
+
// Tier C — best-effort
|
|
3037
|
+
case "cpp":
|
|
3038
|
+
return cppAdapter;
|
|
3039
|
+
default:
|
|
3040
|
+
return void 0;
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
var ADAPTER_VERSION = "0.1.0";
|
|
3044
|
+
var TechStackEngine = class {
|
|
3045
|
+
store;
|
|
3046
|
+
constructor(store) {
|
|
3047
|
+
this.store = store;
|
|
3048
|
+
}
|
|
3049
|
+
// ── Inventory ─────────────────────────────────────────────────────────
|
|
3050
|
+
/**
|
|
3051
|
+
* Run an offline inventory: discover workspaces and parse dependencies
|
|
3052
|
+
* from manifests and lockfiles. No network calls.
|
|
3053
|
+
*
|
|
3054
|
+
* Returns a Snapshot with workspace and dependency data but no
|
|
3055
|
+
* registry/advisory enrichment.
|
|
3056
|
+
*/
|
|
3057
|
+
async inventory(projectId, targetRoot, _jobId, onProgress) {
|
|
3058
|
+
const snapshotId = randomUUID();
|
|
3059
|
+
onProgress?.("discovering", 0, 1);
|
|
3060
|
+
const rawWorkspaces = await discoverWorkspaces(targetRoot);
|
|
3061
|
+
const workspaces = rawWorkspaces.map((w) => ({
|
|
3062
|
+
id: w.id,
|
|
3063
|
+
relativeRoot: w.relativeRoot,
|
|
3064
|
+
ecosystem: w.ecosystem,
|
|
3065
|
+
packageManager: w.packageManager,
|
|
3066
|
+
manifests: [...w.manifests],
|
|
3067
|
+
lockfiles: [...w.lockfiles],
|
|
3068
|
+
confidence: w.confidence,
|
|
3069
|
+
coverage: w.coverage
|
|
3070
|
+
}));
|
|
3071
|
+
onProgress?.("inventorying", 0, workspaces.length);
|
|
3072
|
+
const allDependencies = [];
|
|
3073
|
+
let totalCoverage = "full";
|
|
3074
|
+
for (let i = 0; i < workspaces.length; i++) {
|
|
3075
|
+
const ws = workspaces[i];
|
|
3076
|
+
const adapter = getAdapter(ws.ecosystem);
|
|
3077
|
+
let deps = [];
|
|
3078
|
+
if (adapter) {
|
|
3079
|
+
try {
|
|
3080
|
+
deps = await adapter.inventory(ws, { projectRoot: targetRoot });
|
|
3081
|
+
} catch {
|
|
3082
|
+
deps = [];
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
if (ws.coverage === "unsupported") {
|
|
3086
|
+
totalCoverage = "partial";
|
|
3087
|
+
}
|
|
3088
|
+
allDependencies.push(...deps);
|
|
3089
|
+
onProgress?.("inventorying", i + 1, workspaces.length);
|
|
3090
|
+
}
|
|
3091
|
+
const fingerprint = computeFingerprint(allDependencies);
|
|
3092
|
+
const snapshot = {
|
|
3093
|
+
id: snapshotId,
|
|
3094
|
+
projectId,
|
|
3095
|
+
targetRoot,
|
|
3096
|
+
fingerprint,
|
|
3097
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3098
|
+
workspaces,
|
|
3099
|
+
dependencies: allDependencies,
|
|
3100
|
+
findings: [],
|
|
3101
|
+
coverage: totalCoverage,
|
|
3102
|
+
adapterVersion: ADAPTER_VERSION
|
|
3103
|
+
};
|
|
3104
|
+
this.store.saveSnapshot(snapshot);
|
|
3105
|
+
return snapshot;
|
|
3106
|
+
}
|
|
3107
|
+
// ── Enrichment ─────────────────────────────────────────────────────────
|
|
3108
|
+
/**
|
|
3109
|
+
* Enrich a snapshot with registry metadata and advisory data.
|
|
3110
|
+
*
|
|
3111
|
+
* This is the online pass: fetches latest versions, licenses, deprecation
|
|
3112
|
+
* status from registries, and queries OSV for advisories.
|
|
3113
|
+
*
|
|
3114
|
+
* Key contracts:
|
|
3115
|
+
* - 404/401 → status `private_or_unresolved` (never `dead` or `deprecated`)
|
|
3116
|
+
* - Network failure → status `unknown` with evidence detail (never `current`)
|
|
3117
|
+
*/
|
|
3118
|
+
async enrich(snapshot, options = {}) {
|
|
3119
|
+
const isOnline = options.online !== false;
|
|
3120
|
+
if (!isOnline || options.signal?.aborted) {
|
|
3121
|
+
return snapshot;
|
|
3122
|
+
}
|
|
3123
|
+
const byEcosystem = /* @__PURE__ */ new Map();
|
|
3124
|
+
for (const dep of snapshot.dependencies) {
|
|
3125
|
+
if (dep.sourceType === "path" || dep.sourceType === "git") continue;
|
|
3126
|
+
if (!dep.purl) continue;
|
|
3127
|
+
const list = byEcosystem.get(dep.ecosystem);
|
|
3128
|
+
if (list) {
|
|
3129
|
+
list.push(dep);
|
|
3130
|
+
} else {
|
|
3131
|
+
byEcosystem.set(dep.ecosystem, [dep]);
|
|
3132
|
+
}
|
|
3133
|
+
}
|
|
3134
|
+
const enrichedDeps = /* @__PURE__ */ new Map();
|
|
3135
|
+
const allFindings = [...snapshot.findings];
|
|
3136
|
+
for (const [ecosystem, deps] of byEcosystem) {
|
|
3137
|
+
const names = [...new Set(deps.map((d) => d.name))];
|
|
3138
|
+
for (const name of names) {
|
|
3139
|
+
const lookupOpts = {};
|
|
3140
|
+
if (options.signal) lookupOpts.signal = options.signal;
|
|
3141
|
+
if (options.forceRegistryRefresh) lookupOpts.force = true;
|
|
3142
|
+
let registryEntry;
|
|
3143
|
+
let registryStatus;
|
|
3144
|
+
try {
|
|
3145
|
+
registryEntry = await lookupRegistry(ecosystem, name, lookupOpts);
|
|
3146
|
+
if (registryEntry) {
|
|
3147
|
+
registryStatus = {
|
|
3148
|
+
latestStable: registryEntry.latestStable,
|
|
3149
|
+
deprecated: registryEntry.deprecated,
|
|
3150
|
+
yanked: registryEntry.yanked,
|
|
3151
|
+
evidence: [
|
|
3152
|
+
{
|
|
3153
|
+
kind: "registry",
|
|
3154
|
+
source: registryEntry.source,
|
|
3155
|
+
retrievedAt: registryEntry.retrievedAt,
|
|
3156
|
+
detail: `latestStable: ${registryEntry.latestStable ?? "N/A"}, license: ${registryEntry.license ?? "N/A"}`
|
|
3157
|
+
}
|
|
3158
|
+
]
|
|
3159
|
+
};
|
|
3160
|
+
} else {
|
|
3161
|
+
registryStatus = {
|
|
3162
|
+
privateOrUnresolved: true,
|
|
3163
|
+
evidence: [
|
|
3164
|
+
{
|
|
3165
|
+
kind: "registry",
|
|
3166
|
+
source: `${ecosystem} registry for ${name}`,
|
|
3167
|
+
retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3168
|
+
detail: "Package returned 404/401 \u2014 private or unresolved"
|
|
3169
|
+
}
|
|
3170
|
+
]
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
3173
|
+
} catch (err) {
|
|
3174
|
+
registryStatus = {
|
|
3175
|
+
lookupFailed: true,
|
|
3176
|
+
evidence: [
|
|
3177
|
+
{
|
|
3178
|
+
kind: "registry",
|
|
3179
|
+
source: `${ecosystem} registry for ${name}`,
|
|
3180
|
+
retrievedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3181
|
+
detail: err instanceof Error ? err.message : "Registry lookup failed"
|
|
3182
|
+
}
|
|
3183
|
+
]
|
|
3184
|
+
};
|
|
3185
|
+
}
|
|
3186
|
+
let advisoryStatus;
|
|
3187
|
+
try {
|
|
3188
|
+
const depList = deps.filter((d) => d.name === name);
|
|
3189
|
+
const purls = depList.map((d) => d.purl).filter((p) => !!p);
|
|
3190
|
+
if (purls.length > 0) {
|
|
3191
|
+
const osvResult = await queryOsvBatch(purls, { signal: options.signal });
|
|
3192
|
+
const hasAdvisory = [...osvResult.advisories.values()].some(
|
|
3193
|
+
(advisories) => advisories.length > 0
|
|
3194
|
+
);
|
|
3195
|
+
if (hasAdvisory) {
|
|
3196
|
+
advisoryStatus = {
|
|
3197
|
+
hasAdvisory: true
|
|
3198
|
+
};
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
} catch {
|
|
3202
|
+
}
|
|
3203
|
+
for (const dep of deps) {
|
|
3204
|
+
if (dep.name !== name) continue;
|
|
3205
|
+
const newStatus = classifyStatus(dep, registryStatus, advisoryStatus);
|
|
3206
|
+
const newEvidence = [
|
|
3207
|
+
...dep.evidence,
|
|
3208
|
+
...registryStatus?.evidence ?? [],
|
|
3209
|
+
...advisoryStatus?.evidence ?? []
|
|
3210
|
+
];
|
|
3211
|
+
enrichedDeps.set(dep.id, {
|
|
3212
|
+
...dep,
|
|
3213
|
+
latestStable: registryEntry?.latestStable ?? dep.latestStable,
|
|
3214
|
+
license: registryEntry?.license ?? dep.license,
|
|
3215
|
+
deprecated: registryEntry?.deprecated ?? dep.deprecated,
|
|
3216
|
+
yanked: registryEntry?.yanked ?? dep.yanked,
|
|
3217
|
+
status: newStatus,
|
|
3218
|
+
evidence: newEvidence
|
|
3219
|
+
});
|
|
3220
|
+
if (newStatus !== "current" && newStatus !== "local_path" && newStatus !== "git_dependency") {
|
|
3221
|
+
allFindings.push(
|
|
3222
|
+
createFindingForStatus(dep.id, newStatus, registryEntry?.license)
|
|
3223
|
+
);
|
|
3224
|
+
}
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
const finalDependencies = snapshot.dependencies.map(
|
|
3229
|
+
(dep) => enrichedDeps.get(dep.id) ?? dep
|
|
3230
|
+
);
|
|
3231
|
+
return {
|
|
3232
|
+
...snapshot,
|
|
3233
|
+
dependencies: finalDependencies,
|
|
3234
|
+
findings: allFindings
|
|
3235
|
+
};
|
|
3236
|
+
}
|
|
3237
|
+
// ── Research ───────────────────────────────────────────────────────────
|
|
3238
|
+
/**
|
|
3239
|
+
* Interpret an enriched snapshot with the LLM: triage the problem cases,
|
|
3240
|
+
* research them, and append the resulting findings.
|
|
3241
|
+
*
|
|
3242
|
+
* Additive by construction. Two invariants hold here, and they are the whole
|
|
3243
|
+
* reason this is a separate pass rather than part of `enrich()`:
|
|
3244
|
+
*
|
|
3245
|
+
* 1. **`snapshot.dependencies` is returned untouched.** Version facts come
|
|
3246
|
+
* only from registry evidence — the LLM cannot fabricate a `latestStable`
|
|
3247
|
+
* because `Finding` has nowhere to put one (SDD §472).
|
|
3248
|
+
* 2. **Failure is not fatal.** No researcher, no candidates, a provider
|
|
3249
|
+
* outage, a dry web search — every one of them returns the input snapshot
|
|
3250
|
+
* unchanged. A deterministic report is the floor, never a casualty of the
|
|
3251
|
+
* optional stage above it.
|
|
3252
|
+
*
|
|
3253
|
+
* @see docs/specs/techstack-sdd.md §31, §472
|
|
3254
|
+
*/
|
|
3255
|
+
async research(snapshot, options = {}) {
|
|
3256
|
+
if (!options.researcher || options.signal?.aborted) return snapshot;
|
|
3257
|
+
const candidates = triageCandidates(snapshot.dependencies, {
|
|
3258
|
+
limit: options.researchLimit
|
|
3259
|
+
});
|
|
3260
|
+
if (candidates.length === 0) return snapshot;
|
|
3261
|
+
options.onProgress?.("researching", 0, candidates.length);
|
|
3262
|
+
let findings;
|
|
3263
|
+
try {
|
|
3264
|
+
findings = await options.researcher.research(candidates, {
|
|
3265
|
+
signal: options.signal,
|
|
3266
|
+
onProgress: (completed, total) => {
|
|
3267
|
+
options.onProgress?.("researching", completed, total);
|
|
3268
|
+
}
|
|
3269
|
+
});
|
|
3270
|
+
} catch {
|
|
3271
|
+
return snapshot;
|
|
3272
|
+
}
|
|
3273
|
+
options.onProgress?.("synthesizing", 1, 1);
|
|
3274
|
+
if (findings.length === 0) return snapshot;
|
|
3275
|
+
return { ...snapshot, findings: [...snapshot.findings, ...findings] };
|
|
3276
|
+
}
|
|
3277
|
+
// ── Analyze ───────────────────────────────────────────────────────────
|
|
3278
|
+
/**
|
|
3279
|
+
* Run a full analysis: inventory + enrich + research + persist.
|
|
3280
|
+
* This is the main entry point for the analyze job flow.
|
|
3281
|
+
*/
|
|
3282
|
+
async analyze(projectId, options) {
|
|
3283
|
+
const jobId = options.jobId ?? randomUUID();
|
|
3284
|
+
const requestedBy = options.requestedBy ?? "system";
|
|
3285
|
+
const job = {
|
|
3286
|
+
id: jobId,
|
|
3287
|
+
projectId,
|
|
3288
|
+
targetRoot: options.targetRoot,
|
|
3289
|
+
kind: "analyze",
|
|
3290
|
+
status: "queued",
|
|
3291
|
+
fingerprint: "",
|
|
3292
|
+
requestedBy,
|
|
3293
|
+
sessionId: options.sessionId,
|
|
3294
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3295
|
+
progress: { phase: "queued", completed: 0, total: 0 }
|
|
3296
|
+
};
|
|
3297
|
+
this.store.saveJob(job);
|
|
3298
|
+
const updateJob = (status, progress) => {
|
|
3299
|
+
this.store.updateJobStatus(jobId, status, progress);
|
|
3300
|
+
if (progress) options.onProgress?.(progress.phase, progress.completed, progress.total);
|
|
3301
|
+
};
|
|
3302
|
+
const throwIfAborted = () => {
|
|
3303
|
+
if (options.signal?.aborted) throw new DOMException("TechStack job cancelled", "AbortError");
|
|
3304
|
+
};
|
|
3305
|
+
try {
|
|
3306
|
+
throwIfAborted();
|
|
3307
|
+
updateJob("discovering", { phase: "discovering", completed: 0, total: 1 });
|
|
3308
|
+
const snapshot = await this.inventory(
|
|
3309
|
+
projectId,
|
|
3310
|
+
options.targetRoot,
|
|
3311
|
+
jobId,
|
|
3312
|
+
(phase, completed, total) => {
|
|
3313
|
+
throwIfAborted();
|
|
3314
|
+
updateJob(phase, { phase, completed, total });
|
|
3315
|
+
}
|
|
3316
|
+
);
|
|
3317
|
+
throwIfAborted();
|
|
3318
|
+
const isOnline = options.online !== false;
|
|
3319
|
+
if (isOnline) {
|
|
3320
|
+
updateJob("enriching", { phase: "enriching", completed: 0, total: 1 });
|
|
3321
|
+
const enriched = await this.enrich(snapshot, {
|
|
3322
|
+
online: true,
|
|
3323
|
+
signal: options.signal
|
|
3324
|
+
});
|
|
3325
|
+
throwIfAborted();
|
|
3326
|
+
const researched = await this.research(enriched, {
|
|
3327
|
+
researcher: options.researcher,
|
|
3328
|
+
researchLimit: options.researchLimit,
|
|
3329
|
+
signal: options.signal,
|
|
3330
|
+
onProgress: (phase, completed, total) => {
|
|
3331
|
+
updateJob(phase, { phase, completed, total });
|
|
3332
|
+
}
|
|
3333
|
+
});
|
|
3334
|
+
throwIfAborted();
|
|
3335
|
+
this.store.saveSnapshot(researched);
|
|
3336
|
+
updateJob("completed", { phase: "completed", completed: 1, total: 1 });
|
|
3337
|
+
return {
|
|
3338
|
+
snapshot: researched,
|
|
3339
|
+
job: { ...job, status: "completed", completedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
3340
|
+
};
|
|
3341
|
+
}
|
|
3342
|
+
this.store.saveSnapshot(snapshot);
|
|
3343
|
+
updateJob("completed", { phase: "completed", completed: 1, total: 1 });
|
|
3344
|
+
return {
|
|
3345
|
+
snapshot,
|
|
3346
|
+
job: { ...job, status: "completed", completedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
3347
|
+
};
|
|
3348
|
+
} catch (err) {
|
|
3349
|
+
if (options.signal?.aborted || err instanceof DOMException && err.name === "AbortError") {
|
|
3350
|
+
updateJob("cancelled");
|
|
3351
|
+
} else {
|
|
3352
|
+
updateJob("failed");
|
|
3353
|
+
}
|
|
3354
|
+
throw err;
|
|
3355
|
+
}
|
|
3356
|
+
}
|
|
3357
|
+
// ── Report generation ─────────────────────────────────────────────────
|
|
3358
|
+
/**
|
|
3359
|
+
* Generate a human-readable report from a snapshot.
|
|
3360
|
+
*
|
|
3361
|
+
* @param format 'md' for Markdown, 'json' for raw JSON.
|
|
3362
|
+
* @returns The report as a string.
|
|
3363
|
+
*/
|
|
3364
|
+
generateReport(snapshot, format = "md") {
|
|
3365
|
+
if (format === "json") return JSON.stringify(snapshot, null, 2);
|
|
3366
|
+
const lines = [
|
|
3367
|
+
"# TechStack Report",
|
|
3368
|
+
"",
|
|
3369
|
+
`**Generated:** ${snapshot.createdAt}`,
|
|
3370
|
+
`**Target:** ${snapshot.targetRoot}`,
|
|
3371
|
+
`**Fingerprint:** ${snapshot.fingerprint}`,
|
|
3372
|
+
`**Workspaces:** ${snapshot.workspaces.length}`,
|
|
3373
|
+
`**Dependencies:** ${snapshot.dependencies.length}`,
|
|
3374
|
+
`**Findings:** ${snapshot.findings.length}`,
|
|
3375
|
+
`**Coverage:** ${snapshot.coverage}`,
|
|
3376
|
+
""
|
|
3377
|
+
];
|
|
3378
|
+
if (snapshot.workspaces.length > 0) {
|
|
3379
|
+
lines.push("## Workspaces", "");
|
|
3380
|
+
lines.push("| Workspace | Ecosystem | Coverage | Deps |");
|
|
3381
|
+
lines.push("|---|---|---|---|");
|
|
3382
|
+
for (const ws of snapshot.workspaces) {
|
|
3383
|
+
const depCount = snapshot.dependencies.filter((d) => d.workspaceId === ws.id).length;
|
|
3384
|
+
lines.push(`| ${ws.relativeRoot} | ${ws.ecosystem} | ${ws.coverage} | ${depCount} |`);
|
|
3385
|
+
}
|
|
3386
|
+
lines.push("");
|
|
3387
|
+
}
|
|
3388
|
+
const findings = snapshot.findings;
|
|
3389
|
+
if (findings.length > 0) {
|
|
3390
|
+
lines.push("## Findings", "");
|
|
3391
|
+
const bySeverity = /* @__PURE__ */ new Map();
|
|
3392
|
+
for (const f of findings) {
|
|
3393
|
+
const list = bySeverity.get(f.severity) ?? [];
|
|
3394
|
+
list.push(f);
|
|
3395
|
+
bySeverity.set(f.severity, list);
|
|
3396
|
+
}
|
|
3397
|
+
for (const sev of ["critical", "high", "medium", "low", "info"]) {
|
|
3398
|
+
const items = bySeverity.get(sev);
|
|
3399
|
+
if (!items || items.length === 0) continue;
|
|
3400
|
+
lines.push(`### ${sev.charAt(0).toUpperCase() + sev.slice(1)} (${items.length})`, "");
|
|
3401
|
+
for (const f of items) {
|
|
3402
|
+
const dep = snapshot.dependencies.find((d) => d.id === f.dependencyId);
|
|
3403
|
+
lines.push(`- **${dep?.name ?? f.dependencyId}** \u2014 ${f.type} \u2014 ${f.rationale}`);
|
|
3404
|
+
}
|
|
3405
|
+
lines.push("");
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
if (snapshot.dependencies.length > 0) {
|
|
3409
|
+
lines.push("## Dependencies", "");
|
|
3410
|
+
lines.push("| Name | Ecosystem | Status | Locked | Latest |");
|
|
3411
|
+
lines.push("|---|---|---|---|---|");
|
|
3412
|
+
for (const dep of snapshot.dependencies) {
|
|
3413
|
+
lines.push(
|
|
3414
|
+
`| ${dep.name} | ${dep.ecosystem} | ${dep.status} | ${dep.locked ?? "\u2014"} | ${dep.latestStable ?? "\u2014"} |`
|
|
3415
|
+
);
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
return lines.join("\n");
|
|
3419
|
+
}
|
|
3420
|
+
};
|
|
3421
|
+
function computeFingerprint(dependencies) {
|
|
3422
|
+
const parts = dependencies.map((d) => `${d.name}@${d.locked ?? d.requested ?? "unknown"}`).sort().join(",");
|
|
3423
|
+
let hash = 0;
|
|
3424
|
+
for (let i = 0; i < parts.length; i++) {
|
|
3425
|
+
const char = parts.charCodeAt(i);
|
|
3426
|
+
hash = (hash << 5) - hash + char;
|
|
3427
|
+
hash |= 0;
|
|
3428
|
+
}
|
|
3429
|
+
return `ts-${Math.abs(hash).toString(36)}`;
|
|
3430
|
+
}
|
|
3431
|
+
function createFindingForStatus(dependencyId, status, _license) {
|
|
3432
|
+
switch (status) {
|
|
3433
|
+
case "vulnerable":
|
|
3434
|
+
return {
|
|
3435
|
+
id: `finding-${dependencyId}-vuln`,
|
|
3436
|
+
dependencyId,
|
|
3437
|
+
type: "vulnerability",
|
|
3438
|
+
severity: "high",
|
|
3439
|
+
action: "upgrade_patch",
|
|
3440
|
+
confidence: 1,
|
|
3441
|
+
rationale: "Known security advisory found for this package",
|
|
3442
|
+
evidence: []
|
|
3443
|
+
};
|
|
3444
|
+
case "deprecated":
|
|
3445
|
+
return {
|
|
3446
|
+
id: `finding-${dependencyId}-dep`,
|
|
3447
|
+
dependencyId,
|
|
3448
|
+
type: "deprecated",
|
|
3449
|
+
severity: "medium",
|
|
3450
|
+
action: "replace",
|
|
3451
|
+
confidence: 1,
|
|
3452
|
+
rationale: "Package is deprecated in the registry",
|
|
3453
|
+
evidence: []
|
|
3454
|
+
};
|
|
3455
|
+
case "yanked":
|
|
3456
|
+
return {
|
|
3457
|
+
id: `finding-${dependencyId}-yank`,
|
|
3458
|
+
dependencyId,
|
|
3459
|
+
type: "deprecated",
|
|
3460
|
+
severity: "high",
|
|
3461
|
+
action: "replace",
|
|
3462
|
+
confidence: 1,
|
|
3463
|
+
rationale: "Package version has been yanked from the registry",
|
|
3464
|
+
evidence: []
|
|
3465
|
+
};
|
|
3466
|
+
case "update_available_safe":
|
|
3467
|
+
return {
|
|
3468
|
+
id: `finding-${dependencyId}-update`,
|
|
3469
|
+
dependencyId,
|
|
3470
|
+
type: "upgrade",
|
|
3471
|
+
severity: "info",
|
|
3472
|
+
action: "upgrade_minor",
|
|
3473
|
+
confidence: 1,
|
|
3474
|
+
rationale: "A newer compatible version is available",
|
|
3475
|
+
evidence: []
|
|
3476
|
+
};
|
|
3477
|
+
case "update_available_breaking":
|
|
3478
|
+
return {
|
|
3479
|
+
id: `finding-${dependencyId}-major`,
|
|
3480
|
+
dependencyId,
|
|
3481
|
+
type: "upgrade",
|
|
3482
|
+
severity: "low",
|
|
3483
|
+
action: "upgrade_major",
|
|
3484
|
+
confidence: 1,
|
|
3485
|
+
rationale: "A newer version is available that may require breaking changes",
|
|
3486
|
+
evidence: []
|
|
3487
|
+
};
|
|
3488
|
+
default:
|
|
3489
|
+
return {
|
|
3490
|
+
id: `finding-${dependencyId}-investigate`,
|
|
3491
|
+
dependencyId,
|
|
3492
|
+
type: "investigate",
|
|
3493
|
+
severity: "info",
|
|
3494
|
+
action: "investigate",
|
|
3495
|
+
confidence: 0.5,
|
|
3496
|
+
rationale: `Package status is "${status}" \u2014 may need investigation`,
|
|
3497
|
+
evidence: []
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
// src/research/llm.ts
|
|
3503
|
+
var DEFAULT_TIMEOUT_MS = 45e3;
|
|
3504
|
+
function createProviderLlm(accessor, options = {}) {
|
|
3505
|
+
if (!accessor()) return void 0;
|
|
3506
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
3507
|
+
return async (req) => {
|
|
3508
|
+
const llm = accessor();
|
|
3509
|
+
if (!llm) throw new Error("TechStack research: no provider available");
|
|
3510
|
+
const request = {
|
|
3511
|
+
model: llm.model,
|
|
3512
|
+
system: [{ type: "text", text: req.system }],
|
|
3513
|
+
messages: [{ role: "user", content: req.prompt }],
|
|
3514
|
+
maxTokens: req.maxTokens
|
|
3515
|
+
};
|
|
3516
|
+
if (llm.provider.capabilities.structuredOutput) {
|
|
3517
|
+
request.responseFormat = {
|
|
3518
|
+
type: "json_schema",
|
|
3519
|
+
jsonSchema: { name: req.schemaName, strict: false, schema: req.schema }
|
|
3520
|
+
};
|
|
3521
|
+
} else if (llm.provider.capabilities.jsonMode) {
|
|
3522
|
+
request.responseFormat = { type: "json_object" };
|
|
3523
|
+
}
|
|
3524
|
+
const timer = new AbortController();
|
|
3525
|
+
const onAbort = () => {
|
|
3526
|
+
timer.abort(new Error("TechStack research: cancelled"));
|
|
3527
|
+
};
|
|
3528
|
+
req.signal?.addEventListener("abort", onAbort, { once: true });
|
|
3529
|
+
const to = setTimeout(() => {
|
|
3530
|
+
timer.abort(new Error("TechStack research: LLM timeout"));
|
|
3531
|
+
}, timeoutMs);
|
|
3532
|
+
to.unref?.();
|
|
3533
|
+
try {
|
|
3534
|
+
const res = await llm.provider.complete(request, { signal: timer.signal });
|
|
3535
|
+
return res.content.filter((block) => block.type === "text").map((block) => block.text).join("\n").trim();
|
|
3536
|
+
} finally {
|
|
3537
|
+
req.signal?.removeEventListener("abort", onAbort);
|
|
3538
|
+
clearTimeout(to);
|
|
3539
|
+
timer.abort();
|
|
3540
|
+
}
|
|
3541
|
+
};
|
|
3542
|
+
}
|
|
3543
|
+
function stripOuterFence(text) {
|
|
3544
|
+
const trimmed = text.trim();
|
|
3545
|
+
const match = trimmed.match(/^```(?:[a-z0-9_-]+)?\s*\r?\n([\s\S]*?)\r?\n```$/i);
|
|
3546
|
+
return (match?.[1] ?? trimmed).trim();
|
|
3547
|
+
}
|
|
3548
|
+
function extractJsonObject(text) {
|
|
3549
|
+
const trimmed = stripOuterFence(text);
|
|
3550
|
+
if (trimmed.startsWith("{")) return trimmed;
|
|
3551
|
+
const start = trimmed.indexOf("{");
|
|
3552
|
+
const end = trimmed.lastIndexOf("}");
|
|
3553
|
+
if (start !== -1 && end > start) return trimmed.slice(start, end + 1);
|
|
3554
|
+
return trimmed;
|
|
3555
|
+
}
|
|
3556
|
+
function parseResearchJson(text) {
|
|
3557
|
+
if (!text.trim()) return null;
|
|
3558
|
+
try {
|
|
3559
|
+
const parsed = JSON.parse(extractJsonObject(text));
|
|
3560
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
3561
|
+
} catch {
|
|
3562
|
+
return null;
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
// src/research/researcher.ts
|
|
3567
|
+
var SEARCH_CONCURRENCY = 4;
|
|
3568
|
+
var MAX_SNIPPET_CHARS = 320;
|
|
3569
|
+
var MAX_TOKENS_PER_CLUSTER = 2e3;
|
|
3570
|
+
var MAX_LLM_CONFIDENCE = 0.95;
|
|
3571
|
+
var MIN_LLM_CONFIDENCE = 0.1;
|
|
3572
|
+
var DEFAULT_LLM_CONFIDENCE = 0.5;
|
|
3573
|
+
var FINDING_TYPE_BY_CLUSTER = {
|
|
3574
|
+
breaking_change: "upgrade",
|
|
3575
|
+
replacement: "replacement",
|
|
3576
|
+
vulnerability: "vulnerability"
|
|
3577
|
+
};
|
|
3578
|
+
var VALID_SEVERITIES = /* @__PURE__ */ new Set([
|
|
3579
|
+
"info",
|
|
3580
|
+
"low",
|
|
3581
|
+
"medium",
|
|
3582
|
+
"high",
|
|
3583
|
+
"critical"
|
|
3584
|
+
]);
|
|
3585
|
+
var VALID_ACTIONS = /* @__PURE__ */ new Set([
|
|
3586
|
+
"none",
|
|
3587
|
+
"upgrade_patch",
|
|
3588
|
+
"upgrade_minor",
|
|
3589
|
+
"upgrade_major",
|
|
3590
|
+
"replace",
|
|
3591
|
+
"remove",
|
|
3592
|
+
"investigate"
|
|
3593
|
+
]);
|
|
3594
|
+
var SHARED_RULES = [
|
|
3595
|
+
"Return only JSON. No markdown, prose, or code fences.",
|
|
3596
|
+
"Only reason about the packages listed in the input. Never introduce a package that is not listed.",
|
|
3597
|
+
"Never state or guess version numbers other than the ones given to you; the version facts are already established.",
|
|
3598
|
+
"If the provided sources do not support a conclusion, say so in the rationale and lower the confidence.",
|
|
3599
|
+
"Be concrete and short. The reader is an engineer deciding what to do this afternoon."
|
|
3600
|
+
].join("\n");
|
|
3601
|
+
var SYSTEM_BY_CLUSTER = {
|
|
3602
|
+
breaking_change: [
|
|
3603
|
+
"You assess upgrade risk for software dependencies.",
|
|
3604
|
+
"For each package, judge how disruptive moving to the latest version would be for a typical consumer,",
|
|
3605
|
+
"and what the migration actually involves.",
|
|
3606
|
+
SHARED_RULES
|
|
3607
|
+
].join("\n"),
|
|
3608
|
+
replacement: [
|
|
3609
|
+
"You advise on deprecated, yanked, and unmaintained software dependencies.",
|
|
3610
|
+
"For each package, say whether it should be replaced, what the community has moved to, and how urgent it is.",
|
|
3611
|
+
"Prefer platform-native or well-maintained successors. If the package is fine to keep, say so plainly.",
|
|
3612
|
+
SHARED_RULES
|
|
3613
|
+
].join("\n"),
|
|
3614
|
+
vulnerability: [
|
|
3615
|
+
"You triage security advisories for software dependencies.",
|
|
3616
|
+
"For each package, judge how exploitable the known advisory is in practice and what the fix is.",
|
|
3617
|
+
"Distinguish advisories that require unusual usage from ones that affect every consumer.",
|
|
3618
|
+
SHARED_RULES
|
|
3619
|
+
].join("\n")
|
|
3620
|
+
};
|
|
3621
|
+
var QUESTION_BY_CLUSTER = {
|
|
3622
|
+
breaking_change: "How breaking is this upgrade, and what does the migration involve?",
|
|
3623
|
+
replacement: "Should this be replaced, and with what?",
|
|
3624
|
+
vulnerability: "Does this advisory realistically affect a consumer, and what is the fix?"
|
|
3625
|
+
};
|
|
3626
|
+
var RESEARCH_JSON_SCHEMA = {
|
|
3627
|
+
type: "object",
|
|
3628
|
+
additionalProperties: false,
|
|
3629
|
+
properties: {
|
|
3630
|
+
findings: {
|
|
3631
|
+
type: "array",
|
|
3632
|
+
items: {
|
|
3633
|
+
type: "object",
|
|
3634
|
+
additionalProperties: false,
|
|
3635
|
+
properties: {
|
|
3636
|
+
package: { type: "string", description: "Exact package name from the input list." },
|
|
3637
|
+
severity: { type: "string", enum: ["info", "low", "medium", "high", "critical"] },
|
|
3638
|
+
action: {
|
|
3639
|
+
type: "string",
|
|
3640
|
+
enum: [
|
|
3641
|
+
"none",
|
|
3642
|
+
"upgrade_patch",
|
|
3643
|
+
"upgrade_minor",
|
|
3644
|
+
"upgrade_major",
|
|
3645
|
+
"replace",
|
|
3646
|
+
"remove",
|
|
3647
|
+
"investigate"
|
|
3648
|
+
]
|
|
3649
|
+
},
|
|
3650
|
+
confidence: { type: "number", minimum: 0, maximum: 1 },
|
|
3651
|
+
rationale: { type: "string" },
|
|
3652
|
+
breakingRisk: { type: "string" },
|
|
3653
|
+
sources: { type: "array", items: { type: "string" } }
|
|
3654
|
+
},
|
|
3655
|
+
required: ["package", "severity", "action", "confidence", "rationale"]
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
},
|
|
3659
|
+
required: ["findings"]
|
|
3660
|
+
};
|
|
3661
|
+
function searchQuery(candidate) {
|
|
3662
|
+
const dep = candidate.dependency;
|
|
3663
|
+
const from = dep.locked ?? dep.requested ?? "";
|
|
3664
|
+
const to = dep.latestStable ?? "";
|
|
3665
|
+
switch (candidate.cluster) {
|
|
3666
|
+
case "breaking_change":
|
|
3667
|
+
return `${dep.name} ${from} to ${to} migration guide breaking changes`;
|
|
3668
|
+
case "replacement":
|
|
3669
|
+
return `${dep.name} ${dep.ecosystem} deprecated recommended alternative replacement`;
|
|
3670
|
+
case "vulnerability":
|
|
3671
|
+
return `${dep.name} ${from} security advisory CVE affected versions`;
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
async function mapLimit(items, limit, task) {
|
|
3675
|
+
const out = new Array(items.length);
|
|
3676
|
+
let cursor = 0;
|
|
3677
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
3678
|
+
for (; ; ) {
|
|
3679
|
+
const index = cursor++;
|
|
3680
|
+
if (index >= items.length) return;
|
|
3681
|
+
out[index] = await task(items[index], index);
|
|
3682
|
+
}
|
|
3683
|
+
});
|
|
3684
|
+
await Promise.all(workers);
|
|
3685
|
+
return out;
|
|
3686
|
+
}
|
|
3687
|
+
function describeDependency(dep) {
|
|
3688
|
+
const bits = [
|
|
3689
|
+
`ecosystem: ${dep.ecosystem}`,
|
|
3690
|
+
`installed: ${dep.locked ?? dep.installed ?? dep.requested ?? "unknown"}`
|
|
3691
|
+
];
|
|
3692
|
+
if (dep.latestStable) bits.push(`latest stable: ${dep.latestStable}`);
|
|
3693
|
+
if (dep.requested) bits.push(`constraint: ${dep.requested}`);
|
|
3694
|
+
bits.push(dep.direct ? "direct dependency" : "transitive dependency");
|
|
3695
|
+
bits.push(`scope: ${dep.scope}`);
|
|
3696
|
+
if (dep.license) bits.push(`license: ${dep.license}`);
|
|
3697
|
+
if (dep.deprecated) bits.push("registry flag: deprecated");
|
|
3698
|
+
if (dep.yanked) bits.push("registry flag: yanked");
|
|
3699
|
+
bits.push(`status: ${dep.status}`);
|
|
3700
|
+
return bits.join(", ");
|
|
3701
|
+
}
|
|
3702
|
+
function renderSources(results) {
|
|
3703
|
+
if (results.length === 0) return " (no sources found \u2014 say so and lower confidence)";
|
|
3704
|
+
return results.map((r) => ` - ${r.title}
|
|
3705
|
+
${r.url}
|
|
3706
|
+
${truncate(r.snippet, MAX_SNIPPET_CHARS)}`).join("\n");
|
|
3707
|
+
}
|
|
3708
|
+
function truncate(value, max) {
|
|
3709
|
+
const clean = value.replace(/\s+/g, " ").trim();
|
|
3710
|
+
return clean.length <= max ? clean : `${clean.slice(0, max)}\u2026`;
|
|
3711
|
+
}
|
|
3712
|
+
function buildPrompt(cluster, entries) {
|
|
3713
|
+
const blocks = entries.map(
|
|
3714
|
+
({ candidate, sources }, i) => [
|
|
3715
|
+
`${i + 1}. ${candidate.dependency.name}`,
|
|
3716
|
+
` ${describeDependency(candidate.dependency)}`,
|
|
3717
|
+
" Web search results:",
|
|
3718
|
+
renderSources(sources)
|
|
3719
|
+
].join("\n")
|
|
3720
|
+
);
|
|
3721
|
+
return [
|
|
3722
|
+
`Question for every package below: ${QUESTION_BY_CLUSTER[cluster]}`,
|
|
3723
|
+
"",
|
|
3724
|
+
"Packages:",
|
|
3725
|
+
...blocks,
|
|
3726
|
+
"",
|
|
3727
|
+
"Return JSON shaped exactly as:",
|
|
3728
|
+
'{"findings":[{"package":"exact-name-from-the-list","severity":"medium","action":"upgrade_major","confidence":0.7,"rationale":"one or two sentences","breakingRisk":"optional short note","sources":["https://\u2026"]}]}',
|
|
3729
|
+
"",
|
|
3730
|
+
"Emit one entry per package you can say something useful about. Omit packages you cannot."
|
|
3731
|
+
].join("\n");
|
|
3732
|
+
}
|
|
3733
|
+
function optionalString(value) {
|
|
3734
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
3735
|
+
}
|
|
3736
|
+
function clampConfidence(value) {
|
|
3737
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_LLM_CONFIDENCE;
|
|
3738
|
+
return Math.min(MAX_LLM_CONFIDENCE, Math.max(MIN_LLM_CONFIDENCE, value));
|
|
3739
|
+
}
|
|
3740
|
+
function sourceEvidence(raw, sources, retrievedAt) {
|
|
3741
|
+
const known = new Set(sources.map((s) => s.url));
|
|
3742
|
+
const cited = Array.isArray(raw) ? raw.filter((u) => typeof u === "string" && known.has(u)) : [];
|
|
3743
|
+
const urls = cited.length > 0 ? cited : sources.map((s) => s.url);
|
|
3744
|
+
return urls.map((url) => ({
|
|
3745
|
+
kind: "agent",
|
|
3746
|
+
source: url,
|
|
3747
|
+
retrievedAt,
|
|
3748
|
+
detail: sources.find((s) => s.url === url)?.title
|
|
3749
|
+
}));
|
|
3750
|
+
}
|
|
3751
|
+
function toFinding(raw, cluster, byName, retrievedAt) {
|
|
3752
|
+
const name = optionalString(raw.package);
|
|
3753
|
+
if (!name) return null;
|
|
3754
|
+
const entry = byName.get(name);
|
|
3755
|
+
if (!entry) return null;
|
|
3756
|
+
const rationale = optionalString(raw.rationale);
|
|
3757
|
+
if (!rationale) return null;
|
|
3758
|
+
const severity = VALID_SEVERITIES.has(raw.severity) ? raw.severity : "info";
|
|
3759
|
+
const action = VALID_ACTIONS.has(raw.action) ? raw.action : "investigate";
|
|
3760
|
+
const breakingRisk = optionalString(raw.breakingRisk);
|
|
3761
|
+
return {
|
|
3762
|
+
id: `research-${entry.candidate.dependency.id}-${cluster}`,
|
|
3763
|
+
dependencyId: entry.candidate.dependency.id,
|
|
3764
|
+
type: FINDING_TYPE_BY_CLUSTER[cluster],
|
|
3765
|
+
severity,
|
|
3766
|
+
action,
|
|
3767
|
+
confidence: clampConfidence(raw.confidence),
|
|
3768
|
+
rationale,
|
|
3769
|
+
...breakingRisk ? { breakingRisk } : {},
|
|
3770
|
+
evidence: sourceEvidence(raw.sources, entry.sources, retrievedAt)
|
|
3771
|
+
};
|
|
3772
|
+
}
|
|
3773
|
+
function parseFindings(parsed, cluster, byName, retrievedAt) {
|
|
3774
|
+
if (!parsed || !Array.isArray(parsed.findings)) return [];
|
|
3775
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3776
|
+
const out = [];
|
|
3777
|
+
for (const raw of parsed.findings) {
|
|
3778
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
|
|
3779
|
+
const finding = toFinding(raw, cluster, byName, retrievedAt);
|
|
3780
|
+
if (!finding || seen.has(finding.id)) continue;
|
|
3781
|
+
seen.add(finding.id);
|
|
3782
|
+
out.push(finding);
|
|
3783
|
+
}
|
|
3784
|
+
return out;
|
|
3785
|
+
}
|
|
3786
|
+
function createResearcher(options) {
|
|
3787
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
3788
|
+
return {
|
|
3789
|
+
async research(candidates, opts = {}) {
|
|
3790
|
+
if (candidates.length === 0) return [];
|
|
3791
|
+
const clusters = [...clusterCandidates(candidates).entries()];
|
|
3792
|
+
const findings = [];
|
|
3793
|
+
let completed = 0;
|
|
3794
|
+
opts.onProgress?.(0, clusters.length);
|
|
3795
|
+
for (const [cluster, members] of clusters) {
|
|
3796
|
+
if (opts.signal?.aborted) break;
|
|
3797
|
+
try {
|
|
3798
|
+
const sources = await mapLimit(
|
|
3799
|
+
members,
|
|
3800
|
+
SEARCH_CONCURRENCY,
|
|
3801
|
+
(candidate) => options.search(searchQuery(candidate), { signal: opts.signal })
|
|
3802
|
+
);
|
|
3803
|
+
if (opts.signal?.aborted) break;
|
|
3804
|
+
const entries = members.map((candidate, i) => ({
|
|
3805
|
+
candidate,
|
|
3806
|
+
sources: sources[i] ?? []
|
|
3807
|
+
}));
|
|
3808
|
+
const byName = new Map(entries.map((entry) => [entry.candidate.dependency.name, entry]));
|
|
3809
|
+
const text = await options.llm({
|
|
3810
|
+
system: SYSTEM_BY_CLUSTER[cluster],
|
|
3811
|
+
prompt: buildPrompt(cluster, entries),
|
|
3812
|
+
schema: RESEARCH_JSON_SCHEMA,
|
|
3813
|
+
schemaName: `techstack_${cluster}_findings`,
|
|
3814
|
+
maxTokens: MAX_TOKENS_PER_CLUSTER,
|
|
3815
|
+
signal: opts.signal
|
|
3816
|
+
});
|
|
3817
|
+
findings.push(
|
|
3818
|
+
...parseFindings(parseResearchJson(text), cluster, byName, now().toISOString())
|
|
3819
|
+
);
|
|
3820
|
+
} catch {
|
|
3821
|
+
}
|
|
3822
|
+
completed++;
|
|
3823
|
+
opts.onProgress?.(completed, clusters.length);
|
|
3824
|
+
}
|
|
3825
|
+
return findings;
|
|
3826
|
+
}
|
|
3827
|
+
};
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
// src/research/search.ts
|
|
3831
|
+
import { searchTool } from "@wrongstack/tools";
|
|
3832
|
+
var DEFAULT_NUM_RESULTS = 5;
|
|
3833
|
+
var NO_CONTEXT = void 0;
|
|
3834
|
+
function createToolSearch(options = {}) {
|
|
3835
|
+
const numResults = options.numResults ?? DEFAULT_NUM_RESULTS;
|
|
3836
|
+
return async (query, opts) => {
|
|
3837
|
+
if (opts.signal?.aborted) return [];
|
|
3838
|
+
try {
|
|
3839
|
+
const out = await searchTool.execute(
|
|
3840
|
+
{
|
|
3841
|
+
query,
|
|
3842
|
+
num_results: numResults,
|
|
3843
|
+
...options.source ? { source: options.source } : {}
|
|
3844
|
+
},
|
|
3845
|
+
NO_CONTEXT,
|
|
3846
|
+
{ signal: opts.signal ?? new AbortController().signal }
|
|
3847
|
+
);
|
|
3848
|
+
return out.results.map((result) => ({
|
|
3849
|
+
title: result.title,
|
|
3850
|
+
url: result.url,
|
|
3851
|
+
snippet: result.snippet
|
|
3852
|
+
}));
|
|
3853
|
+
} catch {
|
|
3854
|
+
return [];
|
|
3855
|
+
}
|
|
3856
|
+
};
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
// src/store/sqlite.ts
|
|
3860
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3861
|
+
import { mkdirSync, existsSync as existsSync3 } from "node:fs";
|
|
3862
|
+
import { join as join7 } from "node:path";
|
|
3863
|
+
import { homedir } from "node:os";
|
|
3864
|
+
|
|
3865
|
+
// src/store/schema.ts
|
|
3866
|
+
var SCHEMA_VERSION = 1;
|
|
3867
|
+
var DDL = `
|
|
3868
|
+
CREATE TABLE IF NOT EXISTS techstack_schema_version (
|
|
3869
|
+
version INTEGER NOT NULL
|
|
3870
|
+
);
|
|
3871
|
+
|
|
3872
|
+
CREATE TABLE IF NOT EXISTS snapshots (
|
|
3873
|
+
id TEXT PRIMARY KEY,
|
|
3874
|
+
project_id TEXT NOT NULL,
|
|
3875
|
+
target_root TEXT NOT NULL,
|
|
3876
|
+
fingerprint TEXT NOT NULL,
|
|
3877
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
3878
|
+
raw_json TEXT NOT NULL,
|
|
3879
|
+
adapter_version TEXT NOT NULL DEFAULT ''
|
|
3880
|
+
);
|
|
3881
|
+
|
|
3882
|
+
CREATE INDEX IF NOT EXISTS idx_snapshots_project_id ON snapshots(project_id);
|
|
3883
|
+
CREATE INDEX IF NOT EXISTS idx_snapshots_created_at ON snapshots(created_at DESC);
|
|
3884
|
+
|
|
3885
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
3886
|
+
id TEXT PRIMARY KEY,
|
|
3887
|
+
project_id TEXT NOT NULL,
|
|
3888
|
+
target_root TEXT NOT NULL,
|
|
3889
|
+
kind TEXT NOT NULL CHECK(kind IN ('inventory', 'analyze')),
|
|
3890
|
+
status TEXT NOT NULL DEFAULT 'queued'
|
|
3891
|
+
CHECK(status IN ('queued','discovering','inventorying','enriching','researching','synthesizing','completed','failed','cancelled')),
|
|
3892
|
+
fingerprint TEXT NOT NULL DEFAULT '',
|
|
3893
|
+
requested_by TEXT NOT NULL DEFAULT '',
|
|
3894
|
+
session_id TEXT,
|
|
3895
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
3896
|
+
completed_at TEXT,
|
|
3897
|
+
error TEXT,
|
|
3898
|
+
progress_json TEXT
|
|
3899
|
+
);
|
|
3900
|
+
|
|
3901
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_project_id ON jobs(project_id);
|
|
3902
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
|
|
3903
|
+
|
|
3904
|
+
CREATE TABLE IF NOT EXISTS outbox (
|
|
3905
|
+
delivery_id TEXT PRIMARY KEY,
|
|
3906
|
+
report_id TEXT NOT NULL,
|
|
3907
|
+
session_id TEXT NOT NULL,
|
|
3908
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
3909
|
+
CHECK(status IN ('pending', 'claimed', 'delivered', 'failed')),
|
|
3910
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
3911
|
+
claimed_at TEXT,
|
|
3912
|
+
delivered_at TEXT
|
|
3913
|
+
);
|
|
3914
|
+
|
|
3915
|
+
CREATE INDEX IF NOT EXISTS idx_outbox_status ON outbox(status);
|
|
3916
|
+
`;
|
|
3917
|
+
function applySchema(db) {
|
|
3918
|
+
for (const statement of DDL.split(";")) {
|
|
3919
|
+
const trimmed = statement.trim();
|
|
3920
|
+
if (trimmed) {
|
|
3921
|
+
db.exec(trimmed);
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
const row = db.prepare("SELECT version FROM techstack_schema_version").get();
|
|
3925
|
+
if (!row) {
|
|
3926
|
+
db.prepare("INSERT INTO techstack_schema_version (version) VALUES (?)").run(SCHEMA_VERSION);
|
|
3927
|
+
} else if (row.version < SCHEMA_VERSION) {
|
|
3928
|
+
db.prepare("UPDATE techstack_schema_version SET version = ?").run(SCHEMA_VERSION);
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
// src/store/sqlite.ts
|
|
3933
|
+
var TechStackStore = class {
|
|
3934
|
+
db;
|
|
3935
|
+
dbPath;
|
|
3936
|
+
constructor(options) {
|
|
3937
|
+
this.dbPath = options.dbPath ?? join7(
|
|
3938
|
+
homedir(),
|
|
3939
|
+
".wrongstack",
|
|
3940
|
+
"projects",
|
|
3941
|
+
options.projectSlug,
|
|
3942
|
+
"techstack",
|
|
3943
|
+
"techstack.db"
|
|
3944
|
+
);
|
|
3945
|
+
const dir = this.dbPath.slice(0, this.dbPath.lastIndexOf("\\"));
|
|
3946
|
+
if (!existsSync3(dir)) {
|
|
3947
|
+
mkdirSync(dir, { recursive: true });
|
|
3948
|
+
}
|
|
3949
|
+
this.db = new DatabaseSync(this.dbPath);
|
|
3950
|
+
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
3951
|
+
this.db.exec("PRAGMA foreign_keys = ON;");
|
|
3952
|
+
applySchema(this.db);
|
|
3953
|
+
}
|
|
3954
|
+
// ── Lifecycle ───────────────────────────────────────────────────────────
|
|
3955
|
+
/** Close the database connection. Idempotent. */
|
|
3956
|
+
close() {
|
|
3957
|
+
try {
|
|
3958
|
+
this.db.close();
|
|
3959
|
+
} catch {
|
|
3960
|
+
}
|
|
3961
|
+
}
|
|
3962
|
+
/** Get the database path (useful for tests). */
|
|
3963
|
+
get path() {
|
|
3964
|
+
return this.dbPath;
|
|
3965
|
+
}
|
|
3966
|
+
// ── Snapshots ───────────────────────────────────────────────────────────
|
|
3967
|
+
/** Persist a snapshot. */
|
|
3968
|
+
saveSnapshot(snapshot) {
|
|
3969
|
+
const stmt = this.db.prepare(`
|
|
3970
|
+
INSERT OR REPLACE INTO snapshots (id, project_id, target_root, fingerprint, created_at, raw_json, adapter_version)
|
|
3971
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
3972
|
+
`);
|
|
3973
|
+
stmt.run(
|
|
3974
|
+
snapshot.id,
|
|
3975
|
+
snapshot.projectId,
|
|
3976
|
+
snapshot.targetRoot,
|
|
3977
|
+
snapshot.fingerprint,
|
|
3978
|
+
snapshot.createdAt,
|
|
3979
|
+
JSON.stringify(snapshot),
|
|
3980
|
+
snapshot.adapterVersion
|
|
3981
|
+
);
|
|
3982
|
+
}
|
|
3983
|
+
/** Get a snapshot by project ID (latest). */
|
|
3984
|
+
getSnapshot(projectId) {
|
|
3985
|
+
const stmt = this.db.prepare(`
|
|
3986
|
+
SELECT raw_json FROM snapshots
|
|
3987
|
+
WHERE project_id = ?
|
|
3988
|
+
ORDER BY created_at DESC
|
|
3989
|
+
LIMIT 1
|
|
3990
|
+
`);
|
|
3991
|
+
const row = stmt.get(projectId);
|
|
3992
|
+
if (!row) return void 0;
|
|
3993
|
+
try {
|
|
3994
|
+
return JSON.parse(row.raw_json);
|
|
3995
|
+
} catch {
|
|
3996
|
+
return void 0;
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
/** Get a snapshot by ID. */
|
|
4000
|
+
getSnapshotById(id) {
|
|
4001
|
+
const stmt = this.db.prepare(`
|
|
4002
|
+
SELECT raw_json FROM snapshots WHERE id = ?
|
|
4003
|
+
`);
|
|
4004
|
+
const row = stmt.get(id);
|
|
4005
|
+
if (!row) return void 0;
|
|
4006
|
+
try {
|
|
4007
|
+
return JSON.parse(row.raw_json);
|
|
4008
|
+
} catch {
|
|
4009
|
+
return void 0;
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
/** List all snapshots for a project (newest first). */
|
|
4013
|
+
listSnapshots(projectId, limit = 20) {
|
|
4014
|
+
const stmt = this.db.prepare(`
|
|
4015
|
+
SELECT raw_json FROM snapshots
|
|
4016
|
+
WHERE project_id = ?
|
|
4017
|
+
ORDER BY created_at DESC
|
|
4018
|
+
LIMIT ?
|
|
4019
|
+
`);
|
|
4020
|
+
const rows = stmt.all(projectId, limit);
|
|
4021
|
+
return rows.map((r) => {
|
|
4022
|
+
try {
|
|
4023
|
+
return JSON.parse(r.raw_json);
|
|
4024
|
+
} catch {
|
|
4025
|
+
return void 0;
|
|
4026
|
+
}
|
|
4027
|
+
}).filter((s) => s !== void 0);
|
|
4028
|
+
}
|
|
4029
|
+
/** Delete snapshots older than a given timestamp. */
|
|
4030
|
+
deleteSnapshotsBefore(projectId, before) {
|
|
4031
|
+
const stmt = this.db.prepare(`
|
|
4032
|
+
DELETE FROM snapshots WHERE project_id = ? AND created_at < ?
|
|
4033
|
+
`);
|
|
4034
|
+
const result = stmt.run(projectId, before);
|
|
4035
|
+
return Number(result.changes);
|
|
4036
|
+
}
|
|
4037
|
+
// ── Jobs ────────────────────────────────────────────────────────────────
|
|
4038
|
+
/** Persist a job. */
|
|
4039
|
+
saveJob(job) {
|
|
4040
|
+
const stmt = this.db.prepare(`
|
|
4041
|
+
INSERT OR REPLACE INTO jobs
|
|
4042
|
+
(id, project_id, target_root, kind, status, fingerprint, requested_by, session_id, created_at, completed_at, error, progress_json)
|
|
4043
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4044
|
+
`);
|
|
4045
|
+
stmt.run(
|
|
4046
|
+
job.id,
|
|
4047
|
+
job.projectId,
|
|
4048
|
+
job.targetRoot,
|
|
4049
|
+
job.kind,
|
|
4050
|
+
job.status,
|
|
4051
|
+
job.fingerprint,
|
|
4052
|
+
job.requestedBy,
|
|
4053
|
+
job.sessionId ?? null,
|
|
4054
|
+
job.createdAt,
|
|
4055
|
+
job.completedAt ?? null,
|
|
4056
|
+
job.error ?? null,
|
|
4057
|
+
job.progress ? JSON.stringify(job.progress) : null
|
|
4058
|
+
);
|
|
4059
|
+
}
|
|
4060
|
+
/** Get a job by ID. */
|
|
4061
|
+
getJob(id) {
|
|
4062
|
+
const stmt = this.db.prepare(`
|
|
4063
|
+
SELECT * FROM jobs WHERE id = ?
|
|
4064
|
+
`);
|
|
4065
|
+
const row = stmt.get(id);
|
|
4066
|
+
if (!row) return void 0;
|
|
4067
|
+
return this.rowToJob(row);
|
|
4068
|
+
}
|
|
4069
|
+
/** Update job status and optional progress. */
|
|
4070
|
+
updateJobStatus(id, status, progress) {
|
|
4071
|
+
const progressJson = progress ? JSON.stringify(progress) : null;
|
|
4072
|
+
const completedAt = status === "completed" || status === "failed" || status === "cancelled" ? (/* @__PURE__ */ new Date()).toISOString() : null;
|
|
4073
|
+
const stmt = this.db.prepare(`
|
|
4074
|
+
UPDATE jobs
|
|
4075
|
+
SET status = ?, progress_json = ?, completed_at = COALESCE(?, completed_at)
|
|
4076
|
+
WHERE id = ?
|
|
4077
|
+
`);
|
|
4078
|
+
stmt.run(status, progressJson, completedAt, id);
|
|
4079
|
+
}
|
|
4080
|
+
/** List jobs for a project (newest first). */
|
|
4081
|
+
listJobs(projectId, limit = 50) {
|
|
4082
|
+
const stmt = this.db.prepare(`
|
|
4083
|
+
SELECT * FROM jobs WHERE project_id = ? ORDER BY created_at DESC LIMIT ?
|
|
4084
|
+
`);
|
|
4085
|
+
const rows = stmt.all(projectId, limit);
|
|
4086
|
+
return rows.map((r) => this.rowToJob(r));
|
|
4087
|
+
}
|
|
4088
|
+
// ── Outbox ──────────────────────────────────────────────────────────────
|
|
4089
|
+
/** Create an outbox entry. */
|
|
4090
|
+
createOutbox(deliveryId, reportId, sessionId) {
|
|
4091
|
+
const stmt = this.db.prepare(`
|
|
4092
|
+
INSERT OR IGNORE INTO outbox (delivery_id, report_id, session_id, status, attempts)
|
|
4093
|
+
VALUES (?, ?, ?, 'pending', 0)
|
|
4094
|
+
`);
|
|
4095
|
+
stmt.run(deliveryId, reportId, sessionId);
|
|
4096
|
+
}
|
|
4097
|
+
/** Claim an outbox entry (atomic CAS). */
|
|
4098
|
+
claimOutbox(deliveryId, sessionId) {
|
|
4099
|
+
const stmt = this.db.prepare(`
|
|
4100
|
+
UPDATE outbox
|
|
4101
|
+
SET status = 'claimed', claimed_at = datetime('now'), attempts = attempts + 1
|
|
4102
|
+
WHERE delivery_id = ? AND session_id = ? AND status = 'pending'
|
|
4103
|
+
`);
|
|
4104
|
+
const result = stmt.run(deliveryId, sessionId);
|
|
4105
|
+
return result.changes > 0;
|
|
4106
|
+
}
|
|
4107
|
+
/** Mark an outbox entry as delivered. */
|
|
4108
|
+
deliverOutbox(deliveryId) {
|
|
4109
|
+
const stmt = this.db.prepare(`
|
|
4110
|
+
UPDATE outbox SET status = 'delivered', delivered_at = datetime('now')
|
|
4111
|
+
WHERE delivery_id = ?
|
|
4112
|
+
`);
|
|
4113
|
+
stmt.run(deliveryId);
|
|
4114
|
+
}
|
|
4115
|
+
/** Mark an outbox entry as failed. */
|
|
4116
|
+
failOutbox(deliveryId) {
|
|
4117
|
+
const stmt = this.db.prepare(`
|
|
4118
|
+
UPDATE outbox SET status = 'failed' WHERE delivery_id = ?
|
|
4119
|
+
`);
|
|
4120
|
+
stmt.run(deliveryId);
|
|
4121
|
+
}
|
|
4122
|
+
/** List outbox entries by status. */
|
|
4123
|
+
listOutboxByStatus(status) {
|
|
4124
|
+
const stmt = this.db.prepare(`
|
|
4125
|
+
SELECT * FROM outbox WHERE status = ?
|
|
4126
|
+
`);
|
|
4127
|
+
const rows = stmt.all(status);
|
|
4128
|
+
return rows.map((r) => this.rowToOutbox(r));
|
|
4129
|
+
}
|
|
4130
|
+
// ── Row mapping helpers ─────────────────────────────────────────────────
|
|
4131
|
+
rowToJob(row) {
|
|
4132
|
+
let progress;
|
|
4133
|
+
if (row.progress_json && typeof row.progress_json === "string") {
|
|
4134
|
+
try {
|
|
4135
|
+
progress = JSON.parse(row.progress_json);
|
|
4136
|
+
} catch {
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
return {
|
|
4140
|
+
id: String(row.id),
|
|
4141
|
+
projectId: String(row.project_id),
|
|
4142
|
+
targetRoot: String(row.target_root),
|
|
4143
|
+
kind: row.kind,
|
|
4144
|
+
status: row.status,
|
|
4145
|
+
fingerprint: String(row.fingerprint ?? ""),
|
|
4146
|
+
requestedBy: String(row.requested_by ?? ""),
|
|
4147
|
+
sessionId: row.session_id ? String(row.session_id) : void 0,
|
|
4148
|
+
createdAt: String(row.created_at),
|
|
4149
|
+
completedAt: row.completed_at ? String(row.completed_at) : void 0,
|
|
4150
|
+
error: row.error ? String(row.error) : void 0,
|
|
4151
|
+
...progress ? { progress } : {}
|
|
4152
|
+
};
|
|
4153
|
+
}
|
|
4154
|
+
rowToOutbox(row) {
|
|
4155
|
+
return {
|
|
4156
|
+
deliveryId: String(row.delivery_id),
|
|
4157
|
+
reportId: String(row.report_id),
|
|
4158
|
+
sessionId: String(row.session_id),
|
|
4159
|
+
status: row.status,
|
|
4160
|
+
attempts: Number(row.attempts),
|
|
4161
|
+
claimedAt: row.claimed_at ? String(row.claimed_at) : void 0,
|
|
4162
|
+
deliveredAt: row.delivered_at ? String(row.delivered_at) : void 0
|
|
4163
|
+
};
|
|
4164
|
+
}
|
|
4165
|
+
};
|
|
4166
|
+
|
|
4167
|
+
// src/delivery/coordinator.ts
|
|
4168
|
+
async function attemptDelivery(deliveryId, opts) {
|
|
4169
|
+
const { store, isRunInProgress, deliverToSession, onDelivered } = opts;
|
|
4170
|
+
if (isRunInProgress()) {
|
|
4171
|
+
return { deliveryId, sessionId: "", delivered: false };
|
|
4172
|
+
}
|
|
4173
|
+
const pending = store.listOutboxByStatus("pending");
|
|
4174
|
+
const entry = pending.find((e) => e.deliveryId === deliveryId);
|
|
4175
|
+
if (!entry) {
|
|
4176
|
+
return { deliveryId, sessionId: "", delivered: false };
|
|
4177
|
+
}
|
|
4178
|
+
const claimed = store.claimOutbox(deliveryId, entry.sessionId);
|
|
4179
|
+
if (!claimed) {
|
|
4180
|
+
return { deliveryId, sessionId: entry.sessionId, delivered: false };
|
|
4181
|
+
}
|
|
4182
|
+
const snapshot = store.getSnapshotById(entry.reportId);
|
|
4183
|
+
const summary = snapshot ? buildSummary(snapshot) : `TechStack report ${entry.reportId} is ready.`;
|
|
4184
|
+
const success = await deliverToSession(entry.sessionId, entry.reportId, summary);
|
|
4185
|
+
if (success) {
|
|
4186
|
+
store.deliverOutbox(deliveryId);
|
|
4187
|
+
onDelivered?.(deliveryId, entry.sessionId);
|
|
4188
|
+
return { deliveryId, sessionId: entry.sessionId, delivered: true };
|
|
4189
|
+
}
|
|
4190
|
+
store.failOutbox(deliveryId);
|
|
4191
|
+
return { deliveryId, sessionId: entry.sessionId, delivered: false };
|
|
4192
|
+
}
|
|
4193
|
+
async function drainPendingDeliveries(sessionId, opts) {
|
|
4194
|
+
const { store, isRunInProgress } = opts;
|
|
4195
|
+
if (isRunInProgress()) return 0;
|
|
4196
|
+
const pending = store.listOutboxByStatus("pending");
|
|
4197
|
+
let delivered = 0;
|
|
4198
|
+
for (const entry of pending) {
|
|
4199
|
+
if (entry.sessionId !== sessionId) continue;
|
|
4200
|
+
const result = await attemptDelivery(entry.deliveryId, opts);
|
|
4201
|
+
if (result.delivered) delivered++;
|
|
4202
|
+
}
|
|
4203
|
+
return delivered;
|
|
4204
|
+
}
|
|
4205
|
+
function buildSummary(snapshot) {
|
|
4206
|
+
const lines = [
|
|
4207
|
+
`\u{1F4CA} **TechStack Report Ready**`,
|
|
4208
|
+
"",
|
|
4209
|
+
`**${snapshot.workspaces.length}** workspaces \xB7 **${snapshot.dependencies.length}** dependencies \xB7 **${snapshot.findings.length}** findings`
|
|
4210
|
+
];
|
|
4211
|
+
const findings = snapshot.findings;
|
|
4212
|
+
const critical = findings.filter((f) => f.severity === "critical" || f.severity === "high");
|
|
4213
|
+
if (critical.length > 0) {
|
|
4214
|
+
lines.push("", `**Top ${Math.min(5, critical.length)} urgent findings:**`);
|
|
4215
|
+
for (const f of critical.slice(0, 5)) {
|
|
4216
|
+
const dep = snapshot.dependencies.find((d) => d.id === f.dependencyId);
|
|
4217
|
+
lines.push(` \u2022 **${dep?.name ?? f.dependencyId}** \u2014 ${f.type}: ${f.rationale}`);
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
lines.push("", `_Open the TechStack view for the full report._`);
|
|
4221
|
+
return lines.join("\n");
|
|
4222
|
+
}
|
|
4223
|
+
export {
|
|
4224
|
+
CppAdapter,
|
|
4225
|
+
DDL,
|
|
4226
|
+
DEFAULT_TRIAGE_LIMIT,
|
|
4227
|
+
DartAdapter,
|
|
4228
|
+
DotNetAdapter,
|
|
4229
|
+
ElixirAdapter,
|
|
4230
|
+
GoAdapter,
|
|
4231
|
+
MavenAdapter,
|
|
4232
|
+
NpmAdapter,
|
|
4233
|
+
PhpAdapter,
|
|
4234
|
+
PythonAdapter,
|
|
4235
|
+
RubyAdapter,
|
|
4236
|
+
RustAdapter,
|
|
4237
|
+
TechStackEngine,
|
|
4238
|
+
TechStackStore,
|
|
4239
|
+
applySchema,
|
|
4240
|
+
attemptDelivery,
|
|
4241
|
+
buildPurl,
|
|
4242
|
+
classifyStatus,
|
|
4243
|
+
clearRegistryCache,
|
|
4244
|
+
clusterCandidates,
|
|
4245
|
+
compareVersions,
|
|
4246
|
+
constructPurl,
|
|
4247
|
+
coverageForEcosystem,
|
|
4248
|
+
cppAdapter,
|
|
4249
|
+
createProviderLlm,
|
|
4250
|
+
createResearcher,
|
|
4251
|
+
createToolSearch,
|
|
4252
|
+
dartAdapter,
|
|
4253
|
+
diffSnapshots,
|
|
4254
|
+
discoverWorkspaces,
|
|
4255
|
+
dotNetAdapter,
|
|
4256
|
+
drainPendingDeliveries,
|
|
4257
|
+
ecosystemForPurlType,
|
|
4258
|
+
elixirAdapter,
|
|
4259
|
+
failedLookupStatus,
|
|
4260
|
+
generateUpgradePlan,
|
|
4261
|
+
goAdapter,
|
|
4262
|
+
isNativeAuditAvailable,
|
|
4263
|
+
lookupRegistry,
|
|
4264
|
+
lookupRegistryBatch,
|
|
4265
|
+
mapDetectedWorkspace,
|
|
4266
|
+
mavenAdapter,
|
|
4267
|
+
npmAdapter,
|
|
4268
|
+
parsePurl,
|
|
4269
|
+
parsePurlEcosystem,
|
|
4270
|
+
parseResearchJson,
|
|
4271
|
+
phpAdapter,
|
|
4272
|
+
privateOrUnresolvedStatus,
|
|
4273
|
+
purlTypeForEcosystem,
|
|
4274
|
+
pythonAdapter,
|
|
4275
|
+
queryOsvBatch,
|
|
4276
|
+
queryOsvSingle,
|
|
4277
|
+
renderPlanMarkdown,
|
|
4278
|
+
rubyAdapter,
|
|
4279
|
+
runNativeAudit,
|
|
4280
|
+
runNpmAudit,
|
|
4281
|
+
rustAdapter,
|
|
4282
|
+
supportedRegistryEcosystems,
|
|
4283
|
+
toCycloneDX,
|
|
4284
|
+
toSpdx,
|
|
4285
|
+
triageCandidates
|
|
4286
|
+
};
|
|
4287
|
+
//# sourceMappingURL=index.js.map
|