@wrongstack/techstack 0.293.0 → 0.295.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +1396 -929
- 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 +22 -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
|
@@ -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,81 @@ 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
|
+
let match;
|
|
595
|
+
while ((match = regex.exec(source)) !== null) {
|
|
596
|
+
const key = match[1];
|
|
597
|
+
const value = match[3];
|
|
598
|
+
if (key !== void 0 && value !== void 0) attributes.set(key, value);
|
|
599
|
+
}
|
|
600
|
+
return attributes;
|
|
601
|
+
}
|
|
602
|
+
function xmlTagValue(source, tag) {
|
|
603
|
+
const escaped = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
604
|
+
return new RegExp(`<${escaped}(?:\\s[^>]*)?>\\s*([^<]+?)\\s*</${escaped}>`, "i").exec(source)?.[1]?.trim();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/adapters/python.ts
|
|
511
608
|
function parseTomlSections(content) {
|
|
512
609
|
const sections = [];
|
|
513
610
|
let currentSection = "__header__";
|
|
@@ -595,6 +692,19 @@ function parsePyprojectDeps(content) {
|
|
|
595
692
|
}
|
|
596
693
|
}
|
|
597
694
|
}
|
|
695
|
+
if (section.name === "tool.poetry.dependencies" || section.name.startsWith("tool.poetry.group.")) {
|
|
696
|
+
const scope = section.name === "tool.poetry.dependencies" ? "runtime" : "development";
|
|
697
|
+
for (const raw of section.lines) {
|
|
698
|
+
const entry = parseTomlKeyValue(raw);
|
|
699
|
+
if (!entry) continue;
|
|
700
|
+
const name = entry.key;
|
|
701
|
+
if (!name || name.toLowerCase() === "python") continue;
|
|
702
|
+
const rawValue = entry.value;
|
|
703
|
+
const inlineVersion = rawValue.match(/\bversion\s*=\s*["']([^"']+)["']/)?.[1];
|
|
704
|
+
const stringVersion = rawValue.match(/^["']([^"']+)["']/)?.[1];
|
|
705
|
+
deps.push({ name, constraint: inlineVersion ?? stringVersion, scope });
|
|
706
|
+
}
|
|
707
|
+
}
|
|
598
708
|
}
|
|
599
709
|
return deps;
|
|
600
710
|
}
|
|
@@ -631,26 +741,60 @@ function parseRequirementsLockVersions(content) {
|
|
|
631
741
|
const line = raw.trim();
|
|
632
742
|
if (!line || line.startsWith("#") || line.startsWith("-")) continue;
|
|
633
743
|
const match = line.match(/^([a-zA-Z0-9][a-zA-Z0-9._-]*)\s*==\s*([^\s;]+)/);
|
|
634
|
-
if (match) versions.set(match[1], match[2]);
|
|
744
|
+
if (match?.[1] && match[2]) versions.set(normalizePkgName(match[1]), match[2]);
|
|
635
745
|
}
|
|
636
746
|
return versions;
|
|
637
747
|
}
|
|
748
|
+
function parsePipfileLock(content) {
|
|
749
|
+
const versions = /* @__PURE__ */ new Map();
|
|
750
|
+
try {
|
|
751
|
+
const json = JSON.parse(content);
|
|
752
|
+
for (const section of ["default", "develop"]) {
|
|
753
|
+
for (const [name, entry] of Object.entries(json[section] ?? {})) {
|
|
754
|
+
if (entry.version?.startsWith("==")) versions.set(normalizePkgName(name), entry.version.slice(2));
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
} catch {
|
|
758
|
+
}
|
|
759
|
+
return versions;
|
|
760
|
+
}
|
|
761
|
+
function parsePoetryLock(content) {
|
|
762
|
+
const versions = /* @__PURE__ */ new Map();
|
|
763
|
+
let currentName;
|
|
764
|
+
for (const raw of content.split("\n")) {
|
|
765
|
+
const line = raw.trim();
|
|
766
|
+
const nameMatch = line.match(/^name\s*=\s*"([^"]+)"/);
|
|
767
|
+
if (nameMatch) {
|
|
768
|
+
currentName = nameMatch[1];
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
const versionMatch = line.match(/^version\s*=\s*"([^"]+)"/);
|
|
772
|
+
if (versionMatch && currentName) {
|
|
773
|
+
versions.set(normalizePkgName(currentName), versionMatch[1]);
|
|
774
|
+
currentName = void 0;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return versions;
|
|
778
|
+
}
|
|
779
|
+
function normalizePkgName(name) {
|
|
780
|
+
return name.toLowerCase().replace(/_/g, "-");
|
|
781
|
+
}
|
|
638
782
|
var PythonAdapter = class {
|
|
639
783
|
ecosystem = "python";
|
|
640
784
|
async inventory(workspace, options) {
|
|
641
785
|
const observations = [];
|
|
642
786
|
const root = workspaceRoot(workspace, options);
|
|
643
787
|
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);
|
|
788
|
+
const hasPyproject = workspace.manifests.some((m) => m.includes("pyproject.toml")) || await fileExistsAsync(join2(root, "pyproject.toml"));
|
|
789
|
+
const hasRequirements = workspace.manifests.some((m) => m.includes("requirements.txt")) || await fileExistsAsync(join2(root, "requirements.txt"));
|
|
790
|
+
const hasPipfile = workspace.manifests.some((m) => m.includes("Pipfile")) || await fileExistsAsync(join2(root, "Pipfile"));
|
|
791
|
+
const lockfilePath = await this.detectLockfile(root);
|
|
648
792
|
let allDeps = [];
|
|
649
793
|
let pyprojectEv;
|
|
650
794
|
if (hasPyproject) {
|
|
651
795
|
try {
|
|
652
|
-
const content =
|
|
653
|
-
pyprojectEv =
|
|
796
|
+
const content = await readFile2(join2(root, "pyproject.toml"), "utf-8");
|
|
797
|
+
pyprojectEv = manifestEvidence(join2(root, "pyproject.toml"));
|
|
654
798
|
const parsed = parsePyprojectDeps(content);
|
|
655
799
|
for (const d of parsed) allDeps.push({ ...d, source: "pyproject.toml" });
|
|
656
800
|
} catch {
|
|
@@ -660,8 +804,8 @@ var PythonAdapter = class {
|
|
|
660
804
|
let requirementsEv;
|
|
661
805
|
if (hasRequirements) {
|
|
662
806
|
try {
|
|
663
|
-
const content =
|
|
664
|
-
requirementsEv =
|
|
807
|
+
const content = await readFile2(join2(root, "requirements.txt"), "utf-8");
|
|
808
|
+
requirementsEv = manifestEvidence(join2(root, "requirements.txt"));
|
|
665
809
|
const parsed = parseRequirementsTxt(content);
|
|
666
810
|
for (const d of parsed) {
|
|
667
811
|
if (!allDeps.some((existing) => existing.name === d.name)) {
|
|
@@ -674,8 +818,8 @@ var PythonAdapter = class {
|
|
|
674
818
|
}
|
|
675
819
|
if (hasPipfile) {
|
|
676
820
|
try {
|
|
677
|
-
const content =
|
|
678
|
-
if (!pyprojectEv) pyprojectEv =
|
|
821
|
+
const content = await readFile2(join2(root, "Pipfile"), "utf-8");
|
|
822
|
+
if (!pyprojectEv) pyprojectEv = manifestEvidence(join2(root, "Pipfile"));
|
|
679
823
|
const parsed = parsePipfileDeps(content);
|
|
680
824
|
for (const d of parsed) {
|
|
681
825
|
if (!allDeps.some((existing) => existing.name === d.name)) {
|
|
@@ -686,10 +830,13 @@ var PythonAdapter = class {
|
|
|
686
830
|
}
|
|
687
831
|
}
|
|
688
832
|
let lockEv;
|
|
833
|
+
const lockVersions = new Map(reqLockVersions);
|
|
689
834
|
if (lockfilePath) {
|
|
690
835
|
try {
|
|
691
|
-
|
|
692
|
-
|
|
836
|
+
const lockContent = await readFile2(lockfilePath, "utf-8");
|
|
837
|
+
const parsed = lockfilePath.endsWith("poetry.lock") || lockfilePath.endsWith("uv.lock") ? parsePoetryLock(lockContent) : parsePipfileLock(lockContent);
|
|
838
|
+
for (const [name, version] of parsed) lockVersions.set(name, version);
|
|
839
|
+
lockEv = lockfileEvidence(lockfilePath);
|
|
693
840
|
} catch {
|
|
694
841
|
}
|
|
695
842
|
}
|
|
@@ -697,7 +844,7 @@ var PythonAdapter = class {
|
|
|
697
844
|
for (const dep of allDeps) {
|
|
698
845
|
if (seen.has(dep.name)) continue;
|
|
699
846
|
seen.add(dep.name);
|
|
700
|
-
const locked =
|
|
847
|
+
const locked = lockVersions.get(normalizePkgName(dep.name));
|
|
701
848
|
const isRegistry = !dep.constraint || !dep.constraint.startsWith("file:") && !dep.constraint.startsWith("git+") && !dep.constraint.startsWith("-e");
|
|
702
849
|
const purl = isRegistry && locked ? buildPurl({ type: "python", name: dep.name, version: locked }) : isRegistry ? buildPurl({ type: "python", name: dep.name }) : void 0;
|
|
703
850
|
const evidence = [];
|
|
@@ -722,23 +869,31 @@ var PythonAdapter = class {
|
|
|
722
869
|
evidence
|
|
723
870
|
});
|
|
724
871
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
872
|
+
if (options.includeTransitive && lockEv) {
|
|
873
|
+
for (const [name, locked] of lockVersions) {
|
|
874
|
+
if (seen.has(name)) continue;
|
|
875
|
+
seen.add(name);
|
|
876
|
+
observations.push({
|
|
877
|
+
id: `dep-${workspace.id}-${name}`,
|
|
878
|
+
workspaceId: workspace.id,
|
|
879
|
+
purl: buildPurl({ type: "python", name, version: locked }),
|
|
880
|
+
ecosystem: "python",
|
|
881
|
+
name,
|
|
882
|
+
sourceType: "registry",
|
|
883
|
+
direct: false,
|
|
884
|
+
scope: "transitive",
|
|
885
|
+
locked,
|
|
886
|
+
status: "current",
|
|
887
|
+
evidence: [lockEv]
|
|
888
|
+
});
|
|
889
|
+
}
|
|
733
890
|
}
|
|
891
|
+
return observations;
|
|
734
892
|
}
|
|
735
|
-
detectLockfile(workspaceRoot2) {
|
|
893
|
+
async detectLockfile(workspaceRoot2) {
|
|
736
894
|
for (const file of ["Pipfile.lock", "poetry.lock", "uv.lock"]) {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
return join2(workspaceRoot2, file);
|
|
740
|
-
} catch {
|
|
741
|
-
}
|
|
895
|
+
const candidate = join2(workspaceRoot2, file);
|
|
896
|
+
if (await fileExistsAsync(candidate)) return candidate;
|
|
742
897
|
}
|
|
743
898
|
return void 0;
|
|
744
899
|
}
|
|
@@ -746,13 +901,7 @@ var PythonAdapter = class {
|
|
|
746
901
|
var pythonAdapter = new PythonAdapter();
|
|
747
902
|
|
|
748
903
|
// 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
|
-
}
|
|
904
|
+
import { readFileSync } from "node:fs";
|
|
756
905
|
function parseTomlSections2(content) {
|
|
757
906
|
const sections = [];
|
|
758
907
|
let currentSection = "__header__";
|
|
@@ -772,33 +921,29 @@ function parseTomlSections2(content) {
|
|
|
772
921
|
if (currentLines.length > 0) sections.push({ name: currentSection, lines: currentLines });
|
|
773
922
|
return sections;
|
|
774
923
|
}
|
|
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
924
|
function extractTomlDeps(sectionLines) {
|
|
783
925
|
const deps = [];
|
|
784
926
|
for (const raw of sectionLines) {
|
|
785
927
|
const line = raw.trim();
|
|
786
928
|
if (line.startsWith("#") || line === "") continue;
|
|
787
|
-
const
|
|
929
|
+
const entry = parseTomlKeyValue(line);
|
|
930
|
+
if (!entry) continue;
|
|
931
|
+
const tableMatch = entry.value.match(/^\{\s*(.*?)\s*\}$/);
|
|
788
932
|
if (tableMatch) {
|
|
789
|
-
const name =
|
|
790
|
-
const inner = tableMatch[
|
|
933
|
+
const name = entry.key;
|
|
934
|
+
const inner = tableMatch[1];
|
|
935
|
+
if (inner === void 0) continue;
|
|
791
936
|
const versionMatch = inner.match(/version\s*=\s*"([^"]+)"/);
|
|
792
|
-
|
|
937
|
+
const sourceType = /\bgit\s*=/.test(inner) ? "git" : /\bpath\s*=/.test(inner) ? "path" : "registry";
|
|
938
|
+
deps.push({ name, version: versionMatch?.[1], sourceType });
|
|
793
939
|
continue;
|
|
794
940
|
}
|
|
795
|
-
const simpleMatch =
|
|
941
|
+
const simpleMatch = entry.value.match(/^"([^"]*)"$/);
|
|
796
942
|
if (simpleMatch) {
|
|
797
|
-
deps.push({ name:
|
|
943
|
+
deps.push({ name: entry.key, version: simpleMatch[1] || void 0, sourceType: "registry" });
|
|
798
944
|
continue;
|
|
799
945
|
}
|
|
800
|
-
|
|
801
|
-
if (partialMatch && !partialMatch.value.startsWith("{") && !partialMatch.value.startsWith('"')) {
|
|
946
|
+
if (!entry.value.startsWith("{") && !entry.value.startsWith('"')) {
|
|
802
947
|
}
|
|
803
948
|
}
|
|
804
949
|
return deps;
|
|
@@ -854,23 +999,23 @@ var RustAdapter = class {
|
|
|
854
999
|
const observations = [];
|
|
855
1000
|
const root = workspaceRoot(workspace, options);
|
|
856
1001
|
const seen = /* @__PURE__ */ new Set();
|
|
857
|
-
const cargoTomlPath = workspace.manifests.find((m) => m.includes("Cargo.toml")) || (
|
|
1002
|
+
const cargoTomlPath = workspace.manifests.find((m) => m.includes("Cargo.toml")) || (fileExists(resolveIn(root, "Cargo.toml")) ? "Cargo.toml" : void 0);
|
|
858
1003
|
if (!cargoTomlPath) return [];
|
|
859
1004
|
const fullManifestPath = resolveIn(root, cargoTomlPath);
|
|
860
1005
|
let cargoContent;
|
|
861
1006
|
try {
|
|
862
|
-
cargoContent =
|
|
1007
|
+
cargoContent = readFileSync(fullManifestPath, "utf-8");
|
|
863
1008
|
} catch {
|
|
864
1009
|
return [];
|
|
865
1010
|
}
|
|
866
|
-
const manifestEv =
|
|
1011
|
+
const manifestEv = manifestEvidence(fullManifestPath);
|
|
867
1012
|
const cargoLockPath = resolveIn(root, "Cargo.lock");
|
|
868
1013
|
let lockVersions = /* @__PURE__ */ new Map();
|
|
869
1014
|
let lockEv;
|
|
870
1015
|
try {
|
|
871
|
-
const lockContent =
|
|
1016
|
+
const lockContent = readFileSync(cargoLockPath, "utf-8");
|
|
872
1017
|
lockVersions = parseCargoLock(lockContent);
|
|
873
|
-
lockEv =
|
|
1018
|
+
lockEv = lockfileEvidence(cargoLockPath);
|
|
874
1019
|
} catch {
|
|
875
1020
|
}
|
|
876
1021
|
const sections = parseTomlSections2(cargoContent);
|
|
@@ -891,18 +1036,18 @@ var RustAdapter = class {
|
|
|
891
1036
|
if (seen.has(dep.name)) continue;
|
|
892
1037
|
seen.add(dep.name);
|
|
893
1038
|
const locked = lockVersions.get(dep.name) || dep.version;
|
|
894
|
-
const isRegistry =
|
|
1039
|
+
const isRegistry = dep.sourceType === "registry";
|
|
895
1040
|
const purl = isRegistry && locked ? buildPurl({ type: "rust", name: dep.name, version: locked }) : isRegistry ? buildPurl({ type: "rust", name: dep.name }) : void 0;
|
|
896
1041
|
const evidence = [manifestEv];
|
|
897
1042
|
if (lockEv && locked && lockVersions.has(dep.name)) evidence.push(lockEv);
|
|
898
|
-
const status = dep.
|
|
1043
|
+
const status = dep.sourceType === "git" ? "git_dependency" : dep.sourceType === "path" ? "local_path" : "current";
|
|
899
1044
|
observations.push({
|
|
900
1045
|
id: `dep-${workspace.id}-${dep.name}`,
|
|
901
1046
|
workspaceId: workspace.id,
|
|
902
1047
|
...purl ? { purl } : {},
|
|
903
1048
|
ecosystem: "rust",
|
|
904
1049
|
name: dep.name,
|
|
905
|
-
sourceType:
|
|
1050
|
+
sourceType: dep.sourceType,
|
|
906
1051
|
direct: true,
|
|
907
1052
|
scope,
|
|
908
1053
|
...dep.version ? { requested: dep.version } : {},
|
|
@@ -912,27 +1057,32 @@ var RustAdapter = class {
|
|
|
912
1057
|
});
|
|
913
1058
|
}
|
|
914
1059
|
}
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1060
|
+
if (options.includeTransitive && lockEv) {
|
|
1061
|
+
for (const [name, locked] of lockVersions) {
|
|
1062
|
+
if (seen.has(name)) continue;
|
|
1063
|
+
seen.add(name);
|
|
1064
|
+
observations.push({
|
|
1065
|
+
id: `dep-${workspace.id}-${name}`,
|
|
1066
|
+
workspaceId: workspace.id,
|
|
1067
|
+
purl: buildPurl({ type: "rust", name, version: locked }),
|
|
1068
|
+
ecosystem: "rust",
|
|
1069
|
+
name,
|
|
1070
|
+
sourceType: "registry",
|
|
1071
|
+
direct: false,
|
|
1072
|
+
scope: "transitive",
|
|
1073
|
+
locked,
|
|
1074
|
+
status: "current",
|
|
1075
|
+
evidence: [lockEv]
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
923
1078
|
}
|
|
1079
|
+
return observations;
|
|
924
1080
|
}
|
|
925
1081
|
};
|
|
926
1082
|
var rustAdapter = new RustAdapter();
|
|
927
1083
|
|
|
928
1084
|
// 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
|
-
}
|
|
1085
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
936
1086
|
function cleanGoVersion(v) {
|
|
937
1087
|
return v.replace(/^v/i, "");
|
|
938
1088
|
}
|
|
@@ -973,6 +1123,30 @@ function parseGoMod(content) {
|
|
|
973
1123
|
}
|
|
974
1124
|
return deps;
|
|
975
1125
|
}
|
|
1126
|
+
function parseGoReplacements(content) {
|
|
1127
|
+
const replacements = /* @__PURE__ */ new Map();
|
|
1128
|
+
let inBlock = false;
|
|
1129
|
+
for (const raw of content.split("\n")) {
|
|
1130
|
+
const line = raw.trim();
|
|
1131
|
+
if (line === "replace (") {
|
|
1132
|
+
inBlock = true;
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
if (inBlock && line === ")") {
|
|
1136
|
+
inBlock = false;
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
const candidate = inBlock ? line : line.startsWith("replace ") ? line.slice(8).trim() : "";
|
|
1140
|
+
const match = candidate.match(/^(\S+)(?:\s+v\S+)?\s+=>\s+(\S+)/);
|
|
1141
|
+
if (!match) continue;
|
|
1142
|
+
const modulePath = match[1];
|
|
1143
|
+
const target = match[2];
|
|
1144
|
+
if (!modulePath || !target) continue;
|
|
1145
|
+
const local = target.startsWith(".") || target.startsWith("/") || /^[A-Za-z]:[\\/]/.test(target);
|
|
1146
|
+
replacements.set(modulePath, local ? "path" : "git");
|
|
1147
|
+
}
|
|
1148
|
+
return replacements;
|
|
1149
|
+
}
|
|
976
1150
|
function parseGoSum(content) {
|
|
977
1151
|
const versions = /* @__PURE__ */ new Map();
|
|
978
1152
|
for (const raw of content.split("\n")) {
|
|
@@ -1003,25 +1177,26 @@ var GoAdapter = class {
|
|
|
1003
1177
|
const observations = [];
|
|
1004
1178
|
const root = workspaceRoot(workspace, options);
|
|
1005
1179
|
const seen = /* @__PURE__ */ new Set();
|
|
1006
|
-
const goModPath = workspace.manifests.find((m) => m.includes("go.mod")) || (
|
|
1180
|
+
const goModPath = workspace.manifests.find((m) => m.includes("go.mod")) || (fileExists(resolveIn(root, "go.mod")) ? "go.mod" : void 0);
|
|
1007
1181
|
if (!goModPath) return [];
|
|
1008
1182
|
const fullManifestPath = resolveIn(root, goModPath);
|
|
1009
1183
|
let goModContent;
|
|
1010
1184
|
try {
|
|
1011
|
-
goModContent =
|
|
1185
|
+
goModContent = readFileSync2(fullManifestPath, "utf-8");
|
|
1012
1186
|
} catch {
|
|
1013
1187
|
return [];
|
|
1014
1188
|
}
|
|
1015
|
-
const manifestEv =
|
|
1189
|
+
const manifestEv = manifestEvidence(fullManifestPath);
|
|
1016
1190
|
const requires = parseGoMod(goModContent);
|
|
1191
|
+
const replacements = parseGoReplacements(goModContent);
|
|
1017
1192
|
const modName = parseGoModuleName(goModContent);
|
|
1018
1193
|
const goSumPath = resolveIn(root, "go.sum");
|
|
1019
1194
|
let lockVersions = /* @__PURE__ */ new Map();
|
|
1020
1195
|
let lockEv;
|
|
1021
1196
|
try {
|
|
1022
|
-
const sumContent =
|
|
1197
|
+
const sumContent = readFileSync2(goSumPath, "utf-8");
|
|
1023
1198
|
lockVersions = parseGoSum(sumContent);
|
|
1024
|
-
lockEv =
|
|
1199
|
+
lockEv = lockfileEvidence(goSumPath);
|
|
1025
1200
|
} catch {
|
|
1026
1201
|
}
|
|
1027
1202
|
for (const req of requires) {
|
|
@@ -1031,53 +1206,44 @@ var GoAdapter = class {
|
|
|
1031
1206
|
const scope = req.indirect ? "transitive" : "runtime";
|
|
1032
1207
|
const direct = !req.indirect;
|
|
1033
1208
|
const locked = lockVersions.get(req.modulePath) || req.version;
|
|
1034
|
-
const
|
|
1209
|
+
const replacement = replacements.get(req.modulePath);
|
|
1210
|
+
const purl = replacement ? void 0 : buildPurl({ type: "go", name: req.modulePath, version: locked });
|
|
1035
1211
|
const evidence = [manifestEv];
|
|
1036
1212
|
if (lockEv && lockVersions.has(req.modulePath)) evidence.push(lockEv);
|
|
1037
1213
|
observations.push({
|
|
1038
1214
|
id: `dep-${workspace.id}-${req.modulePath}`,
|
|
1039
1215
|
workspaceId: workspace.id,
|
|
1040
|
-
purl,
|
|
1216
|
+
...purl ? { purl } : {},
|
|
1041
1217
|
ecosystem: "go",
|
|
1042
1218
|
name: req.modulePath,
|
|
1043
|
-
sourceType: "registry",
|
|
1219
|
+
sourceType: replacement ?? "registry",
|
|
1044
1220
|
direct,
|
|
1045
1221
|
scope,
|
|
1046
1222
|
requested: req.version,
|
|
1047
1223
|
...locked ? { locked } : {},
|
|
1048
|
-
status: "current",
|
|
1224
|
+
status: replacement === "path" ? "local_path" : replacement === "git" ? "git_dependency" : "current",
|
|
1049
1225
|
evidence
|
|
1050
1226
|
});
|
|
1051
1227
|
}
|
|
1052
1228
|
return observations;
|
|
1053
1229
|
}
|
|
1054
|
-
fileExists(filePath) {
|
|
1055
|
-
try {
|
|
1056
|
-
readFileSync4(filePath, "utf-8");
|
|
1057
|
-
return true;
|
|
1058
|
-
} catch {
|
|
1059
|
-
return false;
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
1230
|
};
|
|
1063
1231
|
var goAdapter = new GoAdapter();
|
|
1064
1232
|
|
|
1065
1233
|
// src/adapters/dotnet.ts
|
|
1066
|
-
import {
|
|
1234
|
+
import { readFile as readFile3, readdir } from "node:fs/promises";
|
|
1067
1235
|
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
1236
|
function parseCsproj(content) {
|
|
1075
1237
|
const refs = [];
|
|
1076
|
-
const regex = /<PackageReference\
|
|
1238
|
+
const regex = /<PackageReference\b([^>]*?)(?:\/>|>([\s\S]*?)<\/PackageReference>)/gi;
|
|
1077
1239
|
let match;
|
|
1078
1240
|
while ((match = regex.exec(content)) !== null) {
|
|
1079
|
-
const
|
|
1080
|
-
const
|
|
1241
|
+
const attributes = match[1] ?? "";
|
|
1242
|
+
const body = match[2] ?? "";
|
|
1243
|
+
const parsedAttributes = parseXmlAttributes(attributes);
|
|
1244
|
+
const name = parsedAttributes.get("Include");
|
|
1245
|
+
if (!name) continue;
|
|
1246
|
+
const version = parsedAttributes.get("Version") ?? xmlTagValue(body, "Version");
|
|
1081
1247
|
refs.push({ name, version });
|
|
1082
1248
|
}
|
|
1083
1249
|
return refs;
|
|
@@ -1110,7 +1276,7 @@ var DotNetAdapter = class {
|
|
|
1110
1276
|
const seen = /* @__PURE__ */ new Set();
|
|
1111
1277
|
let csprojPath;
|
|
1112
1278
|
try {
|
|
1113
|
-
const files =
|
|
1279
|
+
const files = await readdir(root);
|
|
1114
1280
|
const csproj = files.find((f) => f.endsWith(".csproj"));
|
|
1115
1281
|
if (csproj) csprojPath = join3(root, csproj);
|
|
1116
1282
|
} catch {
|
|
@@ -1118,19 +1284,19 @@ var DotNetAdapter = class {
|
|
|
1118
1284
|
if (!csprojPath) return [];
|
|
1119
1285
|
let csprojContent;
|
|
1120
1286
|
try {
|
|
1121
|
-
csprojContent =
|
|
1287
|
+
csprojContent = await readFile3(csprojPath, "utf-8");
|
|
1122
1288
|
} catch {
|
|
1123
1289
|
return [];
|
|
1124
1290
|
}
|
|
1125
|
-
const manifestEv =
|
|
1291
|
+
const manifestEv = manifestEvidence(csprojPath);
|
|
1126
1292
|
const refs = parseCsproj(csprojContent);
|
|
1127
1293
|
const assetsPath = join3(root, "project.assets.json");
|
|
1128
1294
|
let lockVersions = /* @__PURE__ */ new Map();
|
|
1129
1295
|
let lockEv;
|
|
1130
1296
|
try {
|
|
1131
|
-
const assetsContent =
|
|
1297
|
+
const assetsContent = await readFile3(assetsPath, "utf-8");
|
|
1132
1298
|
lockVersions = parseProjectAssetsJson(assetsContent);
|
|
1133
|
-
lockEv =
|
|
1299
|
+
lockEv = lockfileEvidence(assetsPath);
|
|
1134
1300
|
} catch {
|
|
1135
1301
|
}
|
|
1136
1302
|
for (const ref of refs) {
|
|
@@ -1161,14 +1327,8 @@ var DotNetAdapter = class {
|
|
|
1161
1327
|
var dotNetAdapter = new DotNetAdapter();
|
|
1162
1328
|
|
|
1163
1329
|
// src/adapters/php.ts
|
|
1164
|
-
import { readFileSync as
|
|
1330
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
1165
1331
|
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
1332
|
function parseComposerLock(content) {
|
|
1173
1333
|
const versions = /* @__PURE__ */ new Map();
|
|
1174
1334
|
try {
|
|
@@ -1196,15 +1356,15 @@ var PhpAdapter = class {
|
|
|
1196
1356
|
const observations = [];
|
|
1197
1357
|
const root = workspaceRoot(workspace, options);
|
|
1198
1358
|
const seen = /* @__PURE__ */ new Set();
|
|
1199
|
-
const composerJsonPath = workspace.manifests.find((m) => m.includes("composer.json")) || (
|
|
1359
|
+
const composerJsonPath = workspace.manifests.find((m) => m.includes("composer.json")) || (fileExists(join4(root, "composer.json")) ? join4(root, "composer.json") : void 0);
|
|
1200
1360
|
if (!composerJsonPath) return [];
|
|
1201
1361
|
let content;
|
|
1202
1362
|
try {
|
|
1203
|
-
content =
|
|
1363
|
+
content = readFileSync3(composerJsonPath, "utf-8");
|
|
1204
1364
|
} catch {
|
|
1205
1365
|
return [];
|
|
1206
1366
|
}
|
|
1207
|
-
const manifestEv =
|
|
1367
|
+
const manifestEv = manifestEvidence(composerJsonPath);
|
|
1208
1368
|
let composerJson;
|
|
1209
1369
|
try {
|
|
1210
1370
|
composerJson = JSON.parse(content);
|
|
@@ -1215,9 +1375,9 @@ var PhpAdapter = class {
|
|
|
1215
1375
|
let lockVersions = /* @__PURE__ */ new Map();
|
|
1216
1376
|
let lockEv;
|
|
1217
1377
|
try {
|
|
1218
|
-
const lockContent =
|
|
1378
|
+
const lockContent = readFileSync3(lockPath, "utf-8");
|
|
1219
1379
|
lockVersions = parseComposerLock(lockContent);
|
|
1220
|
-
lockEv =
|
|
1380
|
+
lockEv = lockfileEvidence(lockPath);
|
|
1221
1381
|
} catch {
|
|
1222
1382
|
}
|
|
1223
1383
|
const sections = [
|
|
@@ -1254,26 +1414,12 @@ var PhpAdapter = class {
|
|
|
1254
1414
|
}
|
|
1255
1415
|
return observations;
|
|
1256
1416
|
}
|
|
1257
|
-
fileExists(filePath) {
|
|
1258
|
-
try {
|
|
1259
|
-
readFileSync6(filePath, "utf-8");
|
|
1260
|
-
return true;
|
|
1261
|
-
} catch {
|
|
1262
|
-
return false;
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
1417
|
};
|
|
1266
1418
|
var phpAdapter = new PhpAdapter();
|
|
1267
1419
|
|
|
1268
1420
|
// src/adapters/dart.ts
|
|
1269
|
-
import { readFileSync as
|
|
1421
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
1270
1422
|
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
1423
|
function parsePubspecYaml(content) {
|
|
1278
1424
|
const sections = /* @__PURE__ */ new Map();
|
|
1279
1425
|
let currentSection;
|
|
@@ -1301,6 +1447,14 @@ function parsePubspecYaml(content) {
|
|
|
1301
1447
|
}
|
|
1302
1448
|
const sec = sections.get(currentSection);
|
|
1303
1449
|
sec.set(currentName, constraint);
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
if (currentName && line.startsWith(" ")) {
|
|
1453
|
+
const sec = sections.get(currentSection);
|
|
1454
|
+
if (!sec) continue;
|
|
1455
|
+
if (/^sdk:\s*flutter\b/.test(trimmed)) sec.set(currentName, "sdk:flutter");
|
|
1456
|
+
else if (/^git:\s*/.test(trimmed)) sec.set(currentName, `git:${trimmed.slice(4).trim()}`);
|
|
1457
|
+
else if (/^path:\s*/.test(trimmed)) sec.set(currentName, `path:${trimmed.slice(5).trim()}`);
|
|
1304
1458
|
}
|
|
1305
1459
|
}
|
|
1306
1460
|
return sections;
|
|
@@ -1339,23 +1493,23 @@ var DartAdapter = class {
|
|
|
1339
1493
|
const observations = [];
|
|
1340
1494
|
const root = workspaceRoot(workspace, options);
|
|
1341
1495
|
const seen = /* @__PURE__ */ new Set();
|
|
1342
|
-
const pubspecPath = workspace.manifests.find((m) => m.includes("pubspec.yaml")) || (
|
|
1496
|
+
const pubspecPath = workspace.manifests.find((m) => m.includes("pubspec.yaml")) || (fileExists(join5(root, "pubspec.yaml")) ? join5(root, "pubspec.yaml") : void 0);
|
|
1343
1497
|
if (!pubspecPath) return [];
|
|
1344
1498
|
let content;
|
|
1345
1499
|
try {
|
|
1346
|
-
content =
|
|
1500
|
+
content = readFileSync4(pubspecPath, "utf-8");
|
|
1347
1501
|
} catch {
|
|
1348
1502
|
return [];
|
|
1349
1503
|
}
|
|
1350
|
-
const manifestEv =
|
|
1504
|
+
const manifestEv = manifestEvidence(pubspecPath);
|
|
1351
1505
|
const sections = parsePubspecYaml(content);
|
|
1352
1506
|
const lockPath = join5(root, "pubspec.lock");
|
|
1353
1507
|
let lockVersions = /* @__PURE__ */ new Map();
|
|
1354
1508
|
let lockEv;
|
|
1355
1509
|
try {
|
|
1356
|
-
const lockContent =
|
|
1510
|
+
const lockContent = readFileSync4(lockPath, "utf-8");
|
|
1357
1511
|
lockVersions = parsePubspecLock(lockContent);
|
|
1358
|
-
lockEv =
|
|
1512
|
+
lockEv = lockfileEvidence(lockPath);
|
|
1359
1513
|
} catch {
|
|
1360
1514
|
}
|
|
1361
1515
|
const sectionMapping = [
|
|
@@ -1369,7 +1523,7 @@ var DartAdapter = class {
|
|
|
1369
1523
|
for (const [name, constraint] of deps) {
|
|
1370
1524
|
if (seen.has(name)) continue;
|
|
1371
1525
|
seen.add(name);
|
|
1372
|
-
if (constraint === "*" || constraint.startsWith("{")) continue;
|
|
1526
|
+
if (constraint === "*" || constraint === "sdk:flutter" || constraint.startsWith("{")) continue;
|
|
1373
1527
|
const locked = lockVersions.get(name);
|
|
1374
1528
|
let status = "current";
|
|
1375
1529
|
let sourceType = "registry";
|
|
@@ -1405,32 +1559,57 @@ var DartAdapter = class {
|
|
|
1405
1559
|
}
|
|
1406
1560
|
return observations;
|
|
1407
1561
|
}
|
|
1408
|
-
fileExists(filePath) {
|
|
1409
|
-
try {
|
|
1410
|
-
readFileSync7(filePath, "utf-8");
|
|
1411
|
-
return true;
|
|
1412
|
-
} catch {
|
|
1413
|
-
return false;
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
1562
|
};
|
|
1417
1563
|
var dartAdapter = new DartAdapter();
|
|
1418
1564
|
|
|
1419
1565
|
// src/adapters/maven.ts
|
|
1420
|
-
import { readFileSync as
|
|
1421
|
-
function manifestEvidence8(path) {
|
|
1422
|
-
return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1423
|
-
}
|
|
1566
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
1424
1567
|
function parsePomDependencies(xml) {
|
|
1425
1568
|
const deps = [];
|
|
1569
|
+
const properties = /* @__PURE__ */ new Map();
|
|
1570
|
+
const propertiesBlock = /<properties>([\s\S]*?)<\/properties>/.exec(xml)?.[1] ?? "";
|
|
1571
|
+
const propertyRegex = /<([A-Za-z0-9_.-]+)>\s*([^<]+?)\s*<\/\1>/g;
|
|
1572
|
+
let propertyMatch;
|
|
1573
|
+
while ((propertyMatch = propertyRegex.exec(propertiesBlock)) !== null) {
|
|
1574
|
+
const key = propertyMatch[1];
|
|
1575
|
+
const value = propertyMatch[2];
|
|
1576
|
+
if (key && value) properties.set(key, value.trim());
|
|
1577
|
+
}
|
|
1578
|
+
const parentBlock = /<parent>([\s\S]*?)<\/parent>/.exec(xml)?.[1];
|
|
1579
|
+
if (parentBlock) {
|
|
1580
|
+
const parentVersion = xmlTagValue(parentBlock, "version");
|
|
1581
|
+
const parentGroup = xmlTagValue(parentBlock, "groupId");
|
|
1582
|
+
if (parentVersion) {
|
|
1583
|
+
properties.set("parent.version", parentVersion);
|
|
1584
|
+
properties.set("project.parent.version", parentVersion);
|
|
1585
|
+
}
|
|
1586
|
+
if (parentGroup) {
|
|
1587
|
+
properties.set("parent.groupId", parentGroup);
|
|
1588
|
+
properties.set("project.parent.groupId", parentGroup);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
const managed = /* @__PURE__ */ new Map();
|
|
1592
|
+
const managementBlock = /<dependencyManagement>([\s\S]*?)<\/dependencyManagement>/.exec(xml)?.[1] ?? "";
|
|
1593
|
+
const managementRegex = /<dependency>\s*([\s\S]*?)<\/dependency>/g;
|
|
1594
|
+
let managementMatch;
|
|
1595
|
+
while ((managementMatch = managementRegex.exec(managementBlock)) !== null) {
|
|
1596
|
+
const block = managementMatch[1];
|
|
1597
|
+
if (!block) continue;
|
|
1598
|
+
const groupId = xmlTagValue(block, "groupId");
|
|
1599
|
+
const artifactId = xmlTagValue(block, "artifactId");
|
|
1600
|
+
const version = xmlTagValue(block, "version");
|
|
1601
|
+
if (groupId && artifactId && version) managed.set(`${groupId}:${artifactId}`, version);
|
|
1602
|
+
}
|
|
1603
|
+
const directXml = xml.replace(/<dependencyManagement>[\s\S]*?<\/dependencyManagement>/g, "");
|
|
1426
1604
|
const depRegex = /<dependency>\s*([\s\S]*?)<\/dependency>/g;
|
|
1427
1605
|
let match;
|
|
1428
|
-
while ((match = depRegex.exec(
|
|
1606
|
+
while ((match = depRegex.exec(directXml)) !== null) {
|
|
1429
1607
|
const block = match[1];
|
|
1430
|
-
const groupId = block
|
|
1431
|
-
const artifactId = block
|
|
1432
|
-
const
|
|
1433
|
-
const
|
|
1608
|
+
const groupId = xmlTagValue(block, "groupId");
|
|
1609
|
+
const artifactId = xmlTagValue(block, "artifactId");
|
|
1610
|
+
const rawVersion = xmlTagValue(block, "version") ?? (groupId && artifactId ? managed.get(`${groupId}:${artifactId}`) : void 0);
|
|
1611
|
+
const version = rawVersion?.replace(/\$\{([^}]+)\}/g, (_whole, key) => properties.get(key) ?? `\${${key}}`);
|
|
1612
|
+
const scope = xmlTagValue(block, "scope");
|
|
1434
1613
|
if (groupId && artifactId) {
|
|
1435
1614
|
deps.push({ groupId, artifactId, version, scope });
|
|
1436
1615
|
}
|
|
@@ -1459,11 +1638,11 @@ var MavenAdapter = class {
|
|
|
1459
1638
|
if (!pomPath) return [];
|
|
1460
1639
|
let content;
|
|
1461
1640
|
try {
|
|
1462
|
-
content =
|
|
1641
|
+
content = readFileSync5(pomPath, "utf-8");
|
|
1463
1642
|
} catch {
|
|
1464
1643
|
return [];
|
|
1465
1644
|
}
|
|
1466
|
-
const manifestEv =
|
|
1645
|
+
const manifestEv = manifestEvidence(pomPath);
|
|
1467
1646
|
const deps = parsePomDependencies(content);
|
|
1468
1647
|
const seen = /* @__PURE__ */ new Set();
|
|
1469
1648
|
for (const dep of deps) {
|
|
@@ -1490,22 +1669,141 @@ var MavenAdapter = class {
|
|
|
1490
1669
|
};
|
|
1491
1670
|
var mavenAdapter = new MavenAdapter();
|
|
1492
1671
|
|
|
1493
|
-
// src/adapters/
|
|
1494
|
-
import {
|
|
1495
|
-
|
|
1496
|
-
|
|
1672
|
+
// src/adapters/gradle.ts
|
|
1673
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
1674
|
+
import { join as join6 } from "node:path";
|
|
1675
|
+
function scopeForConfiguration(configuration) {
|
|
1676
|
+
if (/test/i.test(configuration)) return "development";
|
|
1677
|
+
if (/compileOnly|annotationProcessor/i.test(configuration)) return "build";
|
|
1678
|
+
if (/runtimeOnly/i.test(configuration)) return "runtime";
|
|
1679
|
+
return "runtime";
|
|
1497
1680
|
}
|
|
1498
|
-
function
|
|
1499
|
-
|
|
1681
|
+
function parseVersionCatalog(content) {
|
|
1682
|
+
const versions = /* @__PURE__ */ new Map();
|
|
1683
|
+
const libraries = /* @__PURE__ */ new Map();
|
|
1684
|
+
let section = "";
|
|
1685
|
+
for (const raw of content.split("\n")) {
|
|
1686
|
+
const line = raw.replace(/#.*$/, "").trim();
|
|
1687
|
+
const header = /^\[([^\]]+)\]$/.exec(line);
|
|
1688
|
+
if (header) {
|
|
1689
|
+
section = header[1] ?? "";
|
|
1690
|
+
continue;
|
|
1691
|
+
}
|
|
1692
|
+
const entry = /^(?:"([^"]+)"|([\w.-]+))\s*=\s*(.+)$/.exec(line);
|
|
1693
|
+
if (!entry) continue;
|
|
1694
|
+
const key = entry[1] ?? entry[2];
|
|
1695
|
+
const value = entry[3];
|
|
1696
|
+
if (!key || !value) continue;
|
|
1697
|
+
if (section === "versions") {
|
|
1698
|
+
const version = /^["']([^"']+)["']/.exec(value)?.[1];
|
|
1699
|
+
if (version) versions.set(key, version);
|
|
1700
|
+
} else if (section === "libraries") {
|
|
1701
|
+
const module = /\bmodule\s*=\s*["']([^"']+)["']/.exec(value)?.[1] ?? (() => {
|
|
1702
|
+
const group = /\bgroup\s*=\s*["']([^"']+)["']/.exec(value)?.[1];
|
|
1703
|
+
const name = /\bname\s*=\s*["']([^"']+)["']/.exec(value)?.[1];
|
|
1704
|
+
return group && name ? `${group}:${name}` : void 0;
|
|
1705
|
+
})();
|
|
1706
|
+
if (!module) continue;
|
|
1707
|
+
const version = /\bversion\s*=\s*["']([^"']+)["']/.exec(value)?.[1];
|
|
1708
|
+
const ref = /\bversion\.ref\s*=\s*["']([^"']+)["']/.exec(value)?.[1];
|
|
1709
|
+
libraries.set(key.replace(/-/g, "."), `${module}:${version ?? (ref ? versions.get(ref) ?? "" : "")}`.replace(/:$/, ""));
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
return libraries;
|
|
1713
|
+
}
|
|
1714
|
+
function parseGradleManifest(content, catalog) {
|
|
1715
|
+
const deps = [];
|
|
1716
|
+
const coordinateRegex = /\b(implementation|api|compileOnly|runtimeOnly|testImplementation|testRuntimeOnly|annotationProcessor)\s*(?:\(|\s)\s*["']([^"']+)["']/g;
|
|
1717
|
+
let match;
|
|
1718
|
+
while ((match = coordinateRegex.exec(content)) !== null) {
|
|
1719
|
+
const configuration = match[1];
|
|
1720
|
+
const value = match[2];
|
|
1721
|
+
if (!configuration || !value) continue;
|
|
1722
|
+
const [group, artifact, version] = value.split(":");
|
|
1723
|
+
if (!group || !artifact) continue;
|
|
1724
|
+
deps.push({ name: `${group}:${artifact}`, requested: version, scope: scopeForConfiguration(configuration) });
|
|
1725
|
+
}
|
|
1726
|
+
const aliasRegex = /\b(implementation|api|compileOnly|runtimeOnly|testImplementation|testRuntimeOnly)\s*\(\s*libs\.([\w.]+)\s*\)/g;
|
|
1727
|
+
while ((match = aliasRegex.exec(content)) !== null) {
|
|
1728
|
+
const configuration = match[1];
|
|
1729
|
+
const alias = match[2];
|
|
1730
|
+
if (!configuration || !alias) continue;
|
|
1731
|
+
const coordinate = catalog.get(alias);
|
|
1732
|
+
if (!coordinate) continue;
|
|
1733
|
+
const [group, artifact, version] = coordinate.split(":");
|
|
1734
|
+
if (!group || !artifact) continue;
|
|
1735
|
+
deps.push({ name: `${group}:${artifact}`, requested: version, scope: scopeForConfiguration(configuration) });
|
|
1736
|
+
}
|
|
1737
|
+
return deps;
|
|
1500
1738
|
}
|
|
1739
|
+
function parseGradleLock(content) {
|
|
1740
|
+
const locked = /* @__PURE__ */ new Map();
|
|
1741
|
+
for (const raw of content.split("\n")) {
|
|
1742
|
+
const line = raw.replace(/#.*/, "").trim();
|
|
1743
|
+
const coordinate = /^([^:\s=]+):([^:\s=]+):([^=\s]+)(?:=.*)?$/.exec(line);
|
|
1744
|
+
if (coordinate?.[1] && coordinate[2] && coordinate[3]) locked.set(`${coordinate[1]}:${coordinate[2]}`, coordinate[3]);
|
|
1745
|
+
}
|
|
1746
|
+
return locked;
|
|
1747
|
+
}
|
|
1748
|
+
var GradleAdapter = class {
|
|
1749
|
+
ecosystem = "gradle";
|
|
1750
|
+
async inventory(workspace, options) {
|
|
1751
|
+
const root = workspaceRoot(workspace, options);
|
|
1752
|
+
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"));
|
|
1753
|
+
if (!await fileExistsAsync(resolveIn(root, manifestPath))) return [];
|
|
1754
|
+
const catalogPath = join6(root, "gradle", "libs.versions.toml");
|
|
1755
|
+
const catalog = await fileExistsAsync(catalogPath) ? parseVersionCatalog(await readFile4(catalogPath, "utf8")) : /* @__PURE__ */ new Map();
|
|
1756
|
+
const direct = parseGradleManifest(await readFile4(resolveIn(root, manifestPath), "utf8"), catalog);
|
|
1757
|
+
const lockPath = workspace.lockfiles.find((path) => path.endsWith("gradle.lockfile")) ?? join6(root, "gradle.lockfile");
|
|
1758
|
+
const locked = await fileExistsAsync(resolveIn(root, lockPath)) ? parseGradleLock(await readFile4(resolveIn(root, lockPath), "utf8")) : /* @__PURE__ */ new Map();
|
|
1759
|
+
const manifestEv = manifestEvidence(resolveIn(root, manifestPath));
|
|
1760
|
+
const lockEv = locked.size > 0 ? lockfileEvidence(resolveIn(root, lockPath)) : void 0;
|
|
1761
|
+
const observations = [];
|
|
1762
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1763
|
+
const add = (name, requested, scope, isDirect) => {
|
|
1764
|
+
if (seen.has(name)) return;
|
|
1765
|
+
seen.add(name);
|
|
1766
|
+
const version = locked.get(name) ?? requested;
|
|
1767
|
+
const lockedVersion = locked.get(name);
|
|
1768
|
+
const evidence = isDirect ? [manifestEv] : [];
|
|
1769
|
+
if (lockEv && locked.has(name)) evidence.push(lockEv);
|
|
1770
|
+
observations.push({
|
|
1771
|
+
id: `dep-${workspace.id}-${name}`,
|
|
1772
|
+
workspaceId: workspace.id,
|
|
1773
|
+
purl: buildPurl({ type: "maven", name, ...version ? { version } : {} }),
|
|
1774
|
+
ecosystem: "gradle",
|
|
1775
|
+
name,
|
|
1776
|
+
sourceType: "registry",
|
|
1777
|
+
direct: isDirect,
|
|
1778
|
+
scope: isDirect ? scope : "transitive",
|
|
1779
|
+
...requested ? { requested } : {},
|
|
1780
|
+
...lockedVersion ? { locked: lockedVersion } : {},
|
|
1781
|
+
status: "current",
|
|
1782
|
+
evidence
|
|
1783
|
+
});
|
|
1784
|
+
};
|
|
1785
|
+
for (const dep of direct) add(dep.name, dep.requested, dep.scope, true);
|
|
1786
|
+
if (options.includeTransitive) {
|
|
1787
|
+
for (const [name] of locked) add(name, void 0, "transitive", false);
|
|
1788
|
+
}
|
|
1789
|
+
return observations;
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
var gradleAdapter = new GradleAdapter();
|
|
1793
|
+
|
|
1794
|
+
// src/adapters/ruby.ts
|
|
1795
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
1501
1796
|
function parseGemfile(content) {
|
|
1502
1797
|
const gems = [];
|
|
1503
|
-
const gemRegex = /gem\s+['"]([^'"]+)['"](
|
|
1798
|
+
const gemRegex = /gem\s+['"]([^'"]+)['"]([^\n]*)/g;
|
|
1504
1799
|
let match;
|
|
1505
1800
|
while ((match = gemRegex.exec(content)) !== null) {
|
|
1506
1801
|
const name = match[1];
|
|
1507
1802
|
if (name === "rails" || name === "ruby") continue;
|
|
1508
|
-
|
|
1803
|
+
const tail = match[2] ?? "";
|
|
1804
|
+
const version = /^\s*,\s*['"]([^'"]+)['"]/.exec(tail)?.[1];
|
|
1805
|
+
const sourceType = /\b(?:git|github):/.test(tail) ? "git" : /\bpath:/.test(tail) ? "path" : "registry";
|
|
1806
|
+
gems.push({ name, version, sourceType });
|
|
1509
1807
|
}
|
|
1510
1808
|
return gems;
|
|
1511
1809
|
}
|
|
@@ -1514,7 +1812,11 @@ function parseGemfileLock(content) {
|
|
|
1514
1812
|
const lines = content.split("\n");
|
|
1515
1813
|
let inSpecs = false;
|
|
1516
1814
|
for (const line of lines) {
|
|
1517
|
-
if (line.
|
|
1815
|
+
if (/^(?:GEM|GIT|PATH)$/.test(line.trim())) {
|
|
1816
|
+
inSpecs = false;
|
|
1817
|
+
continue;
|
|
1818
|
+
}
|
|
1819
|
+
if (/^\s{2}specs:\s*$/.test(line)) {
|
|
1518
1820
|
inSpecs = true;
|
|
1519
1821
|
continue;
|
|
1520
1822
|
}
|
|
@@ -1539,11 +1841,11 @@ var RubyAdapter = class {
|
|
|
1539
1841
|
if (!gemfilePath) return [];
|
|
1540
1842
|
let content;
|
|
1541
1843
|
try {
|
|
1542
|
-
content =
|
|
1844
|
+
content = readFileSync6(gemfilePath, "utf-8");
|
|
1543
1845
|
} catch {
|
|
1544
1846
|
return [];
|
|
1545
1847
|
}
|
|
1546
|
-
const manifestEv =
|
|
1848
|
+
const manifestEv = manifestEvidence(gemfilePath);
|
|
1547
1849
|
const gems = parseGemfile(content);
|
|
1548
1850
|
const seen = /* @__PURE__ */ new Set();
|
|
1549
1851
|
const lockfilePath = workspace.lockfiles.find((l) => l.includes("Gemfile.lock"));
|
|
@@ -1551,9 +1853,9 @@ var RubyAdapter = class {
|
|
|
1551
1853
|
let lockEv;
|
|
1552
1854
|
if (lockfilePath) {
|
|
1553
1855
|
try {
|
|
1554
|
-
const lockContent =
|
|
1856
|
+
const lockContent = readFileSync6(lockfilePath, "utf-8");
|
|
1555
1857
|
lockVersions = parseGemfileLock(lockContent);
|
|
1556
|
-
lockEv =
|
|
1858
|
+
lockEv = lockfileEvidence(lockfilePath);
|
|
1557
1859
|
} catch {
|
|
1558
1860
|
}
|
|
1559
1861
|
}
|
|
@@ -1562,21 +1864,21 @@ var RubyAdapter = class {
|
|
|
1562
1864
|
seen.add(gem.name);
|
|
1563
1865
|
const locked = lockVersions.get(gem.name);
|
|
1564
1866
|
const version = locked ?? gem.version;
|
|
1565
|
-
const purl = version ? buildPurl({ type: "gem", name: gem.name, version }) : buildPurl({ type: "gem", name: gem.name });
|
|
1867
|
+
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
1868
|
const evidence = [manifestEv];
|
|
1567
1869
|
if (lockEv && locked) evidence.push(lockEv);
|
|
1568
1870
|
observations.push({
|
|
1569
1871
|
id: `dep-${workspace.id}-${gem.name}`,
|
|
1570
1872
|
workspaceId: workspace.id,
|
|
1571
|
-
purl,
|
|
1873
|
+
...purl ? { purl } : {},
|
|
1572
1874
|
ecosystem: "ruby",
|
|
1573
1875
|
name: gem.name,
|
|
1574
|
-
sourceType:
|
|
1876
|
+
sourceType: gem.sourceType,
|
|
1575
1877
|
direct: true,
|
|
1576
1878
|
scope: "runtime",
|
|
1577
1879
|
...gem.version ? { requested: gem.version } : {},
|
|
1578
1880
|
...locked ? { locked } : {},
|
|
1579
|
-
status: "current",
|
|
1881
|
+
status: gem.sourceType === "git" ? "git_dependency" : gem.sourceType === "path" ? "local_path" : "current",
|
|
1580
1882
|
evidence
|
|
1581
1883
|
});
|
|
1582
1884
|
}
|
|
@@ -1585,26 +1887,102 @@ var RubyAdapter = class {
|
|
|
1585
1887
|
};
|
|
1586
1888
|
var rubyAdapter = new RubyAdapter();
|
|
1587
1889
|
|
|
1588
|
-
// src/adapters/
|
|
1589
|
-
import { readFileSync as
|
|
1590
|
-
|
|
1591
|
-
|
|
1890
|
+
// src/adapters/swift.ts
|
|
1891
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
1892
|
+
import { basename, join as join7 } from "node:path";
|
|
1893
|
+
function identityFromLocation(location) {
|
|
1894
|
+
return basename(location.replace(/[\\/]$/, "")).replace(/\.git$/i, "").toLowerCase();
|
|
1592
1895
|
}
|
|
1593
|
-
function
|
|
1594
|
-
|
|
1896
|
+
function parsePackageSwift(content) {
|
|
1897
|
+
const deps = [];
|
|
1898
|
+
const packageRegex = /\.package\s*\(\s*(?:name:\s*["'][^"']+["'],\s*)?(url|path):\s*["']([^"']+)["']\s*(?:,\s*(?:from|exact|branch|revision):\s*["']([^"']+)["'])?\s*\)/g;
|
|
1899
|
+
let match;
|
|
1900
|
+
while ((match = packageRegex.exec(content)) !== null) {
|
|
1901
|
+
const location = match[2];
|
|
1902
|
+
if (!location) continue;
|
|
1903
|
+
deps.push({
|
|
1904
|
+
identity: identityFromLocation(location),
|
|
1905
|
+
requested: match[3],
|
|
1906
|
+
sourceType: match[1] === "path" ? "path" : "git"
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
return deps;
|
|
1595
1910
|
}
|
|
1911
|
+
function parsePackageResolved(content) {
|
|
1912
|
+
const pins = /* @__PURE__ */ new Map();
|
|
1913
|
+
try {
|
|
1914
|
+
const json = JSON.parse(content);
|
|
1915
|
+
for (const pin of json.pins ?? json.object?.pins ?? []) {
|
|
1916
|
+
const identity = (pin.identity ?? pin.package ?? (pin.location ? identityFromLocation(pin.location) : void 0) ?? (pin.repositoryURL ? identityFromLocation(pin.repositoryURL) : void 0))?.toLowerCase();
|
|
1917
|
+
if (identity) pins.set(identity, { ...pin.state?.version ? { version: pin.state.version } : {}, ...pin.state?.revision ? { revision: pin.state.revision } : {} });
|
|
1918
|
+
}
|
|
1919
|
+
} catch {
|
|
1920
|
+
}
|
|
1921
|
+
return pins;
|
|
1922
|
+
}
|
|
1923
|
+
var SwiftAdapter = class {
|
|
1924
|
+
ecosystem = "swift";
|
|
1925
|
+
async inventory(workspace, options) {
|
|
1926
|
+
const root = workspaceRoot(workspace, options);
|
|
1927
|
+
const manifestPath = workspace.manifests.find((path) => path.endsWith("Package.swift")) ?? join7(root, "Package.swift");
|
|
1928
|
+
if (!fileExists(manifestPath)) return [];
|
|
1929
|
+
const direct = parsePackageSwift(readFileSync7(manifestPath, "utf8"));
|
|
1930
|
+
const resolvedPath = workspace.lockfiles.find((path) => path.endsWith("Package.resolved")) ?? join7(root, "Package.resolved");
|
|
1931
|
+
const pins = fileExists(resolvedPath) ? parsePackageResolved(readFileSync7(resolvedPath, "utf8")) : /* @__PURE__ */ new Map();
|
|
1932
|
+
const manifestEv = manifestEvidence(manifestPath);
|
|
1933
|
+
const lockEv = pins.size > 0 ? lockfileEvidence(resolvedPath) : void 0;
|
|
1934
|
+
const observations = [];
|
|
1935
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1936
|
+
const add = (identity, requested, sourceType, isDirect) => {
|
|
1937
|
+
if (seen.has(identity)) return;
|
|
1938
|
+
seen.add(identity);
|
|
1939
|
+
const pin = pins.get(identity);
|
|
1940
|
+
const version = pin?.version ?? pin?.revision ?? requested;
|
|
1941
|
+
const evidence = isDirect ? [manifestEv] : [];
|
|
1942
|
+
if (lockEv && pin) evidence.push(lockEv);
|
|
1943
|
+
observations.push({
|
|
1944
|
+
id: `dep-${workspace.id}-${identity}`,
|
|
1945
|
+
workspaceId: workspace.id,
|
|
1946
|
+
...sourceType === "git" ? { purl: buildPurl({ type: "swift", name: identity, ...version ? { version } : {} }) } : {},
|
|
1947
|
+
ecosystem: "swift",
|
|
1948
|
+
name: identity,
|
|
1949
|
+
sourceType,
|
|
1950
|
+
direct: isDirect,
|
|
1951
|
+
scope: isDirect ? "runtime" : "transitive",
|
|
1952
|
+
...requested ? { requested } : {},
|
|
1953
|
+
...pin?.version ? { locked: pin.version } : pin?.revision ? { locked: pin.revision } : {},
|
|
1954
|
+
status: sourceType === "path" ? "local_path" : "git_dependency",
|
|
1955
|
+
evidence
|
|
1956
|
+
});
|
|
1957
|
+
};
|
|
1958
|
+
for (const dep of direct) add(dep.identity, dep.requested, dep.sourceType, true);
|
|
1959
|
+
if (options.includeTransitive) {
|
|
1960
|
+
for (const [identity] of pins) add(identity, void 0, "git", false);
|
|
1961
|
+
}
|
|
1962
|
+
return observations;
|
|
1963
|
+
}
|
|
1964
|
+
};
|
|
1965
|
+
var swiftAdapter = new SwiftAdapter();
|
|
1966
|
+
|
|
1967
|
+
// src/adapters/elixir.ts
|
|
1968
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
1596
1969
|
function parseMixExsDeps(content) {
|
|
1597
1970
|
const deps = [];
|
|
1598
|
-
const depRegex = /\{:(\w+),\s*
|
|
1971
|
+
const depRegex = /\{:(\w+),\s*([^}]+)\}/g;
|
|
1599
1972
|
let match;
|
|
1600
1973
|
while ((match = depRegex.exec(content)) !== null) {
|
|
1601
|
-
|
|
1974
|
+
const name = match[1];
|
|
1975
|
+
const value = match[2];
|
|
1976
|
+
if (!name || !value) continue;
|
|
1977
|
+
const version = /^\s*["']([^"']+)["']/.exec(value)?.[1];
|
|
1978
|
+
const sourceType = /\bgit:/.test(value) ? "git" : /\bpath:/.test(value) ? "path" : "registry";
|
|
1979
|
+
deps.push({ name, version, sourceType });
|
|
1602
1980
|
}
|
|
1603
1981
|
return deps;
|
|
1604
1982
|
}
|
|
1605
1983
|
function parseMixLock(content) {
|
|
1606
1984
|
const versions = /* @__PURE__ */ new Map();
|
|
1607
|
-
const lockRegex =
|
|
1985
|
+
const lockRegex = /["']([\w-]+)["']\s*=>\s*\{:hex,\s*:[\w-]+,\s*["']([^"']+)["']/g;
|
|
1608
1986
|
let match;
|
|
1609
1987
|
while ((match = lockRegex.exec(content)) !== null) {
|
|
1610
1988
|
versions.set(match[1], match[2]);
|
|
@@ -1619,11 +1997,11 @@ var ElixirAdapter = class {
|
|
|
1619
1997
|
if (!mixExsPath) return [];
|
|
1620
1998
|
let content;
|
|
1621
1999
|
try {
|
|
1622
|
-
content =
|
|
2000
|
+
content = readFileSync8(mixExsPath, "utf-8");
|
|
1623
2001
|
} catch {
|
|
1624
2002
|
return [];
|
|
1625
2003
|
}
|
|
1626
|
-
const manifestEv =
|
|
2004
|
+
const manifestEv = manifestEvidence(mixExsPath);
|
|
1627
2005
|
const deps = parseMixExsDeps(content);
|
|
1628
2006
|
const seen = /* @__PURE__ */ new Set();
|
|
1629
2007
|
const lockfilePath = workspace.lockfiles.find((l) => l.includes("mix.lock"));
|
|
@@ -1631,9 +2009,9 @@ var ElixirAdapter = class {
|
|
|
1631
2009
|
let lockEv;
|
|
1632
2010
|
if (lockfilePath) {
|
|
1633
2011
|
try {
|
|
1634
|
-
const lockContent =
|
|
2012
|
+
const lockContent = readFileSync8(lockfilePath, "utf-8");
|
|
1635
2013
|
lockVersions = parseMixLock(lockContent);
|
|
1636
|
-
lockEv =
|
|
2014
|
+
lockEv = lockfileEvidence(lockfilePath);
|
|
1637
2015
|
} catch {
|
|
1638
2016
|
}
|
|
1639
2017
|
}
|
|
@@ -1642,21 +2020,21 @@ var ElixirAdapter = class {
|
|
|
1642
2020
|
seen.add(dep.name);
|
|
1643
2021
|
const locked = lockVersions.get(dep.name);
|
|
1644
2022
|
const version = locked ?? dep.version;
|
|
1645
|
-
const purl = version ? buildPurl({ type: "hex", name: dep.name, version }) : buildPurl({ type: "hex", name: dep.name });
|
|
2023
|
+
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
2024
|
const evidence = [manifestEv];
|
|
1647
2025
|
if (lockEv && locked) evidence.push(lockEv);
|
|
1648
2026
|
observations.push({
|
|
1649
2027
|
id: `dep-${workspace.id}-${dep.name}`,
|
|
1650
2028
|
workspaceId: workspace.id,
|
|
1651
|
-
purl,
|
|
2029
|
+
...purl ? { purl } : {},
|
|
1652
2030
|
ecosystem: "elixir",
|
|
1653
2031
|
name: dep.name,
|
|
1654
|
-
sourceType:
|
|
2032
|
+
sourceType: dep.sourceType,
|
|
1655
2033
|
direct: true,
|
|
1656
2034
|
scope: "runtime",
|
|
1657
2035
|
...dep.version ? { requested: dep.version } : {},
|
|
1658
2036
|
...locked ? { locked } : {},
|
|
1659
|
-
status: "current",
|
|
2037
|
+
status: dep.sourceType === "git" ? "git_dependency" : dep.sourceType === "path" ? "local_path" : "current",
|
|
1660
2038
|
evidence
|
|
1661
2039
|
});
|
|
1662
2040
|
}
|
|
@@ -1666,10 +2044,7 @@ var ElixirAdapter = class {
|
|
|
1666
2044
|
var elixirAdapter = new ElixirAdapter();
|
|
1667
2045
|
|
|
1668
2046
|
// src/adapters/cpp.ts
|
|
1669
|
-
import { readFileSync as
|
|
1670
|
-
function manifestEvidence11(path) {
|
|
1671
|
-
return { kind: "manifest", source: path, retrievedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1672
|
-
}
|
|
2047
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
1673
2048
|
function parseConanTxt(content) {
|
|
1674
2049
|
const deps = [];
|
|
1675
2050
|
const requiresMatch = /\[requires\]\s*\n([\s\S]*?)(?:\[|$)/;
|
|
@@ -1710,11 +2085,11 @@ var CppAdapter = class {
|
|
|
1710
2085
|
for (const manifestPath of workspace.manifests) {
|
|
1711
2086
|
let content;
|
|
1712
2087
|
try {
|
|
1713
|
-
content =
|
|
2088
|
+
content = readFileSync9(manifestPath, "utf-8");
|
|
1714
2089
|
} catch {
|
|
1715
2090
|
continue;
|
|
1716
2091
|
}
|
|
1717
|
-
const manifestEv =
|
|
2092
|
+
const manifestEv = manifestEvidence(manifestPath);
|
|
1718
2093
|
let deps = [];
|
|
1719
2094
|
if (manifestPath.includes("conanfile")) {
|
|
1720
2095
|
deps = parseConanTxt(content);
|
|
@@ -1790,6 +2165,108 @@ function diffSnapshots(oldSnapshot, newSnapshot) {
|
|
|
1790
2165
|
return { added, removed, changed };
|
|
1791
2166
|
}
|
|
1792
2167
|
|
|
2168
|
+
// src/trend.ts
|
|
2169
|
+
function median(values) {
|
|
2170
|
+
if (!values.length) return void 0;
|
|
2171
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
2172
|
+
const middle = Math.floor(sorted.length / 2);
|
|
2173
|
+
const upper = sorted[middle];
|
|
2174
|
+
if (upper === void 0) return void 0;
|
|
2175
|
+
if (sorted.length % 2) return upper;
|
|
2176
|
+
const lower = sorted[middle - 1];
|
|
2177
|
+
return lower === void 0 ? upper : (lower + upper) / 2;
|
|
2178
|
+
}
|
|
2179
|
+
var TrendStore = class {
|
|
2180
|
+
constructor(source) {
|
|
2181
|
+
this.source = source;
|
|
2182
|
+
}
|
|
2183
|
+
source;
|
|
2184
|
+
analyze(projectId, limit = 100) {
|
|
2185
|
+
const snapshots = this.source.listSnapshots(projectId, limit).sort((a, b) => Date.parse(a.createdAt) - Date.parse(b.createdAt));
|
|
2186
|
+
const versionHistory = /* @__PURE__ */ new Map();
|
|
2187
|
+
const vulnerabilityDurations = [];
|
|
2188
|
+
const openVulnerabilities = /* @__PURE__ */ new Map();
|
|
2189
|
+
for (const snapshot of snapshots) {
|
|
2190
|
+
const at = Date.parse(snapshot.createdAt);
|
|
2191
|
+
const active = /* @__PURE__ */ new Set();
|
|
2192
|
+
for (const dependency of snapshot.dependencies) {
|
|
2193
|
+
const key = `${dependency.ecosystem}:${dependency.name}`;
|
|
2194
|
+
active.add(key);
|
|
2195
|
+
const vulnerable = dependency.status === "vulnerable";
|
|
2196
|
+
const history = versionHistory.get(key) ?? [];
|
|
2197
|
+
history.push({ at, version: dependency.locked ?? dependency.installed ?? dependency.requested, vulnerable, name: dependency.name, ecosystem: dependency.ecosystem });
|
|
2198
|
+
versionHistory.set(key, history);
|
|
2199
|
+
if (vulnerable && !openVulnerabilities.has(key)) openVulnerabilities.set(key, at);
|
|
2200
|
+
if (!vulnerable && openVulnerabilities.has(key)) {
|
|
2201
|
+
const openedAt = openVulnerabilities.get(key);
|
|
2202
|
+
if (openedAt !== void 0) vulnerabilityDurations.push(Math.max(0, at - openedAt));
|
|
2203
|
+
openVulnerabilities.delete(key);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
for (const key of openVulnerabilities.keys()) {
|
|
2207
|
+
if (!active.has(key)) {
|
|
2208
|
+
const openedAt = openVulnerabilities.get(key);
|
|
2209
|
+
if (openedAt !== void 0) vulnerabilityDurations.push(Math.max(0, at - openedAt));
|
|
2210
|
+
openVulnerabilities.delete(key);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
const dependencies = [];
|
|
2215
|
+
for (const [key, history] of versionHistory) {
|
|
2216
|
+
const first = history[0];
|
|
2217
|
+
const latest = history.at(-1);
|
|
2218
|
+
if (!first || !latest) continue;
|
|
2219
|
+
let versionChanges = 0;
|
|
2220
|
+
let sameVersionSince = first.at;
|
|
2221
|
+
for (let index = 1; index < history.length; index++) {
|
|
2222
|
+
const current = history[index];
|
|
2223
|
+
const previous = history[index - 1];
|
|
2224
|
+
if (current && previous && current.version !== previous.version) {
|
|
2225
|
+
versionChanges++;
|
|
2226
|
+
sameVersionSince = current.at;
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
const spanDays = Math.max(1, (latest.at - first.at) / 864e5);
|
|
2230
|
+
dependencies.push({
|
|
2231
|
+
key,
|
|
2232
|
+
name: latest.name,
|
|
2233
|
+
ecosystem: latest.ecosystem,
|
|
2234
|
+
...latest.version ? { currentVersion: latest.version } : {},
|
|
2235
|
+
lockedVersionAgeMs: Math.max(0, latest.at - sameVersionSince),
|
|
2236
|
+
versionChanges,
|
|
2237
|
+
upgradeVelocityPerDay: versionChanges / spanDays
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
dependencies.sort((a, b) => a.key.localeCompare(b.key));
|
|
2241
|
+
const halfLife = median(vulnerabilityDurations);
|
|
2242
|
+
const firstSnapshot = snapshots[0];
|
|
2243
|
+
const lastSnapshot = snapshots.at(-1);
|
|
2244
|
+
return {
|
|
2245
|
+
projectId,
|
|
2246
|
+
...firstSnapshot && lastSnapshot ? { from: firstSnapshot.createdAt, to: lastSnapshot.createdAt } : {},
|
|
2247
|
+
snapshots: snapshots.length,
|
|
2248
|
+
dependencies,
|
|
2249
|
+
...halfLife !== void 0 ? { vulnerabilityHalfLifeMs: halfLife } : {},
|
|
2250
|
+
points: snapshots.map((snapshot) => ({
|
|
2251
|
+
snapshotId: snapshot.id,
|
|
2252
|
+
createdAt: snapshot.createdAt,
|
|
2253
|
+
dependencies: snapshot.dependencies.length,
|
|
2254
|
+
outdated: snapshot.dependencies.filter((dependency) => dependency.status.startsWith("update_available")).length,
|
|
2255
|
+
vulnerable: snapshot.dependencies.filter((dependency) => dependency.status === "vulnerable").length
|
|
2256
|
+
}))
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
};
|
|
2260
|
+
function renderTrendMarkdown(report) {
|
|
2261
|
+
const lines = ["# TechStack Trend", "", `**Snapshots:** ${report.snapshots}`];
|
|
2262
|
+
if (report.vulnerabilityHalfLifeMs !== void 0) lines.push(`**Vulnerability half-life:** ${(report.vulnerabilityHalfLifeMs / 864e5).toFixed(1)} days`);
|
|
2263
|
+
lines.push("", "| Dependency | Version | Age (days) | Changes | Velocity/day |", "|---|---|---:|---:|---:|");
|
|
2264
|
+
for (const dependency of report.dependencies) {
|
|
2265
|
+
lines.push(`| ${dependency.ecosystem}:${dependency.name} | ${dependency.currentVersion ?? "\u2014"} | ${(dependency.lockedVersionAgeMs / 864e5).toFixed(1)} | ${dependency.versionChanges} | ${dependency.upgradeVelocityPerDay.toFixed(3)} |`);
|
|
2266
|
+
}
|
|
2267
|
+
return lines.join("\n");
|
|
2268
|
+
}
|
|
2269
|
+
|
|
1793
2270
|
// src/sbom.ts
|
|
1794
2271
|
function toSpdx(snapshot) {
|
|
1795
2272
|
const created = snapshot.createdAt;
|
|
@@ -1832,6 +2309,47 @@ function toCycloneDX(snapshot) {
|
|
|
1832
2309
|
}
|
|
1833
2310
|
|
|
1834
2311
|
// src/remediation.ts
|
|
2312
|
+
var LANGUAGE_BY_ECOSYSTEM = {
|
|
2313
|
+
python: "python",
|
|
2314
|
+
rust: "rust",
|
|
2315
|
+
go: "go",
|
|
2316
|
+
php: "php",
|
|
2317
|
+
dotnet: "csharp"
|
|
2318
|
+
};
|
|
2319
|
+
var EXECUTABLE_ECOSYSTEMS = /* @__PURE__ */ new Set(["npm", ...Object.keys(LANGUAGE_BY_ECOSYSTEM)]);
|
|
2320
|
+
function versionedPackageName(operation) {
|
|
2321
|
+
const version = operation.targetVersion;
|
|
2322
|
+
if (!version) return operation.dependencyName;
|
|
2323
|
+
switch (operation.ecosystem) {
|
|
2324
|
+
case "python":
|
|
2325
|
+
return `${operation.dependencyName}==${version}`;
|
|
2326
|
+
case "php":
|
|
2327
|
+
return `${operation.dependencyName}:${version}`;
|
|
2328
|
+
default:
|
|
2329
|
+
return `${operation.dependencyName}@${version}`;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
function toLanguagePackageInput(operation, workspace) {
|
|
2333
|
+
if (operation.action === "replace" || operation.action === "investigate") {
|
|
2334
|
+
throw new Error(`${operation.action} requires a manual package choice`);
|
|
2335
|
+
}
|
|
2336
|
+
if (!EXECUTABLE_ECOSYSTEMS.has(operation.ecosystem)) {
|
|
2337
|
+
throw new Error(`Automated remediation is not supported for ${operation.ecosystem}`);
|
|
2338
|
+
}
|
|
2339
|
+
const language = LANGUAGE_BY_ECOSYSTEM[operation.ecosystem];
|
|
2340
|
+
const base = {
|
|
2341
|
+
...workspace ? { workspace } : {},
|
|
2342
|
+
...language ? { language } : {},
|
|
2343
|
+
allowScripts: false
|
|
2344
|
+
};
|
|
2345
|
+
if (operation.action === "remove") {
|
|
2346
|
+
if (operation.ecosystem === "go") {
|
|
2347
|
+
return { ...base, operation: "add", names: [`${operation.dependencyName}@none`] };
|
|
2348
|
+
}
|
|
2349
|
+
return { ...base, operation: "remove", names: [operation.dependencyName] };
|
|
2350
|
+
}
|
|
2351
|
+
return { ...base, operation: "add", names: [versionedPackageName(operation)] };
|
|
2352
|
+
}
|
|
1835
2353
|
function suggestCommand(ecosystem, name, action, targetVersion) {
|
|
1836
2354
|
const ver = targetVersion ? `@${targetVersion}` : "@latest";
|
|
1837
2355
|
switch (ecosystem) {
|
|
@@ -1951,14 +2469,138 @@ function renderPlanMarkdown(plan) {
|
|
|
1951
2469
|
}
|
|
1952
2470
|
return lines.join("\n");
|
|
1953
2471
|
}
|
|
2472
|
+
async function applyPlan(plan, options = {}) {
|
|
2473
|
+
const dryRun = options.dryRun !== false;
|
|
2474
|
+
const execute = options.execute;
|
|
2475
|
+
if (!dryRun && !execute) throw new Error("An execute strategy is required when dryRun is false");
|
|
2476
|
+
const results = [];
|
|
2477
|
+
for (const [index, item] of plan.items.entries()) {
|
|
2478
|
+
if (options.signal?.aborted) throw new DOMException("Remediation cancelled", "AbortError");
|
|
2479
|
+
if (dryRun) {
|
|
2480
|
+
results.push({ dependencyName: item.dependencyName, status: "planned" });
|
|
2481
|
+
continue;
|
|
2482
|
+
}
|
|
2483
|
+
const approved = options.approve ? await options.approve(item, index) : false;
|
|
2484
|
+
if (!approved) {
|
|
2485
|
+
results.push({
|
|
2486
|
+
dependencyName: item.dependencyName,
|
|
2487
|
+
status: "skipped",
|
|
2488
|
+
detail: "Not approved"
|
|
2489
|
+
});
|
|
2490
|
+
continue;
|
|
2491
|
+
}
|
|
2492
|
+
try {
|
|
2493
|
+
if (!execute) throw new Error("An execute strategy is required when dryRun is false");
|
|
2494
|
+
const response = await execute({
|
|
2495
|
+
ecosystem: item.ecosystem,
|
|
2496
|
+
workspaceId: item.workspaceId,
|
|
2497
|
+
dependencyName: item.dependencyName,
|
|
2498
|
+
action: item.action,
|
|
2499
|
+
...item.targetVersion ? { targetVersion: item.targetVersion } : {}
|
|
2500
|
+
});
|
|
2501
|
+
results.push({
|
|
2502
|
+
dependencyName: item.dependencyName,
|
|
2503
|
+
status: "applied",
|
|
2504
|
+
...response?.detail ? { detail: response.detail } : {}
|
|
2505
|
+
});
|
|
2506
|
+
} catch (error) {
|
|
2507
|
+
results.push({
|
|
2508
|
+
dependencyName: item.dependencyName,
|
|
2509
|
+
status: "failed",
|
|
2510
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
return { dryRun, items: results };
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
// src/registry/http-fetch.ts
|
|
2518
|
+
import { get as httpGet, request as httpRequest } from "node:http";
|
|
2519
|
+
import { get as httpsGet, request as httpsRequest } from "node:https";
|
|
2520
|
+
function retryAfterMs(headers, now = Date.now()) {
|
|
2521
|
+
const value = Array.isArray(headers["retry-after"]) ? headers["retry-after"][0] : headers["retry-after"];
|
|
2522
|
+
if (!value) return void 0;
|
|
2523
|
+
const seconds = Number(value);
|
|
2524
|
+
if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
|
|
2525
|
+
const date = Date.parse(value);
|
|
2526
|
+
return Number.isFinite(date) ? Math.max(0, date - now) : void 0;
|
|
2527
|
+
}
|
|
2528
|
+
function delay(ms, signal) {
|
|
2529
|
+
if (signal?.aborted) return Promise.reject(new DOMException("Request aborted", "AbortError"));
|
|
2530
|
+
return new Promise((resolve3, reject) => {
|
|
2531
|
+
const timer = setTimeout(resolve3, ms);
|
|
2532
|
+
signal?.addEventListener("abort", () => {
|
|
2533
|
+
clearTimeout(timer);
|
|
2534
|
+
reject(new DOMException("Request aborted", "AbortError"));
|
|
2535
|
+
}, { once: true });
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
function requestOnce(options) {
|
|
2539
|
+
return new Promise((resolve3, reject) => {
|
|
2540
|
+
const method = options.method ?? "GET";
|
|
2541
|
+
const requestOptions = {
|
|
2542
|
+
hostname: options.hostname,
|
|
2543
|
+
path: options.path,
|
|
2544
|
+
method,
|
|
2545
|
+
headers: options.headers,
|
|
2546
|
+
signal: options.signal,
|
|
2547
|
+
timeout: options.timeoutMs ?? 15e3
|
|
2548
|
+
};
|
|
2549
|
+
const isHttp = options.hostname === "localhost" || options.hostname === "127.0.0.1";
|
|
2550
|
+
const callback = (response) => {
|
|
2551
|
+
let body = "";
|
|
2552
|
+
response.setEncoding?.("utf8");
|
|
2553
|
+
response.on("data", (chunk) => {
|
|
2554
|
+
body += chunk.toString();
|
|
2555
|
+
});
|
|
2556
|
+
response.on("end", () => resolve3({
|
|
2557
|
+
statusCode: response.statusCode ?? 0,
|
|
2558
|
+
headers: response.headers,
|
|
2559
|
+
body
|
|
2560
|
+
}));
|
|
2561
|
+
};
|
|
2562
|
+
const request = method === "GET" ? (isHttp ? httpGet : httpsGet)(requestOptions, callback) : (isHttp ? httpRequest : httpsRequest)(requestOptions, callback);
|
|
2563
|
+
request.on("error", reject);
|
|
2564
|
+
request.on("timeout", () => {
|
|
2565
|
+
request.destroy();
|
|
2566
|
+
reject(new Error(`Request timeout for ${options.hostname}${options.path}`));
|
|
2567
|
+
});
|
|
2568
|
+
if (options.body) request.write(options.body);
|
|
2569
|
+
request.end();
|
|
2570
|
+
});
|
|
2571
|
+
}
|
|
2572
|
+
async function requestWithRetry(options) {
|
|
2573
|
+
const maxAttempts = Math.max(1, options.maxAttempts ?? 3);
|
|
2574
|
+
let lastError;
|
|
2575
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
2576
|
+
try {
|
|
2577
|
+
const response = await requestOnce(options);
|
|
2578
|
+
const retryable = response.statusCode === 429 || response.statusCode >= 500;
|
|
2579
|
+
if (!retryable || attempt === maxAttempts - 1) return response;
|
|
2580
|
+
const serverDelay = retryAfterMs(response.headers);
|
|
2581
|
+
const exponential = (options.baseBackoffMs ?? 1e3) * 2 ** attempt;
|
|
2582
|
+
await delay(serverDelay ?? exponential, options.signal);
|
|
2583
|
+
} catch (error) {
|
|
2584
|
+
if (options.signal?.aborted || error instanceof DOMException && error.name === "AbortError") throw error;
|
|
2585
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
2586
|
+
if (attempt < maxAttempts - 1) {
|
|
2587
|
+
await delay((options.baseBackoffMs ?? 1e3) * 2 ** attempt, options.signal);
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
throw lastError ?? new Error(`Request failed for ${options.hostname}${options.path}`);
|
|
2592
|
+
}
|
|
2593
|
+
function parseJsonResponse(response, source) {
|
|
2594
|
+
try {
|
|
2595
|
+
return JSON.parse(response.body);
|
|
2596
|
+
} catch {
|
|
2597
|
+
throw new Error(`Invalid JSON response from ${source}`);
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
1954
2600
|
|
|
1955
2601
|
// src/registry/client.ts
|
|
1956
|
-
import { get as httpsGet } from "node:https";
|
|
1957
|
-
import { get as httpGet } from "node:http";
|
|
1958
2602
|
var DEFAULT_TTL_MS = 10 * 60 * 1e3;
|
|
1959
2603
|
var MAX_CONCURRENCY_PER_HOST = 3;
|
|
1960
|
-
var MAX_RETRIES = 3;
|
|
1961
|
-
var BASE_BACKOFF_MS = 1e3;
|
|
1962
2604
|
var registryCache = /* @__PURE__ */ new Map();
|
|
1963
2605
|
var hostConcurrency = /* @__PURE__ */ new Map();
|
|
1964
2606
|
function getCacheKey(host, path) {
|
|
@@ -2006,54 +2648,6 @@ function releaseHostSlot(host) {
|
|
|
2006
2648
|
}
|
|
2007
2649
|
}
|
|
2008
2650
|
}
|
|
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
2651
|
function parseNpmPackument(json, name) {
|
|
2058
2652
|
const latestVersion = json["dist-tags"]?.["latest"];
|
|
2059
2653
|
let deprecated;
|
|
@@ -2208,6 +2802,44 @@ var ECOSYSTEM_FETCHERS = {
|
|
|
2208
2802
|
}
|
|
2209
2803
|
}
|
|
2210
2804
|
};
|
|
2805
|
+
var RegistryNotFoundError = class extends Error {
|
|
2806
|
+
constructor(statusCode, packageName) {
|
|
2807
|
+
super(`Registry package not found or inaccessible (${statusCode}): ${packageName}`);
|
|
2808
|
+
this.statusCode = statusCode;
|
|
2809
|
+
this.packageName = packageName;
|
|
2810
|
+
this.name = "RegistryNotFoundError";
|
|
2811
|
+
}
|
|
2812
|
+
statusCode;
|
|
2813
|
+
packageName;
|
|
2814
|
+
};
|
|
2815
|
+
var RegistryAuthError = class extends Error {
|
|
2816
|
+
constructor(statusCode, packageName) {
|
|
2817
|
+
super(`Registry authorization failed (${statusCode}): ${packageName}`);
|
|
2818
|
+
this.statusCode = statusCode;
|
|
2819
|
+
this.packageName = packageName;
|
|
2820
|
+
this.name = "RegistryAuthError";
|
|
2821
|
+
}
|
|
2822
|
+
statusCode;
|
|
2823
|
+
packageName;
|
|
2824
|
+
};
|
|
2825
|
+
var RegistryRateLimitError = class extends Error {
|
|
2826
|
+
constructor(statusCode, host) {
|
|
2827
|
+
super(`Registry ${host} returned ${statusCode} after 3 attempts`);
|
|
2828
|
+
this.statusCode = statusCode;
|
|
2829
|
+
this.host = host;
|
|
2830
|
+
this.name = "RegistryRateLimitError";
|
|
2831
|
+
}
|
|
2832
|
+
statusCode;
|
|
2833
|
+
host;
|
|
2834
|
+
};
|
|
2835
|
+
var RegistryNetworkError = class extends Error {
|
|
2836
|
+
constructor(message, cause) {
|
|
2837
|
+
super(message);
|
|
2838
|
+
this.cause = cause;
|
|
2839
|
+
this.name = "RegistryNetworkError";
|
|
2840
|
+
}
|
|
2841
|
+
cause;
|
|
2842
|
+
};
|
|
2211
2843
|
async function lookupRegistry(ecosystem, name, options = {}) {
|
|
2212
2844
|
const fetcher = ECOSYSTEM_FETCHERS[ecosystem];
|
|
2213
2845
|
if (!fetcher) {
|
|
@@ -2223,48 +2855,52 @@ async function lookupRegistry(ecosystem, name, options = {}) {
|
|
|
2223
2855
|
try {
|
|
2224
2856
|
const existingEntry = registryCache.get(cacheKey);
|
|
2225
2857
|
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
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
}
|
|
2858
|
+
let response;
|
|
2859
|
+
try {
|
|
2860
|
+
response = await requestWithRetry({
|
|
2861
|
+
hostname: fetcher.host,
|
|
2862
|
+
path,
|
|
2863
|
+
headers: {
|
|
2864
|
+
Accept: "application/json",
|
|
2865
|
+
"User-Agent": "WrongStack-TechStack/1.0",
|
|
2866
|
+
...etag ? { "If-None-Match": etag } : {}
|
|
2867
|
+
},
|
|
2868
|
+
signal: options.signal,
|
|
2869
|
+
timeoutMs: 15e3,
|
|
2870
|
+
maxAttempts: 3
|
|
2871
|
+
});
|
|
2872
|
+
} catch (error) {
|
|
2873
|
+
if (options.signal?.aborted) throw error;
|
|
2874
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
2875
|
+
throw new RegistryNetworkError(cause.message, cause);
|
|
2876
|
+
}
|
|
2877
|
+
if (response.statusCode === 304 && existingEntry) {
|
|
2878
|
+
setCache(cacheKey, existingEntry.data, existingEntry.etag, DEFAULT_TTL_MS);
|
|
2879
|
+
return existingEntry.data;
|
|
2880
|
+
}
|
|
2881
|
+
if (response.statusCode === 401 || response.statusCode === 404) {
|
|
2882
|
+
if (options.strictErrors) throw new RegistryNotFoundError(response.statusCode, name);
|
|
2883
|
+
return void 0;
|
|
2884
|
+
}
|
|
2885
|
+
if (response.statusCode === 403) {
|
|
2886
|
+
if (options.strictErrors) throw new RegistryAuthError(response.statusCode, name);
|
|
2887
|
+
return void 0;
|
|
2888
|
+
}
|
|
2889
|
+
if (response.statusCode === 429) {
|
|
2890
|
+
throw new RegistryRateLimitError(response.statusCode, fetcher.host);
|
|
2891
|
+
}
|
|
2892
|
+
if (response.statusCode >= 500) {
|
|
2893
|
+
throw new RegistryNetworkError(`Registry ${fetcher.host} returned ${response.statusCode} after 3 attempts`);
|
|
2894
|
+
}
|
|
2895
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
2896
|
+
throw new RegistryNetworkError(`Unexpected registry response ${response.statusCode} from ${fetcher.host}`);
|
|
2266
2897
|
}
|
|
2267
|
-
|
|
2898
|
+
const json = parseJsonResponse(response, `${fetcher.host}${path}`);
|
|
2899
|
+
const parsed = fetcher.parser(json, name, ecosystem);
|
|
2900
|
+
if (!parsed) return void 0;
|
|
2901
|
+
const responseEtag = Array.isArray(response.headers.etag) ? response.headers.etag[0] : response.headers.etag;
|
|
2902
|
+
setCache(cacheKey, parsed, responseEtag, DEFAULT_TTL_MS);
|
|
2903
|
+
return parsed;
|
|
2268
2904
|
} finally {
|
|
2269
2905
|
releaseHostSlot(fetcher.host);
|
|
2270
2906
|
}
|
|
@@ -2293,14 +2929,21 @@ function clearRegistryCache() {
|
|
|
2293
2929
|
registryCache.clear();
|
|
2294
2930
|
hostConcurrency.clear();
|
|
2295
2931
|
}
|
|
2932
|
+
function invalidateRegistryCache(ecosystem, names) {
|
|
2933
|
+
const fetcher = ECOSYSTEM_FETCHERS[ecosystem];
|
|
2934
|
+
if (!fetcher) return 0;
|
|
2935
|
+
const keys = names ? names.map((name) => getCacheKey(fetcher.host, fetcher.path(name))) : [...registryCache.keys()].filter((key) => key.startsWith(fetcher.host));
|
|
2936
|
+
let removed = 0;
|
|
2937
|
+
for (const key of keys) {
|
|
2938
|
+
if (registryCache.delete(key)) removed++;
|
|
2939
|
+
}
|
|
2940
|
+
return removed;
|
|
2941
|
+
}
|
|
2296
2942
|
|
|
2297
2943
|
// src/advisory/osv.ts
|
|
2298
|
-
import { get as httpsGet2 } from "node:https";
|
|
2299
2944
|
var OSV_API_BASE = "api.osv.dev";
|
|
2300
2945
|
var OSV_QUERY_BATCH_PATH = "/v1/querybatch";
|
|
2301
2946
|
var MAX_BATCH_SIZE = 500;
|
|
2302
|
-
var MAX_RETRIES2 = 3;
|
|
2303
|
-
var BASE_BACKOFF_MS2 = 1e3;
|
|
2304
2947
|
function mapSeverity(osvSeverity, databaseSeverity) {
|
|
2305
2948
|
if (osvSeverity && osvSeverity.length > 0) {
|
|
2306
2949
|
for (const s of osvSeverity) {
|
|
@@ -2322,44 +2965,6 @@ function mapSeverity(osvSeverity, databaseSeverity) {
|
|
|
2322
2965
|
}
|
|
2323
2966
|
return "info";
|
|
2324
2967
|
}
|
|
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
2968
|
async function queryOsvBatch(purls, options = {}) {
|
|
2364
2969
|
const advisories = /* @__PURE__ */ new Map();
|
|
2365
2970
|
for (const purl of purls) {
|
|
@@ -2369,7 +2974,6 @@ async function queryOsvBatch(purls, options = {}) {
|
|
|
2369
2974
|
for (let i = 0; i < purls.length; i += MAX_BATCH_SIZE) {
|
|
2370
2975
|
batches.push(purls.slice(i, i + MAX_BATCH_SIZE));
|
|
2371
2976
|
}
|
|
2372
|
-
let lastError;
|
|
2373
2977
|
for (const batch of batches) {
|
|
2374
2978
|
const requestBody = {
|
|
2375
2979
|
queries: batch.map((purl) => ({
|
|
@@ -2377,54 +2981,38 @@ async function queryOsvBatch(purls, options = {}) {
|
|
|
2377
2981
|
}))
|
|
2378
2982
|
};
|
|
2379
2983
|
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;
|
|
2984
|
+
const response = await requestWithRetry({
|
|
2985
|
+
hostname: OSV_API_BASE,
|
|
2986
|
+
path: OSV_QUERY_BATCH_PATH,
|
|
2987
|
+
method: "POST",
|
|
2988
|
+
headers: {
|
|
2989
|
+
"Content-Type": "application/json",
|
|
2990
|
+
"Content-Length": Buffer.byteLength(jsonBody).toString(),
|
|
2991
|
+
"User-Agent": "WrongStack-TechStack/1.0"
|
|
2992
|
+
},
|
|
2993
|
+
body: jsonBody,
|
|
2994
|
+
signal: options.signal,
|
|
2995
|
+
timeoutMs: 3e4,
|
|
2996
|
+
maxAttempts: 3
|
|
2997
|
+
});
|
|
2998
|
+
if (response.statusCode !== 200) {
|
|
2999
|
+
throw new Error(`OSV API returned ${response.statusCode}: ${response.body}`);
|
|
3000
|
+
}
|
|
3001
|
+
const result = parseJsonResponse(response, "api.osv.dev/v1/querybatch");
|
|
3002
|
+
for (let i = 0; i < result.results.length; i++) {
|
|
3003
|
+
const purl = batch[i];
|
|
3004
|
+
if (!purl) continue;
|
|
3005
|
+
const vulns = result.results[i]?.vulns;
|
|
3006
|
+
if (!vulns || vulns.length === 0) continue;
|
|
3007
|
+
advisories.set(purl, vulns.map((vuln) => ({
|
|
3008
|
+
id: vuln.id,
|
|
3009
|
+
summary: vuln.summary ?? vuln.details ?? "No summary available",
|
|
3010
|
+
severity: mapSeverity(
|
|
3011
|
+
vuln.severity,
|
|
3012
|
+
vuln.database_specific?.severity ?? vuln.affected?.[0]?.database_specific?.severity
|
|
3013
|
+
),
|
|
3014
|
+
aliases: vuln.aliases ?? []
|
|
3015
|
+
})));
|
|
2428
3016
|
}
|
|
2429
3017
|
}
|
|
2430
3018
|
const evidence = {
|
|
@@ -2441,9 +3029,9 @@ async function queryOsvSingle(purl, options = {}) {
|
|
|
2441
3029
|
}
|
|
2442
3030
|
|
|
2443
3031
|
// src/advisory/native-audit.ts
|
|
2444
|
-
import {
|
|
2445
|
-
import {
|
|
2446
|
-
import { join as
|
|
3032
|
+
import { execFile } from "node:child_process";
|
|
3033
|
+
import { access as access3 } from "node:fs/promises";
|
|
3034
|
+
import { join as join8 } from "node:path";
|
|
2447
3035
|
function npmSeverity(s) {
|
|
2448
3036
|
switch (s.toLowerCase()) {
|
|
2449
3037
|
case "critical":
|
|
@@ -2473,8 +3061,8 @@ function cargoSeverity(s) {
|
|
|
2473
3061
|
return "info";
|
|
2474
3062
|
}
|
|
2475
3063
|
}
|
|
2476
|
-
function runNpmAudit(workspaceRoot2) {
|
|
2477
|
-
const result =
|
|
3064
|
+
async function runNpmAudit(workspaceRoot2, runner = defaultAuditCommandRunner) {
|
|
3065
|
+
const result = await runner("npm", ["audit", "--json"], workspaceRoot2);
|
|
2478
3066
|
const advisories = [];
|
|
2479
3067
|
let detailLines = [];
|
|
2480
3068
|
if (result.status === 0 || result.status === 1) {
|
|
@@ -2523,20 +3111,22 @@ function runNpmAudit(workspaceRoot2) {
|
|
|
2523
3111
|
};
|
|
2524
3112
|
return { advisories, evidence };
|
|
2525
3113
|
}
|
|
2526
|
-
function runPipAudit(workspaceRoot2) {
|
|
3114
|
+
async function runPipAudit(workspaceRoot2, runner = defaultAuditCommandRunner) {
|
|
2527
3115
|
const reqFiles = ["requirements.txt", "requirements-dev.txt"];
|
|
2528
3116
|
let reqFlag = "";
|
|
2529
3117
|
for (const f of reqFiles) {
|
|
2530
|
-
|
|
3118
|
+
try {
|
|
3119
|
+
await access3(join8(workspaceRoot2, f));
|
|
2531
3120
|
reqFlag = `--requirement ${f}`;
|
|
2532
3121
|
break;
|
|
3122
|
+
} catch {
|
|
2533
3123
|
}
|
|
2534
3124
|
}
|
|
2535
3125
|
const args = ["audit", "--format", "json"];
|
|
2536
3126
|
if (reqFlag) {
|
|
2537
3127
|
args.push(...reqFlag.split(" "));
|
|
2538
3128
|
}
|
|
2539
|
-
const result =
|
|
3129
|
+
const result = await runner("pip-audit", args, workspaceRoot2);
|
|
2540
3130
|
return parsePipAuditOutput(result);
|
|
2541
3131
|
}
|
|
2542
3132
|
function parsePipAuditOutput(result) {
|
|
@@ -2570,8 +3160,8 @@ function parsePipAuditOutput(result) {
|
|
|
2570
3160
|
};
|
|
2571
3161
|
return { advisories, evidence };
|
|
2572
3162
|
}
|
|
2573
|
-
function runCargoAudit(workspaceRoot2) {
|
|
2574
|
-
const result =
|
|
3163
|
+
async function runCargoAudit(workspaceRoot2, runner = defaultAuditCommandRunner) {
|
|
3164
|
+
const result = await runner("cargo", ["audit", "--json"], workspaceRoot2);
|
|
2575
3165
|
return parseCargoAuditOutput(result);
|
|
2576
3166
|
}
|
|
2577
3167
|
function parseCargoAuditOutput(result) {
|
|
@@ -2612,8 +3202,8 @@ function parseCargoAuditOutput(result) {
|
|
|
2612
3202
|
};
|
|
2613
3203
|
return { advisories, evidence };
|
|
2614
3204
|
}
|
|
2615
|
-
function runGoVulncheck(workspaceRoot2) {
|
|
2616
|
-
const result =
|
|
3205
|
+
async function runGoVulncheck(workspaceRoot2, runner = defaultAuditCommandRunner) {
|
|
3206
|
+
const result = await runner("govulncheck", ["-json"], workspaceRoot2);
|
|
2617
3207
|
const advisories = [];
|
|
2618
3208
|
let detailLines = [];
|
|
2619
3209
|
if (result.status === 0 || result.status === 3) {
|
|
@@ -2651,8 +3241,8 @@ function runGoVulncheck(workspaceRoot2) {
|
|
|
2651
3241
|
};
|
|
2652
3242
|
return { advisories, evidence };
|
|
2653
3243
|
}
|
|
2654
|
-
function runComposerAudit(workspaceRoot2) {
|
|
2655
|
-
const result =
|
|
3244
|
+
async function runComposerAudit(workspaceRoot2, runner = defaultAuditCommandRunner) {
|
|
3245
|
+
const result = await runner("composer", ["audit", "--format=json"], workspaceRoot2);
|
|
2656
3246
|
const advisories = [];
|
|
2657
3247
|
let detailLines = [];
|
|
2658
3248
|
if (result.status === 0) {
|
|
@@ -2688,8 +3278,8 @@ function runComposerAudit(workspaceRoot2) {
|
|
|
2688
3278
|
};
|
|
2689
3279
|
return { advisories, evidence };
|
|
2690
3280
|
}
|
|
2691
|
-
function runDotnetAudit(workspaceRoot2) {
|
|
2692
|
-
const result =
|
|
3281
|
+
async function runDotnetAudit(workspaceRoot2, runner = defaultAuditCommandRunner) {
|
|
3282
|
+
const result = await runner("dotnet", ["package", "audit", "--format", "json"], workspaceRoot2);
|
|
2693
3283
|
const advisories = [];
|
|
2694
3284
|
let detailLines = [];
|
|
2695
3285
|
if (result.status === 0) {
|
|
@@ -2739,44 +3329,42 @@ function runDotnetAudit(workspaceRoot2) {
|
|
|
2739
3329
|
};
|
|
2740
3330
|
return { advisories, evidence };
|
|
2741
3331
|
}
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
3332
|
+
var defaultAuditCommandRunner = (command, args, cwd) => new Promise((resolve3) => {
|
|
3333
|
+
execFile(
|
|
3334
|
+
command,
|
|
3335
|
+
[...args],
|
|
3336
|
+
{
|
|
2745
3337
|
cwd,
|
|
2746
|
-
encoding: "
|
|
3338
|
+
encoding: "utf8",
|
|
2747
3339
|
timeout: 6e4,
|
|
2748
3340
|
maxBuffer: 10 * 1024 * 1024,
|
|
2749
|
-
// 10MB
|
|
2750
3341
|
windowsHide: true
|
|
2751
|
-
}
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
status:
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
}
|
|
2765
|
-
}
|
|
2766
|
-
function runNativeAudit(ecosystem, workspaceRoot2) {
|
|
3342
|
+
},
|
|
3343
|
+
(error, stdout, stderr) => {
|
|
3344
|
+
const exitCode = error?.code;
|
|
3345
|
+
const status = error ? typeof exitCode === "number" ? exitCode : null : 0;
|
|
3346
|
+
resolve3({
|
|
3347
|
+
status,
|
|
3348
|
+
stdout,
|
|
3349
|
+
stderr: stderr || (status === null && error ? error.message : "")
|
|
3350
|
+
});
|
|
3351
|
+
}
|
|
3352
|
+
);
|
|
3353
|
+
});
|
|
3354
|
+
async function runNativeAudit(ecosystem, workspaceRoot2, runner = defaultAuditCommandRunner) {
|
|
2767
3355
|
switch (ecosystem) {
|
|
2768
3356
|
case "npm":
|
|
2769
|
-
return runNpmAudit(workspaceRoot2);
|
|
3357
|
+
return runNpmAudit(workspaceRoot2, runner);
|
|
2770
3358
|
case "python":
|
|
2771
|
-
return runPipAudit(workspaceRoot2);
|
|
3359
|
+
return runPipAudit(workspaceRoot2, runner);
|
|
2772
3360
|
case "rust":
|
|
2773
|
-
return runCargoAudit(workspaceRoot2);
|
|
3361
|
+
return runCargoAudit(workspaceRoot2, runner);
|
|
2774
3362
|
case "go":
|
|
2775
|
-
return runGoVulncheck(workspaceRoot2);
|
|
3363
|
+
return runGoVulncheck(workspaceRoot2, runner);
|
|
2776
3364
|
case "php":
|
|
2777
|
-
return runComposerAudit(workspaceRoot2);
|
|
3365
|
+
return runComposerAudit(workspaceRoot2, runner);
|
|
2778
3366
|
case "dotnet":
|
|
2779
|
-
return runDotnetAudit(workspaceRoot2);
|
|
3367
|
+
return runDotnetAudit(workspaceRoot2, runner);
|
|
2780
3368
|
// dart/pub doesn't have a standard audit command — use OSV instead
|
|
2781
3369
|
default:
|
|
2782
3370
|
return {
|
|
@@ -2790,14 +3378,41 @@ function runNativeAudit(ecosystem, workspaceRoot2) {
|
|
|
2790
3378
|
};
|
|
2791
3379
|
}
|
|
2792
3380
|
}
|
|
2793
|
-
function isNativeAuditAvailable(ecosystem) {
|
|
2794
|
-
const result =
|
|
3381
|
+
async function isNativeAuditAvailable(ecosystem, runner = defaultAuditCommandRunner) {
|
|
3382
|
+
const result = await runner(
|
|
2795
3383
|
ecosystem === "npm" ? "npm" : ecosystem === "python" ? "pip-audit" : ecosystem === "rust" ? "cargo" : ecosystem === "go" ? "govulncheck" : ecosystem === "php" ? "composer" : ecosystem === "dotnet" ? "dotnet" : "",
|
|
2796
3384
|
["--version"],
|
|
2797
3385
|
process.cwd()
|
|
2798
3386
|
);
|
|
2799
3387
|
return result.status === 0;
|
|
2800
3388
|
}
|
|
3389
|
+
function createAuditRunner(commandRunner = defaultAuditCommandRunner) {
|
|
3390
|
+
const commandFor = (ecosystem) => {
|
|
3391
|
+
switch (ecosystem) {
|
|
3392
|
+
case "npm":
|
|
3393
|
+
return "npm";
|
|
3394
|
+
case "python":
|
|
3395
|
+
return "pip-audit";
|
|
3396
|
+
case "rust":
|
|
3397
|
+
return "cargo";
|
|
3398
|
+
case "go":
|
|
3399
|
+
return "govulncheck";
|
|
3400
|
+
case "php":
|
|
3401
|
+
return "composer";
|
|
3402
|
+
case "dotnet":
|
|
3403
|
+
return "dotnet";
|
|
3404
|
+
default:
|
|
3405
|
+
return void 0;
|
|
3406
|
+
}
|
|
3407
|
+
};
|
|
3408
|
+
return {
|
|
3409
|
+
run: (ecosystem, workspaceRoot2) => runNativeAudit(ecosystem, workspaceRoot2, commandRunner),
|
|
3410
|
+
isAvailable: async (ecosystem, cwd = process.cwd()) => {
|
|
3411
|
+
const command = commandFor(ecosystem);
|
|
3412
|
+
return command ? (await commandRunner(command, ["--version"], cwd)).status === 0 : false;
|
|
3413
|
+
}
|
|
3414
|
+
};
|
|
3415
|
+
}
|
|
2801
3416
|
|
|
2802
3417
|
// src/policy/status.ts
|
|
2803
3418
|
function isValidSemver(version) {
|
|
@@ -2940,8 +3555,206 @@ function failedLookupStatus(source, error) {
|
|
|
2940
3555
|
};
|
|
2941
3556
|
}
|
|
2942
3557
|
|
|
2943
|
-
// src/service.ts
|
|
3558
|
+
// src/service/techstack-engine.ts
|
|
3559
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3560
|
+
|
|
3561
|
+
// src/service/finding-factory.ts
|
|
3562
|
+
function computeDependencyFingerprint(dependencies) {
|
|
3563
|
+
const parts = dependencies.map((dependency) => `${dependency.name}@${dependency.locked ?? dependency.requested ?? "unknown"}`).sort().join(",");
|
|
3564
|
+
let hash = 0;
|
|
3565
|
+
for (let index = 0; index < parts.length; index++) {
|
|
3566
|
+
hash = (hash << 5) - hash + parts.charCodeAt(index);
|
|
3567
|
+
hash |= 0;
|
|
3568
|
+
}
|
|
3569
|
+
return `ts-${Math.abs(hash).toString(36)}`;
|
|
3570
|
+
}
|
|
3571
|
+
function createFindingForStatus(dependencyId, status) {
|
|
3572
|
+
const common = { dependencyId, confidence: 1, evidence: [] };
|
|
3573
|
+
switch (status) {
|
|
3574
|
+
case "vulnerable":
|
|
3575
|
+
return { ...common, id: `finding-${dependencyId}-vuln`, type: "vulnerability", severity: "high", action: "upgrade_patch", rationale: "Known security advisory found for this package" };
|
|
3576
|
+
case "deprecated":
|
|
3577
|
+
return { ...common, id: `finding-${dependencyId}-dep`, type: "deprecated", severity: "medium", action: "replace", rationale: "Package is deprecated in the registry" };
|
|
3578
|
+
case "yanked":
|
|
3579
|
+
return { ...common, id: `finding-${dependencyId}-yank`, type: "deprecated", severity: "high", action: "replace", rationale: "Package version has been yanked from the registry" };
|
|
3580
|
+
case "update_available_safe":
|
|
3581
|
+
return { ...common, id: `finding-${dependencyId}-update`, type: "upgrade", severity: "info", action: "upgrade_minor", rationale: "A newer compatible version is available" };
|
|
3582
|
+
case "update_available_breaking":
|
|
3583
|
+
return { ...common, id: `finding-${dependencyId}-major`, type: "upgrade", severity: "low", action: "upgrade_major", rationale: "A newer version is available that may require breaking changes" };
|
|
3584
|
+
default:
|
|
3585
|
+
return { ...common, confidence: 0.5, id: `finding-${dependencyId}-investigate`, type: "investigate", severity: "info", action: "investigate", rationale: `Package status is "${status}" \u2014 may need investigation` };
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
// src/service/enrich-phase.ts
|
|
3590
|
+
async function runEnrichPhase(snapshot, options = {}) {
|
|
3591
|
+
if (options.online === false || options.signal?.aborted) return snapshot;
|
|
3592
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3593
|
+
for (const dependency of snapshot.dependencies) {
|
|
3594
|
+
if (!dependency.purl || dependency.sourceType === "path" || dependency.sourceType === "git") continue;
|
|
3595
|
+
grouped.set(dependency.ecosystem, [...grouped.get(dependency.ecosystem) ?? [], dependency]);
|
|
3596
|
+
}
|
|
3597
|
+
const enriched = /* @__PURE__ */ new Map();
|
|
3598
|
+
const findings = [...snapshot.findings];
|
|
3599
|
+
for (const [ecosystem, dependencies] of grouped) {
|
|
3600
|
+
for (const name of new Set(dependencies.map((dependency) => dependency.name))) {
|
|
3601
|
+
if (options.signal?.aborted) throw new DOMException("TechStack job cancelled", "AbortError");
|
|
3602
|
+
let registryEntry;
|
|
3603
|
+
let registryStatus;
|
|
3604
|
+
try {
|
|
3605
|
+
registryEntry = await lookupRegistry(ecosystem, name, {
|
|
3606
|
+
signal: options.signal,
|
|
3607
|
+
force: options.forceRegistryRefresh,
|
|
3608
|
+
strictErrors: true
|
|
3609
|
+
});
|
|
3610
|
+
registryStatus = registryEntry ? {
|
|
3611
|
+
latestStable: registryEntry.latestStable,
|
|
3612
|
+
deprecated: registryEntry.deprecated,
|
|
3613
|
+
yanked: registryEntry.yanked,
|
|
3614
|
+
evidence: [{ kind: "registry", source: registryEntry.source, retrievedAt: registryEntry.retrievedAt, detail: `latestStable: ${registryEntry.latestStable ?? "N/A"}, license: ${registryEntry.license ?? "N/A"}` }]
|
|
3615
|
+
} : { privateOrUnresolved: true };
|
|
3616
|
+
} catch (error) {
|
|
3617
|
+
const unresolved = error instanceof RegistryNotFoundError || error instanceof RegistryAuthError;
|
|
3618
|
+
registryStatus = unresolved ? {
|
|
3619
|
+
privateOrUnresolved: true,
|
|
3620
|
+
evidence: [{ kind: "registry", source: `${ecosystem} registry for ${name}`, retrievedAt: (/* @__PURE__ */ new Date()).toISOString(), detail: error.message }]
|
|
3621
|
+
} : {
|
|
3622
|
+
lookupFailed: true,
|
|
3623
|
+
evidence: [{ kind: "registry", source: `${ecosystem} registry for ${name}`, retrievedAt: (/* @__PURE__ */ new Date()).toISOString(), detail: error instanceof Error ? error.message : "Registry lookup failed" }]
|
|
3624
|
+
};
|
|
3625
|
+
}
|
|
3626
|
+
let advisoryStatus;
|
|
3627
|
+
try {
|
|
3628
|
+
const purls = dependencies.filter((dependency) => dependency.name === name).flatMap((dependency) => dependency.purl ? [dependency.purl] : []);
|
|
3629
|
+
if (purls.length > 0) {
|
|
3630
|
+
const result = await queryOsvBatch(purls, { signal: options.signal });
|
|
3631
|
+
if ([...result.advisories.values()].some((items) => items.length > 0)) {
|
|
3632
|
+
advisoryStatus = { hasAdvisory: true, evidence: [result.evidence] };
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
} catch {
|
|
3636
|
+
}
|
|
3637
|
+
for (const dependency of dependencies) {
|
|
3638
|
+
if (dependency.name !== name) continue;
|
|
3639
|
+
const status = classifyStatus(dependency, registryStatus, advisoryStatus);
|
|
3640
|
+
const evidence = [...dependency.evidence, ...registryStatus?.evidence ?? [], ...advisoryStatus?.evidence ?? []];
|
|
3641
|
+
enriched.set(dependency.id, {
|
|
3642
|
+
...dependency,
|
|
3643
|
+
latestStable: registryEntry?.latestStable ?? dependency.latestStable,
|
|
3644
|
+
license: registryEntry?.license ?? dependency.license,
|
|
3645
|
+
deprecated: registryEntry?.deprecated ?? dependency.deprecated,
|
|
3646
|
+
yanked: registryEntry?.yanked ?? dependency.yanked,
|
|
3647
|
+
status,
|
|
3648
|
+
evidence
|
|
3649
|
+
});
|
|
3650
|
+
if (status !== "current" && status !== "local_path" && status !== "git_dependency") findings.push(createFindingForStatus(dependency.id, status));
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
return { ...snapshot, dependencies: snapshot.dependencies.map((dependency) => enriched.get(dependency.id) ?? dependency), findings };
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3657
|
+
// src/service/inventory-phase.ts
|
|
2944
3658
|
import { randomUUID } from "node:crypto";
|
|
3659
|
+
var ADAPTER_VERSION = "0.1.0";
|
|
3660
|
+
var ADAPTERS = {
|
|
3661
|
+
npm: npmAdapter,
|
|
3662
|
+
python: pythonAdapter,
|
|
3663
|
+
rust: rustAdapter,
|
|
3664
|
+
go: goAdapter,
|
|
3665
|
+
dotnet: dotNetAdapter,
|
|
3666
|
+
php: phpAdapter,
|
|
3667
|
+
dart: dartAdapter,
|
|
3668
|
+
maven: mavenAdapter,
|
|
3669
|
+
gradle: gradleAdapter,
|
|
3670
|
+
ruby: rubyAdapter,
|
|
3671
|
+
swift: swiftAdapter,
|
|
3672
|
+
elixir: elixirAdapter,
|
|
3673
|
+
cpp: cppAdapter
|
|
3674
|
+
};
|
|
3675
|
+
async function runInventoryPhase(store, projectId, targetRoot, options = {}) {
|
|
3676
|
+
options.onProgress?.("discovering", 0, 1);
|
|
3677
|
+
const workspaces = await discoverWorkspaces(targetRoot, { signal: options.signal });
|
|
3678
|
+
options.onProgress?.("inventorying", 0, workspaces.length);
|
|
3679
|
+
const dependencies = [];
|
|
3680
|
+
let coverage = "full";
|
|
3681
|
+
let completed = 0;
|
|
3682
|
+
for (const workspace of workspaces) {
|
|
3683
|
+
if (options.signal?.aborted) throw new DOMException("TechStack job cancelled", "AbortError");
|
|
3684
|
+
try {
|
|
3685
|
+
dependencies.push(...await ADAPTERS[workspace.ecosystem].inventory(workspace, {
|
|
3686
|
+
projectRoot: targetRoot,
|
|
3687
|
+
includeTransitive: options.includeTransitive,
|
|
3688
|
+
signal: options.signal
|
|
3689
|
+
}));
|
|
3690
|
+
} catch {
|
|
3691
|
+
}
|
|
3692
|
+
if (workspace.coverage === "unsupported") coverage = "partial";
|
|
3693
|
+
completed++;
|
|
3694
|
+
options.onProgress?.("inventorying", completed, workspaces.length);
|
|
3695
|
+
}
|
|
3696
|
+
const snapshot = {
|
|
3697
|
+
id: randomUUID(),
|
|
3698
|
+
projectId,
|
|
3699
|
+
targetRoot,
|
|
3700
|
+
fingerprint: computeDependencyFingerprint(dependencies),
|
|
3701
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3702
|
+
workspaces,
|
|
3703
|
+
dependencies,
|
|
3704
|
+
findings: [],
|
|
3705
|
+
coverage,
|
|
3706
|
+
adapterVersion: ADAPTER_VERSION
|
|
3707
|
+
};
|
|
3708
|
+
store.saveSnapshot(snapshot);
|
|
3709
|
+
return snapshot;
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3712
|
+
// src/service/report-generator.ts
|
|
3713
|
+
function generateReport(snapshot, format = "md") {
|
|
3714
|
+
if (format === "json") return JSON.stringify(snapshot, null, 2);
|
|
3715
|
+
const lines = [
|
|
3716
|
+
"# TechStack Report",
|
|
3717
|
+
"",
|
|
3718
|
+
`**Generated:** ${snapshot.createdAt}`,
|
|
3719
|
+
`**Target:** ${snapshot.targetRoot}`,
|
|
3720
|
+
`**Fingerprint:** ${snapshot.fingerprint}`,
|
|
3721
|
+
`**Workspaces:** ${snapshot.workspaces.length}`,
|
|
3722
|
+
`**Dependencies:** ${snapshot.dependencies.length}`,
|
|
3723
|
+
`**Findings:** ${snapshot.findings.length}`,
|
|
3724
|
+
`**Coverage:** ${snapshot.coverage}`,
|
|
3725
|
+
""
|
|
3726
|
+
];
|
|
3727
|
+
if (snapshot.workspaces.length > 0) {
|
|
3728
|
+
lines.push("## Workspaces", "", "| Workspace | Ecosystem | Coverage | Deps |", "|---|---|---|---|");
|
|
3729
|
+
for (const workspace of snapshot.workspaces) {
|
|
3730
|
+
const count = snapshot.dependencies.filter((dependency) => dependency.workspaceId === workspace.id).length;
|
|
3731
|
+
lines.push(`| ${workspace.relativeRoot} | ${workspace.ecosystem} | ${workspace.coverage} | ${count} |`);
|
|
3732
|
+
}
|
|
3733
|
+
lines.push("");
|
|
3734
|
+
}
|
|
3735
|
+
if (snapshot.findings.length > 0) {
|
|
3736
|
+
lines.push("## Findings", "");
|
|
3737
|
+
const bySeverity = /* @__PURE__ */ new Map();
|
|
3738
|
+
for (const finding of snapshot.findings) bySeverity.set(finding.severity, [...bySeverity.get(finding.severity) ?? [], finding]);
|
|
3739
|
+
for (const severity of ["critical", "high", "medium", "low", "info"]) {
|
|
3740
|
+
const findings = bySeverity.get(severity);
|
|
3741
|
+
if (!findings?.length) continue;
|
|
3742
|
+
lines.push(`### ${severity.charAt(0).toUpperCase() + severity.slice(1)} (${findings.length})`, "");
|
|
3743
|
+
for (const finding of findings) {
|
|
3744
|
+
const dependency = snapshot.dependencies.find((candidate) => candidate.id === finding.dependencyId);
|
|
3745
|
+
lines.push(`- **${dependency?.name ?? finding.dependencyId}** \u2014 ${finding.type} \u2014 ${finding.rationale}`);
|
|
3746
|
+
}
|
|
3747
|
+
lines.push("");
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
if (snapshot.dependencies.length > 0) {
|
|
3751
|
+
lines.push("## Dependencies", "", "| Name | Ecosystem | Status | Locked | Latest |", "|---|---|---|---|---|");
|
|
3752
|
+
for (const dependency of snapshot.dependencies) {
|
|
3753
|
+
lines.push(`| ${dependency.name} | ${dependency.ecosystem} | ${dependency.status} | ${dependency.locked ?? "\u2014"} | ${dependency.latestStable ?? "\u2014"} |`);
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
return lines.join("\n");
|
|
3757
|
+
}
|
|
2945
3758
|
|
|
2946
3759
|
// src/research/triage.ts
|
|
2947
3760
|
var DEFAULT_TRIAGE_LIMIT = 40;
|
|
@@ -3003,285 +3816,45 @@ function clusterCandidates(candidates) {
|
|
|
3003
3816
|
return out;
|
|
3004
3817
|
}
|
|
3005
3818
|
|
|
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;
|
|
3819
|
+
// src/service/research-phase.ts
|
|
3820
|
+
async function runResearchPhase(snapshot, options = {}) {
|
|
3821
|
+
if (!options.researcher || options.signal?.aborted) return snapshot;
|
|
3822
|
+
const candidates = triageCandidates(snapshot.dependencies, { limit: options.researchLimit });
|
|
3823
|
+
if (candidates.length === 0) return snapshot;
|
|
3824
|
+
options.onProgress?.("researching", 0, candidates.length);
|
|
3825
|
+
let findings;
|
|
3826
|
+
try {
|
|
3827
|
+
findings = await options.researcher.research(candidates, {
|
|
3828
|
+
signal: options.signal,
|
|
3829
|
+
onProgress: (completed, total) => options.onProgress?.("researching", completed, total)
|
|
3830
|
+
});
|
|
3831
|
+
} catch {
|
|
3832
|
+
return snapshot;
|
|
3041
3833
|
}
|
|
3834
|
+
options.onProgress?.("synthesizing", 1, 1);
|
|
3835
|
+
return findings.length ? { ...snapshot, findings: [...snapshot.findings, ...findings] } : snapshot;
|
|
3042
3836
|
}
|
|
3043
|
-
|
|
3837
|
+
|
|
3838
|
+
// src/service/techstack-engine.ts
|
|
3044
3839
|
var TechStackEngine = class {
|
|
3045
|
-
store;
|
|
3046
3840
|
constructor(store) {
|
|
3047
3841
|
this.store = store;
|
|
3048
3842
|
}
|
|
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;
|
|
3843
|
+
store;
|
|
3844
|
+
inventory(projectId, targetRoot, _jobId, onProgress, options = {}) {
|
|
3845
|
+
return runInventoryPhase(this.store, projectId, targetRoot, { ...options, onProgress });
|
|
3106
3846
|
}
|
|
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
|
-
};
|
|
3847
|
+
enrich(snapshot, options = {}) {
|
|
3848
|
+
return runEnrichPhase(snapshot, options);
|
|
3236
3849
|
}
|
|
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] };
|
|
3850
|
+
research(snapshot, options = {}) {
|
|
3851
|
+
return runResearchPhase(snapshot, options);
|
|
3852
|
+
}
|
|
3853
|
+
generateReport(snapshot, format = "md") {
|
|
3854
|
+
return generateReport(snapshot, format);
|
|
3276
3855
|
}
|
|
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
3856
|
async analyze(projectId, options) {
|
|
3283
|
-
const jobId = options.jobId ??
|
|
3284
|
-
const requestedBy = options.requestedBy ?? "system";
|
|
3857
|
+
const jobId = options.jobId ?? randomUUID2();
|
|
3285
3858
|
const job = {
|
|
3286
3859
|
id: jobId,
|
|
3287
3860
|
projectId,
|
|
@@ -3289,7 +3862,7 @@ var TechStackEngine = class {
|
|
|
3289
3862
|
kind: "analyze",
|
|
3290
3863
|
status: "queued",
|
|
3291
3864
|
fingerprint: "",
|
|
3292
|
-
requestedBy,
|
|
3865
|
+
requestedBy: options.requestedBy ?? "system",
|
|
3293
3866
|
sessionId: options.sessionId,
|
|
3294
3867
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3295
3868
|
progress: { phase: "queued", completed: 0, total: 0 }
|
|
@@ -3305,199 +3878,32 @@ var TechStackEngine = class {
|
|
|
3305
3878
|
try {
|
|
3306
3879
|
throwIfAborted();
|
|
3307
3880
|
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
|
-
);
|
|
3881
|
+
let snapshot = await this.inventory(projectId, options.targetRoot, jobId, (phase, completed, total) => {
|
|
3882
|
+
throwIfAborted();
|
|
3883
|
+
updateJob(phase, { phase, completed, total });
|
|
3884
|
+
}, { includeTransitive: options.includeTransitive, signal: options.signal });
|
|
3317
3885
|
throwIfAborted();
|
|
3318
|
-
|
|
3319
|
-
if (isOnline) {
|
|
3886
|
+
if (options.online !== false) {
|
|
3320
3887
|
updateJob("enriching", { phase: "enriching", completed: 0, total: 1 });
|
|
3321
|
-
|
|
3322
|
-
online: true,
|
|
3323
|
-
signal: options.signal
|
|
3324
|
-
});
|
|
3888
|
+
snapshot = await this.enrich(snapshot, { online: true, signal: options.signal });
|
|
3325
3889
|
throwIfAborted();
|
|
3326
|
-
|
|
3890
|
+
snapshot = await this.research(snapshot, {
|
|
3327
3891
|
researcher: options.researcher,
|
|
3328
3892
|
researchLimit: options.researchLimit,
|
|
3329
3893
|
signal: options.signal,
|
|
3330
|
-
onProgress: (phase, completed, total) => {
|
|
3331
|
-
updateJob(phase, { phase, completed, total });
|
|
3332
|
-
}
|
|
3894
|
+
onProgress: (phase, completed, total) => updateJob(phase, { phase, completed, total })
|
|
3333
3895
|
});
|
|
3334
3896
|
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
3897
|
}
|
|
3342
3898
|
this.store.saveSnapshot(snapshot);
|
|
3343
3899
|
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;
|
|
3355
|
-
}
|
|
3356
|
-
}
|
|
3357
|
-
// ── Report generation ─────────────────────────────────────────────────
|
|
3358
|
-
/**
|
|
3359
|
-
* Generate a human-readable report from a snapshot.
|
|
3360
|
-
*
|
|
3361
|
-
* @param format 'md' for Markdown, 'json' for raw JSON.
|
|
3362
|
-
* @returns The report as a string.
|
|
3363
|
-
*/
|
|
3364
|
-
generateReport(snapshot, format = "md") {
|
|
3365
|
-
if (format === "json") return JSON.stringify(snapshot, null, 2);
|
|
3366
|
-
const lines = [
|
|
3367
|
-
"# TechStack Report",
|
|
3368
|
-
"",
|
|
3369
|
-
`**Generated:** ${snapshot.createdAt}`,
|
|
3370
|
-
`**Target:** ${snapshot.targetRoot}`,
|
|
3371
|
-
`**Fingerprint:** ${snapshot.fingerprint}`,
|
|
3372
|
-
`**Workspaces:** ${snapshot.workspaces.length}`,
|
|
3373
|
-
`**Dependencies:** ${snapshot.dependencies.length}`,
|
|
3374
|
-
`**Findings:** ${snapshot.findings.length}`,
|
|
3375
|
-
`**Coverage:** ${snapshot.coverage}`,
|
|
3376
|
-
""
|
|
3377
|
-
];
|
|
3378
|
-
if (snapshot.workspaces.length > 0) {
|
|
3379
|
-
lines.push("## Workspaces", "");
|
|
3380
|
-
lines.push("| Workspace | Ecosystem | Coverage | Deps |");
|
|
3381
|
-
lines.push("|---|---|---|---|");
|
|
3382
|
-
for (const ws of snapshot.workspaces) {
|
|
3383
|
-
const depCount = snapshot.dependencies.filter((d) => d.workspaceId === ws.id).length;
|
|
3384
|
-
lines.push(`| ${ws.relativeRoot} | ${ws.ecosystem} | ${ws.coverage} | ${depCount} |`);
|
|
3385
|
-
}
|
|
3386
|
-
lines.push("");
|
|
3387
|
-
}
|
|
3388
|
-
const findings = snapshot.findings;
|
|
3389
|
-
if (findings.length > 0) {
|
|
3390
|
-
lines.push("## Findings", "");
|
|
3391
|
-
const bySeverity = /* @__PURE__ */ new Map();
|
|
3392
|
-
for (const f of findings) {
|
|
3393
|
-
const list = bySeverity.get(f.severity) ?? [];
|
|
3394
|
-
list.push(f);
|
|
3395
|
-
bySeverity.set(f.severity, list);
|
|
3396
|
-
}
|
|
3397
|
-
for (const sev of ["critical", "high", "medium", "low", "info"]) {
|
|
3398
|
-
const items = bySeverity.get(sev);
|
|
3399
|
-
if (!items || items.length === 0) continue;
|
|
3400
|
-
lines.push(`### ${sev.charAt(0).toUpperCase() + sev.slice(1)} (${items.length})`, "");
|
|
3401
|
-
for (const f of items) {
|
|
3402
|
-
const dep = snapshot.dependencies.find((d) => d.id === f.dependencyId);
|
|
3403
|
-
lines.push(`- **${dep?.name ?? f.dependencyId}** \u2014 ${f.type} \u2014 ${f.rationale}`);
|
|
3404
|
-
}
|
|
3405
|
-
lines.push("");
|
|
3406
|
-
}
|
|
3407
|
-
}
|
|
3408
|
-
if (snapshot.dependencies.length > 0) {
|
|
3409
|
-
lines.push("## Dependencies", "");
|
|
3410
|
-
lines.push("| Name | Ecosystem | Status | Locked | Latest |");
|
|
3411
|
-
lines.push("|---|---|---|---|---|");
|
|
3412
|
-
for (const dep of snapshot.dependencies) {
|
|
3413
|
-
lines.push(
|
|
3414
|
-
`| ${dep.name} | ${dep.ecosystem} | ${dep.status} | ${dep.locked ?? "\u2014"} | ${dep.latestStable ?? "\u2014"} |`
|
|
3415
|
-
);
|
|
3416
|
-
}
|
|
3900
|
+
return { snapshot, job: { ...job, status: "completed", completedAt: (/* @__PURE__ */ new Date()).toISOString() } };
|
|
3901
|
+
} catch (error) {
|
|
3902
|
+
updateJob(options.signal?.aborted || error instanceof DOMException && error.name === "AbortError" ? "cancelled" : "failed");
|
|
3903
|
+
throw error;
|
|
3417
3904
|
}
|
|
3418
|
-
return lines.join("\n");
|
|
3419
3905
|
}
|
|
3420
3906
|
};
|
|
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
3907
|
|
|
3502
3908
|
// src/research/llm.ts
|
|
3503
3909
|
var DEFAULT_TIMEOUT_MS = 45e3;
|
|
@@ -3857,10 +4263,10 @@ function createToolSearch(options = {}) {
|
|
|
3857
4263
|
}
|
|
3858
4264
|
|
|
3859
4265
|
// src/store/sqlite.ts
|
|
3860
|
-
import {
|
|
3861
|
-
import { mkdirSync, existsSync as
|
|
3862
|
-
import { join as
|
|
3863
|
-
import {
|
|
4266
|
+
import { createRequire } from "node:module";
|
|
4267
|
+
import { mkdirSync, existsSync as existsSync2 } from "node:fs";
|
|
4268
|
+
import { dirname as dirname2, join as join9 } from "node:path";
|
|
4269
|
+
import { wstackGlobalRoot } from "@wrongstack/core/utils";
|
|
3864
4270
|
|
|
3865
4271
|
// src/store/schema.ts
|
|
3866
4272
|
var SCHEMA_VERSION = 1;
|
|
@@ -3930,30 +4336,77 @@ function applySchema(db) {
|
|
|
3930
4336
|
}
|
|
3931
4337
|
|
|
3932
4338
|
// src/store/sqlite.ts
|
|
4339
|
+
var DatabaseSyncCtor;
|
|
4340
|
+
var SQLITE_EXPERIMENTAL_WARNING = "SQLite is an experimental feature and might change at any time";
|
|
4341
|
+
function loadDatabaseSync() {
|
|
4342
|
+
if (DatabaseSyncCtor) return DatabaseSyncCtor;
|
|
4343
|
+
const originalEmitWarning = process.emitWarning;
|
|
4344
|
+
const forwardWarning = originalEmitWarning.bind(process);
|
|
4345
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
4346
|
+
const message = typeof warning === "string" ? warning : warning instanceof Error ? warning.message : "";
|
|
4347
|
+
const typeOrOptions = rest[0];
|
|
4348
|
+
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 : "";
|
|
4349
|
+
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 : "";
|
|
4350
|
+
if (message === SQLITE_EXPERIMENTAL_WARNING && (warningType === "ExperimentalWarning" || warningCode === "ExperimentalWarning")) {
|
|
4351
|
+
return;
|
|
4352
|
+
}
|
|
4353
|
+
forwardWarning(warning, ...rest);
|
|
4354
|
+
});
|
|
4355
|
+
try {
|
|
4356
|
+
try {
|
|
4357
|
+
const require2 = createRequire(import.meta.url);
|
|
4358
|
+
const sqliteModule = require2("node:sqlite");
|
|
4359
|
+
if (typeof sqliteModule !== "object" || sqliteModule === null || !("DatabaseSync" in sqliteModule) || typeof sqliteModule.DatabaseSync !== "function") {
|
|
4360
|
+
throw new Error("node:sqlite DatabaseSync unavailable");
|
|
4361
|
+
}
|
|
4362
|
+
DatabaseSyncCtor = sqliteModule.DatabaseSync;
|
|
4363
|
+
return DatabaseSyncCtor;
|
|
4364
|
+
} catch (error) {
|
|
4365
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4366
|
+
throw new Error(
|
|
4367
|
+
`TechStack SQLite store needs Node's built-in SQLite (node:sqlite), available since Node 22.5. This runtime doesn't provide it: ${message}`,
|
|
4368
|
+
{ cause: error }
|
|
4369
|
+
);
|
|
4370
|
+
}
|
|
4371
|
+
} finally {
|
|
4372
|
+
process.emitWarning = originalEmitWarning;
|
|
4373
|
+
}
|
|
4374
|
+
}
|
|
3933
4375
|
var TechStackStore = class {
|
|
3934
4376
|
db;
|
|
3935
4377
|
dbPath;
|
|
4378
|
+
/** Compile-once cache for fixed SQL used on hot job/snapshot paths. */
|
|
4379
|
+
stmtCache = /* @__PURE__ */ new Map();
|
|
3936
4380
|
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)) {
|
|
4381
|
+
this.dbPath = options.dbPath ?? join9(wstackGlobalRoot(), "projects", options.projectSlug, "techstack", "techstack.db");
|
|
4382
|
+
const dir = dirname2(this.dbPath);
|
|
4383
|
+
if (!existsSync2(dir)) {
|
|
3947
4384
|
mkdirSync(dir, { recursive: true });
|
|
3948
4385
|
}
|
|
3949
|
-
|
|
4386
|
+
const Database = loadDatabaseSync();
|
|
4387
|
+
this.db = new Database(this.dbPath);
|
|
3950
4388
|
this.db.exec("PRAGMA journal_mode = WAL;");
|
|
4389
|
+
this.db.exec("PRAGMA synchronous = NORMAL;");
|
|
4390
|
+
this.db.exec("PRAGMA busy_timeout = 10000;");
|
|
4391
|
+
this.db.exec("PRAGMA temp_store = MEMORY;");
|
|
4392
|
+
this.db.exec("PRAGMA cache_size = -32768;");
|
|
4393
|
+
this.db.exec("PRAGMA mmap_size = 134217728;");
|
|
3951
4394
|
this.db.exec("PRAGMA foreign_keys = ON;");
|
|
3952
4395
|
applySchema(this.db);
|
|
3953
4396
|
}
|
|
4397
|
+
/** Prepare-once helper: compile `sql` on first use, reuse thereafter. */
|
|
4398
|
+
stmt(sql) {
|
|
4399
|
+
let prepared = this.stmtCache.get(sql);
|
|
4400
|
+
if (prepared === void 0) {
|
|
4401
|
+
prepared = this.db.prepare(sql);
|
|
4402
|
+
this.stmtCache.set(sql, prepared);
|
|
4403
|
+
}
|
|
4404
|
+
return prepared;
|
|
4405
|
+
}
|
|
3954
4406
|
// ── Lifecycle ───────────────────────────────────────────────────────────
|
|
3955
4407
|
/** Close the database connection. Idempotent. */
|
|
3956
4408
|
close() {
|
|
4409
|
+
this.stmtCache.clear();
|
|
3957
4410
|
try {
|
|
3958
4411
|
this.db.close();
|
|
3959
4412
|
} catch {
|
|
@@ -3966,7 +4419,7 @@ var TechStackStore = class {
|
|
|
3966
4419
|
// ── Snapshots ───────────────────────────────────────────────────────────
|
|
3967
4420
|
/** Persist a snapshot. */
|
|
3968
4421
|
saveSnapshot(snapshot) {
|
|
3969
|
-
const stmt = this.
|
|
4422
|
+
const stmt = this.stmt(`
|
|
3970
4423
|
INSERT OR REPLACE INTO snapshots (id, project_id, target_root, fingerprint, created_at, raw_json, adapter_version)
|
|
3971
4424
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
3972
4425
|
`);
|
|
@@ -3982,7 +4435,7 @@ var TechStackStore = class {
|
|
|
3982
4435
|
}
|
|
3983
4436
|
/** Get a snapshot by project ID (latest). */
|
|
3984
4437
|
getSnapshot(projectId) {
|
|
3985
|
-
const stmt = this.
|
|
4438
|
+
const stmt = this.stmt(`
|
|
3986
4439
|
SELECT raw_json FROM snapshots
|
|
3987
4440
|
WHERE project_id = ?
|
|
3988
4441
|
ORDER BY created_at DESC
|
|
@@ -3998,7 +4451,7 @@ var TechStackStore = class {
|
|
|
3998
4451
|
}
|
|
3999
4452
|
/** Get a snapshot by ID. */
|
|
4000
4453
|
getSnapshotById(id) {
|
|
4001
|
-
const stmt = this.
|
|
4454
|
+
const stmt = this.stmt(`
|
|
4002
4455
|
SELECT raw_json FROM snapshots WHERE id = ?
|
|
4003
4456
|
`);
|
|
4004
4457
|
const row = stmt.get(id);
|
|
@@ -4011,7 +4464,7 @@ var TechStackStore = class {
|
|
|
4011
4464
|
}
|
|
4012
4465
|
/** List all snapshots for a project (newest first). */
|
|
4013
4466
|
listSnapshots(projectId, limit = 20) {
|
|
4014
|
-
const stmt = this.
|
|
4467
|
+
const stmt = this.stmt(`
|
|
4015
4468
|
SELECT raw_json FROM snapshots
|
|
4016
4469
|
WHERE project_id = ?
|
|
4017
4470
|
ORDER BY created_at DESC
|
|
@@ -4028,7 +4481,7 @@ var TechStackStore = class {
|
|
|
4028
4481
|
}
|
|
4029
4482
|
/** Delete snapshots older than a given timestamp. */
|
|
4030
4483
|
deleteSnapshotsBefore(projectId, before) {
|
|
4031
|
-
const stmt = this.
|
|
4484
|
+
const stmt = this.stmt(`
|
|
4032
4485
|
DELETE FROM snapshots WHERE project_id = ? AND created_at < ?
|
|
4033
4486
|
`);
|
|
4034
4487
|
const result = stmt.run(projectId, before);
|
|
@@ -4037,7 +4490,7 @@ var TechStackStore = class {
|
|
|
4037
4490
|
// ── Jobs ────────────────────────────────────────────────────────────────
|
|
4038
4491
|
/** Persist a job. */
|
|
4039
4492
|
saveJob(job) {
|
|
4040
|
-
const stmt = this.
|
|
4493
|
+
const stmt = this.stmt(`
|
|
4041
4494
|
INSERT OR REPLACE INTO jobs
|
|
4042
4495
|
(id, project_id, target_root, kind, status, fingerprint, requested_by, session_id, created_at, completed_at, error, progress_json)
|
|
4043
4496
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
@@ -4059,7 +4512,7 @@ var TechStackStore = class {
|
|
|
4059
4512
|
}
|
|
4060
4513
|
/** Get a job by ID. */
|
|
4061
4514
|
getJob(id) {
|
|
4062
|
-
const stmt = this.
|
|
4515
|
+
const stmt = this.stmt(`
|
|
4063
4516
|
SELECT * FROM jobs WHERE id = ?
|
|
4064
4517
|
`);
|
|
4065
4518
|
const row = stmt.get(id);
|
|
@@ -4070,7 +4523,7 @@ var TechStackStore = class {
|
|
|
4070
4523
|
updateJobStatus(id, status, progress) {
|
|
4071
4524
|
const progressJson = progress ? JSON.stringify(progress) : null;
|
|
4072
4525
|
const completedAt = status === "completed" || status === "failed" || status === "cancelled" ? (/* @__PURE__ */ new Date()).toISOString() : null;
|
|
4073
|
-
const stmt = this.
|
|
4526
|
+
const stmt = this.stmt(`
|
|
4074
4527
|
UPDATE jobs
|
|
4075
4528
|
SET status = ?, progress_json = ?, completed_at = COALESCE(?, completed_at)
|
|
4076
4529
|
WHERE id = ?
|
|
@@ -4079,7 +4532,7 @@ var TechStackStore = class {
|
|
|
4079
4532
|
}
|
|
4080
4533
|
/** List jobs for a project (newest first). */
|
|
4081
4534
|
listJobs(projectId, limit = 50) {
|
|
4082
|
-
const stmt = this.
|
|
4535
|
+
const stmt = this.stmt(`
|
|
4083
4536
|
SELECT * FROM jobs WHERE project_id = ? ORDER BY created_at DESC LIMIT ?
|
|
4084
4537
|
`);
|
|
4085
4538
|
const rows = stmt.all(projectId, limit);
|
|
@@ -4088,7 +4541,7 @@ var TechStackStore = class {
|
|
|
4088
4541
|
// ── Outbox ──────────────────────────────────────────────────────────────
|
|
4089
4542
|
/** Create an outbox entry. */
|
|
4090
4543
|
createOutbox(deliveryId, reportId, sessionId) {
|
|
4091
|
-
const stmt = this.
|
|
4544
|
+
const stmt = this.stmt(`
|
|
4092
4545
|
INSERT OR IGNORE INTO outbox (delivery_id, report_id, session_id, status, attempts)
|
|
4093
4546
|
VALUES (?, ?, ?, 'pending', 0)
|
|
4094
4547
|
`);
|
|
@@ -4096,7 +4549,7 @@ var TechStackStore = class {
|
|
|
4096
4549
|
}
|
|
4097
4550
|
/** Claim an outbox entry (atomic CAS). */
|
|
4098
4551
|
claimOutbox(deliveryId, sessionId) {
|
|
4099
|
-
const stmt = this.
|
|
4552
|
+
const stmt = this.stmt(`
|
|
4100
4553
|
UPDATE outbox
|
|
4101
4554
|
SET status = 'claimed', claimed_at = datetime('now'), attempts = attempts + 1
|
|
4102
4555
|
WHERE delivery_id = ? AND session_id = ? AND status = 'pending'
|
|
@@ -4106,7 +4559,7 @@ var TechStackStore = class {
|
|
|
4106
4559
|
}
|
|
4107
4560
|
/** Mark an outbox entry as delivered. */
|
|
4108
4561
|
deliverOutbox(deliveryId) {
|
|
4109
|
-
const stmt = this.
|
|
4562
|
+
const stmt = this.stmt(`
|
|
4110
4563
|
UPDATE outbox SET status = 'delivered', delivered_at = datetime('now')
|
|
4111
4564
|
WHERE delivery_id = ?
|
|
4112
4565
|
`);
|
|
@@ -4114,14 +4567,14 @@ var TechStackStore = class {
|
|
|
4114
4567
|
}
|
|
4115
4568
|
/** Mark an outbox entry as failed. */
|
|
4116
4569
|
failOutbox(deliveryId) {
|
|
4117
|
-
const stmt = this.
|
|
4570
|
+
const stmt = this.stmt(`
|
|
4118
4571
|
UPDATE outbox SET status = 'failed' WHERE delivery_id = ?
|
|
4119
4572
|
`);
|
|
4120
4573
|
stmt.run(deliveryId);
|
|
4121
4574
|
}
|
|
4122
4575
|
/** List outbox entries by status. */
|
|
4123
4576
|
listOutboxByStatus(status) {
|
|
4124
|
-
const stmt = this.
|
|
4577
|
+
const stmt = this.stmt(`
|
|
4125
4578
|
SELECT * FROM outbox WHERE status = ?
|
|
4126
4579
|
`);
|
|
4127
4580
|
const rows = stmt.all(status);
|
|
@@ -4228,14 +4681,22 @@ export {
|
|
|
4228
4681
|
DotNetAdapter,
|
|
4229
4682
|
ElixirAdapter,
|
|
4230
4683
|
GoAdapter,
|
|
4684
|
+
GradleAdapter,
|
|
4231
4685
|
MavenAdapter,
|
|
4232
4686
|
NpmAdapter,
|
|
4233
4687
|
PhpAdapter,
|
|
4234
4688
|
PythonAdapter,
|
|
4689
|
+
RegistryAuthError,
|
|
4690
|
+
RegistryNetworkError,
|
|
4691
|
+
RegistryNotFoundError,
|
|
4692
|
+
RegistryRateLimitError,
|
|
4235
4693
|
RubyAdapter,
|
|
4236
4694
|
RustAdapter,
|
|
4695
|
+
SwiftAdapter,
|
|
4237
4696
|
TechStackEngine,
|
|
4238
4697
|
TechStackStore,
|
|
4698
|
+
TrendStore,
|
|
4699
|
+
applyPlan,
|
|
4239
4700
|
applySchema,
|
|
4240
4701
|
attemptDelivery,
|
|
4241
4702
|
buildPurl,
|
|
@@ -4246,6 +4707,7 @@ export {
|
|
|
4246
4707
|
constructPurl,
|
|
4247
4708
|
coverageForEcosystem,
|
|
4248
4709
|
cppAdapter,
|
|
4710
|
+
createAuditRunner,
|
|
4249
4711
|
createProviderLlm,
|
|
4250
4712
|
createResearcher,
|
|
4251
4713
|
createToolSearch,
|
|
@@ -4259,6 +4721,8 @@ export {
|
|
|
4259
4721
|
failedLookupStatus,
|
|
4260
4722
|
generateUpgradePlan,
|
|
4261
4723
|
goAdapter,
|
|
4724
|
+
gradleAdapter,
|
|
4725
|
+
invalidateRegistryCache,
|
|
4262
4726
|
isNativeAuditAvailable,
|
|
4263
4727
|
lookupRegistry,
|
|
4264
4728
|
lookupRegistryBatch,
|
|
@@ -4275,12 +4739,15 @@ export {
|
|
|
4275
4739
|
queryOsvBatch,
|
|
4276
4740
|
queryOsvSingle,
|
|
4277
4741
|
renderPlanMarkdown,
|
|
4742
|
+
renderTrendMarkdown,
|
|
4278
4743
|
rubyAdapter,
|
|
4279
4744
|
runNativeAudit,
|
|
4280
4745
|
runNpmAudit,
|
|
4281
4746
|
rustAdapter,
|
|
4282
4747
|
supportedRegistryEcosystems,
|
|
4748
|
+
swiftAdapter,
|
|
4283
4749
|
toCycloneDX,
|
|
4750
|
+
toLanguagePackageInput,
|
|
4284
4751
|
toSpdx,
|
|
4285
4752
|
triageCandidates
|
|
4286
4753
|
};
|