border-customs 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -1
- package/dist/assets/release-src/releaseCoherence.ts +423 -0
- package/dist/assets/residue-src/crates.ts +13 -0
- package/dist/assets/residue-src/npm.ts +14 -0
- package/dist/assets/residue-src/pypi.ts +20 -0
- package/dist/assets/residue-src/rubygems.ts +12 -0
- package/dist/index.js +1629 -499
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -13036,9 +13036,9 @@ import { parseArgs } from "node:util";
|
|
|
13036
13036
|
|
|
13037
13037
|
// src/config.ts
|
|
13038
13038
|
var import_yaml = __toESM(require_dist(), 1);
|
|
13039
|
-
import { spawnSync as
|
|
13039
|
+
import { spawnSync as spawnSync11 } from "node:child_process";
|
|
13040
13040
|
import { existsSync as existsSync14, readFileSync as readFileSync13 } from "node:fs";
|
|
13041
|
-
import { dirname as dirname8, join as
|
|
13041
|
+
import { dirname as dirname8, join as join20, resolve as resolve9 } from "node:path";
|
|
13042
13042
|
|
|
13043
13043
|
// node_modules/zod/v4/classic/external.js
|
|
13044
13044
|
var external_exports = {};
|
|
@@ -32047,8 +32047,8 @@ var gitChannel = {
|
|
|
32047
32047
|
import { spawn as spawn2 } from "node:child_process";
|
|
32048
32048
|
|
|
32049
32049
|
// src/artifacts/npm.ts
|
|
32050
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as
|
|
32051
|
-
import { join as
|
|
32050
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
|
|
32051
|
+
import { join as join10, relative as relative2 } from "node:path";
|
|
32052
32052
|
|
|
32053
32053
|
// src/check/lock.ts
|
|
32054
32054
|
import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -38386,90 +38386,580 @@ function sweepResidueNpmArtifact(o) {
|
|
|
38386
38386
|
return hits;
|
|
38387
38387
|
}
|
|
38388
38388
|
|
|
38389
|
-
// src/
|
|
38390
|
-
|
|
38391
|
-
|
|
38389
|
+
// src/rules/releaseCoherence.ts
|
|
38390
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
38391
|
+
import { basename, join as join9 } from "node:path";
|
|
38392
|
+
|
|
38393
|
+
// src/ledger/records.ts
|
|
38394
|
+
import { appendFileSync, readFileSync as readFileSync5 } from "node:fs";
|
|
38395
|
+
import { join as join8 } from "node:path";
|
|
38396
|
+
var LEDGER_FILE = "ledger.jsonl";
|
|
38397
|
+
var RUNS_SUBDIR = "runs";
|
|
38398
|
+
var HEX64 = /^[0-9a-f]{64}$/;
|
|
38399
|
+
var HEX40 = /^[0-9a-f]{40}$/;
|
|
38400
|
+
var HEX8 = /^[0-9a-f]{8}$/;
|
|
38401
|
+
var PUSH_CONFIRMED_VIA = ["ls-remote", "npm-view", "pypi-json", "crates-json", "rubygems-json"];
|
|
38402
|
+
function ledgerPath(repoDir) {
|
|
38403
|
+
return join8(repoDir, BORDER_STATE_DIR, LEDGER_FILE);
|
|
38404
|
+
}
|
|
38405
|
+
function runsDir(repoDir) {
|
|
38406
|
+
return join8(repoDir, BORDER_STATE_DIR, RUNS_SUBDIR);
|
|
38407
|
+
}
|
|
38408
|
+
function formatSkipLine(record2) {
|
|
38409
|
+
return `SKIP ${record2.key8} \u2014 PASS ${record2.ts} report ${record2.reportPath}`;
|
|
38410
|
+
}
|
|
38411
|
+
function buildPushRecord(i) {
|
|
38392
38412
|
return {
|
|
38393
|
-
|
|
38394
|
-
|
|
38395
|
-
target:
|
|
38396
|
-
|
|
38397
|
-
|
|
38398
|
-
|
|
38399
|
-
|
|
38400
|
-
|
|
38413
|
+
t: "push",
|
|
38414
|
+
key: i.key,
|
|
38415
|
+
target: i.target,
|
|
38416
|
+
remoteName: i.remoteName,
|
|
38417
|
+
url: sanitizeUrl(i.url),
|
|
38418
|
+
localSha: i.localSha,
|
|
38419
|
+
...i.remoteSha !== void 0 ? { remoteSha: i.remoteSha } : {},
|
|
38420
|
+
...i.version !== void 0 ? { version: i.version } : {},
|
|
38421
|
+
confirmedVia: i.confirmedVia,
|
|
38422
|
+
ts: i.ts ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
38401
38423
|
};
|
|
38402
38424
|
}
|
|
38403
|
-
function
|
|
38404
|
-
if (
|
|
38405
|
-
|
|
38406
|
-
|
|
38407
|
-
|
|
38408
|
-
|
|
38409
|
-
|
|
38425
|
+
function buildRoundtripRecord(i) {
|
|
38426
|
+
if (!HEX64.test(i.artifactSha256)) bad("artifactSha256 is not hex-shaped");
|
|
38427
|
+
if (!HEX64.test(i.rulesHash)) bad("rulesHash is not hex-shaped");
|
|
38428
|
+
if (i.verdict !== "clean" && i.verdict !== "residue") bad(`verdict must be clean or residue, got '${i.verdict}'`);
|
|
38429
|
+
if (!Number.isInteger(i.rows) || i.rows < 0) bad("rows must be a non-negative integer");
|
|
38430
|
+
return {
|
|
38431
|
+
t: "roundtrip",
|
|
38432
|
+
artifactSha256: i.artifactSha256,
|
|
38433
|
+
verdict: i.verdict,
|
|
38434
|
+
rulesHash: i.rulesHash,
|
|
38435
|
+
rows: i.rows,
|
|
38436
|
+
ts: i.ts ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
38437
|
+
};
|
|
38438
|
+
}
|
|
38439
|
+
function bad(msg) {
|
|
38440
|
+
throw new Error(msg);
|
|
38441
|
+
}
|
|
38442
|
+
function needString(o, field) {
|
|
38443
|
+
const v = o[field];
|
|
38444
|
+
if (typeof v !== "string" || v === "") bad(`${field} must be a non-empty string`);
|
|
38445
|
+
return v;
|
|
38446
|
+
}
|
|
38447
|
+
function needHex(o, field, re) {
|
|
38448
|
+
const v = needString(o, field);
|
|
38449
|
+
if (!re.test(v)) bad(`${field} is not hex-shaped`);
|
|
38450
|
+
return v;
|
|
38451
|
+
}
|
|
38452
|
+
function needStringArray(o, field) {
|
|
38453
|
+
const v = o[field];
|
|
38454
|
+
if (!Array.isArray(v) || !v.every((x) => typeof x === "string")) bad(`${field} must be an array of strings`);
|
|
38455
|
+
return v;
|
|
38456
|
+
}
|
|
38457
|
+
function parseCounts(value) {
|
|
38458
|
+
if (typeof value !== "object" || value === null) bad("counts must be an object");
|
|
38459
|
+
const o = value;
|
|
38460
|
+
for (const field of ["INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL", "total", "blocking", "warnings"]) {
|
|
38461
|
+
if (typeof o[field] !== "number" || !Number.isInteger(o[field])) bad(`counts.${field} must be an integer`);
|
|
38462
|
+
}
|
|
38463
|
+
return o;
|
|
38464
|
+
}
|
|
38465
|
+
function parseArtifacts(o) {
|
|
38466
|
+
if (o.artifacts === null) return null;
|
|
38467
|
+
const v = o.artifacts;
|
|
38468
|
+
if (!Array.isArray(v)) bad("artifacts must be null or an array");
|
|
38469
|
+
return v.map((item, i) => {
|
|
38470
|
+
if (typeof item !== "object" || item === null) bad(`artifacts[${String(i)}] must be an object`);
|
|
38471
|
+
const a = item;
|
|
38472
|
+
return { file: needString(a, "file"), sha256: needHex(a, "sha256", HEX64) };
|
|
38410
38473
|
});
|
|
38411
38474
|
}
|
|
38412
|
-
function
|
|
38413
|
-
if (
|
|
38414
|
-
const
|
|
38415
|
-
|
|
38416
|
-
|
|
38417
|
-
|
|
38475
|
+
function parseLedgerRecord(value) {
|
|
38476
|
+
if (typeof value !== "object" || value === null) bad("record must be an object");
|
|
38477
|
+
const o = value;
|
|
38478
|
+
const t = o.t;
|
|
38479
|
+
if (t === "check") {
|
|
38480
|
+
const key = needHex(o, "key", HEX64);
|
|
38481
|
+
const key8 = needHex(o, "key8", HEX8);
|
|
38482
|
+
if (key.slice(0, 8) !== key8) bad("key8 must be the first 8 hex chars of key");
|
|
38483
|
+
const verdict = needString(o, "verdict");
|
|
38484
|
+
if (verdict !== "PASS" && verdict !== "FAIL") bad(`verdict must be PASS or FAIL, got '${verdict}'`);
|
|
38485
|
+
if (o.degraded !== false) bad("degraded must be false \u2014 degraded runs are never ledgered");
|
|
38486
|
+
if (typeof o.llm !== "boolean") bad("llm must be a boolean");
|
|
38487
|
+
return {
|
|
38488
|
+
t: "check",
|
|
38489
|
+
key,
|
|
38490
|
+
key8,
|
|
38491
|
+
head: needHex(o, "head", HEX40),
|
|
38492
|
+
dirtyDigest: needHex(o, "dirtyDigest", HEX64),
|
|
38493
|
+
refSetHash: needHex(o, "refSetHash", HEX64),
|
|
38494
|
+
exposureSet: needStringArray(o, "exposureSet"),
|
|
38495
|
+
effectiveTargets: needStringArray(o, "effectiveTargets"),
|
|
38496
|
+
rulesHash: needHex(o, "rulesHash", HEX64),
|
|
38497
|
+
artifacts: parseArtifacts(o),
|
|
38498
|
+
llm: o.llm,
|
|
38499
|
+
verdict,
|
|
38500
|
+
counts: parseCounts(o.counts),
|
|
38501
|
+
reportPath: needString(o, "reportPath"),
|
|
38502
|
+
degraded: false,
|
|
38503
|
+
ts: needString(o, "ts")
|
|
38504
|
+
};
|
|
38418
38505
|
}
|
|
38419
|
-
|
|
38420
|
-
|
|
38506
|
+
if (t === "push") {
|
|
38507
|
+
const confirmedVia = needString(o, "confirmedVia");
|
|
38508
|
+
if (!PUSH_CONFIRMED_VIA.includes(confirmedVia)) {
|
|
38509
|
+
bad(`confirmedVia must be one of ${PUSH_CONFIRMED_VIA.join("|")}`);
|
|
38510
|
+
}
|
|
38511
|
+
const remoteSha = o.remoteSha;
|
|
38512
|
+
const version2 = o.version;
|
|
38513
|
+
if (remoteSha === void 0 && version2 === void 0) bad("push record needs remoteSha or version");
|
|
38514
|
+
if (remoteSha !== void 0 && typeof remoteSha !== "string") bad("remoteSha must be a string");
|
|
38515
|
+
if (version2 !== void 0 && typeof version2 !== "string") bad("version must be a string");
|
|
38516
|
+
return {
|
|
38517
|
+
t: "push",
|
|
38518
|
+
key: needHex(o, "key", HEX64),
|
|
38519
|
+
target: needString(o, "target"),
|
|
38520
|
+
remoteName: needString(o, "remoteName"),
|
|
38521
|
+
url: needString(o, "url"),
|
|
38522
|
+
localSha: needString(o, "localSha"),
|
|
38523
|
+
...remoteSha !== void 0 ? { remoteSha } : {},
|
|
38524
|
+
...version2 !== void 0 ? { version: version2 } : {},
|
|
38525
|
+
confirmedVia,
|
|
38526
|
+
ts: needString(o, "ts")
|
|
38527
|
+
};
|
|
38528
|
+
}
|
|
38529
|
+
if (t === "roundtrip") {
|
|
38530
|
+
const verdict = needString(o, "verdict");
|
|
38531
|
+
if (verdict !== "clean" && verdict !== "residue") bad(`verdict must be clean or residue, got '${verdict}'`);
|
|
38532
|
+
const rows = o.rows;
|
|
38533
|
+
if (typeof rows !== "number" || !Number.isInteger(rows) || rows < 0) bad("rows must be a non-negative integer");
|
|
38534
|
+
return {
|
|
38535
|
+
t: "roundtrip",
|
|
38536
|
+
artifactSha256: needHex(o, "artifactSha256", HEX64),
|
|
38537
|
+
verdict,
|
|
38538
|
+
rulesHash: needHex(o, "rulesHash", HEX64),
|
|
38539
|
+
rows,
|
|
38540
|
+
ts: needString(o, "ts")
|
|
38541
|
+
};
|
|
38542
|
+
}
|
|
38543
|
+
return bad(`unknown record type ${JSON.stringify(t)}`);
|
|
38421
38544
|
}
|
|
38422
|
-
function
|
|
38423
|
-
|
|
38424
|
-
|
|
38425
|
-
|
|
38426
|
-
|
|
38427
|
-
|
|
38428
|
-
|
|
38429
|
-
|
|
38430
|
-
|
|
38545
|
+
function codeOf2(err) {
|
|
38546
|
+
return typeof err === "object" && err !== null ? err.code : void 0;
|
|
38547
|
+
}
|
|
38548
|
+
function readLedger(repoDir) {
|
|
38549
|
+
let text;
|
|
38550
|
+
try {
|
|
38551
|
+
text = readFileSync5(ledgerPath(repoDir), "utf8");
|
|
38552
|
+
} catch (err) {
|
|
38553
|
+
if (codeOf2(err) === "ENOENT") return { records: [], warnings: [] };
|
|
38554
|
+
throw err;
|
|
38555
|
+
}
|
|
38556
|
+
const records = [];
|
|
38557
|
+
const warnings = [];
|
|
38558
|
+
text.split("\n").forEach((line, i) => {
|
|
38559
|
+
if (line.trim() === "") return;
|
|
38560
|
+
try {
|
|
38561
|
+
records.push(parseLedgerRecord(JSON.parse(line)));
|
|
38562
|
+
} catch (err) {
|
|
38563
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
38564
|
+
warnings.push(`ledger line ${String(i + 1)} unreadable (${reason.slice(0, 160)}) \u2014 line skipped`);
|
|
38431
38565
|
}
|
|
38566
|
+
});
|
|
38567
|
+
return { records, warnings };
|
|
38568
|
+
}
|
|
38569
|
+
function appendRecord(repoDir, record2) {
|
|
38570
|
+
ensureStateDir(repoDir);
|
|
38571
|
+
appendFileSync(ledgerPath(repoDir), `${JSON.stringify(record2)}
|
|
38572
|
+
`, "utf8");
|
|
38573
|
+
}
|
|
38574
|
+
function newestRecordOf(records, key) {
|
|
38575
|
+
for (let i = records.length - 1; i >= 0; i -= 1) {
|
|
38576
|
+
const r = records[i];
|
|
38577
|
+
if (r.t === "check" && r.key === key) return r;
|
|
38432
38578
|
}
|
|
38433
|
-
return
|
|
38579
|
+
return null;
|
|
38434
38580
|
}
|
|
38435
|
-
|
|
38436
|
-
|
|
38437
|
-
|
|
38581
|
+
function isSkippable(r) {
|
|
38582
|
+
return r !== null && r.verdict === "PASS" && r.degraded === false;
|
|
38583
|
+
}
|
|
38584
|
+
function lookupSkipRecord(records, key, llm) {
|
|
38585
|
+
const newest = newestRecordOf(records, key);
|
|
38586
|
+
return isSkippable(newest) && newest.llm === llm ? newest : null;
|
|
38587
|
+
}
|
|
38588
|
+
function latestPassCoveringTargets(records, key, targets) {
|
|
38589
|
+
const newest = newestRecordOf(records, key);
|
|
38590
|
+
return isSkippable(newest) && targets.every((t) => newest.effectiveTargets.includes(t)) ? newest : null;
|
|
38591
|
+
}
|
|
38592
|
+
function pushRecords(records, key) {
|
|
38593
|
+
return records.filter((r) => r.t === "push" && (key === void 0 || r.key === key));
|
|
38594
|
+
}
|
|
38595
|
+
function latestRoundtripForSha(records, artifactSha256) {
|
|
38596
|
+
for (let i = records.length - 1; i >= 0; i -= 1) {
|
|
38597
|
+
const r = records[i];
|
|
38598
|
+
if (r !== void 0 && r.t === "roundtrip" && r.artifactSha256 === artifactSha256) return r;
|
|
38438
38599
|
}
|
|
38439
|
-
|
|
38440
|
-
|
|
38441
|
-
|
|
38442
|
-
|
|
38600
|
+
return null;
|
|
38601
|
+
}
|
|
38602
|
+
|
|
38603
|
+
// src/rules/releaseCoherence.ts
|
|
38604
|
+
var RELEASE_COHERENCE_DRIFT_RULE = "release-coherence-version-drift";
|
|
38605
|
+
var RELEASE_COHERENCE_TWIN_RULE = "release-coherence-twin-drift";
|
|
38606
|
+
var RELEASE_COHERENCE_UNVERIFIABLE_RULE = "release-coherence-unverifiable-source";
|
|
38607
|
+
var RELEASE_COHERENCE_SEVERITIES = {
|
|
38608
|
+
[RELEASE_COHERENCE_DRIFT_RULE]: "CRITICAL",
|
|
38609
|
+
[RELEASE_COHERENCE_TWIN_RULE]: "CRITICAL",
|
|
38610
|
+
[RELEASE_COHERENCE_UNVERIFIABLE_RULE]: "MEDIUM"
|
|
38611
|
+
};
|
|
38612
|
+
var TARGET = "artifact";
|
|
38613
|
+
var ENGINE = "native";
|
|
38614
|
+
var FOUNDING = "version truth must AGREE across every source packed in the artifact (aihr: a wheel named 0.2.2 shipped __version__ 0.2.1)";
|
|
38615
|
+
function probeDunderVersion(text) {
|
|
38616
|
+
for (const line of text.split(/\r?\n/)) {
|
|
38617
|
+
const assign = /^\s*__version__\s*=\s*(.+?)\s*$/.exec(line);
|
|
38618
|
+
if (assign?.[1] !== void 0) {
|
|
38619
|
+
const lit = /^(['"])((?:(?!\1).)+)\1(?:\s*#.*)?$/.exec(assign[1]);
|
|
38620
|
+
return lit?.[2] !== void 0 ? { status: "literal", value: lit[2] } : { status: "dynamic", reason: `__version__ assigned a non-literal (${assign[1].slice(0, 60)})` };
|
|
38621
|
+
}
|
|
38622
|
+
if (/^\s*(?:from\s+\S+\s+)?import\b.*\b__version__\b/.test(line)) {
|
|
38623
|
+
return { status: "dynamic", reason: "__version__ imported from another module" };
|
|
38624
|
+
}
|
|
38443
38625
|
}
|
|
38444
|
-
|
|
38445
|
-
|
|
38446
|
-
|
|
38626
|
+
return null;
|
|
38627
|
+
}
|
|
38628
|
+
function probeHeaderVersion(text) {
|
|
38629
|
+
const m = /^Version:[ \t]*(.*?)[ \t]*$/m.exec(text);
|
|
38630
|
+
if (m === null) return null;
|
|
38631
|
+
return m[1] !== void 0 && m[1] !== "" ? { status: "literal", value: m[1] } : { status: "dynamic", reason: "Version: header present but empty" };
|
|
38632
|
+
}
|
|
38633
|
+
function probePyprojectVersion(text) {
|
|
38634
|
+
const lines = text.split(/\r?\n/);
|
|
38635
|
+
let inProject = false;
|
|
38636
|
+
const slice = [];
|
|
38637
|
+
for (const line of lines) {
|
|
38638
|
+
if (/^\s*\[/.test(line)) {
|
|
38639
|
+
if (inProject) break;
|
|
38640
|
+
inProject = /^\s*\[project\]\s*$/.test(line);
|
|
38641
|
+
continue;
|
|
38642
|
+
}
|
|
38643
|
+
if (inProject) slice.push(line);
|
|
38644
|
+
}
|
|
38645
|
+
if (!inProject && slice.length === 0 && !text.includes("[project]")) return null;
|
|
38646
|
+
const body = slice.join("\n");
|
|
38647
|
+
const lit = /^\s*version\s*=\s*(['"])((?:(?!\1).)+)\1\s*(?:#.*)?$/m.exec(body);
|
|
38648
|
+
if (lit?.[2] !== void 0) return { status: "literal", value: lit[2] };
|
|
38649
|
+
const dyn = /dynamic\s*=\s*\[([\s\S]*?)\]/.exec(`${body}
|
|
38650
|
+
]`);
|
|
38651
|
+
if (dyn?.[1] !== void 0 && /["']version["']/.test(dyn[1])) return { status: "dynamic", reason: 'project.version listed in dynamic = ["version"]' };
|
|
38652
|
+
return { status: "dynamic", reason: "[project] declares no statically-readable version" };
|
|
38653
|
+
}
|
|
38654
|
+
function probeSetupCfgVersion(text) {
|
|
38655
|
+
const lines = text.split(/\r?\n/);
|
|
38656
|
+
let inSection = false;
|
|
38657
|
+
const slice = [];
|
|
38658
|
+
for (const line of lines) {
|
|
38659
|
+
if (/^\s*\[/.test(line)) {
|
|
38660
|
+
if (inSection) break;
|
|
38661
|
+
inSection = /^\s*\[metadata\]\s*$/.test(line);
|
|
38662
|
+
continue;
|
|
38663
|
+
}
|
|
38664
|
+
if (inSection) slice.push(line);
|
|
38665
|
+
}
|
|
38666
|
+
const v = /^[ \t]*version[ \t]*=[ \t]*(.+?)[ \t]*$/m.exec(slice.join("\n"));
|
|
38667
|
+
if (v?.[1] === void 0) return null;
|
|
38668
|
+
return /^[\w.+-]+$/.test(v[1]) ? { status: "literal", value: v[1] } : { status: "dynamic", reason: `setup.cfg version = '${v[1].slice(0, 40)}' is not a bare literal` };
|
|
38669
|
+
}
|
|
38670
|
+
function probeSetupPyVersion(text) {
|
|
38671
|
+
const lit = /(?:^|[,(]\s*)version\s*=\s*(['"])((?:(?!\1).)+)\1/.exec(text);
|
|
38672
|
+
if (lit?.[2] !== void 0) return { status: "literal", value: lit[2] };
|
|
38673
|
+
return /(?:^|[,(]\s*)version\s*=/.test(text) ? { status: "dynamic", reason: "setup.py version= is not a string literal" } : null;
|
|
38674
|
+
}
|
|
38675
|
+
function parseWheelFilenameVersion(fileName) {
|
|
38676
|
+
const base = basename(fileName).replace(/\.whl$/i, "");
|
|
38677
|
+
const parts = base.split("-");
|
|
38678
|
+
if (parts.length < 5) return null;
|
|
38679
|
+
const name = parts[0];
|
|
38680
|
+
const version2 = parts[1];
|
|
38681
|
+
return name !== "" && version2 !== "" ? { name, version: version2 } : null;
|
|
38682
|
+
}
|
|
38683
|
+
function parseDistInfoDirVersion(relPath) {
|
|
38684
|
+
const dir = /(?:^|\/)([^/]+\.dist-info)\//.exec(relPath)?.[1];
|
|
38685
|
+
if (dir === void 0) return null;
|
|
38686
|
+
const stem = dir.slice(0, -".dist-info".length);
|
|
38687
|
+
const idx = stem.lastIndexOf("-");
|
|
38688
|
+
return idx >= 0 ? stem.slice(idx + 1) : null;
|
|
38689
|
+
}
|
|
38690
|
+
var normName = (s) => s.toLowerCase().replace(/[-_.]+/g, "_");
|
|
38691
|
+
function toFindings2(hits, identity, sanitizer) {
|
|
38692
|
+
return hits.map((hit) => {
|
|
38693
|
+
const { valueDigest, snippet } = redact(`${identity}:release-coherence:${hit.rule}:${hit.disc}`);
|
|
38447
38694
|
return {
|
|
38448
|
-
|
|
38449
|
-
|
|
38450
|
-
|
|
38451
|
-
|
|
38452
|
-
|
|
38453
|
-
|
|
38454
|
-
|
|
38455
|
-
|
|
38456
|
-
artifact: null
|
|
38695
|
+
rule: hit.rule,
|
|
38696
|
+
severity: RELEASE_COHERENCE_SEVERITIES[hit.rule],
|
|
38697
|
+
target: TARGET,
|
|
38698
|
+
path: hit.path,
|
|
38699
|
+
engine: ENGINE,
|
|
38700
|
+
message: sanitizer?.sanitize(hit.message) ?? hit.message,
|
|
38701
|
+
valueDigest,
|
|
38702
|
+
snippet
|
|
38457
38703
|
};
|
|
38458
|
-
}
|
|
38459
|
-
ensureStateDir(repoDir);
|
|
38460
|
-
const stateDir = join8(repoDir, BORDER_STATE_DIR);
|
|
38461
|
-
const distDir = join8(stateDir, "dist");
|
|
38462
|
-
mkdirSync3(distDir, { recursive: true });
|
|
38463
|
-
const { filename, sha256: sha2563, bytes, tarballAbs } = packOnce({
|
|
38464
|
-
repoDir,
|
|
38465
|
-
distDir,
|
|
38466
|
-
...o.env !== void 0 ? { env: o.env } : {},
|
|
38467
|
-
...o.npmBinPath !== void 0 ? { npmBinPath: o.npmBinPath } : {},
|
|
38468
|
-
...o.npmTimeoutMs !== void 0 ? { npmTimeoutMs: o.npmTimeoutMs } : {}
|
|
38469
38704
|
});
|
|
38470
|
-
|
|
38471
|
-
|
|
38472
|
-
|
|
38705
|
+
}
|
|
38706
|
+
function analyze(sources, pathPrefix, hits) {
|
|
38707
|
+
const primary = sources.find((s) => s.probe?.status === "literal");
|
|
38708
|
+
for (const s of sources) {
|
|
38709
|
+
if (s.probe === null) continue;
|
|
38710
|
+
if (s.probe.status === "dynamic") {
|
|
38711
|
+
hits.push({
|
|
38712
|
+
rule: RELEASE_COHERENCE_UNVERIFIABLE_RULE,
|
|
38713
|
+
message: `release coherence: ${s.label} (${s.path}) carries a version that cannot be verified statically \u2014 ${s.probe.reason}; surfaced, never silent-clean`,
|
|
38714
|
+
path: `${pathPrefix}${s.path}`,
|
|
38715
|
+
disc: `${s.path}:unverifiable`
|
|
38716
|
+
});
|
|
38717
|
+
continue;
|
|
38718
|
+
}
|
|
38719
|
+
if (primary === void 0 || s === primary) continue;
|
|
38720
|
+
if (s.probe.value !== primary.probe.value) {
|
|
38721
|
+
hits.push({
|
|
38722
|
+
rule: RELEASE_COHERENCE_DRIFT_RULE,
|
|
38723
|
+
message: `release coherence: ${primary.label} '${primary.probe.value}' disagrees with ${s.label} '${s.probe.value}' \u2014 ${FOUNDING}`,
|
|
38724
|
+
path: `${pathPrefix}${s.path}`,
|
|
38725
|
+
disc: `${s.path}:vs:${primary.label}`
|
|
38726
|
+
});
|
|
38727
|
+
}
|
|
38728
|
+
}
|
|
38729
|
+
}
|
|
38730
|
+
function npmPackCoherenceFindings(o) {
|
|
38731
|
+
if (o.lockText === null || o.packedVersion === null) return [];
|
|
38732
|
+
let lock;
|
|
38733
|
+
try {
|
|
38734
|
+
lock = JSON.parse(o.lockText);
|
|
38735
|
+
} catch {
|
|
38736
|
+
return toFindings2([{ rule: RELEASE_COHERENCE_UNVERIFIABLE_RULE, message: `release coherence: ${o.lockRelPath} is packed but not parseable JSON \u2014 cannot verify, never silent-clean`, path: o.lockRelPath, disc: "malformed" }], o.identity, o.sanitizer);
|
|
38737
|
+
}
|
|
38738
|
+
const hits = [];
|
|
38739
|
+
const rootVer = typeof lock["version"] === "string" ? lock["version"] : null;
|
|
38740
|
+
const pkgs = lock["packages"];
|
|
38741
|
+
const rootPkg = typeof pkgs === "object" && pkgs !== null ? pkgs[""] : void 0;
|
|
38742
|
+
const pkgEmptyVer = typeof rootPkg === "object" && rootPkg !== null ? rootPkg["version"] : null;
|
|
38743
|
+
const cmp = (label, value, disc) => {
|
|
38744
|
+
if (value !== null && value !== o.packedVersion) {
|
|
38745
|
+
hits.push({ rule: RELEASE_COHERENCE_DRIFT_RULE, message: `release coherence: packed package.json version '${String(o.packedVersion)}' disagrees with ${label} '${value}' \u2014 ${FOUNDING}`, path: o.lockRelPath, disc });
|
|
38746
|
+
}
|
|
38747
|
+
};
|
|
38748
|
+
cmp("package-lock.json root version", rootVer, "root");
|
|
38749
|
+
cmp('package-lock.json packages[""] version', typeof pkgEmptyVer === "string" ? pkgEmptyVer : null, "packages-empty");
|
|
38750
|
+
return toFindings2(hits, o.identity, o.sanitizer);
|
|
38751
|
+
}
|
|
38752
|
+
function pypiArtifactCoherenceFindings(o) {
|
|
38753
|
+
const sources = [];
|
|
38754
|
+
const inTree = (rel) => o.files.includes(rel);
|
|
38755
|
+
const textOf = (rel) => inTree(rel) ? o.read(rel) : null;
|
|
38756
|
+
if (o.kind === "wheel") {
|
|
38757
|
+
const metaRel = o.files.find((f) => /\.dist-info\/METADATA$/.test(f));
|
|
38758
|
+
if (metaRel !== void 0) {
|
|
38759
|
+
const body = o.read(metaRel);
|
|
38760
|
+
sources.push({ label: "wheel METADATA Version:", path: metaRel, probe: body === null ? null : probeHeaderVersion(body) ?? { status: "dynamic", reason: "METADATA has no Version: header" } });
|
|
38761
|
+
const dirVer = parseDistInfoDirVersion(metaRel);
|
|
38762
|
+
if (dirVer !== null) sources.push({ label: "wheel .dist-info directory name", path: metaRel, probe: { status: "literal", value: dirVer } });
|
|
38763
|
+
}
|
|
38764
|
+
const fn = parseWheelFilenameVersion(o.archiveName);
|
|
38765
|
+
if (fn !== null) sources.push({ label: "wheel filename", path: metaRel ?? o.archiveName, probe: { status: "literal", value: fn.version } });
|
|
38766
|
+
} else {
|
|
38767
|
+
const pyproject = textOf("pyproject.toml");
|
|
38768
|
+
if (pyproject !== null) sources.push({ label: "pyproject [project].version", path: "pyproject.toml", probe: probePyprojectVersion(pyproject) });
|
|
38769
|
+
const cfg = textOf("setup.cfg");
|
|
38770
|
+
if (cfg !== null) sources.push({ label: "setup.cfg [metadata] version", path: "setup.cfg", probe: probeSetupCfgVersion(cfg) });
|
|
38771
|
+
const py = textOf("setup.py");
|
|
38772
|
+
if (py !== null) sources.push({ label: "setup.py version literal", path: "setup.py", probe: probeSetupPyVersion(py) });
|
|
38773
|
+
const pkgInfoRel = o.files.find((f) => f === "PKG-INFO" || f.endsWith(".egg-info/PKG-INFO"));
|
|
38774
|
+
if (pkgInfoRel !== void 0) {
|
|
38775
|
+
const body = o.read(pkgInfoRel);
|
|
38776
|
+
if (body !== null) sources.push({ label: "PKG-INFO Version:", path: pkgInfoRel, probe: probeHeaderVersion(body) ?? { status: "dynamic", reason: "PKG-INFO has no Version: header" } });
|
|
38777
|
+
}
|
|
38778
|
+
}
|
|
38779
|
+
for (const initRel of o.files.filter((f) => /(?:^|\/)__init__\.py$/.test(f)).sort()) {
|
|
38780
|
+
const body = o.read(initRel);
|
|
38781
|
+
if (body === null) continue;
|
|
38782
|
+
const probe = probeDunderVersion(body);
|
|
38783
|
+
if (probe !== null) sources.push({ label: `__version__ literal in ${initRel}`, path: initRel, probe });
|
|
38784
|
+
}
|
|
38785
|
+
const hits = [];
|
|
38786
|
+
analyze(sources, `${o.archiveName}!`, hits);
|
|
38787
|
+
return toFindings2(hits, o.identity, o.sanitizer);
|
|
38788
|
+
}
|
|
38789
|
+
function cratesCoherenceFindings(o) {
|
|
38790
|
+
if (o.lockText === null) return [];
|
|
38791
|
+
const entries = o.lockText.split(/^\[\[package\]\][ \t]*$/m).map((block) => ({ name: /^\s*name\s*=\s*"([^"]*)"/m.exec(block)?.[1], version: /^\s*version\s*=\s*"([^"]*)"/m.exec(block)?.[1] })).filter((e) => e.name !== void 0 && e.version !== void 0).filter((e) => e.name === o.name);
|
|
38792
|
+
if (entries.length === 0) {
|
|
38793
|
+
return toFindings2([{ rule: RELEASE_COHERENCE_UNVERIFIABLE_RULE, message: `release coherence: ${o.lockRelPath} is packed but names no [[package]] '${o.name}' \u2014 cannot verify, never silent-clean`, path: o.lockRelPath, disc: "no-entry" }], o.identity, o.sanitizer);
|
|
38794
|
+
}
|
|
38795
|
+
const hits = [];
|
|
38796
|
+
for (const v of [...new Set(entries.map((e) => e.version))].sort()) {
|
|
38797
|
+
if (v !== o.version) {
|
|
38798
|
+
hits.push({ rule: RELEASE_COHERENCE_DRIFT_RULE, message: `release coherence: Cargo.toml [package].version '${o.version}' disagrees with Cargo.lock entry '${o.name}' version '${v}' \u2014 ${FOUNDING}`, path: o.lockRelPath, disc: v });
|
|
38799
|
+
}
|
|
38800
|
+
}
|
|
38801
|
+
return toFindings2(hits, o.identity, o.sanitizer);
|
|
38802
|
+
}
|
|
38803
|
+
function gemCoherenceFindings(o) {
|
|
38804
|
+
if (o.metadataText === null) return [];
|
|
38805
|
+
const name = /^name:[ \t]*(\S+)[ \t]*$/m.exec(o.metadataText)?.[1];
|
|
38806
|
+
const version2 = /^version:[ \t]*!ruby\/object:Gem::Version[ \t]*\n[ \t]+version:[ \t]*(\S+)[ \t]*$/m.exec(o.metadataText)?.[1];
|
|
38807
|
+
if (name === void 0 || version2 === void 0) {
|
|
38808
|
+
return toFindings2([{ rule: RELEASE_COHERENCE_UNVERIFIABLE_RULE, message: "release coherence: metadata.gz is present but its Gem::Specification name/version could not be read statically \u2014 cannot verify, never silent-clean", path: "metadata.gz", disc: "unparseable" }], o.identity, o.sanitizer);
|
|
38809
|
+
}
|
|
38810
|
+
const hits = [];
|
|
38811
|
+
if (name !== o.specName) hits.push({ rule: RELEASE_COHERENCE_DRIFT_RULE, message: `release coherence: built gem '${o.identity}' names '${o.specName}' but metadata.gz Gem::Specification name is '${name}' \u2014 ${FOUNDING}`, path: "metadata.gz", disc: "name" });
|
|
38812
|
+
if (version2 !== o.specVersion) hits.push({ rule: RELEASE_COHERENCE_DRIFT_RULE, message: `release coherence: built gem '${o.identity}' names version '${o.specVersion}' but metadata.gz Gem::Version is '${version2}' \u2014 ${FOUNDING}`, path: "metadata.gz", disc: "version" });
|
|
38813
|
+
return toFindings2(hits, o.identity, o.sanitizer);
|
|
38814
|
+
}
|
|
38815
|
+
function packedNpmIdentity(archiveAbs, env2) {
|
|
38816
|
+
const opts = { encoding: "utf8", timeout: 3e4, ...env2 !== void 0 ? { env: env2 } : {} };
|
|
38817
|
+
const listing = spawnSync3("tar", ["-tzf", archiveAbs], opts);
|
|
38818
|
+
if (listing.status !== 0) return null;
|
|
38819
|
+
const members2 = (listing.stdout ?? "").split("\n").filter((l) => /^[^/]+\/package\.json$/.test(l));
|
|
38820
|
+
const member = members2.includes("package/package.json") ? "package/package.json" : members2.length === 1 ? members2[0] : void 0;
|
|
38821
|
+
if (member === void 0) return null;
|
|
38822
|
+
const body = spawnSync3("tar", ["-xzOf", archiveAbs, member], opts);
|
|
38823
|
+
if (body.status !== 0) return null;
|
|
38824
|
+
try {
|
|
38825
|
+
const pkg = JSON.parse(body.stdout);
|
|
38826
|
+
return typeof pkg["name"] === "string" && typeof pkg["version"] === "string" ? { name: pkg["name"], version: pkg["version"] } : null;
|
|
38827
|
+
} catch {
|
|
38828
|
+
return null;
|
|
38829
|
+
}
|
|
38830
|
+
}
|
|
38831
|
+
async function twinCoherenceFindings(o) {
|
|
38832
|
+
const wheels = o.artifacts.filter((a) => a.file.endsWith(".whl")).sort((a, b) => a.file.localeCompare(b.file));
|
|
38833
|
+
const tgzs = o.artifacts.filter((a) => a.file.endsWith(".tgz")).sort((a, b) => a.file.localeCompare(b.file));
|
|
38834
|
+
const hits = [];
|
|
38835
|
+
for (const pair of o.pairs) {
|
|
38836
|
+
const wh = wheels.find((a) => {
|
|
38837
|
+
const parsed = parseWheelFilenameVersion(basename(a.file));
|
|
38838
|
+
return parsed !== null && normName(parsed.name) === normName(pair.pypi);
|
|
38839
|
+
});
|
|
38840
|
+
if (wh === void 0) {
|
|
38841
|
+
hits.push({ rule: RELEASE_COHERENCE_UNVERIFIABLE_RULE, message: `release coherence: release.twin pair '${pair.pypi}'/'${pair.npm}' \u2014 no staged wheel for '${pair.pypi}' in this run, so the declared cross-manager equality cannot be verified`, path: `${pair.pypi}(missing).whl`, disc: `twin:${pair.pypi}:no-wheel` });
|
|
38842
|
+
continue;
|
|
38843
|
+
}
|
|
38844
|
+
const wheelVersion = parseWheelFilenameVersion(basename(wh.file)).version;
|
|
38845
|
+
let npmSide = null;
|
|
38846
|
+
let npmUnreadable = false;
|
|
38847
|
+
for (const t of tgzs) {
|
|
38848
|
+
const id = packedNpmIdentity(join9(o.repoDir, t.file), o.env);
|
|
38849
|
+
if (id === null) {
|
|
38850
|
+
npmUnreadable = true;
|
|
38851
|
+
continue;
|
|
38852
|
+
}
|
|
38853
|
+
if (id.name === pair.npm) {
|
|
38854
|
+
npmSide = { file: t.file, version: id.version };
|
|
38855
|
+
break;
|
|
38856
|
+
}
|
|
38857
|
+
}
|
|
38858
|
+
if (npmSide === null) {
|
|
38859
|
+
hits.push({
|
|
38860
|
+
rule: RELEASE_COHERENCE_UNVERIFIABLE_RULE,
|
|
38861
|
+
message: `release coherence: release.twin pair '${pair.pypi}'/'${pair.npm}' \u2014 ${npmUnreadable ? `npm twin '${pair.npm}' is staged but its package.json could not be read from the tarball bytes` : `no staged npm artifact for twin '${pair.npm}' in this run`}, so the declared cross-manager equality cannot be verified`,
|
|
38862
|
+
path: basename(wh.file),
|
|
38863
|
+
disc: `twin:${pair.pypi}:no-npm`
|
|
38864
|
+
});
|
|
38865
|
+
continue;
|
|
38866
|
+
}
|
|
38867
|
+
if (wheelVersion !== npmSide.version) {
|
|
38868
|
+
hits.push({
|
|
38869
|
+
rule: RELEASE_COHERENCE_TWIN_RULE,
|
|
38870
|
+
message: `release coherence: release.twin pair '${pair.pypi}'/'${pair.npm}' \u2014 wheel '${basename(wh.file)}' says version '${wheelVersion}' but npm twin '${basename(npmSide.file)}' packed package.json says '${npmSide.version}' \u2014 ${FOUNDING}`,
|
|
38871
|
+
path: basename(wh.file),
|
|
38872
|
+
disc: `twin:${pair.pypi}:${basename(npmSide.file)}`
|
|
38873
|
+
});
|
|
38874
|
+
}
|
|
38875
|
+
}
|
|
38876
|
+
return toFindings2(hits, "release.twin", o.sanitizer);
|
|
38877
|
+
}
|
|
38878
|
+
|
|
38879
|
+
// src/artifacts/npm.ts
|
|
38880
|
+
function nativeFinding(rule, severity, message, path4, identity, sanitizer) {
|
|
38881
|
+
const digest2 = redact(identity);
|
|
38882
|
+
return {
|
|
38883
|
+
rule,
|
|
38884
|
+
severity,
|
|
38885
|
+
target: NPM_TARGET_LABEL,
|
|
38886
|
+
path: path4,
|
|
38887
|
+
engine: "native",
|
|
38888
|
+
message: sanitizeOut(sanitizer, message),
|
|
38889
|
+
valueDigest: digest2.valueDigest,
|
|
38890
|
+
snippet: digest2.snippet
|
|
38891
|
+
};
|
|
38892
|
+
}
|
|
38893
|
+
function attributeToTarball(findings, artifact) {
|
|
38894
|
+
if (artifact === null) return findings;
|
|
38895
|
+
const name = artifact.file.slice(artifact.file.lastIndexOf("/") + 1);
|
|
38896
|
+
return findings.map((f) => {
|
|
38897
|
+
const p = f.path ?? "";
|
|
38898
|
+
const rel = p === "package.json" ? p : p.startsWith("package/") ? p.slice("package/".length) : null;
|
|
38899
|
+
return rel === null || rel === "" ? f : { ...f, path: `${name}!${rel}` };
|
|
38900
|
+
});
|
|
38901
|
+
}
|
|
38902
|
+
function scopeFindingPath(f, extractDir) {
|
|
38903
|
+
if (f.path === void 0) return f;
|
|
38904
|
+
const bang = f.path.indexOf("!");
|
|
38905
|
+
if (bang > 0) {
|
|
38906
|
+
const rel2 = relative2(extractDir, f.path.slice(0, bang));
|
|
38907
|
+
if (!rel2.startsWith("..")) return { ...f, path: rel2 === "" ? f.path.slice(bang + 1) : `${rel2}${f.path.slice(bang)}` };
|
|
38908
|
+
}
|
|
38909
|
+
const rel = relative2(extractDir, f.path);
|
|
38910
|
+
return rel === "" || rel.startsWith("..") ? f : { ...f, path: rel };
|
|
38911
|
+
}
|
|
38912
|
+
function listFiles(dir) {
|
|
38913
|
+
const out = [];
|
|
38914
|
+
const stack = [dir];
|
|
38915
|
+
while (stack.length > 0) {
|
|
38916
|
+
const cur = stack.pop();
|
|
38917
|
+
for (const ent of readdirSync2(cur, { withFileTypes: true })) {
|
|
38918
|
+
const p = join10(cur, ent.name);
|
|
38919
|
+
if (ent.isDirectory()) stack.push(p);
|
|
38920
|
+
else if (ent.isFile()) out.push(p);
|
|
38921
|
+
}
|
|
38922
|
+
}
|
|
38923
|
+
return out;
|
|
38924
|
+
}
|
|
38925
|
+
async function runNpmArtifactStage(o) {
|
|
38926
|
+
if (o.cfg.targets.npm === void 0) {
|
|
38927
|
+
throw new EngineRunError("runNpmArtifactStage invoked without targets.npm configured \u2014 pipeline bug", null);
|
|
38928
|
+
}
|
|
38929
|
+
const { repoDir } = o;
|
|
38930
|
+
const manifestPath = join10(repoDir, "package.json");
|
|
38931
|
+
if (!existsSync6(manifestPath)) {
|
|
38932
|
+
throw new EngineRunError("targets.npm is configured but the repo has no package.json to pack", null);
|
|
38933
|
+
}
|
|
38934
|
+
const manifest = parseNpmManifest(readFileSync6(manifestPath, "utf8"));
|
|
38935
|
+
const identity = `${manifest.name ?? "?"}@${manifest.version ?? "?"}`;
|
|
38936
|
+
if (manifest.isPrivate) {
|
|
38937
|
+
return {
|
|
38938
|
+
findings: [nativeFinding(
|
|
38939
|
+
NPM_PRIVATE_RULE,
|
|
38940
|
+
"CRITICAL",
|
|
38941
|
+
`package.json is private:true but border.yaml configures targets.npm \u2014 nothing can ever publish from this repo; drop the private flag or the npm target`,
|
|
38942
|
+
"package.json",
|
|
38943
|
+
`${identity}:private`,
|
|
38944
|
+
o.sanitizer
|
|
38945
|
+
)],
|
|
38946
|
+
artifact: null
|
|
38947
|
+
};
|
|
38948
|
+
}
|
|
38949
|
+
ensureStateDir(repoDir);
|
|
38950
|
+
const stateDir = join10(repoDir, BORDER_STATE_DIR);
|
|
38951
|
+
const distDir = join10(stateDir, "dist");
|
|
38952
|
+
mkdirSync3(distDir, { recursive: true });
|
|
38953
|
+
const { filename, sha256: sha2563, bytes, tarballAbs } = packOnce({
|
|
38954
|
+
repoDir,
|
|
38955
|
+
distDir,
|
|
38956
|
+
...o.env !== void 0 ? { env: o.env } : {},
|
|
38957
|
+
...o.npmBinPath !== void 0 ? { npmBinPath: o.npmBinPath } : {},
|
|
38958
|
+
...o.npmTimeoutMs !== void 0 ? { npmTimeoutMs: o.npmTimeoutMs } : {}
|
|
38959
|
+
});
|
|
38960
|
+
const findings = [];
|
|
38961
|
+
const extractDir = join10(stateDir, "tmp", sha2563.slice(0, 8));
|
|
38962
|
+
rmSync3(extractDir, { recursive: true, force: true });
|
|
38473
38963
|
mkdirSync3(extractDir, { recursive: true });
|
|
38474
38964
|
try {
|
|
38475
38965
|
extractArchive(tarballAbs, extractDir, {
|
|
@@ -38484,8 +38974,16 @@ async function runNpmArtifactStage(o) {
|
|
|
38484
38974
|
}
|
|
38485
38975
|
const packedManifestRel = `${root}/package.json`;
|
|
38486
38976
|
if (!relPaths.includes(packedManifestRel)) throw new EngineRunError("extracted artifact has no package/package.json", null);
|
|
38487
|
-
const packedManifest = parseNpmManifest(
|
|
38488
|
-
const
|
|
38977
|
+
const packedManifest = parseNpmManifest(readFileSync6(join10(extractDir, packedManifestRel), "utf8"));
|
|
38978
|
+
const lockRel = `${root}/package-lock.json`;
|
|
38979
|
+
findings.push(...npmPackCoherenceFindings({
|
|
38980
|
+
packedVersion: packedManifest.version,
|
|
38981
|
+
lockText: relPaths.includes(lockRel) ? readFileSync6(join10(extractDir, lockRel), "utf8") : null,
|
|
38982
|
+
lockRelPath: lockRel,
|
|
38983
|
+
identity,
|
|
38984
|
+
...o.sanitizer !== void 0 ? { sanitizer: o.sanitizer } : {}
|
|
38985
|
+
}));
|
|
38986
|
+
const pkgDir = join10(extractDir, root);
|
|
38489
38987
|
const residueCtx = { root, identity, ...o.sanitizer !== void 0 ? { sanitizer: o.sanitizer } : {} };
|
|
38490
38988
|
for (const key of LIFECYCLE_SCRIPT_KEYS) {
|
|
38491
38989
|
const cmd = packedManifest.scripts[key];
|
|
@@ -38570,7 +39068,7 @@ async function runNpmArtifactStage(o) {
|
|
|
38570
39068
|
}
|
|
38571
39069
|
|
|
38572
39070
|
// src/check/context.ts
|
|
38573
|
-
import { spawnSync as
|
|
39071
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
38574
39072
|
import { createHash as createHash4 } from "node:crypto";
|
|
38575
39073
|
import { dirname as dirname4, resolve as resolve4 } from "node:path";
|
|
38576
39074
|
|
|
@@ -38597,7 +39095,7 @@ function isRecord(value) {
|
|
|
38597
39095
|
// src/check/context.ts
|
|
38598
39096
|
function gitSpawn(repoDir, args, o = {}) {
|
|
38599
39097
|
const abs = resolve4(repoDir);
|
|
38600
|
-
const r =
|
|
39098
|
+
const r = spawnSync4("git", ["-C", abs, ...args], {
|
|
38601
39099
|
encoding: "utf8",
|
|
38602
39100
|
env: { ...o.env ?? process.env, GIT_CEILING_DIRECTORIES: dirname4(abs) },
|
|
38603
39101
|
timeout: 6e4
|
|
@@ -38656,7 +39154,7 @@ function computeEffectiveTargets(cfg, requested) {
|
|
|
38656
39154
|
}
|
|
38657
39155
|
|
|
38658
39156
|
// src/rules/identity.ts
|
|
38659
|
-
import { spawnSync as
|
|
39157
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
38660
39158
|
import { dirname as dirname5, resolve as resolve5 } from "node:path";
|
|
38661
39159
|
|
|
38662
39160
|
// src/rules/allowlist.ts
|
|
@@ -38721,7 +39219,7 @@ function addRole(entries, key, entry, obj, role) {
|
|
|
38721
39219
|
found.objects.set(obj, prev === void 0 ? role : `${prev}+${role}`);
|
|
38722
39220
|
}
|
|
38723
39221
|
function runGit2(repoDir, args) {
|
|
38724
|
-
const r =
|
|
39222
|
+
const r = spawnSync5("git", args, {
|
|
38725
39223
|
cwd: repoDir,
|
|
38726
39224
|
encoding: "utf8",
|
|
38727
39225
|
env: { ...process.env, GIT_CEILING_DIRECTORIES: dirname5(resolve5(repoDir)) }
|
|
@@ -38767,7 +39265,7 @@ function collectCommits(s, revs) {
|
|
|
38767
39265
|
}
|
|
38768
39266
|
}
|
|
38769
39267
|
function revExists(repoDir, rev) {
|
|
38770
|
-
const r =
|
|
39268
|
+
const r = spawnSync5("git", ["rev-parse", "--verify", "--quiet", rev], {
|
|
38771
39269
|
cwd: repoDir,
|
|
38772
39270
|
encoding: "utf8",
|
|
38773
39271
|
env: { ...process.env, GIT_CEILING_DIRECTORIES: dirname5(resolve5(repoDir)) }
|
|
@@ -38871,12 +39369,12 @@ function scanIdentity(o) {
|
|
|
38871
39369
|
// src/ledger.ts
|
|
38872
39370
|
import { createHash as createHash8 } from "node:crypto";
|
|
38873
39371
|
import { writeFileSync as writeFileSync3 } from "node:fs";
|
|
38874
|
-
import { join as
|
|
39372
|
+
import { join as join15 } from "node:path";
|
|
38875
39373
|
|
|
38876
39374
|
// src/check/rulesHash.ts
|
|
38877
39375
|
import { createHash as createHash5 } from "node:crypto";
|
|
38878
39376
|
import { existsSync as existsSync7 } from "node:fs";
|
|
38879
|
-
import { dirname as dirname6, join as
|
|
39377
|
+
import { dirname as dirname6, join as join11 } from "node:path";
|
|
38880
39378
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
38881
39379
|
var RESIDUE_FINGERPRINT_SOURCES = [
|
|
38882
39380
|
{ dir: "rules", base: "residueMatchers.ts" },
|
|
@@ -38894,12 +39392,26 @@ function resolveResidueFingerprintFiles(env2 = process.env) {
|
|
|
38894
39392
|
const here = dirname6(fileURLToPath4(import.meta.url));
|
|
38895
39393
|
const seam = env2.BORDER_RESIDUE_SRC_DIR;
|
|
38896
39394
|
return RESIDUE_FINGERPRINT_SOURCES.map(({ dir, base }) => {
|
|
38897
|
-
if (seam !== void 0) return
|
|
38898
|
-
const live =
|
|
39395
|
+
if (seam !== void 0) return join11(seam, base);
|
|
39396
|
+
const live = join11(here, "..", dir, base);
|
|
38899
39397
|
if (existsSync7(live)) return live;
|
|
38900
39398
|
return resolveAsset(import.meta.url, ["residue-src", base]);
|
|
38901
39399
|
});
|
|
38902
39400
|
}
|
|
39401
|
+
var RELEASE_FINGERPRINT_SOURCES = [
|
|
39402
|
+
{ dir: "rules", base: "releaseCoherence.ts" }
|
|
39403
|
+
];
|
|
39404
|
+
var RELEASE_FINGERPRINT_BASENAMES = RELEASE_FINGERPRINT_SOURCES.map((s) => s.base);
|
|
39405
|
+
function resolveReleaseFingerprintFiles(env2 = process.env) {
|
|
39406
|
+
const here = dirname6(fileURLToPath4(import.meta.url));
|
|
39407
|
+
const seam = env2.BORDER_RELEASE_SRC_DIR;
|
|
39408
|
+
return RELEASE_FINGERPRINT_SOURCES.map(({ dir, base }) => {
|
|
39409
|
+
if (seam !== void 0) return join11(seam, base);
|
|
39410
|
+
const live = join11(here, "..", dir, base);
|
|
39411
|
+
if (existsSync7(live)) return live;
|
|
39412
|
+
return resolveAsset(import.meta.url, ["release-src", base]);
|
|
39413
|
+
});
|
|
39414
|
+
}
|
|
38903
39415
|
function resolvePromptTemplatePath(env2 = process.env) {
|
|
38904
39416
|
return env2.BORDER_PROMPT_TEMPLATE_PATH ?? resolveAsset(import.meta.url, ["prompts", "llm-review.md"]);
|
|
38905
39417
|
}
|
|
@@ -38918,7 +39430,11 @@ function computeConfigDigest(load) {
|
|
|
38918
39430
|
async function computeCheckRulesHash(input2) {
|
|
38919
39431
|
const template = resolvePromptTemplatePath(input2.env ?? process.env);
|
|
38920
39432
|
return computeRulesHash({
|
|
38921
|
-
bundledRulePaths: [
|
|
39433
|
+
bundledRulePaths: [
|
|
39434
|
+
GITLEAKS_VENDORED_CONFIG,
|
|
39435
|
+
...resolveResidueFingerprintFiles(input2.env ?? process.env),
|
|
39436
|
+
...resolveReleaseFingerprintFiles(input2.env ?? process.env)
|
|
39437
|
+
],
|
|
38922
39438
|
configDigest: input2.configDigest,
|
|
38923
39439
|
engineVersions: input2.engineVersions,
|
|
38924
39440
|
promptTemplatePaths: existsSync7(template) ? [template] : []
|
|
@@ -39187,23 +39703,23 @@ function renderReportMd(report) {
|
|
|
39187
39703
|
}
|
|
39188
39704
|
|
|
39189
39705
|
// src/ledger/freshness.ts
|
|
39190
|
-
import { spawnSync as
|
|
39706
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
39191
39707
|
import { createHash as createHash7 } from "node:crypto";
|
|
39192
|
-
import { mkdirSync as mkdirSync5, readFileSync as
|
|
39193
|
-
import { join as
|
|
39708
|
+
import { mkdirSync as mkdirSync5, readFileSync as readFileSync8, readdirSync as readdirSync4, rmSync as rmSync5 } from "node:fs";
|
|
39709
|
+
import { join as join13 } from "node:path";
|
|
39194
39710
|
|
|
39195
39711
|
// src/artifacts/rubygems.ts
|
|
39196
39712
|
import { createHash as createHash6 } from "node:crypto";
|
|
39197
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as
|
|
39198
|
-
import { join as
|
|
39199
|
-
import { spawnSync as
|
|
39713
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync7, readdirSync as readdirSync3, rmSync as rmSync4 } from "node:fs";
|
|
39714
|
+
import { join as join12, relative as relative3 } from "node:path";
|
|
39715
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
39200
39716
|
import { gunzipSync } from "node:zlib";
|
|
39201
39717
|
|
|
39202
39718
|
// src/artifacts/residueGem.ts
|
|
39203
|
-
import { basename } from "node:path";
|
|
39719
|
+
import { basename as basename2 } from "node:path";
|
|
39204
39720
|
var GEM_WORDING = { what: "gem install-time", ledger: "gem" };
|
|
39205
39721
|
function gemPersistenceHit(rel, text) {
|
|
39206
|
-
if (GEM_PERSISTENCE_BASENAMES.includes(
|
|
39722
|
+
if (GEM_PERSISTENCE_BASENAMES.includes(basename2(rel))) {
|
|
39207
39723
|
return {
|
|
39208
39724
|
rule: RESIDUE_T4_RULE,
|
|
39209
39725
|
severity: "CRITICAL",
|
|
@@ -39449,7 +39965,7 @@ function residueGemHits(pkgDir, rels, metadata) {
|
|
|
39449
39965
|
const rel = normalizeRel(raw);
|
|
39450
39966
|
const text = readText(pkgDir, rel);
|
|
39451
39967
|
if (text === null) continue;
|
|
39452
|
-
const base =
|
|
39968
|
+
const base = basename2(rel);
|
|
39453
39969
|
const isExtconf = base === GEM_EXTCONF_BASENAME;
|
|
39454
39970
|
const persistence = gemPersistenceHit(rel, text);
|
|
39455
39971
|
if (persistence !== null) hits.push(persistence);
|
|
@@ -39548,7 +40064,7 @@ function listFiles2(dir) {
|
|
|
39548
40064
|
while (stack.length > 0) {
|
|
39549
40065
|
const cur = stack.pop();
|
|
39550
40066
|
for (const ent of readdirSync3(cur, { withFileTypes: true })) {
|
|
39551
|
-
const p =
|
|
40067
|
+
const p = join12(cur, ent.name);
|
|
39552
40068
|
if (ent.isDirectory()) stack.push(p);
|
|
39553
40069
|
else if (ent.isFile()) out.push(p);
|
|
39554
40070
|
}
|
|
@@ -39561,11 +40077,11 @@ function treeGemspecCandidates(repoDir) {
|
|
|
39561
40077
|
while (stack.length > 0) {
|
|
39562
40078
|
const cur = stack.pop();
|
|
39563
40079
|
for (const ent of readdirSync3(cur, { withFileTypes: true })) {
|
|
39564
|
-
const p =
|
|
40080
|
+
const p = join12(cur, ent.name);
|
|
39565
40081
|
if (ent.isDirectory()) {
|
|
39566
40082
|
if (!SKIP_DIRECTORIES2.has(ent.name)) stack.push(p);
|
|
39567
40083
|
} else if (ent.isFile() && ent.name.toLowerCase().endsWith(".gemspec")) {
|
|
39568
|
-
out.push({ path: relative3(repoDir, p), body:
|
|
40084
|
+
out.push({ path: relative3(repoDir, p), body: readFileSync7(p, "utf8") });
|
|
39569
40085
|
}
|
|
39570
40086
|
}
|
|
39571
40087
|
}
|
|
@@ -39597,7 +40113,7 @@ async function runRubygemsArtifactStage(o) {
|
|
|
39597
40113
|
throw e;
|
|
39598
40114
|
}
|
|
39599
40115
|
const gemName = `${identity.name}-${identity.version}.gem`;
|
|
39600
|
-
const versionCheck =
|
|
40116
|
+
const versionCheck = spawnSync6(bin, ["--version"], {
|
|
39601
40117
|
encoding: "utf8",
|
|
39602
40118
|
stdio: "ignore",
|
|
39603
40119
|
...o.env === void 0 ? {} : { env: { ...o.env } }
|
|
@@ -39608,11 +40124,11 @@ async function runRubygemsArtifactStage(o) {
|
|
|
39608
40124
|
);
|
|
39609
40125
|
}
|
|
39610
40126
|
ensureStateDir(repoDir);
|
|
39611
|
-
const stateDir =
|
|
39612
|
-
const distDir =
|
|
40127
|
+
const stateDir = join12(repoDir, BORDER_STATE_DIR);
|
|
40128
|
+
const distDir = join12(stateDir, "dist");
|
|
39613
40129
|
mkdirSync4(distDir, { recursive: true });
|
|
39614
|
-
const dist =
|
|
39615
|
-
const result =
|
|
40130
|
+
const dist = join12(distDir, gemName);
|
|
40131
|
+
const result = spawnSync6(bin, ["build", candidates[0].path, "-o", dist], {
|
|
39616
40132
|
cwd: repoDir,
|
|
39617
40133
|
encoding: "utf8",
|
|
39618
40134
|
timeout: o.gemTimeoutMs ?? 12e4,
|
|
@@ -39627,10 +40143,10 @@ async function runRubygemsArtifactStage(o) {
|
|
|
39627
40143
|
if (!existsSync8(dist)) {
|
|
39628
40144
|
throw new EngineRunError(`gem build produced no ${gemName} in ${relative3(repoDir, distDir)} \u2014 failing closed (was the gemspec's literal name/version re-read?)`, null);
|
|
39629
40145
|
}
|
|
39630
|
-
const bytes =
|
|
40146
|
+
const bytes = readFileSync7(dist);
|
|
39631
40147
|
const sha2563 = createHash6("sha256").update(bytes).digest("hex");
|
|
39632
40148
|
const findings = [];
|
|
39633
|
-
const extractDir =
|
|
40149
|
+
const extractDir = join12(stateDir, "tmp", sha2563.slice(0, 8));
|
|
39634
40150
|
rmSync4(extractDir, { recursive: true, force: true });
|
|
39635
40151
|
mkdirSync4(extractDir, { recursive: true });
|
|
39636
40152
|
const dataDir = `${extractDir}-data`;
|
|
@@ -39669,13 +40185,13 @@ async function runRubygemsArtifactStage(o) {
|
|
|
39669
40185
|
if (members2.has("metadata.gz") && members2.has("data.tar.gz")) {
|
|
39670
40186
|
let metadata;
|
|
39671
40187
|
try {
|
|
39672
|
-
metadata = gunzipSync(
|
|
40188
|
+
metadata = gunzipSync(readFileSync7(join12(extractDir, "metadata.gz"))).toString("utf8");
|
|
39673
40189
|
} catch {
|
|
39674
40190
|
throw new EngineRunError(`${gemName}: metadata.gz is not gunzip-readable \u2014 the extensions declaration cannot be verified, failing closed`, null);
|
|
39675
40191
|
}
|
|
39676
40192
|
rmSync4(dataDir, { recursive: true, force: true });
|
|
39677
40193
|
mkdirSync4(dataDir, { recursive: true });
|
|
39678
|
-
extractArchive(
|
|
40194
|
+
extractArchive(join12(extractDir, "data.tar.gz"), dataDir, {
|
|
39679
40195
|
...o.env !== void 0 ? { env: o.env } : {},
|
|
39680
40196
|
...o.timeoutMs !== void 0 ? { timeoutMs: o.timeoutMs } : {}
|
|
39681
40197
|
});
|
|
@@ -39694,6 +40210,13 @@ async function runRubygemsArtifactStage(o) {
|
|
|
39694
40210
|
})) {
|
|
39695
40211
|
findings.push(f);
|
|
39696
40212
|
}
|
|
40213
|
+
findings.push(...gemCoherenceFindings({
|
|
40214
|
+
specName: identity.name,
|
|
40215
|
+
specVersion: identity.version,
|
|
40216
|
+
metadataText: metadata,
|
|
40217
|
+
identity: gemName,
|
|
40218
|
+
...o.sanitizer !== void 0 ? { sanitizer: o.sanitizer } : {}
|
|
40219
|
+
}));
|
|
39697
40220
|
}
|
|
39698
40221
|
} finally {
|
|
39699
40222
|
removeSandbox(extractDir);
|
|
@@ -39710,15 +40233,15 @@ function artifactTargets() {
|
|
|
39710
40233
|
return publishChannels().map((c) => c.id);
|
|
39711
40234
|
}
|
|
39712
40235
|
function sha256File(path4) {
|
|
39713
|
-
return createHash7("sha256").update(
|
|
40236
|
+
return createHash7("sha256").update(readFileSync8(path4)).digest("hex");
|
|
39714
40237
|
}
|
|
39715
40238
|
var packSeq = 0;
|
|
39716
40239
|
function packNpmArtifacts(repoDir, o = {}) {
|
|
39717
40240
|
packSeq += 1;
|
|
39718
|
-
const dest =
|
|
40241
|
+
const dest = join13(repoDir, BORDER_STATE_DIR, "tmp", `pack-${String(process.pid)}-${String(packSeq)}`);
|
|
39719
40242
|
mkdirSync5(dest, { recursive: true });
|
|
39720
40243
|
try {
|
|
39721
|
-
const r =
|
|
40244
|
+
const r = spawnSync7("npm", ["pack", "--ignore-scripts", "--silent", `--pack-destination=${dest}`], {
|
|
39722
40245
|
cwd: repoDir,
|
|
39723
40246
|
encoding: "utf8",
|
|
39724
40247
|
env: { ...o.env ?? process.env },
|
|
@@ -39727,27 +40250,27 @@ function packNpmArtifacts(repoDir, o = {}) {
|
|
|
39727
40250
|
if (r.error !== void 0 || r.status !== 0) return null;
|
|
39728
40251
|
const files = readdirSync4(dest).filter((f) => f.endsWith(".tgz")).sort();
|
|
39729
40252
|
if (files.length === 0) return null;
|
|
39730
|
-
return files.map((f) => ({ file: f, sha256: sha256File(
|
|
40253
|
+
return files.map((f) => ({ file: f, sha256: sha256File(join13(dest, f)) }));
|
|
39731
40254
|
} finally {
|
|
39732
40255
|
rmSync5(dest, { recursive: true, force: true });
|
|
39733
40256
|
}
|
|
39734
40257
|
}
|
|
39735
40258
|
function packCrateArtifacts(repoDir, o = {}) {
|
|
39736
40259
|
packSeq += 1;
|
|
39737
|
-
const dest =
|
|
40260
|
+
const dest = join13(repoDir, BORDER_STATE_DIR, "tmp", `cargo-pack-${String(process.pid)}-${String(packSeq)}`);
|
|
39738
40261
|
mkdirSync5(dest, { recursive: true });
|
|
39739
40262
|
try {
|
|
39740
|
-
const r =
|
|
40263
|
+
const r = spawnSync7("cargo", ["package", "--allow-dirty", "--no-verify"], {
|
|
39741
40264
|
cwd: repoDir,
|
|
39742
40265
|
encoding: "utf8",
|
|
39743
40266
|
env: { ...o.env ?? process.env, CARGO_TARGET_DIR: dest },
|
|
39744
40267
|
timeout: 12e4
|
|
39745
40268
|
});
|
|
39746
40269
|
if (r.error !== void 0 || r.status !== 0) return null;
|
|
39747
|
-
const dir =
|
|
40270
|
+
const dir = join13(dest, "package");
|
|
39748
40271
|
const files = readdirSync4(dir).filter((f) => f.endsWith(".crate")).sort();
|
|
39749
40272
|
if (files.length === 0) return null;
|
|
39750
|
-
return files.map((f) => ({ file: f, sha256: sha256File(
|
|
40273
|
+
return files.map((f) => ({ file: f, sha256: sha256File(join13(dir, f)) }));
|
|
39751
40274
|
} finally {
|
|
39752
40275
|
rmSync5(dest, { recursive: true, force: true });
|
|
39753
40276
|
}
|
|
@@ -39756,11 +40279,11 @@ function packRubygemsArtifacts(repoDir, o = {}) {
|
|
|
39756
40279
|
packSeq += 1;
|
|
39757
40280
|
const ident = singleTreeGemspec(repoDir);
|
|
39758
40281
|
if (ident === null) return null;
|
|
39759
|
-
const dest =
|
|
40282
|
+
const dest = join13(repoDir, BORDER_STATE_DIR, "tmp", `gem-pack-${String(process.pid)}-${String(packSeq)}`);
|
|
39760
40283
|
mkdirSync5(dest, { recursive: true });
|
|
39761
40284
|
try {
|
|
39762
40285
|
const gemName = `${ident.name}-${ident.version}.gem`;
|
|
39763
|
-
const r =
|
|
40286
|
+
const r = spawnSync7("gem", ["build", ident.path, "-o", join13(dest, gemName)], {
|
|
39764
40287
|
cwd: repoDir,
|
|
39765
40288
|
encoding: "utf8",
|
|
39766
40289
|
env: { ...o.env ?? process.env },
|
|
@@ -39769,7 +40292,7 @@ function packRubygemsArtifacts(repoDir, o = {}) {
|
|
|
39769
40292
|
if (r.error !== void 0 || r.status !== 0) return null;
|
|
39770
40293
|
const files = readdirSync4(dest).filter((f) => f.endsWith(".gem")).sort();
|
|
39771
40294
|
if (files.length === 0) return null;
|
|
39772
|
-
return files.map((f) => ({ file: f, sha256: sha256File(
|
|
40295
|
+
return files.map((f) => ({ file: f, sha256: sha256File(join13(dest, f)) }));
|
|
39773
40296
|
} finally {
|
|
39774
40297
|
rmSync5(dest, { recursive: true, force: true });
|
|
39775
40298
|
}
|
|
@@ -39812,231 +40335,56 @@ function verifyArtifactFreshness(record2, ctx, repoDir, o = {}) {
|
|
|
39812
40335
|
);
|
|
39813
40336
|
}
|
|
39814
40337
|
|
|
39815
|
-
// src/ledger/
|
|
39816
|
-
import {
|
|
39817
|
-
import { join as
|
|
39818
|
-
var
|
|
39819
|
-
|
|
39820
|
-
|
|
39821
|
-
var HEX40 = /^[0-9a-f]{40}$/;
|
|
39822
|
-
var HEX8 = /^[0-9a-f]{8}$/;
|
|
39823
|
-
var PUSH_CONFIRMED_VIA = ["ls-remote", "npm-view", "pypi-json", "crates-json", "rubygems-json"];
|
|
39824
|
-
function ledgerPath(repoDir) {
|
|
39825
|
-
return join12(repoDir, BORDER_STATE_DIR, LEDGER_FILE);
|
|
39826
|
-
}
|
|
39827
|
-
function runsDir(repoDir) {
|
|
39828
|
-
return join12(repoDir, BORDER_STATE_DIR, RUNS_SUBDIR);
|
|
39829
|
-
}
|
|
39830
|
-
function formatSkipLine(record2) {
|
|
39831
|
-
return `SKIP ${record2.key8} \u2014 PASS ${record2.ts} report ${record2.reportPath}`;
|
|
39832
|
-
}
|
|
39833
|
-
function buildPushRecord(i) {
|
|
39834
|
-
return {
|
|
39835
|
-
t: "push",
|
|
39836
|
-
key: i.key,
|
|
39837
|
-
target: i.target,
|
|
39838
|
-
remoteName: i.remoteName,
|
|
39839
|
-
url: sanitizeUrl(i.url),
|
|
39840
|
-
localSha: i.localSha,
|
|
39841
|
-
...i.remoteSha !== void 0 ? { remoteSha: i.remoteSha } : {},
|
|
39842
|
-
...i.version !== void 0 ? { version: i.version } : {},
|
|
39843
|
-
confirmedVia: i.confirmedVia,
|
|
39844
|
-
ts: i.ts ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
39845
|
-
};
|
|
40338
|
+
// src/ledger/retention.ts
|
|
40339
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readdirSync as readdirSync5, rmSync as rmSync6 } from "node:fs";
|
|
40340
|
+
import { join as join14 } from "node:path";
|
|
40341
|
+
var KEEP_RUN_DIRS_PER_KEY8 = 20;
|
|
40342
|
+
function codeOf3(err) {
|
|
40343
|
+
return typeof err === "object" && err !== null ? err.code : void 0;
|
|
39846
40344
|
}
|
|
39847
|
-
function
|
|
39848
|
-
|
|
40345
|
+
function runDirName(key8, ts) {
|
|
40346
|
+
return `${key8}-${ts.replace(/[:.]/g, "-")}`;
|
|
39849
40347
|
}
|
|
39850
|
-
function
|
|
39851
|
-
const
|
|
39852
|
-
|
|
39853
|
-
|
|
40348
|
+
function uniqueRunDirName(repoDir, key8, ts) {
|
|
40349
|
+
const base = runDirName(key8, ts);
|
|
40350
|
+
let name = base;
|
|
40351
|
+
for (let n = 2; existsSync9(join14(runsDir(repoDir), name)) && n < 1e3; n += 1) {
|
|
40352
|
+
name = `${base}-r${String(n)}`;
|
|
40353
|
+
}
|
|
40354
|
+
return name;
|
|
39854
40355
|
}
|
|
39855
|
-
function
|
|
39856
|
-
const
|
|
39857
|
-
|
|
39858
|
-
return
|
|
40356
|
+
function ensureRunDir(repoDir, name) {
|
|
40357
|
+
const dir = join14(runsDir(repoDir), name);
|
|
40358
|
+
mkdirSync6(dir, { recursive: true });
|
|
40359
|
+
return dir;
|
|
39859
40360
|
}
|
|
39860
|
-
function
|
|
39861
|
-
|
|
39862
|
-
if (!Array.isArray(v) || !v.every((x) => typeof x === "string")) bad(`${field} must be an array of strings`);
|
|
39863
|
-
return v;
|
|
40361
|
+
function reportRelPath(name) {
|
|
40362
|
+
return `${RUNS_SUBDIR}/${name}/report.json`;
|
|
39864
40363
|
}
|
|
39865
|
-
function
|
|
39866
|
-
|
|
39867
|
-
|
|
39868
|
-
|
|
39869
|
-
|
|
40364
|
+
function pruneRunDirs(repoDir, key8, keep = KEEP_RUN_DIRS_PER_KEY8) {
|
|
40365
|
+
let entries;
|
|
40366
|
+
try {
|
|
40367
|
+
entries = readdirSync5(runsDir(repoDir));
|
|
40368
|
+
} catch (err) {
|
|
40369
|
+
if (codeOf3(err) === "ENOENT") return [];
|
|
40370
|
+
throw err;
|
|
39870
40371
|
}
|
|
39871
|
-
|
|
40372
|
+
const prefix = `${key8}-`;
|
|
40373
|
+
const mine = entries.filter((e) => e.startsWith(prefix)).sort();
|
|
40374
|
+
const doomed = mine.slice(0, Math.max(0, mine.length - keep));
|
|
40375
|
+
for (const dir of doomed) {
|
|
40376
|
+
rmSync6(join14(runsDir(repoDir), dir), { recursive: true, force: true });
|
|
40377
|
+
}
|
|
40378
|
+
return doomed;
|
|
39872
40379
|
}
|
|
39873
|
-
|
|
39874
|
-
|
|
39875
|
-
|
|
39876
|
-
|
|
39877
|
-
|
|
39878
|
-
|
|
39879
|
-
|
|
39880
|
-
|
|
39881
|
-
});
|
|
39882
|
-
}
|
|
39883
|
-
function parseLedgerRecord(value) {
|
|
39884
|
-
if (typeof value !== "object" || value === null) bad("record must be an object");
|
|
39885
|
-
const o = value;
|
|
39886
|
-
const t = o.t;
|
|
39887
|
-
if (t === "check") {
|
|
39888
|
-
const key = needHex(o, "key", HEX64);
|
|
39889
|
-
const key8 = needHex(o, "key8", HEX8);
|
|
39890
|
-
if (key.slice(0, 8) !== key8) bad("key8 must be the first 8 hex chars of key");
|
|
39891
|
-
const verdict = needString(o, "verdict");
|
|
39892
|
-
if (verdict !== "PASS" && verdict !== "FAIL") bad(`verdict must be PASS or FAIL, got '${verdict}'`);
|
|
39893
|
-
if (o.degraded !== false) bad("degraded must be false \u2014 degraded runs are never ledgered");
|
|
39894
|
-
if (typeof o.llm !== "boolean") bad("llm must be a boolean");
|
|
39895
|
-
return {
|
|
39896
|
-
t: "check",
|
|
39897
|
-
key,
|
|
39898
|
-
key8,
|
|
39899
|
-
head: needHex(o, "head", HEX40),
|
|
39900
|
-
dirtyDigest: needHex(o, "dirtyDigest", HEX64),
|
|
39901
|
-
refSetHash: needHex(o, "refSetHash", HEX64),
|
|
39902
|
-
exposureSet: needStringArray(o, "exposureSet"),
|
|
39903
|
-
effectiveTargets: needStringArray(o, "effectiveTargets"),
|
|
39904
|
-
rulesHash: needHex(o, "rulesHash", HEX64),
|
|
39905
|
-
artifacts: parseArtifacts(o),
|
|
39906
|
-
llm: o.llm,
|
|
39907
|
-
verdict,
|
|
39908
|
-
counts: parseCounts(o.counts),
|
|
39909
|
-
reportPath: needString(o, "reportPath"),
|
|
39910
|
-
degraded: false,
|
|
39911
|
-
ts: needString(o, "ts")
|
|
39912
|
-
};
|
|
39913
|
-
}
|
|
39914
|
-
if (t === "push") {
|
|
39915
|
-
const confirmedVia = needString(o, "confirmedVia");
|
|
39916
|
-
if (!PUSH_CONFIRMED_VIA.includes(confirmedVia)) {
|
|
39917
|
-
bad(`confirmedVia must be one of ${PUSH_CONFIRMED_VIA.join("|")}`);
|
|
39918
|
-
}
|
|
39919
|
-
const remoteSha = o.remoteSha;
|
|
39920
|
-
const version2 = o.version;
|
|
39921
|
-
if (remoteSha === void 0 && version2 === void 0) bad("push record needs remoteSha or version");
|
|
39922
|
-
if (remoteSha !== void 0 && typeof remoteSha !== "string") bad("remoteSha must be a string");
|
|
39923
|
-
if (version2 !== void 0 && typeof version2 !== "string") bad("version must be a string");
|
|
39924
|
-
return {
|
|
39925
|
-
t: "push",
|
|
39926
|
-
key: needHex(o, "key", HEX64),
|
|
39927
|
-
target: needString(o, "target"),
|
|
39928
|
-
remoteName: needString(o, "remoteName"),
|
|
39929
|
-
url: needString(o, "url"),
|
|
39930
|
-
localSha: needString(o, "localSha"),
|
|
39931
|
-
...remoteSha !== void 0 ? { remoteSha } : {},
|
|
39932
|
-
...version2 !== void 0 ? { version: version2 } : {},
|
|
39933
|
-
confirmedVia,
|
|
39934
|
-
ts: needString(o, "ts")
|
|
39935
|
-
};
|
|
39936
|
-
}
|
|
39937
|
-
return bad(`unknown record type ${JSON.stringify(t)}`);
|
|
39938
|
-
}
|
|
39939
|
-
function codeOf2(err) {
|
|
39940
|
-
return typeof err === "object" && err !== null ? err.code : void 0;
|
|
39941
|
-
}
|
|
39942
|
-
function readLedger(repoDir) {
|
|
39943
|
-
let text;
|
|
39944
|
-
try {
|
|
39945
|
-
text = readFileSync8(ledgerPath(repoDir), "utf8");
|
|
39946
|
-
} catch (err) {
|
|
39947
|
-
if (codeOf2(err) === "ENOENT") return { records: [], warnings: [] };
|
|
39948
|
-
throw err;
|
|
39949
|
-
}
|
|
39950
|
-
const records = [];
|
|
39951
|
-
const warnings = [];
|
|
39952
|
-
text.split("\n").forEach((line, i) => {
|
|
39953
|
-
if (line.trim() === "") return;
|
|
39954
|
-
try {
|
|
39955
|
-
records.push(parseLedgerRecord(JSON.parse(line)));
|
|
39956
|
-
} catch (err) {
|
|
39957
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
39958
|
-
warnings.push(`ledger line ${String(i + 1)} unreadable (${reason.slice(0, 160)}) \u2014 line skipped`);
|
|
39959
|
-
}
|
|
39960
|
-
});
|
|
39961
|
-
return { records, warnings };
|
|
39962
|
-
}
|
|
39963
|
-
function appendRecord(repoDir, record2) {
|
|
39964
|
-
ensureStateDir(repoDir);
|
|
39965
|
-
appendFileSync(ledgerPath(repoDir), `${JSON.stringify(record2)}
|
|
39966
|
-
`, "utf8");
|
|
39967
|
-
}
|
|
39968
|
-
function newestRecordOf(records, key) {
|
|
39969
|
-
for (let i = records.length - 1; i >= 0; i -= 1) {
|
|
39970
|
-
const r = records[i];
|
|
39971
|
-
if (r.t === "check" && r.key === key) return r;
|
|
39972
|
-
}
|
|
39973
|
-
return null;
|
|
39974
|
-
}
|
|
39975
|
-
function isSkippable(r) {
|
|
39976
|
-
return r !== null && r.verdict === "PASS" && r.degraded === false;
|
|
39977
|
-
}
|
|
39978
|
-
function lookupSkipRecord(records, key, llm) {
|
|
39979
|
-
const newest = newestRecordOf(records, key);
|
|
39980
|
-
return isSkippable(newest) && newest.llm === llm ? newest : null;
|
|
39981
|
-
}
|
|
39982
|
-
function latestPassCoveringTargets(records, key, targets) {
|
|
39983
|
-
const newest = newestRecordOf(records, key);
|
|
39984
|
-
return isSkippable(newest) && targets.every((t) => newest.effectiveTargets.includes(t)) ? newest : null;
|
|
39985
|
-
}
|
|
39986
|
-
function pushRecords(records, key) {
|
|
39987
|
-
return records.filter((r) => r.t === "push" && (key === void 0 || r.key === key));
|
|
39988
|
-
}
|
|
39989
|
-
|
|
39990
|
-
// src/ledger/retention.ts
|
|
39991
|
-
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readdirSync as readdirSync5, rmSync as rmSync6 } from "node:fs";
|
|
39992
|
-
import { join as join13 } from "node:path";
|
|
39993
|
-
var KEEP_RUN_DIRS_PER_KEY8 = 20;
|
|
39994
|
-
function codeOf3(err) {
|
|
39995
|
-
return typeof err === "object" && err !== null ? err.code : void 0;
|
|
39996
|
-
}
|
|
39997
|
-
function runDirName(key8, ts) {
|
|
39998
|
-
return `${key8}-${ts.replace(/[:.]/g, "-")}`;
|
|
39999
|
-
}
|
|
40000
|
-
function uniqueRunDirName(repoDir, key8, ts) {
|
|
40001
|
-
const base = runDirName(key8, ts);
|
|
40002
|
-
let name = base;
|
|
40003
|
-
for (let n = 2; existsSync9(join13(runsDir(repoDir), name)) && n < 1e3; n += 1) {
|
|
40004
|
-
name = `${base}-r${String(n)}`;
|
|
40005
|
-
}
|
|
40006
|
-
return name;
|
|
40007
|
-
}
|
|
40008
|
-
function ensureRunDir(repoDir, name) {
|
|
40009
|
-
const dir = join13(runsDir(repoDir), name);
|
|
40010
|
-
mkdirSync6(dir, { recursive: true });
|
|
40011
|
-
return dir;
|
|
40012
|
-
}
|
|
40013
|
-
function reportRelPath(name) {
|
|
40014
|
-
return `${RUNS_SUBDIR}/${name}/report.json`;
|
|
40015
|
-
}
|
|
40016
|
-
function pruneRunDirs(repoDir, key8, keep = KEEP_RUN_DIRS_PER_KEY8) {
|
|
40017
|
-
let entries;
|
|
40018
|
-
try {
|
|
40019
|
-
entries = readdirSync5(runsDir(repoDir));
|
|
40020
|
-
} catch (err) {
|
|
40021
|
-
if (codeOf3(err) === "ENOENT") return [];
|
|
40022
|
-
throw err;
|
|
40023
|
-
}
|
|
40024
|
-
const prefix = `${key8}-`;
|
|
40025
|
-
const mine = entries.filter((e) => e.startsWith(prefix)).sort();
|
|
40026
|
-
const doomed = mine.slice(0, Math.max(0, mine.length - keep));
|
|
40027
|
-
for (const dir of doomed) {
|
|
40028
|
-
rmSync6(join13(runsDir(repoDir), dir), { recursive: true, force: true });
|
|
40029
|
-
}
|
|
40030
|
-
return doomed;
|
|
40031
|
-
}
|
|
40032
|
-
|
|
40033
|
-
// src/ledger.ts
|
|
40034
|
-
async function computeFingerprint(repoDir, cfg, configDigest, effectiveTargets, o = {}) {
|
|
40035
|
-
const envOpt = o.env !== void 0 ? { env: o.env } : {};
|
|
40036
|
-
const ctx = await gatherContext(repoDir, envOpt);
|
|
40037
|
-
const probe = await probeEngines(cfg, {
|
|
40038
|
-
...envOpt,
|
|
40039
|
-
...o.requireOverride !== void 0 ? { requireOverride: o.requireOverride } : {}
|
|
40380
|
+
|
|
40381
|
+
// src/ledger.ts
|
|
40382
|
+
async function computeFingerprint(repoDir, cfg, configDigest, effectiveTargets, o = {}) {
|
|
40383
|
+
const envOpt = o.env !== void 0 ? { env: o.env } : {};
|
|
40384
|
+
const ctx = await gatherContext(repoDir, envOpt);
|
|
40385
|
+
const probe = await probeEngines(cfg, {
|
|
40386
|
+
...envOpt,
|
|
40387
|
+
...o.requireOverride !== void 0 ? { requireOverride: o.requireOverride } : {}
|
|
40040
40388
|
});
|
|
40041
40389
|
const rulesHash = await computeCheckRulesHash({
|
|
40042
40390
|
engineVersions: probe.engineVersions,
|
|
@@ -40091,9 +40439,9 @@ function recordCheckRun(i) {
|
|
|
40091
40439
|
const envOpt = i.env !== void 0 ? { env: i.env } : {};
|
|
40092
40440
|
const artifacts = i.artifacts ?? (i.report.verdict === "PASS" && !i.ctx.dirty && i.effectiveTargets.includes("npm") ? packNpmArtifacts(i.repoDir, envOpt) : null);
|
|
40093
40441
|
const persisted = artifacts !== null ? { ...i.report, artifacts } : i.report;
|
|
40094
|
-
writeFileSync3(
|
|
40442
|
+
writeFileSync3(join15(i.repoDir, BORDER_STATE_DIR, relReport), `${JSON.stringify(persisted, null, 2)}
|
|
40095
40443
|
`, "utf8");
|
|
40096
|
-
writeFileSync3(
|
|
40444
|
+
writeFileSync3(join15(i.repoDir, BORDER_STATE_DIR, relReport.replace("report.json", "report.md")), renderReportMd(i.report), "utf8");
|
|
40097
40445
|
const record2 = {
|
|
40098
40446
|
t: "check",
|
|
40099
40447
|
key: i.report.key,
|
|
@@ -40118,13 +40466,13 @@ function recordCheckRun(i) {
|
|
|
40118
40466
|
}
|
|
40119
40467
|
|
|
40120
40468
|
// src/channels/pypi.ts
|
|
40121
|
-
import { spawnSync as
|
|
40469
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
40122
40470
|
import { relative as relative5 } from "node:path";
|
|
40123
40471
|
|
|
40124
40472
|
// src/artifacts/pypi.ts
|
|
40125
40473
|
import { createHash as createHash9 } from "node:crypto";
|
|
40126
40474
|
import { existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync2, readFileSync as readFileSync9, readdirSync as readdirSync6, rmdirSync as rmdirSync2, rmSync as rmSync7 } from "node:fs";
|
|
40127
|
-
import { basename as
|
|
40475
|
+
import { basename as basename3, dirname as dirname7, join as join16, relative as relative4, resolve as resolve7 } from "node:path";
|
|
40128
40476
|
|
|
40129
40477
|
// src/artifacts/residuePy.ts
|
|
40130
40478
|
function residuePypiHits(pkgDir, innerRels) {
|
|
@@ -40188,13 +40536,13 @@ function requireModule(o, module) {
|
|
|
40188
40536
|
function buildPypiArtifacts(o) {
|
|
40189
40537
|
requireModule(o, "build");
|
|
40190
40538
|
const repoDir = resolve7(o.repoDir);
|
|
40191
|
-
if (!existsSync10(
|
|
40539
|
+
if (!existsSync10(join16(repoDir, "pyproject.toml"))) {
|
|
40192
40540
|
throw new PypiPrerequisiteError(`no pyproject.toml in the repo root \u2014 the PyPI pipeline builds from pyproject's own [build-system]`);
|
|
40193
40541
|
}
|
|
40194
|
-
const distDir =
|
|
40542
|
+
const distDir = join16(resolve7(o.stateDir ?? join16(repoDir, ".border")), "dist");
|
|
40195
40543
|
mkdirSync7(distDir, { recursive: true });
|
|
40196
40544
|
for (const name of readdirSync6(distDir)) {
|
|
40197
|
-
if (name.endsWith(".tar.gz") || name.endsWith(".whl")) rmSync7(
|
|
40545
|
+
if (name.endsWith(".tar.gz") || name.endsWith(".whl")) rmSync7(join16(distDir, name), { force: true });
|
|
40198
40546
|
}
|
|
40199
40547
|
const out = spawnEngine(binaryCandidates("python3", o), ["-m", "build", "--no-isolation", "--outdir", distDir, repoDir], o);
|
|
40200
40548
|
if (out.status !== 0) {
|
|
@@ -40205,13 +40553,13 @@ function buildPypiArtifacts(o) {
|
|
|
40205
40553
|
throw new EngineRunError(`\`python3 -m build\` produced no .tar.gz/.whl in ${distDir} (border exit 2)`, null);
|
|
40206
40554
|
}
|
|
40207
40555
|
const artifacts = names.map((name) => {
|
|
40208
|
-
const path4 =
|
|
40556
|
+
const path4 = join16(distDir, name);
|
|
40209
40557
|
return { path: path4, kind: name.endsWith(".whl") ? "wheel" : "sdist", sha256: createHash9("sha256").update(readFileSync9(path4)).digest("hex") };
|
|
40210
40558
|
});
|
|
40211
40559
|
return { artifacts, distDir };
|
|
40212
40560
|
}
|
|
40213
40561
|
function walkFilesRel(root) {
|
|
40214
|
-
return readdirSync6(root, { withFileTypes: true }).flatMap((e) => e.isDirectory() ? walkFilesRel(
|
|
40562
|
+
return readdirSync6(root, { withFileTypes: true }).flatMap((e) => e.isDirectory() ? walkFilesRel(join16(root, e.name)).map((r) => `${e.name}/${r}`) : e.isFile() ? [e.name] : []);
|
|
40215
40563
|
}
|
|
40216
40564
|
function stripWrapper(entries) {
|
|
40217
40565
|
const tops = new Set(entries.map((e) => e.includes("/") ? e.slice(0, e.indexOf("/")) : "\0"));
|
|
@@ -40224,7 +40572,7 @@ function toInner(findingPath, scanRoot) {
|
|
|
40224
40572
|
const head = bang === -1 ? findingPath : findingPath.slice(0, bang);
|
|
40225
40573
|
const rest = bang === -1 ? "" : findingPath.slice(bang + 1);
|
|
40226
40574
|
const rel = relative4(scanRoot, head);
|
|
40227
|
-
const inner = rel !== "" && !rel.startsWith("..") ? rel :
|
|
40575
|
+
const inner = rel !== "" && !rel.startsWith("..") ? rel : basename3(head);
|
|
40228
40576
|
return rest === "" ? inner : `${inner}!${rest}`;
|
|
40229
40577
|
}
|
|
40230
40578
|
function gitTracked(o, repoDir) {
|
|
@@ -40241,7 +40589,7 @@ function toRe(globs) {
|
|
|
40241
40589
|
function explainMatchers(repoDir) {
|
|
40242
40590
|
const inc = [];
|
|
40243
40591
|
const exc = [];
|
|
40244
|
-
const manifest =
|
|
40592
|
+
const manifest = join16(repoDir, "MANIFEST.in");
|
|
40245
40593
|
if (existsSync10(manifest)) {
|
|
40246
40594
|
for (const line of readFileSync9(manifest, "utf8").split(/\r?\n/)) {
|
|
40247
40595
|
const t = line.split("#")[0]?.trim() ?? "";
|
|
@@ -40256,7 +40604,7 @@ function explainMatchers(repoDir) {
|
|
|
40256
40604
|
else if (cmd === "prune") exc.push(`${dir}/**`);
|
|
40257
40605
|
}
|
|
40258
40606
|
}
|
|
40259
|
-
const pyproject =
|
|
40607
|
+
const pyproject = join16(repoDir, "pyproject.toml");
|
|
40260
40608
|
if (existsSync10(pyproject)) {
|
|
40261
40609
|
const text = readFileSync9(pyproject, "utf8");
|
|
40262
40610
|
const readme = /^\s*readme\s*=\s*"([^"]+)"/m.exec(text);
|
|
@@ -40308,7 +40656,7 @@ ${out.stderr}`.replace(ANSI_RE, "");
|
|
|
40308
40656
|
}
|
|
40309
40657
|
return failed.map((m) => {
|
|
40310
40658
|
const abs = m[1] ?? "";
|
|
40311
|
-
const base =
|
|
40659
|
+
const base = basename3(abs);
|
|
40312
40660
|
const idx = text.indexOf(m[0]);
|
|
40313
40661
|
const next = text.indexOf("Checking ", idx + m[0].length);
|
|
40314
40662
|
const block = tail2(text.slice(idx, next === -1 ? void 0 : next), 400).replaceAll(abs, base);
|
|
@@ -40329,10 +40677,10 @@ async function scanPyPiArtifacts(o) {
|
|
|
40329
40677
|
requireModule(o, "build");
|
|
40330
40678
|
requireModule(o, "twine");
|
|
40331
40679
|
const repoDir = resolve7(o.repoDir);
|
|
40332
|
-
const stateDir = resolve7(o.stateDir ??
|
|
40680
|
+
const stateDir = resolve7(o.stateDir ?? join16(repoDir, ".border"));
|
|
40333
40681
|
const target = o.target ?? "artifact";
|
|
40334
40682
|
const { artifacts } = buildPypiArtifacts(o);
|
|
40335
|
-
const tmpParent =
|
|
40683
|
+
const tmpParent = join16(stateDir, "tmp");
|
|
40336
40684
|
mkdirSync7(tmpParent, { recursive: true });
|
|
40337
40685
|
const merged = /* @__PURE__ */ new Map();
|
|
40338
40686
|
const absorb = (f, path4) => {
|
|
@@ -40341,16 +40689,16 @@ async function scanPyPiArtifacts(o) {
|
|
|
40341
40689
|
};
|
|
40342
40690
|
try {
|
|
40343
40691
|
for (const artifact of artifacts) {
|
|
40344
|
-
const sandbox = mkdtempSync2(
|
|
40692
|
+
const sandbox = mkdtempSync2(join16(tmpParent, "pypi-"));
|
|
40345
40693
|
try {
|
|
40346
40694
|
if (artifact.kind === "wheel") {
|
|
40347
40695
|
const z2 = spawnEngine(binaryCandidates("python3", o), ["-m", "zipfile", "-e", artifact.path, sandbox], o);
|
|
40348
|
-
if (z2.status !== 0) throw new EngineRunError(`\`python3 -m zipfile -e ${
|
|
40696
|
+
if (z2.status !== 0) throw new EngineRunError(`\`python3 -m zipfile -e ${basename3(artifact.path)}\` exited ${String(z2.status)} (border exit 2): ${tail2(z2.stderr)}`, z2.status);
|
|
40349
40697
|
} else {
|
|
40350
40698
|
extractArchive(artifact.path, sandbox, o);
|
|
40351
40699
|
}
|
|
40352
40700
|
const { root, rest } = artifact.kind === "sdist" ? stripWrapper(walkFilesRel(sandbox)) : { root: "", rest: walkFilesRel(sandbox) };
|
|
40353
|
-
const scanRoot =
|
|
40701
|
+
const scanRoot = join16(sandbox, root);
|
|
40354
40702
|
const scanOpts = { ...o, target };
|
|
40355
40703
|
if (o.skipGitleaks !== true) {
|
|
40356
40704
|
for (const f of scanTree({ ...scanOpts, dir: scanRoot, stateDir })) absorb(f, toInner(f.path, scanRoot));
|
|
@@ -40359,9 +40707,23 @@ async function scanPyPiArtifacts(o) {
|
|
|
40359
40707
|
for (const f of await scanPaths({ ...scanOpts, ...o.sanitizer !== void 0 ? { sanitizer: o.sanitizer } : {}, ...o.rules !== void 0 ? { rules: o.rules } : {}, dir: scanRoot, files: rest })) absorb(f, f.path ?? "");
|
|
40360
40708
|
}
|
|
40361
40709
|
for (const f of residueFindings(residuePypiHits(scanRoot, rest), {
|
|
40362
|
-
root:
|
|
40710
|
+
root: basename3(artifact.path),
|
|
40363
40711
|
sep: "!",
|
|
40364
|
-
identity:
|
|
40712
|
+
identity: basename3(artifact.path),
|
|
40713
|
+
...o.sanitizer !== void 0 ? { sanitizer: o.sanitizer } : {}
|
|
40714
|
+
})) absorb(f, f.path ?? "");
|
|
40715
|
+
for (const f of pypiArtifactCoherenceFindings({
|
|
40716
|
+
kind: artifact.kind,
|
|
40717
|
+
archiveName: basename3(artifact.path),
|
|
40718
|
+
files: rest,
|
|
40719
|
+
read: (rel) => {
|
|
40720
|
+
try {
|
|
40721
|
+
return readFileSync9(join16(scanRoot, rel), "utf8");
|
|
40722
|
+
} catch {
|
|
40723
|
+
return null;
|
|
40724
|
+
}
|
|
40725
|
+
},
|
|
40726
|
+
identity: basename3(artifact.path),
|
|
40365
40727
|
...o.sanitizer !== void 0 ? { sanitizer: o.sanitizer } : {}
|
|
40366
40728
|
})) absorb(f, f.path ?? "");
|
|
40367
40729
|
if (artifact.kind === "sdist") for (const f of manifestFindings(o, rest, target)) absorb(f, f.path ?? "");
|
|
@@ -40383,7 +40745,7 @@ async function scanPyPiArtifacts(o) {
|
|
|
40383
40745
|
import { spawn } from "node:child_process";
|
|
40384
40746
|
import { createHash as createHash10 } from "node:crypto";
|
|
40385
40747
|
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "node:fs";
|
|
40386
|
-
import { basename as
|
|
40748
|
+
import { basename as basename4, join as join17 } from "node:path";
|
|
40387
40749
|
|
|
40388
40750
|
// src/cli/exit.ts
|
|
40389
40751
|
var EXIT_PASS = 0;
|
|
@@ -40424,7 +40786,7 @@ function translateError(err) {
|
|
|
40424
40786
|
|
|
40425
40787
|
// src/push/core.ts
|
|
40426
40788
|
var PUBLISH_WARNING = "WARNING: published versions cannot be re-uploaded unchanged; deletion windows differ per registry \u2014 verify version bump policy";
|
|
40427
|
-
var DIST_DIR =
|
|
40789
|
+
var DIST_DIR = join17(BORDER_STATE_DIR, "dist");
|
|
40428
40790
|
var defaultOut = (line) => void process.stdout.write(`${line}
|
|
40429
40791
|
`);
|
|
40430
40792
|
var defaultErr = (line) => void process.stderr.write(`${line}
|
|
@@ -40453,8 +40815,8 @@ function rehashRecordedArtifacts(kind, extensions, record2, repoDir) {
|
|
|
40453
40815
|
}
|
|
40454
40816
|
const files = [];
|
|
40455
40817
|
for (const a of entries) {
|
|
40456
|
-
const rel =
|
|
40457
|
-
const abs =
|
|
40818
|
+
const rel = join17(DIST_DIR, basename4(a.file));
|
|
40819
|
+
const abs = join17(repoDir, rel);
|
|
40458
40820
|
if (!existsSync11(abs)) return fail3(`${rel} is missing from the working tree`);
|
|
40459
40821
|
const digest2 = createHash10("sha256").update(readFileSync10(abs)).digest("hex");
|
|
40460
40822
|
if (digest2 !== a.sha256) return fail3(`${rel} no longer hashes to the digest recorded by the check`);
|
|
@@ -40630,7 +40992,7 @@ function twineAvailable(i, command) {
|
|
|
40630
40992
|
let status = null;
|
|
40631
40993
|
let failed = false;
|
|
40632
40994
|
try {
|
|
40633
|
-
const res =
|
|
40995
|
+
const res = spawnSync8(command.bin, args, {
|
|
40634
40996
|
encoding: "utf8",
|
|
40635
40997
|
stdio: "ignore",
|
|
40636
40998
|
...i.env === void 0 ? {} : { env: { ...i.env } }
|
|
@@ -40945,7 +41307,7 @@ function npmNameClaimed(o) {
|
|
|
40945
41307
|
return true;
|
|
40946
41308
|
}
|
|
40947
41309
|
function npmViewJson(args, registry2, timeoutMs, env2) {
|
|
40948
|
-
return new Promise((
|
|
41310
|
+
return new Promise((resolve16, reject) => {
|
|
40949
41311
|
const child = spawn2("npm", ["view", ...args, "--registry", registry2, "--fetch-retries=0", "--json"], {
|
|
40950
41312
|
stdio: ["ignore", "pipe", "pipe"],
|
|
40951
41313
|
...env2 === void 0 ? {} : { env: env2 }
|
|
@@ -40964,7 +41326,7 @@ function npmViewJson(args, registry2, timeoutMs, env2) {
|
|
|
40964
41326
|
});
|
|
40965
41327
|
child.on("close", (status) => {
|
|
40966
41328
|
clearTimeout(timer);
|
|
40967
|
-
|
|
41329
|
+
resolve16({ status: status ?? -1, stdout, stderr });
|
|
40968
41330
|
});
|
|
40969
41331
|
});
|
|
40970
41332
|
}
|
|
@@ -41060,14 +41422,14 @@ var npmChannel = {
|
|
|
41060
41422
|
// src/channels/crates.ts
|
|
41061
41423
|
import { createHash as createHash12 } from "node:crypto";
|
|
41062
41424
|
import { existsSync as existsSync13, mkdirSync as mkdirSync9, readFileSync as readFileSync12, readdirSync as readdirSync8, rmSync as rmSync9 } from "node:fs";
|
|
41063
|
-
import { join as
|
|
41064
|
-
import { spawnSync as
|
|
41425
|
+
import { join as join19, resolve as resolve8 } from "node:path";
|
|
41426
|
+
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
41065
41427
|
|
|
41066
41428
|
// src/artifacts/crates.ts
|
|
41067
41429
|
import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync11, readdirSync as readdirSync7, renameSync, rmSync as rmSync8 } from "node:fs";
|
|
41068
41430
|
import { createHash as createHash11 } from "node:crypto";
|
|
41069
|
-
import { join as
|
|
41070
|
-
import { spawnSync as
|
|
41431
|
+
import { join as join18, relative as relative6 } from "node:path";
|
|
41432
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
41071
41433
|
|
|
41072
41434
|
// src/artifacts/residueRust.ts
|
|
41073
41435
|
function residueCratesHits(pkgDir, innerRels) {
|
|
@@ -41266,7 +41628,7 @@ function listFiles3(dir) {
|
|
|
41266
41628
|
while (stack.length > 0) {
|
|
41267
41629
|
const cur = stack.pop();
|
|
41268
41630
|
for (const ent of readdirSync7(cur, { withFileTypes: true })) {
|
|
41269
|
-
const p =
|
|
41631
|
+
const p = join18(cur, ent.name);
|
|
41270
41632
|
if (ent.isDirectory()) stack.push(p);
|
|
41271
41633
|
else if (ent.isFile()) out.push(p);
|
|
41272
41634
|
}
|
|
@@ -41274,7 +41636,7 @@ function listFiles3(dir) {
|
|
|
41274
41636
|
return out;
|
|
41275
41637
|
}
|
|
41276
41638
|
function manifestIdentity(repoDir, cfg) {
|
|
41277
|
-
const manifestPath =
|
|
41639
|
+
const manifestPath = join18(repoDir, "Cargo.toml");
|
|
41278
41640
|
if (!existsSync12(manifestPath)) {
|
|
41279
41641
|
throw new EngineRunError("targets.crates is configured but the repo has no Cargo.toml to package", null);
|
|
41280
41642
|
}
|
|
@@ -41302,7 +41664,7 @@ async function runCargoArtifactStage(o) {
|
|
|
41302
41664
|
const bin = o.cargoBinPath ?? "cargo";
|
|
41303
41665
|
const identity = manifestIdentity(repoDir, o.cfg);
|
|
41304
41666
|
const crateName = `${identity.name}-${identity.version}.crate`;
|
|
41305
|
-
const versionCheck =
|
|
41667
|
+
const versionCheck = spawnSync9(bin, ["--version"], {
|
|
41306
41668
|
encoding: "utf8",
|
|
41307
41669
|
stdio: "ignore",
|
|
41308
41670
|
...o.env === void 0 ? {} : { env: { ...o.env } }
|
|
@@ -41313,14 +41675,14 @@ async function runCargoArtifactStage(o) {
|
|
|
41313
41675
|
);
|
|
41314
41676
|
}
|
|
41315
41677
|
ensureStateDir(repoDir);
|
|
41316
|
-
const stateDir =
|
|
41317
|
-
const distDir =
|
|
41318
|
-
const targetDir =
|
|
41678
|
+
const stateDir = join18(repoDir, BORDER_STATE_DIR);
|
|
41679
|
+
const distDir = join18(stateDir, "dist");
|
|
41680
|
+
const targetDir = join18(stateDir, "tmp", "cargo-target");
|
|
41319
41681
|
mkdirSync8(distDir, { recursive: true });
|
|
41320
41682
|
rmSync8(targetDir, { recursive: true, force: true });
|
|
41321
41683
|
mkdirSync8(targetDir, { recursive: true });
|
|
41322
41684
|
try {
|
|
41323
|
-
const result =
|
|
41685
|
+
const result = spawnSync9(bin, ["package", "--allow-dirty", "--no-verify"], {
|
|
41324
41686
|
cwd: repoDir,
|
|
41325
41687
|
encoding: "utf8",
|
|
41326
41688
|
timeout: o.cargoTimeoutMs ?? 12e4,
|
|
@@ -41335,17 +41697,17 @@ async function runCargoArtifactStage(o) {
|
|
|
41335
41697
|
result.status
|
|
41336
41698
|
);
|
|
41337
41699
|
}
|
|
41338
|
-
const packedDir =
|
|
41339
|
-
const packed =
|
|
41700
|
+
const packedDir = join18(targetDir, "package");
|
|
41701
|
+
const packed = join18(packedDir, crateName);
|
|
41340
41702
|
if (!existsSync12(packed)) {
|
|
41341
41703
|
throw new EngineRunError(`cargo package produced no ${crateName} in ${relative6(repoDir, packedDir)} \u2014 failing closed`, null);
|
|
41342
41704
|
}
|
|
41343
|
-
const dist =
|
|
41705
|
+
const dist = join18(distDir, crateName);
|
|
41344
41706
|
renameSync(packed, dist);
|
|
41345
41707
|
const bytes = readFileSync11(dist);
|
|
41346
41708
|
const sha2563 = createHash11("sha256").update(bytes).digest("hex");
|
|
41347
41709
|
const findings = [];
|
|
41348
|
-
const extractDir =
|
|
41710
|
+
const extractDir = join18(stateDir, "tmp", sha2563.slice(0, 8));
|
|
41349
41711
|
rmSync8(extractDir, { recursive: true, force: true });
|
|
41350
41712
|
mkdirSync8(extractDir, { recursive: true });
|
|
41351
41713
|
try {
|
|
@@ -41384,6 +41746,15 @@ async function runCargoArtifactStage(o) {
|
|
|
41384
41746
|
identity: crateName,
|
|
41385
41747
|
...o.sanitizer !== void 0 ? { sanitizer: o.sanitizer } : {}
|
|
41386
41748
|
}));
|
|
41749
|
+
const lockRel = `${identity.name}-${identity.version}/Cargo.lock`;
|
|
41750
|
+
findings.push(...cratesCoherenceFindings({
|
|
41751
|
+
name: identity.name,
|
|
41752
|
+
version: identity.version,
|
|
41753
|
+
lockText: relPaths.includes(lockRel) ? readFileSync11(join18(extractDir, lockRel), "utf8") : null,
|
|
41754
|
+
lockRelPath: lockRel,
|
|
41755
|
+
identity: crateName,
|
|
41756
|
+
...o.sanitizer !== void 0 ? { sanitizer: o.sanitizer } : {}
|
|
41757
|
+
}));
|
|
41387
41758
|
} finally {
|
|
41388
41759
|
removeSandbox(extractDir);
|
|
41389
41760
|
}
|
|
@@ -41539,11 +41910,11 @@ function cratesPublishArgv(files, _cfg) {
|
|
|
41539
41910
|
return files.map(() => ["publish", "--allow-dirty", "--no-verify"]);
|
|
41540
41911
|
}
|
|
41541
41912
|
function repackageSameBytes(i, bin) {
|
|
41542
|
-
const targetDir =
|
|
41913
|
+
const targetDir = join19(resolve8(i.repoDir), BORDER_STATE_DIR, "tmp", "publish-repackage");
|
|
41543
41914
|
rmSync9(targetDir, { recursive: true, force: true });
|
|
41544
41915
|
mkdirSync9(targetDir, { recursive: true });
|
|
41545
41916
|
try {
|
|
41546
|
-
const r =
|
|
41917
|
+
const r = spawnSync10(bin, ["package", "--allow-dirty", "--no-verify"], {
|
|
41547
41918
|
cwd: i.repoDir,
|
|
41548
41919
|
encoding: "utf8",
|
|
41549
41920
|
timeout: 12e4,
|
|
@@ -41555,14 +41926,14 @@ function repackageSameBytes(i, bin) {
|
|
|
41555
41926
|
if (r.status !== 0) {
|
|
41556
41927
|
return { ok: false, message: `cargo package --allow-dirty --no-verify (pre-publish repackage) exited ${String(r.status)} \u2014 no retry loop, push blocked (fail-closed): ${(r.stderr ?? "").trim().slice(0, 300)}`, exit: EXIT_ERROR };
|
|
41557
41928
|
}
|
|
41558
|
-
const packedDir =
|
|
41929
|
+
const packedDir = join19(targetDir, "package");
|
|
41559
41930
|
const crates = readdirSync8(packedDir).filter((f) => f.endsWith(".crate")).sort();
|
|
41560
41931
|
if (crates.length !== 1) {
|
|
41561
41932
|
return { ok: false, message: `pre-publish repackage produced ${String(crates.length)} .crate file(s) \u2014 expected exactly 1, push blocked (fail-closed)`, exit: EXIT_ERROR };
|
|
41562
41933
|
}
|
|
41563
41934
|
const crateFile = crates[0] ?? "";
|
|
41564
|
-
const freshSha = createHash12("sha256").update(readFileSync12(
|
|
41565
|
-
const staged =
|
|
41935
|
+
const freshSha = createHash12("sha256").update(readFileSync12(join19(packedDir, crateFile))).digest("hex");
|
|
41936
|
+
const staged = join19(resolve8(i.repoDir), BORDER_STATE_DIR, "dist", crateFile);
|
|
41566
41937
|
if (!existsSync13(staged)) {
|
|
41567
41938
|
return { ok: false, message: `${crateFile} is missing from .border/dist \u2014 the PASSED check recorded no such crate; re-run border check (push blocked)`, exit: EXIT_ERROR };
|
|
41568
41939
|
}
|
|
@@ -41955,8 +42326,32 @@ function buildBorderConfigSchema() {
|
|
|
41955
42326
|
// of the residue scan is this toggle; a scan that cannot RUN is a FAIL
|
|
41956
42327
|
// (fail-closed doctrine), and the digest seam keeps every cached PASS
|
|
41957
42328
|
// honest about the residue sources it was certified against.
|
|
42329
|
+
// W2.2 (border-inspect-roadmap): requireProof arms the PROOF VALVE — a
|
|
42330
|
+
// blocking residue-* row on a staged artifact then additionally demands a
|
|
42331
|
+
// fresh `t:"roundtrip"` ledger record for that artifact's sha256 before
|
|
42332
|
+
// the channel may PASS. Default FALSE (0.3.x behavior byte-identical);
|
|
42333
|
+
// the valve reads `residue?.requireProof === true`, so absence IS the
|
|
42334
|
+
// false default — same construction contract as the outer key. The flag
|
|
42335
|
+
// rides the whole-object configDigest (stableStringify), so flipping it
|
|
42336
|
+
// rotates the check rulesHash and the ledger refuses every cached PASS.
|
|
41958
42337
|
residue: external_exports.object({
|
|
41959
|
-
enabled: external_exports.boolean().default(true)
|
|
42338
|
+
enabled: external_exports.boolean().default(true),
|
|
42339
|
+
requireProof: external_exports.boolean().default(false)
|
|
42340
|
+
}).strict().optional(),
|
|
42341
|
+
// W4.1 release-coherence twin obligations (plan border-inspect-roadmap §127-138):
|
|
42342
|
+
// the ONLY cross-manager declaration border can statically enforce — when a project
|
|
42343
|
+
// ships a PyPI/npm twin pair under one release train, their versions must be equal.
|
|
42344
|
+
// The per-artifact internal coherence rules (release-coherence-*) are ALWAYS on and
|
|
42345
|
+
// need no opt-in; this list is opt-in and strict — unknown keys fail typed exit 2.
|
|
42346
|
+
// The pair list rides the whole-object configDigest, so editing it rotates the check
|
|
42347
|
+
// rulesHash exactly like the residue block above.
|
|
42348
|
+
release: external_exports.object({
|
|
42349
|
+
twin: external_exports.array(
|
|
42350
|
+
external_exports.object({
|
|
42351
|
+
pypi: external_exports.string().min(1),
|
|
42352
|
+
npm: external_exports.string().min(1)
|
|
42353
|
+
}).strict()
|
|
42354
|
+
).default([])
|
|
41960
42355
|
}).strict().optional()
|
|
41961
42356
|
}).strict();
|
|
41962
42357
|
}
|
|
@@ -42044,7 +42439,7 @@ function deepMergeRaw(base, overlay) {
|
|
|
42044
42439
|
return overlay;
|
|
42045
42440
|
}
|
|
42046
42441
|
function runGit3(cwd, args) {
|
|
42047
|
-
const r =
|
|
42442
|
+
const r = spawnSync11("git", args, { cwd, encoding: "utf8" });
|
|
42048
42443
|
return { ok: r.status === 0, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
42049
42444
|
}
|
|
42050
42445
|
function gitToplevel(cwd) {
|
|
@@ -42104,7 +42499,7 @@ function loadConfig(options = {}) {
|
|
|
42104
42499
|
throw new ConfigError("unreadable", `config file not found: ${basePath}`);
|
|
42105
42500
|
}
|
|
42106
42501
|
} else {
|
|
42107
|
-
for (const candidate of [
|
|
42502
|
+
for (const candidate of [join20(cwd, "border.yaml"), toplevel === void 0 ? "" : join20(toplevel, "border.yaml")]) {
|
|
42108
42503
|
if (candidate !== "" && existsSync14(candidate)) {
|
|
42109
42504
|
basePath = candidate;
|
|
42110
42505
|
baseText = readFileSync13(candidate, "utf8");
|
|
@@ -42119,7 +42514,7 @@ function loadConfig(options = {}) {
|
|
|
42119
42514
|
])];
|
|
42120
42515
|
let overlay;
|
|
42121
42516
|
for (const dir of overlayDirs) {
|
|
42122
|
-
const p =
|
|
42517
|
+
const p = join20(dir, ".border", "config.local.yaml");
|
|
42123
42518
|
const text = readFileOrUndefined(p);
|
|
42124
42519
|
if (text !== void 0) {
|
|
42125
42520
|
overlay = { path: p, doc: yamlToJson(text, p) };
|
|
@@ -42164,10 +42559,10 @@ function exposureSet(cfg, options = {}) {
|
|
|
42164
42559
|
}
|
|
42165
42560
|
|
|
42166
42561
|
// src/check.ts
|
|
42167
|
-
import { join as
|
|
42562
|
+
import { join as join22 } from "node:path";
|
|
42168
42563
|
|
|
42169
42564
|
// src/rules/aiArtifacts.ts
|
|
42170
|
-
import { spawnSync as
|
|
42565
|
+
import { spawnSync as spawnSync12 } from "node:child_process";
|
|
42171
42566
|
import { dirname as dirname9, resolve as resolve10 } from "node:path";
|
|
42172
42567
|
var AI_ARTIFACTS_ENGINE = "ai-artifacts";
|
|
42173
42568
|
var BINARY_SNIFF_BYTES = 8e3;
|
|
@@ -42186,7 +42581,7 @@ function makeCtx(o) {
|
|
|
42186
42581
|
return { repoDir, env: env2, timeoutMs: o.timeoutMs ?? 3e5, maxBufferBytes: o.maxBufferBytes ?? 32 * 1024 * 1024 };
|
|
42187
42582
|
}
|
|
42188
42583
|
function gitSpawn2(c, args, input2) {
|
|
42189
|
-
const r =
|
|
42584
|
+
const r = spawnSync12("git", ["-C", c.repoDir, ...args], {
|
|
42190
42585
|
encoding: "buffer",
|
|
42191
42586
|
timeout: c.timeoutMs,
|
|
42192
42587
|
maxBuffer: c.maxBufferBytes,
|
|
@@ -42407,11 +42802,49 @@ function applyAllowList(findings, allow, repoDir) {
|
|
|
42407
42802
|
return { kept, allowHits: [...hits.values()] };
|
|
42408
42803
|
}
|
|
42409
42804
|
|
|
42805
|
+
// src/check/proofValve.ts
|
|
42806
|
+
var ROUNDTRIP_PROOF_MISSING_RULE = "roundtrip-proof-missing";
|
|
42807
|
+
var ROUNDTRIP_PROOF_STALE_RULE = "roundtrip-proof-stale";
|
|
42808
|
+
var RESIDUE_RULE_IDS = new Set(Object.keys(RESIDUE_SEVERITIES));
|
|
42809
|
+
function hasBlockingResidueCapability(findings) {
|
|
42810
|
+
return findings.some((f) => RESIDUE_RULE_IDS.has(f.rule) && isBlocking(f.severity));
|
|
42811
|
+
}
|
|
42812
|
+
function proofFinding(rule, art, cause) {
|
|
42813
|
+
return {
|
|
42814
|
+
rule,
|
|
42815
|
+
severity: "CRITICAL",
|
|
42816
|
+
target: "artifact",
|
|
42817
|
+
path: art.file,
|
|
42818
|
+
engine: "native",
|
|
42819
|
+
message: `${art.file} (sha256 ${art.sha256.slice(0, 12)}\u2026) tripped the residue scan while residue.requireProof is on, but ${cause}. Run 'border roundtrip <spec>' for exactly these bytes to mint the ledger proof.`,
|
|
42820
|
+
...redact(`${art.sha256}:${cause}`),
|
|
42821
|
+
snippet: `${art.file} ${art.sha256.slice(0, 12)}`
|
|
42822
|
+
};
|
|
42823
|
+
}
|
|
42824
|
+
function proofFindings(o) {
|
|
42825
|
+
const out = [];
|
|
42826
|
+
for (const art of o.artifacts) {
|
|
42827
|
+
const rec = latestRoundtripForSha(o.records, art.sha256);
|
|
42828
|
+
if (rec === null) {
|
|
42829
|
+
out.push(proofFinding(ROUNDTRIP_PROOF_MISSING_RULE, art, "no roundtrip proof record exists for its digest"));
|
|
42830
|
+
} else if (rec.rulesHash !== o.rulesHash) {
|
|
42831
|
+
out.push(
|
|
42832
|
+
proofFinding(
|
|
42833
|
+
ROUNDTRIP_PROOF_STALE_RULE,
|
|
42834
|
+
art,
|
|
42835
|
+
`its proof record predates the current rules policy (recorded ${rec.rulesHash.slice(0, 12)}\u2026, required ${o.rulesHash.slice(0, 12)}\u2026)`
|
|
42836
|
+
)
|
|
42837
|
+
);
|
|
42838
|
+
}
|
|
42839
|
+
}
|
|
42840
|
+
return out;
|
|
42841
|
+
}
|
|
42842
|
+
|
|
42410
42843
|
// src/check/tagScan.ts
|
|
42411
|
-
import { spawnSync as
|
|
42844
|
+
import { spawnSync as spawnSync13 } from "node:child_process";
|
|
42412
42845
|
import { mkdtempSync as mkdtempSync3, rmSync as rmSync10, writeFileSync as writeFileSync4 } from "node:fs";
|
|
42413
42846
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
42414
|
-
import { join as
|
|
42847
|
+
import { join as join21, resolve as resolve12 } from "node:path";
|
|
42415
42848
|
var TAG_MESSAGE_RULE = "tag-message-secret";
|
|
42416
42849
|
function isEnoent2(error61) {
|
|
42417
42850
|
return typeof error61 === "object" && error61 !== null && error61.code === "ENOENT";
|
|
@@ -42431,7 +42864,7 @@ function stdinFindings(text, ignorePath, o) {
|
|
|
42431
42864
|
];
|
|
42432
42865
|
let lastError;
|
|
42433
42866
|
for (const bin of binaryCandidates("gitleaks", o)) {
|
|
42434
|
-
const r =
|
|
42867
|
+
const r = spawnSync13(bin, args, {
|
|
42435
42868
|
encoding: "utf8",
|
|
42436
42869
|
input: text,
|
|
42437
42870
|
timeout: o.timeoutMs ?? 3e5,
|
|
@@ -42502,8 +42935,8 @@ function scanTagMessages(o) {
|
|
|
42502
42935
|
const gitOpts = { ...o.env !== void 0 ? { env: o.env } : {} };
|
|
42503
42936
|
const refs = runGitChecked(repoDir, ["for-each-ref", "--format=%(refname)", "refs/tags"], gitOpts).split("\n").filter((ref) => ref !== "");
|
|
42504
42937
|
if (refs.length === 0) return [];
|
|
42505
|
-
const ignoreDir = mkdtempSync3(
|
|
42506
|
-
const ignorePath =
|
|
42938
|
+
const ignoreDir = mkdtempSync3(join21(tmpdir2(), "border-gitleaks-ignore-"));
|
|
42939
|
+
const ignorePath = join21(ignoreDir, ".gitleaksignore");
|
|
42507
42940
|
writeFileSync4(ignorePath, "");
|
|
42508
42941
|
try {
|
|
42509
42942
|
const findings = [];
|
|
@@ -42523,7 +42956,6 @@ function scanTagMessages(o) {
|
|
|
42523
42956
|
|
|
42524
42957
|
// src/check.ts
|
|
42525
42958
|
var TRACKED_BORDER_RULE = "repo-tracks-border-state";
|
|
42526
|
-
var RESIDUE_RULE_IDS = new Set(Object.keys(RESIDUE_SEVERITIES));
|
|
42527
42959
|
var GUARD_PATH_CAP = 50;
|
|
42528
42960
|
function trackedBorderStateFinding(repoDir, headSha, env2) {
|
|
42529
42961
|
const listing = runGitChecked(repoDir, ["ls-files", "--", `${BORDER_STATE_DIR}/`], { ...env2 !== void 0 ? { env: env2 } : {} }).split("\n").filter((p) => p !== "");
|
|
@@ -42580,7 +43012,7 @@ async function runPipeline(o, ctx, lockWarning) {
|
|
|
42580
43012
|
if (!broken.has("gitleaks")) {
|
|
42581
43013
|
const eng = legOptions(o, sanitizer);
|
|
42582
43014
|
findings.push(...filterBorderStateFindings(scanGitHistory({ repoDir, refRange: historyRefRange(ctx), target: "git", ...eng }), repoDir));
|
|
42583
|
-
findings.push(...filterBorderStateFindings(scanTree({ dir: repoDir, stateDir:
|
|
43015
|
+
findings.push(...filterBorderStateFindings(scanTree({ dir: repoDir, stateDir: join22(repoDir, BORDER_STATE_DIR), target: "tree", ...eng }), repoDir));
|
|
42584
43016
|
findings.push(...filterBorderStateFindings(scanTagMessages({ repoDir, target: "git", ...eng }), repoDir));
|
|
42585
43017
|
}
|
|
42586
43018
|
if (!broken.has("secretlint")) {
|
|
@@ -42605,6 +43037,17 @@ async function runPipeline(o, ctx, lockWarning) {
|
|
|
42605
43037
|
findings.push(...stage.findings);
|
|
42606
43038
|
ledgerArtifacts.push(...stage.artifacts);
|
|
42607
43039
|
}
|
|
43040
|
+
if ((o.cfg.release?.twin.length ?? 0) > 0) {
|
|
43041
|
+
findings.push(
|
|
43042
|
+
...await twinCoherenceFindings({
|
|
43043
|
+
pairs: o.cfg.release?.twin ?? [],
|
|
43044
|
+
artifacts: ledgerArtifacts,
|
|
43045
|
+
repoDir,
|
|
43046
|
+
...envOpt,
|
|
43047
|
+
sanitizer
|
|
43048
|
+
})
|
|
43049
|
+
);
|
|
43050
|
+
}
|
|
42608
43051
|
findings.push(...await runRegistryProbes({ repoDir, cfg: o.cfg, effectiveTargets: o.effectiveTargets, ...envOpt }));
|
|
42609
43052
|
if (o.cfg.residue?.enabled === false) {
|
|
42610
43053
|
for (let i = findings.length - 1; i >= 0; i -= 1) {
|
|
@@ -42612,13 +43055,16 @@ async function runPipeline(o, ctx, lockWarning) {
|
|
|
42612
43055
|
if (f !== void 0 && RESIDUE_RULE_IDS.has(f.rule)) findings.splice(i, 1);
|
|
42613
43056
|
}
|
|
42614
43057
|
}
|
|
42615
|
-
const exposure = [...exposureSet(o.cfg, { cwd: repoDir })];
|
|
42616
|
-
const allow = applyAllowList(findings, o.cfg.allow, repoDir);
|
|
42617
43058
|
const rulesHash = await computeCheckRulesHash({
|
|
42618
43059
|
engineVersions: probe.engineVersions,
|
|
42619
43060
|
configDigest: o.configDigest,
|
|
42620
43061
|
...o.env !== void 0 ? { env: o.env } : {}
|
|
42621
43062
|
});
|
|
43063
|
+
if (o.cfg.residue?.requireProof === true && ledgerArtifacts.length > 0 && hasBlockingResidueCapability(findings)) {
|
|
43064
|
+
findings.push(...proofFindings({ artifacts: ledgerArtifacts, records: readLedger(repoDir).records, rulesHash }));
|
|
43065
|
+
}
|
|
43066
|
+
const exposure = [...exposureSet(o.cfg, { cwd: repoDir })];
|
|
43067
|
+
const allow = applyAllowList(findings, o.cfg.allow, repoDir);
|
|
42622
43068
|
const report = {
|
|
42623
43069
|
schemaVersion: 1,
|
|
42624
43070
|
key: computeCheckKey({
|
|
@@ -42754,16 +43200,16 @@ var NO_TARGETS_PROBE_CFG = {
|
|
|
42754
43200
|
// src/llm/bundle.ts
|
|
42755
43201
|
import { createHash as createHash13 } from "node:crypto";
|
|
42756
43202
|
import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "node:fs";
|
|
42757
|
-
import { dirname as dirname11, join as
|
|
43203
|
+
import { dirname as dirname11, join as join24 } from "node:path";
|
|
42758
43204
|
|
|
42759
43205
|
// src/llm/diff.ts
|
|
42760
|
-
import { spawnSync as
|
|
43206
|
+
import { spawnSync as spawnSync14 } from "node:child_process";
|
|
42761
43207
|
import { mkdtempSync as mkdtempSync4, rmSync as rmSync11, writeFileSync as writeFileSync5 } from "node:fs";
|
|
42762
|
-
import { dirname as dirname10, join as
|
|
43208
|
+
import { dirname as dirname10, join as join23, resolve as resolve13 } from "node:path";
|
|
42763
43209
|
var GIT_DIFF_MAX_BUFFER = 64 * 1024 * 1024;
|
|
42764
43210
|
function gitOut(repoDir, args, o, allowFail = false) {
|
|
42765
43211
|
const abs = resolve13(repoDir);
|
|
42766
|
-
const r =
|
|
43212
|
+
const r = spawnSync14("git", ["-C", abs, ...args], {
|
|
42767
43213
|
encoding: "utf8",
|
|
42768
43214
|
env: { ...o.env ?? process.env, GIT_CEILING_DIRECTORIES: dirname10(abs) },
|
|
42769
43215
|
timeout: 12e4,
|
|
@@ -42856,18 +43302,18 @@ function collectRawDeltas(repoDir, baseSha, headSha, o = {}) {
|
|
|
42856
43302
|
var MASK_CHUNK_CHARS = 2e6;
|
|
42857
43303
|
function maskViaEngineDetection(repoDir, texts, o = {}) {
|
|
42858
43304
|
const sanitizer = new TextSanitizer();
|
|
42859
|
-
const sandbox = mkdtempSync4(
|
|
43305
|
+
const sandbox = mkdtempSync4(join23(repoDir, BORDER_STATE_DIR, "llm-mask-"));
|
|
42860
43306
|
try {
|
|
42861
43307
|
let idx = 0;
|
|
42862
43308
|
for (const text of texts) {
|
|
42863
43309
|
if (text === "") continue;
|
|
42864
43310
|
for (let off = 0; off < text.length; off += MASK_CHUNK_CHARS, idx += 1) {
|
|
42865
|
-
writeFileSync5(
|
|
43311
|
+
writeFileSync5(join23(sandbox, `detect-${String(idx)}.txt`), text.slice(off, off + MASK_CHUNK_CHARS), "utf8");
|
|
42866
43312
|
}
|
|
42867
43313
|
}
|
|
42868
43314
|
scanTree({
|
|
42869
43315
|
dir: sandbox,
|
|
42870
|
-
stateDir:
|
|
43316
|
+
stateDir: join23(repoDir, BORDER_STATE_DIR),
|
|
42871
43317
|
target: "llm-mask",
|
|
42872
43318
|
sanitizer,
|
|
42873
43319
|
...engineEnv(o)
|
|
@@ -42925,7 +43371,7 @@ async function runLlmRequestCore(ctx) {
|
|
|
42925
43371
|
ctx.stderr("border: llm-request: no check record for the current state \u2014 run 'border check' first (the bundle derives from the recorded ctx)");
|
|
42926
43372
|
return EXIT_ERROR;
|
|
42927
43373
|
}
|
|
42928
|
-
const absReport =
|
|
43374
|
+
const absReport = join24(repoDir, record2.reportPath);
|
|
42929
43375
|
if (!existsSync15(absReport)) {
|
|
42930
43376
|
ctx.stderr(`border: llm-request: report archive missing (${record2.reportPath} pruned?) \u2014 run 'border check' again`);
|
|
42931
43377
|
return EXIT_ERROR;
|
|
@@ -42984,7 +43430,7 @@ async function runLlmRequestCore(ctx) {
|
|
|
42984
43430
|
deterministic: { verdict: report.verdict, counts: report.counts, findings: report.findings },
|
|
42985
43431
|
promptTemplate: { path: templatePath, sha256: createHash13("sha256").update(readFileSync14(templatePath)).digest("hex") }
|
|
42986
43432
|
};
|
|
42987
|
-
const outPath =
|
|
43433
|
+
const outPath = join24(dirname11(absReport), "llm-request.json");
|
|
42988
43434
|
mkdirSync10(dirname11(outPath), { recursive: true });
|
|
42989
43435
|
writeFileSync6(outPath, `${JSON.stringify(bundle, null, 2)}
|
|
42990
43436
|
`, "utf8");
|
|
@@ -42996,7 +43442,7 @@ async function runLlmRequestCore(ctx) {
|
|
|
42996
43442
|
// src/llm/ingest.ts
|
|
42997
43443
|
import { createHash as createHash14 } from "node:crypto";
|
|
42998
43444
|
import { existsSync as existsSync16, readFileSync as readFileSync15, readdirSync as readdirSync9 } from "node:fs";
|
|
42999
|
-
import { isAbsolute, join as
|
|
43445
|
+
import { isAbsolute, join as join25, resolve as resolve14 } from "node:path";
|
|
43000
43446
|
var RULE_RE = /^[a-z0-9-]+$/;
|
|
43001
43447
|
var MAX_TEXT = 4e3;
|
|
43002
43448
|
var ALLOWED_KEYS = /* @__PURE__ */ new Set(["rule", "severity", "target", "path", "line", "commit", "message", "snippet", "engine", "valueDigest"]);
|
|
@@ -43015,7 +43461,7 @@ function findLatestBundle(repoDir) {
|
|
|
43015
43461
|
return "no llm-request bundle found \u2014 run 'border llm-request' first";
|
|
43016
43462
|
}
|
|
43017
43463
|
for (const d of dirs.sort().reverse()) {
|
|
43018
|
-
const p =
|
|
43464
|
+
const p = join25(runsDir(repoDir), d, "llm-request.json");
|
|
43019
43465
|
if (!existsSync16(p)) continue;
|
|
43020
43466
|
let parsed;
|
|
43021
43467
|
try {
|
|
@@ -43028,7 +43474,7 @@ function findLatestBundle(repoDir) {
|
|
|
43028
43474
|
if (b.schemaVersion !== 1 || typeof b.key !== "string" || !/^[0-9a-f]{64}$/.test(b.key) || typeof b.head !== "string" || !/^[0-9a-f]{40}$/.test(b.head) || !Array.isArray(b.fileDeltas) || b.deterministic === void 0) {
|
|
43029
43475
|
return `bundle ${p} fails its own schema \u2014 re-run 'border llm-request'`;
|
|
43030
43476
|
}
|
|
43031
|
-
return { dir:
|
|
43477
|
+
return { dir: join25(runsDir(repoDir), d), bundle: b };
|
|
43032
43478
|
}
|
|
43033
43479
|
return "no llm-request bundle found \u2014 run 'border llm-request' first";
|
|
43034
43480
|
}
|
|
@@ -43173,7 +43619,7 @@ async function runLlmRequest(ctx) {
|
|
|
43173
43619
|
}
|
|
43174
43620
|
|
|
43175
43621
|
// src/push/git.ts
|
|
43176
|
-
import { spawnSync as
|
|
43622
|
+
import { spawnSync as spawnSync15 } from "node:child_process";
|
|
43177
43623
|
var DIVERGED_MESSAGE = "history diverged; border never force-pushes; resolve manually";
|
|
43178
43624
|
function pushArgv(url2, branch, mode) {
|
|
43179
43625
|
return mode === "dry-run" ? ["push", "--dry-run", url2, branch, "--follow-tags"] : ["push", url2, branch, "--follow-tags"];
|
|
@@ -43208,7 +43654,7 @@ function lsRemoteMap(repoDir, remote, env2) {
|
|
|
43208
43654
|
return parseLsRemote(text, remote.target);
|
|
43209
43655
|
}
|
|
43210
43656
|
function isAncestor(repoDir, remoteSha, localSha, env2) {
|
|
43211
|
-
const r =
|
|
43657
|
+
const r = spawnSync15("git", ["-C", repoDir, "merge-base", "--is-ancestor", remoteSha, localSha], {
|
|
43212
43658
|
encoding: "utf8",
|
|
43213
43659
|
env: { ...env2, GIT_CEILING_DIRECTORIES: repoDir }
|
|
43214
43660
|
});
|
|
@@ -43243,7 +43689,7 @@ function pushTimeoutMs(env2) {
|
|
|
43243
43689
|
}
|
|
43244
43690
|
function executePush(i) {
|
|
43245
43691
|
const timeoutMs = pushTimeoutMs(i.env);
|
|
43246
|
-
const r =
|
|
43692
|
+
const r = spawnSync15("git", ["-C", i.repoDir, ...pushArgv(i.remote.url, i.branch, "execute")], {
|
|
43247
43693
|
stdio: "inherit",
|
|
43248
43694
|
env: i.env,
|
|
43249
43695
|
timeout: timeoutMs
|
|
@@ -43436,41 +43882,18 @@ async function executeYesPush(ctx, loaded) {
|
|
|
43436
43882
|
return EXIT_PASS;
|
|
43437
43883
|
}
|
|
43438
43884
|
|
|
43439
|
-
// src/
|
|
43440
|
-
import { mkdtempSync as mkdtempSync5 } from "node:fs";
|
|
43885
|
+
// src/roundtrip/orchestrate.ts
|
|
43886
|
+
import { mkdtempSync as mkdtempSync5, rmSync as rmSync12, writeFileSync as writeFileSync7 } from "node:fs";
|
|
43887
|
+
import { randomBytes } from "node:crypto";
|
|
43441
43888
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
43442
43889
|
import { join as join26 } from "node:path";
|
|
43443
43890
|
|
|
43444
|
-
// src/scan/config.ts
|
|
43445
|
-
function buildScanConfigYaml(eco) {
|
|
43446
|
-
return [
|
|
43447
|
-
"version: 1",
|
|
43448
|
-
"targets:",
|
|
43449
|
-
" git:",
|
|
43450
|
-
" remotes: []",
|
|
43451
|
-
` ${eco}: {}`,
|
|
43452
|
-
"rules:",
|
|
43453
|
-
" authors:",
|
|
43454
|
-
" emails: []",
|
|
43455
|
-
" names: []",
|
|
43456
|
-
" hosts: []",
|
|
43457
|
-
" ips: []",
|
|
43458
|
-
" pathPatterns: []",
|
|
43459
|
-
"residue:",
|
|
43460
|
-
" enabled: true",
|
|
43461
|
-
""
|
|
43462
|
-
].join("\n");
|
|
43463
|
-
}
|
|
43464
|
-
function synthesizeScanConfig(eco) {
|
|
43465
|
-
return parseConfig(buildScanConfigYaml(eco), "<border scan>");
|
|
43466
|
-
}
|
|
43467
|
-
|
|
43468
43891
|
// src/scan/fetch.ts
|
|
43469
43892
|
import { createHash as createHash15 } from "node:crypto";
|
|
43470
|
-
import { basename as
|
|
43893
|
+
import { basename as basename5 } from "node:path";
|
|
43471
43894
|
var SCAN_MAX_ARTIFACT_BYTES = 200 * 1024 * 1024;
|
|
43472
43895
|
var SCAN_REQUEST_TIMEOUT_MS = 6e4;
|
|
43473
|
-
var SCAN_USER_AGENT = "border-customs/0.
|
|
43896
|
+
var SCAN_USER_AGENT = "border-customs/0.4.1 (+https://github.com/sumteclab/border; border scan)";
|
|
43474
43897
|
var CRATES_DOWNLOAD_HOST = "https://static.crates.io";
|
|
43475
43898
|
var defaultScanFetcher = async (url2, init) => {
|
|
43476
43899
|
if (!/^https?:\/\//.test(url2)) throw new EngineRunError(`border scan refuses non-http(s) URL: ${url2}`, null);
|
|
@@ -43546,7 +43969,7 @@ function rubygemsDownloadUrl(name, version2, base = channelBaseUrl("rubygems"))
|
|
|
43546
43969
|
return `${base}/downloads/${name}-${version2}.gem`;
|
|
43547
43970
|
}
|
|
43548
43971
|
function safeArtifactName(raw, fallback) {
|
|
43549
|
-
const base =
|
|
43972
|
+
const base = basename5(raw.split("?")[0] ?? "").replace(/[^A-Za-z0-9._+-]/g, "_");
|
|
43550
43973
|
return base.length > 0 && base !== "." && base !== ".." ? base : fallback;
|
|
43551
43974
|
}
|
|
43552
43975
|
async function fetchArtifact(spec, o) {
|
|
@@ -43599,9 +44022,746 @@ function sha256Hex(data) {
|
|
|
43599
44022
|
return createHash15("sha256").update(data).digest("hex");
|
|
43600
44023
|
}
|
|
43601
44024
|
|
|
44025
|
+
// src/scan/spec.ts
|
|
44026
|
+
var ECOSYSTEMS = ["npm", "pypi", "crates", "rubygems"];
|
|
44027
|
+
var SCAN_SPEC_USAGE = "usage: border scan <[ecosystem:]name@version> \u2014 e.g. border scan puppeteer@23.11.1 | pypi:requests@2.32.3 | crates:serde@1.0.219 | rubygems:rails@7.1.1";
|
|
44028
|
+
var PLAIN_NAME = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
|
|
44029
|
+
var SCOPED_NAME = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
44030
|
+
var VERSION = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
|
|
44031
|
+
var bad2 = (why) => {
|
|
44032
|
+
throw new UnknownArgError(`${why}; ${SCAN_SPEC_USAGE}`);
|
|
44033
|
+
};
|
|
44034
|
+
function parseScanSpec(raw) {
|
|
44035
|
+
const text = raw.trim();
|
|
44036
|
+
if (text.length === 0) return bad2("empty spec");
|
|
44037
|
+
let ecosystem = "npm";
|
|
44038
|
+
let rest = text;
|
|
44039
|
+
const colon = text.indexOf(":");
|
|
44040
|
+
if (colon >= 0) {
|
|
44041
|
+
const prefix = text.slice(0, colon);
|
|
44042
|
+
const found = ECOSYSTEMS.find((e) => e === prefix);
|
|
44043
|
+
if (found === void 0) {
|
|
44044
|
+
return bad2(`unknown ecosystem '${prefix}' \u2014 expected one of: ${ECOSYSTEMS.join(", ")}`);
|
|
44045
|
+
}
|
|
44046
|
+
ecosystem = found;
|
|
44047
|
+
rest = text.slice(colon + 1);
|
|
44048
|
+
if (rest.length === 0) return bad2("missing package name after the ecosystem prefix");
|
|
44049
|
+
}
|
|
44050
|
+
const scoped = rest.startsWith("@");
|
|
44051
|
+
const at = rest.lastIndexOf("@");
|
|
44052
|
+
if (at <= 0) return bad2(`spec '${raw}' is missing '@version'`);
|
|
44053
|
+
const name = rest.slice(0, at);
|
|
44054
|
+
const version2 = rest.slice(at + 1);
|
|
44055
|
+
if (!(scoped ? SCOPED_NAME : PLAIN_NAME).test(name)) {
|
|
44056
|
+
return bad2(`invalid package name '${name}'`);
|
|
44057
|
+
}
|
|
44058
|
+
if (!VERSION.test(version2)) {
|
|
44059
|
+
return bad2(`invalid version '${version2}' in spec '${raw}'`);
|
|
44060
|
+
}
|
|
44061
|
+
return { ecosystem, name, version: version2 };
|
|
44062
|
+
}
|
|
44063
|
+
|
|
44064
|
+
// src/roundtrip/localArtifact.ts
|
|
44065
|
+
import { createReadStream, existsSync as existsSync17, statSync as statSync2 } from "node:fs";
|
|
44066
|
+
import { createHash as createHash16 } from "node:crypto";
|
|
44067
|
+
import { basename as basename6, resolve as resolve15 } from "node:path";
|
|
44068
|
+
var EXT_TABLE = /* @__PURE__ */ new Map([
|
|
44069
|
+
[".whl", "pypi"],
|
|
44070
|
+
[".tgz", "npm"],
|
|
44071
|
+
[".crate", "crates"],
|
|
44072
|
+
[".gem", "rubygems"]
|
|
44073
|
+
]);
|
|
44074
|
+
var EXT_LIST = ".whl (pypi), .tgz (npm), .crate (crates), .gem (rubygems)";
|
|
44075
|
+
var CONTAINER_NAME = /* @__PURE__ */ new Map([
|
|
44076
|
+
[".tgz", "/root/p.tgz"],
|
|
44077
|
+
[".crate", "/root/p.crate"],
|
|
44078
|
+
[".gem", "/root/p.gem"]
|
|
44079
|
+
]);
|
|
44080
|
+
var wheelContainerPath = (base, name, version2) => `/root/${safeArtifactName(base, `${name}-${version2}-py3-none-any.whl`)}`;
|
|
44081
|
+
var bad3 = (why) => {
|
|
44082
|
+
throw new UnknownArgError(`border roundtrip: ${why}`);
|
|
44083
|
+
};
|
|
44084
|
+
function identityFromFilename(base, eco) {
|
|
44085
|
+
if (eco === "pypi") {
|
|
44086
|
+
const parts = base.slice(0, -".whl".length).split("-");
|
|
44087
|
+
const name = parts[0] ?? "";
|
|
44088
|
+
const version2 = parts[1] ?? "";
|
|
44089
|
+
if (parts.length < 5 || !/^[A-Za-z0-9][A-Za-z0-9._]*$/.test(name) || !/^\d[A-Za-z0-9._+!]*$/.test(version2)) return null;
|
|
44090
|
+
return { name, version: version2 };
|
|
44091
|
+
}
|
|
44092
|
+
if (eco === "npm") {
|
|
44093
|
+
const m2 = /^(.+)-(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)\.tgz$/i.exec(base);
|
|
44094
|
+
if (m2 === null || m2[1] === void 0 || m2[2] === void 0) return null;
|
|
44095
|
+
return { name: m2[1], version: m2[2] };
|
|
44096
|
+
}
|
|
44097
|
+
if (eco === "crates") {
|
|
44098
|
+
const m2 = /^([a-z0-9_][a-z0-9._-]*?)-(\d[A-Za-z0-9._+-]*)\.crate$/i.exec(base);
|
|
44099
|
+
if (m2 === null || m2[1] === void 0 || m2[2] === void 0) return null;
|
|
44100
|
+
return { name: m2[1], version: m2[2] };
|
|
44101
|
+
}
|
|
44102
|
+
const m = /^([A-Za-z0-9][A-Za-z0-9._-]*?)-(\d[A-Za-z0-9._+-]*)\.gem$/i.exec(base);
|
|
44103
|
+
if (m === null || m[1] === void 0 || m[2] === void 0) return null;
|
|
44104
|
+
return { name: m[1], version: m[2] };
|
|
44105
|
+
}
|
|
44106
|
+
var GRAMMAR_HINT = {
|
|
44107
|
+
pypi: "PEP 427 wheel names, e.g. jupyter_core-5.8.1-py3-none-any.whl",
|
|
44108
|
+
npm: "npm pack names, e.g. node-fetch-2.6.1.tgz (scoped packages flatten to scope-pkg-1.0.0.tgz)",
|
|
44109
|
+
crates: "cargo package names, e.g. rt-crate-1.0.0.crate",
|
|
44110
|
+
rubygems: "gem package names, e.g. rt-gem-1.0.0.gem"
|
|
44111
|
+
};
|
|
44112
|
+
function classifyLocalInput(raw, cwd, parsesAsRegistrySpec) {
|
|
44113
|
+
const abs = resolve15(cwd, raw);
|
|
44114
|
+
if (existsSync17(abs)) {
|
|
44115
|
+
const st = statSync2(abs);
|
|
44116
|
+
if (st.isDirectory()) {
|
|
44117
|
+
bad3(`'${raw}' resolves to a directory (${abs}) \u2014 local mode needs a single artifact FILE, not a directory`);
|
|
44118
|
+
}
|
|
44119
|
+
if (!st.isFile()) {
|
|
44120
|
+
bad3(`'${raw}' resolves to something that is not a regular file (${abs})`);
|
|
44121
|
+
}
|
|
44122
|
+
const lower = basename6(abs).toLowerCase();
|
|
44123
|
+
const ext = [...EXT_TABLE.keys()].find((e) => lower.endsWith(e)) ?? null;
|
|
44124
|
+
if (ext === null) {
|
|
44125
|
+
const known = [...EXT_TABLE.keys()].map((e) => `\`${e}\``).join(", ");
|
|
44126
|
+
bad3(`'${basename6(abs)}' exists but its extension is not in the closed local-artifact table (${known}) \u2014 no content sniffing is done; supported: ${EXT_LIST}`);
|
|
44127
|
+
}
|
|
44128
|
+
const eco = EXT_TABLE.get(ext);
|
|
44129
|
+
const identity = identityFromFilename(basename6(abs), eco);
|
|
44130
|
+
if (identity === null) {
|
|
44131
|
+
bad3(`cannot derive package identity from '${basename6(abs)}' \u2014 expected ${GRAMMAR_HINT[eco]}`);
|
|
44132
|
+
}
|
|
44133
|
+
return {
|
|
44134
|
+
absPath: abs,
|
|
44135
|
+
ecosystem: eco,
|
|
44136
|
+
name: identity.name,
|
|
44137
|
+
version: identity.version,
|
|
44138
|
+
containerPath: ext === ".whl" ? wheelContainerPath(basename6(abs), identity.name, identity.version) : CONTAINER_NAME.get(ext),
|
|
44139
|
+
source: `local:${abs}`
|
|
44140
|
+
};
|
|
44141
|
+
}
|
|
44142
|
+
const looksLocal = raw.includes("/") || [...EXT_TABLE.keys()].some((e) => raw.toLowerCase().endsWith(e));
|
|
44143
|
+
if (looksLocal && !parsesAsRegistrySpec(raw)) {
|
|
44144
|
+
bad3(`'${raw}' (resolved: ${abs}) is not an existing file \u2014 a path-shaped argument is LOCAL mode and is never fetched; fix the path or pass a registry spec`);
|
|
44145
|
+
}
|
|
44146
|
+
return null;
|
|
44147
|
+
}
|
|
44148
|
+
async function readLocalArtifact(absPath, cap = SCAN_MAX_ARTIFACT_BYTES) {
|
|
44149
|
+
const hash2 = createHash16("sha256");
|
|
44150
|
+
const parts = [];
|
|
44151
|
+
let total = 0;
|
|
44152
|
+
for await (const chunk of createReadStream(absPath)) {
|
|
44153
|
+
const b = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
44154
|
+
total += b.byteLength;
|
|
44155
|
+
if (total > cap) {
|
|
44156
|
+
throw new EngineRunError(
|
|
44157
|
+
`border roundtrip: ${absPath} streams past the 200 MiB artifact size cap after ${String(total)} bytes \u2014 refusing to prove a partial read (fail-closed)`,
|
|
44158
|
+
null
|
|
44159
|
+
);
|
|
44160
|
+
}
|
|
44161
|
+
hash2.update(b);
|
|
44162
|
+
parts.push(b);
|
|
44163
|
+
}
|
|
44164
|
+
return { bytes: Buffer.concat(parts), sha256: hash2.digest("hex") };
|
|
44165
|
+
}
|
|
44166
|
+
|
|
44167
|
+
// src/roundtrip/docker.ts
|
|
44168
|
+
var DOCKER_PROBE_TIMEOUT_MS = 15e3;
|
|
44169
|
+
var DOCKER_RUN_TIMEOUT_MS = 12e4;
|
|
44170
|
+
var DOCKER_CP_TIMEOUT_MS = 12e4;
|
|
44171
|
+
var SNAPSHOT_TIMEOUT_MS = 12e4;
|
|
44172
|
+
var PHASE_TIMEOUT_MS = 6e5;
|
|
44173
|
+
var DOCKER_RM_TIMEOUT_MS = 6e4;
|
|
44174
|
+
var CONTAINER_TTL_SEC = 900;
|
|
44175
|
+
var BASE_EXCLUDES = [
|
|
44176
|
+
"/proc",
|
|
44177
|
+
"/sys",
|
|
44178
|
+
"/dev",
|
|
44179
|
+
"/root/.npm",
|
|
44180
|
+
"/tmp/node-compile-cache",
|
|
44181
|
+
"/etc/hostname",
|
|
44182
|
+
"/etc/resolv.conf",
|
|
44183
|
+
"/etc/hosts"
|
|
44184
|
+
];
|
|
44185
|
+
var PYTHON_EXCLUDES = ["/root/.cache", "/var/log", "/var/lib/apt/lists", "*/__pycache__"];
|
|
44186
|
+
var CARGO_EXCLUDES = ["/root/.cargo", "/usr/local/cargo", "/usr/local/.crates2.json", "/usr/local/.crates.toml"];
|
|
44187
|
+
function snapshotScript(excludes) {
|
|
44188
|
+
const prune = `"(" ${excludes.map((p) => `-path "${p}"`).join(" -o ")} ")" -prune -o`;
|
|
44189
|
+
return [
|
|
44190
|
+
"set -e",
|
|
44191
|
+
'T=/dev/shm/rt-snap.$$; mkdir -p "$T"',
|
|
44192
|
+
`find / -xdev ${prune} -type f -print0 > "$T/f0"`,
|
|
44193
|
+
// || true: a file can vanish between find passes (package managers churn
|
|
44194
|
+
// temp state); the missing row is consistent noise, not a false residue.
|
|
44195
|
+
`xargs -0 -r sha256sum < "$T/f0" 2>/dev/null > "$T/hashes" || true`,
|
|
44196
|
+
// sha256sum text mode = 64 hex + 2 spaces + path; substr keeps spaced paths whole.
|
|
44197
|
+
`awk '{ printf "F\\t%s\\t%s\\n", substr($0,67), substr($0,1,64) }' "$T/hashes" > "$T/files"`,
|
|
44198
|
+
`find / -xdev ${prune} -type l -printf 'L\\t%p\\t%l\\n' > "$T/links"`,
|
|
44199
|
+
`find / -xdev ${prune} -type d -printf 'D\\t%p\\t-\\n' > "$T/dirs"`,
|
|
44200
|
+
'LC_ALL=C sort "$T/files" "$T/links" "$T/dirs"',
|
|
44201
|
+
'rm -rf "$T"'
|
|
44202
|
+
].join("\n");
|
|
44203
|
+
}
|
|
44204
|
+
function realDockerExec() {
|
|
44205
|
+
return (args, timeoutMs) => {
|
|
44206
|
+
const r = spawnEngine(binaryCandidates("docker", {}), [...args], { timeoutMs });
|
|
44207
|
+
return { status: r.status, stdout: r.stdout, stderr: r.stderr };
|
|
44208
|
+
};
|
|
44209
|
+
}
|
|
44210
|
+
|
|
44211
|
+
// src/roundtrip/calibrate.ts
|
|
44212
|
+
var RT_DEP_RULE = "residue-roundtrip-dep-owned";
|
|
44213
|
+
var PIP_REPORT_PATH = "/dev/shm/rt-report.json";
|
|
44214
|
+
function normalizePipName(name) {
|
|
44215
|
+
return name.toLowerCase().replace(/[-_.]+/g, "-");
|
|
44216
|
+
}
|
|
44217
|
+
function parsePipReport(text) {
|
|
44218
|
+
const die = (why) => {
|
|
44219
|
+
throw new EngineRunError(`roundtrip: pypi closure report unusable (${why}) \u2014 cannot verify (fail-closed)`, null);
|
|
44220
|
+
};
|
|
44221
|
+
let doc;
|
|
44222
|
+
try {
|
|
44223
|
+
doc = JSON.parse(text);
|
|
44224
|
+
} catch {
|
|
44225
|
+
die("not JSON");
|
|
44226
|
+
}
|
|
44227
|
+
if (typeof doc !== "object" || doc === null) die("not an object");
|
|
44228
|
+
const rec = doc;
|
|
44229
|
+
if (String(rec.version) !== "1") die(`report format v${String(rec.version)} is not the v1 surface we validated on pip 25.0.1`);
|
|
44230
|
+
if (!Array.isArray(rec.install)) die("install is not an array");
|
|
44231
|
+
const closure = /* @__PURE__ */ new Map();
|
|
44232
|
+
for (const item of rec.install) {
|
|
44233
|
+
const name = item?.metadata?.name;
|
|
44234
|
+
const version2 = item?.metadata?.version;
|
|
44235
|
+
if (typeof name !== "string" || name.length === 0 || typeof version2 !== "string" || version2.length === 0) {
|
|
44236
|
+
throw new EngineRunError("roundtrip: pypi closure report unusable (an install entry lacks metadata.name/version) \u2014 cannot verify (fail-closed)", null);
|
|
44237
|
+
}
|
|
44238
|
+
closure.set(normalizePipName(name), version2);
|
|
44239
|
+
}
|
|
44240
|
+
if (closure.size === 0) die("empty install list \u2014 a resolvable target must report at least itself");
|
|
44241
|
+
return closure;
|
|
44242
|
+
}
|
|
44243
|
+
function claim2(map2, ambiguous, path4, owner) {
|
|
44244
|
+
if (ambiguous.has(path4)) return;
|
|
44245
|
+
const cur = map2.get(path4);
|
|
44246
|
+
if (cur === void 0) map2.set(path4, owner);
|
|
44247
|
+
else if (cur !== owner) {
|
|
44248
|
+
map2.delete(path4);
|
|
44249
|
+
ambiguous.add(path4);
|
|
44250
|
+
}
|
|
44251
|
+
}
|
|
44252
|
+
function parentOf(path4) {
|
|
44253
|
+
const cut = path4.lastIndexOf("/");
|
|
44254
|
+
return cut <= 0 ? "/" : path4.slice(0, cut);
|
|
44255
|
+
}
|
|
44256
|
+
function buildAttribution(claimText, guard) {
|
|
44257
|
+
const exactOwners = /* @__PURE__ */ new Map();
|
|
44258
|
+
const dirOwners = /* @__PURE__ */ new Map();
|
|
44259
|
+
const ambiguous = /* @__PURE__ */ new Set();
|
|
44260
|
+
const claimants = /* @__PURE__ */ new Map();
|
|
44261
|
+
const normCount = /* @__PURE__ */ new Map();
|
|
44262
|
+
const lines = claimText.split("\n");
|
|
44263
|
+
if (guard !== void 0) {
|
|
44264
|
+
for (const line of lines) {
|
|
44265
|
+
if (!line.startsWith("@ ")) continue;
|
|
44266
|
+
const f = line.slice(2).split(" ");
|
|
44267
|
+
if (f.length !== 6) continue;
|
|
44268
|
+
const [owner, norm, ver, di] = f;
|
|
44269
|
+
normCount.set(norm, (normCount.get(norm) ?? 0) + 1);
|
|
44270
|
+
if (!claimants.has(owner)) claimants.set(owner, { owner, norm, ver, di, root: parentOf(di), dirName: f[4], dirVer: f[5] });
|
|
44271
|
+
}
|
|
44272
|
+
}
|
|
44273
|
+
const legit = (owner) => {
|
|
44274
|
+
if (guard === void 0) return {};
|
|
44275
|
+
const c = claimants.get(owner);
|
|
44276
|
+
if (c === void 0) return void 0;
|
|
44277
|
+
if (c.norm === guard.target) return void 0;
|
|
44278
|
+
if (guard.pins.get(c.norm) !== c.ver) return void 0;
|
|
44279
|
+
if (c.dirName !== c.norm || c.dirVer !== c.ver) return void 0;
|
|
44280
|
+
if ((normCount.get(c.norm) ?? 0) !== 1) return void 0;
|
|
44281
|
+
return c;
|
|
44282
|
+
};
|
|
44283
|
+
for (const line of lines) {
|
|
44284
|
+
if (line.trim().length === 0 || line.startsWith("@ ")) continue;
|
|
44285
|
+
const tab = line.indexOf(" ");
|
|
44286
|
+
if (tab <= 0 || line.indexOf(" ", tab + 1) !== -1) continue;
|
|
44287
|
+
const owner = line.slice(0, tab);
|
|
44288
|
+
const path4 = line.slice(tab + 1);
|
|
44289
|
+
if (!path4.startsWith("/") || path4.length === 1) continue;
|
|
44290
|
+
if (guard !== void 0) {
|
|
44291
|
+
const c = legit(owner);
|
|
44292
|
+
if (c === void 0 || c.root === void 0 || !path4.startsWith(`${c.root}/`)) continue;
|
|
44293
|
+
}
|
|
44294
|
+
claim2(exactOwners, ambiguous, path4, owner);
|
|
44295
|
+
for (let dir = parentOf(path4); dir.length > 1; dir = parentOf(dir)) {
|
|
44296
|
+
claim2(dirOwners, ambiguous, dir, owner);
|
|
44297
|
+
}
|
|
44298
|
+
}
|
|
44299
|
+
return { exactOwners, dirOwners, ambiguous };
|
|
44300
|
+
}
|
|
44301
|
+
function ownerOf(entry, attribution) {
|
|
44302
|
+
const exact = attribution.exactOwners.get(entry.path);
|
|
44303
|
+
if (exact !== void 0) return exact;
|
|
44304
|
+
return entry.kind === "D" ? attribution.dirOwners.get(entry.path) : void 0;
|
|
44305
|
+
}
|
|
44306
|
+
var CLAIM_SCANNER_PY = [
|
|
44307
|
+
"import csv, json, os, re, site, sys",
|
|
44308
|
+
"try:",
|
|
44309
|
+
" closure = json.loads(os.environ['BORDER_CLOSURE'])",
|
|
44310
|
+
" assert isinstance(closure, dict) and closure",
|
|
44311
|
+
"except Exception:",
|
|
44312
|
+
" sys.exit(4)",
|
|
44313
|
+
"def norm(n):",
|
|
44314
|
+
" return re.sub(r'[-_.]+', '-', n).lower()",
|
|
44315
|
+
"def read_meta(di):",
|
|
44316
|
+
" name = ''",
|
|
44317
|
+
" ver = ''",
|
|
44318
|
+
" try:",
|
|
44319
|
+
" f = open(os.path.join(di, 'METADATA'), encoding='utf-8', errors='replace')",
|
|
44320
|
+
" except OSError:",
|
|
44321
|
+
" return None",
|
|
44322
|
+
" with f:",
|
|
44323
|
+
" for raw in f:",
|
|
44324
|
+
" line = raw.rstrip('\\n')",
|
|
44325
|
+
" if line == '':",
|
|
44326
|
+
" break",
|
|
44327
|
+
" if not name and line.startswith('Name:'):",
|
|
44328
|
+
" name = line[5:].strip()",
|
|
44329
|
+
" elif not ver and line.startswith('Version:'):",
|
|
44330
|
+
" ver = line[8:].strip()",
|
|
44331
|
+
" return (name, ver) if name and ver else None",
|
|
44332
|
+
"roots = []",
|
|
44333
|
+
"try:",
|
|
44334
|
+
" roots.extend(site.getsitepackages())",
|
|
44335
|
+
"except Exception:",
|
|
44336
|
+
" pass",
|
|
44337
|
+
"out = []",
|
|
44338
|
+
"seen = set()",
|
|
44339
|
+
"for root in roots:",
|
|
44340
|
+
" if not os.path.isdir(root) or root in seen:",
|
|
44341
|
+
" continue",
|
|
44342
|
+
" seen.add(root)",
|
|
44343
|
+
" for d in sorted(os.listdir(root)):",
|
|
44344
|
+
" if not d.endswith('.dist-info'):",
|
|
44345
|
+
" continue",
|
|
44346
|
+
" di = os.path.join(root, d)",
|
|
44347
|
+
" meta = read_meta(di)",
|
|
44348
|
+
" if meta is None or norm(meta[0]) not in closure:",
|
|
44349
|
+
" continue",
|
|
44350
|
+
" owner = meta[0] + '==' + meta[1]",
|
|
44351
|
+
" core = d[:-10]",
|
|
44352
|
+
" cut = core.rfind('-')",
|
|
44353
|
+
" dn = norm(core[:cut]) if cut > 0 else ''",
|
|
44354
|
+
" dv = core[cut + 1:] if cut > 0 else ''",
|
|
44355
|
+
" out.append('@\\t' + owner + '\\t' + norm(meta[0]) + '\\t' + meta[1] + '\\t' + di + '\\t' + dn + '\\t' + dv)",
|
|
44356
|
+
" out.append(owner + '\\t' + di)",
|
|
44357
|
+
" for base, _dirs, files in os.walk(di):",
|
|
44358
|
+
" for fn in sorted(files):",
|
|
44359
|
+
" out.append(owner + '\\t' + os.path.join(base, fn))",
|
|
44360
|
+
" try:",
|
|
44361
|
+
" rf = open(os.path.join(di, 'RECORD'), newline='', encoding='utf-8', errors='replace')",
|
|
44362
|
+
" with rf:",
|
|
44363
|
+
" for row in csv.reader(rf):",
|
|
44364
|
+
" if not row or not row[0].strip():",
|
|
44365
|
+
" continue",
|
|
44366
|
+
" p = os.path.normpath(os.path.join(root, row[0].strip()))",
|
|
44367
|
+
" if p.startswith(root + os.sep):",
|
|
44368
|
+
" out.append(owner + '\\t' + p)",
|
|
44369
|
+
" except Exception:",
|
|
44370
|
+
" pass",
|
|
44371
|
+
"sys.stdout.write(''.join(x + '\\n' for x in out))"
|
|
44372
|
+
].join("\n");
|
|
44373
|
+
|
|
44374
|
+
// src/roundtrip/manifest.ts
|
|
44375
|
+
import { createHash as createHash17 } from "node:crypto";
|
|
44376
|
+
var RT_ENGINE = "roundtrip";
|
|
44377
|
+
var RT_RESIDUE_RULE = "roundtrip-residue-file";
|
|
44378
|
+
var RT_MODIFY_RULE = "roundtrip-modified-file";
|
|
44379
|
+
var RT_PERSIST_RULE = "roundtrip-modified-persistence";
|
|
44380
|
+
var KEY_SEP = "";
|
|
44381
|
+
var keyOf = (kind, path4) => `${kind}${KEY_SEP}${path4}`;
|
|
44382
|
+
function parseManifest(text) {
|
|
44383
|
+
const out = /* @__PURE__ */ new Map();
|
|
44384
|
+
for (const line of text.split("\n")) {
|
|
44385
|
+
if (line.length === 0) continue;
|
|
44386
|
+
const parts = line.split(" ");
|
|
44387
|
+
if (parts.length < 3) continue;
|
|
44388
|
+
const kind = parts[0];
|
|
44389
|
+
const path4 = parts[1];
|
|
44390
|
+
const value = parts.slice(2).join(" ");
|
|
44391
|
+
if (kind !== "F" && kind !== "L" && kind !== "D") continue;
|
|
44392
|
+
out.set(keyOf(kind, path4), { kind, path: path4, value });
|
|
44393
|
+
}
|
|
44394
|
+
return out;
|
|
44395
|
+
}
|
|
44396
|
+
function diffManifests(before, after) {
|
|
44397
|
+
const added = [];
|
|
44398
|
+
const removed = [];
|
|
44399
|
+
const modified = [];
|
|
44400
|
+
for (const [key, entry] of after) {
|
|
44401
|
+
const old = before.get(key);
|
|
44402
|
+
if (old === void 0) added.push(entry);
|
|
44403
|
+
else if (old.value !== entry.value) modified.push(entry);
|
|
44404
|
+
}
|
|
44405
|
+
for (const [key, entry] of before) {
|
|
44406
|
+
if (!after.has(key)) removed.push(entry);
|
|
44407
|
+
}
|
|
44408
|
+
return { added, removed, modified };
|
|
44409
|
+
}
|
|
44410
|
+
var PERSISTENCE_PATTERNS = [
|
|
44411
|
+
/(^|\/)\.(bashrc|bash_profile|bash_login|profile|zshrc|zprofile|zshenv|kshrc|cshrc|login\.sh)$/,
|
|
44412
|
+
/^\/etc\/profile\.d\//,
|
|
44413
|
+
/^\/etc\/(bash\.bashrc|profile|environment|zsh\/zshrc|inputrc)$/,
|
|
44414
|
+
/^\/etc\/systemd\//,
|
|
44415
|
+
/(^|\/)\.config\/systemd\//,
|
|
44416
|
+
/(^|\/)systemd\/system\//,
|
|
44417
|
+
/(^|\/)(cron|cron\.d|cron\.[0-9dhmwy]+|spool\/cron)\//,
|
|
44418
|
+
/^\/etc\/crontab$/,
|
|
44419
|
+
/(^|\/)\.config\/fish\//,
|
|
44420
|
+
/^\/(usr\/)?(local\/)?(s?bin)\//,
|
|
44421
|
+
/(^|\/)\.local\/bin\//
|
|
44422
|
+
];
|
|
44423
|
+
function isPersistenceSurface(path4) {
|
|
44424
|
+
return PERSISTENCE_PATTERNS.some((re) => re.test(path4));
|
|
44425
|
+
}
|
|
44426
|
+
var digestOf = (what) => createHash17("sha256").update(what).digest("hex");
|
|
44427
|
+
function classifyResidue(label, diff, attribution = null) {
|
|
44428
|
+
const findings = [];
|
|
44429
|
+
for (const e of diff.added) {
|
|
44430
|
+
const owner = attribution !== null ? ownerOf(e, attribution) : void 0;
|
|
44431
|
+
if (owner !== void 0) {
|
|
44432
|
+
findings.push({
|
|
44433
|
+
rule: RT_DEP_RULE,
|
|
44434
|
+
severity: "LOW",
|
|
44435
|
+
target: label,
|
|
44436
|
+
path: e.path,
|
|
44437
|
+
engine: RT_ENGINE,
|
|
44438
|
+
message: `ADDED path survives uninstall but is claimed by still-installed dependency '${owner}' (pip-accountable) \u2014 demoted, non-blocking`,
|
|
44439
|
+
valueDigest: digestOf(`ADDED:${e.path}:${e.value}`),
|
|
44440
|
+
snippet: `dep-owned: ${e.path}`
|
|
44441
|
+
});
|
|
44442
|
+
continue;
|
|
44443
|
+
}
|
|
44444
|
+
findings.push({
|
|
44445
|
+
rule: RT_RESIDUE_RULE,
|
|
44446
|
+
severity: "HIGH",
|
|
44447
|
+
target: label,
|
|
44448
|
+
path: e.path,
|
|
44449
|
+
engine: RT_ENGINE,
|
|
44450
|
+
message: `installed file survived uninstall (ADDED in post-uninstall manifest)`,
|
|
44451
|
+
valueDigest: digestOf(`ADDED:${e.path}:${e.value}`),
|
|
44452
|
+
snippet: `residue: ${e.path}`
|
|
44453
|
+
});
|
|
44454
|
+
}
|
|
44455
|
+
for (const e of diff.modified) {
|
|
44456
|
+
const persist = isPersistenceSurface(e.path);
|
|
44457
|
+
findings.push({
|
|
44458
|
+
rule: persist ? RT_PERSIST_RULE : RT_MODIFY_RULE,
|
|
44459
|
+
severity: persist ? "CRITICAL" : "HIGH",
|
|
44460
|
+
target: label,
|
|
44461
|
+
path: e.path,
|
|
44462
|
+
engine: RT_ENGINE,
|
|
44463
|
+
message: persist ? `uninstall mutated a persistence surface (rc/systemd/cron/PATH) \u2014 content differs pre-install vs post-uninstall` : `file content differs pre-install vs post-uninstall (MODIFIED, not reverted by uninstall)`,
|
|
44464
|
+
valueDigest: digestOf(`MODIFIED:${e.path}:${e.value}`),
|
|
44465
|
+
snippet: persist ? `persistence: ${e.path}` : `modified: ${e.path}`
|
|
44466
|
+
});
|
|
44467
|
+
}
|
|
44468
|
+
return findings;
|
|
44469
|
+
}
|
|
44470
|
+
|
|
44471
|
+
// src/roundtrip/orchestrate.ts
|
|
44472
|
+
function containerNameFor(label, rand = randomBytes(3).toString("hex")) {
|
|
44473
|
+
return `rt-${sha256Hex(label).slice(0, 8)}-${rand}`;
|
|
44474
|
+
}
|
|
44475
|
+
var shArgv = (name) => {
|
|
44476
|
+
if (!/^[A-Za-z0-9@._+/-]+$/.test(name)) {
|
|
44477
|
+
throw new EngineRunError(`roundtrip: package name '${name}' rejected for shell composition`, null);
|
|
44478
|
+
}
|
|
44479
|
+
return [name];
|
|
44480
|
+
};
|
|
44481
|
+
var PROFILES = {
|
|
44482
|
+
npm: {
|
|
44483
|
+
image: "node:24-slim",
|
|
44484
|
+
artifactPath: "/root/p.tgz",
|
|
44485
|
+
excludes: BASE_EXCLUDES,
|
|
44486
|
+
install: (p) => ["npm", "install", "-g", "--foreground-scripts", "--no-audit", "--no-fund", p],
|
|
44487
|
+
uninstall: (s) => ["npm", "uninstall", "-g", shArgv(s.name)[0]]
|
|
44488
|
+
},
|
|
44489
|
+
pypi: {
|
|
44490
|
+
image: "python:3.12-slim",
|
|
44491
|
+
artifactPath: "/root/p.tar.gz",
|
|
44492
|
+
excludes: [...BASE_EXCLUDES, ...PYTHON_EXCLUDES],
|
|
44493
|
+
install: (p) => ["pip", "install", "--no-input", "--root-user-action=ignore", p],
|
|
44494
|
+
uninstall: (s) => ["pip", "uninstall", "-y", shArgv(s.name)[0]]
|
|
44495
|
+
},
|
|
44496
|
+
rubygems: {
|
|
44497
|
+
image: "ruby:3.4-slim",
|
|
44498
|
+
artifactPath: "/root/p.gem",
|
|
44499
|
+
excludes: BASE_EXCLUDES,
|
|
44500
|
+
install: (p) => ["gem", "install", "--no-document", p],
|
|
44501
|
+
uninstall: (s) => ["gem", "uninstall", shArgv(s.name)[0], "-x", "--force"]
|
|
44502
|
+
},
|
|
44503
|
+
crates: {
|
|
44504
|
+
image: "rust:1-slim",
|
|
44505
|
+
artifactPath: "/root/p.crate",
|
|
44506
|
+
excludes: [...BASE_EXCLUDES, ...CARGO_EXCLUDES],
|
|
44507
|
+
// W2.1 docker legs, verified on rust:1-slim / cargo 1.98: a bare positional
|
|
44508
|
+
// is parsed as a CRATE@VER spec (a directory 400s on "invalid character `/`"),
|
|
44509
|
+
// so the unpacked staging dir goes through --path; --no-track must NOT be
|
|
44510
|
+
// used because it suppresses the $ROOT/.crates2.json install record, which is
|
|
44511
|
+
// the only thing `cargo uninstall` reads (with it, every uninstall dies on
|
|
44512
|
+
// "package ID specification did not match"); and border's own /opt/<dir>
|
|
44513
|
+
// staging tree is never owned by cargo, so the chain that creates it removes
|
|
44514
|
+
// it — after `cargo install` succeeded, keeping install failure fail-closed.
|
|
44515
|
+
install: (p, s) => {
|
|
44516
|
+
const name = shArgv(s.name)[0];
|
|
44517
|
+
const dir = `${name}-${s.version}`;
|
|
44518
|
+
return ["sh", "-c", `tar xf ${p} -C /opt && cargo install --root /usr/local --path "/opt/${dir}" && rm -rf "/opt/${dir}"`];
|
|
44519
|
+
},
|
|
44520
|
+
uninstall: (s) => ["cargo", "uninstall", "--root", "/usr/local", shArgv(s.name)[0]]
|
|
44521
|
+
}
|
|
44522
|
+
};
|
|
44523
|
+
function profileFor(eco) {
|
|
44524
|
+
return PROFILES[eco];
|
|
44525
|
+
}
|
|
44526
|
+
function execChecked(exec, args, timeoutMs, what) {
|
|
44527
|
+
const r = exec(args, timeoutMs);
|
|
44528
|
+
if (r.status !== 0) {
|
|
44529
|
+
const tail3 = r.stderr.replace(/\s+/g, " ").slice(-240);
|
|
44530
|
+
throw new EngineRunError(`roundtrip: ${what} failed (exit ${String(r.status)})${tail3 ? `: ${tail3}` : ""} \u2014 cannot verify (fail-closed)`, r.status);
|
|
44531
|
+
}
|
|
44532
|
+
return r;
|
|
44533
|
+
}
|
|
44534
|
+
function resolvePipClosure(exec, profile, hostPath, label, spec) {
|
|
44535
|
+
const ctr = containerNameFor(`${label}#closure`);
|
|
44536
|
+
try {
|
|
44537
|
+
execChecked(exec, ["run", "-d", "--name", ctr, profile.image, "sleep", String(CONTAINER_TTL_SEC)], DOCKER_RUN_TIMEOUT_MS, "closure resolver start");
|
|
44538
|
+
execChecked(exec, ["cp", hostPath, `${ctr}:${profile.artifactPath}`], DOCKER_CP_TIMEOUT_MS, "resolver artifact copy");
|
|
44539
|
+
execChecked(
|
|
44540
|
+
exec,
|
|
44541
|
+
["exec", ctr, "pip", "install", "--dry-run", "--report", PIP_REPORT_PATH, "--no-input", "--root-user-action=ignore", profile.artifactPath],
|
|
44542
|
+
PHASE_TIMEOUT_MS,
|
|
44543
|
+
"pypi closure dry-run"
|
|
44544
|
+
);
|
|
44545
|
+
const rep = execChecked(exec, ["exec", ctr, "cat", PIP_REPORT_PATH], SNAPSHOT_TIMEOUT_MS, "pypi closure report read");
|
|
44546
|
+
const closure = parsePipReport(rep.stdout);
|
|
44547
|
+
if (!closure.has(normalizePipName(spec.name))) {
|
|
44548
|
+
throw new EngineRunError(`roundtrip: pip closure report does not list the target ${spec.name} \u2014 cannot verify (fail-closed)`, null);
|
|
44549
|
+
}
|
|
44550
|
+
return closure;
|
|
44551
|
+
} finally {
|
|
44552
|
+
try {
|
|
44553
|
+
exec(["rm", "-f", ctr], DOCKER_RM_TIMEOUT_MS);
|
|
44554
|
+
} catch {
|
|
44555
|
+
}
|
|
44556
|
+
}
|
|
44557
|
+
}
|
|
44558
|
+
function scanDepClaims(exec, container, closure, target, note) {
|
|
44559
|
+
const guard = { pins: closure, target };
|
|
44560
|
+
const argv = ["exec", "-e", `BORDER_CLOSURE=${JSON.stringify(Object.fromEntries(closure))}`, container, "python3", "-c", CLAIM_SCANNER_PY];
|
|
44561
|
+
try {
|
|
44562
|
+
const r = exec(argv, SNAPSHOT_TIMEOUT_MS);
|
|
44563
|
+
if (r.status !== 0) {
|
|
44564
|
+
note(`roundtrip: dep-attribution scan failed (exit ${String(r.status)}) \u2014 every row keeps its blocking grade (over-report tolerated)`);
|
|
44565
|
+
return buildAttribution("", guard);
|
|
44566
|
+
}
|
|
44567
|
+
return buildAttribution(r.stdout, guard);
|
|
44568
|
+
} catch {
|
|
44569
|
+
note("roundtrip: dep-attribution scan could not run \u2014 every row keeps its blocking grade (over-report tolerated)");
|
|
44570
|
+
return buildAttribution("", guard);
|
|
44571
|
+
}
|
|
44572
|
+
}
|
|
44573
|
+
function runPipeline2(exec, profile, spec, label, hostPath, container, closure, note) {
|
|
44574
|
+
execChecked(exec, ["run", "-d", "--name", container, profile.image, "sleep", String(CONTAINER_TTL_SEC)], DOCKER_RUN_TIMEOUT_MS, "container start");
|
|
44575
|
+
try {
|
|
44576
|
+
execChecked(exec, ["cp", hostPath, `${container}:${profile.artifactPath}`], DOCKER_CP_TIMEOUT_MS, "artifact copy");
|
|
44577
|
+
const m1 = parseManifest(takeSnapshot(exec, container, profile.excludes));
|
|
44578
|
+
execChecked(exec, ["exec", container, ...profile.install(profile.artifactPath, spec)], PHASE_TIMEOUT_MS, `install ${label}`);
|
|
44579
|
+
const m2 = parseManifest(takeSnapshot(exec, container, profile.excludes));
|
|
44580
|
+
const installDiff = diffManifests(m1, m2);
|
|
44581
|
+
const installDelta = installDiff.added.length + installDiff.removed.length + installDiff.modified.length;
|
|
44582
|
+
if (installDelta === 0) {
|
|
44583
|
+
throw new EngineRunError(
|
|
44584
|
+
`roundtrip: install of ${label} produced an empty filesystem delta \u2014 cannot verify it ran, refusing to grade a no-op`,
|
|
44585
|
+
null
|
|
44586
|
+
);
|
|
44587
|
+
}
|
|
44588
|
+
execChecked(exec, ["exec", container, ...profile.uninstall(spec)], PHASE_TIMEOUT_MS, `uninstall ${label}`);
|
|
44589
|
+
const m3 = parseManifest(takeSnapshot(exec, container, profile.excludes));
|
|
44590
|
+
const attribution = closure !== null ? scanDepClaims(exec, container, closure, normalizePipName(spec.name), note) : null;
|
|
44591
|
+
return {
|
|
44592
|
+
findings: classifyResidue(label, diffManifests(m1, m3), attribution),
|
|
44593
|
+
installDelta,
|
|
44594
|
+
baselineDigest: sha256Hex([...m1.keys()].join("\n"))
|
|
44595
|
+
};
|
|
44596
|
+
} finally {
|
|
44597
|
+
try {
|
|
44598
|
+
exec(["rm", "-f", container], DOCKER_RM_TIMEOUT_MS);
|
|
44599
|
+
} catch {
|
|
44600
|
+
}
|
|
44601
|
+
}
|
|
44602
|
+
}
|
|
44603
|
+
function takeSnapshot(exec, container, excludes) {
|
|
44604
|
+
return execChecked(
|
|
44605
|
+
exec,
|
|
44606
|
+
["exec", container, "sh", "-c", snapshotScript(excludes)],
|
|
44607
|
+
SNAPSHOT_TIMEOUT_MS,
|
|
44608
|
+
"manifest snapshot"
|
|
44609
|
+
).stdout;
|
|
44610
|
+
}
|
|
44611
|
+
async function runRoundtripCore(ctx, deps = {}) {
|
|
44612
|
+
const raw = ctx.positionals[0];
|
|
44613
|
+
if (ctx.positionals.length !== 1 || raw === void 0) {
|
|
44614
|
+
throw new UnknownArgError(
|
|
44615
|
+
`border roundtrip expects exactly one argument \u2014 a registry spec or a local artifact file (got ${String(ctx.positionals.length)}); ${SCAN_SPEC_USAGE}`
|
|
44616
|
+
);
|
|
44617
|
+
}
|
|
44618
|
+
const local = classifyLocalInput(raw, ctx.cwd, parsesRegistrySpec);
|
|
44619
|
+
const spec = local !== null ? { ecosystem: local.ecosystem, name: local.name, version: local.version } : parseScanSpec(raw);
|
|
44620
|
+
const laneProfile = profileFor(spec.ecosystem);
|
|
44621
|
+
const profile = local !== null ? { ...laneProfile, artifactPath: local.containerPath } : laneProfile;
|
|
44622
|
+
const label = `${spec.ecosystem}:${spec.name}@${spec.version}`;
|
|
44623
|
+
const exec = deps.exec ?? realDockerExec();
|
|
44624
|
+
if (!dockerAvailableGuard(exec)) {
|
|
44625
|
+
ctx.stderr("roundtrip: docker unavailable \u2014 cannot verify");
|
|
44626
|
+
return EXIT_ERROR;
|
|
44627
|
+
}
|
|
44628
|
+
const baseDir = mkdtempSync5(join26(tmpdir3(), "border-roundtrip-"));
|
|
44629
|
+
try {
|
|
44630
|
+
const localRead = local !== null ? await readLocalArtifact(local.absPath) : null;
|
|
44631
|
+
const bytes = localRead?.bytes ?? (await fetchArtifact(spec, { fetcher: deps.fetcher ?? defaultScanFetcher })).bytes;
|
|
44632
|
+
const artifactSha256 = localRead !== null ? localRead.sha256 : sha256Hex(bytes);
|
|
44633
|
+
const hostPath = join26(baseDir, "artifact");
|
|
44634
|
+
writeFileSync7(hostPath, bytes);
|
|
44635
|
+
const container = containerNameFor(label);
|
|
44636
|
+
const closure = spec.ecosystem === "pypi" ? resolvePipClosure(exec, profile, hostPath, label, spec) : null;
|
|
44637
|
+
const { findings, installDelta, baselineDigest } = runPipeline2(exec, profile, spec, label, hostPath, container, closure, (msg) => ctx.stderr(msg));
|
|
44638
|
+
const verdict = computeVerdict(findings);
|
|
44639
|
+
const report = {
|
|
44640
|
+
schemaVersion: 1,
|
|
44641
|
+
key: sha256Hex(`roundtrip:${label}:${baselineDigest}:${verdict}`),
|
|
44642
|
+
head: baselineDigest.slice(0, 40),
|
|
44643
|
+
dirty: false,
|
|
44644
|
+
exposureSet: [label],
|
|
44645
|
+
refSet: [],
|
|
44646
|
+
rulesHash: sha256Hex(`${profile.image}|${profile.install(profile.artifactPath, spec).join(" ")}|${profile.uninstall(spec).join(" ")}`),
|
|
44647
|
+
verdict,
|
|
44648
|
+
counts: countFindings(findings),
|
|
44649
|
+
findings,
|
|
44650
|
+
// W2.4(b) provenance honesty: a local run proves ITS OWN bytes. The
|
|
44651
|
+
// fields are absent (byte-identical JSON) on the registry lane and
|
|
44652
|
+
// never phrase the artifact as registry-verified.
|
|
44653
|
+
...local !== null ? { source: local.source, artifactSha256 } : {},
|
|
44654
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
44655
|
+
};
|
|
44656
|
+
const sanitizer = new TextSanitizer();
|
|
44657
|
+
const line = (text) => sanitizer.sanitize(text).replace(/\s+/g, " ").trim();
|
|
44658
|
+
if (ctx.flags.json) {
|
|
44659
|
+
ctx.stdout(renderReportJson(report));
|
|
44660
|
+
} else {
|
|
44661
|
+
if (local !== null) {
|
|
44662
|
+
ctx.stdout(line(`roundtrip: source ${local.source} artifact sha256 ${artifactSha256}`));
|
|
44663
|
+
}
|
|
44664
|
+
for (const f of report.findings) {
|
|
44665
|
+
ctx.stdout(line(` ${f.severity} ${f.rule} [${f.engine}] ${f.path ?? f.target} ${f.message}`));
|
|
44666
|
+
}
|
|
44667
|
+
const depOwned = findings.reduce((n, f) => n + (f.rule === RT_DEP_RULE ? 1 : 0), 0);
|
|
44668
|
+
if (depOwned > 0) {
|
|
44669
|
+
ctx.stdout(line(`roundtrip: ${String(depOwned)} row(s) attributed to still-installed pip dependencies \u2014 demoted to ${RT_DEP_RULE} (non-blocking, informational)`));
|
|
44670
|
+
}
|
|
44671
|
+
ctx.stdout(line(`roundtrip: ${String(report.findings.length)} residue row(s); install-delta ${String(installDelta)} files`));
|
|
44672
|
+
}
|
|
44673
|
+
if (ctx.flags.record !== false) {
|
|
44674
|
+
await recordRoundtripProof(ctx, artifactSha256, report.findings.length, report.findings.length === 0 ? "clean" : "residue");
|
|
44675
|
+
}
|
|
44676
|
+
return exitCodeFromVerdict(verdict);
|
|
44677
|
+
} finally {
|
|
44678
|
+
rmSync12(baseDir, { recursive: true, force: true });
|
|
44679
|
+
}
|
|
44680
|
+
}
|
|
44681
|
+
function parsesRegistrySpec(raw) {
|
|
44682
|
+
try {
|
|
44683
|
+
parseScanSpec(raw);
|
|
44684
|
+
return true;
|
|
44685
|
+
} catch {
|
|
44686
|
+
return false;
|
|
44687
|
+
}
|
|
44688
|
+
}
|
|
44689
|
+
async function recordRoundtripProof(ctx, artifactSha256, rows, verdict) {
|
|
44690
|
+
const notice = (msg) => ctx.stderr(`roundtrip: ${msg}`);
|
|
44691
|
+
try {
|
|
44692
|
+
const env2 = ctx.env;
|
|
44693
|
+
const load = loadConfig({
|
|
44694
|
+
cwd: ctx.cwd,
|
|
44695
|
+
...ctx.flags.config !== void 0 ? { configPath: ctx.flags.config } : {},
|
|
44696
|
+
...env2 !== void 0 ? { env: env2 } : {}
|
|
44697
|
+
});
|
|
44698
|
+
let effective = null;
|
|
44699
|
+
if (load.kind === "loaded") effective = load;
|
|
44700
|
+
else if (load.explicit !== void 0) effective = { kind: "loaded", config: load.explicit.config, warnings: [], source: load.explicit.source };
|
|
44701
|
+
if (effective === null) {
|
|
44702
|
+
notice("no border.yaml here \u2014 proof NOT recorded");
|
|
44703
|
+
return;
|
|
44704
|
+
}
|
|
44705
|
+
const repoDir = resolveRepoDir(ctx.cwd, { ...env2 !== void 0 ? { env: env2 } : {} });
|
|
44706
|
+
const probe = await probeEngines(effective.config, { ...env2 !== void 0 ? { env: env2 } : {} });
|
|
44707
|
+
if (probe.degraded) {
|
|
44708
|
+
notice("engine probe degraded \u2014 cannot mint the rulesHash this proof must match; proof NOT recorded");
|
|
44709
|
+
return;
|
|
44710
|
+
}
|
|
44711
|
+
const rulesHash = await computeCheckRulesHash({
|
|
44712
|
+
engineVersions: probe.engineVersions,
|
|
44713
|
+
configDigest: computeConfigDigest(effective),
|
|
44714
|
+
...env2 !== void 0 ? { env: env2 } : {}
|
|
44715
|
+
});
|
|
44716
|
+
appendRecord(repoDir, buildRoundtripRecord({ artifactSha256, verdict, rulesHash, rows }));
|
|
44717
|
+
notice(`proof recorded for ${artifactSha256.slice(0, 12)}\u2026 (verdict ${verdict}, ${String(rows)} residue row(s))`);
|
|
44718
|
+
} catch (err) {
|
|
44719
|
+
notice(`proof NOT recorded (${err instanceof Error ? err.message : String(err)}) \u2014 'border check' with residue.requireProof will treat this artifact as unproven`);
|
|
44720
|
+
}
|
|
44721
|
+
}
|
|
44722
|
+
function dockerAvailableGuard(exec) {
|
|
44723
|
+
try {
|
|
44724
|
+
return exec(["--version"], DOCKER_PROBE_TIMEOUT_MS).status === 0;
|
|
44725
|
+
} catch {
|
|
44726
|
+
return false;
|
|
44727
|
+
}
|
|
44728
|
+
}
|
|
44729
|
+
|
|
44730
|
+
// src/commands/roundtrip.ts
|
|
44731
|
+
var runRoundtrip = (ctx) => runRoundtripCore(ctx);
|
|
44732
|
+
|
|
44733
|
+
// src/scan/scan.ts
|
|
44734
|
+
import { mkdtempSync as mkdtempSync6 } from "node:fs";
|
|
44735
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
44736
|
+
import { join as join28 } from "node:path";
|
|
44737
|
+
|
|
44738
|
+
// src/scan/config.ts
|
|
44739
|
+
function buildScanConfigYaml(eco) {
|
|
44740
|
+
return [
|
|
44741
|
+
"version: 1",
|
|
44742
|
+
"targets:",
|
|
44743
|
+
" git:",
|
|
44744
|
+
" remotes: []",
|
|
44745
|
+
` ${eco}: {}`,
|
|
44746
|
+
"rules:",
|
|
44747
|
+
" authors:",
|
|
44748
|
+
" emails: []",
|
|
44749
|
+
" names: []",
|
|
44750
|
+
" hosts: []",
|
|
44751
|
+
" ips: []",
|
|
44752
|
+
" pathPatterns: []",
|
|
44753
|
+
"residue:",
|
|
44754
|
+
" enabled: true",
|
|
44755
|
+
""
|
|
44756
|
+
].join("\n");
|
|
44757
|
+
}
|
|
44758
|
+
function synthesizeScanConfig(eco) {
|
|
44759
|
+
return parseConfig(buildScanConfigYaml(eco), "<border scan>");
|
|
44760
|
+
}
|
|
44761
|
+
|
|
43602
44762
|
// src/scan/materialize.ts
|
|
43603
|
-
import { existsSync as
|
|
43604
|
-
import { join as
|
|
44763
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync11, readdirSync as readdirSync10, readFileSync as readFileSync16, rmSync as rmSync13, writeFileSync as writeFileSync8 } from "node:fs";
|
|
44764
|
+
import { join as join27 } from "node:path";
|
|
43605
44765
|
var CRATES_ENVELOPE_FILES = ["Cargo.toml.orig", ".cargo-ok", ".cargo_vcs_info.json"];
|
|
43606
44766
|
var fail2 = (why) => {
|
|
43607
44767
|
throw new Error(`border scan cannot materialize the artifact (${why}) \u2014 refusing to stage an unusable tree (fail-closed)`);
|
|
@@ -43618,12 +44778,12 @@ function unwrapSingleDir(dir) {
|
|
|
43618
44778
|
const entries = readdirSync10(dir, { withFileTypes: true });
|
|
43619
44779
|
const dirs = entries.filter((e) => e.isDirectory());
|
|
43620
44780
|
if (entries.length === 1 && dirs.length === 1 && dirs[0] !== void 0) {
|
|
43621
|
-
return
|
|
44781
|
+
return join27(dir, dirs[0].name);
|
|
43622
44782
|
}
|
|
43623
44783
|
return dir;
|
|
43624
44784
|
}
|
|
43625
44785
|
function requireManifest(repoDir, ecosystem, candidates) {
|
|
43626
|
-
if (!candidates.some((rel) =>
|
|
44786
|
+
if (!candidates.some((rel) => existsSync18(join27(repoDir, rel)))) {
|
|
43627
44787
|
fail2(`${ecosystem} package tree has none of [${candidates.join(", ")}] at its root`);
|
|
43628
44788
|
}
|
|
43629
44789
|
}
|
|
@@ -43633,18 +44793,18 @@ function materializePackage(o) {
|
|
|
43633
44793
|
`);
|
|
43634
44794
|
});
|
|
43635
44795
|
const safe = safeArtifactName(o.filename, `${o.ecosystem}-artifact`);
|
|
43636
|
-
const archive =
|
|
43637
|
-
|
|
43638
|
-
const extractDir =
|
|
44796
|
+
const archive = join27(o.baseDir, safe);
|
|
44797
|
+
writeFileSync8(archive, o.bytes);
|
|
44798
|
+
const extractDir = join27(o.baseDir, "extract");
|
|
43639
44799
|
mkdirSync11(extractDir, { recursive: true });
|
|
43640
44800
|
extractFlat(archive, extractDir);
|
|
43641
44801
|
let repoDir = unwrapSingleDir(extractDir);
|
|
43642
44802
|
if (o.ecosystem === "rubygems") {
|
|
43643
44803
|
const outer = repoDir;
|
|
43644
44804
|
const dataCandidates = findNativeMissArchives(outer).filter((f) => f.endsWith("data.tar.gz"));
|
|
43645
|
-
const inner = dataCandidates[0] ??
|
|
43646
|
-
if (!
|
|
43647
|
-
const gemTree =
|
|
44805
|
+
const inner = dataCandidates[0] ?? join27(outer, "data.tar.gz");
|
|
44806
|
+
if (!existsSync18(inner)) fail2(".gem archive contains no data.tar.gz");
|
|
44807
|
+
const gemTree = join27(o.baseDir, "gem-tree");
|
|
43648
44808
|
mkdirSync11(gemTree, { recursive: true });
|
|
43649
44809
|
extractFlat(inner, gemTree);
|
|
43650
44810
|
repoDir = unwrapSingleDir(gemTree);
|
|
@@ -43655,10 +44815,10 @@ function materializePackage(o) {
|
|
|
43655
44815
|
} else {
|
|
43656
44816
|
requireManifest(repoDir, o.ecosystem, ["Cargo.toml"]);
|
|
43657
44817
|
for (const file2 of CRATES_ENVELOPE_FILES) {
|
|
43658
|
-
const path4 =
|
|
43659
|
-
if (!
|
|
44818
|
+
const path4 = join27(repoDir, file2);
|
|
44819
|
+
if (!existsSync18(path4)) continue;
|
|
43660
44820
|
const original = readFileSync16(path4);
|
|
43661
|
-
|
|
44821
|
+
rmSync13(path4, { force: true });
|
|
43662
44822
|
note(`border scan: crates envelope normalized: ${file2} sha256=${sha256Hex(original)}`);
|
|
43663
44823
|
}
|
|
43664
44824
|
}
|
|
@@ -43670,45 +44830,6 @@ function materializePackage(o) {
|
|
|
43670
44830
|
return repoDir;
|
|
43671
44831
|
}
|
|
43672
44832
|
|
|
43673
|
-
// src/scan/spec.ts
|
|
43674
|
-
var ECOSYSTEMS = ["npm", "pypi", "crates", "rubygems"];
|
|
43675
|
-
var SCAN_SPEC_USAGE = "usage: border scan <[ecosystem:]name@version> \u2014 e.g. border scan puppeteer@23.11.1 | pypi:requests@2.32.3 | crates:serde@1.0.219 | rubygems:rails@7.1.1";
|
|
43676
|
-
var PLAIN_NAME = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
|
|
43677
|
-
var SCOPED_NAME = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
43678
|
-
var VERSION = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
|
|
43679
|
-
var bad2 = (why) => {
|
|
43680
|
-
throw new UnknownArgError(`${why}; ${SCAN_SPEC_USAGE}`);
|
|
43681
|
-
};
|
|
43682
|
-
function parseScanSpec(raw) {
|
|
43683
|
-
const text = raw.trim();
|
|
43684
|
-
if (text.length === 0) return bad2("empty spec");
|
|
43685
|
-
let ecosystem = "npm";
|
|
43686
|
-
let rest = text;
|
|
43687
|
-
const colon = text.indexOf(":");
|
|
43688
|
-
if (colon >= 0) {
|
|
43689
|
-
const prefix = text.slice(0, colon);
|
|
43690
|
-
const found = ECOSYSTEMS.find((e) => e === prefix);
|
|
43691
|
-
if (found === void 0) {
|
|
43692
|
-
return bad2(`unknown ecosystem '${prefix}' \u2014 expected one of: ${ECOSYSTEMS.join(", ")}`);
|
|
43693
|
-
}
|
|
43694
|
-
ecosystem = found;
|
|
43695
|
-
rest = text.slice(colon + 1);
|
|
43696
|
-
if (rest.length === 0) return bad2("missing package name after the ecosystem prefix");
|
|
43697
|
-
}
|
|
43698
|
-
const scoped = rest.startsWith("@");
|
|
43699
|
-
const at = rest.lastIndexOf("@");
|
|
43700
|
-
if (at <= 0) return bad2(`spec '${raw}' is missing '@version'`);
|
|
43701
|
-
const name = rest.slice(0, at);
|
|
43702
|
-
const version2 = rest.slice(at + 1);
|
|
43703
|
-
if (!(scoped ? SCOPED_NAME : PLAIN_NAME).test(name)) {
|
|
43704
|
-
return bad2(`invalid package name '${name}'`);
|
|
43705
|
-
}
|
|
43706
|
-
if (!VERSION.test(version2)) {
|
|
43707
|
-
return bad2(`invalid version '${version2}' in spec '${raw}'`);
|
|
43708
|
-
}
|
|
43709
|
-
return { ecosystem, name, version: version2 };
|
|
43710
|
-
}
|
|
43711
|
-
|
|
43712
44833
|
// src/scan/scan.ts
|
|
43713
44834
|
async function runScanCore(ctx, deps = {}) {
|
|
43714
44835
|
const raw = ctx.positionals[0];
|
|
@@ -43724,7 +44845,7 @@ async function runScanCore(ctx, deps = {}) {
|
|
|
43724
44845
|
}
|
|
43725
44846
|
const cfg = synthesizeScanConfig(spec.ecosystem);
|
|
43726
44847
|
const fetcher = deps.fetcher ?? defaultScanFetcher;
|
|
43727
|
-
const baseDir =
|
|
44848
|
+
const baseDir = mkdtempSync6(join28(tmpdir4(), "border-scan-"));
|
|
43728
44849
|
try {
|
|
43729
44850
|
const { bytes, filename } = await fetchArtifact(spec, { fetcher });
|
|
43730
44851
|
const env2 = { ...ctx.env };
|
|
@@ -43848,11 +44969,12 @@ var handlers = {
|
|
|
43848
44969
|
status: runStatus,
|
|
43849
44970
|
"llm-request": runLlmRequest,
|
|
43850
44971
|
"llm-ingest": runLlmIngest,
|
|
43851
|
-
scan: runScan2
|
|
44972
|
+
scan: runScan2,
|
|
44973
|
+
roundtrip: runRoundtrip
|
|
43852
44974
|
};
|
|
43853
44975
|
|
|
43854
44976
|
// src/cli/types.ts
|
|
43855
|
-
var SUBCOMMANDS = ["check", "push", "status", "llm-request", "llm-ingest", "scan"];
|
|
44977
|
+
var SUBCOMMANDS = ["check", "push", "status", "llm-request", "llm-ingest", "scan", "roundtrip"];
|
|
43856
44978
|
|
|
43857
44979
|
// src/cli.ts
|
|
43858
44980
|
var VALID_TARGETS = targetIds();
|
|
@@ -43869,6 +44991,7 @@ function usage() {
|
|
|
43869
44991
|
" llm-request emit the provenance request for LLM-authored commits",
|
|
43870
44992
|
" llm-ingest record provenance captured by the harness",
|
|
43871
44993
|
" scan inspect a third-party package ([ecosystem:]name@version) for residue",
|
|
44994
|
+
" roundtrip install+uninstall a package (registry spec or local artifact file) in a throwaway container, report uninstall residue",
|
|
43872
44995
|
"",
|
|
43873
44996
|
"flags:",
|
|
43874
44997
|
" --config <path> config file (default: ./border.yaml, then git-remote fallback)",
|
|
@@ -43878,11 +45001,12 @@ function usage() {
|
|
|
43878
45001
|
" --require-engine <list> comma-separated engines that must be healthy (else exit 2)",
|
|
43879
45002
|
" --llm include the optional LLM review layer",
|
|
43880
45003
|
" --json machine-readable report on stdout",
|
|
45004
|
+
" --record / --no-record `border roundtrip` records a ledger proof by default; --no-record opts out",
|
|
43881
45005
|
" --help, -h this table",
|
|
43882
45006
|
"",
|
|
43883
45007
|
"exit codes: 0 pass (MEDIUM/INFO/LOW allowed) | 1 gate-blocked (CRITICAL/HIGH) or partial push | 2 config/tool error",
|
|
43884
45008
|
"",
|
|
43885
|
-
"subcommands: check, push, status, llm-request, llm-ingest, scan"
|
|
45009
|
+
"subcommands: check, push, status, llm-request, llm-ingest, scan, roundtrip"
|
|
43886
45010
|
].join("\n");
|
|
43887
45011
|
}
|
|
43888
45012
|
function isSubcommand(value) {
|
|
@@ -43913,10 +45037,15 @@ function parseFlags(argv) {
|
|
|
43913
45037
|
"require-engine": { type: "string" },
|
|
43914
45038
|
llm: { type: "boolean" },
|
|
43915
45039
|
json: { type: "boolean" },
|
|
45040
|
+
record: { type: "boolean" },
|
|
45041
|
+
"no-record": { type: "boolean" },
|
|
43916
45042
|
help: { type: "boolean" }
|
|
43917
45043
|
}
|
|
43918
45044
|
});
|
|
43919
45045
|
const { config: config2, targets, force, yes, llm, json: json2 } = values;
|
|
45046
|
+
if (values.record === true && values["no-record"] === true) {
|
|
45047
|
+
throw new UnknownArgError("--record and --no-record contradict each other");
|
|
45048
|
+
}
|
|
43920
45049
|
const requireEngine = values["require-engine"];
|
|
43921
45050
|
const flags = {
|
|
43922
45051
|
...config2 !== void 0 ? { config: config2 } : {},
|
|
@@ -43925,7 +45054,8 @@ function parseFlags(argv) {
|
|
|
43925
45054
|
yes: yes === true,
|
|
43926
45055
|
...requireEngine !== void 0 ? { requireEngine: splitList(requireEngine, { flag: "--require-engine", item: "engine" }) } : {},
|
|
43927
45056
|
llm: llm === true,
|
|
43928
|
-
json: json2 === true
|
|
45057
|
+
json: json2 === true,
|
|
45058
|
+
...values["no-record"] === true ? { record: false } : values.record === true ? { record: true } : {}
|
|
43929
45059
|
};
|
|
43930
45060
|
return { flags, positionals, help: values.help === true };
|
|
43931
45061
|
} catch (err) {
|