jules-orchestrator-kit 0.54.0 → 0.54.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 +3 -1
- package/bin/agentctl.mjs +9 -0
- package/package.json +1 -1
- package/src/config.mjs +5 -0
- package/src/engine.mjs +52 -7
- package/src/git.mjs +97 -2
- package/src/security.mjs +97 -1
package/README.md
CHANGED
|
@@ -197,10 +197,12 @@ To maximize PR merge rates, dispatch tasks according to deterministic boundaries
|
|
|
197
197
|
* **Zero Runtime Dependencies:** Built exclusively on Node.js 20+ built-in modules (`node:fs`, `node:child_process`, `node:crypto`, `node:path`, `node:http`, `node:tty`, `node:test`).
|
|
198
198
|
* **Cross-Platform Parity:** Verified 100% green across Linux, macOS (Darwin), and Windows on Node 20, 22, and 24.
|
|
199
199
|
* **Autonomous Self-Healing Loop:** Captures test stderr/stdout, fingerprints error traces, and feeds structured context back into automated repair turns (up to 3 attempts) before human escalation.
|
|
200
|
+
* **Fail-Closed Verification:** A change that ran no verification command at all is rejected, not approved — "nothing to run" is not a pass. Repositories using only the scope and secret phases opt out explicitly with `verify.required: false`.
|
|
201
|
+
* **Binary-Aware Scanning:** Files git renders as `Binary files ... differ` are read directly for structured credentials, and their real size is charged against the diff ceiling, so a leading NUL byte cannot hide a token and a committed blob cannot walk past the payload governor.
|
|
200
202
|
* **Fail-Closed Security & Secret Redaction:** Evaluates explicit Deny rules before Allow rules against canonicalized, case-folded paths. Redacts high-entropy keys and base64-encoded credentials (such as Kubernetes `Secret` manifests).
|
|
201
203
|
* **Complexity & Cost Router:** Zero-dependency heuristic classifier (`src/router.mjs`) routing mechanical tasks to lightweight models while reserving primary models for complex refactors, with a `node --check` syntax-verification gate that transparently escalates a FAST-tier result to the primary provider if it left broken JS on disk.
|
|
202
204
|
* **Terminal UI & Diagnostic Matrix (`agentctl doctor`):** Interactive terminal dashboard, task sidecar manager, and automated transactional self-repair.
|
|
203
|
-
* **Verified Test Suite:** Tested with **
|
|
205
|
+
* **Verified Test Suite:** Tested with **835 unit tests across 109 suites passing in < 14.0s**.
|
|
204
206
|
|
|
205
207
|
<br/>
|
|
206
208
|
|
package/bin/agentctl.mjs
CHANGED
|
@@ -484,6 +484,15 @@ async function main() {
|
|
|
484
484
|
console.log(`💡 Remediation Hint (Exit 188 Offline Network Violation / Infrastructure):`);
|
|
485
485
|
console.log(` • An unmocked network request was blocked by the preload network guard during verification.`);
|
|
486
486
|
console.log(` • Ensure dependencies are installed locally (run: npm install) and all network calls in tests are mocked.\n`);
|
|
487
|
+
} else if (res.phases.find((p) => p.phase === "verify" && !p.ok)?.failure?.stageId === "oracle") {
|
|
488
|
+
// Nothing exited non-zero here and --fix cannot help: there was no
|
|
489
|
+
// command to run. Offering the repair loop would send an agent to
|
|
490
|
+
// fix a failure that does not exist.
|
|
491
|
+
console.log(`💡 Remediation Hint (Exit ${res.code} No Verification Oracle):`);
|
|
492
|
+
console.log(` • Nothing was executed against this change, so the gate cannot approve it.`);
|
|
493
|
+
console.log(` • Give it a command: agentctl bootstrap (generates one for this stack)`);
|
|
494
|
+
console.log(` • Or set it by hand: verify.test in ${config._file || ".agent/config.yml"}`);
|
|
495
|
+
console.log(` • Scope- and secret-scanning only, on purpose? Set verify.required: false there.\n`);
|
|
487
496
|
} else if (failedPhase === "verify" || failedPhase === "evidence") {
|
|
488
497
|
console.log(`💡 Remediation Hint (Exit ${res.code} Verification Failed):`);
|
|
489
498
|
console.log(` • The stage above exited non-zero. Reproduce it locally, then re-run the gate.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jules-orchestrator-kit",
|
|
3
|
-
"version": "0.54.
|
|
3
|
+
"version": "0.54.1",
|
|
4
4
|
"description": "Zero-dependency safety gatekeeper, test oracle generator, and multi-agent coordination protocol for autonomous coding agents — Google Jules, Claude Code, Codex and Gemini CLI.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
package/src/config.mjs
CHANGED
|
@@ -522,6 +522,11 @@ export function loadConfig(root = resolveRoot(), explicitPath = null) {
|
|
|
522
522
|
teardown: rawTeardown ?? autoVerify.teardown ?? "",
|
|
523
523
|
build: rawBuild ?? autoVerify.build,
|
|
524
524
|
policy: parsed.verify?.policy ?? autoVerify.policy,
|
|
525
|
+
// Whether the gate may approve a change that ran no verification at all.
|
|
526
|
+
// True by default: "nothing to run" is not a pass, and a security tool that
|
|
527
|
+
// says APPROVED after checking nothing is worse than no tool. A repository
|
|
528
|
+
// that deliberately uses only the scope and secret phases sets this false.
|
|
529
|
+
required: parsed.verify?.required !== false,
|
|
525
530
|
};
|
|
526
531
|
|
|
527
532
|
// A hand-written `verify.stages:` is the operator being explicit and always
|
package/src/engine.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { loadConfig, parseYaml, normalizeScope } from "./config.mjs";
|
|
2
|
-
import { checkScope, scanDiff, redactSecrets } from "./security.mjs";
|
|
3
|
-
import { changedFiles, diffBytes, diffText, showFromOrigin, runCmd } from "./git.mjs";
|
|
2
|
+
import { checkScope, scanDiff, scanBinaryPayloads, redactSecrets } from "./security.mjs";
|
|
3
|
+
import { changedFiles, diffBytes, diffText, binaryDiffEntries, showFromOrigin, runCmd } from "./git.mjs";
|
|
4
4
|
import { createProvider, ProviderRateLimitError, ProviderUnavailableError } from "./provider.mjs";
|
|
5
5
|
import { resolveRoutedProvider } from "./router.mjs";
|
|
6
6
|
import { withBudget, appendLedger, getQueueDir, ensureDir, rollbackBudgetReservation, isConcurrencyGroupLocked, checkDailyBudget } from "./state.mjs";
|
|
@@ -198,6 +198,9 @@ export async function gate(opts = {}) {
|
|
|
198
198
|
build: parsed.verify?.build || parsed.build_cmd || config.verify.build,
|
|
199
199
|
stages: parsed.verify?.stages || config.verify.stages,
|
|
200
200
|
policy: parsed.verify?.policy || config.verify.policy,
|
|
201
|
+
// Read from the base commit like every other trusted field: an
|
|
202
|
+
// uncommitted `required: false` must not be able to switch the gate off.
|
|
203
|
+
required: parsed.verify?.required !== undefined ? parsed.verify.required !== false : config.verify.required !== false,
|
|
201
204
|
timeoutMs: parsed.verify?.timeoutMs || parsed.verify?.timeout_ms || config.verify.timeoutMs,
|
|
202
205
|
};
|
|
203
206
|
}
|
|
@@ -242,13 +245,25 @@ export async function gate(opts = {}) {
|
|
|
242
245
|
|
|
243
246
|
// Phase 3: Diff Secret Scanner & Security Checks
|
|
244
247
|
const secretResult = scanDiff(diffStr, { root });
|
|
245
|
-
|
|
246
|
-
|
|
248
|
+
// A binary file reaches the scanner as one summary line, so its contents were
|
|
249
|
+
// never looked at — a NUL byte in front of a token was enough to hide it.
|
|
250
|
+
// Inspect those files directly and fold the verdict in.
|
|
251
|
+
let binaryFindings = [];
|
|
252
|
+
try {
|
|
253
|
+
binaryFindings = scanBinaryPayloads(binaryDiffEntries(root, base, mode), root);
|
|
254
|
+
} catch (_) {
|
|
255
|
+
// Never let the extra pass break a gate that would otherwise have run; the
|
|
256
|
+
// text scan above has already been applied.
|
|
257
|
+
}
|
|
258
|
+
const allSecretFindings = [...(secretResult.findings || []), ...binaryFindings];
|
|
259
|
+
const secretsOk = secretResult.ok && binaryFindings.length === 0;
|
|
260
|
+
phases.push({ phase: "secrets", ok: secretsOk, findings: allSecretFindings });
|
|
261
|
+
appendTelemetry(root, "gate_phase", { phase: "secrets", ok: secretsOk });
|
|
247
262
|
if (progressBus && progressToken) {
|
|
248
263
|
progressBus.reportProgress(progressToken, 75, 100, "Phase 3/4: Diff Secret Scanner check complete");
|
|
249
264
|
}
|
|
250
265
|
|
|
251
|
-
if (!
|
|
266
|
+
if (!secretsOk) {
|
|
252
267
|
appendTelemetry(root, "gate_finished", { ok: false, code: 6 });
|
|
253
268
|
return { ok: false, code: 6, phases };
|
|
254
269
|
}
|
|
@@ -415,7 +430,23 @@ export async function gate(opts = {}) {
|
|
|
415
430
|
}
|
|
416
431
|
}
|
|
417
432
|
|
|
418
|
-
|
|
433
|
+
// A gate that ran no verification at all must not report APPROVED.
|
|
434
|
+
//
|
|
435
|
+
// `testResult` starts optimistic and the stage loop skips a stage with no
|
|
436
|
+
// command, so a repository with no test oracle produced zero execution
|
|
437
|
+
// records and a clean bill of health — syntactically broken code included.
|
|
438
|
+
// That is the product's central claim inverted: the whole point is that a
|
|
439
|
+
// change is verified before it is approved, and "nothing to run" is not
|
|
440
|
+
// verification. Repositories that deliberately use only the scope and secret
|
|
441
|
+
// phases opt out with `verify.required: false`.
|
|
442
|
+
// Assertions are guards, not oracles: `assert:test-integrity` proves the diff
|
|
443
|
+
// did not weaken a test, which says nothing about whether the code works. The
|
|
444
|
+
// question is whether any command was executed against the change at all.
|
|
445
|
+
const verificationRequired = trustedVerify.required !== false;
|
|
446
|
+
const ranNoVerification = !executionRecords.some((r) => r && r.kind !== "assert");
|
|
447
|
+
const missingOracle = verificationRequired && ranNoVerification;
|
|
448
|
+
|
|
449
|
+
const verifyOk = !failingCmd && !testTampered && !missingOracle;
|
|
419
450
|
|
|
420
451
|
// What actually broke. Without this the verify phase reported `ok: false` and
|
|
421
452
|
// nothing else — not the stage, not the exit code, not a line of output — so
|
|
@@ -448,7 +479,21 @@ export async function gate(opts = {}) {
|
|
|
448
479
|
stderr: `Test files changed during the run (${preTestHash.slice(0, 12)} → ${postTestHash.slice(0, 12)}). evidence.strictTestLock treats a passing suite that the diff also rewrote as unproven.`,
|
|
449
480
|
diagnostics: [],
|
|
450
481
|
}
|
|
451
|
-
:
|
|
482
|
+
: missingOracle
|
|
483
|
+
? {
|
|
484
|
+
stageId: "oracle",
|
|
485
|
+
command: null,
|
|
486
|
+
exitCode: null,
|
|
487
|
+
stdout: "",
|
|
488
|
+
stderr:
|
|
489
|
+
"No verification command ran, so nothing about this change was checked. " +
|
|
490
|
+
"Set verify.test in .agent/config.yml (or run `agentctl bootstrap` to generate one). " +
|
|
491
|
+
"If this repository intentionally uses only the scope and secret phases, set verify.required: false.",
|
|
492
|
+
diagnostics: [
|
|
493
|
+
"The gate approves a change because verification passed. Zero stages executed is not a pass.",
|
|
494
|
+
],
|
|
495
|
+
}
|
|
496
|
+
: null;
|
|
452
497
|
|
|
453
498
|
// Generate & persist Evidence Manifest
|
|
454
499
|
const evidenceManifest = generateEvidenceManifest(root, {
|
package/src/git.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFileSync, execSync } from "node:child_process";
|
|
2
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
3
3
|
import { join, delimiter } from "node:path";
|
|
4
4
|
import { normalizePath } from "./config.mjs";
|
|
5
5
|
|
|
@@ -394,9 +394,104 @@ export function diffText(root = process.cwd(), base = "main", mode = "committed"
|
|
|
394
394
|
return git(["diff", `${resolvedRef}...HEAD`], { cwd: root, raw: true });
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Paths git summarised as binary in this diff, with the size of what changed.
|
|
399
|
+
*
|
|
400
|
+
* `git diff` prints one 43-byte line for a binary file — "Binary files a/x and
|
|
401
|
+
* b/x differ" — regardless of whether x grew by a byte or by half a megabyte.
|
|
402
|
+
* Everything downstream reads the diff *text*, so a binary file is invisible to
|
|
403
|
+
* both the payload governor and the secret scanner: a 500 KB blob measured 250
|
|
404
|
+
* bytes, and a credential with a leading NUL byte was never looked at.
|
|
405
|
+
*
|
|
406
|
+
* Sizes come from the object git actually recorded where there is one, and from
|
|
407
|
+
* the working file otherwise, so both `committed` and `working-tree` modes get a
|
|
408
|
+
* real number.
|
|
409
|
+
*
|
|
410
|
+
* @param {string} root
|
|
411
|
+
* @param {string} base
|
|
412
|
+
* @param {string} mode
|
|
413
|
+
* @returns {Array<{ file: string, bytes: number }>}
|
|
414
|
+
*/
|
|
415
|
+
export function binaryDiffEntries(root = process.cwd(), base = "main", mode = "committed") {
|
|
416
|
+
const resolvedRef = resolveBase(root, base);
|
|
417
|
+
const ranges =
|
|
418
|
+
mode === "working-tree" || mode === "working"
|
|
419
|
+
? [[`${resolvedRef}...HEAD`], ["HEAD"]]
|
|
420
|
+
: [[`${resolvedRef}...HEAD`]];
|
|
421
|
+
|
|
422
|
+
const entries = new Map();
|
|
423
|
+
for (const range of ranges) {
|
|
424
|
+
let raw = "";
|
|
425
|
+
try {
|
|
426
|
+
raw = git(["diff", "--raw", "--no-renames", "-z", ...range], { cwd: root, raw: true, ignoreError: true }) || "";
|
|
427
|
+
} catch (_) {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
// `--raw -z` emits ":<srcmode> <dstmode> <srcsha> <dstsha> <status>\0<path>\0".
|
|
431
|
+
const fields = raw.split("\0").filter(Boolean);
|
|
432
|
+
for (let i = 0; i < fields.length; i++) {
|
|
433
|
+
const meta = fields[i];
|
|
434
|
+
if (!meta.startsWith(":")) continue;
|
|
435
|
+
const parts = meta.slice(1).split(/\s+/);
|
|
436
|
+
const dstSha = parts[3];
|
|
437
|
+
const status = (parts[4] || "").charAt(0);
|
|
438
|
+
const file = fields[i + 1];
|
|
439
|
+
i += 1;
|
|
440
|
+
if (!file || status === "D") continue;
|
|
441
|
+
|
|
442
|
+
let bytes = 0;
|
|
443
|
+
if (dstSha && !/^0+$/.test(dstSha)) {
|
|
444
|
+
const size = git(["cat-file", "-s", dstSha], { cwd: root, ignoreError: true });
|
|
445
|
+
bytes = Number(size) || 0;
|
|
446
|
+
}
|
|
447
|
+
// An unstaged change has an all-zero destination sha; the working file is
|
|
448
|
+
// the only place its size exists.
|
|
449
|
+
if (!bytes) {
|
|
450
|
+
try {
|
|
451
|
+
bytes = statSync(join(root, file)).size;
|
|
452
|
+
} catch (_) {
|
|
453
|
+
bytes = 0;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// Keep the largest observation: the same path can appear in both ranges.
|
|
457
|
+
entries.set(file, Math.max(entries.get(file) || 0, bytes));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Only the paths git itself refused to render as text are relevant; a file
|
|
462
|
+
// that diffed normally is already counted in the diff text.
|
|
463
|
+
const binaryPaths = new Set();
|
|
464
|
+
const text = diffText(root, base, mode);
|
|
465
|
+
for (const line of text.split("\n")) {
|
|
466
|
+
const m = line.match(/^Binary files (?:a\/(.+) and )?(?:b\/(.+)|\/dev\/null) differ$/);
|
|
467
|
+
if (m) binaryPaths.add(normalizePath(m[2] || m[1] || ""));
|
|
468
|
+
const m2 = line.match(/^Binary files \/dev\/null and b\/(.+) differ$/);
|
|
469
|
+
if (m2) binaryPaths.add(normalizePath(m2[1]));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return [...entries.entries()]
|
|
473
|
+
.filter(([file]) => binaryPaths.has(normalizePath(file)))
|
|
474
|
+
.map(([file, bytes]) => ({ file, bytes }));
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Total bytes this change actually carries.
|
|
479
|
+
*
|
|
480
|
+
* The diff text plus the real size of every binary blob it only summarised.
|
|
481
|
+
* Without the second term the payload governor could be walked straight past
|
|
482
|
+
* with a committed binary of any size.
|
|
483
|
+
*/
|
|
397
484
|
export function diffBytes(root = process.cwd(), base = "main", mode = "committed") {
|
|
398
485
|
const text = diffText(root, base, mode);
|
|
399
|
-
|
|
486
|
+
let bytes = Buffer.byteLength(text, "utf-8");
|
|
487
|
+
try {
|
|
488
|
+
for (const entry of binaryDiffEntries(root, base, mode)) bytes += entry.bytes;
|
|
489
|
+
} catch (_) {
|
|
490
|
+
// A payload figure that is too low is the dangerous direction, but throwing
|
|
491
|
+
// here would break every gate on a repo git cannot describe. The text-only
|
|
492
|
+
// number is still returned.
|
|
493
|
+
}
|
|
494
|
+
return bytes;
|
|
400
495
|
}
|
|
401
496
|
|
|
402
497
|
export function showFromOrigin(root = process.cwd(), base = "main", filePath = "") {
|
package/src/security.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { openSync, writeSync, fsyncSync, closeSync, renameSync, realpathSync, existsSync, lstatSync, unlinkSync } from "node:fs";
|
|
1
|
+
import { openSync, readFileSync, writeSync, fsyncSync, closeSync, renameSync, realpathSync, existsSync, lstatSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { dirname, join, basename } from "node:path";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { canonicalizePath, isWindowsAbsolutePath } from "./config.mjs";
|
|
@@ -911,6 +911,102 @@ export function hasHighEntropyToken(text = "", file = null) {
|
|
|
911
911
|
return false;
|
|
912
912
|
}
|
|
913
913
|
|
|
914
|
+
/** Bytes of any one binary file the scanner will read. */
|
|
915
|
+
const BINARY_SCAN_CAP_BYTES = 8 * 1024 * 1024;
|
|
916
|
+
|
|
917
|
+
/** Runs of printable ASCII at least this long are worth classifying. */
|
|
918
|
+
const BINARY_STRING_MIN_RUN = 8;
|
|
919
|
+
|
|
920
|
+
/**
|
|
921
|
+
* Scan the contents of files git summarised as "Binary files ... differ".
|
|
922
|
+
*
|
|
923
|
+
* Everything else in this module reads the diff *text*, and git renders a
|
|
924
|
+
* binary file as one 43-byte summary line — so a credential became invisible to
|
|
925
|
+
* the entire scanner by prefixing the file with a single NUL byte. That is not
|
|
926
|
+
* a theoretical bypass: `printf '\0ghp_...' > secret.dat` walked a live GitHub
|
|
927
|
+
* token straight through a green gate.
|
|
928
|
+
*
|
|
929
|
+
* Only the *structured* high-confidence patterns are applied here, never
|
|
930
|
+
* entropy. A real PNG is full of high-entropy bytes and would fail every gate
|
|
931
|
+
* it touched; a string matching `ghp_[A-Za-z0-9]{36}` inside a file claiming to
|
|
932
|
+
* be an image is not a coincidence.
|
|
933
|
+
*
|
|
934
|
+
* @param {Array<{ file: string, bytes: number }>} entries
|
|
935
|
+
* @param {string} root
|
|
936
|
+
* @param {object} [opts]
|
|
937
|
+
* @param {number} [opts.capBytes] - per-file read ceiling
|
|
938
|
+
* @returns {Array<{ severity: string, type: string, file: string, line: null, description: string }>}
|
|
939
|
+
*/
|
|
940
|
+
export function scanBinaryPayloads(entries = [], root = process.cwd(), opts = {}) {
|
|
941
|
+
const cap = Number.isFinite(opts.capBytes) ? opts.capBytes : BINARY_SCAN_CAP_BYTES;
|
|
942
|
+
const findings = [];
|
|
943
|
+
// `entries` comes from a git call that returns null on failure, and the
|
|
944
|
+
// default parameter only covers `undefined`.
|
|
945
|
+
const list = Array.isArray(entries) ? entries : [];
|
|
946
|
+
|
|
947
|
+
for (const entry of list) {
|
|
948
|
+
if (!entry || !entry.file) continue;
|
|
949
|
+
// A file too large to read is reported rather than skipped: silence here is
|
|
950
|
+
// exactly the hole being closed.
|
|
951
|
+
if (entry.bytes > cap) {
|
|
952
|
+
findings.push({
|
|
953
|
+
severity: "HIGH",
|
|
954
|
+
type: "BINARY_PAYLOAD_UNSCANNED",
|
|
955
|
+
file: entry.file,
|
|
956
|
+
line: null,
|
|
957
|
+
description: `Binary file ${entry.file} is ${Math.round(entry.bytes / 1024)} KB, above the ${Math.round(cap / 1024)} KB scan ceiling, and was not inspected for credentials`,
|
|
958
|
+
});
|
|
959
|
+
continue;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
let buf;
|
|
963
|
+
try {
|
|
964
|
+
buf = readFileSync(join(root, entry.file));
|
|
965
|
+
} catch (_) {
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Extract printable runs the way `strings(1)` does: a credential inside a
|
|
970
|
+
// binary is still ASCII, and decoding the whole buffer as UTF-8 would let
|
|
971
|
+
// replacement characters split the token apart.
|
|
972
|
+
const runs = [];
|
|
973
|
+
let current = "";
|
|
974
|
+
for (const byte of buf) {
|
|
975
|
+
if (byte >= 0x20 && byte <= 0x7e) {
|
|
976
|
+
current += String.fromCharCode(byte);
|
|
977
|
+
} else {
|
|
978
|
+
if (current.length >= BINARY_STRING_MIN_RUN) runs.push(current);
|
|
979
|
+
current = "";
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if (current.length >= BINARY_STRING_MIN_RUN) runs.push(current);
|
|
983
|
+
if (runs.length === 0) continue;
|
|
984
|
+
|
|
985
|
+
const text = runs.join("\n");
|
|
986
|
+
if (hasHighConfidenceSecret(text)) {
|
|
987
|
+
findings.push({
|
|
988
|
+
severity: "CRITICAL",
|
|
989
|
+
type: "HIGH_CONFIDENCE_SECRET",
|
|
990
|
+
file: entry.file,
|
|
991
|
+
line: null,
|
|
992
|
+
description: `High-confidence secret pattern found inside binary file ${entry.file}, which the diff renders only as "Binary files ... differ"`,
|
|
993
|
+
});
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
if (hasEncodedSecret(text)) {
|
|
997
|
+
findings.push({
|
|
998
|
+
severity: "CRITICAL",
|
|
999
|
+
type: "HIGH_CONFIDENCE_SECRET",
|
|
1000
|
+
file: entry.file,
|
|
1001
|
+
line: null,
|
|
1002
|
+
description: `Base64-encoded secret found inside binary file ${entry.file}`,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
return findings;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
914
1010
|
/**
|
|
915
1011
|
* Classify a block of added lines. Returns the single most severe finding, or
|
|
916
1012
|
* null when the block is clean.
|