border-customs 0.4.0 → 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 +112 -12
- 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 +1269 -563
- 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,126 +38386,624 @@ 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
|
-
|
|
38410
|
-
|
|
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
|
+
};
|
|
38411
38438
|
}
|
|
38412
|
-
function
|
|
38413
|
-
|
|
38414
|
-
const bang = f.path.indexOf("!");
|
|
38415
|
-
if (bang > 0) {
|
|
38416
|
-
const rel2 = relative2(extractDir, f.path.slice(0, bang));
|
|
38417
|
-
if (!rel2.startsWith("..")) return { ...f, path: rel2 === "" ? f.path.slice(bang + 1) : `${rel2}${f.path.slice(bang)}` };
|
|
38418
|
-
}
|
|
38419
|
-
const rel = relative2(extractDir, f.path);
|
|
38420
|
-
return rel === "" || rel.startsWith("..") ? f : { ...f, path: rel };
|
|
38439
|
+
function bad(msg) {
|
|
38440
|
+
throw new Error(msg);
|
|
38421
38441
|
}
|
|
38422
|
-
function
|
|
38423
|
-
const
|
|
38424
|
-
|
|
38425
|
-
|
|
38426
|
-
|
|
38427
|
-
|
|
38428
|
-
|
|
38429
|
-
|
|
38430
|
-
|
|
38431
|
-
|
|
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`);
|
|
38432
38462
|
}
|
|
38433
|
-
return
|
|
38463
|
+
return o;
|
|
38434
38464
|
}
|
|
38435
|
-
|
|
38436
|
-
if (o.
|
|
38437
|
-
|
|
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) };
|
|
38473
|
+
});
|
|
38474
|
+
}
|
|
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
|
+
};
|
|
38438
38505
|
}
|
|
38439
|
-
|
|
38440
|
-
|
|
38441
|
-
|
|
38442
|
-
|
|
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
|
+
};
|
|
38443
38528
|
}
|
|
38444
|
-
|
|
38445
|
-
|
|
38446
|
-
|
|
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");
|
|
38447
38534
|
return {
|
|
38448
|
-
|
|
38449
|
-
|
|
38450
|
-
|
|
38451
|
-
|
|
38452
|
-
|
|
38453
|
-
|
|
38454
|
-
o.sanitizer
|
|
38455
|
-
)],
|
|
38456
|
-
artifact: null
|
|
38535
|
+
t: "roundtrip",
|
|
38536
|
+
artifactSha256: needHex(o, "artifactSha256", HEX64),
|
|
38537
|
+
verdict,
|
|
38538
|
+
rulesHash: needHex(o, "rulesHash", HEX64),
|
|
38539
|
+
rows,
|
|
38540
|
+
ts: needString(o, "ts")
|
|
38457
38541
|
};
|
|
38458
38542
|
}
|
|
38459
|
-
|
|
38460
|
-
|
|
38461
|
-
|
|
38462
|
-
|
|
38463
|
-
|
|
38464
|
-
|
|
38465
|
-
|
|
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
|
-
});
|
|
38470
|
-
const findings = [];
|
|
38471
|
-
const extractDir = join8(stateDir, "tmp", sha2563.slice(0, 8));
|
|
38472
|
-
rmSync3(extractDir, { recursive: true, force: true });
|
|
38473
|
-
mkdirSync3(extractDir, { recursive: true });
|
|
38543
|
+
return bad(`unknown record type ${JSON.stringify(t)}`);
|
|
38544
|
+
}
|
|
38545
|
+
function codeOf2(err) {
|
|
38546
|
+
return typeof err === "object" && err !== null ? err.code : void 0;
|
|
38547
|
+
}
|
|
38548
|
+
function readLedger(repoDir) {
|
|
38549
|
+
let text;
|
|
38474
38550
|
try {
|
|
38475
|
-
|
|
38476
|
-
|
|
38477
|
-
|
|
38478
|
-
|
|
38479
|
-
|
|
38480
|
-
|
|
38481
|
-
|
|
38482
|
-
|
|
38483
|
-
|
|
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`);
|
|
38484
38565
|
}
|
|
38485
|
-
|
|
38486
|
-
|
|
38487
|
-
|
|
38488
|
-
|
|
38489
|
-
|
|
38490
|
-
|
|
38491
|
-
|
|
38492
|
-
|
|
38493
|
-
|
|
38494
|
-
|
|
38495
|
-
|
|
38496
|
-
|
|
38497
|
-
|
|
38498
|
-
|
|
38499
|
-
|
|
38500
|
-
|
|
38501
|
-
|
|
38502
|
-
|
|
38503
|
-
|
|
38504
|
-
|
|
38505
|
-
|
|
38506
|
-
|
|
38507
|
-
|
|
38508
|
-
|
|
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;
|
|
38578
|
+
}
|
|
38579
|
+
return null;
|
|
38580
|
+
}
|
|
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;
|
|
38599
|
+
}
|
|
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
|
+
}
|
|
38625
|
+
}
|
|
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}`);
|
|
38694
|
+
return {
|
|
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
|
|
38703
|
+
};
|
|
38704
|
+
});
|
|
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 });
|
|
38963
|
+
mkdirSync3(extractDir, { recursive: true });
|
|
38964
|
+
try {
|
|
38965
|
+
extractArchive(tarballAbs, extractDir, {
|
|
38966
|
+
...o.env !== void 0 ? { env: o.env } : {},
|
|
38967
|
+
...o.timeoutMs !== void 0 ? { timeoutMs: o.timeoutMs } : {}
|
|
38968
|
+
});
|
|
38969
|
+
const relPaths = listFiles(extractDir).map((p) => relative2(extractDir, p));
|
|
38970
|
+
if (relPaths.length === 0) throw new EngineRunError("extracted artifact is empty \u2014 unusable npm pack output", null);
|
|
38971
|
+
const root = relPaths[0]?.split("/")[0] ?? "package";
|
|
38972
|
+
if (!relPaths.every((p) => p.startsWith(`${root}/`))) {
|
|
38973
|
+
throw new EngineRunError("extracted artifact mixes multiple top-level roots \u2014 unexpected tar layout, failing closed", null);
|
|
38974
|
+
}
|
|
38975
|
+
const packedManifestRel = `${root}/package.json`;
|
|
38976
|
+
if (!relPaths.includes(packedManifestRel)) throw new EngineRunError("extracted artifact has no package/package.json", null);
|
|
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);
|
|
38987
|
+
const residueCtx = { root, identity, ...o.sanitizer !== void 0 ? { sanitizer: o.sanitizer } : {} };
|
|
38988
|
+
for (const key of LIFECYCLE_SCRIPT_KEYS) {
|
|
38989
|
+
const cmd = packedManifest.scripts[key];
|
|
38990
|
+
if (cmd === void 0) continue;
|
|
38991
|
+
const scan = scanNpmLifecycleHook({ pkgDir, hook: cmd });
|
|
38992
|
+
if (scan.signatureId !== null) {
|
|
38993
|
+
findings.push(nativeFinding(
|
|
38994
|
+
RESIDUE_T1_RULE,
|
|
38995
|
+
"MEDIUM",
|
|
38996
|
+
`package.json script '${key}' matches closed safe signature '${scan.signatureId}' (T1: install-root writes only, frozen probe/fallback argv literals, zero network/out-of-tree/persistence/cross-manager hits) \u2014 allowed but audit-visible; any deviation keeps the blocking lifecycle-script CRITICAL`,
|
|
38997
|
+
packedManifestRel,
|
|
38998
|
+
`${identity}:residue:t1:${key}`,
|
|
38999
|
+
o.sanitizer
|
|
39000
|
+
));
|
|
39001
|
+
} else {
|
|
39002
|
+
findings.push(nativeFinding(
|
|
39003
|
+
NPM_LIFECYCLE_RULE,
|
|
39004
|
+
"CRITICAL",
|
|
39005
|
+
`package.json script '${key}' is a lifecycle hook \u2014 npm executes it on every consumer install (G33): ${tail(sanitizeOut(o.sanitizer, cmd))}`,
|
|
39006
|
+
packedManifestRel,
|
|
38509
39007
|
`${identity}:lifecycle:${key}`,
|
|
38510
39008
|
o.sanitizer
|
|
38511
39009
|
));
|
|
@@ -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,337 +40210,134 @@ 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
|
-
} finally {
|
|
39699
|
-
removeSandbox(extractDir);
|
|
39700
|
-
removeSandbox(dataDir);
|
|
39701
|
-
}
|
|
39702
|
-
return {
|
|
39703
|
-
findings,
|
|
39704
|
-
artifact: { file: `${BORDER_STATE_DIR}/dist/${gemName}`, sha256: sha2563, bytes: bytes.length }
|
|
39705
|
-
};
|
|
39706
|
-
}
|
|
39707
|
-
|
|
39708
|
-
// src/ledger/freshness.ts
|
|
39709
|
-
function artifactTargets() {
|
|
39710
|
-
return publishChannels().map((c) => c.id);
|
|
39711
|
-
}
|
|
39712
|
-
function sha256File(path4) {
|
|
39713
|
-
return createHash7("sha256").update(readFileSync7(path4)).digest("hex");
|
|
39714
|
-
}
|
|
39715
|
-
var packSeq = 0;
|
|
39716
|
-
function packNpmArtifacts(repoDir, o = {}) {
|
|
39717
|
-
packSeq += 1;
|
|
39718
|
-
const dest = join11(repoDir, BORDER_STATE_DIR, "tmp", `pack-${String(process.pid)}-${String(packSeq)}`);
|
|
39719
|
-
mkdirSync5(dest, { recursive: true });
|
|
39720
|
-
try {
|
|
39721
|
-
const r = spawnSync6("npm", ["pack", "--ignore-scripts", "--silent", `--pack-destination=${dest}`], {
|
|
39722
|
-
cwd: repoDir,
|
|
39723
|
-
encoding: "utf8",
|
|
39724
|
-
env: { ...o.env ?? process.env },
|
|
39725
|
-
timeout: 12e4
|
|
39726
|
-
});
|
|
39727
|
-
if (r.error !== void 0 || r.status !== 0) return null;
|
|
39728
|
-
const files = readdirSync4(dest).filter((f) => f.endsWith(".tgz")).sort();
|
|
39729
|
-
if (files.length === 0) return null;
|
|
39730
|
-
return files.map((f) => ({ file: f, sha256: sha256File(join11(dest, f)) }));
|
|
39731
|
-
} finally {
|
|
39732
|
-
rmSync5(dest, { recursive: true, force: true });
|
|
39733
|
-
}
|
|
39734
|
-
}
|
|
39735
|
-
function packCrateArtifacts(repoDir, o = {}) {
|
|
39736
|
-
packSeq += 1;
|
|
39737
|
-
const dest = join11(repoDir, BORDER_STATE_DIR, "tmp", `cargo-pack-${String(process.pid)}-${String(packSeq)}`);
|
|
39738
|
-
mkdirSync5(dest, { recursive: true });
|
|
39739
|
-
try {
|
|
39740
|
-
const r = spawnSync6("cargo", ["package", "--allow-dirty", "--no-verify"], {
|
|
39741
|
-
cwd: repoDir,
|
|
39742
|
-
encoding: "utf8",
|
|
39743
|
-
env: { ...o.env ?? process.env, CARGO_TARGET_DIR: dest },
|
|
39744
|
-
timeout: 12e4
|
|
39745
|
-
});
|
|
39746
|
-
if (r.error !== void 0 || r.status !== 0) return null;
|
|
39747
|
-
const dir = join11(dest, "package");
|
|
39748
|
-
const files = readdirSync4(dir).filter((f) => f.endsWith(".crate")).sort();
|
|
39749
|
-
if (files.length === 0) return null;
|
|
39750
|
-
return files.map((f) => ({ file: f, sha256: sha256File(join11(dir, f)) }));
|
|
39751
|
-
} finally {
|
|
39752
|
-
rmSync5(dest, { recursive: true, force: true });
|
|
39753
|
-
}
|
|
39754
|
-
}
|
|
39755
|
-
function packRubygemsArtifacts(repoDir, o = {}) {
|
|
39756
|
-
packSeq += 1;
|
|
39757
|
-
const ident = singleTreeGemspec(repoDir);
|
|
39758
|
-
if (ident === null) return null;
|
|
39759
|
-
const dest = join11(repoDir, BORDER_STATE_DIR, "tmp", `gem-pack-${String(process.pid)}-${String(packSeq)}`);
|
|
39760
|
-
mkdirSync5(dest, { recursive: true });
|
|
39761
|
-
try {
|
|
39762
|
-
const gemName = `${ident.name}-${ident.version}.gem`;
|
|
39763
|
-
const r = spawnSync6("gem", ["build", ident.path, "-o", join11(dest, gemName)], {
|
|
39764
|
-
cwd: repoDir,
|
|
39765
|
-
encoding: "utf8",
|
|
39766
|
-
env: { ...o.env ?? process.env },
|
|
39767
|
-
timeout: 12e4
|
|
39768
|
-
});
|
|
39769
|
-
if (r.error !== void 0 || r.status !== 0) return null;
|
|
39770
|
-
const files = readdirSync4(dest).filter((f) => f.endsWith(".gem")).sort();
|
|
39771
|
-
if (files.length === 0) return null;
|
|
39772
|
-
return files.map((f) => ({ file: f, sha256: sha256File(join11(dest, f)) }));
|
|
39773
|
-
} finally {
|
|
39774
|
-
rmSync5(dest, { recursive: true, force: true });
|
|
39775
|
-
}
|
|
39776
|
-
}
|
|
39777
|
-
var REPACKERS = {
|
|
39778
|
-
npm: packNpmArtifacts,
|
|
39779
|
-
crates: packCrateArtifacts,
|
|
39780
|
-
rubygems: packRubygemsArtifacts
|
|
39781
|
-
};
|
|
39782
|
-
function repackChannels() {
|
|
39783
|
-
return publishChannels().filter((c) => c.freshness === "repack").map((c) => c.id);
|
|
39784
|
-
}
|
|
39785
|
-
function sameArtifacts(a, b, extensions) {
|
|
39786
|
-
const digests = (xs) => xs.filter((x) => extensions.some((ext) => x.file.slice(x.file.lastIndexOf("/") + 1).endsWith(ext))).map((x) => `${x.file.slice(x.file.lastIndexOf("/") + 1)}\0${x.sha256}`).sort();
|
|
39787
|
-
const ka = digests(a);
|
|
39788
|
-
const kb = digests(b);
|
|
39789
|
-
return ka.length === kb.length && ka.every((x, i) => x === kb[i]);
|
|
39790
|
-
}
|
|
39791
|
-
function verifyArtifactFreshness(record2, ctx, repoDir, o = {}) {
|
|
39792
|
-
if (!record2.effectiveTargets.some((t) => artifactTargets().includes(t))) return true;
|
|
39793
|
-
if (ctx.dirty) return false;
|
|
39794
|
-
const needed = repackChannels().filter((t) => record2.effectiveTargets.includes(t));
|
|
39795
|
-
if (needed.length === 0) return true;
|
|
39796
|
-
if (record2.artifacts === null) return false;
|
|
39797
|
-
const now = [];
|
|
39798
|
-
for (const id of needed) {
|
|
39799
|
-
const pack = REPACKERS[id];
|
|
39800
|
-
if (pack === void 0) return false;
|
|
39801
|
-
const got = pack(repoDir, o);
|
|
39802
|
-
if (got === null) return false;
|
|
39803
|
-
now.push(...got);
|
|
39804
|
-
}
|
|
39805
|
-
return sameArtifacts(
|
|
39806
|
-
record2.artifacts,
|
|
39807
|
-
now,
|
|
39808
|
-
needed.flatMap((id) => {
|
|
39809
|
-
const channel = publishChannels().find((c) => c.id === id);
|
|
39810
|
-
return channel?.artifactExtensions ?? [];
|
|
39811
|
-
})
|
|
39812
|
-
);
|
|
39813
|
-
}
|
|
39814
|
-
|
|
39815
|
-
// src/ledger/records.ts
|
|
39816
|
-
import { appendFileSync, readFileSync as readFileSync8 } from "node:fs";
|
|
39817
|
-
import { join as join12 } from "node:path";
|
|
39818
|
-
var LEDGER_FILE = "ledger.jsonl";
|
|
39819
|
-
var RUNS_SUBDIR = "runs";
|
|
39820
|
-
var HEX64 = /^[0-9a-f]{64}$/;
|
|
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
|
-
};
|
|
39846
|
-
}
|
|
39847
|
-
function buildRoundtripRecord(i) {
|
|
39848
|
-
if (!HEX64.test(i.artifactSha256)) bad("artifactSha256 is not hex-shaped");
|
|
39849
|
-
if (!HEX64.test(i.rulesHash)) bad("rulesHash is not hex-shaped");
|
|
39850
|
-
if (i.verdict !== "clean" && i.verdict !== "residue") bad(`verdict must be clean or residue, got '${i.verdict}'`);
|
|
39851
|
-
if (!Number.isInteger(i.rows) || i.rows < 0) bad("rows must be a non-negative integer");
|
|
39852
|
-
return {
|
|
39853
|
-
t: "roundtrip",
|
|
39854
|
-
artifactSha256: i.artifactSha256,
|
|
39855
|
-
verdict: i.verdict,
|
|
39856
|
-
rulesHash: i.rulesHash,
|
|
39857
|
-
rows: i.rows,
|
|
39858
|
-
ts: i.ts ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
39859
|
-
};
|
|
39860
|
-
}
|
|
39861
|
-
function bad(msg) {
|
|
39862
|
-
throw new Error(msg);
|
|
39863
|
-
}
|
|
39864
|
-
function needString(o, field) {
|
|
39865
|
-
const v = o[field];
|
|
39866
|
-
if (typeof v !== "string" || v === "") bad(`${field} must be a non-empty string`);
|
|
39867
|
-
return v;
|
|
39868
|
-
}
|
|
39869
|
-
function needHex(o, field, re) {
|
|
39870
|
-
const v = needString(o, field);
|
|
39871
|
-
if (!re.test(v)) bad(`${field} is not hex-shaped`);
|
|
39872
|
-
return v;
|
|
39873
|
-
}
|
|
39874
|
-
function needStringArray(o, field) {
|
|
39875
|
-
const v = o[field];
|
|
39876
|
-
if (!Array.isArray(v) || !v.every((x) => typeof x === "string")) bad(`${field} must be an array of strings`);
|
|
39877
|
-
return v;
|
|
39878
|
-
}
|
|
39879
|
-
function parseCounts(value) {
|
|
39880
|
-
if (typeof value !== "object" || value === null) bad("counts must be an object");
|
|
39881
|
-
const o = value;
|
|
39882
|
-
for (const field of ["INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL", "total", "blocking", "warnings"]) {
|
|
39883
|
-
if (typeof o[field] !== "number" || !Number.isInteger(o[field])) bad(`counts.${field} must be an integer`);
|
|
39884
|
-
}
|
|
39885
|
-
return o;
|
|
39886
|
-
}
|
|
39887
|
-
function parseArtifacts(o) {
|
|
39888
|
-
if (o.artifacts === null) return null;
|
|
39889
|
-
const v = o.artifacts;
|
|
39890
|
-
if (!Array.isArray(v)) bad("artifacts must be null or an array");
|
|
39891
|
-
return v.map((item, i) => {
|
|
39892
|
-
if (typeof item !== "object" || item === null) bad(`artifacts[${String(i)}] must be an object`);
|
|
39893
|
-
const a = item;
|
|
39894
|
-
return { file: needString(a, "file"), sha256: needHex(a, "sha256", HEX64) };
|
|
39895
|
-
});
|
|
39896
|
-
}
|
|
39897
|
-
function parseLedgerRecord(value) {
|
|
39898
|
-
if (typeof value !== "object" || value === null) bad("record must be an object");
|
|
39899
|
-
const o = value;
|
|
39900
|
-
const t = o.t;
|
|
39901
|
-
if (t === "check") {
|
|
39902
|
-
const key = needHex(o, "key", HEX64);
|
|
39903
|
-
const key8 = needHex(o, "key8", HEX8);
|
|
39904
|
-
if (key.slice(0, 8) !== key8) bad("key8 must be the first 8 hex chars of key");
|
|
39905
|
-
const verdict = needString(o, "verdict");
|
|
39906
|
-
if (verdict !== "PASS" && verdict !== "FAIL") bad(`verdict must be PASS or FAIL, got '${verdict}'`);
|
|
39907
|
-
if (o.degraded !== false) bad("degraded must be false \u2014 degraded runs are never ledgered");
|
|
39908
|
-
if (typeof o.llm !== "boolean") bad("llm must be a boolean");
|
|
39909
|
-
return {
|
|
39910
|
-
t: "check",
|
|
39911
|
-
key,
|
|
39912
|
-
key8,
|
|
39913
|
-
head: needHex(o, "head", HEX40),
|
|
39914
|
-
dirtyDigest: needHex(o, "dirtyDigest", HEX64),
|
|
39915
|
-
refSetHash: needHex(o, "refSetHash", HEX64),
|
|
39916
|
-
exposureSet: needStringArray(o, "exposureSet"),
|
|
39917
|
-
effectiveTargets: needStringArray(o, "effectiveTargets"),
|
|
39918
|
-
rulesHash: needHex(o, "rulesHash", HEX64),
|
|
39919
|
-
artifacts: parseArtifacts(o),
|
|
39920
|
-
llm: o.llm,
|
|
39921
|
-
verdict,
|
|
39922
|
-
counts: parseCounts(o.counts),
|
|
39923
|
-
reportPath: needString(o, "reportPath"),
|
|
39924
|
-
degraded: false,
|
|
39925
|
-
ts: needString(o, "ts")
|
|
39926
|
-
};
|
|
39927
|
-
}
|
|
39928
|
-
if (t === "push") {
|
|
39929
|
-
const confirmedVia = needString(o, "confirmedVia");
|
|
39930
|
-
if (!PUSH_CONFIRMED_VIA.includes(confirmedVia)) {
|
|
39931
|
-
bad(`confirmedVia must be one of ${PUSH_CONFIRMED_VIA.join("|")}`);
|
|
39932
|
-
}
|
|
39933
|
-
const remoteSha = o.remoteSha;
|
|
39934
|
-
const version2 = o.version;
|
|
39935
|
-
if (remoteSha === void 0 && version2 === void 0) bad("push record needs remoteSha or version");
|
|
39936
|
-
if (remoteSha !== void 0 && typeof remoteSha !== "string") bad("remoteSha must be a string");
|
|
39937
|
-
if (version2 !== void 0 && typeof version2 !== "string") bad("version must be a string");
|
|
39938
|
-
return {
|
|
39939
|
-
t: "push",
|
|
39940
|
-
key: needHex(o, "key", HEX64),
|
|
39941
|
-
target: needString(o, "target"),
|
|
39942
|
-
remoteName: needString(o, "remoteName"),
|
|
39943
|
-
url: needString(o, "url"),
|
|
39944
|
-
localSha: needString(o, "localSha"),
|
|
39945
|
-
...remoteSha !== void 0 ? { remoteSha } : {},
|
|
39946
|
-
...version2 !== void 0 ? { version: version2 } : {},
|
|
39947
|
-
confirmedVia,
|
|
39948
|
-
ts: needString(o, "ts")
|
|
39949
|
-
};
|
|
39950
|
-
}
|
|
39951
|
-
if (t === "roundtrip") {
|
|
39952
|
-
const verdict = needString(o, "verdict");
|
|
39953
|
-
if (verdict !== "clean" && verdict !== "residue") bad(`verdict must be clean or residue, got '${verdict}'`);
|
|
39954
|
-
const rows = o.rows;
|
|
39955
|
-
if (typeof rows !== "number" || !Number.isInteger(rows) || rows < 0) bad("rows must be a non-negative integer");
|
|
39956
|
-
return {
|
|
39957
|
-
t: "roundtrip",
|
|
39958
|
-
artifactSha256: needHex(o, "artifactSha256", HEX64),
|
|
39959
|
-
verdict,
|
|
39960
|
-
rulesHash: needHex(o, "rulesHash", HEX64),
|
|
39961
|
-
rows,
|
|
39962
|
-
ts: needString(o, "ts")
|
|
39963
|
-
};
|
|
40221
|
+
} finally {
|
|
40222
|
+
removeSandbox(extractDir);
|
|
40223
|
+
removeSandbox(dataDir);
|
|
39964
40224
|
}
|
|
39965
|
-
return
|
|
40225
|
+
return {
|
|
40226
|
+
findings,
|
|
40227
|
+
artifact: { file: `${BORDER_STATE_DIR}/dist/${gemName}`, sha256: sha2563, bytes: bytes.length }
|
|
40228
|
+
};
|
|
39966
40229
|
}
|
|
39967
|
-
|
|
39968
|
-
|
|
40230
|
+
|
|
40231
|
+
// src/ledger/freshness.ts
|
|
40232
|
+
function artifactTargets() {
|
|
40233
|
+
return publishChannels().map((c) => c.id);
|
|
39969
40234
|
}
|
|
39970
|
-
function
|
|
39971
|
-
|
|
40235
|
+
function sha256File(path4) {
|
|
40236
|
+
return createHash7("sha256").update(readFileSync8(path4)).digest("hex");
|
|
40237
|
+
}
|
|
40238
|
+
var packSeq = 0;
|
|
40239
|
+
function packNpmArtifacts(repoDir, o = {}) {
|
|
40240
|
+
packSeq += 1;
|
|
40241
|
+
const dest = join13(repoDir, BORDER_STATE_DIR, "tmp", `pack-${String(process.pid)}-${String(packSeq)}`);
|
|
40242
|
+
mkdirSync5(dest, { recursive: true });
|
|
39972
40243
|
try {
|
|
39973
|
-
|
|
39974
|
-
|
|
39975
|
-
|
|
39976
|
-
|
|
40244
|
+
const r = spawnSync7("npm", ["pack", "--ignore-scripts", "--silent", `--pack-destination=${dest}`], {
|
|
40245
|
+
cwd: repoDir,
|
|
40246
|
+
encoding: "utf8",
|
|
40247
|
+
env: { ...o.env ?? process.env },
|
|
40248
|
+
timeout: 12e4
|
|
40249
|
+
});
|
|
40250
|
+
if (r.error !== void 0 || r.status !== 0) return null;
|
|
40251
|
+
const files = readdirSync4(dest).filter((f) => f.endsWith(".tgz")).sort();
|
|
40252
|
+
if (files.length === 0) return null;
|
|
40253
|
+
return files.map((f) => ({ file: f, sha256: sha256File(join13(dest, f)) }));
|
|
40254
|
+
} finally {
|
|
40255
|
+
rmSync5(dest, { recursive: true, force: true });
|
|
39977
40256
|
}
|
|
39978
|
-
const records = [];
|
|
39979
|
-
const warnings = [];
|
|
39980
|
-
text.split("\n").forEach((line, i) => {
|
|
39981
|
-
if (line.trim() === "") return;
|
|
39982
|
-
try {
|
|
39983
|
-
records.push(parseLedgerRecord(JSON.parse(line)));
|
|
39984
|
-
} catch (err) {
|
|
39985
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
39986
|
-
warnings.push(`ledger line ${String(i + 1)} unreadable (${reason.slice(0, 160)}) \u2014 line skipped`);
|
|
39987
|
-
}
|
|
39988
|
-
});
|
|
39989
|
-
return { records, warnings };
|
|
39990
40257
|
}
|
|
39991
|
-
function
|
|
39992
|
-
|
|
39993
|
-
|
|
39994
|
-
|
|
39995
|
-
|
|
39996
|
-
|
|
39997
|
-
|
|
39998
|
-
|
|
39999
|
-
|
|
40258
|
+
function packCrateArtifacts(repoDir, o = {}) {
|
|
40259
|
+
packSeq += 1;
|
|
40260
|
+
const dest = join13(repoDir, BORDER_STATE_DIR, "tmp", `cargo-pack-${String(process.pid)}-${String(packSeq)}`);
|
|
40261
|
+
mkdirSync5(dest, { recursive: true });
|
|
40262
|
+
try {
|
|
40263
|
+
const r = spawnSync7("cargo", ["package", "--allow-dirty", "--no-verify"], {
|
|
40264
|
+
cwd: repoDir,
|
|
40265
|
+
encoding: "utf8",
|
|
40266
|
+
env: { ...o.env ?? process.env, CARGO_TARGET_DIR: dest },
|
|
40267
|
+
timeout: 12e4
|
|
40268
|
+
});
|
|
40269
|
+
if (r.error !== void 0 || r.status !== 0) return null;
|
|
40270
|
+
const dir = join13(dest, "package");
|
|
40271
|
+
const files = readdirSync4(dir).filter((f) => f.endsWith(".crate")).sort();
|
|
40272
|
+
if (files.length === 0) return null;
|
|
40273
|
+
return files.map((f) => ({ file: f, sha256: sha256File(join13(dir, f)) }));
|
|
40274
|
+
} finally {
|
|
40275
|
+
rmSync5(dest, { recursive: true, force: true });
|
|
40000
40276
|
}
|
|
40001
|
-
return null;
|
|
40002
40277
|
}
|
|
40003
|
-
function
|
|
40004
|
-
|
|
40005
|
-
|
|
40006
|
-
|
|
40007
|
-
const
|
|
40008
|
-
|
|
40278
|
+
function packRubygemsArtifacts(repoDir, o = {}) {
|
|
40279
|
+
packSeq += 1;
|
|
40280
|
+
const ident = singleTreeGemspec(repoDir);
|
|
40281
|
+
if (ident === null) return null;
|
|
40282
|
+
const dest = join13(repoDir, BORDER_STATE_DIR, "tmp", `gem-pack-${String(process.pid)}-${String(packSeq)}`);
|
|
40283
|
+
mkdirSync5(dest, { recursive: true });
|
|
40284
|
+
try {
|
|
40285
|
+
const gemName = `${ident.name}-${ident.version}.gem`;
|
|
40286
|
+
const r = spawnSync7("gem", ["build", ident.path, "-o", join13(dest, gemName)], {
|
|
40287
|
+
cwd: repoDir,
|
|
40288
|
+
encoding: "utf8",
|
|
40289
|
+
env: { ...o.env ?? process.env },
|
|
40290
|
+
timeout: 12e4
|
|
40291
|
+
});
|
|
40292
|
+
if (r.error !== void 0 || r.status !== 0) return null;
|
|
40293
|
+
const files = readdirSync4(dest).filter((f) => f.endsWith(".gem")).sort();
|
|
40294
|
+
if (files.length === 0) return null;
|
|
40295
|
+
return files.map((f) => ({ file: f, sha256: sha256File(join13(dest, f)) }));
|
|
40296
|
+
} finally {
|
|
40297
|
+
rmSync5(dest, { recursive: true, force: true });
|
|
40298
|
+
}
|
|
40009
40299
|
}
|
|
40010
|
-
|
|
40011
|
-
|
|
40012
|
-
|
|
40300
|
+
var REPACKERS = {
|
|
40301
|
+
npm: packNpmArtifacts,
|
|
40302
|
+
crates: packCrateArtifacts,
|
|
40303
|
+
rubygems: packRubygemsArtifacts
|
|
40304
|
+
};
|
|
40305
|
+
function repackChannels() {
|
|
40306
|
+
return publishChannels().filter((c) => c.freshness === "repack").map((c) => c.id);
|
|
40013
40307
|
}
|
|
40014
|
-
function
|
|
40015
|
-
|
|
40308
|
+
function sameArtifacts(a, b, extensions) {
|
|
40309
|
+
const digests = (xs) => xs.filter((x) => extensions.some((ext) => x.file.slice(x.file.lastIndexOf("/") + 1).endsWith(ext))).map((x) => `${x.file.slice(x.file.lastIndexOf("/") + 1)}\0${x.sha256}`).sort();
|
|
40310
|
+
const ka = digests(a);
|
|
40311
|
+
const kb = digests(b);
|
|
40312
|
+
return ka.length === kb.length && ka.every((x, i) => x === kb[i]);
|
|
40016
40313
|
}
|
|
40017
|
-
function
|
|
40018
|
-
|
|
40019
|
-
|
|
40020
|
-
|
|
40314
|
+
function verifyArtifactFreshness(record2, ctx, repoDir, o = {}) {
|
|
40315
|
+
if (!record2.effectiveTargets.some((t) => artifactTargets().includes(t))) return true;
|
|
40316
|
+
if (ctx.dirty) return false;
|
|
40317
|
+
const needed = repackChannels().filter((t) => record2.effectiveTargets.includes(t));
|
|
40318
|
+
if (needed.length === 0) return true;
|
|
40319
|
+
if (record2.artifacts === null) return false;
|
|
40320
|
+
const now = [];
|
|
40321
|
+
for (const id of needed) {
|
|
40322
|
+
const pack = REPACKERS[id];
|
|
40323
|
+
if (pack === void 0) return false;
|
|
40324
|
+
const got = pack(repoDir, o);
|
|
40325
|
+
if (got === null) return false;
|
|
40326
|
+
now.push(...got);
|
|
40021
40327
|
}
|
|
40022
|
-
return
|
|
40328
|
+
return sameArtifacts(
|
|
40329
|
+
record2.artifacts,
|
|
40330
|
+
now,
|
|
40331
|
+
needed.flatMap((id) => {
|
|
40332
|
+
const channel = publishChannels().find((c) => c.id === id);
|
|
40333
|
+
return channel?.artifactExtensions ?? [];
|
|
40334
|
+
})
|
|
40335
|
+
);
|
|
40023
40336
|
}
|
|
40024
40337
|
|
|
40025
40338
|
// src/ledger/retention.ts
|
|
40026
40339
|
import { existsSync as existsSync9, mkdirSync as mkdirSync6, readdirSync as readdirSync5, rmSync as rmSync6 } from "node:fs";
|
|
40027
|
-
import { join as
|
|
40340
|
+
import { join as join14 } from "node:path";
|
|
40028
40341
|
var KEEP_RUN_DIRS_PER_KEY8 = 20;
|
|
40029
40342
|
function codeOf3(err) {
|
|
40030
40343
|
return typeof err === "object" && err !== null ? err.code : void 0;
|
|
@@ -40035,13 +40348,13 @@ function runDirName(key8, ts) {
|
|
|
40035
40348
|
function uniqueRunDirName(repoDir, key8, ts) {
|
|
40036
40349
|
const base = runDirName(key8, ts);
|
|
40037
40350
|
let name = base;
|
|
40038
|
-
for (let n = 2; existsSync9(
|
|
40351
|
+
for (let n = 2; existsSync9(join14(runsDir(repoDir), name)) && n < 1e3; n += 1) {
|
|
40039
40352
|
name = `${base}-r${String(n)}`;
|
|
40040
40353
|
}
|
|
40041
40354
|
return name;
|
|
40042
40355
|
}
|
|
40043
40356
|
function ensureRunDir(repoDir, name) {
|
|
40044
|
-
const dir =
|
|
40357
|
+
const dir = join14(runsDir(repoDir), name);
|
|
40045
40358
|
mkdirSync6(dir, { recursive: true });
|
|
40046
40359
|
return dir;
|
|
40047
40360
|
}
|
|
@@ -40060,7 +40373,7 @@ function pruneRunDirs(repoDir, key8, keep = KEEP_RUN_DIRS_PER_KEY8) {
|
|
|
40060
40373
|
const mine = entries.filter((e) => e.startsWith(prefix)).sort();
|
|
40061
40374
|
const doomed = mine.slice(0, Math.max(0, mine.length - keep));
|
|
40062
40375
|
for (const dir of doomed) {
|
|
40063
|
-
rmSync6(
|
|
40376
|
+
rmSync6(join14(runsDir(repoDir), dir), { recursive: true, force: true });
|
|
40064
40377
|
}
|
|
40065
40378
|
return doomed;
|
|
40066
40379
|
}
|
|
@@ -40126,9 +40439,9 @@ function recordCheckRun(i) {
|
|
|
40126
40439
|
const envOpt = i.env !== void 0 ? { env: i.env } : {};
|
|
40127
40440
|
const artifacts = i.artifacts ?? (i.report.verdict === "PASS" && !i.ctx.dirty && i.effectiveTargets.includes("npm") ? packNpmArtifacts(i.repoDir, envOpt) : null);
|
|
40128
40441
|
const persisted = artifacts !== null ? { ...i.report, artifacts } : i.report;
|
|
40129
|
-
writeFileSync3(
|
|
40442
|
+
writeFileSync3(join15(i.repoDir, BORDER_STATE_DIR, relReport), `${JSON.stringify(persisted, null, 2)}
|
|
40130
40443
|
`, "utf8");
|
|
40131
|
-
writeFileSync3(
|
|
40444
|
+
writeFileSync3(join15(i.repoDir, BORDER_STATE_DIR, relReport.replace("report.json", "report.md")), renderReportMd(i.report), "utf8");
|
|
40132
40445
|
const record2 = {
|
|
40133
40446
|
t: "check",
|
|
40134
40447
|
key: i.report.key,
|
|
@@ -40153,13 +40466,13 @@ function recordCheckRun(i) {
|
|
|
40153
40466
|
}
|
|
40154
40467
|
|
|
40155
40468
|
// src/channels/pypi.ts
|
|
40156
|
-
import { spawnSync as
|
|
40469
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
40157
40470
|
import { relative as relative5 } from "node:path";
|
|
40158
40471
|
|
|
40159
40472
|
// src/artifacts/pypi.ts
|
|
40160
40473
|
import { createHash as createHash9 } from "node:crypto";
|
|
40161
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";
|
|
40162
|
-
import { basename as
|
|
40475
|
+
import { basename as basename3, dirname as dirname7, join as join16, relative as relative4, resolve as resolve7 } from "node:path";
|
|
40163
40476
|
|
|
40164
40477
|
// src/artifacts/residuePy.ts
|
|
40165
40478
|
function residuePypiHits(pkgDir, innerRels) {
|
|
@@ -40223,13 +40536,13 @@ function requireModule(o, module) {
|
|
|
40223
40536
|
function buildPypiArtifacts(o) {
|
|
40224
40537
|
requireModule(o, "build");
|
|
40225
40538
|
const repoDir = resolve7(o.repoDir);
|
|
40226
|
-
if (!existsSync10(
|
|
40539
|
+
if (!existsSync10(join16(repoDir, "pyproject.toml"))) {
|
|
40227
40540
|
throw new PypiPrerequisiteError(`no pyproject.toml in the repo root \u2014 the PyPI pipeline builds from pyproject's own [build-system]`);
|
|
40228
40541
|
}
|
|
40229
|
-
const distDir =
|
|
40542
|
+
const distDir = join16(resolve7(o.stateDir ?? join16(repoDir, ".border")), "dist");
|
|
40230
40543
|
mkdirSync7(distDir, { recursive: true });
|
|
40231
40544
|
for (const name of readdirSync6(distDir)) {
|
|
40232
|
-
if (name.endsWith(".tar.gz") || name.endsWith(".whl")) rmSync7(
|
|
40545
|
+
if (name.endsWith(".tar.gz") || name.endsWith(".whl")) rmSync7(join16(distDir, name), { force: true });
|
|
40233
40546
|
}
|
|
40234
40547
|
const out = spawnEngine(binaryCandidates("python3", o), ["-m", "build", "--no-isolation", "--outdir", distDir, repoDir], o);
|
|
40235
40548
|
if (out.status !== 0) {
|
|
@@ -40240,13 +40553,13 @@ function buildPypiArtifacts(o) {
|
|
|
40240
40553
|
throw new EngineRunError(`\`python3 -m build\` produced no .tar.gz/.whl in ${distDir} (border exit 2)`, null);
|
|
40241
40554
|
}
|
|
40242
40555
|
const artifacts = names.map((name) => {
|
|
40243
|
-
const path4 =
|
|
40556
|
+
const path4 = join16(distDir, name);
|
|
40244
40557
|
return { path: path4, kind: name.endsWith(".whl") ? "wheel" : "sdist", sha256: createHash9("sha256").update(readFileSync9(path4)).digest("hex") };
|
|
40245
40558
|
});
|
|
40246
40559
|
return { artifacts, distDir };
|
|
40247
40560
|
}
|
|
40248
40561
|
function walkFilesRel(root) {
|
|
40249
|
-
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] : []);
|
|
40250
40563
|
}
|
|
40251
40564
|
function stripWrapper(entries) {
|
|
40252
40565
|
const tops = new Set(entries.map((e) => e.includes("/") ? e.slice(0, e.indexOf("/")) : "\0"));
|
|
@@ -40259,7 +40572,7 @@ function toInner(findingPath, scanRoot) {
|
|
|
40259
40572
|
const head = bang === -1 ? findingPath : findingPath.slice(0, bang);
|
|
40260
40573
|
const rest = bang === -1 ? "" : findingPath.slice(bang + 1);
|
|
40261
40574
|
const rel = relative4(scanRoot, head);
|
|
40262
|
-
const inner = rel !== "" && !rel.startsWith("..") ? rel :
|
|
40575
|
+
const inner = rel !== "" && !rel.startsWith("..") ? rel : basename3(head);
|
|
40263
40576
|
return rest === "" ? inner : `${inner}!${rest}`;
|
|
40264
40577
|
}
|
|
40265
40578
|
function gitTracked(o, repoDir) {
|
|
@@ -40276,7 +40589,7 @@ function toRe(globs) {
|
|
|
40276
40589
|
function explainMatchers(repoDir) {
|
|
40277
40590
|
const inc = [];
|
|
40278
40591
|
const exc = [];
|
|
40279
|
-
const manifest =
|
|
40592
|
+
const manifest = join16(repoDir, "MANIFEST.in");
|
|
40280
40593
|
if (existsSync10(manifest)) {
|
|
40281
40594
|
for (const line of readFileSync9(manifest, "utf8").split(/\r?\n/)) {
|
|
40282
40595
|
const t = line.split("#")[0]?.trim() ?? "";
|
|
@@ -40291,7 +40604,7 @@ function explainMatchers(repoDir) {
|
|
|
40291
40604
|
else if (cmd === "prune") exc.push(`${dir}/**`);
|
|
40292
40605
|
}
|
|
40293
40606
|
}
|
|
40294
|
-
const pyproject =
|
|
40607
|
+
const pyproject = join16(repoDir, "pyproject.toml");
|
|
40295
40608
|
if (existsSync10(pyproject)) {
|
|
40296
40609
|
const text = readFileSync9(pyproject, "utf8");
|
|
40297
40610
|
const readme = /^\s*readme\s*=\s*"([^"]+)"/m.exec(text);
|
|
@@ -40343,7 +40656,7 @@ ${out.stderr}`.replace(ANSI_RE, "");
|
|
|
40343
40656
|
}
|
|
40344
40657
|
return failed.map((m) => {
|
|
40345
40658
|
const abs = m[1] ?? "";
|
|
40346
|
-
const base =
|
|
40659
|
+
const base = basename3(abs);
|
|
40347
40660
|
const idx = text.indexOf(m[0]);
|
|
40348
40661
|
const next = text.indexOf("Checking ", idx + m[0].length);
|
|
40349
40662
|
const block = tail2(text.slice(idx, next === -1 ? void 0 : next), 400).replaceAll(abs, base);
|
|
@@ -40364,10 +40677,10 @@ async function scanPyPiArtifacts(o) {
|
|
|
40364
40677
|
requireModule(o, "build");
|
|
40365
40678
|
requireModule(o, "twine");
|
|
40366
40679
|
const repoDir = resolve7(o.repoDir);
|
|
40367
|
-
const stateDir = resolve7(o.stateDir ??
|
|
40680
|
+
const stateDir = resolve7(o.stateDir ?? join16(repoDir, ".border"));
|
|
40368
40681
|
const target = o.target ?? "artifact";
|
|
40369
40682
|
const { artifacts } = buildPypiArtifacts(o);
|
|
40370
|
-
const tmpParent =
|
|
40683
|
+
const tmpParent = join16(stateDir, "tmp");
|
|
40371
40684
|
mkdirSync7(tmpParent, { recursive: true });
|
|
40372
40685
|
const merged = /* @__PURE__ */ new Map();
|
|
40373
40686
|
const absorb = (f, path4) => {
|
|
@@ -40376,16 +40689,16 @@ async function scanPyPiArtifacts(o) {
|
|
|
40376
40689
|
};
|
|
40377
40690
|
try {
|
|
40378
40691
|
for (const artifact of artifacts) {
|
|
40379
|
-
const sandbox = mkdtempSync2(
|
|
40692
|
+
const sandbox = mkdtempSync2(join16(tmpParent, "pypi-"));
|
|
40380
40693
|
try {
|
|
40381
40694
|
if (artifact.kind === "wheel") {
|
|
40382
40695
|
const z2 = spawnEngine(binaryCandidates("python3", o), ["-m", "zipfile", "-e", artifact.path, sandbox], o);
|
|
40383
|
-
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);
|
|
40384
40697
|
} else {
|
|
40385
40698
|
extractArchive(artifact.path, sandbox, o);
|
|
40386
40699
|
}
|
|
40387
40700
|
const { root, rest } = artifact.kind === "sdist" ? stripWrapper(walkFilesRel(sandbox)) : { root: "", rest: walkFilesRel(sandbox) };
|
|
40388
|
-
const scanRoot =
|
|
40701
|
+
const scanRoot = join16(sandbox, root);
|
|
40389
40702
|
const scanOpts = { ...o, target };
|
|
40390
40703
|
if (o.skipGitleaks !== true) {
|
|
40391
40704
|
for (const f of scanTree({ ...scanOpts, dir: scanRoot, stateDir })) absorb(f, toInner(f.path, scanRoot));
|
|
@@ -40394,9 +40707,23 @@ async function scanPyPiArtifacts(o) {
|
|
|
40394
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 ?? "");
|
|
40395
40708
|
}
|
|
40396
40709
|
for (const f of residueFindings(residuePypiHits(scanRoot, rest), {
|
|
40397
|
-
root:
|
|
40710
|
+
root: basename3(artifact.path),
|
|
40398
40711
|
sep: "!",
|
|
40399
|
-
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),
|
|
40400
40727
|
...o.sanitizer !== void 0 ? { sanitizer: o.sanitizer } : {}
|
|
40401
40728
|
})) absorb(f, f.path ?? "");
|
|
40402
40729
|
if (artifact.kind === "sdist") for (const f of manifestFindings(o, rest, target)) absorb(f, f.path ?? "");
|
|
@@ -40418,7 +40745,7 @@ async function scanPyPiArtifacts(o) {
|
|
|
40418
40745
|
import { spawn } from "node:child_process";
|
|
40419
40746
|
import { createHash as createHash10 } from "node:crypto";
|
|
40420
40747
|
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "node:fs";
|
|
40421
|
-
import { basename as
|
|
40748
|
+
import { basename as basename4, join as join17 } from "node:path";
|
|
40422
40749
|
|
|
40423
40750
|
// src/cli/exit.ts
|
|
40424
40751
|
var EXIT_PASS = 0;
|
|
@@ -40459,7 +40786,7 @@ function translateError(err) {
|
|
|
40459
40786
|
|
|
40460
40787
|
// src/push/core.ts
|
|
40461
40788
|
var PUBLISH_WARNING = "WARNING: published versions cannot be re-uploaded unchanged; deletion windows differ per registry \u2014 verify version bump policy";
|
|
40462
|
-
var DIST_DIR =
|
|
40789
|
+
var DIST_DIR = join17(BORDER_STATE_DIR, "dist");
|
|
40463
40790
|
var defaultOut = (line) => void process.stdout.write(`${line}
|
|
40464
40791
|
`);
|
|
40465
40792
|
var defaultErr = (line) => void process.stderr.write(`${line}
|
|
@@ -40488,8 +40815,8 @@ function rehashRecordedArtifacts(kind, extensions, record2, repoDir) {
|
|
|
40488
40815
|
}
|
|
40489
40816
|
const files = [];
|
|
40490
40817
|
for (const a of entries) {
|
|
40491
|
-
const rel =
|
|
40492
|
-
const abs =
|
|
40818
|
+
const rel = join17(DIST_DIR, basename4(a.file));
|
|
40819
|
+
const abs = join17(repoDir, rel);
|
|
40493
40820
|
if (!existsSync11(abs)) return fail3(`${rel} is missing from the working tree`);
|
|
40494
40821
|
const digest2 = createHash10("sha256").update(readFileSync10(abs)).digest("hex");
|
|
40495
40822
|
if (digest2 !== a.sha256) return fail3(`${rel} no longer hashes to the digest recorded by the check`);
|
|
@@ -40665,7 +40992,7 @@ function twineAvailable(i, command) {
|
|
|
40665
40992
|
let status = null;
|
|
40666
40993
|
let failed = false;
|
|
40667
40994
|
try {
|
|
40668
|
-
const res =
|
|
40995
|
+
const res = spawnSync8(command.bin, args, {
|
|
40669
40996
|
encoding: "utf8",
|
|
40670
40997
|
stdio: "ignore",
|
|
40671
40998
|
...i.env === void 0 ? {} : { env: { ...i.env } }
|
|
@@ -40980,7 +41307,7 @@ function npmNameClaimed(o) {
|
|
|
40980
41307
|
return true;
|
|
40981
41308
|
}
|
|
40982
41309
|
function npmViewJson(args, registry2, timeoutMs, env2) {
|
|
40983
|
-
return new Promise((
|
|
41310
|
+
return new Promise((resolve16, reject) => {
|
|
40984
41311
|
const child = spawn2("npm", ["view", ...args, "--registry", registry2, "--fetch-retries=0", "--json"], {
|
|
40985
41312
|
stdio: ["ignore", "pipe", "pipe"],
|
|
40986
41313
|
...env2 === void 0 ? {} : { env: env2 }
|
|
@@ -40999,7 +41326,7 @@ function npmViewJson(args, registry2, timeoutMs, env2) {
|
|
|
40999
41326
|
});
|
|
41000
41327
|
child.on("close", (status) => {
|
|
41001
41328
|
clearTimeout(timer);
|
|
41002
|
-
|
|
41329
|
+
resolve16({ status: status ?? -1, stdout, stderr });
|
|
41003
41330
|
});
|
|
41004
41331
|
});
|
|
41005
41332
|
}
|
|
@@ -41095,14 +41422,14 @@ var npmChannel = {
|
|
|
41095
41422
|
// src/channels/crates.ts
|
|
41096
41423
|
import { createHash as createHash12 } from "node:crypto";
|
|
41097
41424
|
import { existsSync as existsSync13, mkdirSync as mkdirSync9, readFileSync as readFileSync12, readdirSync as readdirSync8, rmSync as rmSync9 } from "node:fs";
|
|
41098
|
-
import { join as
|
|
41099
|
-
import { spawnSync as
|
|
41425
|
+
import { join as join19, resolve as resolve8 } from "node:path";
|
|
41426
|
+
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
41100
41427
|
|
|
41101
41428
|
// src/artifacts/crates.ts
|
|
41102
41429
|
import { existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync11, readdirSync as readdirSync7, renameSync, rmSync as rmSync8 } from "node:fs";
|
|
41103
41430
|
import { createHash as createHash11 } from "node:crypto";
|
|
41104
|
-
import { join as
|
|
41105
|
-
import { spawnSync as
|
|
41431
|
+
import { join as join18, relative as relative6 } from "node:path";
|
|
41432
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
41106
41433
|
|
|
41107
41434
|
// src/artifacts/residueRust.ts
|
|
41108
41435
|
function residueCratesHits(pkgDir, innerRels) {
|
|
@@ -41301,7 +41628,7 @@ function listFiles3(dir) {
|
|
|
41301
41628
|
while (stack.length > 0) {
|
|
41302
41629
|
const cur = stack.pop();
|
|
41303
41630
|
for (const ent of readdirSync7(cur, { withFileTypes: true })) {
|
|
41304
|
-
const p =
|
|
41631
|
+
const p = join18(cur, ent.name);
|
|
41305
41632
|
if (ent.isDirectory()) stack.push(p);
|
|
41306
41633
|
else if (ent.isFile()) out.push(p);
|
|
41307
41634
|
}
|
|
@@ -41309,7 +41636,7 @@ function listFiles3(dir) {
|
|
|
41309
41636
|
return out;
|
|
41310
41637
|
}
|
|
41311
41638
|
function manifestIdentity(repoDir, cfg) {
|
|
41312
|
-
const manifestPath =
|
|
41639
|
+
const manifestPath = join18(repoDir, "Cargo.toml");
|
|
41313
41640
|
if (!existsSync12(manifestPath)) {
|
|
41314
41641
|
throw new EngineRunError("targets.crates is configured but the repo has no Cargo.toml to package", null);
|
|
41315
41642
|
}
|
|
@@ -41337,7 +41664,7 @@ async function runCargoArtifactStage(o) {
|
|
|
41337
41664
|
const bin = o.cargoBinPath ?? "cargo";
|
|
41338
41665
|
const identity = manifestIdentity(repoDir, o.cfg);
|
|
41339
41666
|
const crateName = `${identity.name}-${identity.version}.crate`;
|
|
41340
|
-
const versionCheck =
|
|
41667
|
+
const versionCheck = spawnSync9(bin, ["--version"], {
|
|
41341
41668
|
encoding: "utf8",
|
|
41342
41669
|
stdio: "ignore",
|
|
41343
41670
|
...o.env === void 0 ? {} : { env: { ...o.env } }
|
|
@@ -41348,14 +41675,14 @@ async function runCargoArtifactStage(o) {
|
|
|
41348
41675
|
);
|
|
41349
41676
|
}
|
|
41350
41677
|
ensureStateDir(repoDir);
|
|
41351
|
-
const stateDir =
|
|
41352
|
-
const distDir =
|
|
41353
|
-
const targetDir =
|
|
41678
|
+
const stateDir = join18(repoDir, BORDER_STATE_DIR);
|
|
41679
|
+
const distDir = join18(stateDir, "dist");
|
|
41680
|
+
const targetDir = join18(stateDir, "tmp", "cargo-target");
|
|
41354
41681
|
mkdirSync8(distDir, { recursive: true });
|
|
41355
41682
|
rmSync8(targetDir, { recursive: true, force: true });
|
|
41356
41683
|
mkdirSync8(targetDir, { recursive: true });
|
|
41357
41684
|
try {
|
|
41358
|
-
const result =
|
|
41685
|
+
const result = spawnSync9(bin, ["package", "--allow-dirty", "--no-verify"], {
|
|
41359
41686
|
cwd: repoDir,
|
|
41360
41687
|
encoding: "utf8",
|
|
41361
41688
|
timeout: o.cargoTimeoutMs ?? 12e4,
|
|
@@ -41370,17 +41697,17 @@ async function runCargoArtifactStage(o) {
|
|
|
41370
41697
|
result.status
|
|
41371
41698
|
);
|
|
41372
41699
|
}
|
|
41373
|
-
const packedDir =
|
|
41374
|
-
const packed =
|
|
41700
|
+
const packedDir = join18(targetDir, "package");
|
|
41701
|
+
const packed = join18(packedDir, crateName);
|
|
41375
41702
|
if (!existsSync12(packed)) {
|
|
41376
41703
|
throw new EngineRunError(`cargo package produced no ${crateName} in ${relative6(repoDir, packedDir)} \u2014 failing closed`, null);
|
|
41377
41704
|
}
|
|
41378
|
-
const dist =
|
|
41705
|
+
const dist = join18(distDir, crateName);
|
|
41379
41706
|
renameSync(packed, dist);
|
|
41380
41707
|
const bytes = readFileSync11(dist);
|
|
41381
41708
|
const sha2563 = createHash11("sha256").update(bytes).digest("hex");
|
|
41382
41709
|
const findings = [];
|
|
41383
|
-
const extractDir =
|
|
41710
|
+
const extractDir = join18(stateDir, "tmp", sha2563.slice(0, 8));
|
|
41384
41711
|
rmSync8(extractDir, { recursive: true, force: true });
|
|
41385
41712
|
mkdirSync8(extractDir, { recursive: true });
|
|
41386
41713
|
try {
|
|
@@ -41419,6 +41746,15 @@ async function runCargoArtifactStage(o) {
|
|
|
41419
41746
|
identity: crateName,
|
|
41420
41747
|
...o.sanitizer !== void 0 ? { sanitizer: o.sanitizer } : {}
|
|
41421
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
|
+
}));
|
|
41422
41758
|
} finally {
|
|
41423
41759
|
removeSandbox(extractDir);
|
|
41424
41760
|
}
|
|
@@ -41574,11 +41910,11 @@ function cratesPublishArgv(files, _cfg) {
|
|
|
41574
41910
|
return files.map(() => ["publish", "--allow-dirty", "--no-verify"]);
|
|
41575
41911
|
}
|
|
41576
41912
|
function repackageSameBytes(i, bin) {
|
|
41577
|
-
const targetDir =
|
|
41913
|
+
const targetDir = join19(resolve8(i.repoDir), BORDER_STATE_DIR, "tmp", "publish-repackage");
|
|
41578
41914
|
rmSync9(targetDir, { recursive: true, force: true });
|
|
41579
41915
|
mkdirSync9(targetDir, { recursive: true });
|
|
41580
41916
|
try {
|
|
41581
|
-
const r =
|
|
41917
|
+
const r = spawnSync10(bin, ["package", "--allow-dirty", "--no-verify"], {
|
|
41582
41918
|
cwd: i.repoDir,
|
|
41583
41919
|
encoding: "utf8",
|
|
41584
41920
|
timeout: 12e4,
|
|
@@ -41590,14 +41926,14 @@ function repackageSameBytes(i, bin) {
|
|
|
41590
41926
|
if (r.status !== 0) {
|
|
41591
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 };
|
|
41592
41928
|
}
|
|
41593
|
-
const packedDir =
|
|
41929
|
+
const packedDir = join19(targetDir, "package");
|
|
41594
41930
|
const crates = readdirSync8(packedDir).filter((f) => f.endsWith(".crate")).sort();
|
|
41595
41931
|
if (crates.length !== 1) {
|
|
41596
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 };
|
|
41597
41933
|
}
|
|
41598
41934
|
const crateFile = crates[0] ?? "";
|
|
41599
|
-
const freshSha = createHash12("sha256").update(readFileSync12(
|
|
41600
|
-
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);
|
|
41601
41937
|
if (!existsSync13(staged)) {
|
|
41602
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 };
|
|
41603
41939
|
}
|
|
@@ -42001,6 +42337,21 @@ function buildBorderConfigSchema() {
|
|
|
42001
42337
|
residue: external_exports.object({
|
|
42002
42338
|
enabled: external_exports.boolean().default(true),
|
|
42003
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([])
|
|
42004
42355
|
}).strict().optional()
|
|
42005
42356
|
}).strict();
|
|
42006
42357
|
}
|
|
@@ -42088,7 +42439,7 @@ function deepMergeRaw(base, overlay) {
|
|
|
42088
42439
|
return overlay;
|
|
42089
42440
|
}
|
|
42090
42441
|
function runGit3(cwd, args) {
|
|
42091
|
-
const r =
|
|
42442
|
+
const r = spawnSync11("git", args, { cwd, encoding: "utf8" });
|
|
42092
42443
|
return { ok: r.status === 0, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
|
|
42093
42444
|
}
|
|
42094
42445
|
function gitToplevel(cwd) {
|
|
@@ -42148,7 +42499,7 @@ function loadConfig(options = {}) {
|
|
|
42148
42499
|
throw new ConfigError("unreadable", `config file not found: ${basePath}`);
|
|
42149
42500
|
}
|
|
42150
42501
|
} else {
|
|
42151
|
-
for (const candidate of [
|
|
42502
|
+
for (const candidate of [join20(cwd, "border.yaml"), toplevel === void 0 ? "" : join20(toplevel, "border.yaml")]) {
|
|
42152
42503
|
if (candidate !== "" && existsSync14(candidate)) {
|
|
42153
42504
|
basePath = candidate;
|
|
42154
42505
|
baseText = readFileSync13(candidate, "utf8");
|
|
@@ -42163,7 +42514,7 @@ function loadConfig(options = {}) {
|
|
|
42163
42514
|
])];
|
|
42164
42515
|
let overlay;
|
|
42165
42516
|
for (const dir of overlayDirs) {
|
|
42166
|
-
const p =
|
|
42517
|
+
const p = join20(dir, ".border", "config.local.yaml");
|
|
42167
42518
|
const text = readFileOrUndefined(p);
|
|
42168
42519
|
if (text !== void 0) {
|
|
42169
42520
|
overlay = { path: p, doc: yamlToJson(text, p) };
|
|
@@ -42208,10 +42559,10 @@ function exposureSet(cfg, options = {}) {
|
|
|
42208
42559
|
}
|
|
42209
42560
|
|
|
42210
42561
|
// src/check.ts
|
|
42211
|
-
import { join as
|
|
42562
|
+
import { join as join22 } from "node:path";
|
|
42212
42563
|
|
|
42213
42564
|
// src/rules/aiArtifacts.ts
|
|
42214
|
-
import { spawnSync as
|
|
42565
|
+
import { spawnSync as spawnSync12 } from "node:child_process";
|
|
42215
42566
|
import { dirname as dirname9, resolve as resolve10 } from "node:path";
|
|
42216
42567
|
var AI_ARTIFACTS_ENGINE = "ai-artifacts";
|
|
42217
42568
|
var BINARY_SNIFF_BYTES = 8e3;
|
|
@@ -42230,7 +42581,7 @@ function makeCtx(o) {
|
|
|
42230
42581
|
return { repoDir, env: env2, timeoutMs: o.timeoutMs ?? 3e5, maxBufferBytes: o.maxBufferBytes ?? 32 * 1024 * 1024 };
|
|
42231
42582
|
}
|
|
42232
42583
|
function gitSpawn2(c, args, input2) {
|
|
42233
|
-
const r =
|
|
42584
|
+
const r = spawnSync12("git", ["-C", c.repoDir, ...args], {
|
|
42234
42585
|
encoding: "buffer",
|
|
42235
42586
|
timeout: c.timeoutMs,
|
|
42236
42587
|
maxBuffer: c.maxBufferBytes,
|
|
@@ -42490,10 +42841,10 @@ function proofFindings(o) {
|
|
|
42490
42841
|
}
|
|
42491
42842
|
|
|
42492
42843
|
// src/check/tagScan.ts
|
|
42493
|
-
import { spawnSync as
|
|
42844
|
+
import { spawnSync as spawnSync13 } from "node:child_process";
|
|
42494
42845
|
import { mkdtempSync as mkdtempSync3, rmSync as rmSync10, writeFileSync as writeFileSync4 } from "node:fs";
|
|
42495
42846
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
42496
|
-
import { join as
|
|
42847
|
+
import { join as join21, resolve as resolve12 } from "node:path";
|
|
42497
42848
|
var TAG_MESSAGE_RULE = "tag-message-secret";
|
|
42498
42849
|
function isEnoent2(error61) {
|
|
42499
42850
|
return typeof error61 === "object" && error61 !== null && error61.code === "ENOENT";
|
|
@@ -42513,7 +42864,7 @@ function stdinFindings(text, ignorePath, o) {
|
|
|
42513
42864
|
];
|
|
42514
42865
|
let lastError;
|
|
42515
42866
|
for (const bin of binaryCandidates("gitleaks", o)) {
|
|
42516
|
-
const r =
|
|
42867
|
+
const r = spawnSync13(bin, args, {
|
|
42517
42868
|
encoding: "utf8",
|
|
42518
42869
|
input: text,
|
|
42519
42870
|
timeout: o.timeoutMs ?? 3e5,
|
|
@@ -42584,8 +42935,8 @@ function scanTagMessages(o) {
|
|
|
42584
42935
|
const gitOpts = { ...o.env !== void 0 ? { env: o.env } : {} };
|
|
42585
42936
|
const refs = runGitChecked(repoDir, ["for-each-ref", "--format=%(refname)", "refs/tags"], gitOpts).split("\n").filter((ref) => ref !== "");
|
|
42586
42937
|
if (refs.length === 0) return [];
|
|
42587
|
-
const ignoreDir = mkdtempSync3(
|
|
42588
|
-
const ignorePath =
|
|
42938
|
+
const ignoreDir = mkdtempSync3(join21(tmpdir2(), "border-gitleaks-ignore-"));
|
|
42939
|
+
const ignorePath = join21(ignoreDir, ".gitleaksignore");
|
|
42589
42940
|
writeFileSync4(ignorePath, "");
|
|
42590
42941
|
try {
|
|
42591
42942
|
const findings = [];
|
|
@@ -42661,7 +43012,7 @@ async function runPipeline(o, ctx, lockWarning) {
|
|
|
42661
43012
|
if (!broken.has("gitleaks")) {
|
|
42662
43013
|
const eng = legOptions(o, sanitizer);
|
|
42663
43014
|
findings.push(...filterBorderStateFindings(scanGitHistory({ repoDir, refRange: historyRefRange(ctx), target: "git", ...eng }), repoDir));
|
|
42664
|
-
findings.push(...filterBorderStateFindings(scanTree({ dir: repoDir, stateDir:
|
|
43015
|
+
findings.push(...filterBorderStateFindings(scanTree({ dir: repoDir, stateDir: join22(repoDir, BORDER_STATE_DIR), target: "tree", ...eng }), repoDir));
|
|
42665
43016
|
findings.push(...filterBorderStateFindings(scanTagMessages({ repoDir, target: "git", ...eng }), repoDir));
|
|
42666
43017
|
}
|
|
42667
43018
|
if (!broken.has("secretlint")) {
|
|
@@ -42686,6 +43037,17 @@ async function runPipeline(o, ctx, lockWarning) {
|
|
|
42686
43037
|
findings.push(...stage.findings);
|
|
42687
43038
|
ledgerArtifacts.push(...stage.artifacts);
|
|
42688
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
|
+
}
|
|
42689
43051
|
findings.push(...await runRegistryProbes({ repoDir, cfg: o.cfg, effectiveTargets: o.effectiveTargets, ...envOpt }));
|
|
42690
43052
|
if (o.cfg.residue?.enabled === false) {
|
|
42691
43053
|
for (let i = findings.length - 1; i >= 0; i -= 1) {
|
|
@@ -42838,16 +43200,16 @@ var NO_TARGETS_PROBE_CFG = {
|
|
|
42838
43200
|
// src/llm/bundle.ts
|
|
42839
43201
|
import { createHash as createHash13 } from "node:crypto";
|
|
42840
43202
|
import { existsSync as existsSync15, mkdirSync as mkdirSync10, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "node:fs";
|
|
42841
|
-
import { dirname as dirname11, join as
|
|
43203
|
+
import { dirname as dirname11, join as join24 } from "node:path";
|
|
42842
43204
|
|
|
42843
43205
|
// src/llm/diff.ts
|
|
42844
|
-
import { spawnSync as
|
|
43206
|
+
import { spawnSync as spawnSync14 } from "node:child_process";
|
|
42845
43207
|
import { mkdtempSync as mkdtempSync4, rmSync as rmSync11, writeFileSync as writeFileSync5 } from "node:fs";
|
|
42846
|
-
import { dirname as dirname10, join as
|
|
43208
|
+
import { dirname as dirname10, join as join23, resolve as resolve13 } from "node:path";
|
|
42847
43209
|
var GIT_DIFF_MAX_BUFFER = 64 * 1024 * 1024;
|
|
42848
43210
|
function gitOut(repoDir, args, o, allowFail = false) {
|
|
42849
43211
|
const abs = resolve13(repoDir);
|
|
42850
|
-
const r =
|
|
43212
|
+
const r = spawnSync14("git", ["-C", abs, ...args], {
|
|
42851
43213
|
encoding: "utf8",
|
|
42852
43214
|
env: { ...o.env ?? process.env, GIT_CEILING_DIRECTORIES: dirname10(abs) },
|
|
42853
43215
|
timeout: 12e4,
|
|
@@ -42940,18 +43302,18 @@ function collectRawDeltas(repoDir, baseSha, headSha, o = {}) {
|
|
|
42940
43302
|
var MASK_CHUNK_CHARS = 2e6;
|
|
42941
43303
|
function maskViaEngineDetection(repoDir, texts, o = {}) {
|
|
42942
43304
|
const sanitizer = new TextSanitizer();
|
|
42943
|
-
const sandbox = mkdtempSync4(
|
|
43305
|
+
const sandbox = mkdtempSync4(join23(repoDir, BORDER_STATE_DIR, "llm-mask-"));
|
|
42944
43306
|
try {
|
|
42945
43307
|
let idx = 0;
|
|
42946
43308
|
for (const text of texts) {
|
|
42947
43309
|
if (text === "") continue;
|
|
42948
43310
|
for (let off = 0; off < text.length; off += MASK_CHUNK_CHARS, idx += 1) {
|
|
42949
|
-
writeFileSync5(
|
|
43311
|
+
writeFileSync5(join23(sandbox, `detect-${String(idx)}.txt`), text.slice(off, off + MASK_CHUNK_CHARS), "utf8");
|
|
42950
43312
|
}
|
|
42951
43313
|
}
|
|
42952
43314
|
scanTree({
|
|
42953
43315
|
dir: sandbox,
|
|
42954
|
-
stateDir:
|
|
43316
|
+
stateDir: join23(repoDir, BORDER_STATE_DIR),
|
|
42955
43317
|
target: "llm-mask",
|
|
42956
43318
|
sanitizer,
|
|
42957
43319
|
...engineEnv(o)
|
|
@@ -43009,7 +43371,7 @@ async function runLlmRequestCore(ctx) {
|
|
|
43009
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)");
|
|
43010
43372
|
return EXIT_ERROR;
|
|
43011
43373
|
}
|
|
43012
|
-
const absReport =
|
|
43374
|
+
const absReport = join24(repoDir, record2.reportPath);
|
|
43013
43375
|
if (!existsSync15(absReport)) {
|
|
43014
43376
|
ctx.stderr(`border: llm-request: report archive missing (${record2.reportPath} pruned?) \u2014 run 'border check' again`);
|
|
43015
43377
|
return EXIT_ERROR;
|
|
@@ -43068,7 +43430,7 @@ async function runLlmRequestCore(ctx) {
|
|
|
43068
43430
|
deterministic: { verdict: report.verdict, counts: report.counts, findings: report.findings },
|
|
43069
43431
|
promptTemplate: { path: templatePath, sha256: createHash13("sha256").update(readFileSync14(templatePath)).digest("hex") }
|
|
43070
43432
|
};
|
|
43071
|
-
const outPath =
|
|
43433
|
+
const outPath = join24(dirname11(absReport), "llm-request.json");
|
|
43072
43434
|
mkdirSync10(dirname11(outPath), { recursive: true });
|
|
43073
43435
|
writeFileSync6(outPath, `${JSON.stringify(bundle, null, 2)}
|
|
43074
43436
|
`, "utf8");
|
|
@@ -43080,7 +43442,7 @@ async function runLlmRequestCore(ctx) {
|
|
|
43080
43442
|
// src/llm/ingest.ts
|
|
43081
43443
|
import { createHash as createHash14 } from "node:crypto";
|
|
43082
43444
|
import { existsSync as existsSync16, readFileSync as readFileSync15, readdirSync as readdirSync9 } from "node:fs";
|
|
43083
|
-
import { isAbsolute, join as
|
|
43445
|
+
import { isAbsolute, join as join25, resolve as resolve14 } from "node:path";
|
|
43084
43446
|
var RULE_RE = /^[a-z0-9-]+$/;
|
|
43085
43447
|
var MAX_TEXT = 4e3;
|
|
43086
43448
|
var ALLOWED_KEYS = /* @__PURE__ */ new Set(["rule", "severity", "target", "path", "line", "commit", "message", "snippet", "engine", "valueDigest"]);
|
|
@@ -43099,7 +43461,7 @@ function findLatestBundle(repoDir) {
|
|
|
43099
43461
|
return "no llm-request bundle found \u2014 run 'border llm-request' first";
|
|
43100
43462
|
}
|
|
43101
43463
|
for (const d of dirs.sort().reverse()) {
|
|
43102
|
-
const p =
|
|
43464
|
+
const p = join25(runsDir(repoDir), d, "llm-request.json");
|
|
43103
43465
|
if (!existsSync16(p)) continue;
|
|
43104
43466
|
let parsed;
|
|
43105
43467
|
try {
|
|
@@ -43112,7 +43474,7 @@ function findLatestBundle(repoDir) {
|
|
|
43112
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) {
|
|
43113
43475
|
return `bundle ${p} fails its own schema \u2014 re-run 'border llm-request'`;
|
|
43114
43476
|
}
|
|
43115
|
-
return { dir:
|
|
43477
|
+
return { dir: join25(runsDir(repoDir), d), bundle: b };
|
|
43116
43478
|
}
|
|
43117
43479
|
return "no llm-request bundle found \u2014 run 'border llm-request' first";
|
|
43118
43480
|
}
|
|
@@ -43257,7 +43619,7 @@ async function runLlmRequest(ctx) {
|
|
|
43257
43619
|
}
|
|
43258
43620
|
|
|
43259
43621
|
// src/push/git.ts
|
|
43260
|
-
import { spawnSync as
|
|
43622
|
+
import { spawnSync as spawnSync15 } from "node:child_process";
|
|
43261
43623
|
var DIVERGED_MESSAGE = "history diverged; border never force-pushes; resolve manually";
|
|
43262
43624
|
function pushArgv(url2, branch, mode) {
|
|
43263
43625
|
return mode === "dry-run" ? ["push", "--dry-run", url2, branch, "--follow-tags"] : ["push", url2, branch, "--follow-tags"];
|
|
@@ -43292,7 +43654,7 @@ function lsRemoteMap(repoDir, remote, env2) {
|
|
|
43292
43654
|
return parseLsRemote(text, remote.target);
|
|
43293
43655
|
}
|
|
43294
43656
|
function isAncestor(repoDir, remoteSha, localSha, env2) {
|
|
43295
|
-
const r =
|
|
43657
|
+
const r = spawnSync15("git", ["-C", repoDir, "merge-base", "--is-ancestor", remoteSha, localSha], {
|
|
43296
43658
|
encoding: "utf8",
|
|
43297
43659
|
env: { ...env2, GIT_CEILING_DIRECTORIES: repoDir }
|
|
43298
43660
|
});
|
|
@@ -43327,7 +43689,7 @@ function pushTimeoutMs(env2) {
|
|
|
43327
43689
|
}
|
|
43328
43690
|
function executePush(i) {
|
|
43329
43691
|
const timeoutMs = pushTimeoutMs(i.env);
|
|
43330
|
-
const r =
|
|
43692
|
+
const r = spawnSync15("git", ["-C", i.repoDir, ...pushArgv(i.remote.url, i.branch, "execute")], {
|
|
43331
43693
|
stdio: "inherit",
|
|
43332
43694
|
env: i.env,
|
|
43333
43695
|
timeout: timeoutMs
|
|
@@ -43524,14 +43886,14 @@ async function executeYesPush(ctx, loaded) {
|
|
|
43524
43886
|
import { mkdtempSync as mkdtempSync5, rmSync as rmSync12, writeFileSync as writeFileSync7 } from "node:fs";
|
|
43525
43887
|
import { randomBytes } from "node:crypto";
|
|
43526
43888
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
43527
|
-
import { join as
|
|
43889
|
+
import { join as join26 } from "node:path";
|
|
43528
43890
|
|
|
43529
43891
|
// src/scan/fetch.ts
|
|
43530
43892
|
import { createHash as createHash15 } from "node:crypto";
|
|
43531
|
-
import { basename as
|
|
43893
|
+
import { basename as basename5 } from "node:path";
|
|
43532
43894
|
var SCAN_MAX_ARTIFACT_BYTES = 200 * 1024 * 1024;
|
|
43533
43895
|
var SCAN_REQUEST_TIMEOUT_MS = 6e4;
|
|
43534
|
-
var SCAN_USER_AGENT = "border-customs/0.4.
|
|
43896
|
+
var SCAN_USER_AGENT = "border-customs/0.4.1 (+https://github.com/sumteclab/border; border scan)";
|
|
43535
43897
|
var CRATES_DOWNLOAD_HOST = "https://static.crates.io";
|
|
43536
43898
|
var defaultScanFetcher = async (url2, init) => {
|
|
43537
43899
|
if (!/^https?:\/\//.test(url2)) throw new EngineRunError(`border scan refuses non-http(s) URL: ${url2}`, null);
|
|
@@ -43607,7 +43969,7 @@ function rubygemsDownloadUrl(name, version2, base = channelBaseUrl("rubygems"))
|
|
|
43607
43969
|
return `${base}/downloads/${name}-${version2}.gem`;
|
|
43608
43970
|
}
|
|
43609
43971
|
function safeArtifactName(raw, fallback) {
|
|
43610
|
-
const base =
|
|
43972
|
+
const base = basename5(raw.split("?")[0] ?? "").replace(/[^A-Za-z0-9._+-]/g, "_");
|
|
43611
43973
|
return base.length > 0 && base !== "." && base !== ".." ? base : fallback;
|
|
43612
43974
|
}
|
|
43613
43975
|
async function fetchArtifact(spec, o) {
|
|
@@ -43699,6 +44061,109 @@ function parseScanSpec(raw) {
|
|
|
43699
44061
|
return { ecosystem, name, version: version2 };
|
|
43700
44062
|
}
|
|
43701
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
|
+
|
|
43702
44167
|
// src/roundtrip/docker.ts
|
|
43703
44168
|
var DOCKER_PROBE_TIMEOUT_MS = 15e3;
|
|
43704
44169
|
var DOCKER_RUN_TIMEOUT_MS = 12e4;
|
|
@@ -43743,8 +44208,171 @@ function realDockerExec() {
|
|
|
43743
44208
|
};
|
|
43744
44209
|
}
|
|
43745
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
|
+
|
|
43746
44374
|
// src/roundtrip/manifest.ts
|
|
43747
|
-
import { createHash as
|
|
44375
|
+
import { createHash as createHash17 } from "node:crypto";
|
|
43748
44376
|
var RT_ENGINE = "roundtrip";
|
|
43749
44377
|
var RT_RESIDUE_RULE = "roundtrip-residue-file";
|
|
43750
44378
|
var RT_MODIFY_RULE = "roundtrip-modified-file";
|
|
@@ -43795,10 +44423,24 @@ var PERSISTENCE_PATTERNS = [
|
|
|
43795
44423
|
function isPersistenceSurface(path4) {
|
|
43796
44424
|
return PERSISTENCE_PATTERNS.some((re) => re.test(path4));
|
|
43797
44425
|
}
|
|
43798
|
-
var digestOf = (what) =>
|
|
43799
|
-
function classifyResidue(label, diff) {
|
|
44426
|
+
var digestOf = (what) => createHash17("sha256").update(what).digest("hex");
|
|
44427
|
+
function classifyResidue(label, diff, attribution = null) {
|
|
43800
44428
|
const findings = [];
|
|
43801
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
|
+
}
|
|
43802
44444
|
findings.push({
|
|
43803
44445
|
rule: RT_RESIDUE_RULE,
|
|
43804
44446
|
severity: "HIGH",
|
|
@@ -43889,7 +44531,46 @@ function execChecked(exec, args, timeoutMs, what) {
|
|
|
43889
44531
|
}
|
|
43890
44532
|
return r;
|
|
43891
44533
|
}
|
|
43892
|
-
function
|
|
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) {
|
|
43893
44574
|
execChecked(exec, ["run", "-d", "--name", container, profile.image, "sleep", String(CONTAINER_TTL_SEC)], DOCKER_RUN_TIMEOUT_MS, "container start");
|
|
43894
44575
|
try {
|
|
43895
44576
|
execChecked(exec, ["cp", hostPath, `${container}:${profile.artifactPath}`], DOCKER_CP_TIMEOUT_MS, "artifact copy");
|
|
@@ -43906,8 +44587,9 @@ function runPipeline2(exec, profile, spec, label, hostPath, container) {
|
|
|
43906
44587
|
}
|
|
43907
44588
|
execChecked(exec, ["exec", container, ...profile.uninstall(spec)], PHASE_TIMEOUT_MS, `uninstall ${label}`);
|
|
43908
44589
|
const m3 = parseManifest(takeSnapshot(exec, container, profile.excludes));
|
|
44590
|
+
const attribution = closure !== null ? scanDepClaims(exec, container, closure, normalizePipName(spec.name), note) : null;
|
|
43909
44591
|
return {
|
|
43910
|
-
findings: classifyResidue(label, diffManifests(m1, m3)),
|
|
44592
|
+
findings: classifyResidue(label, diffManifests(m1, m3), attribution),
|
|
43911
44593
|
installDelta,
|
|
43912
44594
|
baselineDigest: sha256Hex([...m1.keys()].join("\n"))
|
|
43913
44595
|
};
|
|
@@ -43930,24 +44612,29 @@ async function runRoundtripCore(ctx, deps = {}) {
|
|
|
43930
44612
|
const raw = ctx.positionals[0];
|
|
43931
44613
|
if (ctx.positionals.length !== 1 || raw === void 0) {
|
|
43932
44614
|
throw new UnknownArgError(
|
|
43933
|
-
`border roundtrip expects exactly one
|
|
44615
|
+
`border roundtrip expects exactly one argument \u2014 a registry spec or a local artifact file (got ${String(ctx.positionals.length)}); ${SCAN_SPEC_USAGE}`
|
|
43934
44616
|
);
|
|
43935
44617
|
}
|
|
43936
|
-
const
|
|
43937
|
-
const
|
|
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;
|
|
43938
44622
|
const label = `${spec.ecosystem}:${spec.name}@${spec.version}`;
|
|
43939
44623
|
const exec = deps.exec ?? realDockerExec();
|
|
43940
44624
|
if (!dockerAvailableGuard(exec)) {
|
|
43941
44625
|
ctx.stderr("roundtrip: docker unavailable \u2014 cannot verify");
|
|
43942
44626
|
return EXIT_ERROR;
|
|
43943
44627
|
}
|
|
43944
|
-
const baseDir = mkdtempSync5(
|
|
44628
|
+
const baseDir = mkdtempSync5(join26(tmpdir3(), "border-roundtrip-"));
|
|
43945
44629
|
try {
|
|
43946
|
-
const
|
|
43947
|
-
const
|
|
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");
|
|
43948
44634
|
writeFileSync7(hostPath, bytes);
|
|
43949
44635
|
const container = containerNameFor(label);
|
|
43950
|
-
const
|
|
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));
|
|
43951
44638
|
const verdict = computeVerdict(findings);
|
|
43952
44639
|
const report = {
|
|
43953
44640
|
schemaVersion: 1,
|
|
@@ -43960,6 +44647,10 @@ async function runRoundtripCore(ctx, deps = {}) {
|
|
|
43960
44647
|
verdict,
|
|
43961
44648
|
counts: countFindings(findings),
|
|
43962
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 } : {},
|
|
43963
44654
|
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
43964
44655
|
};
|
|
43965
44656
|
const sanitizer = new TextSanitizer();
|
|
@@ -43967,19 +44658,34 @@ async function runRoundtripCore(ctx, deps = {}) {
|
|
|
43967
44658
|
if (ctx.flags.json) {
|
|
43968
44659
|
ctx.stdout(renderReportJson(report));
|
|
43969
44660
|
} else {
|
|
44661
|
+
if (local !== null) {
|
|
44662
|
+
ctx.stdout(line(`roundtrip: source ${local.source} artifact sha256 ${artifactSha256}`));
|
|
44663
|
+
}
|
|
43970
44664
|
for (const f of report.findings) {
|
|
43971
44665
|
ctx.stdout(line(` ${f.severity} ${f.rule} [${f.engine}] ${f.path ?? f.target} ${f.message}`));
|
|
43972
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
|
+
}
|
|
43973
44671
|
ctx.stdout(line(`roundtrip: ${String(report.findings.length)} residue row(s); install-delta ${String(installDelta)} files`));
|
|
43974
44672
|
}
|
|
43975
44673
|
if (ctx.flags.record !== false) {
|
|
43976
|
-
await recordRoundtripProof(ctx,
|
|
44674
|
+
await recordRoundtripProof(ctx, artifactSha256, report.findings.length, report.findings.length === 0 ? "clean" : "residue");
|
|
43977
44675
|
}
|
|
43978
44676
|
return exitCodeFromVerdict(verdict);
|
|
43979
44677
|
} finally {
|
|
43980
44678
|
rmSync12(baseDir, { recursive: true, force: true });
|
|
43981
44679
|
}
|
|
43982
44680
|
}
|
|
44681
|
+
function parsesRegistrySpec(raw) {
|
|
44682
|
+
try {
|
|
44683
|
+
parseScanSpec(raw);
|
|
44684
|
+
return true;
|
|
44685
|
+
} catch {
|
|
44686
|
+
return false;
|
|
44687
|
+
}
|
|
44688
|
+
}
|
|
43983
44689
|
async function recordRoundtripProof(ctx, artifactSha256, rows, verdict) {
|
|
43984
44690
|
const notice = (msg) => ctx.stderr(`roundtrip: ${msg}`);
|
|
43985
44691
|
try {
|
|
@@ -44027,7 +44733,7 @@ var runRoundtrip = (ctx) => runRoundtripCore(ctx);
|
|
|
44027
44733
|
// src/scan/scan.ts
|
|
44028
44734
|
import { mkdtempSync as mkdtempSync6 } from "node:fs";
|
|
44029
44735
|
import { tmpdir as tmpdir4 } from "node:os";
|
|
44030
|
-
import { join as
|
|
44736
|
+
import { join as join28 } from "node:path";
|
|
44031
44737
|
|
|
44032
44738
|
// src/scan/config.ts
|
|
44033
44739
|
function buildScanConfigYaml(eco) {
|
|
@@ -44054,8 +44760,8 @@ function synthesizeScanConfig(eco) {
|
|
|
44054
44760
|
}
|
|
44055
44761
|
|
|
44056
44762
|
// src/scan/materialize.ts
|
|
44057
|
-
import { existsSync as
|
|
44058
|
-
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";
|
|
44059
44765
|
var CRATES_ENVELOPE_FILES = ["Cargo.toml.orig", ".cargo-ok", ".cargo_vcs_info.json"];
|
|
44060
44766
|
var fail2 = (why) => {
|
|
44061
44767
|
throw new Error(`border scan cannot materialize the artifact (${why}) \u2014 refusing to stage an unusable tree (fail-closed)`);
|
|
@@ -44072,12 +44778,12 @@ function unwrapSingleDir(dir) {
|
|
|
44072
44778
|
const entries = readdirSync10(dir, { withFileTypes: true });
|
|
44073
44779
|
const dirs = entries.filter((e) => e.isDirectory());
|
|
44074
44780
|
if (entries.length === 1 && dirs.length === 1 && dirs[0] !== void 0) {
|
|
44075
|
-
return
|
|
44781
|
+
return join27(dir, dirs[0].name);
|
|
44076
44782
|
}
|
|
44077
44783
|
return dir;
|
|
44078
44784
|
}
|
|
44079
44785
|
function requireManifest(repoDir, ecosystem, candidates) {
|
|
44080
|
-
if (!candidates.some((rel) =>
|
|
44786
|
+
if (!candidates.some((rel) => existsSync18(join27(repoDir, rel)))) {
|
|
44081
44787
|
fail2(`${ecosystem} package tree has none of [${candidates.join(", ")}] at its root`);
|
|
44082
44788
|
}
|
|
44083
44789
|
}
|
|
@@ -44087,18 +44793,18 @@ function materializePackage(o) {
|
|
|
44087
44793
|
`);
|
|
44088
44794
|
});
|
|
44089
44795
|
const safe = safeArtifactName(o.filename, `${o.ecosystem}-artifact`);
|
|
44090
|
-
const archive =
|
|
44796
|
+
const archive = join27(o.baseDir, safe);
|
|
44091
44797
|
writeFileSync8(archive, o.bytes);
|
|
44092
|
-
const extractDir =
|
|
44798
|
+
const extractDir = join27(o.baseDir, "extract");
|
|
44093
44799
|
mkdirSync11(extractDir, { recursive: true });
|
|
44094
44800
|
extractFlat(archive, extractDir);
|
|
44095
44801
|
let repoDir = unwrapSingleDir(extractDir);
|
|
44096
44802
|
if (o.ecosystem === "rubygems") {
|
|
44097
44803
|
const outer = repoDir;
|
|
44098
44804
|
const dataCandidates = findNativeMissArchives(outer).filter((f) => f.endsWith("data.tar.gz"));
|
|
44099
|
-
const inner = dataCandidates[0] ??
|
|
44100
|
-
if (!
|
|
44101
|
-
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");
|
|
44102
44808
|
mkdirSync11(gemTree, { recursive: true });
|
|
44103
44809
|
extractFlat(inner, gemTree);
|
|
44104
44810
|
repoDir = unwrapSingleDir(gemTree);
|
|
@@ -44109,8 +44815,8 @@ function materializePackage(o) {
|
|
|
44109
44815
|
} else {
|
|
44110
44816
|
requireManifest(repoDir, o.ecosystem, ["Cargo.toml"]);
|
|
44111
44817
|
for (const file2 of CRATES_ENVELOPE_FILES) {
|
|
44112
|
-
const path4 =
|
|
44113
|
-
if (!
|
|
44818
|
+
const path4 = join27(repoDir, file2);
|
|
44819
|
+
if (!existsSync18(path4)) continue;
|
|
44114
44820
|
const original = readFileSync16(path4);
|
|
44115
44821
|
rmSync13(path4, { force: true });
|
|
44116
44822
|
note(`border scan: crates envelope normalized: ${file2} sha256=${sha256Hex(original)}`);
|
|
@@ -44139,7 +44845,7 @@ async function runScanCore(ctx, deps = {}) {
|
|
|
44139
44845
|
}
|
|
44140
44846
|
const cfg = synthesizeScanConfig(spec.ecosystem);
|
|
44141
44847
|
const fetcher = deps.fetcher ?? defaultScanFetcher;
|
|
44142
|
-
const baseDir = mkdtempSync6(
|
|
44848
|
+
const baseDir = mkdtempSync6(join28(tmpdir4(), "border-scan-"));
|
|
44143
44849
|
try {
|
|
44144
44850
|
const { bytes, filename } = await fetchArtifact(spec, { fetcher });
|
|
44145
44851
|
const env2 = { ...ctx.env };
|
|
@@ -44285,7 +44991,7 @@ function usage() {
|
|
|
44285
44991
|
" llm-request emit the provenance request for LLM-authored commits",
|
|
44286
44992
|
" llm-ingest record provenance captured by the harness",
|
|
44287
44993
|
" scan inspect a third-party package ([ecosystem:]name@version) for residue",
|
|
44288
|
-
" roundtrip install+uninstall a package in a throwaway container, report uninstall residue",
|
|
44994
|
+
" roundtrip install+uninstall a package (registry spec or local artifact file) in a throwaway container, report uninstall residue",
|
|
44289
44995
|
"",
|
|
44290
44996
|
"flags:",
|
|
44291
44997
|
" --config <path> config file (default: ./border.yaml, then git-remote fallback)",
|