border-customs 0.5.1 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -6
- package/dist/assets/exfil-src/rules.ts +179 -0
- package/dist/assets/exfil-src/rulesPath.ts +112 -0
- package/dist/assets/exfil-src/scan.ts +146 -0
- package/dist/assets/exfil-src/severity.ts +156 -0
- package/dist/assets/gitleaks-defaults-v8.30.1.toml +50 -0
- package/dist/assets/package-lock.json +2 -2
- package/dist/index.js +1026 -116
- package/index.ts +7 -0
- package/package.json +3 -2
- package/plugin/border.ts +169 -58
package/dist/index.js
CHANGED
|
@@ -33124,8 +33124,8 @@ var createMaskValue = (value) => {
|
|
|
33124
33124
|
};
|
|
33125
33125
|
var maskWithValues = (str, maskedValues) => {
|
|
33126
33126
|
let result = str;
|
|
33127
|
-
for (const
|
|
33128
|
-
result = result.replaceAll(
|
|
33127
|
+
for (const maskValue2 of maskedValues) {
|
|
33128
|
+
result = result.replaceAll(maskValue2, "*".repeat(maskValue2.length));
|
|
33129
33129
|
}
|
|
33130
33130
|
return result;
|
|
33131
33131
|
};
|
|
@@ -37387,6 +37387,158 @@ var creator4 = {
|
|
|
37387
37387
|
}
|
|
37388
37388
|
};
|
|
37389
37389
|
|
|
37390
|
+
// src/exfil/rulesPath.ts
|
|
37391
|
+
var HOME_POSIX_RE = /\/home\/([A-Za-z0-9._-]+)/g;
|
|
37392
|
+
var HOME_WINDOWS_RE = /([A-Za-z]:[\\/]+Users[\\/]+)([A-Za-z0-9._-]+)/gi;
|
|
37393
|
+
function matchHomePath(text, _o) {
|
|
37394
|
+
const hits = [];
|
|
37395
|
+
text.split("\n").forEach((body, idx) => {
|
|
37396
|
+
for (const m of body.matchAll(HOME_POSIX_RE)) {
|
|
37397
|
+
hits.push({ rule: "exfil-home-path", matched: m[0] ?? "", line: idx + 1, message: "Home-directory path exposing a local username (privacy leak \u2014 T5 calibration graded this rule MEDIUM per lead ruling)." });
|
|
37398
|
+
}
|
|
37399
|
+
for (const m of body.matchAll(HOME_WINDOWS_RE)) {
|
|
37400
|
+
hits.push({ rule: "exfil-home-path", matched: m[0] ?? "", line: idx + 1, message: "Windows user-profile path exposing a local username (privacy leak \u2014 T5 calibration graded this rule MEDIUM per lead ruling)." });
|
|
37401
|
+
}
|
|
37402
|
+
});
|
|
37403
|
+
return hits;
|
|
37404
|
+
}
|
|
37405
|
+
var PYPIRC_RE = /~\/\.pypirc/g;
|
|
37406
|
+
var SSHPASS_RE = /\bSSHPASS\b/g;
|
|
37407
|
+
var ENVFILE_RE = /[A-Za-z0-9_.~/-]*\.env(?![A-Za-z0-9_./-])/g;
|
|
37408
|
+
function envTokenIsLocation(tok) {
|
|
37409
|
+
const stem = tok.slice(0, -".env".length);
|
|
37410
|
+
if (stem === "") return true;
|
|
37411
|
+
if (/[\/~]/.test(stem)) return true;
|
|
37412
|
+
const parts = stem.split(".");
|
|
37413
|
+
const receiver = parts[parts.length - 1] ?? "";
|
|
37414
|
+
if (receiver === "process" || receiver === "os") return false;
|
|
37415
|
+
return receiver.length >= 3;
|
|
37416
|
+
}
|
|
37417
|
+
function matchCredLocation(text, _o) {
|
|
37418
|
+
const hits = [];
|
|
37419
|
+
const push = (matched, line) => {
|
|
37420
|
+
hits.push({ rule: "exfil-cred-location", matched, line, message: "Reference to a credential-bearing location (env file / pypirc / SSHPASS); the location itself, value not required to fire." });
|
|
37421
|
+
};
|
|
37422
|
+
text.split("\n").forEach((body, idx) => {
|
|
37423
|
+
for (const m of body.matchAll(PYPIRC_RE)) push(m[0] ?? "", idx + 1);
|
|
37424
|
+
for (const m of body.matchAll(SSHPASS_RE)) push(m[0] ?? "", idx + 1);
|
|
37425
|
+
for (const m of body.matchAll(ENVFILE_RE)) {
|
|
37426
|
+
const tok = m[0] ?? "";
|
|
37427
|
+
if ((body[(m.index ?? 0) - 1] ?? "") === "?") continue;
|
|
37428
|
+
if (envTokenIsLocation(tok)) push(tok, idx + 1);
|
|
37429
|
+
}
|
|
37430
|
+
});
|
|
37431
|
+
return hits;
|
|
37432
|
+
}
|
|
37433
|
+
var PROFILE_SIGNALS = [
|
|
37434
|
+
{ name: "os+version", re: /\b(?:Ubuntu|Debian|Raspbian|CentOS|Rocky Linux|AlmaLinux|Amazon Linux|Red Hat Enterprise Linux|RHEL|Windows Server|SUSE Linux Enterprise|Fedora)\s+v?[0-9][0-9.]*/i },
|
|
37435
|
+
{ name: "no-docker", re: /\bno docker\b|\bdocker\b[^\n]{0,24}?\bnot (?:installed|present|available|running)\b|\bwithout docker\b|\bno container (?:runtime|engine)\b/i },
|
|
37436
|
+
{ name: "sudo-pattern", re: /\bpasswordless sudo\b|\bnopasswd\b|\bsudo\b[^\n]{0,24}?\b(?:without|needs?|requires?) (?:a )?password\b|\b(?:needs?|requires?) (?:a )?password\b[^\n]{0,12}?\bsudo\b/i }
|
|
37437
|
+
];
|
|
37438
|
+
function matchHostProfile(text, _o) {
|
|
37439
|
+
const hits = [];
|
|
37440
|
+
let paraLines = [];
|
|
37441
|
+
let paraStart = 1;
|
|
37442
|
+
const flush = () => {
|
|
37443
|
+
if (paraLines.length === 0) return;
|
|
37444
|
+
const para = paraLines.join("\n");
|
|
37445
|
+
const found = PROFILE_SIGNALS.filter((s) => s.re.test(para)).map((s) => s.name);
|
|
37446
|
+
if (found.length >= 2) {
|
|
37447
|
+
hits.push({ rule: "exfil-host-profile", matched: found.join("+"), line: paraStart, message: `Host configuration fingerprint: ${String(found.length)} profile signals (os+version / no-docker / sudo-pattern) clustered in one paragraph.` });
|
|
37448
|
+
}
|
|
37449
|
+
paraLines = [];
|
|
37450
|
+
};
|
|
37451
|
+
text.split("\n").forEach((body, idx) => {
|
|
37452
|
+
if (body.trim() === "") {
|
|
37453
|
+
flush();
|
|
37454
|
+
return;
|
|
37455
|
+
}
|
|
37456
|
+
if (paraLines.length === 0) paraStart = idx + 1;
|
|
37457
|
+
paraLines.push(body);
|
|
37458
|
+
});
|
|
37459
|
+
flush();
|
|
37460
|
+
return hits;
|
|
37461
|
+
}
|
|
37462
|
+
|
|
37463
|
+
// src/exfil/rules.ts
|
|
37464
|
+
var OCTET = "(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])";
|
|
37465
|
+
var IPV4_SRC = `(?<![0-9A-Za-z.\\-])${OCTET}\\.${OCTET}\\.${OCTET}\\.${OCTET}(?![0-9A-Za-z.\\-])`;
|
|
37466
|
+
var DEFAULT_EXEMPT_BANDS = ["127", "192.0.2", "198.51.100", "203.0.113", "10.200"];
|
|
37467
|
+
function bandOctets(band) {
|
|
37468
|
+
return band.split(".").map(Number);
|
|
37469
|
+
}
|
|
37470
|
+
function inAnyBand(oct, bands) {
|
|
37471
|
+
return bands.some((b) => {
|
|
37472
|
+
const parts = bandOctets(b);
|
|
37473
|
+
return parts.every((v, i) => oct[i] === v);
|
|
37474
|
+
});
|
|
37475
|
+
}
|
|
37476
|
+
function inRfc1918(o) {
|
|
37477
|
+
const [a = 0, b = 0] = o;
|
|
37478
|
+
if (a === 10) return true;
|
|
37479
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
37480
|
+
return a === 192 && b === 168;
|
|
37481
|
+
}
|
|
37482
|
+
function octetsOf(ip) {
|
|
37483
|
+
return ip.split(".").map(Number);
|
|
37484
|
+
}
|
|
37485
|
+
function isIpLiteral(host) {
|
|
37486
|
+
return /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(host);
|
|
37487
|
+
}
|
|
37488
|
+
var OCTET_GRP = `(?:${OCTET})`;
|
|
37489
|
+
var RFC1918_FULL = `10\\.${OCTET_GRP}\\.${OCTET_GRP}\\.${OCTET_GRP}|172\\.(?:1[6-9]|2[0-9]|3[01])\\.${OCTET_GRP}\\.${OCTET_GRP}|192\\.168\\.${OCTET_GRP}\\.${OCTET_GRP}`;
|
|
37490
|
+
var SUFFIX_HOST = `[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*\\.(?:internal|lan|local|corp|intra)`;
|
|
37491
|
+
var BAND_NEGATIVE = `(?!127\\.|10\\.200\\.|192\\.0\\.2\\.|198\\.51\\.100\\.|203\\.0\\.113\\.)`;
|
|
37492
|
+
var EXFIL_TWIN_PATTERNS = [
|
|
37493
|
+
{ name: "exfil-rfc1918", source: `(?<![0-9A-Za-z.\\-])${BAND_NEGATIVE}(?:${RFC1918_FULL})(?![0-9A-Za-z.\\-])` },
|
|
37494
|
+
{ name: "exfil-ssh-target", source: `(?<![A-Za-z0-9._+%-])[A-Za-z0-9._+%-]+@${BAND_NEGATIVE}(?:${RFC1918_FULL}|${SUFFIX_HOST})(?![A-Za-z0-9._-])` }
|
|
37495
|
+
];
|
|
37496
|
+
function matchRfc1918(text, o) {
|
|
37497
|
+
const re = new RegExp(IPV4_SRC, "g");
|
|
37498
|
+
const bands = o.exemptBands ?? DEFAULT_EXEMPT_BANDS;
|
|
37499
|
+
const hits = [];
|
|
37500
|
+
text.split("\n").forEach((body, idx) => {
|
|
37501
|
+
for (const m of body.matchAll(re)) {
|
|
37502
|
+
const ip = m[0] ?? "";
|
|
37503
|
+
const oct = octetsOf(ip);
|
|
37504
|
+
if (!inRfc1918(oct)) continue;
|
|
37505
|
+
if (o.facet !== "message" && inAnyBand(oct, bands)) continue;
|
|
37506
|
+
hits.push({ rule: "exfil-rfc1918", matched: ip, line: idx + 1, message: "RFC1918 private-network address literal on a surface about to become public." });
|
|
37507
|
+
}
|
|
37508
|
+
});
|
|
37509
|
+
return hits;
|
|
37510
|
+
}
|
|
37511
|
+
var PRIVATE_SUFFIXES = [".internal", ".lan", ".local", ".corp", ".intra"];
|
|
37512
|
+
var RFC2606_SUFFIXES = [".example", ".test", ".invalid", ".localhost"];
|
|
37513
|
+
var SSH_TOKEN_RE = /(?<![A-Za-z0-9._+%-])[A-Za-z0-9._+%-]+@([A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?)/g;
|
|
37514
|
+
function hasDotSuffix(host, suffixes) {
|
|
37515
|
+
return suffixes.some((s) => host.length > s.length && host.endsWith(s));
|
|
37516
|
+
}
|
|
37517
|
+
function matchSshTarget(text, o) {
|
|
37518
|
+
const hosts = (o.hosts ?? []).map((h) => h.trim().toLowerCase()).filter((h) => h !== "");
|
|
37519
|
+
const bands = o.exemptBands ?? DEFAULT_EXEMPT_BANDS;
|
|
37520
|
+
const hits = [];
|
|
37521
|
+
text.split("\n").forEach((body, idx) => {
|
|
37522
|
+
for (const m of body.matchAll(SSH_TOKEN_RE)) {
|
|
37523
|
+
const host = (m[1] ?? "").toLowerCase();
|
|
37524
|
+
if (host === "") continue;
|
|
37525
|
+
const isIp = isIpLiteral(host);
|
|
37526
|
+
let fired = false;
|
|
37527
|
+
if (isIp ? inRfc1918(octetsOf(host)) && !(o.facet !== "message" && inAnyBand(octetsOf(host), bands)) : false) {
|
|
37528
|
+
fired = true;
|
|
37529
|
+
} else if (!isIp && hosts.includes(host)) {
|
|
37530
|
+
fired = true;
|
|
37531
|
+
} else if (!isIp && !hasDotSuffix(host, RFC2606_SUFFIXES) && hasDotSuffix(host, PRIVATE_SUFFIXES)) {
|
|
37532
|
+
fired = true;
|
|
37533
|
+
}
|
|
37534
|
+
if (fired) {
|
|
37535
|
+
hits.push({ rule: "exfil-ssh-target", matched: `${m[0]}`, line: idx + 1, message: "SSH-style user@host naming an internal-only target (RFC1918 literal, configured host, or private suffix)." });
|
|
37536
|
+
}
|
|
37537
|
+
}
|
|
37538
|
+
});
|
|
37539
|
+
return hits;
|
|
37540
|
+
}
|
|
37541
|
+
|
|
37390
37542
|
// src/findings.ts
|
|
37391
37543
|
var SEVERITIES = ["INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL"];
|
|
37392
37544
|
function isBlocking(severity) {
|
|
@@ -37491,6 +37643,7 @@ function generatePatterns(rules2) {
|
|
|
37491
37643
|
for (const ip of rules2?.ips ?? []) optionPatterns.push(makePattern(`internal-ip:${ip}`, ip, true, compiled));
|
|
37492
37644
|
for (const pp of rules2?.pathPatterns ?? []) optionPatterns.push(makePattern(`path-pattern:${pp}`, pp, true, compiled));
|
|
37493
37645
|
for (const dflt of DEFAULT_PATH_PATTERNS) optionPatterns.push(makePattern(`path-pattern:${dflt}`, dflt, false, compiled));
|
|
37646
|
+
for (const twin of EXFIL_TWIN_PATTERNS) optionPatterns.push(makePattern(twin.name, twin.source, false, compiled));
|
|
37494
37647
|
return { optionPatterns, compiled };
|
|
37495
37648
|
}
|
|
37496
37649
|
var AWS_OVERRIDE = [{ id: AWS_RULE_ID, options: { enableIDScanRule: true } }];
|
|
@@ -38399,6 +38552,7 @@ var HEX64 = /^[0-9a-f]{64}$/;
|
|
|
38399
38552
|
var HEX40 = /^[0-9a-f]{40}$/;
|
|
38400
38553
|
var HEX8 = /^[0-9a-f]{8}$/;
|
|
38401
38554
|
var PUSH_CONFIRMED_VIA = ["ls-remote", "npm-view", "pypi-json", "crates-json", "rubygems-json"];
|
|
38555
|
+
var LANDING_VERDICTS = ["clean", "blocked"];
|
|
38402
38556
|
function ledgerPath(repoDir) {
|
|
38403
38557
|
return join8(repoDir, BORDER_STATE_DIR, LEDGER_FILE);
|
|
38404
38558
|
}
|
|
@@ -38439,6 +38593,26 @@ function buildRoundtripRecord(i) {
|
|
|
38439
38593
|
function bad(msg) {
|
|
38440
38594
|
throw new Error(msg);
|
|
38441
38595
|
}
|
|
38596
|
+
function buildLandingRecord(i) {
|
|
38597
|
+
if (!HEX64.test(i.key)) bad("key is not hex-shaped");
|
|
38598
|
+
if (!HEX64.test(i.rulesHash)) bad("rulesHash is not hex-shaped");
|
|
38599
|
+
if (!HEX40.test(i.remoteSha)) bad("remoteSha is not hex-shaped");
|
|
38600
|
+
if (i.target === "") bad("target must be non-empty");
|
|
38601
|
+
if (i.ref === "") bad("ref must be non-empty");
|
|
38602
|
+
if (!LANDING_VERDICTS.includes(i.verdict)) bad(`verdict must be one of ${LANDING_VERDICTS.join("|")}, got '${i.verdict}'`);
|
|
38603
|
+
if (!Number.isInteger(i.blocking) || i.blocking < 0) bad("blocking must be a non-negative integer");
|
|
38604
|
+
return {
|
|
38605
|
+
t: "landing",
|
|
38606
|
+
key: i.key,
|
|
38607
|
+
target: i.target,
|
|
38608
|
+
ref: i.ref,
|
|
38609
|
+
remoteSha: i.remoteSha,
|
|
38610
|
+
verdict: i.verdict,
|
|
38611
|
+
blocking: i.blocking,
|
|
38612
|
+
rulesHash: i.rulesHash,
|
|
38613
|
+
ts: i.ts ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
38614
|
+
};
|
|
38615
|
+
}
|
|
38442
38616
|
function needString(o, field) {
|
|
38443
38617
|
const v = o[field];
|
|
38444
38618
|
if (typeof v !== "string" || v === "") bad(`${field} must be a non-empty string`);
|
|
@@ -38540,6 +38714,23 @@ function parseLedgerRecord(value) {
|
|
|
38540
38714
|
ts: needString(o, "ts")
|
|
38541
38715
|
};
|
|
38542
38716
|
}
|
|
38717
|
+
if (t === "landing") {
|
|
38718
|
+
const verdict = needString(o, "verdict");
|
|
38719
|
+
if (!LANDING_VERDICTS.includes(verdict)) bad(`verdict must be one of ${LANDING_VERDICTS.join("|")}, got '${verdict}'`);
|
|
38720
|
+
const blocking = o.blocking;
|
|
38721
|
+
if (typeof blocking !== "number" || !Number.isInteger(blocking) || blocking < 0) bad("blocking must be a non-negative integer");
|
|
38722
|
+
return {
|
|
38723
|
+
t: "landing",
|
|
38724
|
+
key: needHex(o, "key", HEX64),
|
|
38725
|
+
target: needString(o, "target"),
|
|
38726
|
+
ref: needString(o, "ref"),
|
|
38727
|
+
remoteSha: needHex(o, "remoteSha", HEX40),
|
|
38728
|
+
verdict,
|
|
38729
|
+
blocking,
|
|
38730
|
+
rulesHash: needHex(o, "rulesHash", HEX64),
|
|
38731
|
+
ts: needString(o, "ts")
|
|
38732
|
+
};
|
|
38733
|
+
}
|
|
38543
38734
|
return bad(`unknown record type ${JSON.stringify(t)}`);
|
|
38544
38735
|
}
|
|
38545
38736
|
function codeOf2(err) {
|
|
@@ -39412,6 +39603,23 @@ function resolveReleaseFingerprintFiles(env2 = process.env) {
|
|
|
39412
39603
|
return resolveAsset(import.meta.url, ["release-src", base]);
|
|
39413
39604
|
});
|
|
39414
39605
|
}
|
|
39606
|
+
var EXFIL_FINGERPRINT_SOURCES = [
|
|
39607
|
+
{ dir: "exfil", base: "severity.ts" },
|
|
39608
|
+
{ dir: "exfil", base: "rules.ts" },
|
|
39609
|
+
{ dir: "exfil", base: "rulesPath.ts" },
|
|
39610
|
+
{ dir: "exfil", base: "scan.ts" }
|
|
39611
|
+
];
|
|
39612
|
+
var EXFIL_FINGERPRINT_BASENAMES = EXFIL_FINGERPRINT_SOURCES.map((s) => s.base);
|
|
39613
|
+
function resolveExfilFingerprintFiles(env2 = process.env) {
|
|
39614
|
+
const here = dirname6(fileURLToPath4(import.meta.url));
|
|
39615
|
+
const seam = env2.BORDER_EXFIL_SRC_DIR;
|
|
39616
|
+
return EXFIL_FINGERPRINT_SOURCES.map(({ dir, base }) => {
|
|
39617
|
+
if (seam !== void 0) return join11(seam, base);
|
|
39618
|
+
const live = join11(here, "..", dir, base);
|
|
39619
|
+
if (existsSync7(live)) return live;
|
|
39620
|
+
return resolveAsset(import.meta.url, ["exfil-src", base]);
|
|
39621
|
+
});
|
|
39622
|
+
}
|
|
39415
39623
|
function resolvePromptTemplatePath(env2 = process.env) {
|
|
39416
39624
|
return env2.BORDER_PROMPT_TEMPLATE_PATH ?? resolveAsset(import.meta.url, ["prompts", "llm-review.md"]);
|
|
39417
39625
|
}
|
|
@@ -39433,7 +39641,8 @@ async function computeCheckRulesHash(input2) {
|
|
|
39433
39641
|
bundledRulePaths: [
|
|
39434
39642
|
GITLEAKS_VENDORED_CONFIG,
|
|
39435
39643
|
...resolveResidueFingerprintFiles(input2.env ?? process.env),
|
|
39436
|
-
...resolveReleaseFingerprintFiles(input2.env ?? process.env)
|
|
39644
|
+
...resolveReleaseFingerprintFiles(input2.env ?? process.env),
|
|
39645
|
+
...resolveExfilFingerprintFiles(input2.env ?? process.env)
|
|
39437
39646
|
],
|
|
39438
39647
|
configDigest: input2.configDigest,
|
|
39439
39648
|
engineVersions: input2.engineVersions,
|
|
@@ -41389,7 +41598,7 @@ function npmNameClaimed(o) {
|
|
|
41389
41598
|
return true;
|
|
41390
41599
|
}
|
|
41391
41600
|
function npmViewJson(args, registry2, timeoutMs, env2) {
|
|
41392
|
-
return new Promise((
|
|
41601
|
+
return new Promise((resolve20, reject) => {
|
|
41393
41602
|
const child = spawn2("npm", ["view", ...args, "--registry", registry2, "--fetch-retries=0", "--json"], {
|
|
41394
41603
|
stdio: ["ignore", "pipe", "pipe"],
|
|
41395
41604
|
...env2 === void 0 ? {} : { env: env2 }
|
|
@@ -41408,7 +41617,7 @@ function npmViewJson(args, registry2, timeoutMs, env2) {
|
|
|
41408
41617
|
});
|
|
41409
41618
|
child.on("close", (status) => {
|
|
41410
41619
|
clearTimeout(timer);
|
|
41411
|
-
|
|
41620
|
+
resolve20({ status: status ?? -1, stdout, stderr });
|
|
41412
41621
|
});
|
|
41413
41622
|
});
|
|
41414
41623
|
}
|
|
@@ -42546,6 +42755,22 @@ function headFile4(toplevel, rel) {
|
|
|
42546
42755
|
}
|
|
42547
42756
|
return r.stdout;
|
|
42548
42757
|
}
|
|
42758
|
+
function remoteTrackingDrift(cwd) {
|
|
42759
|
+
const configured = parseRemoteV(runGit3(cwd, ["remote", "-v"]).stdout).map((r) => r.name).filter((n) => n !== void 0).sort();
|
|
42760
|
+
const refs = runGit3(cwd, ["for-each-ref", "--format=%(refname:lstrip=2)", "refs/remotes"]);
|
|
42761
|
+
const refNames = /* @__PURE__ */ new Set();
|
|
42762
|
+
if (refs.ok) {
|
|
42763
|
+
for (const line of refs.stdout.split("\n")) {
|
|
42764
|
+
const first = line.trim().split("/")[0];
|
|
42765
|
+
if (first !== void 0 && first !== "") {
|
|
42766
|
+
refNames.add(first);
|
|
42767
|
+
}
|
|
42768
|
+
}
|
|
42769
|
+
}
|
|
42770
|
+
const configuredSet = new Set(configured);
|
|
42771
|
+
const orphans = [...refNames].filter((n) => !configuredSet.has(n)).sort();
|
|
42772
|
+
return { configured, orphans };
|
|
42773
|
+
}
|
|
42549
42774
|
function hasTargets(cfg) {
|
|
42550
42775
|
return cfg.targets.git.remotes.length > 0 || publishChannels().some((c) => c.configured(cfg));
|
|
42551
42776
|
}
|
|
@@ -42641,7 +42866,7 @@ function exposureSet(cfg, options = {}) {
|
|
|
42641
42866
|
}
|
|
42642
42867
|
|
|
42643
42868
|
// src/check.ts
|
|
42644
|
-
import { join as
|
|
42869
|
+
import { join as join23 } from "node:path";
|
|
42645
42870
|
|
|
42646
42871
|
// src/rules/aiArtifacts.ts
|
|
42647
42872
|
import { spawnSync as spawnSync12 } from "node:child_process";
|
|
@@ -42884,6 +43109,301 @@ function applyAllowList(findings, allow, repoDir) {
|
|
|
42884
43109
|
return { kept, allowHits: [...hits.values()] };
|
|
42885
43110
|
}
|
|
42886
43111
|
|
|
43112
|
+
// src/check/exfilTreeScan.ts
|
|
43113
|
+
import { readFileSync as readFileSync14, statSync as statSync2 } from "node:fs";
|
|
43114
|
+
import { join as join21, resolve as resolve12 } from "node:path";
|
|
43115
|
+
|
|
43116
|
+
// src/exfil/scan.ts
|
|
43117
|
+
import { createHash as createHash13 } from "node:crypto";
|
|
43118
|
+
|
|
43119
|
+
// src/exfil/severity.ts
|
|
43120
|
+
var EXFIL_RULE_IDS = [
|
|
43121
|
+
"exfil-rfc1918",
|
|
43122
|
+
"exfil-ssh-target",
|
|
43123
|
+
"exfil-home-path",
|
|
43124
|
+
"exfil-cred-location",
|
|
43125
|
+
"exfil-host-profile"
|
|
43126
|
+
];
|
|
43127
|
+
var EXFIL_MESSAGE_ID_SUFFIX = ":message";
|
|
43128
|
+
var EXFIL_TABLE_SEVERITY = {
|
|
43129
|
+
"exfil-rfc1918": "HIGH",
|
|
43130
|
+
"exfil-ssh-target": "HIGH",
|
|
43131
|
+
"exfil-home-path": "MEDIUM",
|
|
43132
|
+
"exfil-cred-location": "MEDIUM",
|
|
43133
|
+
"exfil-host-profile": "MEDIUM"
|
|
43134
|
+
};
|
|
43135
|
+
var EXFIL_HIGH_FAMILY = ["exfil-rfc1918", "exfil-ssh-target"];
|
|
43136
|
+
var EXFIL_SECRET_VALUE_RULES = EXFIL_HIGH_FAMILY;
|
|
43137
|
+
var TWIN_HIGH = {
|
|
43138
|
+
"exfil-rfc1918": "CRITICAL",
|
|
43139
|
+
"exfil-ssh-target": "CRITICAL",
|
|
43140
|
+
"exfil-home-path": null,
|
|
43141
|
+
"exfil-cred-location": null,
|
|
43142
|
+
"exfil-host-profile": null
|
|
43143
|
+
};
|
|
43144
|
+
var NULL_ROW = {
|
|
43145
|
+
"exfil-rfc1918": null,
|
|
43146
|
+
"exfil-ssh-target": null,
|
|
43147
|
+
"exfil-home-path": null,
|
|
43148
|
+
"exfil-cred-location": null,
|
|
43149
|
+
"exfil-host-profile": null
|
|
43150
|
+
};
|
|
43151
|
+
var EXFIL_MATRIX = {
|
|
43152
|
+
tree: {
|
|
43153
|
+
// native tree leg = MEDIUM family补发 only (v2 double-emission revoked).
|
|
43154
|
+
native: { ...NULL_ROW, "exfil-home-path": "MEDIUM", "exfil-cred-location": "MEDIUM", "exfil-host-profile": "MEDIUM" },
|
|
43155
|
+
// gitleaks.ts:81 hardcodes CRITICAL for every ingested finding.
|
|
43156
|
+
"gitleaks-twin": TWIN_HIGH,
|
|
43157
|
+
// secretlint.ts:299-309 has no HIGH rung — error maps to CRITICAL as-is.
|
|
43158
|
+
"secretlint-twin": TWIN_HIGH
|
|
43159
|
+
},
|
|
43160
|
+
message: {
|
|
43161
|
+
// native-exclusive `:message` family at table severity.
|
|
43162
|
+
native: { ...EXFIL_TABLE_SEVERITY },
|
|
43163
|
+
"gitleaks-twin": NULL_ROW,
|
|
43164
|
+
"secretlint-twin": NULL_ROW
|
|
43165
|
+
},
|
|
43166
|
+
tagNote: {
|
|
43167
|
+
// engine既有腿 owns this facet as `tag-message-secret` (a non-exfil id);
|
|
43168
|
+
// NO exfil-* emission is legal on tag notes through ANY channel.
|
|
43169
|
+
native: NULL_ROW,
|
|
43170
|
+
"gitleaks-twin": NULL_ROW,
|
|
43171
|
+
"secretlint-twin": NULL_ROW
|
|
43172
|
+
}
|
|
43173
|
+
};
|
|
43174
|
+
function observedSeverity(rule, facet, channel) {
|
|
43175
|
+
return EXFIL_MATRIX[facet][channel][rule];
|
|
43176
|
+
}
|
|
43177
|
+
function emitRulesFor(facet, channel) {
|
|
43178
|
+
return EXFIL_RULE_IDS.filter((rule) => observedSeverity(rule, facet, channel) !== null);
|
|
43179
|
+
}
|
|
43180
|
+
function exfilFindingId(rule, facet) {
|
|
43181
|
+
switch (facet) {
|
|
43182
|
+
case "message":
|
|
43183
|
+
return `${rule}${EXFIL_MESSAGE_ID_SUFFIX}`;
|
|
43184
|
+
case "tree":
|
|
43185
|
+
case "tagNote":
|
|
43186
|
+
return rule;
|
|
43187
|
+
}
|
|
43188
|
+
}
|
|
43189
|
+
|
|
43190
|
+
// src/exfil/scan.ts
|
|
43191
|
+
var EXFIL_ENGINE = "border-exfil";
|
|
43192
|
+
var FULL_MASK2 = "\u25AE\u25AE\u25AE\u25AE";
|
|
43193
|
+
var ELLIPSIS2 = "\u2026";
|
|
43194
|
+
function maskValue(value) {
|
|
43195
|
+
const valueDigest = createHash13("sha256").update(value, "utf8").digest("hex");
|
|
43196
|
+
const points = [...value];
|
|
43197
|
+
const snippet = points.length <= 12 ? FULL_MASK2 : `${points.slice(0, 4).join("")}${ELLIPSIS2}${points.slice(-4).join("")}`;
|
|
43198
|
+
return { valueDigest, snippet };
|
|
43199
|
+
}
|
|
43200
|
+
var PREDICATES = {
|
|
43201
|
+
"exfil-rfc1918": matchRfc1918,
|
|
43202
|
+
"exfil-ssh-target": matchSshTarget,
|
|
43203
|
+
"exfil-home-path": matchHomePath,
|
|
43204
|
+
"exfil-cred-location": matchCredLocation,
|
|
43205
|
+
"exfil-host-profile": matchHostProfile
|
|
43206
|
+
};
|
|
43207
|
+
function scanFacet(input2, facet) {
|
|
43208
|
+
const channel = "native";
|
|
43209
|
+
const options = { facet, ...input2.hosts !== void 0 ? { hosts: input2.hosts } : {} };
|
|
43210
|
+
const findings = [];
|
|
43211
|
+
for (const rule of emitRulesFor(facet, channel)) {
|
|
43212
|
+
const severity = observedSeverity(rule, facet, channel);
|
|
43213
|
+
if (severity === null) continue;
|
|
43214
|
+
for (const hit of PREDICATES[rule](input2.text, options)) {
|
|
43215
|
+
const { valueDigest, snippet } = maskValue(hit.matched);
|
|
43216
|
+
if (EXFIL_SECRET_VALUE_RULES.includes(rule)) input2.onMatch?.(hit.matched, valueDigest);
|
|
43217
|
+
findings.push({
|
|
43218
|
+
id: exfilFindingId(rule, facet),
|
|
43219
|
+
rule,
|
|
43220
|
+
severity,
|
|
43221
|
+
facet,
|
|
43222
|
+
channel,
|
|
43223
|
+
engine: EXFIL_ENGINE,
|
|
43224
|
+
message: hit.message,
|
|
43225
|
+
source: input2.source,
|
|
43226
|
+
line: hit.line,
|
|
43227
|
+
valueDigest,
|
|
43228
|
+
snippet
|
|
43229
|
+
});
|
|
43230
|
+
}
|
|
43231
|
+
}
|
|
43232
|
+
return findings.sort((a, b) => a.line - b.line || a.id.localeCompare(b.id) || a.valueDigest.localeCompare(b.valueDigest));
|
|
43233
|
+
}
|
|
43234
|
+
function scanTreeText(input2) {
|
|
43235
|
+
return scanFacet(input2, "tree");
|
|
43236
|
+
}
|
|
43237
|
+
function scanMessageText(input2) {
|
|
43238
|
+
return scanFacet(input2, "message");
|
|
43239
|
+
}
|
|
43240
|
+
|
|
43241
|
+
// src/check/exfilTreeScan.ts
|
|
43242
|
+
var BORDER_SEGMENT = ".border";
|
|
43243
|
+
var MAX_SCAN_BYTES = 2 * 1024 * 1024;
|
|
43244
|
+
function toFinding3(f, target) {
|
|
43245
|
+
return {
|
|
43246
|
+
rule: f.id,
|
|
43247
|
+
severity: f.severity,
|
|
43248
|
+
target,
|
|
43249
|
+
path: f.source,
|
|
43250
|
+
line: f.line,
|
|
43251
|
+
engine: f.engine,
|
|
43252
|
+
message: f.message,
|
|
43253
|
+
valueDigest: f.valueDigest,
|
|
43254
|
+
snippet: f.snippet
|
|
43255
|
+
};
|
|
43256
|
+
}
|
|
43257
|
+
function scanExfilTree(o) {
|
|
43258
|
+
const repoDir = resolve12(o.repoDir);
|
|
43259
|
+
const gitOpts = { ...o.env !== void 0 ? { env: o.env } : {} };
|
|
43260
|
+
const files = runGitChecked(repoDir, ["ls-files", "-z"], gitOpts).split("\0").filter((rel) => rel !== "" && !rel.split("/").includes(BORDER_SEGMENT));
|
|
43261
|
+
const findings = [];
|
|
43262
|
+
for (const rel of files) {
|
|
43263
|
+
const abs = join21(repoDir, rel);
|
|
43264
|
+
let size = 0;
|
|
43265
|
+
try {
|
|
43266
|
+
size = statSync2(abs).size;
|
|
43267
|
+
} catch {
|
|
43268
|
+
continue;
|
|
43269
|
+
}
|
|
43270
|
+
if (size > MAX_SCAN_BYTES) continue;
|
|
43271
|
+
let text = "";
|
|
43272
|
+
try {
|
|
43273
|
+
text = readFileSync14(abs, "utf8");
|
|
43274
|
+
} catch {
|
|
43275
|
+
continue;
|
|
43276
|
+
}
|
|
43277
|
+
if (text.includes("\0")) continue;
|
|
43278
|
+
for (const f of scanTreeText({
|
|
43279
|
+
text,
|
|
43280
|
+
source: rel,
|
|
43281
|
+
...o.hosts !== void 0 ? { hosts: o.hosts } : {},
|
|
43282
|
+
...o.sanitizer !== void 0 ? { onMatch: (raw, digest2) => o.sanitizer?.register(digest2, raw) } : {}
|
|
43283
|
+
})) {
|
|
43284
|
+
findings.push(toFinding3(f, o.target ?? "tree"));
|
|
43285
|
+
}
|
|
43286
|
+
}
|
|
43287
|
+
return findings;
|
|
43288
|
+
}
|
|
43289
|
+
|
|
43290
|
+
// src/check/messageScan.ts
|
|
43291
|
+
import { spawnSync as spawnSync13 } from "node:child_process";
|
|
43292
|
+
import { dirname as dirname10, resolve as resolve13 } from "node:path";
|
|
43293
|
+
var MESSAGE_REV_BATCH = 200;
|
|
43294
|
+
var MESSAGE_GIT_TIMEOUT_MS = 55e3;
|
|
43295
|
+
function defaultRunner(o) {
|
|
43296
|
+
const repoDir = resolve13(o.repoDir);
|
|
43297
|
+
const env2 = {};
|
|
43298
|
+
for (const [name, value] of Object.entries(o.env ?? process.env)) {
|
|
43299
|
+
if (!name.startsWith("GIT_")) env2[name] = value;
|
|
43300
|
+
}
|
|
43301
|
+
env2.GIT_CEILING_DIRECTORIES = dirname10(repoDir);
|
|
43302
|
+
env2.GIT_OPTIONAL_LOCKS = "0";
|
|
43303
|
+
return (args) => {
|
|
43304
|
+
const r = spawnSync13("git", ["-C", repoDir, ...args], {
|
|
43305
|
+
encoding: "utf8",
|
|
43306
|
+
env: env2,
|
|
43307
|
+
timeout: o.timeoutMs ?? MESSAGE_GIT_TIMEOUT_MS,
|
|
43308
|
+
maxBuffer: o.maxBufferBytes ?? 32 * 1024 * 1024
|
|
43309
|
+
});
|
|
43310
|
+
if (r.error !== void 0) {
|
|
43311
|
+
throw new ConfigError("git-failed", `git ${args.slice(0, 2).join(" ")} in ${repoDir}: ${r.error.message}`);
|
|
43312
|
+
}
|
|
43313
|
+
if (r.status === null) {
|
|
43314
|
+
throw new ConfigError("git-failed", `git ${args.slice(0, 2).join(" ")} in ${repoDir} killed by ${String(r.signal)} (timeout ${String(o.timeoutMs ?? MESSAGE_GIT_TIMEOUT_MS)}ms) \u2014 fail closed, never a silent clean`);
|
|
43315
|
+
}
|
|
43316
|
+
if (r.status !== 0) {
|
|
43317
|
+
throw new ConfigError("git-failed", `git ${args.slice(0, 2).join(" ")} in ${repoDir} exited ${String(r.status)}: ${(r.stderr ?? "").trim().slice(0, 200)}`);
|
|
43318
|
+
}
|
|
43319
|
+
return r.stdout ?? "";
|
|
43320
|
+
};
|
|
43321
|
+
}
|
|
43322
|
+
function revExists2(run2, rev) {
|
|
43323
|
+
try {
|
|
43324
|
+
run2(["rev-parse", "--verify", "--quiet", rev]);
|
|
43325
|
+
return true;
|
|
43326
|
+
} catch {
|
|
43327
|
+
return false;
|
|
43328
|
+
}
|
|
43329
|
+
}
|
|
43330
|
+
function trackingEndpoint2(run2, remoteName, ref) {
|
|
43331
|
+
const candidates = [`refs/remotes/${remoteName}/${ref}`];
|
|
43332
|
+
const short = ref.replace(/^refs\/(?:heads|tags)\//, "");
|
|
43333
|
+
if (short !== ref) candidates.push(`refs/remotes/${remoteName}/${short}`);
|
|
43334
|
+
return candidates.find((c) => revExists2(run2, c)) ?? null;
|
|
43335
|
+
}
|
|
43336
|
+
function willPublishShas(run2, refSet, remotes) {
|
|
43337
|
+
const out = [];
|
|
43338
|
+
const seen = /* @__PURE__ */ new Set();
|
|
43339
|
+
const collect = (revs) => {
|
|
43340
|
+
for (const sha of run2(["rev-list", ...revs]).split("\n")) {
|
|
43341
|
+
if (sha !== "" && !seen.has(sha)) {
|
|
43342
|
+
seen.add(sha);
|
|
43343
|
+
out.push(sha);
|
|
43344
|
+
}
|
|
43345
|
+
}
|
|
43346
|
+
};
|
|
43347
|
+
for (const ref of refSet) {
|
|
43348
|
+
if (!revExists2(run2, ref)) continue;
|
|
43349
|
+
const ranges = [];
|
|
43350
|
+
for (const remote of remotes) {
|
|
43351
|
+
const name = remote.name;
|
|
43352
|
+
if (name === void 0) continue;
|
|
43353
|
+
const endpoint = trackingEndpoint2(run2, name, ref);
|
|
43354
|
+
if (endpoint !== null) ranges.push(`${endpoint}..${ref}`);
|
|
43355
|
+
}
|
|
43356
|
+
if (ranges.length === 0) collect([ref]);
|
|
43357
|
+
else collect(ranges);
|
|
43358
|
+
}
|
|
43359
|
+
return out;
|
|
43360
|
+
}
|
|
43361
|
+
var MESSAGE_LOG_FORMAT = "--format=%x1e%H%x00%B";
|
|
43362
|
+
function scanOneRecord(record2, o, findings) {
|
|
43363
|
+
const trimmed = record2.replace(/^\n/, "");
|
|
43364
|
+
if (trimmed === "") return;
|
|
43365
|
+
const nul = trimmed.indexOf("\0");
|
|
43366
|
+
if (nul === -1) {
|
|
43367
|
+
throw new ConfigError("git-failed", "git log message record malformed (no NUL separator) \u2014 fail closed");
|
|
43368
|
+
}
|
|
43369
|
+
const sha = trimmed.slice(0, nul);
|
|
43370
|
+
const body = trimmed.slice(nul + 1).replace(/\n$/, "");
|
|
43371
|
+
for (const f of scanMessageText({
|
|
43372
|
+
text: body,
|
|
43373
|
+
source: sha,
|
|
43374
|
+
...o.hosts !== void 0 ? { hosts: o.hosts } : {},
|
|
43375
|
+
...o.sanitizer !== void 0 ? { onMatch: (raw, digest2) => o.sanitizer?.register(digest2, raw) } : {}
|
|
43376
|
+
})) {
|
|
43377
|
+
findings.push(toMessageFinding(f, sha));
|
|
43378
|
+
}
|
|
43379
|
+
}
|
|
43380
|
+
function toMessageFinding(f, sha) {
|
|
43381
|
+
return {
|
|
43382
|
+
rule: f.id,
|
|
43383
|
+
severity: f.severity,
|
|
43384
|
+
target: "git",
|
|
43385
|
+
path: f.source,
|
|
43386
|
+
// = sha: toRepoRelative passes it through unchanged (exclusions.ts:20-25) ⇒ per-commit allow rows
|
|
43387
|
+
commit: sha,
|
|
43388
|
+
line: f.line,
|
|
43389
|
+
engine: f.engine,
|
|
43390
|
+
message: f.message,
|
|
43391
|
+
valueDigest: f.valueDigest,
|
|
43392
|
+
snippet: f.snippet
|
|
43393
|
+
};
|
|
43394
|
+
}
|
|
43395
|
+
function scanCommitMessages(o) {
|
|
43396
|
+
const run2 = o.runGit ?? defaultRunner(o);
|
|
43397
|
+
const shas = willPublishShas(run2, o.refSet, o.remotes);
|
|
43398
|
+
const findings = [];
|
|
43399
|
+
for (let i = 0; i < shas.length; i += MESSAGE_REV_BATCH) {
|
|
43400
|
+
const batch = shas.slice(i, i + MESSAGE_REV_BATCH);
|
|
43401
|
+
const out = run2(["log", "--no-walk", MESSAGE_LOG_FORMAT, ...batch]);
|
|
43402
|
+
for (const record2 of out.split("")) scanOneRecord(record2, o, findings);
|
|
43403
|
+
}
|
|
43404
|
+
return findings;
|
|
43405
|
+
}
|
|
43406
|
+
|
|
42887
43407
|
// src/check/proofValve.ts
|
|
42888
43408
|
var ROUNDTRIP_PROOF_MISSING_RULE = "roundtrip-proof-missing";
|
|
42889
43409
|
var ROUNDTRIP_PROOF_STALE_RULE = "roundtrip-proof-stale";
|
|
@@ -42923,10 +43443,10 @@ function proofFindings(o) {
|
|
|
42923
43443
|
}
|
|
42924
43444
|
|
|
42925
43445
|
// src/check/tagScan.ts
|
|
42926
|
-
import { spawnSync as
|
|
43446
|
+
import { spawnSync as spawnSync14 } from "node:child_process";
|
|
42927
43447
|
import { mkdtempSync as mkdtempSync3, rmSync as rmSync10, writeFileSync as writeFileSync4 } from "node:fs";
|
|
42928
43448
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
42929
|
-
import { join as
|
|
43449
|
+
import { join as join22, resolve as resolve14 } from "node:path";
|
|
42930
43450
|
var TAG_MESSAGE_RULE = "tag-message-secret";
|
|
42931
43451
|
function isEnoent2(error61) {
|
|
42932
43452
|
return typeof error61 === "object" && error61 !== null && error61.code === "ENOENT";
|
|
@@ -42946,7 +43466,7 @@ function stdinFindings(text, ignorePath, o) {
|
|
|
42946
43466
|
];
|
|
42947
43467
|
let lastError;
|
|
42948
43468
|
for (const bin of binaryCandidates("gitleaks", o)) {
|
|
42949
|
-
const r =
|
|
43469
|
+
const r = spawnSync14(bin, args, {
|
|
42950
43470
|
encoding: "utf8",
|
|
42951
43471
|
input: text,
|
|
42952
43472
|
timeout: o.timeoutMs ?? 3e5,
|
|
@@ -43013,12 +43533,12 @@ function dereferenceTagCommit(repoDir, ref, o) {
|
|
|
43013
43533
|
}
|
|
43014
43534
|
}
|
|
43015
43535
|
function scanTagMessages(o) {
|
|
43016
|
-
const repoDir =
|
|
43536
|
+
const repoDir = resolve14(o.repoDir);
|
|
43017
43537
|
const gitOpts = { ...o.env !== void 0 ? { env: o.env } : {} };
|
|
43018
43538
|
const refs = runGitChecked(repoDir, ["for-each-ref", "--format=%(refname)", "refs/tags"], gitOpts).split("\n").filter((ref) => ref !== "");
|
|
43019
43539
|
if (refs.length === 0) return [];
|
|
43020
|
-
const ignoreDir = mkdtempSync3(
|
|
43021
|
-
const ignorePath =
|
|
43540
|
+
const ignoreDir = mkdtempSync3(join22(tmpdir2(), "border-gitleaks-ignore-"));
|
|
43541
|
+
const ignorePath = join22(ignoreDir, ".gitleaksignore");
|
|
43022
43542
|
writeFileSync4(ignorePath, "");
|
|
43023
43543
|
try {
|
|
43024
43544
|
const findings = [];
|
|
@@ -43094,7 +43614,7 @@ async function runPipeline(o, ctx, lockWarning) {
|
|
|
43094
43614
|
if (!broken.has("gitleaks")) {
|
|
43095
43615
|
const eng = legOptions(o, sanitizer);
|
|
43096
43616
|
findings.push(...filterBorderStateFindings(scanGitHistory({ repoDir, refRange: historyRefRange(ctx), target: "git", ...eng }), repoDir));
|
|
43097
|
-
findings.push(...filterBorderStateFindings(scanTree({ dir: repoDir, stateDir:
|
|
43617
|
+
findings.push(...filterBorderStateFindings(scanTree({ dir: repoDir, stateDir: join23(repoDir, BORDER_STATE_DIR), target: "tree", ...eng }), repoDir));
|
|
43098
43618
|
findings.push(...filterBorderStateFindings(scanTagMessages({ repoDir, target: "git", ...eng }), repoDir));
|
|
43099
43619
|
}
|
|
43100
43620
|
if (!broken.has("secretlint")) {
|
|
@@ -43105,6 +43625,17 @@ async function runPipeline(o, ctx, lockWarning) {
|
|
|
43105
43625
|
}
|
|
43106
43626
|
findings.push(...scanAiArtifacts({ repoDir, refSet: [...ctx.refSet], cfg: o.cfg.rules, ...envOpt }));
|
|
43107
43627
|
findings.push(...scanIdentity({ repoDir, refSet: [...ctx.refSet], cfg: o.cfg, ...envOpt }));
|
|
43628
|
+
findings.push(...filterBorderStateFindings(scanExfilTree({ repoDir, hosts: o.cfg.rules.hosts, ...legOptions(o, sanitizer) }), repoDir));
|
|
43629
|
+
findings.push(...filterBorderStateFindings(
|
|
43630
|
+
scanCommitMessages({
|
|
43631
|
+
repoDir,
|
|
43632
|
+
refSet: ctx.refSet.length > 0 ? [...ctx.refSet] : [ctx.headSha],
|
|
43633
|
+
remotes: o.cfg.targets.git.remotes,
|
|
43634
|
+
hosts: o.cfg.rules.hosts,
|
|
43635
|
+
...legOptions(o, sanitizer)
|
|
43636
|
+
}),
|
|
43637
|
+
repoDir
|
|
43638
|
+
));
|
|
43108
43639
|
const ledgerArtifacts = [];
|
|
43109
43640
|
for (const channel of publishChannels()) {
|
|
43110
43641
|
if (!o.effectiveTargets.includes(channel.id) || !channel.configured(o.cfg)) continue;
|
|
@@ -43279,21 +43810,271 @@ var NO_TARGETS_PROBE_CFG = {
|
|
|
43279
43810
|
engines: { require: [], trufflehog: false }
|
|
43280
43811
|
};
|
|
43281
43812
|
|
|
43813
|
+
// src/commands/exfil.ts
|
|
43814
|
+
import { createHash as createHash14 } from "node:crypto";
|
|
43815
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync4, readFileSync as readFileSync15, rmSync as rmSync11, writeFileSync as writeFileSync5 } from "node:fs";
|
|
43816
|
+
import { tmpdir as tmpdir3 } from "node:os";
|
|
43817
|
+
import { dirname as dirname12, join as join24 } from "node:path";
|
|
43818
|
+
|
|
43819
|
+
// src/commands/exfilGit.ts
|
|
43820
|
+
import { spawnSync as spawnSync15 } from "node:child_process";
|
|
43821
|
+
import { dirname as dirname11, resolve as resolve15 } from "node:path";
|
|
43822
|
+
var GIT_TIMEOUT_MS = 55e3;
|
|
43823
|
+
var MAX_BUFFER_BYTES = 32 * 1024 * 1024;
|
|
43824
|
+
var MAX_BLOB_BYTES = 2 * 1024 * 1024;
|
|
43825
|
+
var BATCH_SHAS = 500;
|
|
43826
|
+
function makeGit(repoDir, env2) {
|
|
43827
|
+
const clean = {};
|
|
43828
|
+
for (const [name, value] of Object.entries(env2)) {
|
|
43829
|
+
if (!name.startsWith("GIT_")) clean[name] = value;
|
|
43830
|
+
}
|
|
43831
|
+
clean.GIT_CEILING_DIRECTORIES = dirname11(resolve15(repoDir));
|
|
43832
|
+
clean.GIT_OPTIONAL_LOCKS = "0";
|
|
43833
|
+
clean.GIT_TERMINAL_PROMPT = "0";
|
|
43834
|
+
const spawnWith = (args, input2) => {
|
|
43835
|
+
const r = spawnSync15("git", ["-C", resolve15(repoDir), ...args], {
|
|
43836
|
+
timeout: GIT_TIMEOUT_MS,
|
|
43837
|
+
maxBuffer: MAX_BUFFER_BYTES,
|
|
43838
|
+
env: clean,
|
|
43839
|
+
...input2 !== void 0 ? { input: input2 } : {}
|
|
43840
|
+
});
|
|
43841
|
+
if (r.error !== void 0) return { stdout: Buffer.alloc(0), fail: `git ${args.slice(0, 2).join(" ")}: ${r.error.message}` };
|
|
43842
|
+
if (r.status !== 0) {
|
|
43843
|
+
const why = r.status === null ? `killed by ${String(r.signal)} (timeout ${String(GIT_TIMEOUT_MS)}ms)` : `exited ${String(r.status)}`;
|
|
43844
|
+
return { stdout: Buffer.alloc(0), fail: `git ${args.slice(0, 3).join(" ")} ${why}: ${(r.stderr?.toString("utf8") ?? "").trim().slice(0, 200)}` };
|
|
43845
|
+
}
|
|
43846
|
+
return { stdout: r.stdout ?? Buffer.alloc(0), fail: null };
|
|
43847
|
+
};
|
|
43848
|
+
const runBuffer = (args, input2) => {
|
|
43849
|
+
const r = spawnWith(args, input2);
|
|
43850
|
+
if (r.fail !== null) throw new ConfigError("git-failed", r.fail);
|
|
43851
|
+
return r.stdout;
|
|
43852
|
+
};
|
|
43853
|
+
return { run: (args) => runBuffer(args).toString("utf8"), runBuffer };
|
|
43854
|
+
}
|
|
43855
|
+
function parseLsTree(line) {
|
|
43856
|
+
const m = /^\d{6} (\w+) ([0-9a-f]{40})\s+(\d+)\t(.+)$/.exec(line);
|
|
43857
|
+
if (m === null || m[1] !== "blob") return null;
|
|
43858
|
+
return { sha: m[2] ?? "", path: m[4] ?? "", size: Number(m[3]) };
|
|
43859
|
+
}
|
|
43860
|
+
function tipTree(git, rev) {
|
|
43861
|
+
return git.run(["-c", "core.quotePath=false", "ls-tree", "-r", "--long", rev]).split("\n").map(parseLsTree).filter((b) => b !== null);
|
|
43862
|
+
}
|
|
43863
|
+
function historyBlobs(git, rev) {
|
|
43864
|
+
const paths = /* @__PURE__ */ new Map();
|
|
43865
|
+
for (const line of git.run(["rev-list", "--objects", rev]).split("\n")) {
|
|
43866
|
+
if (line === "") continue;
|
|
43867
|
+
const sp = line.indexOf(" ");
|
|
43868
|
+
if (sp <= 0) continue;
|
|
43869
|
+
if (!paths.has(line.slice(0, sp))) paths.set(line.slice(0, sp), line.slice(sp + 1));
|
|
43870
|
+
}
|
|
43871
|
+
const shas = [...paths.keys()];
|
|
43872
|
+
const out = [];
|
|
43873
|
+
for (let i = 0; i < shas.length; i += BATCH_SHAS) {
|
|
43874
|
+
const checked = git.runBuffer(["cat-file", "--batch-check"], `${shas.slice(i, i + BATCH_SHAS).join("\n")}
|
|
43875
|
+
`).toString("utf8");
|
|
43876
|
+
for (const line of checked.split("\n")) {
|
|
43877
|
+
const m = /^([0-9a-f]{40}) (\w+) (\d+)$/.exec(line);
|
|
43878
|
+
if (m === null || m[2] !== "blob") continue;
|
|
43879
|
+
const path4 = paths.get(m[1] ?? "");
|
|
43880
|
+
if (path4 !== void 0) out.push({ sha: m[1] ?? "", path: path4, size: Number(m[3]) });
|
|
43881
|
+
}
|
|
43882
|
+
}
|
|
43883
|
+
return out;
|
|
43884
|
+
}
|
|
43885
|
+
function blobTexts(git, blobs) {
|
|
43886
|
+
const sizes = new Map(blobs.map((b) => [b.sha, b.size]));
|
|
43887
|
+
const wanted = [...new Set(blobs.map((b) => b.sha))].filter((s) => (sizes.get(s) ?? 0) <= MAX_BLOB_BYTES);
|
|
43888
|
+
const out = /* @__PURE__ */ new Map();
|
|
43889
|
+
for (let i = 0; i < wanted.length; i += BATCH_SHAS) {
|
|
43890
|
+
const buf = git.runBuffer(["cat-file", "--batch", "--buffer"], `${wanted.slice(i, i + BATCH_SHAS).join("\n")}
|
|
43891
|
+
`);
|
|
43892
|
+
let pos = 0;
|
|
43893
|
+
while (pos < buf.length) {
|
|
43894
|
+
const nl = buf.indexOf(10, pos);
|
|
43895
|
+
if (nl === -1) break;
|
|
43896
|
+
const parts = buf.subarray(pos, nl).toString("utf8").split(" ");
|
|
43897
|
+
pos = nl + 1;
|
|
43898
|
+
const sha = parts[0] ?? "";
|
|
43899
|
+
if (parts[1] === "missing" || parts.length < 3) continue;
|
|
43900
|
+
const size = Number(parts[2]);
|
|
43901
|
+
const body = buf.subarray(pos, pos + size);
|
|
43902
|
+
pos += size + 1;
|
|
43903
|
+
if (!body.includes(0)) out.set(sha, body.toString("utf8"));
|
|
43904
|
+
}
|
|
43905
|
+
}
|
|
43906
|
+
return out;
|
|
43907
|
+
}
|
|
43908
|
+
|
|
43909
|
+
// src/commands/exfil.ts
|
|
43910
|
+
var EXFIL_USAGE = "usage: border exfil <ref|url> [ref...] [--deep] [--json]";
|
|
43911
|
+
var URL_SHAPE_RE = /^(?:[A-Za-z][A-Za-z0-9+.-]*:\/\/\S+|\S+@\S+:\S+)$/;
|
|
43912
|
+
var isExfilId = (rule) => rule.startsWith("exfil-");
|
|
43913
|
+
function toTreeFinding(f) {
|
|
43914
|
+
return {
|
|
43915
|
+
rule: f.id,
|
|
43916
|
+
severity: f.severity,
|
|
43917
|
+
target: "tree",
|
|
43918
|
+
path: f.source,
|
|
43919
|
+
line: f.line,
|
|
43920
|
+
engine: f.engine,
|
|
43921
|
+
message: f.message,
|
|
43922
|
+
valueDigest: f.valueDigest,
|
|
43923
|
+
snippet: f.snippet
|
|
43924
|
+
};
|
|
43925
|
+
}
|
|
43926
|
+
function scanBlobsNative(git, blobs) {
|
|
43927
|
+
const texts = blobTexts(git, blobs);
|
|
43928
|
+
const findings = [];
|
|
43929
|
+
for (const b of blobs) {
|
|
43930
|
+
const text = texts.get(b.sha);
|
|
43931
|
+
if (text === void 0) continue;
|
|
43932
|
+
for (const f of scanTreeText({ text, source: b.path })) findings.push(toTreeFinding(f));
|
|
43933
|
+
}
|
|
43934
|
+
return findings;
|
|
43935
|
+
}
|
|
43936
|
+
async function scanTipTree(git, treeRoot, rev, env2) {
|
|
43937
|
+
const blobs = tipTree(git, rev);
|
|
43938
|
+
const texts = blobTexts(git, blobs);
|
|
43939
|
+
const native = [];
|
|
43940
|
+
const written = [];
|
|
43941
|
+
for (const b of blobs) {
|
|
43942
|
+
const text = texts.get(b.sha);
|
|
43943
|
+
if (text === void 0) continue;
|
|
43944
|
+
for (const f of scanTreeText({ text, source: b.path })) native.push(toTreeFinding(f));
|
|
43945
|
+
if (b.path.split("/").includes("..") || b.path === "") continue;
|
|
43946
|
+
const abs = join24(treeRoot, b.path);
|
|
43947
|
+
if (existsSync15(abs)) continue;
|
|
43948
|
+
mkdirSync10(dirname12(abs), { recursive: true });
|
|
43949
|
+
writeFileSync5(abs, text, "utf8");
|
|
43950
|
+
written.push(b.path);
|
|
43951
|
+
}
|
|
43952
|
+
const lint = (await scanPaths({ dir: treeRoot, files: written, target: "tree", env: env2 })).filter((f) => isExfilId(f.rule));
|
|
43953
|
+
const gl = scanTree({ dir: treeRoot, stateDir: join24(treeRoot, ".border-state"), target: "tree", env: env2 }).filter((f) => isExfilId(f.rule)).map((f) => f.path !== void 0 && f.path.startsWith(`${treeRoot}/`) ? { ...f, path: f.path.slice(treeRoot.length + 1) } : f);
|
|
43954
|
+
return [...native, ...lint, ...gl];
|
|
43955
|
+
}
|
|
43956
|
+
function exfilRulesDigest() {
|
|
43957
|
+
const h = createHash14("sha256");
|
|
43958
|
+
for (const p of [...resolveExfilFingerprintFiles({}), GITLEAKS_VENDORED_CONFIG]) {
|
|
43959
|
+
let bytes;
|
|
43960
|
+
try {
|
|
43961
|
+
bytes = readFileSync15(p);
|
|
43962
|
+
} catch {
|
|
43963
|
+
throw new ConfigError("unreadable", `border exfil: rule input unreadable (fail closed): ${p}`);
|
|
43964
|
+
}
|
|
43965
|
+
h.update(p).update("\0").update(bytes).update("\0");
|
|
43966
|
+
}
|
|
43967
|
+
return h.digest("hex");
|
|
43968
|
+
}
|
|
43969
|
+
function sha256Hex2(text) {
|
|
43970
|
+
return createHash14("sha256").update(text, "utf8").digest("hex");
|
|
43971
|
+
}
|
|
43972
|
+
async function runExfilCore(ctx) {
|
|
43973
|
+
const positionals = ctx.positionals;
|
|
43974
|
+
if (positionals.length === 0) throw new UnknownArgError(`border exfil expects <ref|url> [ref...] \u2014 ${EXFIL_USAGE}`);
|
|
43975
|
+
const deep = ctx.flags.deep === true;
|
|
43976
|
+
const first = positionals[0] ?? "";
|
|
43977
|
+
const sandbox = { dir: "", tree: "" };
|
|
43978
|
+
try {
|
|
43979
|
+
const isUrl = URL_SHAPE_RE.test(first);
|
|
43980
|
+
let repoDir;
|
|
43981
|
+
let units;
|
|
43982
|
+
let exposure = [];
|
|
43983
|
+
if (isUrl) {
|
|
43984
|
+
const url2 = first;
|
|
43985
|
+
exposure = [sanitizeUrl(url2)];
|
|
43986
|
+
sandbox.dir = mkdtempSync4(join24(tmpdir3(), "border-exfil-"));
|
|
43987
|
+
const git2 = makeGit(sandbox.dir, ctx.env);
|
|
43988
|
+
git2.run(["init", "-q"]);
|
|
43989
|
+
git2.run(["remote", "add", "origin", url2]);
|
|
43990
|
+
const specs = positionals.slice(1).length === 0 ? ["HEAD"] : positionals.slice(1);
|
|
43991
|
+
units = [];
|
|
43992
|
+
for (const spec of specs) {
|
|
43993
|
+
git2.run(["fetch", "-q", "--no-tags", "origin", spec]);
|
|
43994
|
+
units.push({ label: spec, rev: git2.run(["rev-parse", "FETCH_HEAD"]).trim() });
|
|
43995
|
+
}
|
|
43996
|
+
repoDir = sandbox.dir;
|
|
43997
|
+
} else {
|
|
43998
|
+
repoDir = resolveRepoDir(ctx.cwd, { env: ctx.env });
|
|
43999
|
+
const git2 = makeGit(repoDir, ctx.env);
|
|
44000
|
+
units = positionals.map((ref) => ({ label: ref, rev: git2.run(["rev-parse", "--verify", ref]).trim() }));
|
|
44001
|
+
}
|
|
44002
|
+
const git = makeGit(repoDir, ctx.env);
|
|
44003
|
+
sandbox.tree = mkdtempSync4(join24(tmpdir3(), "border-exfil-tree-"));
|
|
44004
|
+
const findings = [];
|
|
44005
|
+
const seenBlob = /* @__PURE__ */ new Set();
|
|
44006
|
+
for (const unit of units) {
|
|
44007
|
+
findings.push(...await scanTipTree(git, sandbox.tree, unit.rev, ctx.env));
|
|
44008
|
+
if (deep) {
|
|
44009
|
+
const fresh = historyBlobs(git, unit.rev).filter((b) => {
|
|
44010
|
+
const key = `${b.sha}\0${b.path}`;
|
|
44011
|
+
if (seenBlob.has(key)) return false;
|
|
44012
|
+
seenBlob.add(key);
|
|
44013
|
+
return true;
|
|
44014
|
+
});
|
|
44015
|
+
findings.push(...scanBlobsNative(git, fresh));
|
|
44016
|
+
}
|
|
44017
|
+
}
|
|
44018
|
+
if (isUrl) ctx.stderr("border exfil: URL mode scanned fetched tips only \u2014 tag notes are not fetched (remote-side leg, check/push own it)");
|
|
44019
|
+
else findings.push(...scanTagMessages({ repoDir, target: "git", env: ctx.env }));
|
|
44020
|
+
if (deep) {
|
|
44021
|
+
findings.push(...scanGitHistory({ repoDir, refRange: units.map((u) => u.rev).join(" "), target: "git", env: ctx.env }).filter((f) => isExfilId(f.rule)));
|
|
44022
|
+
findings.push(
|
|
44023
|
+
...scanCommitMessages({
|
|
44024
|
+
repoDir,
|
|
44025
|
+
refSet: units.map((u) => u.rev),
|
|
44026
|
+
remotes: [],
|
|
44027
|
+
hosts: [],
|
|
44028
|
+
runGit: git.run,
|
|
44029
|
+
env: ctx.env
|
|
44030
|
+
})
|
|
44031
|
+
);
|
|
44032
|
+
}
|
|
44033
|
+
const verdict = computeVerdict(findings);
|
|
44034
|
+
const report = {
|
|
44035
|
+
schemaVersion: 1,
|
|
44036
|
+
key: sha256Hex2(`exfil:${isUrl ? "url" : "ref"}:${positionals.join(" ")}:${deep ? "deep" : "fast"}:${units.map((u) => u.rev).join(" ")}`),
|
|
44037
|
+
head: units[0]?.rev ?? "",
|
|
44038
|
+
dirty: false,
|
|
44039
|
+
exposureSet: exposure,
|
|
44040
|
+
refSet: positionals,
|
|
44041
|
+
rulesHash: exfilRulesDigest(),
|
|
44042
|
+
verdict,
|
|
44043
|
+
counts: countFindings(findings),
|
|
44044
|
+
findings,
|
|
44045
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
44046
|
+
};
|
|
44047
|
+
if (ctx.flags.json) ctx.stdout(renderReportJson(report));
|
|
44048
|
+
else {
|
|
44049
|
+
const scope = `${isUrl ? "URL" : "refs"} ${positionals.join(",")}${deep ? " (deep)" : ""}`;
|
|
44050
|
+
ctx.stdout(`border exfil ${scope} ${report.verdict}: ${String(report.counts.total)} finding(s), ${String(report.counts.blocking)} blocking`);
|
|
44051
|
+
for (const f of report.findings) {
|
|
44052
|
+
ctx.stdout(` ${f.severity} ${f.rule} [${f.engine}] ${f.path ?? f.commit ?? f.target}${f.line !== void 0 ? `:${String(f.line)}` : ""} ${f.message}`);
|
|
44053
|
+
}
|
|
44054
|
+
}
|
|
44055
|
+
return exitCodeFromVerdict(verdict);
|
|
44056
|
+
} finally {
|
|
44057
|
+
if (sandbox.dir !== "") rmSync11(sandbox.dir, { recursive: true, force: true });
|
|
44058
|
+
if (sandbox.tree !== "") rmSync11(sandbox.tree, { recursive: true, force: true });
|
|
44059
|
+
}
|
|
44060
|
+
}
|
|
44061
|
+
var runExfil = (ctx) => runExfilCore(ctx);
|
|
44062
|
+
|
|
43282
44063
|
// src/llm/bundle.ts
|
|
43283
|
-
import { createHash as
|
|
43284
|
-
import { existsSync as
|
|
43285
|
-
import { dirname as
|
|
44064
|
+
import { createHash as createHash15 } from "node:crypto";
|
|
44065
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync7 } from "node:fs";
|
|
44066
|
+
import { dirname as dirname14, join as join26 } from "node:path";
|
|
43286
44067
|
|
|
43287
44068
|
// src/llm/diff.ts
|
|
43288
|
-
import { spawnSync as
|
|
43289
|
-
import { mkdtempSync as
|
|
43290
|
-
import { dirname as
|
|
44069
|
+
import { spawnSync as spawnSync16 } from "node:child_process";
|
|
44070
|
+
import { mkdtempSync as mkdtempSync5, rmSync as rmSync12, writeFileSync as writeFileSync6 } from "node:fs";
|
|
44071
|
+
import { dirname as dirname13, join as join25, resolve as resolve16 } from "node:path";
|
|
43291
44072
|
var GIT_DIFF_MAX_BUFFER = 64 * 1024 * 1024;
|
|
43292
44073
|
function gitOut(repoDir, args, o, allowFail = false) {
|
|
43293
|
-
const abs =
|
|
43294
|
-
const r =
|
|
44074
|
+
const abs = resolve16(repoDir);
|
|
44075
|
+
const r = spawnSync16("git", ["-C", abs, ...args], {
|
|
43295
44076
|
encoding: "utf8",
|
|
43296
|
-
env: { ...o.env ?? process.env, GIT_CEILING_DIRECTORIES:
|
|
44077
|
+
env: { ...o.env ?? process.env, GIT_CEILING_DIRECTORIES: dirname13(abs) },
|
|
43297
44078
|
timeout: 12e4,
|
|
43298
44079
|
maxBuffer: GIT_DIFF_MAX_BUFFER
|
|
43299
44080
|
});
|
|
@@ -43384,24 +44165,24 @@ function collectRawDeltas(repoDir, baseSha, headSha, o = {}) {
|
|
|
43384
44165
|
var MASK_CHUNK_CHARS = 2e6;
|
|
43385
44166
|
function maskViaEngineDetection(repoDir, texts, o = {}) {
|
|
43386
44167
|
const sanitizer = new TextSanitizer();
|
|
43387
|
-
const sandbox =
|
|
44168
|
+
const sandbox = mkdtempSync5(join25(repoDir, BORDER_STATE_DIR, "llm-mask-"));
|
|
43388
44169
|
try {
|
|
43389
44170
|
let idx = 0;
|
|
43390
44171
|
for (const text of texts) {
|
|
43391
44172
|
if (text === "") continue;
|
|
43392
44173
|
for (let off = 0; off < text.length; off += MASK_CHUNK_CHARS, idx += 1) {
|
|
43393
|
-
|
|
44174
|
+
writeFileSync6(join25(sandbox, `detect-${String(idx)}.txt`), text.slice(off, off + MASK_CHUNK_CHARS), "utf8");
|
|
43394
44175
|
}
|
|
43395
44176
|
}
|
|
43396
44177
|
scanTree({
|
|
43397
44178
|
dir: sandbox,
|
|
43398
|
-
stateDir:
|
|
44179
|
+
stateDir: join25(repoDir, BORDER_STATE_DIR),
|
|
43399
44180
|
target: "llm-mask",
|
|
43400
44181
|
sanitizer,
|
|
43401
44182
|
...engineEnv(o)
|
|
43402
44183
|
});
|
|
43403
44184
|
} finally {
|
|
43404
|
-
|
|
44185
|
+
rmSync12(sandbox, { recursive: true, force: true });
|
|
43405
44186
|
}
|
|
43406
44187
|
return (text) => sanitizer.sanitize(text);
|
|
43407
44188
|
}
|
|
@@ -43453,18 +44234,18 @@ async function runLlmRequestCore(ctx) {
|
|
|
43453
44234
|
ctx.stderr("border: llm-request: no check record for the current state \u2014 run 'border check' first (the bundle derives from the recorded ctx)");
|
|
43454
44235
|
return EXIT_ERROR;
|
|
43455
44236
|
}
|
|
43456
|
-
const absReport =
|
|
43457
|
-
if (!
|
|
44237
|
+
const absReport = join26(repoDir, record2.reportPath);
|
|
44238
|
+
if (!existsSync16(absReport)) {
|
|
43458
44239
|
ctx.stderr(`border: llm-request: report archive missing (${record2.reportPath} pruned?) \u2014 run 'border check' again`);
|
|
43459
44240
|
return EXIT_ERROR;
|
|
43460
44241
|
}
|
|
43461
|
-
const report = JSON.parse(
|
|
44242
|
+
const report = JSON.parse(readFileSync16(absReport, "utf8"));
|
|
43462
44243
|
if (report.key !== record2.key) {
|
|
43463
44244
|
ctx.stderr("border: llm-request: report.json does not match its ledger record \u2014 run 'border check' again");
|
|
43464
44245
|
return EXIT_ERROR;
|
|
43465
44246
|
}
|
|
43466
44247
|
const templatePath = resolvePromptTemplatePath(ctx.env);
|
|
43467
|
-
if (!
|
|
44248
|
+
if (!existsSync16(templatePath)) {
|
|
43468
44249
|
ctx.stderr(`border: llm-request: MISSING_TEMPLATE \u2014 prompt template not found at ${templatePath}; refusing to emit an unanswerable bundle`);
|
|
43469
44250
|
return EXIT_ERROR;
|
|
43470
44251
|
}
|
|
@@ -43510,11 +44291,11 @@ async function runLlmRequestCore(ctx) {
|
|
|
43510
44291
|
fileDeltas,
|
|
43511
44292
|
artifacts: record2.artifacts,
|
|
43512
44293
|
deterministic: { verdict: report.verdict, counts: report.counts, findings: report.findings },
|
|
43513
|
-
promptTemplate: { path: templatePath, sha256:
|
|
44294
|
+
promptTemplate: { path: templatePath, sha256: createHash15("sha256").update(readFileSync16(templatePath)).digest("hex") }
|
|
43514
44295
|
};
|
|
43515
|
-
const outPath =
|
|
43516
|
-
|
|
43517
|
-
|
|
44296
|
+
const outPath = join26(dirname14(absReport), "llm-request.json");
|
|
44297
|
+
mkdirSync11(dirname14(outPath), { recursive: true });
|
|
44298
|
+
writeFileSync7(outPath, `${JSON.stringify(bundle, null, 2)}
|
|
43518
44299
|
`, "utf8");
|
|
43519
44300
|
const rel = outPath.slice(repoDir.length + 1);
|
|
43520
44301
|
ctx.stdout(`border: llm-request: wrote ${rel} (base ${base.mode}, ${String(fileDeltas.length)} file deltas, ${String(report.findings.length)} deterministic findings)`);
|
|
@@ -43522,14 +44303,14 @@ async function runLlmRequestCore(ctx) {
|
|
|
43522
44303
|
}
|
|
43523
44304
|
|
|
43524
44305
|
// src/llm/ingest.ts
|
|
43525
|
-
import { createHash as
|
|
43526
|
-
import { existsSync as
|
|
43527
|
-
import { isAbsolute, join as
|
|
44306
|
+
import { createHash as createHash16 } from "node:crypto";
|
|
44307
|
+
import { existsSync as existsSync17, readFileSync as readFileSync17, readdirSync as readdirSync9 } from "node:fs";
|
|
44308
|
+
import { isAbsolute, join as join27, resolve as resolve17 } from "node:path";
|
|
43528
44309
|
var RULE_RE = /^[a-z0-9-]+$/;
|
|
43529
44310
|
var MAX_TEXT = 4e3;
|
|
43530
44311
|
var ALLOWED_KEYS = /* @__PURE__ */ new Set(["rule", "severity", "target", "path", "line", "commit", "message", "snippet", "engine", "valueDigest"]);
|
|
43531
44312
|
function sha2562(text) {
|
|
43532
|
-
return
|
|
44313
|
+
return createHash16("sha256").update(text, "utf8").digest("hex");
|
|
43533
44314
|
}
|
|
43534
44315
|
function usageError(ctx, why) {
|
|
43535
44316
|
ctx.stderr(`border: llm-ingest: ${why}`);
|
|
@@ -43543,11 +44324,11 @@ function findLatestBundle(repoDir) {
|
|
|
43543
44324
|
return "no llm-request bundle found \u2014 run 'border llm-request' first";
|
|
43544
44325
|
}
|
|
43545
44326
|
for (const d of dirs.sort().reverse()) {
|
|
43546
|
-
const p =
|
|
43547
|
-
if (!
|
|
44327
|
+
const p = join27(runsDir(repoDir), d, "llm-request.json");
|
|
44328
|
+
if (!existsSync17(p)) continue;
|
|
43548
44329
|
let parsed;
|
|
43549
44330
|
try {
|
|
43550
|
-
parsed = JSON.parse(
|
|
44331
|
+
parsed = JSON.parse(readFileSync17(p, "utf8"));
|
|
43551
44332
|
} catch (err) {
|
|
43552
44333
|
return `bundle ${p} is not valid JSON (${err instanceof Error ? err.message : String(err)}) \u2014 re-run 'border llm-request'`;
|
|
43553
44334
|
}
|
|
@@ -43556,7 +44337,7 @@ function findLatestBundle(repoDir) {
|
|
|
43556
44337
|
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) {
|
|
43557
44338
|
return `bundle ${p} fails its own schema \u2014 re-run 'border llm-request'`;
|
|
43558
44339
|
}
|
|
43559
|
-
return { dir:
|
|
44340
|
+
return { dir: join27(runsDir(repoDir), d), bundle: b };
|
|
43560
44341
|
}
|
|
43561
44342
|
return "no llm-request bundle found \u2014 run 'border llm-request' first";
|
|
43562
44343
|
}
|
|
@@ -43607,10 +44388,10 @@ async function runLlmIngestCore(ctx) {
|
|
|
43607
44388
|
const found = findLatestBundle(repoDir);
|
|
43608
44389
|
if (typeof found === "string") return usageError(ctx, found);
|
|
43609
44390
|
const { bundle } = found;
|
|
43610
|
-
const findingsPath = isAbsolute(arg) ? arg :
|
|
44391
|
+
const findingsPath = isAbsolute(arg) ? arg : resolve17(ctx.cwd, arg);
|
|
43611
44392
|
let text;
|
|
43612
44393
|
try {
|
|
43613
|
-
text =
|
|
44394
|
+
text = readFileSync17(findingsPath, "utf8");
|
|
43614
44395
|
} catch {
|
|
43615
44396
|
return usageError(ctx, `findings file not found: ${arg}`);
|
|
43616
44397
|
}
|
|
@@ -43701,10 +44482,10 @@ async function runLlmRequest(ctx) {
|
|
|
43701
44482
|
}
|
|
43702
44483
|
|
|
43703
44484
|
// src/commands/opencode.ts
|
|
43704
|
-
import { createHash as
|
|
43705
|
-
import { existsSync as
|
|
44485
|
+
import { createHash as createHash17 } from "node:crypto";
|
|
44486
|
+
import { existsSync as existsSync18, lstatSync, mkdirSync as mkdirSync12, readFileSync as readFileSync18, rmSync as rmSync13, writeFileSync as writeFileSync8 } from "node:fs";
|
|
43706
44487
|
import os3 from "node:os";
|
|
43707
|
-
import { dirname as
|
|
44488
|
+
import { dirname as dirname15, join as join28 } from "node:path";
|
|
43708
44489
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
43709
44490
|
var PLUGIN_MARKER = "// border-opencode-plugin v";
|
|
43710
44491
|
var COMMAND_MARKER = "<!-- border-opencode-command -->";
|
|
@@ -43712,27 +44493,27 @@ function opencodeConfigRoot(env2) {
|
|
|
43712
44493
|
const xdg = env2["XDG_CONFIG_HOME"] ?? "";
|
|
43713
44494
|
if (xdg.length > 0) return xdg;
|
|
43714
44495
|
const home = env2["HOME"] ?? "";
|
|
43715
|
-
return
|
|
44496
|
+
return join28(home.length > 0 ? home : os3.homedir(), ".config");
|
|
43716
44497
|
}
|
|
43717
44498
|
function resolvePluginAsset(moduleUrl, fileName) {
|
|
43718
|
-
const here =
|
|
44499
|
+
const here = dirname15(fileURLToPath5(moduleUrl));
|
|
43719
44500
|
const candidates = [
|
|
43720
|
-
|
|
44501
|
+
join28(here, "..", "plugin", fileName),
|
|
43721
44502
|
// dist mode (bundled): <root>/plugin/<f>
|
|
43722
|
-
|
|
44503
|
+
join28(here, "..", "..", "plugin", fileName)
|
|
43723
44504
|
// src mode: <root>/plugin/<f>
|
|
43724
44505
|
];
|
|
43725
|
-
return candidates.find((p) =>
|
|
44506
|
+
return candidates.find((p) => existsSync18(p)) ?? candidates[1];
|
|
43726
44507
|
}
|
|
43727
44508
|
function targetsFor(env2) {
|
|
43728
|
-
const root =
|
|
44509
|
+
const root = join28(opencodeConfigRoot(env2), "opencode");
|
|
43729
44510
|
return [
|
|
43730
|
-
{ fileName: "border.ts", destination:
|
|
43731
|
-
{ fileName: "border-command.md", destination:
|
|
44511
|
+
{ fileName: "border.ts", destination: join28(root, "plugins", "border.ts"), marker: PLUGIN_MARKER },
|
|
44512
|
+
{ fileName: "border-command.md", destination: join28(root, "commands", "border.md"), marker: COMMAND_MARKER }
|
|
43732
44513
|
];
|
|
43733
44514
|
}
|
|
43734
44515
|
function sha2563(bytes) {
|
|
43735
|
-
return
|
|
44516
|
+
return createHash17("sha256").update(bytes).digest("hex");
|
|
43736
44517
|
}
|
|
43737
44518
|
function firstLine(bytes) {
|
|
43738
44519
|
const text = bytes.toString("utf8");
|
|
@@ -43749,7 +44530,7 @@ function fail(ctx, message) {
|
|
|
43749
44530
|
function readPackagedAsset(ctx, fileName) {
|
|
43750
44531
|
const source = resolvePluginAsset(import.meta.url, fileName);
|
|
43751
44532
|
try {
|
|
43752
|
-
return
|
|
44533
|
+
return readFileSync18(source);
|
|
43753
44534
|
} catch {
|
|
43754
44535
|
ctx.stderr(`border: opencode: packaged asset is missing (${source})`);
|
|
43755
44536
|
ctx.stderr("border: opencode: the border install looks broken \u2014 `npm i -g border-customs` or a fresh checkout should carry plugin/.");
|
|
@@ -43758,7 +44539,7 @@ function readPackagedAsset(ctx, fileName) {
|
|
|
43758
44539
|
}
|
|
43759
44540
|
function readTarget(destination) {
|
|
43760
44541
|
try {
|
|
43761
|
-
return
|
|
44542
|
+
return readFileSync18(destination);
|
|
43762
44543
|
} catch {
|
|
43763
44544
|
return null;
|
|
43764
44545
|
}
|
|
@@ -43825,8 +44606,8 @@ function opencodeInstall(ctx) {
|
|
|
43825
44606
|
ctx.stdout(`${target.destination}: up to date`);
|
|
43826
44607
|
continue;
|
|
43827
44608
|
}
|
|
43828
|
-
|
|
43829
|
-
|
|
44609
|
+
mkdirSync12(dirname15(target.destination), { recursive: true });
|
|
44610
|
+
writeFileSync8(target.destination, packaged);
|
|
43830
44611
|
ctx.stdout(`${target.destination}: ${state === "outdated" ? "updated" : "installed"}`);
|
|
43831
44612
|
}
|
|
43832
44613
|
ctx.stdout("note: restart opencode to load the plugin \u2014 tools and commands are scanned at startup.");
|
|
@@ -43860,7 +44641,7 @@ function opencodeUninstall(ctx) {
|
|
|
43860
44641
|
`refusing to remove ${target.destination} \u2014 the file is not ours (its first line lacks the marker '${target.marker}'); nothing was removed anywhere`
|
|
43861
44642
|
);
|
|
43862
44643
|
}
|
|
43863
|
-
|
|
44644
|
+
rmSync13(target.destination);
|
|
43864
44645
|
ctx.stdout(`${target.destination}: removed`);
|
|
43865
44646
|
}
|
|
43866
44647
|
ctx.stdout("note: restart opencode for the tool and command to disappear.");
|
|
@@ -43888,7 +44669,7 @@ function runOpencode(ctx) {
|
|
|
43888
44669
|
}
|
|
43889
44670
|
|
|
43890
44671
|
// src/push/git.ts
|
|
43891
|
-
import { spawnSync as
|
|
44672
|
+
import { spawnSync as spawnSync17 } from "node:child_process";
|
|
43892
44673
|
var DIVERGED_MESSAGE = "history diverged; border never force-pushes; resolve manually";
|
|
43893
44674
|
function pushArgv(url2, branch, mode) {
|
|
43894
44675
|
return mode === "dry-run" ? ["push", "--dry-run", url2, branch, "--follow-tags"] : ["push", url2, branch, "--follow-tags"];
|
|
@@ -43923,7 +44704,7 @@ function lsRemoteMap(repoDir, remote, env2) {
|
|
|
43923
44704
|
return parseLsRemote(text, remote.target);
|
|
43924
44705
|
}
|
|
43925
44706
|
function isAncestor(repoDir, remoteSha, localSha, env2) {
|
|
43926
|
-
const r =
|
|
44707
|
+
const r = spawnSync17("git", ["-C", repoDir, "merge-base", "--is-ancestor", remoteSha, localSha], {
|
|
43927
44708
|
encoding: "utf8",
|
|
43928
44709
|
env: { ...env2, GIT_CEILING_DIRECTORIES: repoDir }
|
|
43929
44710
|
});
|
|
@@ -43958,7 +44739,7 @@ function pushTimeoutMs(env2) {
|
|
|
43958
44739
|
}
|
|
43959
44740
|
function executePush(i) {
|
|
43960
44741
|
const timeoutMs = pushTimeoutMs(i.env);
|
|
43961
|
-
const r =
|
|
44742
|
+
const r = spawnSync17("git", ["-C", i.repoDir, ...pushArgv(i.remote.url, i.branch, "execute")], {
|
|
43962
44743
|
stdio: "inherit",
|
|
43963
44744
|
env: i.env,
|
|
43964
44745
|
timeout: timeoutMs
|
|
@@ -43983,6 +44764,101 @@ function confirmRemoteBranch(i) {
|
|
|
43983
44764
|
return sha;
|
|
43984
44765
|
}
|
|
43985
44766
|
|
|
44767
|
+
// src/push/landing.ts
|
|
44768
|
+
import { spawnSync as spawnSync18 } from "node:child_process";
|
|
44769
|
+
import { mkdtempSync as mkdtempSync6, rmSync as rmSync14 } from "node:fs";
|
|
44770
|
+
import { tmpdir as tmpdir4 } from "node:os";
|
|
44771
|
+
import { dirname as dirname16, join as join29, resolve as resolve18 } from "node:path";
|
|
44772
|
+
var LANDINGS_REF_PREFIX = "refs/border-landings/";
|
|
44773
|
+
async function runLandingVerification(o) {
|
|
44774
|
+
if (o.legs.length === 0) return null;
|
|
44775
|
+
const rulesHash = landingRulesHash(o);
|
|
44776
|
+
const observe = o.observeRemoteTip ?? ((remote, branch) => defaultObserveRemoteTip(o.repoDir, remote, branch, o.env));
|
|
44777
|
+
const scan = o.scanRemoteTip ?? ((remote, branch, sha) => defaultScanRemoteTip(o, remote, branch, sha));
|
|
44778
|
+
const sink = o.record ?? ((r) => appendRecord(o.repoDir, r));
|
|
44779
|
+
for (const leg of o.legs) {
|
|
44780
|
+
const sha = observe(leg.target, leg.branch);
|
|
44781
|
+
if (sha === null || sha === "") {
|
|
44782
|
+
o.err(`border: ${leg.target.target} landed, verification unavailable (remote tip unfetchable) \u2014 exit 2; PASS not endorsed for the public face`);
|
|
44783
|
+
return EXIT_ERROR;
|
|
44784
|
+
}
|
|
44785
|
+
let findings;
|
|
44786
|
+
try {
|
|
44787
|
+
findings = await scan(leg.target, leg.branch, sha);
|
|
44788
|
+
} catch (err) {
|
|
44789
|
+
o.err(
|
|
44790
|
+
`border: ${leg.target.target} landed, verification unavailable (remote tip fetch/scan failed: ${err instanceof Error ? err.message : String(err)}; first bytes: ${sha.slice(0, 8)}) \u2014 exit 2; PASS not endorsed for the public face`
|
|
44791
|
+
);
|
|
44792
|
+
return EXIT_ERROR;
|
|
44793
|
+
}
|
|
44794
|
+
const blocking = findings.filter((f) => isBlocking(f.severity)).length;
|
|
44795
|
+
const verdict = blocking > 0 ? "blocked" : "clean";
|
|
44796
|
+
sink(buildLandingRecord({ key: o.key, target: leg.target.target, ref: `refs/heads/${leg.branch}`, remoteSha: sha, verdict, blocking, rulesHash }));
|
|
44797
|
+
if (blocking > 0) {
|
|
44798
|
+
o.err(`border: ${leg.target.target} ALREADY PUBLIC; border detects, never erases \u2014 ${String(blocking)} blocking finding(s) on the landed tip ${sha.slice(0, 12)}`);
|
|
44799
|
+
for (const f of findings.filter((x) => isBlocking(x.severity))) {
|
|
44800
|
+
o.err(`border: ${f.severity} ${f.rule} [${f.engine}] ${f.path ?? f.commit ?? f.target}${f.line !== void 0 ? `:${String(f.line)}` : ""}`);
|
|
44801
|
+
}
|
|
44802
|
+
return EXIT_BLOCKED;
|
|
44803
|
+
}
|
|
44804
|
+
o.out(`border: ${leg.target.target} landing verified \u2014 remote tip ${sha.slice(0, 12)} clean (t:${verdict} record appended)`);
|
|
44805
|
+
}
|
|
44806
|
+
return null;
|
|
44807
|
+
}
|
|
44808
|
+
function landingRulesHash(o) {
|
|
44809
|
+
const { records } = readLedger(o.repoDir);
|
|
44810
|
+
const pass = latestPassCoveringTargets(records, o.key, ["git"]);
|
|
44811
|
+
if (pass === null) {
|
|
44812
|
+
throw new Error(`landing: no PASS record under key ${o.key.slice(0, 8)} \u2014 refusing to record a landing fact against unknown rules`);
|
|
44813
|
+
}
|
|
44814
|
+
return pass.rulesHash;
|
|
44815
|
+
}
|
|
44816
|
+
function gitPlain(repoDir, env2, args) {
|
|
44817
|
+
const clean = {};
|
|
44818
|
+
for (const [name, value] of Object.entries(env2)) {
|
|
44819
|
+
if (!name.startsWith("GIT_")) clean[name] = value;
|
|
44820
|
+
}
|
|
44821
|
+
clean.GIT_CEILING_DIRECTORIES = dirname16(resolve18(repoDir));
|
|
44822
|
+
clean.GIT_OPTIONAL_LOCKS = "0";
|
|
44823
|
+
clean.GIT_TERMINAL_PROMPT = "0";
|
|
44824
|
+
const r = spawnSync18("git", ["-C", resolve18(repoDir), ...args], { encoding: "utf8", env: clean, timeout: 55e3, maxBuffer: 32 * 1024 * 1024 });
|
|
44825
|
+
return { status: r.status ?? 1, stdout: r.stdout ?? "" };
|
|
44826
|
+
}
|
|
44827
|
+
function defaultObserveRemoteTip(repoDir, remote, branch, env2) {
|
|
44828
|
+
const r = gitPlain(repoDir, env2, ["ls-remote", remote.url, `refs/heads/${branch}`]);
|
|
44829
|
+
if (r.status !== 0) return null;
|
|
44830
|
+
const sha = /^[0-9a-f]{40}/.exec(r.stdout)?.[0] ?? null;
|
|
44831
|
+
return sha;
|
|
44832
|
+
}
|
|
44833
|
+
async function defaultScanRemoteTip(o, remote, branch, remoteSha) {
|
|
44834
|
+
const git = makeGit(o.repoDir, o.env);
|
|
44835
|
+
let rev = remoteSha;
|
|
44836
|
+
let ownedTempRef = null;
|
|
44837
|
+
try {
|
|
44838
|
+
try {
|
|
44839
|
+
git.run(["cat-file", "-e", `${remoteSha}^{object}`]);
|
|
44840
|
+
} catch {
|
|
44841
|
+
const landingRef = `${LANDINGS_REF_PREFIX}${remote.target.replace(/[^A-Za-z0-9_.-]/g, "-")}`;
|
|
44842
|
+
git.run(["fetch", "-q", "--no-tags", remote.url, `refs/heads/${branch}:${landingRef}`]);
|
|
44843
|
+
ownedTempRef = landingRef;
|
|
44844
|
+
rev = git.run(["rev-parse", landingRef]).trim();
|
|
44845
|
+
}
|
|
44846
|
+
const treeRoot = mkdtempSync6(join29(tmpdir4(), "border-landing-tree-"));
|
|
44847
|
+
try {
|
|
44848
|
+
return await scanTipTree(git, treeRoot, rev, o.env);
|
|
44849
|
+
} finally {
|
|
44850
|
+
rmSync14(treeRoot, { recursive: true, force: true });
|
|
44851
|
+
}
|
|
44852
|
+
} finally {
|
|
44853
|
+
if (ownedTempRef !== null) {
|
|
44854
|
+
try {
|
|
44855
|
+
git.run(["update-ref", "-d", ownedTempRef]);
|
|
44856
|
+
} catch {
|
|
44857
|
+
}
|
|
44858
|
+
}
|
|
44859
|
+
}
|
|
44860
|
+
}
|
|
44861
|
+
|
|
43986
44862
|
// src/commands/push.ts
|
|
43987
44863
|
var DRY_RUN_PREFIX = "DRY-RUN";
|
|
43988
44864
|
var GIT_PUSH_DRY_RUN = "git push --dry-run";
|
|
@@ -44001,6 +44877,18 @@ async function runPush(ctx) {
|
|
|
44001
44877
|
});
|
|
44002
44878
|
if (loaded.kind === "no-op") {
|
|
44003
44879
|
for (const warning of loaded.warnings) ctx.stderr(`border: ${warning}`);
|
|
44880
|
+
let orphans = [];
|
|
44881
|
+
try {
|
|
44882
|
+
orphans = remoteTrackingDrift(resolveRepoDir(ctx.cwd, { env: ctx.env })).orphans;
|
|
44883
|
+
} catch {
|
|
44884
|
+
orphans = [];
|
|
44885
|
+
}
|
|
44886
|
+
if (orphans.length > 0) {
|
|
44887
|
+
ctx.stderr(
|
|
44888
|
+
`border: remote config drift \u2014 remote-tracking refs exist for UNCONFIGURED remotes: ${orphans.join(", ")}; their remote.* entries are gone (wiped or partial clone). Restore them or prune the refs; refusing to report this as a no-op.`
|
|
44889
|
+
);
|
|
44890
|
+
return EXIT_ERROR;
|
|
44891
|
+
}
|
|
44004
44892
|
ctx.stdout("border: no targets discovered \u2014 nothing to push (NO-OP)");
|
|
44005
44893
|
return EXIT_PASS;
|
|
44006
44894
|
}
|
|
@@ -44134,6 +45022,15 @@ async function executeYesPush(ctx, loaded) {
|
|
|
44134
45022
|
done.push(leg.target);
|
|
44135
45023
|
ctx.stdout(`border: pushed ${leg.target} \u2014 ${shortSha(remoteSha)} confirmed via ls-remote`);
|
|
44136
45024
|
}
|
|
45025
|
+
const landing = await runLandingVerification({
|
|
45026
|
+
repoDir,
|
|
45027
|
+
key: state.key,
|
|
45028
|
+
legs: legs.map((leg) => ({ target: leg, branch })),
|
|
45029
|
+
env: env2,
|
|
45030
|
+
out: ctx.stdout,
|
|
45031
|
+
err: ctx.stderr
|
|
45032
|
+
});
|
|
45033
|
+
if (landing !== null) return landing;
|
|
44137
45034
|
}
|
|
44138
45035
|
for (const channel of publishChannels()) {
|
|
44139
45036
|
if (!pending.some((t) => t.kind === channel.id)) continue;
|
|
@@ -44152,17 +45049,17 @@ async function executeYesPush(ctx, loaded) {
|
|
|
44152
45049
|
}
|
|
44153
45050
|
|
|
44154
45051
|
// src/roundtrip/orchestrate.ts
|
|
44155
|
-
import { mkdtempSync as
|
|
45052
|
+
import { mkdtempSync as mkdtempSync7, rmSync as rmSync15, writeFileSync as writeFileSync9 } from "node:fs";
|
|
44156
45053
|
import { randomBytes } from "node:crypto";
|
|
44157
|
-
import { tmpdir as
|
|
44158
|
-
import { join as
|
|
45054
|
+
import { tmpdir as tmpdir5 } from "node:os";
|
|
45055
|
+
import { join as join30 } from "node:path";
|
|
44159
45056
|
|
|
44160
45057
|
// src/scan/fetch.ts
|
|
44161
|
-
import { createHash as
|
|
45058
|
+
import { createHash as createHash18 } from "node:crypto";
|
|
44162
45059
|
import { basename as basename5 } from "node:path";
|
|
44163
45060
|
var SCAN_MAX_ARTIFACT_BYTES = 200 * 1024 * 1024;
|
|
44164
45061
|
var SCAN_REQUEST_TIMEOUT_MS = 6e4;
|
|
44165
|
-
var SCAN_USER_AGENT = "border-customs/0.
|
|
45062
|
+
var SCAN_USER_AGENT = "border-customs/0.7.1 (+https://github.com/sumteclab/border; border scan)";
|
|
44166
45063
|
var CRATES_DOWNLOAD_HOST = "https://static.crates.io";
|
|
44167
45064
|
var defaultScanFetcher = async (url2, init) => {
|
|
44168
45065
|
if (!/^https?:\/\//.test(url2)) throw new EngineRunError(`border scan refuses non-http(s) URL: ${url2}`, null);
|
|
@@ -44287,8 +45184,8 @@ async function fetchArtifact(spec, o) {
|
|
|
44287
45184
|
}
|
|
44288
45185
|
}
|
|
44289
45186
|
}
|
|
44290
|
-
function
|
|
44291
|
-
return
|
|
45187
|
+
function sha256Hex3(data) {
|
|
45188
|
+
return createHash18("sha256").update(data).digest("hex");
|
|
44292
45189
|
}
|
|
44293
45190
|
|
|
44294
45191
|
// src/scan/spec.ts
|
|
@@ -44331,9 +45228,9 @@ function parseScanSpec(raw) {
|
|
|
44331
45228
|
}
|
|
44332
45229
|
|
|
44333
45230
|
// src/roundtrip/localArtifact.ts
|
|
44334
|
-
import { createReadStream, existsSync as
|
|
44335
|
-
import { createHash as
|
|
44336
|
-
import { basename as basename6, resolve as
|
|
45231
|
+
import { createReadStream, existsSync as existsSync19, statSync as statSync3 } from "node:fs";
|
|
45232
|
+
import { createHash as createHash19 } from "node:crypto";
|
|
45233
|
+
import { basename as basename6, resolve as resolve19 } from "node:path";
|
|
44337
45234
|
var EXT_TABLE = /* @__PURE__ */ new Map([
|
|
44338
45235
|
[".whl", "pypi"],
|
|
44339
45236
|
[".tgz", "npm"],
|
|
@@ -44379,9 +45276,9 @@ var GRAMMAR_HINT = {
|
|
|
44379
45276
|
rubygems: "gem package names, e.g. rt-gem-1.0.0.gem"
|
|
44380
45277
|
};
|
|
44381
45278
|
function classifyLocalInput(raw, cwd, parsesAsRegistrySpec) {
|
|
44382
|
-
const abs =
|
|
44383
|
-
if (
|
|
44384
|
-
const st =
|
|
45279
|
+
const abs = resolve19(cwd, raw);
|
|
45280
|
+
if (existsSync19(abs)) {
|
|
45281
|
+
const st = statSync3(abs);
|
|
44385
45282
|
if (st.isDirectory()) {
|
|
44386
45283
|
bad3(`'${raw}' resolves to a directory (${abs}) \u2014 local mode needs a single artifact FILE, not a directory`);
|
|
44387
45284
|
}
|
|
@@ -44415,7 +45312,7 @@ function classifyLocalInput(raw, cwd, parsesAsRegistrySpec) {
|
|
|
44415
45312
|
return null;
|
|
44416
45313
|
}
|
|
44417
45314
|
async function readLocalArtifact(absPath, cap = SCAN_MAX_ARTIFACT_BYTES) {
|
|
44418
|
-
const hash2 =
|
|
45315
|
+
const hash2 = createHash19("sha256");
|
|
44419
45316
|
const parts = [];
|
|
44420
45317
|
let total = 0;
|
|
44421
45318
|
for await (const chunk of createReadStream(absPath)) {
|
|
@@ -44641,7 +45538,7 @@ var CLAIM_SCANNER_PY = [
|
|
|
44641
45538
|
].join("\n");
|
|
44642
45539
|
|
|
44643
45540
|
// src/roundtrip/manifest.ts
|
|
44644
|
-
import { createHash as
|
|
45541
|
+
import { createHash as createHash20 } from "node:crypto";
|
|
44645
45542
|
var RT_ENGINE = "roundtrip";
|
|
44646
45543
|
var RT_RESIDUE_RULE = "roundtrip-residue-file";
|
|
44647
45544
|
var RT_MODIFY_RULE = "roundtrip-modified-file";
|
|
@@ -44692,7 +45589,7 @@ var PERSISTENCE_PATTERNS = [
|
|
|
44692
45589
|
function isPersistenceSurface(path4) {
|
|
44693
45590
|
return PERSISTENCE_PATTERNS.some((re) => re.test(path4));
|
|
44694
45591
|
}
|
|
44695
|
-
var digestOf = (what) =>
|
|
45592
|
+
var digestOf = (what) => createHash20("sha256").update(what).digest("hex");
|
|
44696
45593
|
function classifyResidue(label, diff, attribution = null) {
|
|
44697
45594
|
const findings = [];
|
|
44698
45595
|
for (const e of diff.added) {
|
|
@@ -44739,7 +45636,7 @@ function classifyResidue(label, diff, attribution = null) {
|
|
|
44739
45636
|
|
|
44740
45637
|
// src/roundtrip/orchestrate.ts
|
|
44741
45638
|
function containerNameFor(label, rand = randomBytes(3).toString("hex")) {
|
|
44742
|
-
return `rt-${
|
|
45639
|
+
return `rt-${sha256Hex3(label).slice(0, 8)}-${rand}`;
|
|
44743
45640
|
}
|
|
44744
45641
|
var shArgv = (name) => {
|
|
44745
45642
|
if (!/^[A-Za-z0-9@._+/-]+$/.test(name)) {
|
|
@@ -44860,7 +45757,7 @@ function runPipeline2(exec, profile, spec, label, hostPath, container, closure,
|
|
|
44860
45757
|
return {
|
|
44861
45758
|
findings: classifyResidue(label, diffManifests(m1, m3), attribution),
|
|
44862
45759
|
installDelta,
|
|
44863
|
-
baselineDigest:
|
|
45760
|
+
baselineDigest: sha256Hex3([...m1.keys()].join("\n"))
|
|
44864
45761
|
};
|
|
44865
45762
|
} finally {
|
|
44866
45763
|
try {
|
|
@@ -44894,25 +45791,25 @@ async function runRoundtripCore(ctx, deps = {}) {
|
|
|
44894
45791
|
ctx.stderr("roundtrip: docker unavailable \u2014 cannot verify");
|
|
44895
45792
|
return EXIT_ERROR;
|
|
44896
45793
|
}
|
|
44897
|
-
const baseDir =
|
|
45794
|
+
const baseDir = mkdtempSync7(join30(tmpdir5(), "border-roundtrip-"));
|
|
44898
45795
|
try {
|
|
44899
45796
|
const localRead = local !== null ? await readLocalArtifact(local.absPath) : null;
|
|
44900
45797
|
const bytes = localRead?.bytes ?? (await fetchArtifact(spec, { fetcher: deps.fetcher ?? defaultScanFetcher })).bytes;
|
|
44901
|
-
const artifactSha256 = localRead !== null ? localRead.sha256 :
|
|
44902
|
-
const hostPath =
|
|
44903
|
-
|
|
45798
|
+
const artifactSha256 = localRead !== null ? localRead.sha256 : sha256Hex3(bytes);
|
|
45799
|
+
const hostPath = join30(baseDir, "artifact");
|
|
45800
|
+
writeFileSync9(hostPath, bytes);
|
|
44904
45801
|
const container = containerNameFor(label);
|
|
44905
45802
|
const closure = spec.ecosystem === "pypi" ? resolvePipClosure(exec, profile, hostPath, label, spec) : null;
|
|
44906
45803
|
const { findings, installDelta, baselineDigest } = runPipeline2(exec, profile, spec, label, hostPath, container, closure, (msg) => ctx.stderr(msg));
|
|
44907
45804
|
const verdict = computeVerdict(findings);
|
|
44908
45805
|
const report = {
|
|
44909
45806
|
schemaVersion: 1,
|
|
44910
|
-
key:
|
|
45807
|
+
key: sha256Hex3(`roundtrip:${label}:${baselineDigest}:${verdict}`),
|
|
44911
45808
|
head: baselineDigest.slice(0, 40),
|
|
44912
45809
|
dirty: false,
|
|
44913
45810
|
exposureSet: [label],
|
|
44914
45811
|
refSet: [],
|
|
44915
|
-
rulesHash:
|
|
45812
|
+
rulesHash: sha256Hex3(`${profile.image}|${profile.install(profile.artifactPath, spec).join(" ")}|${profile.uninstall(spec).join(" ")}`),
|
|
44916
45813
|
verdict,
|
|
44917
45814
|
counts: countFindings(findings),
|
|
44918
45815
|
findings,
|
|
@@ -44944,7 +45841,7 @@ async function runRoundtripCore(ctx, deps = {}) {
|
|
|
44944
45841
|
}
|
|
44945
45842
|
return exitCodeFromVerdict(verdict);
|
|
44946
45843
|
} finally {
|
|
44947
|
-
|
|
45844
|
+
rmSync15(baseDir, { recursive: true, force: true });
|
|
44948
45845
|
}
|
|
44949
45846
|
}
|
|
44950
45847
|
function parsesRegistrySpec(raw) {
|
|
@@ -45000,9 +45897,9 @@ function dockerAvailableGuard(exec) {
|
|
|
45000
45897
|
var runRoundtrip = (ctx) => runRoundtripCore(ctx);
|
|
45001
45898
|
|
|
45002
45899
|
// src/scan/scan.ts
|
|
45003
|
-
import { mkdtempSync as
|
|
45004
|
-
import { tmpdir as
|
|
45005
|
-
import { join as
|
|
45900
|
+
import { mkdtempSync as mkdtempSync8 } from "node:fs";
|
|
45901
|
+
import { tmpdir as tmpdir6 } from "node:os";
|
|
45902
|
+
import { join as join32 } from "node:path";
|
|
45006
45903
|
|
|
45007
45904
|
// src/scan/config.ts
|
|
45008
45905
|
function buildScanConfigYaml(eco) {
|
|
@@ -45029,8 +45926,8 @@ function synthesizeScanConfig(eco) {
|
|
|
45029
45926
|
}
|
|
45030
45927
|
|
|
45031
45928
|
// src/scan/materialize.ts
|
|
45032
|
-
import { existsSync as
|
|
45033
|
-
import { join as
|
|
45929
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync13, readdirSync as readdirSync10, readFileSync as readFileSync19, rmSync as rmSync16, writeFileSync as writeFileSync10 } from "node:fs";
|
|
45930
|
+
import { join as join31 } from "node:path";
|
|
45034
45931
|
var CRATES_ENVELOPE_FILES = ["Cargo.toml.orig", ".cargo-ok", ".cargo_vcs_info.json"];
|
|
45035
45932
|
var fail3 = (why) => {
|
|
45036
45933
|
throw new Error(`border scan cannot materialize the artifact (${why}) \u2014 refusing to stage an unusable tree (fail-closed)`);
|
|
@@ -45047,12 +45944,12 @@ function unwrapSingleDir(dir) {
|
|
|
45047
45944
|
const entries = readdirSync10(dir, { withFileTypes: true });
|
|
45048
45945
|
const dirs = entries.filter((e) => e.isDirectory());
|
|
45049
45946
|
if (entries.length === 1 && dirs.length === 1 && dirs[0] !== void 0) {
|
|
45050
|
-
return
|
|
45947
|
+
return join31(dir, dirs[0].name);
|
|
45051
45948
|
}
|
|
45052
45949
|
return dir;
|
|
45053
45950
|
}
|
|
45054
45951
|
function requireManifest(repoDir, ecosystem, candidates) {
|
|
45055
|
-
if (!candidates.some((rel) =>
|
|
45952
|
+
if (!candidates.some((rel) => existsSync20(join31(repoDir, rel)))) {
|
|
45056
45953
|
fail3(`${ecosystem} package tree has none of [${candidates.join(", ")}] at its root`);
|
|
45057
45954
|
}
|
|
45058
45955
|
}
|
|
@@ -45062,19 +45959,19 @@ function materializePackage(o) {
|
|
|
45062
45959
|
`);
|
|
45063
45960
|
});
|
|
45064
45961
|
const safe = safeArtifactName(o.filename, `${o.ecosystem}-artifact`);
|
|
45065
|
-
const archive =
|
|
45066
|
-
|
|
45067
|
-
const extractDir =
|
|
45068
|
-
|
|
45962
|
+
const archive = join31(o.baseDir, safe);
|
|
45963
|
+
writeFileSync10(archive, o.bytes);
|
|
45964
|
+
const extractDir = join31(o.baseDir, "extract");
|
|
45965
|
+
mkdirSync13(extractDir, { recursive: true });
|
|
45069
45966
|
extractFlat(archive, extractDir);
|
|
45070
45967
|
let repoDir = unwrapSingleDir(extractDir);
|
|
45071
45968
|
if (o.ecosystem === "rubygems") {
|
|
45072
45969
|
const outer = repoDir;
|
|
45073
45970
|
const dataCandidates = findNativeMissArchives(outer).filter((f) => f.endsWith("data.tar.gz"));
|
|
45074
|
-
const inner = dataCandidates[0] ??
|
|
45075
|
-
if (!
|
|
45076
|
-
const gemTree =
|
|
45077
|
-
|
|
45971
|
+
const inner = dataCandidates[0] ?? join31(outer, "data.tar.gz");
|
|
45972
|
+
if (!existsSync20(inner)) fail3(".gem archive contains no data.tar.gz");
|
|
45973
|
+
const gemTree = join31(o.baseDir, "gem-tree");
|
|
45974
|
+
mkdirSync13(gemTree, { recursive: true });
|
|
45078
45975
|
extractFlat(inner, gemTree);
|
|
45079
45976
|
repoDir = unwrapSingleDir(gemTree);
|
|
45080
45977
|
} else if (o.ecosystem === "npm") {
|
|
@@ -45084,11 +45981,11 @@ function materializePackage(o) {
|
|
|
45084
45981
|
} else {
|
|
45085
45982
|
requireManifest(repoDir, o.ecosystem, ["Cargo.toml"]);
|
|
45086
45983
|
for (const file2 of CRATES_ENVELOPE_FILES) {
|
|
45087
|
-
const path4 =
|
|
45088
|
-
if (!
|
|
45089
|
-
const original =
|
|
45090
|
-
|
|
45091
|
-
note(`border scan: crates envelope normalized: ${file2} sha256=${
|
|
45984
|
+
const path4 = join31(repoDir, file2);
|
|
45985
|
+
if (!existsSync20(path4)) continue;
|
|
45986
|
+
const original = readFileSync19(path4);
|
|
45987
|
+
rmSync16(path4, { force: true });
|
|
45988
|
+
note(`border scan: crates envelope normalized: ${file2} sha256=${sha256Hex3(original)}`);
|
|
45092
45989
|
}
|
|
45093
45990
|
}
|
|
45094
45991
|
const env2 = o.env;
|
|
@@ -45114,7 +46011,7 @@ async function runScanCore(ctx, deps = {}) {
|
|
|
45114
46011
|
}
|
|
45115
46012
|
const cfg = synthesizeScanConfig(spec.ecosystem);
|
|
45116
46013
|
const fetcher = deps.fetcher ?? defaultScanFetcher;
|
|
45117
|
-
const baseDir =
|
|
46014
|
+
const baseDir = mkdtempSync8(join32(tmpdir6(), "border-scan-"));
|
|
45118
46015
|
try {
|
|
45119
46016
|
const { bytes, filename } = await fetchArtifact(spec, { fetcher });
|
|
45120
46017
|
const env2 = { ...ctx.env };
|
|
@@ -45125,12 +46022,12 @@ async function runScanCore(ctx, deps = {}) {
|
|
|
45125
46022
|
const verdict = computeVerdict(findings);
|
|
45126
46023
|
const report = {
|
|
45127
46024
|
schemaVersion: 1,
|
|
45128
|
-
key:
|
|
46025
|
+
key: sha256Hex3(`scan:${spec.ecosystem}:${spec.name}@${spec.version}:${head}`),
|
|
45129
46026
|
head,
|
|
45130
46027
|
dirty: false,
|
|
45131
46028
|
exposureSet: [`${spec.ecosystem}:${spec.name}@${spec.version}`],
|
|
45132
46029
|
refSet: [],
|
|
45133
|
-
rulesHash:
|
|
46030
|
+
rulesHash: sha256Hex3(buildScanConfigYaml(spec.ecosystem)),
|
|
45134
46031
|
verdict,
|
|
45135
46032
|
counts: countFindings(findings),
|
|
45136
46033
|
findings,
|
|
@@ -45193,6 +46090,14 @@ function runStatus(ctx) {
|
|
|
45193
46090
|
return EXIT_ERROR;
|
|
45194
46091
|
}
|
|
45195
46092
|
for (const w of ledger.warnings) ctx.stderr(`border: WARNING ${w}`);
|
|
46093
|
+
try {
|
|
46094
|
+
const drift = remoteTrackingDrift(repoDir);
|
|
46095
|
+
const names = drift.configured.length > 0 ? drift.configured.join(", ") : "(none configured \u2014 nothing can ever be pushed)";
|
|
46096
|
+
const orphanNote = drift.orphans.length > 0 ? ` \u2014 DRIFT: tracking refs without config for: ${drift.orphans.join(", ")}` : "";
|
|
46097
|
+
ctx.stdout(`git remotes: ${names}${orphanNote}`);
|
|
46098
|
+
} catch {
|
|
46099
|
+
ctx.stdout("git remotes: (unknown \u2014 not a git repo?)");
|
|
46100
|
+
}
|
|
45196
46101
|
let newest;
|
|
45197
46102
|
for (const r of ledger.records) if (r.t === "check") newest = r;
|
|
45198
46103
|
if (newest === void 0) {
|
|
@@ -45240,11 +46145,12 @@ var handlers = {
|
|
|
45240
46145
|
"llm-ingest": runLlmIngest,
|
|
45241
46146
|
scan: runScan2,
|
|
45242
46147
|
roundtrip: runRoundtrip,
|
|
45243
|
-
opencode: runOpencode
|
|
46148
|
+
opencode: runOpencode,
|
|
46149
|
+
exfil: runExfil
|
|
45244
46150
|
};
|
|
45245
46151
|
|
|
45246
46152
|
// src/cli/types.ts
|
|
45247
|
-
var SUBCOMMANDS = ["check", "push", "status", "llm-request", "llm-ingest", "scan", "roundtrip", "opencode"];
|
|
46153
|
+
var SUBCOMMANDS = ["check", "push", "status", "llm-request", "llm-ingest", "scan", "roundtrip", "opencode", "exfil"];
|
|
45248
46154
|
|
|
45249
46155
|
// src/cli.ts
|
|
45250
46156
|
var VALID_TARGETS = targetIds();
|
|
@@ -45263,6 +46169,7 @@ function usage() {
|
|
|
45263
46169
|
" scan inspect a third-party package ([ecosystem:]name@version) for residue",
|
|
45264
46170
|
" roundtrip install+uninstall a package (registry spec or local artifact file) in a throwaway container, report uninstall residue",
|
|
45265
46171
|
" opencode install|status|uninstall the official opencode plugin adapter (files under $XDG_CONFIG_HOME/opencode)",
|
|
46172
|
+
" exfil read-only exfil-lens scan of <ref|url> [ref...]: tip tree + tag notes (--deep adds reachable-history blobs + commit messages)",
|
|
45266
46173
|
"",
|
|
45267
46174
|
"flags:",
|
|
45268
46175
|
" --config <path> config file (default: ./border.yaml, then git-remote fallback)",
|
|
@@ -45273,11 +46180,12 @@ function usage() {
|
|
|
45273
46180
|
" --llm include the optional LLM review layer",
|
|
45274
46181
|
" --json machine-readable report on stdout",
|
|
45275
46182
|
" --record / --no-record `border roundtrip` records a ledger proof by default; --no-record opts out",
|
|
46183
|
+
" --deep `border exfil`: also scan reachable-history blobs and the `:message` commit-message facet",
|
|
45276
46184
|
" --help, -h this table",
|
|
45277
46185
|
"",
|
|
45278
46186
|
"exit codes: 0 pass (MEDIUM/INFO/LOW allowed) | 1 gate-blocked (CRITICAL/HIGH) or partial push | 2 config/tool error",
|
|
45279
46187
|
"",
|
|
45280
|
-
"subcommands: check, push, status, llm-request, llm-ingest, scan, roundtrip, opencode"
|
|
46188
|
+
"subcommands: check, push, status, llm-request, llm-ingest, scan, roundtrip, opencode, exfil"
|
|
45281
46189
|
].join("\n");
|
|
45282
46190
|
}
|
|
45283
46191
|
function isSubcommand(value) {
|
|
@@ -45310,6 +46218,7 @@ function parseFlags(argv) {
|
|
|
45310
46218
|
json: { type: "boolean" },
|
|
45311
46219
|
record: { type: "boolean" },
|
|
45312
46220
|
"no-record": { type: "boolean" },
|
|
46221
|
+
deep: { type: "boolean" },
|
|
45313
46222
|
help: { type: "boolean" }
|
|
45314
46223
|
}
|
|
45315
46224
|
});
|
|
@@ -45326,7 +46235,8 @@ function parseFlags(argv) {
|
|
|
45326
46235
|
...requireEngine !== void 0 ? { requireEngine: splitList(requireEngine, { flag: "--require-engine", item: "engine" }) } : {},
|
|
45327
46236
|
llm: llm === true,
|
|
45328
46237
|
json: json2 === true,
|
|
45329
|
-
...values["no-record"] === true ? { record: false } : values.record === true ? { record: true } : {}
|
|
46238
|
+
...values["no-record"] === true ? { record: false } : values.record === true ? { record: true } : {},
|
|
46239
|
+
...values.deep === true ? { deep: true } : {}
|
|
45330
46240
|
};
|
|
45331
46241
|
return { flags, positionals, help: values.help === true };
|
|
45332
46242
|
} catch (err) {
|