@wrongstack/techstack 0.293.0 → 0.295.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/cpp.d.ts +1 -1
- package/dist/adapters/cpp.d.ts.map +1 -1
- package/dist/adapters/dart.d.ts +1 -2
- package/dist/adapters/dart.d.ts.map +1 -1
- package/dist/adapters/dotnet.d.ts +1 -1
- package/dist/adapters/dotnet.d.ts.map +1 -1
- package/dist/adapters/elixir.d.ts +1 -1
- package/dist/adapters/elixir.d.ts.map +1 -1
- package/dist/adapters/go.d.ts +1 -2
- package/dist/adapters/go.d.ts.map +1 -1
- package/dist/adapters/gradle.d.ts +9 -0
- package/dist/adapters/gradle.d.ts.map +1 -0
- package/dist/adapters/interface.d.ts +1 -0
- package/dist/adapters/interface.d.ts.map +1 -1
- package/dist/adapters/maven.d.ts +1 -1
- package/dist/adapters/maven.d.ts.map +1 -1
- package/dist/adapters/npm.d.ts +1 -1
- package/dist/adapters/npm.d.ts.map +1 -1
- package/dist/adapters/parse-utils.d.ts +15 -0
- package/dist/adapters/parse-utils.d.ts.map +1 -0
- package/dist/adapters/paths.d.ts +8 -1
- package/dist/adapters/paths.d.ts.map +1 -1
- package/dist/adapters/php.d.ts +1 -2
- package/dist/adapters/php.d.ts.map +1 -1
- package/dist/adapters/python.d.ts +1 -2
- package/dist/adapters/python.d.ts.map +1 -1
- package/dist/adapters/ruby.d.ts +1 -1
- package/dist/adapters/ruby.d.ts.map +1 -1
- package/dist/adapters/rust.d.ts +1 -2
- package/dist/adapters/rust.d.ts.map +1 -1
- package/dist/adapters/swift.d.ts +9 -0
- package/dist/adapters/swift.d.ts.map +1 -0
- package/dist/advisory/native-audit.d.ts +20 -8
- package/dist/advisory/native-audit.d.ts.map +1 -1
- package/dist/advisory/osv.d.ts.map +1 -1
- package/dist/discovery/index.d.ts +3 -13
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/index.d.ts +10 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1420 -942
- package/dist/index.js.map +4 -4
- package/dist/registry/client.d.ts +23 -0
- package/dist/registry/client.d.ts.map +1 -1
- package/dist/registry/http-fetch.d.ts +25 -0
- package/dist/registry/http-fetch.d.ts.map +1 -0
- package/dist/remediation.d.ts +35 -0
- package/dist/remediation.d.ts.map +1 -1
- package/dist/research/llm.d.ts +1 -1
- package/dist/research/llm.d.ts.map +1 -1
- package/dist/research/triage.d.ts +2 -15
- package/dist/research/triage.d.ts.map +1 -1
- package/dist/service/enrich-phase.d.ts +8 -0
- package/dist/service/enrich-phase.d.ts.map +1 -0
- package/dist/service/finding-factory.d.ts +4 -0
- package/dist/service/finding-factory.d.ts.map +1 -0
- package/dist/service/inventory-phase.d.ts +9 -0
- package/dist/service/inventory-phase.d.ts.map +1 -0
- package/dist/service/report-generator.d.ts +3 -0
- package/dist/service/report-generator.d.ts.map +1 -0
- package/dist/service/research-phase.d.ts +10 -0
- package/dist/service/research-phase.d.ts.map +1 -0
- package/dist/service/techstack-engine.d.ts +33 -0
- package/dist/service/techstack-engine.d.ts.map +1 -0
- package/dist/service.d.ts +5 -106
- package/dist/service.d.ts.map +1 -1
- package/dist/store/sqlite.d.ts +4 -0
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/trend.d.ts +36 -0
- package/dist/trend.d.ts.map +1 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -81,7 +81,7 @@ function parsePurl(purl) {
|
|
|
81
81
|
const slashIdx = main.indexOf("/");
|
|
82
82
|
if (slashIdx < 0) return void 0;
|
|
83
83
|
const type = main.slice(0, slashIdx);
|
|
84
|
-
|
|
84
|
+
const remainder = main.slice(slashIdx + 1);
|
|
85
85
|
let namespace;
|
|
86
86
|
let name;
|
|
87
87
|
const nsSlashIdx = remainder.indexOf("/");
|
|
@@ -265,10 +265,12 @@ function coverageForEcosystem(ecosystem) {
|
|
|
265
265
|
}
|
|
266
266
|
|
|
267
267
|
// src/adapters/npm.ts
|
|
268
|
-
import {
|
|
268
|
+
import { access as access2, readFile } from "node:fs/promises";
|
|
269
269
|
import { dirname, join, relative, resolve as resolve2 } from "node:path";
|
|
270
270
|
|
|
271
271
|
// src/adapters/paths.ts
|
|
272
|
+
import { existsSync } from "node:fs";
|
|
273
|
+
import { access } from "node:fs/promises";
|
|
272
274
|
import { resolve } from "node:path";
|
|
273
275
|
function workspaceRoot(workspace, options) {
|
|
274
276
|
const relative2 = workspace.relativeRoot || ".";
|
|
@@ -277,9 +279,26 @@ function workspaceRoot(workspace, options) {
|
|
|
277
279
|
function resolveIn(root, candidate) {
|
|
278
280
|
return resolve(root, candidate);
|
|
279
281
|
}
|
|
282
|
+
function manifestEvidence(path) {
|
|
283
|
+
return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
284
|
+
}
|
|
285
|
+
function lockfileEvidence(path) {
|
|
286
|
+
return { kind: "lockfile", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
287
|
+
}
|
|
288
|
+
function fileExists(filePath) {
|
|
289
|
+
return existsSync(filePath);
|
|
290
|
+
}
|
|
291
|
+
async function fileExistsAsync(filePath) {
|
|
292
|
+
try {
|
|
293
|
+
await access(filePath);
|
|
294
|
+
return true;
|
|
295
|
+
} catch {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
280
299
|
|
|
281
300
|
// src/adapters/npm.ts
|
|
282
|
-
function detectLockfile(workspaceDir, stopAt) {
|
|
301
|
+
async function detectLockfile(workspaceDir, stopAt) {
|
|
283
302
|
const candidates = [
|
|
284
303
|
{ file: "pnpm-lock.yaml", kind: "pnpm" },
|
|
285
304
|
{ file: "package-lock.json", kind: "npm" },
|
|
@@ -291,7 +310,11 @@ function detectLockfile(workspaceDir, stopAt) {
|
|
|
291
310
|
for (; ; ) {
|
|
292
311
|
for (const c of candidates) {
|
|
293
312
|
const candidate = join(dir, c.file);
|
|
294
|
-
|
|
313
|
+
try {
|
|
314
|
+
await access2(candidate);
|
|
315
|
+
return { kind: c.kind, path: candidate };
|
|
316
|
+
} catch {
|
|
317
|
+
}
|
|
295
318
|
}
|
|
296
319
|
if (ceiling && dir === ceiling) break;
|
|
297
320
|
const parent = dirname(dir);
|
|
@@ -379,19 +402,29 @@ function parseNpmLockVersions(lockContent) {
|
|
|
379
402
|
}
|
|
380
403
|
return versions;
|
|
381
404
|
}
|
|
382
|
-
function
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
405
|
+
function parsePnpmAllVersions(lockContent) {
|
|
406
|
+
const versions = /* @__PURE__ */ new Map();
|
|
407
|
+
let inPackages = false;
|
|
408
|
+
for (const raw of lockContent.split(/\r?\n/)) {
|
|
409
|
+
if (!/^\s/.test(raw)) {
|
|
410
|
+
inPackages = raw.startsWith("packages:") || raw.startsWith("snapshots:");
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (!inPackages) continue;
|
|
414
|
+
const indent = raw.length - raw.trimStart().length;
|
|
415
|
+
if (indent !== 2) continue;
|
|
416
|
+
const trimmed = raw.trim();
|
|
417
|
+
const separator = trimmed.indexOf(":");
|
|
418
|
+
if (separator < 0) continue;
|
|
419
|
+
const key = unquote(trimmed.slice(0, separator));
|
|
420
|
+
const clean = stripPeerSuffix(key);
|
|
421
|
+
const splitAt = clean.lastIndexOf("@");
|
|
422
|
+
if (splitAt <= 0) continue;
|
|
423
|
+
const name = clean.slice(0, splitAt);
|
|
424
|
+
const version = clean.slice(splitAt + 1);
|
|
425
|
+
if (name && /^\d/.test(version) && !versions.has(name)) versions.set(name, version);
|
|
426
|
+
}
|
|
427
|
+
return versions;
|
|
395
428
|
}
|
|
396
429
|
function scopeForSection(section) {
|
|
397
430
|
switch (section) {
|
|
@@ -431,29 +464,32 @@ var NpmAdapter = class {
|
|
|
431
464
|
let pkg;
|
|
432
465
|
let manifestContent;
|
|
433
466
|
try {
|
|
434
|
-
manifestContent =
|
|
467
|
+
manifestContent = await readFile(manifestPath, "utf-8");
|
|
435
468
|
pkg = JSON.parse(manifestContent);
|
|
436
469
|
} catch {
|
|
437
470
|
return [];
|
|
438
471
|
}
|
|
439
472
|
const manifestEv = manifestEvidence(manifestPath);
|
|
440
|
-
const lockInfo = detectLockfile(root, options.projectRoot);
|
|
473
|
+
const lockInfo = await detectLockfile(root, options.projectRoot);
|
|
441
474
|
const resolvedVersions = /* @__PURE__ */ new Map();
|
|
475
|
+
const allLockVersions = /* @__PURE__ */ new Map();
|
|
442
476
|
let lockEv;
|
|
443
477
|
if (lockInfo.kind === "pnpm") {
|
|
444
478
|
try {
|
|
445
|
-
const lockContent =
|
|
479
|
+
const lockContent = await readFile(lockInfo.path, "utf-8");
|
|
446
480
|
const importerPath = relative(dirname(lockInfo.path), root).split(/[/\\]/).filter(Boolean).join("/") || ".";
|
|
447
481
|
const parsed = parsePnpmImporterVersions(lockContent, importerPath);
|
|
448
482
|
for (const [k, v] of parsed) resolvedVersions.set(k, v);
|
|
483
|
+
for (const [k, v] of parsePnpmAllVersions(lockContent)) allLockVersions.set(k, v);
|
|
449
484
|
if (parsed.size > 0) lockEv = lockfileEvidence(lockInfo.path);
|
|
450
485
|
} catch {
|
|
451
486
|
}
|
|
452
487
|
} else if (lockInfo.kind === "npm") {
|
|
453
488
|
try {
|
|
454
|
-
const lockContent =
|
|
489
|
+
const lockContent = await readFile(lockInfo.path, "utf-8");
|
|
455
490
|
const parsed = parseNpmLockVersions(lockContent);
|
|
456
491
|
for (const [k, v] of parsed) resolvedVersions.set(k, v);
|
|
492
|
+
for (const [k, v] of parsed) allLockVersions.set(k, v);
|
|
457
493
|
lockEv = lockfileEvidence(lockInfo.path);
|
|
458
494
|
} catch {
|
|
459
495
|
}
|
|
@@ -494,20 +530,80 @@ var NpmAdapter = class {
|
|
|
494
530
|
});
|
|
495
531
|
}
|
|
496
532
|
}
|
|
533
|
+
if (options.includeTransitive && lockEv) {
|
|
534
|
+
for (const [name, locked] of allLockVersions) {
|
|
535
|
+
if (seen.has(name)) continue;
|
|
536
|
+
seen.add(name);
|
|
537
|
+
observations.push({
|
|
538
|
+
id: `dep-${workspace.id}-${name}`,
|
|
539
|
+
workspaceId: workspace.id,
|
|
540
|
+
purl: buildPurl({ type: "npm", name, version: locked }),
|
|
541
|
+
ecosystem: "npm",
|
|
542
|
+
name,
|
|
543
|
+
sourceType: "registry",
|
|
544
|
+
direct: false,
|
|
545
|
+
scope: "transitive",
|
|
546
|
+
locked,
|
|
547
|
+
status: "current",
|
|
548
|
+
evidence: [lockEv]
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
497
552
|
return observations;
|
|
498
553
|
}
|
|
499
554
|
};
|
|
500
555
|
var npmAdapter = new NpmAdapter();
|
|
501
556
|
|
|
502
557
|
// src/adapters/python.ts
|
|
503
|
-
import {
|
|
558
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
504
559
|
import { join as join2 } from "node:path";
|
|
505
|
-
|
|
506
|
-
|
|
560
|
+
|
|
561
|
+
// src/adapters/parse-utils.ts
|
|
562
|
+
function stripInlineComment(line, marker = "#") {
|
|
563
|
+
let quote;
|
|
564
|
+
let escaped = false;
|
|
565
|
+
for (let index = 0; index < line.length; index++) {
|
|
566
|
+
const character = line.charAt(index);
|
|
567
|
+
if (escaped) {
|
|
568
|
+
escaped = false;
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (character === "\\" && quote === '"') {
|
|
572
|
+
escaped = true;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
if (character === '"' || character === "'") {
|
|
576
|
+
quote = quote === character ? void 0 : quote ?? character;
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
if (!quote && line.startsWith(marker, index)) return line.slice(0, index).trimEnd();
|
|
580
|
+
}
|
|
581
|
+
return line;
|
|
507
582
|
}
|
|
508
|
-
function
|
|
509
|
-
|
|
583
|
+
function parseTomlKeyValue(line) {
|
|
584
|
+
const cleaned = stripInlineComment(line).trim();
|
|
585
|
+
const match = /^(?:"([^"]+)"|'([^']+)'|([A-Za-z0-9_.-]+))\s*=\s*(.+)$/.exec(cleaned);
|
|
586
|
+
if (!match) return void 0;
|
|
587
|
+
const key = match[1] ?? match[2] ?? match[3];
|
|
588
|
+
const value = match[4];
|
|
589
|
+
return key && value ? { key, value: value.trim() } : void 0;
|
|
510
590
|
}
|
|
591
|
+
function parseXmlAttributes(source) {
|
|
592
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
593
|
+
const regex = /([A-Za-z_:][\w:.-]*)\s*=\s*(["'])(.*?)\2/g;
|
|
594
|
+
for (const match of source.matchAll(regex)) {
|
|
595
|
+
const key = match[1];
|
|
596
|
+
const value = match[3];
|
|
597
|
+
if (key !== void 0 && value !== void 0) attributes.set(key, value);
|
|
598
|
+
}
|
|
599
|
+
return attributes;
|
|
600
|
+
}
|
|
601
|
+
function xmlTagValue(source, tag) {
|
|
602
|
+
const escaped = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
603
|
+
return new RegExp(`<${escaped}(?:\\s[^>]*)?>\\s*([^<]+?)\\s*</${escaped}>`, "i").exec(source)?.[1]?.trim();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// src/adapters/python.ts
|
|
511
607
|
function parseTomlSections(content) {
|
|
512
608
|
const sections = [];
|
|
513
609
|
let currentSection = "__header__";
|
|
@@ -595,6 +691,19 @@ function parsePyprojectDeps(content) {
|
|
|
595
691
|
}
|
|
596
692
|
}
|
|
597
693
|
}
|
|
694
|
+
if (section.name === "tool.poetry.dependencies" || section.name.startsWith("tool.poetry.group.")) {
|
|
695
|
+
const scope = section.name === "tool.poetry.dependencies" ? "runtime" : "development";
|
|
696
|
+
for (const raw of section.lines) {
|
|
697
|
+
const entry = parseTomlKeyValue(raw);
|
|
698
|
+
if (!entry) continue;
|
|
699
|
+
const name = entry.key;
|
|
700
|
+
if (!name || name.toLowerCase() === "python") continue;
|
|
701
|
+
const rawValue = entry.value;
|
|
702
|
+
const inlineVersion = rawValue.match(/\bversion\s*=\s*["']([^"']+)["']/)?.[1];
|
|
703
|
+
const stringVersion = rawValue.match(/^["']([^"']+)["']/)?.[1];
|
|
704
|
+
deps.push({ name, constraint: inlineVersion ?? stringVersion, scope });
|
|
705
|
+
}
|
|
706
|
+
}
|
|
598
707
|
}
|
|
599
708
|
return deps;
|
|
600
709
|
}
|
|
@@ -631,26 +740,60 @@ function parseRequirementsLockVersions(content) {
|
|
|
631
740
|
const line = raw.trim();
|
|
632
741
|
if (!line || line.startsWith("#") || line.startsWith("-")) continue;
|
|
633
742
|
const match = line.match(/^([a-zA-Z0-9][a-zA-Z0-9._-]*)\s*==\s*([^\s;]+)/);
|
|
634
|
-
if (match) versions.set(match[1], match[2]);
|
|
743
|
+
if (match?.[1] && match[2]) versions.set(normalizePkgName(match[1]), match[2]);
|
|
744
|
+
}
|
|
745
|
+
return versions;
|
|
746
|
+
}
|
|
747
|
+
function parsePipfileLock(content) {
|
|
748
|
+
const versions = /* @__PURE__ */ new Map();
|
|
749
|
+
try {
|
|
750
|
+
const json = JSON.parse(content);
|
|
751
|
+
for (const section of ["default", "develop"]) {
|
|
752
|
+
for (const [name, entry] of Object.entries(json[section] ?? {})) {
|
|
753
|
+
if (entry.version?.startsWith("==")) versions.set(normalizePkgName(name), entry.version.slice(2));
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
} catch {
|
|
757
|
+
}
|
|
758
|
+
return versions;
|
|
759
|
+
}
|
|
760
|
+
function parsePoetryLock(content) {
|
|
761
|
+
const versions = /* @__PURE__ */ new Map();
|
|
762
|
+
let currentName;
|
|
763
|
+
for (const raw of content.split("\n")) {
|
|
764
|
+
const line = raw.trim();
|
|
765
|
+
const nameMatch = line.match(/^name\s*=\s*"([^"]+)"/);
|
|
766
|
+
if (nameMatch) {
|
|
767
|
+
currentName = nameMatch[1];
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
const versionMatch = line.match(/^version\s*=\s*"([^"]+)"/);
|
|
771
|
+
if (versionMatch && currentName) {
|
|
772
|
+
versions.set(normalizePkgName(currentName), versionMatch[1]);
|
|
773
|
+
currentName = void 0;
|
|
774
|
+
}
|
|
635
775
|
}
|
|
636
776
|
return versions;
|
|
637
777
|
}
|
|
778
|
+
function normalizePkgName(name) {
|
|
779
|
+
return name.toLowerCase().replace(/_/g, "-");
|
|
780
|
+
}
|
|
638
781
|
var PythonAdapter = class {
|
|
639
782
|
ecosystem = "python";
|
|
640
783
|
async inventory(workspace, options) {
|
|
641
784
|
const observations = [];
|
|
642
785
|
const root = workspaceRoot(workspace, options);
|
|
643
786
|
const seen = /* @__PURE__ */ new Set();
|
|
644
|
-
const hasPyproject = workspace.manifests.some((m) => m.includes("pyproject.toml")) ||
|
|
645
|
-
const hasRequirements = workspace.manifests.some((m) => m.includes("requirements.txt")) ||
|
|
646
|
-
const hasPipfile = workspace.manifests.some((m) => m.includes("Pipfile")) ||
|
|
647
|
-
const lockfilePath = this.detectLockfile(root);
|
|
648
|
-
|
|
787
|
+
const hasPyproject = workspace.manifests.some((m) => m.includes("pyproject.toml")) || await fileExistsAsync(join2(root, "pyproject.toml"));
|
|
788
|
+
const hasRequirements = workspace.manifests.some((m) => m.includes("requirements.txt")) || await fileExistsAsync(join2(root, "requirements.txt"));
|
|
789
|
+
const hasPipfile = workspace.manifests.some((m) => m.includes("Pipfile")) || await fileExistsAsync(join2(root, "Pipfile"));
|
|
790
|
+
const lockfilePath = await this.detectLockfile(root);
|
|
791
|
+
const allDeps = [];
|
|
649
792
|
let pyprojectEv;
|
|
650
793
|
if (hasPyproject) {
|
|
651
794
|
try {
|
|
652
|
-
const content =
|
|
653
|
-
pyprojectEv =
|
|
795
|
+
const content = await readFile2(join2(root, "pyproject.toml"), "utf-8");
|
|
796
|
+
pyprojectEv = manifestEvidence(join2(root, "pyproject.toml"));
|
|
654
797
|
const parsed = parsePyprojectDeps(content);
|
|
655
798
|
for (const d of parsed) allDeps.push({ ...d, source: "pyproject.toml" });
|
|
656
799
|
} catch {
|
|
@@ -660,8 +803,8 @@ var PythonAdapter = class {
|
|
|
660
803
|
let requirementsEv;
|
|
661
804
|
if (hasRequirements) {
|
|
662
805
|
try {
|
|
663
|
-
const content =
|
|
664
|
-
requirementsEv =
|
|
806
|
+
const content = await readFile2(join2(root, "requirements.txt"), "utf-8");
|
|
807
|
+
requirementsEv = manifestEvidence(join2(root, "requirements.txt"));
|
|
665
808
|
const parsed = parseRequirementsTxt(content);
|
|
666
809
|
for (const d of parsed) {
|
|
667
810
|
if (!allDeps.some((existing) => existing.name === d.name)) {
|
|
@@ -674,8 +817,8 @@ var PythonAdapter = class {
|
|
|
674
817
|
}
|
|
675
818
|
if (hasPipfile) {
|
|
676
819
|
try {
|
|
677
|
-
const content =
|
|
678
|
-
if (!pyprojectEv) pyprojectEv =
|
|
820
|
+
const content = await readFile2(join2(root, "Pipfile"), "utf-8");
|
|
821
|
+
if (!pyprojectEv) pyprojectEv = manifestEvidence(join2(root, "Pipfile"));
|
|
679
822
|
const parsed = parsePipfileDeps(content);
|
|
680
823
|
for (const d of parsed) {
|
|
681
824
|
if (!allDeps.some((existing) => existing.name === d.name)) {
|
|
@@ -686,10 +829,13 @@ var PythonAdapter = class {
|
|
|
686
829
|
}
|
|
687
830
|
}
|
|
688
831
|
let lockEv;
|
|
832
|
+
const lockVersions = new Map(reqLockVersions);
|
|
689
833
|
if (lockfilePath) {
|
|
690
834
|
try {
|
|
691
|
-
|
|
692
|
-
|
|
835
|
+
const lockContent = await readFile2(lockfilePath, "utf-8");
|
|
836
|
+
const parsed = lockfilePath.endsWith("poetry.lock") || lockfilePath.endsWith("uv.lock") ? parsePoetryLock(lockContent) : parsePipfileLock(lockContent);
|
|
837
|
+
for (const [name, version] of parsed) lockVersions.set(name, version);
|
|
838
|
+
lockEv = lockfileEvidence(lockfilePath);
|
|
693
839
|
} catch {
|
|
694
840
|
}
|
|
695
841
|
}
|
|
@@ -697,7 +843,7 @@ var PythonAdapter = class {
|
|
|
697
843
|
for (const dep of allDeps) {
|
|
698
844
|
if (seen.has(dep.name)) continue;
|
|
699
845
|
seen.add(dep.name);
|
|
700
|
-
const locked =
|
|
846
|
+
const locked = lockVersions.get(normalizePkgName(dep.name));
|
|
701
847
|
const isRegistry = !dep.constraint || !dep.constraint.startsWith("file:") && !dep.constraint.startsWith("git+") && !dep.constraint.startsWith("-e");
|
|
702
848
|
const purl = isRegistry && locked ? buildPurl({ type: "python", name: dep.name, version: locked }) : isRegistry ? buildPurl({ type: "python", name: dep.name }) : void 0;
|
|
703
849
|
const evidence = [];
|
|
@@ -722,23 +868,31 @@ var PythonAdapter = class {
|
|
|
722
868
|
evidence
|
|
723
869
|
});
|
|
724
870
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
871
|
+
if (options.includeTransitive && lockEv) {
|
|
872
|
+
for (const [name, locked] of lockVersions) {
|
|
873
|
+
if (seen.has(name)) continue;
|
|
874
|
+
seen.add(name);
|
|
875
|
+
observations.push({
|
|
876
|
+
id: `dep-${workspace.id}-${name}`,
|
|
877
|
+
workspaceId: workspace.id,
|
|
878
|
+
purl: buildPurl({ type: "python", name, version: locked }),
|
|
879
|
+
ecosystem: "python",
|
|
880
|
+
name,
|
|
881
|
+
sourceType: "registry",
|
|
882
|
+
direct: false,
|
|
883
|
+
scope: "transitive",
|
|
884
|
+
locked,
|
|
885
|
+
status: "current",
|
|
886
|
+
evidence: [lockEv]
|
|
887
|
+
});
|
|
888
|
+
}
|
|
733
889
|
}
|
|
890
|
+
return observations;
|
|
734
891
|
}
|
|
735
|
-
detectLockfile(workspaceRoot2) {
|
|
892
|
+
async detectLockfile(workspaceRoot2) {
|
|
736
893
|
for (const file of ["Pipfile.lock", "poetry.lock", "uv.lock"]) {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
return join2(workspaceRoot2, file);
|
|
740
|
-
} catch {
|
|
741
|
-
}
|
|
894
|
+
const candidate = join2(workspaceRoot2, file);
|
|
895
|
+
if (await fileExistsAsync(candidate)) return candidate;
|
|
742
896
|
}
|
|
743
897
|
return void 0;
|
|
744
898
|
}
|
|
@@ -746,13 +900,7 @@ var PythonAdapter = class {
|
|
|
746
900
|
var pythonAdapter = new PythonAdapter();
|
|
747
901
|
|
|
748
902
|
// src/adapters/rust.ts
|
|
749
|
-
import { readFileSync
|
|
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
|
-
}
|
|
903
|
+
import { readFileSync } from "node:fs";
|
|
756
904
|
function parseTomlSections2(content) {
|
|
757
905
|
const sections = [];
|
|
758
906
|
let currentSection = "__header__";
|
|
@@ -772,33 +920,29 @@ function parseTomlSections2(content) {
|
|
|
772
920
|
if (currentLines.length > 0) sections.push({ name: currentSection, lines: currentLines });
|
|
773
921
|
return sections;
|
|
774
922
|
}
|
|
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
923
|
function extractTomlDeps(sectionLines) {
|
|
783
924
|
const deps = [];
|
|
784
925
|
for (const raw of sectionLines) {
|
|
785
926
|
const line = raw.trim();
|
|
786
927
|
if (line.startsWith("#") || line === "") continue;
|
|
787
|
-
const
|
|
928
|
+
const entry = parseTomlKeyValue(line);
|
|
929
|
+
if (!entry) continue;
|
|
930
|
+
const tableMatch = entry.value.match(/^\{\s*(.*?)\s*\}$/);
|
|
788
931
|
if (tableMatch) {
|
|
789
|
-
const name =
|
|
790
|
-
const inner = tableMatch[
|
|
932
|
+
const name = entry.key;
|
|
933
|
+
const inner = tableMatch[1];
|
|
934
|
+
if (inner === void 0) continue;
|
|
791
935
|
const versionMatch = inner.match(/version\s*=\s*"([^"]+)"/);
|
|
792
|
-
|
|
936
|
+
const sourceType = /\bgit\s*=/.test(inner) ? "git" : /\bpath\s*=/.test(inner) ? "path" : "registry";
|
|
937
|
+
deps.push({ name, version: versionMatch?.[1], sourceType });
|
|
793
938
|
continue;
|
|
794
939
|
}
|
|
795
|
-
const simpleMatch =
|
|
940
|
+
const simpleMatch = entry.value.match(/^"([^"]*)"$/);
|
|
796
941
|
if (simpleMatch) {
|
|
797
|
-
deps.push({ name:
|
|
942
|
+
deps.push({ name: entry.key, version: simpleMatch[1] || void 0, sourceType: "registry" });
|
|
798
943
|
continue;
|
|
799
944
|
}
|
|
800
|
-
|
|
801
|
-
if (partialMatch && !partialMatch.value.startsWith("{") && !partialMatch.value.startsWith('"')) {
|
|
945
|
+
if (!entry.value.startsWith("{") && !entry.value.startsWith('"')) {
|
|
802
946
|
}
|
|
803
947
|
}
|
|
804
948
|
return deps;
|
|
@@ -854,23 +998,23 @@ var RustAdapter = class {
|
|
|
854
998
|
const observations = [];
|
|
855
999
|
const root = workspaceRoot(workspace, options);
|
|
856
1000
|
const seen = /* @__PURE__ */ new Set();
|
|
857
|
-
const cargoTomlPath = workspace.manifests.find((m) => m.includes("Cargo.toml")) || (
|
|
1001
|
+
const cargoTomlPath = workspace.manifests.find((m) => m.includes("Cargo.toml")) || (fileExists(resolveIn(root, "Cargo.toml")) ? "Cargo.toml" : void 0);
|
|
858
1002
|
if (!cargoTomlPath) return [];
|
|
859
1003
|
const fullManifestPath = resolveIn(root, cargoTomlPath);
|
|
860
1004
|
let cargoContent;
|
|
861
1005
|
try {
|
|
862
|
-
cargoContent =
|
|
1006
|
+
cargoContent = readFileSync(fullManifestPath, "utf-8");
|
|
863
1007
|
} catch {
|
|
864
1008
|
return [];
|
|
865
1009
|
}
|
|
866
|
-
const manifestEv =
|
|
1010
|
+
const manifestEv = manifestEvidence(fullManifestPath);
|
|
867
1011
|
const cargoLockPath = resolveIn(root, "Cargo.lock");
|
|
868
1012
|
let lockVersions = /* @__PURE__ */ new Map();
|
|
869
1013
|
let lockEv;
|
|
870
1014
|
try {
|
|
871
|
-
const lockContent =
|
|
1015
|
+
const lockContent = readFileSync(cargoLockPath, "utf-8");
|
|
872
1016
|
lockVersions = parseCargoLock(lockContent);
|
|
873
|
-
lockEv =
|
|
1017
|
+
lockEv = lockfileEvidence(cargoLockPath);
|
|
874
1018
|
} catch {
|
|
875
1019
|
}
|
|
876
1020
|
const sections = parseTomlSections2(cargoContent);
|
|
@@ -891,18 +1035,18 @@ var RustAdapter = class {
|
|
|
891
1035
|
if (seen.has(dep.name)) continue;
|
|
892
1036
|
seen.add(dep.name);
|
|
893
1037
|
const locked = lockVersions.get(dep.name) || dep.version;
|
|
894
|
-
const isRegistry =
|
|
1038
|
+
const isRegistry = dep.sourceType === "registry";
|
|
895
1039
|
const purl = isRegistry && locked ? buildPurl({ type: "rust", name: dep.name, version: locked }) : isRegistry ? buildPurl({ type: "rust", name: dep.name }) : void 0;
|
|
896
1040
|
const evidence = [manifestEv];
|
|
897
1041
|
if (lockEv && locked && lockVersions.has(dep.name)) evidence.push(lockEv);
|
|
898
|
-
const status = dep.
|
|
1042
|
+
const status = dep.sourceType === "git" ? "git_dependency" : dep.sourceType === "path" ? "local_path" : "current";
|
|
899
1043
|
observations.push({
|
|
900
1044
|
id: `dep-${workspace.id}-${dep.name}`,
|
|
901
1045
|
workspaceId: workspace.id,
|
|
902
1046
|
...purl ? { purl } : {},
|
|
903
1047
|
ecosystem: "rust",
|
|
904
1048
|
name: dep.name,
|
|
905
|
-
sourceType:
|
|
1049
|
+
sourceType: dep.sourceType,
|
|
906
1050
|
direct: true,
|
|
907
1051
|
scope,
|
|
908
1052
|
...dep.version ? { requested: dep.version } : {},
|
|
@@ -912,27 +1056,32 @@ var RustAdapter = class {
|
|
|
912
1056
|
});
|
|
913
1057
|
}
|
|
914
1058
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1059
|
+
if (options.includeTransitive && lockEv) {
|
|
1060
|
+
for (const [name, locked] of lockVersions) {
|
|
1061
|
+
if (seen.has(name)) continue;
|
|
1062
|
+
seen.add(name);
|
|
1063
|
+
observations.push({
|
|
1064
|
+
id: `dep-${workspace.id}-${name}`,
|
|
1065
|
+
workspaceId: workspace.id,
|
|
1066
|
+
purl: buildPurl({ type: "rust", name, version: locked }),
|
|
1067
|
+
ecosystem: "rust",
|
|
1068
|
+
name,
|
|
1069
|
+
sourceType: "registry",
|
|
1070
|
+
direct: false,
|
|
1071
|
+
scope: "transitive",
|
|
1072
|
+
locked,
|
|
1073
|
+
status: "current",
|
|
1074
|
+
evidence: [lockEv]
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
923
1077
|
}
|
|
1078
|
+
return observations;
|
|
924
1079
|
}
|
|
925
1080
|
};
|
|
926
1081
|
var rustAdapter = new RustAdapter();
|
|
927
1082
|
|
|
928
1083
|
// src/adapters/go.ts
|
|
929
|
-
import { readFileSync as
|
|
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
|
-
}
|
|
1084
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
936
1085
|
function cleanGoVersion(v) {
|
|
937
1086
|
return v.replace(/^v/i, "");
|
|
938
1087
|
}
|
|
@@ -968,11 +1117,34 @@ function parseGoMod(content) {
|
|
|
968
1117
|
continue;
|
|
969
1118
|
}
|
|
970
1119
|
if (line.startsWith("exclude") || line.startsWith("replace") || line.startsWith("retract")) {
|
|
971
|
-
continue;
|
|
972
1120
|
}
|
|
973
1121
|
}
|
|
974
1122
|
return deps;
|
|
975
1123
|
}
|
|
1124
|
+
function parseGoReplacements(content) {
|
|
1125
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
1126
|
+
let inBlock = false;
|
|
1127
|
+
for (const raw of content.split("\n")) {
|
|
1128
|
+
const line = raw.trim();
|
|
1129
|
+
if (line === "replace (") {
|
|
1130
|
+
inBlock = true;
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
if (inBlock && line === ")") {
|
|
1134
|
+
inBlock = false;
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
const candidate = inBlock ? line : line.startsWith("replace ") ? line.slice(8).trim() : "";
|
|
1138
|
+
const match = candidate.match(/^(\S+)(?:\s+v\S+)?\s+=>\s+(\S+)/);
|
|
1139
|
+
if (!match) continue;
|
|
1140
|
+
const modulePath = match[1];
|
|
1141
|
+
const target = match[2];
|
|
1142
|
+
if (!modulePath || !target) continue;
|
|
1143
|
+
const local = target.startsWith(".") || target.startsWith("/") || /^[A-Za-z]:[\\/]/.test(target);
|
|
1144
|
+
replacements.set(modulePath, local ? "path" : "git");
|
|
1145
|
+
}
|
|
1146
|
+
return replacements;
|
|
1147
|
+
}
|
|
976
1148
|
function parseGoSum(content) {
|
|
977
1149
|
const versions = /* @__PURE__ */ new Map();
|
|
978
1150
|
for (const raw of content.split("\n")) {
|
|
@@ -1003,25 +1175,26 @@ var GoAdapter = class {
|
|
|
1003
1175
|
const observations = [];
|
|
1004
1176
|
const root = workspaceRoot(workspace, options);
|
|
1005
1177
|
const seen = /* @__PURE__ */ new Set();
|
|
1006
|
-
const goModPath = workspace.manifests.find((m) => m.includes("go.mod")) || (
|
|
1178
|
+
const goModPath = workspace.manifests.find((m) => m.includes("go.mod")) || (fileExists(resolveIn(root, "go.mod")) ? "go.mod" : void 0);
|
|
1007
1179
|
if (!goModPath) return [];
|
|
1008
1180
|
const fullManifestPath = resolveIn(root, goModPath);
|
|
1009
1181
|
let goModContent;
|
|
1010
1182
|
try {
|
|
1011
|
-
goModContent =
|
|
1183
|
+
goModContent = readFileSync2(fullManifestPath, "utf-8");
|
|
1012
1184
|
} catch {
|
|
1013
1185
|
return [];
|
|
1014
1186
|
}
|
|
1015
|
-
const manifestEv =
|
|
1187
|
+
const manifestEv = manifestEvidence(fullManifestPath);
|
|
1016
1188
|
const requires = parseGoMod(goModContent);
|
|
1189
|
+
const replacements = parseGoReplacements(goModContent);
|
|
1017
1190
|
const modName = parseGoModuleName(goModContent);
|
|
1018
1191
|
const goSumPath = resolveIn(root, "go.sum");
|
|
1019
1192
|
let lockVersions = /* @__PURE__ */ new Map();
|
|
1020
1193
|
let lockEv;
|
|
1021
1194
|
try {
|
|
1022
|
-
const sumContent =
|
|
1195
|
+
const sumContent = readFileSync2(goSumPath, "utf-8");
|
|
1023
1196
|
lockVersions = parseGoSum(sumContent);
|
|
1024
|
-
lockEv =
|
|
1197
|
+
lockEv = lockfileEvidence(goSumPath);
|
|
1025
1198
|
} catch {
|
|
1026
1199
|
}
|
|
1027
1200
|
for (const req of requires) {
|
|
@@ -1031,53 +1204,43 @@ var GoAdapter = class {
|
|
|
1031
1204
|
const scope = req.indirect ? "transitive" : "runtime";
|
|
1032
1205
|
const direct = !req.indirect;
|
|
1033
1206
|
const locked = lockVersions.get(req.modulePath) || req.version;
|
|
1034
|
-
const
|
|
1207
|
+
const replacement = replacements.get(req.modulePath);
|
|
1208
|
+
const purl = replacement ? void 0 : buildPurl({ type: "go", name: req.modulePath, version: locked });
|
|
1035
1209
|
const evidence = [manifestEv];
|
|
1036
1210
|
if (lockEv && lockVersions.has(req.modulePath)) evidence.push(lockEv);
|
|
1037
1211
|
observations.push({
|
|
1038
1212
|
id: `dep-${workspace.id}-${req.modulePath}`,
|
|
1039
1213
|
workspaceId: workspace.id,
|
|
1040
|
-
purl,
|
|
1214
|
+
...purl ? { purl } : {},
|
|
1041
1215
|
ecosystem: "go",
|
|
1042
1216
|
name: req.modulePath,
|
|
1043
|
-
sourceType: "registry",
|
|
1217
|
+
sourceType: replacement ?? "registry",
|
|
1044
1218
|
direct,
|
|
1045
1219
|
scope,
|
|
1046
1220
|
requested: req.version,
|
|
1047
1221
|
...locked ? { locked } : {},
|
|
1048
|
-
status: "current",
|
|
1222
|
+
status: replacement === "path" ? "local_path" : replacement === "git" ? "git_dependency" : "current",
|
|
1049
1223
|
evidence
|
|
1050
1224
|
});
|
|
1051
1225
|
}
|
|
1052
1226
|
return observations;
|
|
1053
1227
|
}
|
|
1054
|
-
fileExists(filePath) {
|
|
1055
|
-
try {
|
|
1056
|
-
readFileSync4(filePath, "utf-8");
|
|
1057
|
-
return true;
|
|
1058
|
-
} catch {
|
|
1059
|
-
return false;
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
1228
|
};
|
|
1063
1229
|
var goAdapter = new GoAdapter();
|
|
1064
1230
|
|
|
1065
1231
|
// src/adapters/dotnet.ts
|
|
1066
|
-
import {
|
|
1232
|
+
import { readFile as readFile3, readdir } from "node:fs/promises";
|
|
1067
1233
|
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
1234
|
function parseCsproj(content) {
|
|
1075
1235
|
const refs = [];
|
|
1076
|
-
const regex = /<PackageReference\
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
const
|
|
1080
|
-
const
|
|
1236
|
+
const regex = /<PackageReference\b([^>]*?)(?:\/>|>([\s\S]*?)<\/PackageReference>)/gi;
|
|
1237
|
+
for (const match of content.matchAll(regex)) {
|
|
1238
|
+
const attributes = match[1] ?? "";
|
|
1239
|
+
const body = match[2] ?? "";
|
|
1240
|
+
const parsedAttributes = parseXmlAttributes(attributes);
|
|
1241
|
+
const name = parsedAttributes.get("Include");
|
|
1242
|
+
if (!name) continue;
|
|
1243
|
+
const version = parsedAttributes.get("Version") ?? xmlTagValue(body, "Version");
|
|
1081
1244
|
refs.push({ name, version });
|
|
1082
1245
|
}
|
|
1083
1246
|
return refs;
|
|
@@ -1110,7 +1273,7 @@ var DotNetAdapter = class {
|
|
|
1110
1273
|
const seen = /* @__PURE__ */ new Set();
|
|
1111
1274
|
let csprojPath;
|
|
1112
1275
|
try {
|
|
1113
|
-
const files =
|
|
1276
|
+
const files = await readdir(root);
|
|
1114
1277
|
const csproj = files.find((f) => f.endsWith(".csproj"));
|
|
1115
1278
|
if (csproj) csprojPath = join3(root, csproj);
|
|
1116
1279
|
} catch {
|
|
@@ -1118,19 +1281,19 @@ var DotNetAdapter = class {
|
|
|
1118
1281
|
if (!csprojPath) return [];
|
|
1119
1282
|
let csprojContent;
|
|
1120
1283
|
try {
|
|
1121
|
-
csprojContent =
|
|
1284
|
+
csprojContent = await readFile3(csprojPath, "utf-8");
|
|
1122
1285
|
} catch {
|
|
1123
1286
|
return [];
|
|
1124
1287
|
}
|
|
1125
|
-
const manifestEv =
|
|
1288
|
+
const manifestEv = manifestEvidence(csprojPath);
|
|
1126
1289
|
const refs = parseCsproj(csprojContent);
|
|
1127
1290
|
const assetsPath = join3(root, "project.assets.json");
|
|
1128
1291
|
let lockVersions = /* @__PURE__ */ new Map();
|
|
1129
1292
|
let lockEv;
|
|
1130
1293
|
try {
|
|
1131
|
-
const assetsContent =
|
|
1294
|
+
const assetsContent = await readFile3(assetsPath, "utf-8");
|
|
1132
1295
|
lockVersions = parseProjectAssetsJson(assetsContent);
|
|
1133
|
-
lockEv =
|
|
1296
|
+
lockEv = lockfileEvidence(assetsPath);
|
|
1134
1297
|
} catch {
|
|
1135
1298
|
}
|
|
1136
1299
|
for (const ref of refs) {
|
|
@@ -1161,14 +1324,8 @@ var DotNetAdapter = class {
|
|
|
1161
1324
|
var dotNetAdapter = new DotNetAdapter();
|
|
1162
1325
|
|
|
1163
1326
|
// src/adapters/php.ts
|
|
1164
|
-
import { readFileSync as
|
|
1327
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
1165
1328
|
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
1329
|
function parseComposerLock(content) {
|
|
1173
1330
|
const versions = /* @__PURE__ */ new Map();
|
|
1174
1331
|
try {
|
|
@@ -1196,15 +1353,15 @@ var PhpAdapter = class {
|
|
|
1196
1353
|
const observations = [];
|
|
1197
1354
|
const root = workspaceRoot(workspace, options);
|
|
1198
1355
|
const seen = /* @__PURE__ */ new Set();
|
|
1199
|
-
const composerJsonPath = workspace.manifests.find((m) => m.includes("composer.json")) || (
|
|
1356
|
+
const composerJsonPath = workspace.manifests.find((m) => m.includes("composer.json")) || (fileExists(join4(root, "composer.json")) ? join4(root, "composer.json") : void 0);
|
|
1200
1357
|
if (!composerJsonPath) return [];
|
|
1201
1358
|
let content;
|
|
1202
1359
|
try {
|
|
1203
|
-
content =
|
|
1360
|
+
content = readFileSync3(composerJsonPath, "utf-8");
|
|
1204
1361
|
} catch {
|
|
1205
1362
|
return [];
|
|
1206
1363
|
}
|
|
1207
|
-
const manifestEv =
|
|
1364
|
+
const manifestEv = manifestEvidence(composerJsonPath);
|
|
1208
1365
|
let composerJson;
|
|
1209
1366
|
try {
|
|
1210
1367
|
composerJson = JSON.parse(content);
|
|
@@ -1215,9 +1372,9 @@ var PhpAdapter = class {
|
|
|
1215
1372
|
let lockVersions = /* @__PURE__ */ new Map();
|
|
1216
1373
|
let lockEv;
|
|
1217
1374
|
try {
|
|
1218
|
-
const lockContent =
|
|
1375
|
+
const lockContent = readFileSync3(lockPath, "utf-8");
|
|
1219
1376
|
lockVersions = parseComposerLock(lockContent);
|
|
1220
|
-
lockEv =
|
|
1377
|
+
lockEv = lockfileEvidence(lockPath);
|
|
1221
1378
|
} catch {
|
|
1222
1379
|
}
|
|
1223
1380
|
const sections = [
|
|
@@ -1254,26 +1411,12 @@ var PhpAdapter = class {
|
|
|
1254
1411
|
}
|
|
1255
1412
|
return observations;
|
|
1256
1413
|
}
|
|
1257
|
-
fileExists(filePath) {
|
|
1258
|
-
try {
|
|
1259
|
-
readFileSync6(filePath, "utf-8");
|
|
1260
|
-
return true;
|
|
1261
|
-
} catch {
|
|
1262
|
-
return false;
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
1414
|
};
|
|
1266
1415
|
var phpAdapter = new PhpAdapter();
|
|
1267
1416
|
|
|
1268
1417
|
// src/adapters/dart.ts
|
|
1269
|
-
import { readFileSync as
|
|
1418
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
1270
1419
|
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
1420
|
function parsePubspecYaml(content) {
|
|
1278
1421
|
const sections = /* @__PURE__ */ new Map();
|
|
1279
1422
|
let currentSection;
|
|
@@ -1301,6 +1444,14 @@ function parsePubspecYaml(content) {
|
|
|
1301
1444
|
}
|
|
1302
1445
|
const sec = sections.get(currentSection);
|
|
1303
1446
|
sec.set(currentName, constraint);
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
if (currentName && line.startsWith(" ")) {
|
|
1450
|
+
const sec = sections.get(currentSection);
|
|
1451
|
+
if (!sec) continue;
|
|
1452
|
+
if (/^sdk:\s*flutter\b/.test(trimmed)) sec.set(currentName, "sdk:flutter");
|
|
1453
|
+
else if (/^git:\s*/.test(trimmed)) sec.set(currentName, `git:${trimmed.slice(4).trim()}`);
|
|
1454
|
+
else if (/^path:\s*/.test(trimmed)) sec.set(currentName, `path:${trimmed.slice(5).trim()}`);
|
|
1304
1455
|
}
|
|
1305
1456
|
}
|
|
1306
1457
|
return sections;
|
|
@@ -1339,23 +1490,23 @@ var DartAdapter = class {
|
|
|
1339
1490
|
const observations = [];
|
|
1340
1491
|
const root = workspaceRoot(workspace, options);
|
|
1341
1492
|
const seen = /* @__PURE__ */ new Set();
|
|
1342
|
-
const pubspecPath = workspace.manifests.find((m) => m.includes("pubspec.yaml")) || (
|
|
1493
|
+
const pubspecPath = workspace.manifests.find((m) => m.includes("pubspec.yaml")) || (fileExists(join5(root, "pubspec.yaml")) ? join5(root, "pubspec.yaml") : void 0);
|
|
1343
1494
|
if (!pubspecPath) return [];
|
|
1344
1495
|
let content;
|
|
1345
1496
|
try {
|
|
1346
|
-
content =
|
|
1497
|
+
content = readFileSync4(pubspecPath, "utf-8");
|
|
1347
1498
|
} catch {
|
|
1348
1499
|
return [];
|
|
1349
1500
|
}
|
|
1350
|
-
const manifestEv =
|
|
1501
|
+
const manifestEv = manifestEvidence(pubspecPath);
|
|
1351
1502
|
const sections = parsePubspecYaml(content);
|
|
1352
1503
|
const lockPath = join5(root, "pubspec.lock");
|
|
1353
1504
|
let lockVersions = /* @__PURE__ */ new Map();
|
|
1354
1505
|
let lockEv;
|
|
1355
1506
|
try {
|
|
1356
|
-
const lockContent =
|
|
1507
|
+
const lockContent = readFileSync4(lockPath, "utf-8");
|
|
1357
1508
|
lockVersions = parsePubspecLock(lockContent);
|
|
1358
|
-
lockEv =
|
|
1509
|
+
lockEv = lockfileEvidence(lockPath);
|
|
1359
1510
|
} catch {
|
|
1360
1511
|
}
|
|
1361
1512
|
const sectionMapping = [
|
|
@@ -1369,7 +1520,7 @@ var DartAdapter = class {
|
|
|
1369
1520
|
for (const [name, constraint] of deps) {
|
|
1370
1521
|
if (seen.has(name)) continue;
|
|
1371
1522
|
seen.add(name);
|
|
1372
|
-
if (constraint === "*" || constraint.startsWith("{")) continue;
|
|
1523
|
+
if (constraint === "*" || constraint === "sdk:flutter" || constraint.startsWith("{")) continue;
|
|
1373
1524
|
const locked = lockVersions.get(name);
|
|
1374
1525
|
let status = "current";
|
|
1375
1526
|
let sourceType = "registry";
|
|
@@ -1405,32 +1556,54 @@ var DartAdapter = class {
|
|
|
1405
1556
|
}
|
|
1406
1557
|
return observations;
|
|
1407
1558
|
}
|
|
1408
|
-
fileExists(filePath) {
|
|
1409
|
-
try {
|
|
1410
|
-
readFileSync7(filePath, "utf-8");
|
|
1411
|
-
return true;
|
|
1412
|
-
} catch {
|
|
1413
|
-
return false;
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
1559
|
};
|
|
1417
1560
|
var dartAdapter = new DartAdapter();
|
|
1418
1561
|
|
|
1419
1562
|
// src/adapters/maven.ts
|
|
1420
|
-
import { readFileSync as
|
|
1421
|
-
function manifestEvidence8(path) {
|
|
1422
|
-
return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1423
|
-
}
|
|
1563
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
1424
1564
|
function parsePomDependencies(xml) {
|
|
1425
1565
|
const deps = [];
|
|
1566
|
+
const properties = /* @__PURE__ */ new Map();
|
|
1567
|
+
const propertiesBlock = /<properties>([\s\S]*?)<\/properties>/.exec(xml)?.[1] ?? "";
|
|
1568
|
+
const propertyRegex = /<([A-Za-z0-9_.-]+)>\s*([^<]+?)\s*<\/\1>/g;
|
|
1569
|
+
for (const propertyMatch of propertiesBlock.matchAll(propertyRegex)) {
|
|
1570
|
+
const key = propertyMatch[1];
|
|
1571
|
+
const value = propertyMatch[2];
|
|
1572
|
+
if (key && value) properties.set(key, value.trim());
|
|
1573
|
+
}
|
|
1574
|
+
const parentBlock = /<parent>([\s\S]*?)<\/parent>/.exec(xml)?.[1];
|
|
1575
|
+
if (parentBlock) {
|
|
1576
|
+
const parentVersion = xmlTagValue(parentBlock, "version");
|
|
1577
|
+
const parentGroup = xmlTagValue(parentBlock, "groupId");
|
|
1578
|
+
if (parentVersion) {
|
|
1579
|
+
properties.set("parent.version", parentVersion);
|
|
1580
|
+
properties.set("project.parent.version", parentVersion);
|
|
1581
|
+
}
|
|
1582
|
+
if (parentGroup) {
|
|
1583
|
+
properties.set("parent.groupId", parentGroup);
|
|
1584
|
+
properties.set("project.parent.groupId", parentGroup);
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
const managed = /* @__PURE__ */ new Map();
|
|
1588
|
+
const managementBlock = /<dependencyManagement>([\s\S]*?)<\/dependencyManagement>/.exec(xml)?.[1] ?? "";
|
|
1589
|
+
const managementRegex = /<dependency>\s*([\s\S]*?)<\/dependency>/g;
|
|
1590
|
+
for (const managementMatch of managementBlock.matchAll(managementRegex)) {
|
|
1591
|
+
const block = managementMatch[1];
|
|
1592
|
+
if (!block) continue;
|
|
1593
|
+
const groupId = xmlTagValue(block, "groupId");
|
|
1594
|
+
const artifactId = xmlTagValue(block, "artifactId");
|
|
1595
|
+
const version = xmlTagValue(block, "version");
|
|
1596
|
+
if (groupId && artifactId && version) managed.set(`${groupId}:${artifactId}`, version);
|
|
1597
|
+
}
|
|
1598
|
+
const directXml = xml.replace(/<dependencyManagement>[\s\S]*?<\/dependencyManagement>/g, "");
|
|
1426
1599
|
const depRegex = /<dependency>\s*([\s\S]*?)<\/dependency>/g;
|
|
1427
|
-
|
|
1428
|
-
while ((match = depRegex.exec(xml)) !== null) {
|
|
1600
|
+
for (const match of directXml.matchAll(depRegex)) {
|
|
1429
1601
|
const block = match[1];
|
|
1430
|
-
const groupId = block
|
|
1431
|
-
const artifactId = block
|
|
1432
|
-
const
|
|
1433
|
-
const
|
|
1602
|
+
const groupId = xmlTagValue(block, "groupId");
|
|
1603
|
+
const artifactId = xmlTagValue(block, "artifactId");
|
|
1604
|
+
const rawVersion = xmlTagValue(block, "version") ?? (groupId && artifactId ? managed.get(`${groupId}:${artifactId}`) : void 0);
|
|
1605
|
+
const version = rawVersion?.replace(/\$\{([^}]+)\}/g, (_whole, key) => properties.get(key) ?? `\${${key}}`);
|
|
1606
|
+
const scope = xmlTagValue(block, "scope");
|
|
1434
1607
|
if (groupId && artifactId) {
|
|
1435
1608
|
deps.push({ groupId, artifactId, version, scope });
|
|
1436
1609
|
}
|
|
@@ -1459,11 +1632,11 @@ var MavenAdapter = class {
|
|
|
1459
1632
|
if (!pomPath) return [];
|
|
1460
1633
|
let content;
|
|
1461
1634
|
try {
|
|
1462
|
-
content =
|
|
1635
|
+
content = readFileSync5(pomPath, "utf-8");
|
|
1463
1636
|
} catch {
|
|
1464
1637
|
return [];
|
|
1465
1638
|
}
|
|
1466
|
-
const manifestEv =
|
|
1639
|
+
const manifestEv = manifestEvidence(pomPath);
|
|
1467
1640
|
const deps = parsePomDependencies(content);
|
|
1468
1641
|
const seen = /* @__PURE__ */ new Set();
|
|
1469
1642
|
for (const dep of deps) {
|
|
@@ -1490,22 +1663,139 @@ var MavenAdapter = class {
|
|
|
1490
1663
|
};
|
|
1491
1664
|
var mavenAdapter = new MavenAdapter();
|
|
1492
1665
|
|
|
1493
|
-
// src/adapters/
|
|
1494
|
-
import {
|
|
1495
|
-
|
|
1496
|
-
|
|
1666
|
+
// src/adapters/gradle.ts
|
|
1667
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
1668
|
+
import { join as join6 } from "node:path";
|
|
1669
|
+
function scopeForConfiguration(configuration) {
|
|
1670
|
+
if (/test/i.test(configuration)) return "development";
|
|
1671
|
+
if (/compileOnly|annotationProcessor/i.test(configuration)) return "build";
|
|
1672
|
+
if (/runtimeOnly/i.test(configuration)) return "runtime";
|
|
1673
|
+
return "runtime";
|
|
1497
1674
|
}
|
|
1498
|
-
function
|
|
1499
|
-
|
|
1675
|
+
function parseVersionCatalog(content) {
|
|
1676
|
+
const versions = /* @__PURE__ */ new Map();
|
|
1677
|
+
const libraries = /* @__PURE__ */ new Map();
|
|
1678
|
+
let section = "";
|
|
1679
|
+
for (const raw of content.split("\n")) {
|
|
1680
|
+
const line = raw.replace(/#.*$/, "").trim();
|
|
1681
|
+
const header = /^\[([^\]]+)\]$/.exec(line);
|
|
1682
|
+
if (header) {
|
|
1683
|
+
section = header[1] ?? "";
|
|
1684
|
+
continue;
|
|
1685
|
+
}
|
|
1686
|
+
const entry = /^(?:"([^"]+)"|([\w.-]+))\s*=\s*(.+)$/.exec(line);
|
|
1687
|
+
if (!entry) continue;
|
|
1688
|
+
const key = entry[1] ?? entry[2];
|
|
1689
|
+
const value = entry[3];
|
|
1690
|
+
if (!key || !value) continue;
|
|
1691
|
+
if (section === "versions") {
|
|
1692
|
+
const version = /^["']([^"']+)["']/.exec(value)?.[1];
|
|
1693
|
+
if (version) versions.set(key, version);
|
|
1694
|
+
} else if (section === "libraries") {
|
|
1695
|
+
const module = /\bmodule\s*=\s*["']([^"']+)["']/.exec(value)?.[1] ?? (() => {
|
|
1696
|
+
const group = /\bgroup\s*=\s*["']([^"']+)["']/.exec(value)?.[1];
|
|
1697
|
+
const name = /\bname\s*=\s*["']([^"']+)["']/.exec(value)?.[1];
|
|
1698
|
+
return group && name ? `${group}:${name}` : void 0;
|
|
1699
|
+
})();
|
|
1700
|
+
if (!module) continue;
|
|
1701
|
+
const version = /\bversion\s*=\s*["']([^"']+)["']/.exec(value)?.[1];
|
|
1702
|
+
const ref = /\bversion\.ref\s*=\s*["']([^"']+)["']/.exec(value)?.[1];
|
|
1703
|
+
libraries.set(key.replace(/-/g, "."), `${module}:${version ?? (ref ? versions.get(ref) ?? "" : "")}`.replace(/:$/, ""));
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
return libraries;
|
|
1707
|
+
}
|
|
1708
|
+
function parseGradleManifest(content, catalog) {
|
|
1709
|
+
const deps = [];
|
|
1710
|
+
const coordinateRegex = /\b(implementation|api|compileOnly|runtimeOnly|testImplementation|testRuntimeOnly|annotationProcessor)\s*(?:\(|\s)\s*["']([^"']+)["']/g;
|
|
1711
|
+
for (const match of content.matchAll(coordinateRegex)) {
|
|
1712
|
+
const configuration = match[1];
|
|
1713
|
+
const value = match[2];
|
|
1714
|
+
if (!configuration || !value) continue;
|
|
1715
|
+
const [group, artifact, version] = value.split(":");
|
|
1716
|
+
if (!group || !artifact) continue;
|
|
1717
|
+
deps.push({ name: `${group}:${artifact}`, requested: version, scope: scopeForConfiguration(configuration) });
|
|
1718
|
+
}
|
|
1719
|
+
const aliasRegex = /\b(implementation|api|compileOnly|runtimeOnly|testImplementation|testRuntimeOnly)\s*\(\s*libs\.([\w.]+)\s*\)/g;
|
|
1720
|
+
for (const match of content.matchAll(aliasRegex)) {
|
|
1721
|
+
const configuration = match[1];
|
|
1722
|
+
const alias = match[2];
|
|
1723
|
+
if (!configuration || !alias) continue;
|
|
1724
|
+
const coordinate = catalog.get(alias);
|
|
1725
|
+
if (!coordinate) continue;
|
|
1726
|
+
const [group, artifact, version] = coordinate.split(":");
|
|
1727
|
+
if (!group || !artifact) continue;
|
|
1728
|
+
deps.push({ name: `${group}:${artifact}`, requested: version, scope: scopeForConfiguration(configuration) });
|
|
1729
|
+
}
|
|
1730
|
+
return deps;
|
|
1731
|
+
}
|
|
1732
|
+
function parseGradleLock(content) {
|
|
1733
|
+
const locked = /* @__PURE__ */ new Map();
|
|
1734
|
+
for (const raw of content.split("\n")) {
|
|
1735
|
+
const line = raw.replace(/#.*/, "").trim();
|
|
1736
|
+
const coordinate = /^([^:\s=]+):([^:\s=]+):([^=\s]+)(?:=.*)?$/.exec(line);
|
|
1737
|
+
if (coordinate?.[1] && coordinate[2] && coordinate[3]) locked.set(`${coordinate[1]}:${coordinate[2]}`, coordinate[3]);
|
|
1738
|
+
}
|
|
1739
|
+
return locked;
|
|
1500
1740
|
}
|
|
1741
|
+
var GradleAdapter = class {
|
|
1742
|
+
ecosystem = "gradle";
|
|
1743
|
+
async inventory(workspace, options) {
|
|
1744
|
+
const root = workspaceRoot(workspace, options);
|
|
1745
|
+
const manifestPath = workspace.manifests.find((path) => /build\.gradle(?:\.kts)?$/.test(path)) ?? (await fileExistsAsync(join6(root, "build.gradle.kts")) ? join6(root, "build.gradle.kts") : join6(root, "build.gradle"));
|
|
1746
|
+
if (!await fileExistsAsync(resolveIn(root, manifestPath))) return [];
|
|
1747
|
+
const catalogPath = join6(root, "gradle", "libs.versions.toml");
|
|
1748
|
+
const catalog = await fileExistsAsync(catalogPath) ? parseVersionCatalog(await readFile4(catalogPath, "utf8")) : /* @__PURE__ */ new Map();
|
|
1749
|
+
const direct = parseGradleManifest(await readFile4(resolveIn(root, manifestPath), "utf8"), catalog);
|
|
1750
|
+
const lockPath = workspace.lockfiles.find((path) => path.endsWith("gradle.lockfile")) ?? join6(root, "gradle.lockfile");
|
|
1751
|
+
const locked = await fileExistsAsync(resolveIn(root, lockPath)) ? parseGradleLock(await readFile4(resolveIn(root, lockPath), "utf8")) : /* @__PURE__ */ new Map();
|
|
1752
|
+
const manifestEv = manifestEvidence(resolveIn(root, manifestPath));
|
|
1753
|
+
const lockEv = locked.size > 0 ? lockfileEvidence(resolveIn(root, lockPath)) : void 0;
|
|
1754
|
+
const observations = [];
|
|
1755
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1756
|
+
const add = (name, requested, scope, isDirect) => {
|
|
1757
|
+
if (seen.has(name)) return;
|
|
1758
|
+
seen.add(name);
|
|
1759
|
+
const version = locked.get(name) ?? requested;
|
|
1760
|
+
const lockedVersion = locked.get(name);
|
|
1761
|
+
const evidence = isDirect ? [manifestEv] : [];
|
|
1762
|
+
if (lockEv && locked.has(name)) evidence.push(lockEv);
|
|
1763
|
+
observations.push({
|
|
1764
|
+
id: `dep-${workspace.id}-${name}`,
|
|
1765
|
+
workspaceId: workspace.id,
|
|
1766
|
+
purl: buildPurl({ type: "maven", name, ...version ? { version } : {} }),
|
|
1767
|
+
ecosystem: "gradle",
|
|
1768
|
+
name,
|
|
1769
|
+
sourceType: "registry",
|
|
1770
|
+
direct: isDirect,
|
|
1771
|
+
scope: isDirect ? scope : "transitive",
|
|
1772
|
+
...requested ? { requested } : {},
|
|
1773
|
+
...lockedVersion ? { locked: lockedVersion } : {},
|
|
1774
|
+
status: "current",
|
|
1775
|
+
evidence
|
|
1776
|
+
});
|
|
1777
|
+
};
|
|
1778
|
+
for (const dep of direct) add(dep.name, dep.requested, dep.scope, true);
|
|
1779
|
+
if (options.includeTransitive) {
|
|
1780
|
+
for (const [name] of locked) add(name, void 0, "transitive", false);
|
|
1781
|
+
}
|
|
1782
|
+
return observations;
|
|
1783
|
+
}
|
|
1784
|
+
};
|
|
1785
|
+
var gradleAdapter = new GradleAdapter();
|
|
1786
|
+
|
|
1787
|
+
// src/adapters/ruby.ts
|
|
1788
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
1501
1789
|
function parseGemfile(content) {
|
|
1502
1790
|
const gems = [];
|
|
1503
|
-
const gemRegex = /gem\s+['"]([^'"]+)['"](
|
|
1504
|
-
|
|
1505
|
-
while ((match = gemRegex.exec(content)) !== null) {
|
|
1791
|
+
const gemRegex = /gem\s+['"]([^'"]+)['"]([^\n]*)/g;
|
|
1792
|
+
for (const match of content.matchAll(gemRegex)) {
|
|
1506
1793
|
const name = match[1];
|
|
1507
1794
|
if (name === "rails" || name === "ruby") continue;
|
|
1508
|
-
|
|
1795
|
+
const tail = match[2] ?? "";
|
|
1796
|
+
const version = /^\s*,\s*['"]([^'"]+)['"]/.exec(tail)?.[1];
|
|
1797
|
+
const sourceType = /\b(?:git|github):/.test(tail) ? "git" : /\bpath:/.test(tail) ? "path" : "registry";
|
|
1798
|
+
gems.push({ name, version, sourceType });
|
|
1509
1799
|
}
|
|
1510
1800
|
return gems;
|
|
1511
1801
|
}
|
|
@@ -1514,7 +1804,11 @@ function parseGemfileLock(content) {
|
|
|
1514
1804
|
const lines = content.split("\n");
|
|
1515
1805
|
let inSpecs = false;
|
|
1516
1806
|
for (const line of lines) {
|
|
1517
|
-
if (line.
|
|
1807
|
+
if (/^(?:GEM|GIT|PATH)$/.test(line.trim())) {
|
|
1808
|
+
inSpecs = false;
|
|
1809
|
+
continue;
|
|
1810
|
+
}
|
|
1811
|
+
if (/^\s{2}specs:\s*$/.test(line)) {
|
|
1518
1812
|
inSpecs = true;
|
|
1519
1813
|
continue;
|
|
1520
1814
|
}
|
|
@@ -1539,11 +1833,11 @@ var RubyAdapter = class {
|
|
|
1539
1833
|
if (!gemfilePath) return [];
|
|
1540
1834
|
let content;
|
|
1541
1835
|
try {
|
|
1542
|
-
content =
|
|
1836
|
+
content = readFileSync6(gemfilePath, "utf-8");
|
|
1543
1837
|
} catch {
|
|
1544
1838
|
return [];
|
|
1545
1839
|
}
|
|
1546
|
-
const manifestEv =
|
|
1840
|
+
const manifestEv = manifestEvidence(gemfilePath);
|
|
1547
1841
|
const gems = parseGemfile(content);
|
|
1548
1842
|
const seen = /* @__PURE__ */ new Set();
|
|
1549
1843
|
const lockfilePath = workspace.lockfiles.find((l) => l.includes("Gemfile.lock"));
|
|
@@ -1551,9 +1845,9 @@ var RubyAdapter = class {
|
|
|
1551
1845
|
let lockEv;
|
|
1552
1846
|
if (lockfilePath) {
|
|
1553
1847
|
try {
|
|
1554
|
-
const lockContent =
|
|
1848
|
+
const lockContent = readFileSync6(lockfilePath, "utf-8");
|
|
1555
1849
|
lockVersions = parseGemfileLock(lockContent);
|
|
1556
|
-
lockEv =
|
|
1850
|
+
lockEv = lockfileEvidence(lockfilePath);
|
|
1557
1851
|
} catch {
|
|
1558
1852
|
}
|
|
1559
1853
|
}
|
|
@@ -1562,21 +1856,21 @@ var RubyAdapter = class {
|
|
|
1562
1856
|
seen.add(gem.name);
|
|
1563
1857
|
const locked = lockVersions.get(gem.name);
|
|
1564
1858
|
const version = locked ?? gem.version;
|
|
1565
|
-
const purl = version ? buildPurl({ type: "gem", name: gem.name, version }) : buildPurl({ type: "gem", name: gem.name });
|
|
1859
|
+
const purl = gem.sourceType === "registry" && version ? buildPurl({ type: "gem", name: gem.name, version }) : gem.sourceType === "registry" ? buildPurl({ type: "gem", name: gem.name }) : void 0;
|
|
1566
1860
|
const evidence = [manifestEv];
|
|
1567
1861
|
if (lockEv && locked) evidence.push(lockEv);
|
|
1568
1862
|
observations.push({
|
|
1569
1863
|
id: `dep-${workspace.id}-${gem.name}`,
|
|
1570
1864
|
workspaceId: workspace.id,
|
|
1571
|
-
purl,
|
|
1865
|
+
...purl ? { purl } : {},
|
|
1572
1866
|
ecosystem: "ruby",
|
|
1573
1867
|
name: gem.name,
|
|
1574
|
-
sourceType:
|
|
1868
|
+
sourceType: gem.sourceType,
|
|
1575
1869
|
direct: true,
|
|
1576
1870
|
scope: "runtime",
|
|
1577
1871
|
...gem.version ? { requested: gem.version } : {},
|
|
1578
1872
|
...locked ? { locked } : {},
|
|
1579
|
-
status: "current",
|
|
1873
|
+
status: gem.sourceType === "git" ? "git_dependency" : gem.sourceType === "path" ? "local_path" : "current",
|
|
1580
1874
|
evidence
|
|
1581
1875
|
});
|
|
1582
1876
|
}
|
|
@@ -1585,28 +1879,101 @@ var RubyAdapter = class {
|
|
|
1585
1879
|
};
|
|
1586
1880
|
var rubyAdapter = new RubyAdapter();
|
|
1587
1881
|
|
|
1588
|
-
// src/adapters/
|
|
1589
|
-
import { readFileSync as
|
|
1590
|
-
|
|
1591
|
-
|
|
1882
|
+
// src/adapters/swift.ts
|
|
1883
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
1884
|
+
import { basename, join as join7 } from "node:path";
|
|
1885
|
+
function identityFromLocation(location) {
|
|
1886
|
+
return basename(location.replace(/[\\/]$/, "")).replace(/\.git$/i, "").toLowerCase();
|
|
1592
1887
|
}
|
|
1593
|
-
function
|
|
1594
|
-
|
|
1888
|
+
function parsePackageSwift(content) {
|
|
1889
|
+
const deps = [];
|
|
1890
|
+
const packageRegex = /\.package\s*\(\s*(?:name:\s*["'][^"']+["'],\s*)?(url|path):\s*["']([^"']+)["']\s*(?:,\s*(?:from|exact|branch|revision):\s*["']([^"']+)["'])?\s*\)/g;
|
|
1891
|
+
for (const match of content.matchAll(packageRegex)) {
|
|
1892
|
+
const location = match[2];
|
|
1893
|
+
if (!location) continue;
|
|
1894
|
+
deps.push({
|
|
1895
|
+
identity: identityFromLocation(location),
|
|
1896
|
+
requested: match[3],
|
|
1897
|
+
sourceType: match[1] === "path" ? "path" : "git"
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
return deps;
|
|
1901
|
+
}
|
|
1902
|
+
function parsePackageResolved(content) {
|
|
1903
|
+
const pins = /* @__PURE__ */ new Map();
|
|
1904
|
+
try {
|
|
1905
|
+
const json = JSON.parse(content);
|
|
1906
|
+
for (const pin of json.pins ?? json.object?.pins ?? []) {
|
|
1907
|
+
const identity = (pin.identity ?? pin.package ?? (pin.location ? identityFromLocation(pin.location) : void 0) ?? (pin.repositoryURL ? identityFromLocation(pin.repositoryURL) : void 0))?.toLowerCase();
|
|
1908
|
+
if (identity) pins.set(identity, { ...pin.state?.version ? { version: pin.state.version } : {}, ...pin.state?.revision ? { revision: pin.state.revision } : {} });
|
|
1909
|
+
}
|
|
1910
|
+
} catch {
|
|
1911
|
+
}
|
|
1912
|
+
return pins;
|
|
1595
1913
|
}
|
|
1914
|
+
var SwiftAdapter = class {
|
|
1915
|
+
ecosystem = "swift";
|
|
1916
|
+
async inventory(workspace, options) {
|
|
1917
|
+
const root = workspaceRoot(workspace, options);
|
|
1918
|
+
const manifestPath = workspace.manifests.find((path) => path.endsWith("Package.swift")) ?? join7(root, "Package.swift");
|
|
1919
|
+
if (!fileExists(manifestPath)) return [];
|
|
1920
|
+
const direct = parsePackageSwift(readFileSync7(manifestPath, "utf8"));
|
|
1921
|
+
const resolvedPath = workspace.lockfiles.find((path) => path.endsWith("Package.resolved")) ?? join7(root, "Package.resolved");
|
|
1922
|
+
const pins = fileExists(resolvedPath) ? parsePackageResolved(readFileSync7(resolvedPath, "utf8")) : /* @__PURE__ */ new Map();
|
|
1923
|
+
const manifestEv = manifestEvidence(manifestPath);
|
|
1924
|
+
const lockEv = pins.size > 0 ? lockfileEvidence(resolvedPath) : void 0;
|
|
1925
|
+
const observations = [];
|
|
1926
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1927
|
+
const add = (identity, requested, sourceType, isDirect) => {
|
|
1928
|
+
if (seen.has(identity)) return;
|
|
1929
|
+
seen.add(identity);
|
|
1930
|
+
const pin = pins.get(identity);
|
|
1931
|
+
const version = pin?.version ?? pin?.revision ?? requested;
|
|
1932
|
+
const evidence = isDirect ? [manifestEv] : [];
|
|
1933
|
+
if (lockEv && pin) evidence.push(lockEv);
|
|
1934
|
+
observations.push({
|
|
1935
|
+
id: `dep-${workspace.id}-${identity}`,
|
|
1936
|
+
workspaceId: workspace.id,
|
|
1937
|
+
...sourceType === "git" ? { purl: buildPurl({ type: "swift", name: identity, ...version ? { version } : {} }) } : {},
|
|
1938
|
+
ecosystem: "swift",
|
|
1939
|
+
name: identity,
|
|
1940
|
+
sourceType,
|
|
1941
|
+
direct: isDirect,
|
|
1942
|
+
scope: isDirect ? "runtime" : "transitive",
|
|
1943
|
+
...requested ? { requested } : {},
|
|
1944
|
+
...pin?.version ? { locked: pin.version } : pin?.revision ? { locked: pin.revision } : {},
|
|
1945
|
+
status: sourceType === "path" ? "local_path" : "git_dependency",
|
|
1946
|
+
evidence
|
|
1947
|
+
});
|
|
1948
|
+
};
|
|
1949
|
+
for (const dep of direct) add(dep.identity, dep.requested, dep.sourceType, true);
|
|
1950
|
+
if (options.includeTransitive) {
|
|
1951
|
+
for (const [identity] of pins) add(identity, void 0, "git", false);
|
|
1952
|
+
}
|
|
1953
|
+
return observations;
|
|
1954
|
+
}
|
|
1955
|
+
};
|
|
1956
|
+
var swiftAdapter = new SwiftAdapter();
|
|
1957
|
+
|
|
1958
|
+
// src/adapters/elixir.ts
|
|
1959
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
1596
1960
|
function parseMixExsDeps(content) {
|
|
1597
1961
|
const deps = [];
|
|
1598
|
-
const depRegex = /\{:(\w+),\s*
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1962
|
+
const depRegex = /\{:(\w+),\s*([^}]+)\}/g;
|
|
1963
|
+
for (const match of content.matchAll(depRegex)) {
|
|
1964
|
+
const name = match[1];
|
|
1965
|
+
const value = match[2];
|
|
1966
|
+
if (!name || !value) continue;
|
|
1967
|
+
const version = /^\s*["']([^"']+)["']/.exec(value)?.[1];
|
|
1968
|
+
const sourceType = /\bgit:/.test(value) ? "git" : /\bpath:/.test(value) ? "path" : "registry";
|
|
1969
|
+
deps.push({ name, version, sourceType });
|
|
1602
1970
|
}
|
|
1603
1971
|
return deps;
|
|
1604
1972
|
}
|
|
1605
1973
|
function parseMixLock(content) {
|
|
1606
1974
|
const versions = /* @__PURE__ */ new Map();
|
|
1607
|
-
const lockRegex =
|
|
1608
|
-
|
|
1609
|
-
while ((match = lockRegex.exec(content)) !== null) {
|
|
1975
|
+
const lockRegex = /["']([\w-]+)["']\s*=>\s*\{:hex,\s*:[\w-]+,\s*["']([^"']+)["']/g;
|
|
1976
|
+
for (const match of content.matchAll(lockRegex)) {
|
|
1610
1977
|
versions.set(match[1], match[2]);
|
|
1611
1978
|
}
|
|
1612
1979
|
return versions;
|
|
@@ -1619,11 +1986,11 @@ var ElixirAdapter = class {
|
|
|
1619
1986
|
if (!mixExsPath) return [];
|
|
1620
1987
|
let content;
|
|
1621
1988
|
try {
|
|
1622
|
-
content =
|
|
1989
|
+
content = readFileSync8(mixExsPath, "utf-8");
|
|
1623
1990
|
} catch {
|
|
1624
1991
|
return [];
|
|
1625
1992
|
}
|
|
1626
|
-
const manifestEv =
|
|
1993
|
+
const manifestEv = manifestEvidence(mixExsPath);
|
|
1627
1994
|
const deps = parseMixExsDeps(content);
|
|
1628
1995
|
const seen = /* @__PURE__ */ new Set();
|
|
1629
1996
|
const lockfilePath = workspace.lockfiles.find((l) => l.includes("mix.lock"));
|
|
@@ -1631,9 +1998,9 @@ var ElixirAdapter = class {
|
|
|
1631
1998
|
let lockEv;
|
|
1632
1999
|
if (lockfilePath) {
|
|
1633
2000
|
try {
|
|
1634
|
-
const lockContent =
|
|
2001
|
+
const lockContent = readFileSync8(lockfilePath, "utf-8");
|
|
1635
2002
|
lockVersions = parseMixLock(lockContent);
|
|
1636
|
-
lockEv =
|
|
2003
|
+
lockEv = lockfileEvidence(lockfilePath);
|
|
1637
2004
|
} catch {
|
|
1638
2005
|
}
|
|
1639
2006
|
}
|
|
@@ -1642,21 +2009,21 @@ var ElixirAdapter = class {
|
|
|
1642
2009
|
seen.add(dep.name);
|
|
1643
2010
|
const locked = lockVersions.get(dep.name);
|
|
1644
2011
|
const version = locked ?? dep.version;
|
|
1645
|
-
const purl = version ? buildPurl({ type: "hex", name: dep.name, version }) : buildPurl({ type: "hex", name: dep.name });
|
|
2012
|
+
const purl = dep.sourceType === "registry" && version ? buildPurl({ type: "hex", name: dep.name, version }) : dep.sourceType === "registry" ? buildPurl({ type: "hex", name: dep.name }) : void 0;
|
|
1646
2013
|
const evidence = [manifestEv];
|
|
1647
2014
|
if (lockEv && locked) evidence.push(lockEv);
|
|
1648
2015
|
observations.push({
|
|
1649
2016
|
id: `dep-${workspace.id}-${dep.name}`,
|
|
1650
2017
|
workspaceId: workspace.id,
|
|
1651
|
-
purl,
|
|
2018
|
+
...purl ? { purl } : {},
|
|
1652
2019
|
ecosystem: "elixir",
|
|
1653
2020
|
name: dep.name,
|
|
1654
|
-
sourceType:
|
|
2021
|
+
sourceType: dep.sourceType,
|
|
1655
2022
|
direct: true,
|
|
1656
2023
|
scope: "runtime",
|
|
1657
2024
|
...dep.version ? { requested: dep.version } : {},
|
|
1658
2025
|
...locked ? { locked } : {},
|
|
1659
|
-
status: "current",
|
|
2026
|
+
status: dep.sourceType === "git" ? "git_dependency" : dep.sourceType === "path" ? "local_path" : "current",
|
|
1660
2027
|
evidence
|
|
1661
2028
|
});
|
|
1662
2029
|
}
|
|
@@ -1666,10 +2033,7 @@ var ElixirAdapter = class {
|
|
|
1666
2033
|
var elixirAdapter = new ElixirAdapter();
|
|
1667
2034
|
|
|
1668
2035
|
// src/adapters/cpp.ts
|
|
1669
|
-
import { readFileSync as
|
|
1670
|
-
function manifestEvidence11(path) {
|
|
1671
|
-
return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1672
|
-
}
|
|
2036
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
1673
2037
|
function parseConanTxt(content) {
|
|
1674
2038
|
const deps = [];
|
|
1675
2039
|
const requiresMatch = /\[requires\]\s*\n([\s\S]*?)(?:\[|$)/;
|
|
@@ -1710,11 +2074,11 @@ var CppAdapter = class {
|
|
|
1710
2074
|
for (const manifestPath of workspace.manifests) {
|
|
1711
2075
|
let content;
|
|
1712
2076
|
try {
|
|
1713
|
-
content =
|
|
2077
|
+
content = readFileSync9(manifestPath, "utf-8");
|
|
1714
2078
|
} catch {
|
|
1715
2079
|
continue;
|
|
1716
2080
|
}
|
|
1717
|
-
const manifestEv =
|
|
2081
|
+
const manifestEv = manifestEvidence(manifestPath);
|
|
1718
2082
|
let deps = [];
|
|
1719
2083
|
if (manifestPath.includes("conanfile")) {
|
|
1720
2084
|
deps = parseConanTxt(content);
|
|
@@ -1790,6 +2154,108 @@ function diffSnapshots(oldSnapshot, newSnapshot) {
|
|
|
1790
2154
|
return { added, removed, changed };
|
|
1791
2155
|
}
|
|
1792
2156
|
|
|
2157
|
+
// src/trend.ts
|
|
2158
|
+
function median(values) {
|
|
2159
|
+
if (!values.length) return void 0;
|
|
2160
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
2161
|
+
const middle = Math.floor(sorted.length / 2);
|
|
2162
|
+
const upper = sorted[middle];
|
|
2163
|
+
if (upper === void 0) return void 0;
|
|
2164
|
+
if (sorted.length % 2) return upper;
|
|
2165
|
+
const lower = sorted[middle - 1];
|
|
2166
|
+
return lower === void 0 ? upper : (lower + upper) / 2;
|
|
2167
|
+
}
|
|
2168
|
+
var TrendStore = class {
|
|
2169
|
+
constructor(source) {
|
|
2170
|
+
this.source = source;
|
|
2171
|
+
}
|
|
2172
|
+
source;
|
|
2173
|
+
analyze(projectId, limit = 100) {
|
|
2174
|
+
const snapshots = this.source.listSnapshots(projectId, limit).sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt));
|
|
2175
|
+
const versionHistory = /* @__PURE__ */ new Map();
|
|
2176
|
+
const vulnerabilityDurations = [];
|
|
2177
|
+
const openVulnerabilities = /* @__PURE__ */ new Map();
|
|
2178
|
+
for (const snapshot of snapshots) {
|
|
2179
|
+
const at = Date.parse(snapshot.createdAt);
|
|
2180
|
+
const active = /* @__PURE__ */ new Set();
|
|
2181
|
+
for (const dependency of snapshot.dependencies) {
|
|
2182
|
+
const key = `${dependency.ecosystem}:${dependency.name}`;
|
|
2183
|
+
active.add(key);
|
|
2184
|
+
const vulnerable = dependency.status === "vulnerable";
|
|
2185
|
+
const history = versionHistory.get(key) ?? [];
|
|
2186
|
+
history.push({ at, version: dependency.locked ?? dependency.installed ?? dependency.requested, vulnerable, name: dependency.name, ecosystem: dependency.ecosystem });
|
|
2187
|
+
versionHistory.set(key, history);
|
|
2188
|
+
if (vulnerable && !openVulnerabilities.has(key)) openVulnerabilities.set(key, at);
|
|
2189
|
+
if (!vulnerable && openVulnerabilities.has(key)) {
|
|
2190
|
+
const openedAt = openVulnerabilities.get(key);
|
|
2191
|
+
if (openedAt !== void 0) vulnerabilityDurations.push(Math.max(0, at - openedAt));
|
|
2192
|
+
openVulnerabilities.delete(key);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
for (const key of openVulnerabilities.keys()) {
|
|
2196
|
+
if (!active.has(key)) {
|
|
2197
|
+
const openedAt = openVulnerabilities.get(key);
|
|
2198
|
+
if (openedAt !== void 0) vulnerabilityDurations.push(Math.max(0, at - openedAt));
|
|
2199
|
+
openVulnerabilities.delete(key);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
const dependencies = [];
|
|
2204
|
+
for (const [key, history] of versionHistory) {
|
|
2205
|
+
const first = history[0];
|
|
2206
|
+
const latest = history.at(-1);
|
|
2207
|
+
if (!first || !latest) continue;
|
|
2208
|
+
let versionChanges = 0;
|
|
2209
|
+
let sameVersionSince = first.at;
|
|
2210
|
+
for (let index = 1; index < history.length; index++) {
|
|
2211
|
+
const current = history[index];
|
|
2212
|
+
const previous = history[index - 1];
|
|
2213
|
+
if (current && previous && current.version !== previous.version) {
|
|
2214
|
+
versionChanges++;
|
|
2215
|
+
sameVersionSince = current.at;
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
const spanDays = Math.max(1, (latest.at - first.at) / 864e5);
|
|
2219
|
+
dependencies.push({
|
|
2220
|
+
key,
|
|
2221
|
+
name: latest.name,
|
|
2222
|
+
ecosystem: latest.ecosystem,
|
|
2223
|
+
...latest.version ? { currentVersion: latest.version } : {},
|
|
2224
|
+
lockedVersionAgeMs: Math.max(0, latest.at - sameVersionSince),
|
|
2225
|
+
versionChanges,
|
|
2226
|
+
upgradeVelocityPerDay: versionChanges / spanDays
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
dependencies.sort((a, b) => a.key.localeCompare(b.key));
|
|
2230
|
+
const halfLife = median(vulnerabilityDurations);
|
|
2231
|
+
const firstSnapshot = snapshots[0];
|
|
2232
|
+
const lastSnapshot = snapshots.at(-1);
|
|
2233
|
+
return {
|
|
2234
|
+
projectId,
|
|
2235
|
+
...firstSnapshot && lastSnapshot ? { from: firstSnapshot.createdAt, to: lastSnapshot.createdAt } : {},
|
|
2236
|
+
snapshots: snapshots.length,
|
|
2237
|
+
dependencies,
|
|
2238
|
+
...halfLife !== void 0 ? { vulnerabilityHalfLifeMs: halfLife } : {},
|
|
2239
|
+
points: snapshots.map((snapshot) => ({
|
|
2240
|
+
snapshotId: snapshot.id,
|
|
2241
|
+
createdAt: snapshot.createdAt,
|
|
2242
|
+
dependencies: snapshot.dependencies.length,
|
|
2243
|
+
outdated: snapshot.dependencies.filter((dependency) => dependency.status.startsWith("update_available")).length,
|
|
2244
|
+
vulnerable: snapshot.dependencies.filter((dependency) => dependency.status === "vulnerable").length
|
|
2245
|
+
}))
|
|
2246
|
+
};
|
|
2247
|
+
}
|
|
2248
|
+
};
|
|
2249
|
+
function renderTrendMarkdown(report) {
|
|
2250
|
+
const lines = ["# TechStack Trend", "", `**Snapshots:** ${report.snapshots}`];
|
|
2251
|
+
if (report.vulnerabilityHalfLifeMs !== void 0) lines.push(`**Vulnerability half-life:** ${(report.vulnerabilityHalfLifeMs / 864e5).toFixed(1)} days`);
|
|
2252
|
+
lines.push("", "| Dependency | Version | Age (days) | Changes | Velocity/day |", "|---|---|---:|---:|---:|");
|
|
2253
|
+
for (const dependency of report.dependencies) {
|
|
2254
|
+
lines.push(`| ${dependency.ecosystem}:${dependency.name} | ${dependency.currentVersion ?? "\u2014"} | ${(dependency.lockedVersionAgeMs / 864e5).toFixed(1)} | ${dependency.versionChanges} | ${dependency.upgradeVelocityPerDay.toFixed(3)} |`);
|
|
2255
|
+
}
|
|
2256
|
+
return lines.join("\n");
|
|
2257
|
+
}
|
|
2258
|
+
|
|
1793
2259
|
// src/sbom.ts
|
|
1794
2260
|
function toSpdx(snapshot) {
|
|
1795
2261
|
const created = snapshot.createdAt;
|
|
@@ -1832,6 +2298,47 @@ function toCycloneDX(snapshot) {
|
|
|
1832
2298
|
}
|
|
1833
2299
|
|
|
1834
2300
|
// src/remediation.ts
|
|
2301
|
+
var LANGUAGE_BY_ECOSYSTEM = {
|
|
2302
|
+
python: "python",
|
|
2303
|
+
rust: "rust",
|
|
2304
|
+
go: "go",
|
|
2305
|
+
php: "php",
|
|
2306
|
+
dotnet: "csharp"
|
|
2307
|
+
};
|
|
2308
|
+
var EXECUTABLE_ECOSYSTEMS = /* @__PURE__ */ new Set(["npm", ...Object.keys(LANGUAGE_BY_ECOSYSTEM)]);
|
|
2309
|
+
function versionedPackageName(operation) {
|
|
2310
|
+
const version = operation.targetVersion;
|
|
2311
|
+
if (!version) return operation.dependencyName;
|
|
2312
|
+
switch (operation.ecosystem) {
|
|
2313
|
+
case "python":
|
|
2314
|
+
return `${operation.dependencyName}==${version}`;
|
|
2315
|
+
case "php":
|
|
2316
|
+
return `${operation.dependencyName}:${version}`;
|
|
2317
|
+
default:
|
|
2318
|
+
return `${operation.dependencyName}@${version}`;
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
function toLanguagePackageInput(operation, workspace) {
|
|
2322
|
+
if (operation.action === "replace" || operation.action === "investigate") {
|
|
2323
|
+
throw new Error(`${operation.action} requires a manual package choice`);
|
|
2324
|
+
}
|
|
2325
|
+
if (!EXECUTABLE_ECOSYSTEMS.has(operation.ecosystem)) {
|
|
2326
|
+
throw new Error(`Automated remediation is not supported for ${operation.ecosystem}`);
|
|
2327
|
+
}
|
|
2328
|
+
const language = LANGUAGE_BY_ECOSYSTEM[operation.ecosystem];
|
|
2329
|
+
const base = {
|
|
2330
|
+
...workspace ? { workspace } : {},
|
|
2331
|
+
...language ? { language } : {},
|
|
2332
|
+
allowScripts: false
|
|
2333
|
+
};
|
|
2334
|
+
if (operation.action === "remove") {
|
|
2335
|
+
if (operation.ecosystem === "go") {
|
|
2336
|
+
return { ...base, operation: "add", names: [`${operation.dependencyName}@none`] };
|
|
2337
|
+
}
|
|
2338
|
+
return { ...base, operation: "remove", names: [operation.dependencyName] };
|
|
2339
|
+
}
|
|
2340
|
+
return { ...base, operation: "add", names: [versionedPackageName(operation)] };
|
|
2341
|
+
}
|
|
1835
2342
|
function suggestCommand(ecosystem, name, action, targetVersion) {
|
|
1836
2343
|
const ver = targetVersion ? `@${targetVersion}` : "@latest";
|
|
1837
2344
|
switch (ecosystem) {
|
|
@@ -1951,14 +2458,152 @@ function renderPlanMarkdown(plan) {
|
|
|
1951
2458
|
}
|
|
1952
2459
|
return lines.join("\n");
|
|
1953
2460
|
}
|
|
2461
|
+
async function applyPlan(plan, options = {}) {
|
|
2462
|
+
const dryRun = options.dryRun !== false;
|
|
2463
|
+
const execute = options.execute;
|
|
2464
|
+
if (!dryRun && !execute) throw new Error("An execute strategy is required when dryRun is false");
|
|
2465
|
+
const results = [];
|
|
2466
|
+
for (const [index, item] of plan.items.entries()) {
|
|
2467
|
+
if (options.signal?.aborted) throw new DOMException("Remediation cancelled", "AbortError");
|
|
2468
|
+
if (dryRun) {
|
|
2469
|
+
results.push({ dependencyName: item.dependencyName, status: "planned" });
|
|
2470
|
+
continue;
|
|
2471
|
+
}
|
|
2472
|
+
const approved = options.approve ? await options.approve(item, index) : false;
|
|
2473
|
+
if (!approved) {
|
|
2474
|
+
results.push({
|
|
2475
|
+
dependencyName: item.dependencyName,
|
|
2476
|
+
status: "skipped",
|
|
2477
|
+
detail: "Not approved"
|
|
2478
|
+
});
|
|
2479
|
+
continue;
|
|
2480
|
+
}
|
|
2481
|
+
try {
|
|
2482
|
+
if (!execute) throw new Error("An execute strategy is required when dryRun is false");
|
|
2483
|
+
const response = await execute({
|
|
2484
|
+
ecosystem: item.ecosystem,
|
|
2485
|
+
workspaceId: item.workspaceId,
|
|
2486
|
+
dependencyName: item.dependencyName,
|
|
2487
|
+
action: item.action,
|
|
2488
|
+
...item.targetVersion ? { targetVersion: item.targetVersion } : {}
|
|
2489
|
+
});
|
|
2490
|
+
results.push({
|
|
2491
|
+
dependencyName: item.dependencyName,
|
|
2492
|
+
status: "applied",
|
|
2493
|
+
...response?.detail ? { detail: response.detail } : {}
|
|
2494
|
+
});
|
|
2495
|
+
} catch (error) {
|
|
2496
|
+
results.push({
|
|
2497
|
+
dependencyName: item.dependencyName,
|
|
2498
|
+
status: "failed",
|
|
2499
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
return { dryRun, items: results };
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
// src/registry/http-fetch.ts
|
|
2507
|
+
import { get as httpGet, request as httpRequest } from "node:http";
|
|
2508
|
+
import { get as httpsGet, request as httpsRequest } from "node:https";
|
|
2509
|
+
var DEFAULT_MAX_BODY_BYTES = 64 * 1024 * 1024;
|
|
2510
|
+
function retryAfterMs(headers, now = Date.now()) {
|
|
2511
|
+
const value = Array.isArray(headers["retry-after"]) ? headers["retry-after"][0] : headers["retry-after"];
|
|
2512
|
+
if (!value) return void 0;
|
|
2513
|
+
const seconds = Number(value);
|
|
2514
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
2515
|
+
const date = Date.parse(value);
|
|
2516
|
+
return Number.isFinite(date) ? Math.max(0, date - now) : void 0;
|
|
2517
|
+
}
|
|
2518
|
+
function delay(ms, signal) {
|
|
2519
|
+
if (signal?.aborted) return Promise.reject(new DOMException("Request aborted", "AbortError"));
|
|
2520
|
+
return new Promise((resolve3, reject) => {
|
|
2521
|
+
const onAbort = () => {
|
|
2522
|
+
clearTimeout(timer);
|
|
2523
|
+
reject(new DOMException("Request aborted", "AbortError"));
|
|
2524
|
+
};
|
|
2525
|
+
const timer = setTimeout(() => {
|
|
2526
|
+
signal?.removeEventListener("abort", onAbort);
|
|
2527
|
+
resolve3();
|
|
2528
|
+
}, ms);
|
|
2529
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2530
|
+
});
|
|
2531
|
+
}
|
|
2532
|
+
function requestOnce(options) {
|
|
2533
|
+
return new Promise((resolve3, reject) => {
|
|
2534
|
+
const method = options.method ?? "GET";
|
|
2535
|
+
const requestOptions = {
|
|
2536
|
+
hostname: options.hostname,
|
|
2537
|
+
path: options.path,
|
|
2538
|
+
method,
|
|
2539
|
+
headers: options.headers,
|
|
2540
|
+
signal: options.signal,
|
|
2541
|
+
timeout: options.timeoutMs ?? 15e3
|
|
2542
|
+
};
|
|
2543
|
+
const isHttp = options.hostname === "localhost" || options.hostname === "127.0.0.1";
|
|
2544
|
+
const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
2545
|
+
const callback = (response) => {
|
|
2546
|
+
let body = "";
|
|
2547
|
+
response.setEncoding?.("utf8");
|
|
2548
|
+
response.on("data", (chunk) => {
|
|
2549
|
+
body += chunk.toString();
|
|
2550
|
+
if (body.length > maxBodyBytes) {
|
|
2551
|
+
response.destroy();
|
|
2552
|
+
reject(
|
|
2553
|
+
new Error(
|
|
2554
|
+
`Response body exceeded ${maxBodyBytes} bytes for ${options.hostname}${options.path}`
|
|
2555
|
+
)
|
|
2556
|
+
);
|
|
2557
|
+
}
|
|
2558
|
+
});
|
|
2559
|
+
response.on("end", () => resolve3({
|
|
2560
|
+
statusCode: response.statusCode ?? 0,
|
|
2561
|
+
headers: response.headers,
|
|
2562
|
+
body
|
|
2563
|
+
}));
|
|
2564
|
+
};
|
|
2565
|
+
const request = method === "GET" ? (isHttp ? httpGet : httpsGet)(requestOptions, callback) : (isHttp ? httpRequest : httpsRequest)(requestOptions, callback);
|
|
2566
|
+
request.on("error", reject);
|
|
2567
|
+
request.on("timeout", () => {
|
|
2568
|
+
request.destroy();
|
|
2569
|
+
reject(new Error(`Request timeout for ${options.hostname}${options.path}`));
|
|
2570
|
+
});
|
|
2571
|
+
if (options.body) request.write(options.body);
|
|
2572
|
+
request.end();
|
|
2573
|
+
});
|
|
2574
|
+
}
|
|
2575
|
+
async function requestWithRetry(options) {
|
|
2576
|
+
const maxAttempts = Math.max(1, options.maxAttempts ?? 3);
|
|
2577
|
+
let lastError;
|
|
2578
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
2579
|
+
try {
|
|
2580
|
+
const response = await requestOnce(options);
|
|
2581
|
+
const retryable = response.statusCode === 429 || response.statusCode >= 500;
|
|
2582
|
+
if (!retryable || attempt === maxAttempts - 1) return response;
|
|
2583
|
+
const serverDelay = retryAfterMs(response.headers);
|
|
2584
|
+
const exponential = (options.baseBackoffMs ?? 1e3) * 2 ** attempt;
|
|
2585
|
+
await delay(serverDelay ?? exponential, options.signal);
|
|
2586
|
+
} catch (error) {
|
|
2587
|
+
if (options.signal?.aborted || error instanceof DOMException && error.name === "AbortError") throw error;
|
|
2588
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
2589
|
+
if (attempt < maxAttempts - 1) {
|
|
2590
|
+
await delay((options.baseBackoffMs ?? 1e3) * 2 ** attempt, options.signal);
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
throw lastError ?? new Error(`Request failed for ${options.hostname}${options.path}`);
|
|
2595
|
+
}
|
|
2596
|
+
function parseJsonResponse(response, source) {
|
|
2597
|
+
try {
|
|
2598
|
+
return JSON.parse(response.body);
|
|
2599
|
+
} catch {
|
|
2600
|
+
throw new Error(`Invalid JSON response from ${source}`);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
1954
2603
|
|
|
1955
2604
|
// src/registry/client.ts
|
|
1956
|
-
import { get as httpsGet } from "node:https";
|
|
1957
|
-
import { get as httpGet } from "node:http";
|
|
1958
2605
|
var DEFAULT_TTL_MS = 10 * 60 * 1e3;
|
|
1959
2606
|
var MAX_CONCURRENCY_PER_HOST = 3;
|
|
1960
|
-
var MAX_RETRIES = 3;
|
|
1961
|
-
var BASE_BACKOFF_MS = 1e3;
|
|
1962
2607
|
var registryCache = /* @__PURE__ */ new Map();
|
|
1963
2608
|
var hostConcurrency = /* @__PURE__ */ new Map();
|
|
1964
2609
|
function getCacheKey(host, path) {
|
|
@@ -2006,54 +2651,6 @@ function releaseHostSlot(host) {
|
|
|
2006
2651
|
}
|
|
2007
2652
|
}
|
|
2008
2653
|
}
|
|
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
2654
|
function parseNpmPackument(json, name) {
|
|
2058
2655
|
const latestVersion = json["dist-tags"]?.["latest"];
|
|
2059
2656
|
let deprecated;
|
|
@@ -2208,6 +2805,44 @@ var ECOSYSTEM_FETCHERS = {
|
|
|
2208
2805
|
}
|
|
2209
2806
|
}
|
|
2210
2807
|
};
|
|
2808
|
+
var RegistryNotFoundError = class extends Error {
|
|
2809
|
+
constructor(statusCode, packageName) {
|
|
2810
|
+
super(`Registry package not found or inaccessible (${statusCode}): ${packageName}`);
|
|
2811
|
+
this.statusCode = statusCode;
|
|
2812
|
+
this.packageName = packageName;
|
|
2813
|
+
this.name = "RegistryNotFoundError";
|
|
2814
|
+
}
|
|
2815
|
+
statusCode;
|
|
2816
|
+
packageName;
|
|
2817
|
+
};
|
|
2818
|
+
var RegistryAuthError = class extends Error {
|
|
2819
|
+
constructor(statusCode, packageName) {
|
|
2820
|
+
super(`Registry authorization failed (${statusCode}): ${packageName}`);
|
|
2821
|
+
this.statusCode = statusCode;
|
|
2822
|
+
this.packageName = packageName;
|
|
2823
|
+
this.name = "RegistryAuthError";
|
|
2824
|
+
}
|
|
2825
|
+
statusCode;
|
|
2826
|
+
packageName;
|
|
2827
|
+
};
|
|
2828
|
+
var RegistryRateLimitError = class extends Error {
|
|
2829
|
+
constructor(statusCode, host) {
|
|
2830
|
+
super(`Registry ${host} returned ${statusCode} after 3 attempts`);
|
|
2831
|
+
this.statusCode = statusCode;
|
|
2832
|
+
this.host = host;
|
|
2833
|
+
this.name = "RegistryRateLimitError";
|
|
2834
|
+
}
|
|
2835
|
+
statusCode;
|
|
2836
|
+
host;
|
|
2837
|
+
};
|
|
2838
|
+
var RegistryNetworkError = class extends Error {
|
|
2839
|
+
constructor(message, cause) {
|
|
2840
|
+
super(message);
|
|
2841
|
+
this.cause = cause;
|
|
2842
|
+
this.name = "RegistryNetworkError";
|
|
2843
|
+
}
|
|
2844
|
+
cause;
|
|
2845
|
+
};
|
|
2211
2846
|
async function lookupRegistry(ecosystem, name, options = {}) {
|
|
2212
2847
|
const fetcher = ECOSYSTEM_FETCHERS[ecosystem];
|
|
2213
2848
|
if (!fetcher) {
|
|
@@ -2223,48 +2858,52 @@ async function lookupRegistry(ecosystem, name, options = {}) {
|
|
|
2223
2858
|
try {
|
|
2224
2859
|
const existingEntry = registryCache.get(cacheKey);
|
|
2225
2860
|
const etag = existingEntry?.etag;
|
|
2226
|
-
let
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
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
|
-
}
|
|
2861
|
+
let response;
|
|
2862
|
+
try {
|
|
2863
|
+
response = await requestWithRetry({
|
|
2864
|
+
hostname: fetcher.host,
|
|
2865
|
+
path,
|
|
2866
|
+
headers: {
|
|
2867
|
+
Accept: "application/json",
|
|
2868
|
+
"User-Agent": "WrongStack-TechStack/1.0",
|
|
2869
|
+
...etag ? { "If-None-Match": etag } : {}
|
|
2870
|
+
},
|
|
2871
|
+
signal: options.signal,
|
|
2872
|
+
timeoutMs: 15e3,
|
|
2873
|
+
maxAttempts: 3
|
|
2874
|
+
});
|
|
2875
|
+
} catch (error) {
|
|
2876
|
+
if (options.signal?.aborted) throw error;
|
|
2877
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
2878
|
+
throw new RegistryNetworkError(cause.message, cause);
|
|
2879
|
+
}
|
|
2880
|
+
if (response.statusCode === 304 && existingEntry) {
|
|
2881
|
+
setCache(cacheKey, existingEntry.data, existingEntry.etag, DEFAULT_TTL_MS);
|
|
2882
|
+
return existingEntry.data;
|
|
2266
2883
|
}
|
|
2267
|
-
|
|
2884
|
+
if (response.statusCode === 401 || response.statusCode === 404) {
|
|
2885
|
+
if (options.strictErrors) throw new RegistryNotFoundError(response.statusCode, name);
|
|
2886
|
+
return void 0;
|
|
2887
|
+
}
|
|
2888
|
+
if (response.statusCode === 403) {
|
|
2889
|
+
if (options.strictErrors) throw new RegistryAuthError(response.statusCode, name);
|
|
2890
|
+
return void 0;
|
|
2891
|
+
}
|
|
2892
|
+
if (response.statusCode === 429) {
|
|
2893
|
+
throw new RegistryRateLimitError(response.statusCode, fetcher.host);
|
|
2894
|
+
}
|
|
2895
|
+
if (response.statusCode >= 500) {
|
|
2896
|
+
throw new RegistryNetworkError(`Registry ${fetcher.host} returned ${response.statusCode} after 3 attempts`);
|
|
2897
|
+
}
|
|
2898
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
2899
|
+
throw new RegistryNetworkError(`Unexpected registry response ${response.statusCode} from ${fetcher.host}`);
|
|
2900
|
+
}
|
|
2901
|
+
const json = parseJsonResponse(response, `${fetcher.host}${path}`);
|
|
2902
|
+
const parsed = fetcher.parser(json, name, ecosystem);
|
|
2903
|
+
if (!parsed) return void 0;
|
|
2904
|
+
const responseEtag = Array.isArray(response.headers.etag) ? response.headers.etag[0] : response.headers.etag;
|
|
2905
|
+
setCache(cacheKey, parsed, responseEtag, DEFAULT_TTL_MS);
|
|
2906
|
+
return parsed;
|
|
2268
2907
|
} finally {
|
|
2269
2908
|
releaseHostSlot(fetcher.host);
|
|
2270
2909
|
}
|
|
@@ -2293,14 +2932,21 @@ function clearRegistryCache() {
|
|
|
2293
2932
|
registryCache.clear();
|
|
2294
2933
|
hostConcurrency.clear();
|
|
2295
2934
|
}
|
|
2935
|
+
function invalidateRegistryCache(ecosystem, names) {
|
|
2936
|
+
const fetcher = ECOSYSTEM_FETCHERS[ecosystem];
|
|
2937
|
+
if (!fetcher) return 0;
|
|
2938
|
+
const keys = names ? names.map((name) => getCacheKey(fetcher.host, fetcher.path(name))) : [...registryCache.keys()].filter((key) => key.startsWith(fetcher.host));
|
|
2939
|
+
let removed = 0;
|
|
2940
|
+
for (const key of keys) {
|
|
2941
|
+
if (registryCache.delete(key)) removed++;
|
|
2942
|
+
}
|
|
2943
|
+
return removed;
|
|
2944
|
+
}
|
|
2296
2945
|
|
|
2297
2946
|
// src/advisory/osv.ts
|
|
2298
|
-
import { get as httpsGet2 } from "node:https";
|
|
2299
2947
|
var OSV_API_BASE = "api.osv.dev";
|
|
2300
2948
|
var OSV_QUERY_BATCH_PATH = "/v1/querybatch";
|
|
2301
2949
|
var MAX_BATCH_SIZE = 500;
|
|
2302
|
-
var MAX_RETRIES2 = 3;
|
|
2303
|
-
var BASE_BACKOFF_MS2 = 1e3;
|
|
2304
2950
|
function mapSeverity(osvSeverity, databaseSeverity) {
|
|
2305
2951
|
if (osvSeverity && osvSeverity.length > 0) {
|
|
2306
2952
|
for (const s of osvSeverity) {
|
|
@@ -2322,44 +2968,6 @@ function mapSeverity(osvSeverity, databaseSeverity) {
|
|
|
2322
2968
|
}
|
|
2323
2969
|
return "info";
|
|
2324
2970
|
}
|
|
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
2971
|
async function queryOsvBatch(purls, options = {}) {
|
|
2364
2972
|
const advisories = /* @__PURE__ */ new Map();
|
|
2365
2973
|
for (const purl of purls) {
|
|
@@ -2369,7 +2977,6 @@ async function queryOsvBatch(purls, options = {}) {
|
|
|
2369
2977
|
for (let i = 0; i < purls.length; i += MAX_BATCH_SIZE) {
|
|
2370
2978
|
batches.push(purls.slice(i, i + MAX_BATCH_SIZE));
|
|
2371
2979
|
}
|
|
2372
|
-
let lastError;
|
|
2373
2980
|
for (const batch of batches) {
|
|
2374
2981
|
const requestBody = {
|
|
2375
2982
|
queries: batch.map((purl) => ({
|
|
@@ -2377,54 +2984,38 @@ async function queryOsvBatch(purls, options = {}) {
|
|
|
2377
2984
|
}))
|
|
2378
2985
|
};
|
|
2379
2986
|
const jsonBody = JSON.stringify(requestBody);
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
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;
|
|
2987
|
+
const response = await requestWithRetry({
|
|
2988
|
+
hostname: OSV_API_BASE,
|
|
2989
|
+
path: OSV_QUERY_BATCH_PATH,
|
|
2990
|
+
method: "POST",
|
|
2991
|
+
headers: {
|
|
2992
|
+
"Content-Type": "application/json",
|
|
2993
|
+
"Content-Length": Buffer.byteLength(jsonBody).toString(),
|
|
2994
|
+
"User-Agent": "WrongStack-TechStack/1.0"
|
|
2995
|
+
},
|
|
2996
|
+
body: jsonBody,
|
|
2997
|
+
signal: options.signal,
|
|
2998
|
+
timeoutMs: 3e4,
|
|
2999
|
+
maxAttempts: 3
|
|
3000
|
+
});
|
|
3001
|
+
if (response.statusCode !== 200) {
|
|
3002
|
+
throw new Error(`OSV API returned ${response.statusCode}: ${response.body}`);
|
|
3003
|
+
}
|
|
3004
|
+
const result = parseJsonResponse(response, "api.osv.dev/v1/querybatch");
|
|
3005
|
+
for (let i = 0; i < result.results.length; i++) {
|
|
3006
|
+
const purl = batch[i];
|
|
3007
|
+
if (!purl) continue;
|
|
3008
|
+
const vulns = result.results[i]?.vulns;
|
|
3009
|
+
if (!vulns || vulns.length === 0) continue;
|
|
3010
|
+
advisories.set(purl, vulns.map((vuln) => ({
|
|
3011
|
+
id: vuln.id,
|
|
3012
|
+
summary: vuln.summary ?? vuln.details ?? "No summary available",
|
|
3013
|
+
severity: mapSeverity(
|
|
3014
|
+
vuln.severity,
|
|
3015
|
+
vuln.database_specific?.severity ?? vuln.affected?.[0]?.database_specific?.severity
|
|
3016
|
+
),
|
|
3017
|
+
aliases: vuln.aliases ?? []
|
|
3018
|
+
})));
|
|
2428
3019
|
}
|
|
2429
3020
|
}
|
|
2430
3021
|
const evidence = {
|
|
@@ -2441,9 +3032,9 @@ async function queryOsvSingle(purl, options = {}) {
|
|
|
2441
3032
|
}
|
|
2442
3033
|
|
|
2443
3034
|
// src/advisory/native-audit.ts
|
|
2444
|
-
import {
|
|
2445
|
-
import {
|
|
2446
|
-
import { join as
|
|
3035
|
+
import { execFile } from "node:child_process";
|
|
3036
|
+
import { access as access3 } from "node:fs/promises";
|
|
3037
|
+
import { join as join8 } from "node:path";
|
|
2447
3038
|
function npmSeverity(s) {
|
|
2448
3039
|
switch (s.toLowerCase()) {
|
|
2449
3040
|
case "critical":
|
|
@@ -2473,8 +3064,8 @@ function cargoSeverity(s) {
|
|
|
2473
3064
|
return "info";
|
|
2474
3065
|
}
|
|
2475
3066
|
}
|
|
2476
|
-
function runNpmAudit(workspaceRoot2) {
|
|
2477
|
-
const result =
|
|
3067
|
+
async function runNpmAudit(workspaceRoot2, runner = defaultAuditCommandRunner) {
|
|
3068
|
+
const result = await runner("npm", ["audit", "--json"], workspaceRoot2);
|
|
2478
3069
|
const advisories = [];
|
|
2479
3070
|
let detailLines = [];
|
|
2480
3071
|
if (result.status === 0 || result.status === 1) {
|
|
@@ -2523,20 +3114,22 @@ function runNpmAudit(workspaceRoot2) {
|
|
|
2523
3114
|
};
|
|
2524
3115
|
return { advisories, evidence };
|
|
2525
3116
|
}
|
|
2526
|
-
function runPipAudit(workspaceRoot2) {
|
|
3117
|
+
async function runPipAudit(workspaceRoot2, runner = defaultAuditCommandRunner) {
|
|
2527
3118
|
const reqFiles = ["requirements.txt", "requirements-dev.txt"];
|
|
2528
3119
|
let reqFlag = "";
|
|
2529
3120
|
for (const f of reqFiles) {
|
|
2530
|
-
|
|
3121
|
+
try {
|
|
3122
|
+
await access3(join8(workspaceRoot2, f));
|
|
2531
3123
|
reqFlag = `--requirement ${f}`;
|
|
2532
3124
|
break;
|
|
3125
|
+
} catch {
|
|
2533
3126
|
}
|
|
2534
3127
|
}
|
|
2535
3128
|
const args = ["audit", "--format", "json"];
|
|
2536
3129
|
if (reqFlag) {
|
|
2537
3130
|
args.push(...reqFlag.split(" "));
|
|
2538
3131
|
}
|
|
2539
|
-
const result =
|
|
3132
|
+
const result = await runner("pip-audit", args, workspaceRoot2);
|
|
2540
3133
|
return parsePipAuditOutput(result);
|
|
2541
3134
|
}
|
|
2542
3135
|
function parsePipAuditOutput(result) {
|
|
@@ -2570,8 +3163,8 @@ function parsePipAuditOutput(result) {
|
|
|
2570
3163
|
};
|
|
2571
3164
|
return { advisories, evidence };
|
|
2572
3165
|
}
|
|
2573
|
-
function runCargoAudit(workspaceRoot2) {
|
|
2574
|
-
const result =
|
|
3166
|
+
async function runCargoAudit(workspaceRoot2, runner = defaultAuditCommandRunner) {
|
|
3167
|
+
const result = await runner("cargo", ["audit", "--json"], workspaceRoot2);
|
|
2575
3168
|
return parseCargoAuditOutput(result);
|
|
2576
3169
|
}
|
|
2577
3170
|
function parseCargoAuditOutput(result) {
|
|
@@ -2612,8 +3205,8 @@ function parseCargoAuditOutput(result) {
|
|
|
2612
3205
|
};
|
|
2613
3206
|
return { advisories, evidence };
|
|
2614
3207
|
}
|
|
2615
|
-
function runGoVulncheck(workspaceRoot2) {
|
|
2616
|
-
const result =
|
|
3208
|
+
async function runGoVulncheck(workspaceRoot2, runner = defaultAuditCommandRunner) {
|
|
3209
|
+
const result = await runner("govulncheck", ["-json"], workspaceRoot2);
|
|
2617
3210
|
const advisories = [];
|
|
2618
3211
|
let detailLines = [];
|
|
2619
3212
|
if (result.status === 0 || result.status === 3) {
|
|
@@ -2651,8 +3244,8 @@ function runGoVulncheck(workspaceRoot2) {
|
|
|
2651
3244
|
};
|
|
2652
3245
|
return { advisories, evidence };
|
|
2653
3246
|
}
|
|
2654
|
-
function runComposerAudit(workspaceRoot2) {
|
|
2655
|
-
const result =
|
|
3247
|
+
async function runComposerAudit(workspaceRoot2, runner = defaultAuditCommandRunner) {
|
|
3248
|
+
const result = await runner("composer", ["audit", "--format=json"], workspaceRoot2);
|
|
2656
3249
|
const advisories = [];
|
|
2657
3250
|
let detailLines = [];
|
|
2658
3251
|
if (result.status === 0) {
|
|
@@ -2688,8 +3281,8 @@ function runComposerAudit(workspaceRoot2) {
|
|
|
2688
3281
|
};
|
|
2689
3282
|
return { advisories, evidence };
|
|
2690
3283
|
}
|
|
2691
|
-
function runDotnetAudit(workspaceRoot2) {
|
|
2692
|
-
const result =
|
|
3284
|
+
async function runDotnetAudit(workspaceRoot2, runner = defaultAuditCommandRunner) {
|
|
3285
|
+
const result = await runner("dotnet", ["package", "audit", "--format", "json"], workspaceRoot2);
|
|
2693
3286
|
const advisories = [];
|
|
2694
3287
|
let detailLines = [];
|
|
2695
3288
|
if (result.status === 0) {
|
|
@@ -2739,44 +3332,50 @@ function runDotnetAudit(workspaceRoot2) {
|
|
|
2739
3332
|
};
|
|
2740
3333
|
return { advisories, evidence };
|
|
2741
3334
|
}
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
3335
|
+
var defaultAuditCommandRunner = (command, args, cwd) => new Promise((resolve3) => {
|
|
3336
|
+
execFile(
|
|
3337
|
+
command,
|
|
3338
|
+
[...args],
|
|
3339
|
+
{
|
|
2745
3340
|
cwd,
|
|
2746
|
-
encoding: "
|
|
3341
|
+
encoding: "utf8",
|
|
2747
3342
|
timeout: 6e4,
|
|
2748
3343
|
maxBuffer: 10 * 1024 * 1024,
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
3344
|
+
windowsHide: true,
|
|
3345
|
+
// On Windows the audit tools are batch shims — `npm`/`composer` resolve
|
|
3346
|
+
// to npm.cmd/composer.cmd, which execFile cannot launch without a shell
|
|
3347
|
+
// (it ignores PATHEXT), so it fails ENOENT and every Windows audit
|
|
3348
|
+
// silently reports "exited with code null" with zero advisories. Route
|
|
3349
|
+
// through the shell there. Safe because every command and argument here
|
|
3350
|
+
// is a static literal (conventional filenames only) — no user input
|
|
3351
|
+
// reaches the command line; the dynamic workspace path is passed as cwd.
|
|
3352
|
+
shell: process.platform === "win32"
|
|
3353
|
+
},
|
|
3354
|
+
(error, stdout, stderr) => {
|
|
3355
|
+
const exitCode = error?.code;
|
|
3356
|
+
const status = error ? typeof exitCode === "number" ? exitCode : null : 0;
|
|
3357
|
+
resolve3({
|
|
3358
|
+
status,
|
|
3359
|
+
stdout,
|
|
3360
|
+
stderr: stderr || (status === null && error ? error.message : "")
|
|
3361
|
+
});
|
|
3362
|
+
}
|
|
3363
|
+
);
|
|
3364
|
+
});
|
|
3365
|
+
async function runNativeAudit(ecosystem, workspaceRoot2, runner = defaultAuditCommandRunner) {
|
|
2767
3366
|
switch (ecosystem) {
|
|
2768
3367
|
case "npm":
|
|
2769
|
-
return runNpmAudit(workspaceRoot2);
|
|
3368
|
+
return runNpmAudit(workspaceRoot2, runner);
|
|
2770
3369
|
case "python":
|
|
2771
|
-
return runPipAudit(workspaceRoot2);
|
|
3370
|
+
return runPipAudit(workspaceRoot2, runner);
|
|
2772
3371
|
case "rust":
|
|
2773
|
-
return runCargoAudit(workspaceRoot2);
|
|
3372
|
+
return runCargoAudit(workspaceRoot2, runner);
|
|
2774
3373
|
case "go":
|
|
2775
|
-
return runGoVulncheck(workspaceRoot2);
|
|
3374
|
+
return runGoVulncheck(workspaceRoot2, runner);
|
|
2776
3375
|
case "php":
|
|
2777
|
-
return runComposerAudit(workspaceRoot2);
|
|
3376
|
+
return runComposerAudit(workspaceRoot2, runner);
|
|
2778
3377
|
case "dotnet":
|
|
2779
|
-
return runDotnetAudit(workspaceRoot2);
|
|
3378
|
+
return runDotnetAudit(workspaceRoot2, runner);
|
|
2780
3379
|
// dart/pub doesn't have a standard audit command — use OSV instead
|
|
2781
3380
|
default:
|
|
2782
3381
|
return {
|
|
@@ -2790,14 +3389,41 @@ function runNativeAudit(ecosystem, workspaceRoot2) {
|
|
|
2790
3389
|
};
|
|
2791
3390
|
}
|
|
2792
3391
|
}
|
|
2793
|
-
function isNativeAuditAvailable(ecosystem) {
|
|
2794
|
-
const result =
|
|
3392
|
+
async function isNativeAuditAvailable(ecosystem, runner = defaultAuditCommandRunner) {
|
|
3393
|
+
const result = await runner(
|
|
2795
3394
|
ecosystem === "npm" ? "npm" : ecosystem === "python" ? "pip-audit" : ecosystem === "rust" ? "cargo" : ecosystem === "go" ? "govulncheck" : ecosystem === "php" ? "composer" : ecosystem === "dotnet" ? "dotnet" : "",
|
|
2796
3395
|
["--version"],
|
|
2797
3396
|
process.cwd()
|
|
2798
3397
|
);
|
|
2799
3398
|
return result.status === 0;
|
|
2800
3399
|
}
|
|
3400
|
+
function createAuditRunner(commandRunner = defaultAuditCommandRunner) {
|
|
3401
|
+
const commandFor = (ecosystem) => {
|
|
3402
|
+
switch (ecosystem) {
|
|
3403
|
+
case "npm":
|
|
3404
|
+
return "npm";
|
|
3405
|
+
case "python":
|
|
3406
|
+
return "pip-audit";
|
|
3407
|
+
case "rust":
|
|
3408
|
+
return "cargo";
|
|
3409
|
+
case "go":
|
|
3410
|
+
return "govulncheck";
|
|
3411
|
+
case "php":
|
|
3412
|
+
return "composer";
|
|
3413
|
+
case "dotnet":
|
|
3414
|
+
return "dotnet";
|
|
3415
|
+
default:
|
|
3416
|
+
return void 0;
|
|
3417
|
+
}
|
|
3418
|
+
};
|
|
3419
|
+
return {
|
|
3420
|
+
run: (ecosystem, workspaceRoot2) => runNativeAudit(ecosystem, workspaceRoot2, commandRunner),
|
|
3421
|
+
isAvailable: async (ecosystem, cwd = process.cwd()) => {
|
|
3422
|
+
const command = commandFor(ecosystem);
|
|
3423
|
+
return command ? (await commandRunner(command, ["--version"], cwd)).status === 0 : false;
|
|
3424
|
+
}
|
|
3425
|
+
};
|
|
3426
|
+
}
|
|
2801
3427
|
|
|
2802
3428
|
// src/policy/status.ts
|
|
2803
3429
|
function isValidSemver(version) {
|
|
@@ -2940,8 +3566,206 @@ function failedLookupStatus(source, error) {
|
|
|
2940
3566
|
};
|
|
2941
3567
|
}
|
|
2942
3568
|
|
|
2943
|
-
// src/service.ts
|
|
3569
|
+
// src/service/techstack-engine.ts
|
|
3570
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3571
|
+
|
|
3572
|
+
// src/service/finding-factory.ts
|
|
3573
|
+
function computeDependencyFingerprint(dependencies) {
|
|
3574
|
+
const parts = dependencies.map((dependency) => `${dependency.name}@${dependency.locked ?? dependency.requested ?? "unknown"}`).sort().join(",");
|
|
3575
|
+
let hash = 0;
|
|
3576
|
+
for (let index = 0; index < parts.length; index++) {
|
|
3577
|
+
hash = (hash << 5) - hash + parts.charCodeAt(index);
|
|
3578
|
+
hash |= 0;
|
|
3579
|
+
}
|
|
3580
|
+
return `ts-${Math.abs(hash).toString(36)}`;
|
|
3581
|
+
}
|
|
3582
|
+
function createFindingForStatus(dependencyId, status) {
|
|
3583
|
+
const common = { dependencyId, confidence: 1, evidence: [] };
|
|
3584
|
+
switch (status) {
|
|
3585
|
+
case "vulnerable":
|
|
3586
|
+
return { ...common, id: `finding-${dependencyId}-vuln`, type: "vulnerability", severity: "high", action: "upgrade_patch", rationale: "Known security advisory found for this package" };
|
|
3587
|
+
case "deprecated":
|
|
3588
|
+
return { ...common, id: `finding-${dependencyId}-dep`, type: "deprecated", severity: "medium", action: "replace", rationale: "Package is deprecated in the registry" };
|
|
3589
|
+
case "yanked":
|
|
3590
|
+
return { ...common, id: `finding-${dependencyId}-yank`, type: "deprecated", severity: "high", action: "replace", rationale: "Package version has been yanked from the registry" };
|
|
3591
|
+
case "update_available_safe":
|
|
3592
|
+
return { ...common, id: `finding-${dependencyId}-update`, type: "upgrade", severity: "info", action: "upgrade_minor", rationale: "A newer compatible version is available" };
|
|
3593
|
+
case "update_available_breaking":
|
|
3594
|
+
return { ...common, id: `finding-${dependencyId}-major`, type: "upgrade", severity: "low", action: "upgrade_major", rationale: "A newer version is available that may require breaking changes" };
|
|
3595
|
+
default:
|
|
3596
|
+
return { ...common, confidence: 0.5, id: `finding-${dependencyId}-investigate`, type: "investigate", severity: "info", action: "investigate", rationale: `Package status is "${status}" \u2014 may need investigation` };
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
// src/service/enrich-phase.ts
|
|
3601
|
+
async function runEnrichPhase(snapshot, options = {}) {
|
|
3602
|
+
if (options.online === false || options.signal?.aborted) return snapshot;
|
|
3603
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3604
|
+
for (const dependency of snapshot.dependencies) {
|
|
3605
|
+
if (!dependency.purl || dependency.sourceType === "path" || dependency.sourceType === "git") continue;
|
|
3606
|
+
grouped.set(dependency.ecosystem, [...grouped.get(dependency.ecosystem) ?? [], dependency]);
|
|
3607
|
+
}
|
|
3608
|
+
const enriched = /* @__PURE__ */ new Map();
|
|
3609
|
+
const findings = [...snapshot.findings];
|
|
3610
|
+
for (const [ecosystem, dependencies] of grouped) {
|
|
3611
|
+
for (const name of new Set(dependencies.map((dependency) => dependency.name))) {
|
|
3612
|
+
if (options.signal?.aborted) throw new DOMException("TechStack job cancelled", "AbortError");
|
|
3613
|
+
let registryEntry;
|
|
3614
|
+
let registryStatus;
|
|
3615
|
+
try {
|
|
3616
|
+
registryEntry = await lookupRegistry(ecosystem, name, {
|
|
3617
|
+
signal: options.signal,
|
|
3618
|
+
force: options.forceRegistryRefresh,
|
|
3619
|
+
strictErrors: true
|
|
3620
|
+
});
|
|
3621
|
+
registryStatus = registryEntry ? {
|
|
3622
|
+
latestStable: registryEntry.latestStable,
|
|
3623
|
+
deprecated: registryEntry.deprecated,
|
|
3624
|
+
yanked: registryEntry.yanked,
|
|
3625
|
+
evidence: [{ kind: "registry", source: registryEntry.source, retrievedAt: registryEntry.retrievedAt, detail: `latestStable: ${registryEntry.latestStable ?? "N/A"}, license: ${registryEntry.license ?? "N/A"}` }]
|
|
3626
|
+
} : { privateOrUnresolved: true };
|
|
3627
|
+
} catch (error) {
|
|
3628
|
+
const unresolved = error instanceof RegistryNotFoundError || error instanceof RegistryAuthError;
|
|
3629
|
+
registryStatus = unresolved ? {
|
|
3630
|
+
privateOrUnresolved: true,
|
|
3631
|
+
evidence: [{ kind: "registry", source: `${ecosystem} registry for ${name}`, retrievedAt: (/* @__PURE__ */ new Date()).toISOString(), detail: error.message }]
|
|
3632
|
+
} : {
|
|
3633
|
+
lookupFailed: true,
|
|
3634
|
+
evidence: [{ kind: "registry", source: `${ecosystem} registry for ${name}`, retrievedAt: (/* @__PURE__ */ new Date()).toISOString(), detail: error instanceof Error ? error.message : "Registry lookup failed" }]
|
|
3635
|
+
};
|
|
3636
|
+
}
|
|
3637
|
+
let advisoryStatus;
|
|
3638
|
+
try {
|
|
3639
|
+
const purls = dependencies.filter((dependency) => dependency.name === name).flatMap((dependency) => dependency.purl ? [dependency.purl] : []);
|
|
3640
|
+
if (purls.length > 0) {
|
|
3641
|
+
const result = await queryOsvBatch(purls, { signal: options.signal });
|
|
3642
|
+
if ([...result.advisories.values()].some((items) => items.length > 0)) {
|
|
3643
|
+
advisoryStatus = { hasAdvisory: true, evidence: [result.evidence] };
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
} catch {
|
|
3647
|
+
}
|
|
3648
|
+
for (const dependency of dependencies) {
|
|
3649
|
+
if (dependency.name !== name) continue;
|
|
3650
|
+
const status = classifyStatus(dependency, registryStatus, advisoryStatus);
|
|
3651
|
+
const evidence = [...dependency.evidence, ...registryStatus?.evidence ?? [], ...advisoryStatus?.evidence ?? []];
|
|
3652
|
+
enriched.set(dependency.id, {
|
|
3653
|
+
...dependency,
|
|
3654
|
+
latestStable: registryEntry?.latestStable ?? dependency.latestStable,
|
|
3655
|
+
license: registryEntry?.license ?? dependency.license,
|
|
3656
|
+
deprecated: registryEntry?.deprecated ?? dependency.deprecated,
|
|
3657
|
+
yanked: registryEntry?.yanked ?? dependency.yanked,
|
|
3658
|
+
status,
|
|
3659
|
+
evidence
|
|
3660
|
+
});
|
|
3661
|
+
if (status !== "current" && status !== "local_path" && status !== "git_dependency") findings.push(createFindingForStatus(dependency.id, status));
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
return { ...snapshot, dependencies: snapshot.dependencies.map((dependency) => enriched.get(dependency.id) ?? dependency), findings };
|
|
3666
|
+
}
|
|
3667
|
+
|
|
3668
|
+
// src/service/inventory-phase.ts
|
|
2944
3669
|
import { randomUUID } from "node:crypto";
|
|
3670
|
+
var ADAPTER_VERSION = "0.1.0";
|
|
3671
|
+
var ADAPTERS = {
|
|
3672
|
+
npm: npmAdapter,
|
|
3673
|
+
python: pythonAdapter,
|
|
3674
|
+
rust: rustAdapter,
|
|
3675
|
+
go: goAdapter,
|
|
3676
|
+
dotnet: dotNetAdapter,
|
|
3677
|
+
php: phpAdapter,
|
|
3678
|
+
dart: dartAdapter,
|
|
3679
|
+
maven: mavenAdapter,
|
|
3680
|
+
gradle: gradleAdapter,
|
|
3681
|
+
ruby: rubyAdapter,
|
|
3682
|
+
swift: swiftAdapter,
|
|
3683
|
+
elixir: elixirAdapter,
|
|
3684
|
+
cpp: cppAdapter
|
|
3685
|
+
};
|
|
3686
|
+
async function runInventoryPhase(store, projectId, targetRoot, options = {}) {
|
|
3687
|
+
options.onProgress?.("discovering", 0, 1);
|
|
3688
|
+
const workspaces = await discoverWorkspaces(targetRoot, { signal: options.signal });
|
|
3689
|
+
options.onProgress?.("inventorying", 0, workspaces.length);
|
|
3690
|
+
const dependencies = [];
|
|
3691
|
+
let coverage = "full";
|
|
3692
|
+
let completed = 0;
|
|
3693
|
+
for (const workspace of workspaces) {
|
|
3694
|
+
if (options.signal?.aborted) throw new DOMException("TechStack job cancelled", "AbortError");
|
|
3695
|
+
try {
|
|
3696
|
+
dependencies.push(...await ADAPTERS[workspace.ecosystem].inventory(workspace, {
|
|
3697
|
+
projectRoot: targetRoot,
|
|
3698
|
+
includeTransitive: options.includeTransitive,
|
|
3699
|
+
signal: options.signal
|
|
3700
|
+
}));
|
|
3701
|
+
} catch {
|
|
3702
|
+
}
|
|
3703
|
+
if (workspace.coverage === "unsupported") coverage = "partial";
|
|
3704
|
+
completed++;
|
|
3705
|
+
options.onProgress?.("inventorying", completed, workspaces.length);
|
|
3706
|
+
}
|
|
3707
|
+
const snapshot = {
|
|
3708
|
+
id: randomUUID(),
|
|
3709
|
+
projectId,
|
|
3710
|
+
targetRoot,
|
|
3711
|
+
fingerprint: computeDependencyFingerprint(dependencies),
|
|
3712
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3713
|
+
workspaces,
|
|
3714
|
+
dependencies,
|
|
3715
|
+
findings: [],
|
|
3716
|
+
coverage,
|
|
3717
|
+
adapterVersion: ADAPTER_VERSION
|
|
3718
|
+
};
|
|
3719
|
+
store.saveSnapshot(snapshot);
|
|
3720
|
+
return snapshot;
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3723
|
+
// src/service/report-generator.ts
|
|
3724
|
+
function generateReport(snapshot, format = "md") {
|
|
3725
|
+
if (format === "json") return JSON.stringify(snapshot, null, 2);
|
|
3726
|
+
const lines = [
|
|
3727
|
+
"# TechStack Report",
|
|
3728
|
+
"",
|
|
3729
|
+
`**Generated:** ${snapshot.createdAt}`,
|
|
3730
|
+
`**Target:** ${snapshot.targetRoot}`,
|
|
3731
|
+
`**Fingerprint:** ${snapshot.fingerprint}`,
|
|
3732
|
+
`**Workspaces:** ${snapshot.workspaces.length}`,
|
|
3733
|
+
`**Dependencies:** ${snapshot.dependencies.length}`,
|
|
3734
|
+
`**Findings:** ${snapshot.findings.length}`,
|
|
3735
|
+
`**Coverage:** ${snapshot.coverage}`,
|
|
3736
|
+
""
|
|
3737
|
+
];
|
|
3738
|
+
if (snapshot.workspaces.length > 0) {
|
|
3739
|
+
lines.push("## Workspaces", "", "| Workspace | Ecosystem | Coverage | Deps |", "|---|---|---|---|");
|
|
3740
|
+
for (const workspace of snapshot.workspaces) {
|
|
3741
|
+
const count = snapshot.dependencies.filter((dependency) => dependency.workspaceId === workspace.id).length;
|
|
3742
|
+
lines.push(`| ${workspace.relativeRoot} | ${workspace.ecosystem} | ${workspace.coverage} | ${count} |`);
|
|
3743
|
+
}
|
|
3744
|
+
lines.push("");
|
|
3745
|
+
}
|
|
3746
|
+
if (snapshot.findings.length > 0) {
|
|
3747
|
+
lines.push("## Findings", "");
|
|
3748
|
+
const bySeverity = /* @__PURE__ */ new Map();
|
|
3749
|
+
for (const finding of snapshot.findings) bySeverity.set(finding.severity, [...bySeverity.get(finding.severity) ?? [], finding]);
|
|
3750
|
+
for (const severity of ["critical", "high", "medium", "low", "info"]) {
|
|
3751
|
+
const findings = bySeverity.get(severity);
|
|
3752
|
+
if (!findings?.length) continue;
|
|
3753
|
+
lines.push(`### ${severity.charAt(0).toUpperCase() + severity.slice(1)} (${findings.length})`, "");
|
|
3754
|
+
for (const finding of findings) {
|
|
3755
|
+
const dependency = snapshot.dependencies.find((candidate) => candidate.id === finding.dependencyId);
|
|
3756
|
+
lines.push(`- **${dependency?.name ?? finding.dependencyId}** \u2014 ${finding.type} \u2014 ${finding.rationale}`);
|
|
3757
|
+
}
|
|
3758
|
+
lines.push("");
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
if (snapshot.dependencies.length > 0) {
|
|
3762
|
+
lines.push("## Dependencies", "", "| Name | Ecosystem | Status | Locked | Latest |", "|---|---|---|---|---|");
|
|
3763
|
+
for (const dependency of snapshot.dependencies) {
|
|
3764
|
+
lines.push(`| ${dependency.name} | ${dependency.ecosystem} | ${dependency.status} | ${dependency.locked ?? "\u2014"} | ${dependency.latestStable ?? "\u2014"} |`);
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
return lines.join("\n");
|
|
3768
|
+
}
|
|
2945
3769
|
|
|
2946
3770
|
// src/research/triage.ts
|
|
2947
3771
|
var DEFAULT_TRIAGE_LIMIT = 40;
|
|
@@ -3003,285 +3827,45 @@ function clusterCandidates(candidates) {
|
|
|
3003
3827
|
return out;
|
|
3004
3828
|
}
|
|
3005
3829
|
|
|
3006
|
-
// src/service.ts
|
|
3007
|
-
function
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
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;
|
|
3830
|
+
// src/service/research-phase.ts
|
|
3831
|
+
async function runResearchPhase(snapshot, options = {}) {
|
|
3832
|
+
if (!options.researcher || options.signal?.aborted) return snapshot;
|
|
3833
|
+
const candidates = triageCandidates(snapshot.dependencies, { limit: options.researchLimit });
|
|
3834
|
+
if (candidates.length === 0) return snapshot;
|
|
3835
|
+
options.onProgress?.("researching", 0, candidates.length);
|
|
3836
|
+
let findings;
|
|
3837
|
+
try {
|
|
3838
|
+
findings = await options.researcher.research(candidates, {
|
|
3839
|
+
signal: options.signal,
|
|
3840
|
+
onProgress: (completed, total) => options.onProgress?.("researching", completed, total)
|
|
3841
|
+
});
|
|
3842
|
+
} catch {
|
|
3843
|
+
return snapshot;
|
|
3041
3844
|
}
|
|
3845
|
+
options.onProgress?.("synthesizing", 1, 1);
|
|
3846
|
+
return findings.length ? { ...snapshot, findings: [...snapshot.findings, ...findings] } : snapshot;
|
|
3042
3847
|
}
|
|
3043
|
-
|
|
3848
|
+
|
|
3849
|
+
// src/service/techstack-engine.ts
|
|
3044
3850
|
var TechStackEngine = class {
|
|
3045
|
-
store;
|
|
3046
3851
|
constructor(store) {
|
|
3047
3852
|
this.store = store;
|
|
3048
3853
|
}
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
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;
|
|
3854
|
+
store;
|
|
3855
|
+
inventory(projectId, targetRoot, _jobId, onProgress, options = {}) {
|
|
3856
|
+
return runInventoryPhase(this.store, projectId, targetRoot, { ...options, onProgress });
|
|
3106
3857
|
}
|
|
3107
|
-
|
|
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
|
-
};
|
|
3858
|
+
enrich(snapshot, options = {}) {
|
|
3859
|
+
return runEnrichPhase(snapshot, options);
|
|
3236
3860
|
}
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
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] };
|
|
3861
|
+
research(snapshot, options = {}) {
|
|
3862
|
+
return runResearchPhase(snapshot, options);
|
|
3863
|
+
}
|
|
3864
|
+
generateReport(snapshot, format = "md") {
|
|
3865
|
+
return generateReport(snapshot, format);
|
|
3276
3866
|
}
|
|
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
3867
|
async analyze(projectId, options) {
|
|
3283
|
-
const jobId = options.jobId ??
|
|
3284
|
-
const requestedBy = options.requestedBy ?? "system";
|
|
3868
|
+
const jobId = options.jobId ?? randomUUID2();
|
|
3285
3869
|
const job = {
|
|
3286
3870
|
id: jobId,
|
|
3287
3871
|
projectId,
|
|
@@ -3289,7 +3873,7 @@ var TechStackEngine = class {
|
|
|
3289
3873
|
kind: "analyze",
|
|
3290
3874
|
status: "queued",
|
|
3291
3875
|
fingerprint: "",
|
|
3292
|
-
requestedBy,
|
|
3876
|
+
requestedBy: options.requestedBy ?? "system",
|
|
3293
3877
|
sessionId: options.sessionId,
|
|
3294
3878
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3295
3879
|
progress: { phase: "queued", completed: 0, total: 0 }
|
|
@@ -3305,199 +3889,32 @@ var TechStackEngine = class {
|
|
|
3305
3889
|
try {
|
|
3306
3890
|
throwIfAborted();
|
|
3307
3891
|
updateJob("discovering", { phase: "discovering", completed: 0, total: 1 });
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
(phase, completed, total) => {
|
|
3313
|
-
throwIfAborted();
|
|
3314
|
-
updateJob(phase, { phase, completed, total });
|
|
3315
|
-
}
|
|
3316
|
-
);
|
|
3892
|
+
let snapshot = await this.inventory(projectId, options.targetRoot, jobId, (phase, completed, total) => {
|
|
3893
|
+
throwIfAborted();
|
|
3894
|
+
updateJob(phase, { phase, completed, total });
|
|
3895
|
+
}, { includeTransitive: options.includeTransitive, signal: options.signal });
|
|
3317
3896
|
throwIfAborted();
|
|
3318
|
-
|
|
3319
|
-
if (isOnline) {
|
|
3897
|
+
if (options.online !== false) {
|
|
3320
3898
|
updateJob("enriching", { phase: "enriching", completed: 0, total: 1 });
|
|
3321
|
-
|
|
3322
|
-
online: true,
|
|
3323
|
-
signal: options.signal
|
|
3324
|
-
});
|
|
3899
|
+
snapshot = await this.enrich(snapshot, { online: true, signal: options.signal });
|
|
3325
3900
|
throwIfAborted();
|
|
3326
|
-
|
|
3901
|
+
snapshot = await this.research(snapshot, {
|
|
3327
3902
|
researcher: options.researcher,
|
|
3328
3903
|
researchLimit: options.researchLimit,
|
|
3329
3904
|
signal: options.signal,
|
|
3330
|
-
onProgress: (phase, completed, total) => {
|
|
3331
|
-
updateJob(phase, { phase, completed, total });
|
|
3332
|
-
}
|
|
3905
|
+
onProgress: (phase, completed, total) => updateJob(phase, { phase, completed, total })
|
|
3333
3906
|
});
|
|
3334
3907
|
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
3908
|
}
|
|
3342
3909
|
this.store.saveSnapshot(snapshot);
|
|
3343
3910
|
updateJob("completed", { phase: "completed", completed: 1, total: 1 });
|
|
3344
|
-
return {
|
|
3345
|
-
|
|
3346
|
-
|
|
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;
|
|
3911
|
+
return { snapshot, job: { ...job, status: "completed", completedAt: (/* @__PURE__ */ new Date()).toISOString() } };
|
|
3912
|
+
} catch (error) {
|
|
3913
|
+
updateJob(options.signal?.aborted || error instanceof DOMException && error.name === "AbortError" ? "cancelled" : "failed");
|
|
3914
|
+
throw error;
|
|
3355
3915
|
}
|
|
3356
3916
|
}
|
|
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
3917
|
};
|
|
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
3918
|
|
|
3502
3919
|
// src/research/llm.ts
|
|
3503
3920
|
var DEFAULT_TIMEOUT_MS = 45e3;
|
|
@@ -3857,10 +4274,10 @@ function createToolSearch(options = {}) {
|
|
|
3857
4274
|
}
|
|
3858
4275
|
|
|
3859
4276
|
// src/store/sqlite.ts
|
|
3860
|
-
import {
|
|
3861
|
-
import { mkdirSync, existsSync as
|
|
3862
|
-
import { join as
|
|
3863
|
-
import {
|
|
4277
|
+
import { createRequire } from "node:module";
|
|
4278
|
+
import { mkdirSync, existsSync as existsSync2 } from "node:fs";
|
|
4279
|
+
import { dirname as dirname2, join as join9 } from "node:path";
|
|
4280
|
+
import { wstackGlobalRoot } from "@wrongstack/core/utils";
|
|
3864
4281
|
|
|
3865
4282
|
// src/store/schema.ts
|
|
3866
4283
|
var SCHEMA_VERSION = 1;
|
|
@@ -3930,30 +4347,77 @@ function applySchema(db) {
|
|
|
3930
4347
|
}
|
|
3931
4348
|
|
|
3932
4349
|
// src/store/sqlite.ts
|
|
4350
|
+
var DatabaseSyncCtor;
|
|
4351
|
+
var SQLITE_EXPERIMENTAL_WARNING = "SQLite is an experimental feature and might change at any time";
|
|
4352
|
+
function loadDatabaseSync() {
|
|
4353
|
+
if (DatabaseSyncCtor) return DatabaseSyncCtor;
|
|
4354
|
+
const originalEmitWarning = process.emitWarning;
|
|
4355
|
+
const forwardWarning = originalEmitWarning.bind(process);
|
|
4356
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
4357
|
+
const message = typeof warning === "string" ? warning : warning instanceof Error ? warning.message : "";
|
|
4358
|
+
const typeOrOptions = rest[0];
|
|
4359
|
+
const warningType = typeof warning === "string" ? typeof typeOrOptions === "string" ? typeOrOptions : typeof typeOrOptions === "object" && typeOrOptions !== null && "type" in typeOrOptions && typeof typeOrOptions.type === "string" ? typeOrOptions.type : "" : warning instanceof Error ? warning.name : "";
|
|
4360
|
+
const warningCode = typeof warning === "string" ? typeof typeOrOptions === "object" && typeOrOptions !== null && "code" in typeOrOptions && typeof typeOrOptions.code === "string" ? typeOrOptions.code : typeof rest[1] === "string" ? rest[1] : "" : warning instanceof Error && "code" in warning && typeof warning.code === "string" ? warning.code : "";
|
|
4361
|
+
if (message === SQLITE_EXPERIMENTAL_WARNING && (warningType === "ExperimentalWarning" || warningCode === "ExperimentalWarning")) {
|
|
4362
|
+
return;
|
|
4363
|
+
}
|
|
4364
|
+
forwardWarning(warning, ...rest);
|
|
4365
|
+
});
|
|
4366
|
+
try {
|
|
4367
|
+
try {
|
|
4368
|
+
const require2 = createRequire(import.meta.url);
|
|
4369
|
+
const sqliteModule = require2("node:sqlite");
|
|
4370
|
+
if (typeof sqliteModule !== "object" || sqliteModule === null || !("DatabaseSync" in sqliteModule) || typeof sqliteModule.DatabaseSync !== "function") {
|
|
4371
|
+
throw new Error("node:sqlite DatabaseSync unavailable");
|
|
4372
|
+
}
|
|
4373
|
+
DatabaseSyncCtor = sqliteModule.DatabaseSync;
|
|
4374
|
+
return DatabaseSyncCtor;
|
|
4375
|
+
} catch (error) {
|
|
4376
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4377
|
+
throw new Error(
|
|
4378
|
+
`TechStack SQLite store needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${message}`,
|
|
4379
|
+
{ cause: error }
|
|
4380
|
+
);
|
|
4381
|
+
}
|
|
4382
|
+
} finally {
|
|
4383
|
+
process.emitWarning = originalEmitWarning;
|
|
4384
|
+
}
|
|
4385
|
+
}
|
|
3933
4386
|
var TechStackStore = class {
|
|
3934
4387
|
db;
|
|
3935
4388
|
dbPath;
|
|
4389
|
+
/** Compile-once cache for fixed SQL used on hot job/snapshot paths. */
|
|
4390
|
+
stmtCache = /* @__PURE__ */ new Map();
|
|
3936
4391
|
constructor(options) {
|
|
3937
|
-
this.dbPath = options.dbPath ??
|
|
3938
|
-
|
|
3939
|
-
|
|
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)) {
|
|
4392
|
+
this.dbPath = options.dbPath ?? join9(wstackGlobalRoot(), "projects", options.projectSlug, "techstack", "techstack.db");
|
|
4393
|
+
const dir = dirname2(this.dbPath);
|
|
4394
|
+
if (!existsSync2(dir)) {
|
|
3947
4395
|
mkdirSync(dir, { recursive: true });
|
|
3948
4396
|
}
|
|
3949
|
-
|
|
4397
|
+
const Database = loadDatabaseSync();
|
|
4398
|
+
this.db = new Database(this.dbPath);
|
|
3950
4399
|
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
4400
|
+
this.db.exec("PRAGMA synchronous = NORMAL;");
|
|
4401
|
+
this.db.exec("PRAGMA busy_timeout = 10000;");
|
|
4402
|
+
this.db.exec("PRAGMA temp_store = MEMORY;");
|
|
4403
|
+
this.db.exec("PRAGMA cache_size = -32768;");
|
|
4404
|
+
this.db.exec("PRAGMA mmap_size = 134217728;");
|
|
3951
4405
|
this.db.exec("PRAGMA foreign_keys = ON;");
|
|
3952
4406
|
applySchema(this.db);
|
|
3953
4407
|
}
|
|
4408
|
+
/** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
|
|
4409
|
+
stmt(sql) {
|
|
4410
|
+
let prepared = this.stmtCache.get(sql);
|
|
4411
|
+
if (prepared === void 0) {
|
|
4412
|
+
prepared = this.db.prepare(sql);
|
|
4413
|
+
this.stmtCache.set(sql, prepared);
|
|
4414
|
+
}
|
|
4415
|
+
return prepared;
|
|
4416
|
+
}
|
|
3954
4417
|
// ── Lifecycle ───────────────────────────────────────────────────────────
|
|
3955
4418
|
/** Close the database connection. Idempotent. */
|
|
3956
4419
|
close() {
|
|
4420
|
+
this.stmtCache.clear();
|
|
3957
4421
|
try {
|
|
3958
4422
|
this.db.close();
|
|
3959
4423
|
} catch {
|
|
@@ -3966,7 +4430,7 @@ var TechStackStore = class {
|
|
|
3966
4430
|
// ── Snapshots ───────────────────────────────────────────────────────────
|
|
3967
4431
|
/** Persist a snapshot. */
|
|
3968
4432
|
saveSnapshot(snapshot) {
|
|
3969
|
-
const stmt = this.
|
|
4433
|
+
const stmt = this.stmt(`
|
|
3970
4434
|
INSERT OR REPLACE INTO snapshots (id, project_id, target_root, fingerprint, created_at, raw_json, adapter_version)
|
|
3971
4435
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
3972
4436
|
`);
|
|
@@ -3982,7 +4446,7 @@ var TechStackStore = class {
|
|
|
3982
4446
|
}
|
|
3983
4447
|
/** Get a snapshot by project ID (latest). */
|
|
3984
4448
|
getSnapshot(projectId) {
|
|
3985
|
-
const stmt = this.
|
|
4449
|
+
const stmt = this.stmt(`
|
|
3986
4450
|
SELECT raw_json FROM snapshots
|
|
3987
4451
|
WHERE project_id = ?
|
|
3988
4452
|
ORDER BY created_at DESC
|
|
@@ -3998,7 +4462,7 @@ var TechStackStore = class {
|
|
|
3998
4462
|
}
|
|
3999
4463
|
/** Get a snapshot by ID. */
|
|
4000
4464
|
getSnapshotById(id) {
|
|
4001
|
-
const stmt = this.
|
|
4465
|
+
const stmt = this.stmt(`
|
|
4002
4466
|
SELECT raw_json FROM snapshots WHERE id = ?
|
|
4003
4467
|
`);
|
|
4004
4468
|
const row = stmt.get(id);
|
|
@@ -4011,7 +4475,7 @@ var TechStackStore = class {
|
|
|
4011
4475
|
}
|
|
4012
4476
|
/** List all snapshots for a project (newest first). */
|
|
4013
4477
|
listSnapshots(projectId, limit = 20) {
|
|
4014
|
-
const stmt = this.
|
|
4478
|
+
const stmt = this.stmt(`
|
|
4015
4479
|
SELECT raw_json FROM snapshots
|
|
4016
4480
|
WHERE project_id = ?
|
|
4017
4481
|
ORDER BY created_at DESC
|
|
@@ -4028,7 +4492,7 @@ var TechStackStore = class {
|
|
|
4028
4492
|
}
|
|
4029
4493
|
/** Delete snapshots older than a given timestamp. */
|
|
4030
4494
|
deleteSnapshotsBefore(projectId, before) {
|
|
4031
|
-
const stmt = this.
|
|
4495
|
+
const stmt = this.stmt(`
|
|
4032
4496
|
DELETE FROM snapshots WHERE project_id = ? AND created_at < ?
|
|
4033
4497
|
`);
|
|
4034
4498
|
const result = stmt.run(projectId, before);
|
|
@@ -4037,7 +4501,7 @@ var TechStackStore = class {
|
|
|
4037
4501
|
// ── Jobs ────────────────────────────────────────────────────────────────
|
|
4038
4502
|
/** Persist a job. */
|
|
4039
4503
|
saveJob(job) {
|
|
4040
|
-
const stmt = this.
|
|
4504
|
+
const stmt = this.stmt(`
|
|
4041
4505
|
INSERT OR REPLACE INTO jobs
|
|
4042
4506
|
(id, project_id, target_root, kind, status, fingerprint, requested_by, session_id, created_at, completed_at, error, progress_json)
|
|
4043
4507
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
@@ -4059,7 +4523,7 @@ var TechStackStore = class {
|
|
|
4059
4523
|
}
|
|
4060
4524
|
/** Get a job by ID. */
|
|
4061
4525
|
getJob(id) {
|
|
4062
|
-
const stmt = this.
|
|
4526
|
+
const stmt = this.stmt(`
|
|
4063
4527
|
SELECT * FROM jobs WHERE id = ?
|
|
4064
4528
|
`);
|
|
4065
4529
|
const row = stmt.get(id);
|
|
@@ -4070,7 +4534,7 @@ var TechStackStore = class {
|
|
|
4070
4534
|
updateJobStatus(id, status, progress) {
|
|
4071
4535
|
const progressJson = progress ? JSON.stringify(progress) : null;
|
|
4072
4536
|
const completedAt = status === "completed" || status === "failed" || status === "cancelled" ? (/* @__PURE__ */ new Date()).toISOString() : null;
|
|
4073
|
-
const stmt = this.
|
|
4537
|
+
const stmt = this.stmt(`
|
|
4074
4538
|
UPDATE jobs
|
|
4075
4539
|
SET status = ?, progress_json = ?, completed_at = COALESCE(?, completed_at)
|
|
4076
4540
|
WHERE id = ?
|
|
@@ -4079,7 +4543,7 @@ var TechStackStore = class {
|
|
|
4079
4543
|
}
|
|
4080
4544
|
/** List jobs for a project (newest first). */
|
|
4081
4545
|
listJobs(projectId, limit = 50) {
|
|
4082
|
-
const stmt = this.
|
|
4546
|
+
const stmt = this.stmt(`
|
|
4083
4547
|
SELECT * FROM jobs WHERE project_id = ? ORDER BY created_at DESC LIMIT ?
|
|
4084
4548
|
`);
|
|
4085
4549
|
const rows = stmt.all(projectId, limit);
|
|
@@ -4088,7 +4552,7 @@ var TechStackStore = class {
|
|
|
4088
4552
|
// ── Outbox ──────────────────────────────────────────────────────────────
|
|
4089
4553
|
/** Create an outbox entry. */
|
|
4090
4554
|
createOutbox(deliveryId, reportId, sessionId) {
|
|
4091
|
-
const stmt = this.
|
|
4555
|
+
const stmt = this.stmt(`
|
|
4092
4556
|
INSERT OR IGNORE INTO outbox (delivery_id, report_id, session_id, status, attempts)
|
|
4093
4557
|
VALUES (?, ?, ?, 'pending', 0)
|
|
4094
4558
|
`);
|
|
@@ -4096,7 +4560,7 @@ var TechStackStore = class {
|
|
|
4096
4560
|
}
|
|
4097
4561
|
/** Claim an outbox entry (atomic CAS). */
|
|
4098
4562
|
claimOutbox(deliveryId, sessionId) {
|
|
4099
|
-
const stmt = this.
|
|
4563
|
+
const stmt = this.stmt(`
|
|
4100
4564
|
UPDATE outbox
|
|
4101
4565
|
SET status = 'claimed', claimed_at = datetime('now'), attempts = attempts + 1
|
|
4102
4566
|
WHERE delivery_id = ? AND session_id = ? AND status = 'pending'
|
|
@@ -4106,7 +4570,7 @@ var TechStackStore = class {
|
|
|
4106
4570
|
}
|
|
4107
4571
|
/** Mark an outbox entry as delivered. */
|
|
4108
4572
|
deliverOutbox(deliveryId) {
|
|
4109
|
-
const stmt = this.
|
|
4573
|
+
const stmt = this.stmt(`
|
|
4110
4574
|
UPDATE outbox SET status = 'delivered', delivered_at = datetime('now')
|
|
4111
4575
|
WHERE delivery_id = ?
|
|
4112
4576
|
`);
|
|
@@ -4114,14 +4578,14 @@ var TechStackStore = class {
|
|
|
4114
4578
|
}
|
|
4115
4579
|
/** Mark an outbox entry as failed. */
|
|
4116
4580
|
failOutbox(deliveryId) {
|
|
4117
|
-
const stmt = this.
|
|
4581
|
+
const stmt = this.stmt(`
|
|
4118
4582
|
UPDATE outbox SET status = 'failed' WHERE delivery_id = ?
|
|
4119
4583
|
`);
|
|
4120
4584
|
stmt.run(deliveryId);
|
|
4121
4585
|
}
|
|
4122
4586
|
/** List outbox entries by status. */
|
|
4123
4587
|
listOutboxByStatus(status) {
|
|
4124
|
-
const stmt = this.
|
|
4588
|
+
const stmt = this.stmt(`
|
|
4125
4589
|
SELECT * FROM outbox WHERE status = ?
|
|
4126
4590
|
`);
|
|
4127
4591
|
const rows = stmt.all(status);
|
|
@@ -4228,14 +4692,22 @@ export {
|
|
|
4228
4692
|
DotNetAdapter,
|
|
4229
4693
|
ElixirAdapter,
|
|
4230
4694
|
GoAdapter,
|
|
4695
|
+
GradleAdapter,
|
|
4231
4696
|
MavenAdapter,
|
|
4232
4697
|
NpmAdapter,
|
|
4233
4698
|
PhpAdapter,
|
|
4234
4699
|
PythonAdapter,
|
|
4700
|
+
RegistryAuthError,
|
|
4701
|
+
RegistryNetworkError,
|
|
4702
|
+
RegistryNotFoundError,
|
|
4703
|
+
RegistryRateLimitError,
|
|
4235
4704
|
RubyAdapter,
|
|
4236
4705
|
RustAdapter,
|
|
4706
|
+
SwiftAdapter,
|
|
4237
4707
|
TechStackEngine,
|
|
4238
4708
|
TechStackStore,
|
|
4709
|
+
TrendStore,
|
|
4710
|
+
applyPlan,
|
|
4239
4711
|
applySchema,
|
|
4240
4712
|
attemptDelivery,
|
|
4241
4713
|
buildPurl,
|
|
@@ -4246,6 +4718,7 @@ export {
|
|
|
4246
4718
|
constructPurl,
|
|
4247
4719
|
coverageForEcosystem,
|
|
4248
4720
|
cppAdapter,
|
|
4721
|
+
createAuditRunner,
|
|
4249
4722
|
createProviderLlm,
|
|
4250
4723
|
createResearcher,
|
|
4251
4724
|
createToolSearch,
|
|
@@ -4259,6 +4732,8 @@ export {
|
|
|
4259
4732
|
failedLookupStatus,
|
|
4260
4733
|
generateUpgradePlan,
|
|
4261
4734
|
goAdapter,
|
|
4735
|
+
gradleAdapter,
|
|
4736
|
+
invalidateRegistryCache,
|
|
4262
4737
|
isNativeAuditAvailable,
|
|
4263
4738
|
lookupRegistry,
|
|
4264
4739
|
lookupRegistryBatch,
|
|
@@ -4275,12 +4750,15 @@ export {
|
|
|
4275
4750
|
queryOsvBatch,
|
|
4276
4751
|
queryOsvSingle,
|
|
4277
4752
|
renderPlanMarkdown,
|
|
4753
|
+
renderTrendMarkdown,
|
|
4278
4754
|
rubyAdapter,
|
|
4279
4755
|
runNativeAudit,
|
|
4280
4756
|
runNpmAudit,
|
|
4281
4757
|
rustAdapter,
|
|
4282
4758
|
supportedRegistryEcosystems,
|
|
4759
|
+
swiftAdapter,
|
|
4283
4760
|
toCycloneDX,
|
|
4761
|
+
toLanguagePackageInput,
|
|
4284
4762
|
toSpdx,
|
|
4285
4763
|
triageCandidates
|
|
4286
4764
|
};
|