@synkro-sh/cli 1.3.24 → 1.3.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap.js +103 -26
- package/dist/bootstrap.js.map +1 -1
- package/package.json +1 -1
package/dist/bootstrap.js
CHANGED
|
@@ -2508,7 +2508,7 @@ jobs:
|
|
|
2508
2508
|
scan:
|
|
2509
2509
|
runs-on: ubuntu-latest
|
|
2510
2510
|
permissions:
|
|
2511
|
-
contents:
|
|
2511
|
+
contents: write
|
|
2512
2512
|
pull-requests: write
|
|
2513
2513
|
checks: write
|
|
2514
2514
|
steps:
|
|
@@ -3333,7 +3333,7 @@ function writeConfigEnv(opts) {
|
|
|
3333
3333
|
`SYNKRO_CREDENTIALS_PATH=${shellQuoteSingle(credsPath)}`,
|
|
3334
3334
|
`SYNKRO_TIER=${shellQuoteSingle(safeTier)}`,
|
|
3335
3335
|
`SYNKRO_INFERENCE=${shellQuoteSingle(safeInference)}`,
|
|
3336
|
-
`SYNKRO_VERSION=${shellQuoteSingle("1.3.
|
|
3336
|
+
`SYNKRO_VERSION=${shellQuoteSingle("1.3.26")}`
|
|
3337
3337
|
];
|
|
3338
3338
|
if (safeUserId) lines.push(`SYNKRO_USER_ID=${shellQuoteSingle(safeUserId)}`);
|
|
3339
3339
|
if (safeOrgId) lines.push(`SYNKRO_ORG_ID=${shellQuoteSingle(safeOrgId)}`);
|
|
@@ -4374,6 +4374,60 @@ function ghJson(args2) {
|
|
|
4374
4374
|
});
|
|
4375
4375
|
return JSON.parse(out);
|
|
4376
4376
|
}
|
|
4377
|
+
async function ensureOpenPr(repo, prNumber, sha) {
|
|
4378
|
+
let pr;
|
|
4379
|
+
try {
|
|
4380
|
+
pr = ghJson(["api", `/repos/${repo}/pulls/${prNumber}`]);
|
|
4381
|
+
} catch {
|
|
4382
|
+
return { prNumber, sha, branched: false };
|
|
4383
|
+
}
|
|
4384
|
+
if (pr.state === "open") {
|
|
4385
|
+
return { prNumber, sha, branched: false };
|
|
4386
|
+
}
|
|
4387
|
+
if (pr.merged) {
|
|
4388
|
+
console.log(`PR #${prNumber} is merged. Scanning original diff and posting findings there.
|
|
4389
|
+
`);
|
|
4390
|
+
return { prNumber, sha: pr.head.sha, branched: false };
|
|
4391
|
+
}
|
|
4392
|
+
console.log(`PR #${prNumber} is closed. Creating a scan branch...
|
|
4393
|
+
`);
|
|
4394
|
+
const scanBranch = `synkro/scan-${pr.head.ref}-${Date.now()}`;
|
|
4395
|
+
try {
|
|
4396
|
+
execSync5(`gh api -X POST /repos/${repo}/git/refs --input -`, {
|
|
4397
|
+
encoding: "utf-8",
|
|
4398
|
+
input: JSON.stringify({ ref: `refs/heads/${scanBranch}`, sha: pr.head.sha }),
|
|
4399
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4400
|
+
});
|
|
4401
|
+
} catch (err) {
|
|
4402
|
+
console.warn(`Failed to create scan branch: ${err.message}`);
|
|
4403
|
+
return { prNumber, sha, branched: false };
|
|
4404
|
+
}
|
|
4405
|
+
try {
|
|
4406
|
+
const newPrBody = `Security scan of closed PR #${prNumber} (\`${pr.head.ref}\`).
|
|
4407
|
+
|
|
4408
|
+
This PR contains no code changes \u2014 it exists so Synkro can post review findings on an active PR.`;
|
|
4409
|
+
const result = ghJson([
|
|
4410
|
+
"api",
|
|
4411
|
+
"-X",
|
|
4412
|
+
"POST",
|
|
4413
|
+
`/repos/${repo}/pulls`,
|
|
4414
|
+
"-f",
|
|
4415
|
+
`title=Synkro Scan: ${pr.title}`,
|
|
4416
|
+
"-f",
|
|
4417
|
+
`body=${newPrBody}`,
|
|
4418
|
+
"-f",
|
|
4419
|
+
`head=${scanBranch}`,
|
|
4420
|
+
"-f",
|
|
4421
|
+
`base=${pr.base.ref}`
|
|
4422
|
+
]);
|
|
4423
|
+
console.log(`Opened PR #${result.number} for scan findings.
|
|
4424
|
+
`);
|
|
4425
|
+
return { prNumber: result.number, sha: result.head.sha, branched: true };
|
|
4426
|
+
} catch (err) {
|
|
4427
|
+
console.warn(`Failed to open scan PR: ${err.message}`);
|
|
4428
|
+
return { prNumber, sha, branched: false };
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4377
4431
|
function getPrFiles(repo, prNumber) {
|
|
4378
4432
|
const data = ghJson([
|
|
4379
4433
|
"api",
|
|
@@ -4381,23 +4435,43 @@ function getPrFiles(repo, prNumber) {
|
|
|
4381
4435
|
]);
|
|
4382
4436
|
return data;
|
|
4383
4437
|
}
|
|
4438
|
+
function getLastReviewedSha(repo, prNumber) {
|
|
4439
|
+
try {
|
|
4440
|
+
const reviews = ghJson([
|
|
4441
|
+
"api",
|
|
4442
|
+
`/repos/${repo}/pulls/${prNumber}/reviews?per_page=100`
|
|
4443
|
+
]);
|
|
4444
|
+
const synkro = reviews.filter((r) => r.body?.includes("Synkro Security Review")).sort((a, b) => new Date(b.submitted_at).getTime() - new Date(a.submitted_at).getTime());
|
|
4445
|
+
return synkro.length > 0 ? synkro[0].commit_id : null;
|
|
4446
|
+
} catch {
|
|
4447
|
+
return null;
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
function getChangedFilesSince(repo, baseSha, headSha) {
|
|
4451
|
+
try {
|
|
4452
|
+
const data = ghJson([
|
|
4453
|
+
"api",
|
|
4454
|
+
`/repos/${repo}/compare/${baseSha}...${headSha}`
|
|
4455
|
+
]);
|
|
4456
|
+
return (data.files || []).map((f) => f.filename);
|
|
4457
|
+
} catch {
|
|
4458
|
+
return null;
|
|
4459
|
+
}
|
|
4460
|
+
}
|
|
4384
4461
|
async function fetchScanContext(gatewayUrl, apiKey, repo, prNumber, sha) {
|
|
4462
|
+
const lastSha = getLastReviewedSha(repo, prNumber);
|
|
4463
|
+
const changedFiles = lastSha && lastSha !== sha ? getChangedFilesSince(repo, lastSha, sha) : void 0;
|
|
4385
4464
|
try {
|
|
4386
|
-
const url = `${gatewayUrl.replace(/\/$/, "")}/api/pr-scans/scan-context
|
|
4387
|
-
const headers = { "x-synkro-api-key": apiKey };
|
|
4388
|
-
const ghToken = process.env.GH_TOKEN || process.env.GITHUB_TOKEN || "";
|
|
4389
|
-
if (ghToken) headers["x-github-token"] = ghToken;
|
|
4390
|
-
console.log(`[scan-context] POST ${url}`);
|
|
4465
|
+
const url = `${gatewayUrl.replace(/\/$/, "")}/api/pr-scans/scan-context`;
|
|
4391
4466
|
const resp = await fetch(url, {
|
|
4392
|
-
|
|
4467
|
+
method: "POST",
|
|
4468
|
+
headers: { "x-synkro-api-key": apiKey, "Content-Type": "application/json" },
|
|
4469
|
+
body: JSON.stringify({ sha, last_reviewed_sha: lastSha, changed_files: changedFiles }),
|
|
4393
4470
|
signal: AbortSignal.timeout(15e3)
|
|
4394
4471
|
});
|
|
4395
|
-
const body = await resp.text();
|
|
4396
|
-
console.log(`[scan-context] ${resp.status}: ${body.slice(0, 300)}`);
|
|
4397
4472
|
if (!resp.ok) return { scan_all: true };
|
|
4398
|
-
return
|
|
4399
|
-
} catch
|
|
4400
|
-
console.warn(`[scan-context] error: ${err.message}`);
|
|
4473
|
+
return await resp.json();
|
|
4474
|
+
} catch {
|
|
4401
4475
|
return { scan_all: true };
|
|
4402
4476
|
}
|
|
4403
4477
|
}
|
|
@@ -4746,6 +4820,9 @@ async function scanPrCommand() {
|
|
|
4746
4820
|
}
|
|
4747
4821
|
console.log(`Synkro scan-pr: ${repo}#${prNumber} @ ${sha.slice(0, 7)}
|
|
4748
4822
|
`);
|
|
4823
|
+
const prTarget = await ensureOpenPr(repo, prNumber, sha);
|
|
4824
|
+
const activePrNumber = prTarget.prNumber;
|
|
4825
|
+
const activeSha = prTarget.sha;
|
|
4749
4826
|
const orgRules = await fetchOrgRules(gatewayUrl, synkroApiKey);
|
|
4750
4827
|
const auditRules = orgRules.filter((r) => r.mode === "audit");
|
|
4751
4828
|
const literalNegativeRules = orgRules.filter((r) => {
|
|
@@ -4756,22 +4833,22 @@ async function scanPrCommand() {
|
|
|
4756
4833
|
const promptHeader = buildPrPrompt(auditRules);
|
|
4757
4834
|
let files;
|
|
4758
4835
|
try {
|
|
4759
|
-
files = getPrFiles(repo,
|
|
4836
|
+
files = getPrFiles(repo, activePrNumber);
|
|
4760
4837
|
} catch (err) {
|
|
4761
4838
|
console.error("Failed to fetch PR files:", err.message);
|
|
4762
4839
|
process.exit(2);
|
|
4763
4840
|
}
|
|
4764
|
-
const scanCtx = await fetchScanContext(gatewayUrl, synkroApiKey, repo,
|
|
4841
|
+
const scanCtx = await fetchScanContext(gatewayUrl, synkroApiKey, repo, activePrNumber, activeSha);
|
|
4765
4842
|
if (scanCtx.skip) {
|
|
4766
|
-
console.log(`Already scanned at ${
|
|
4843
|
+
console.log(`Already scanned at ${activeSha.slice(0, 7)}, skipping.
|
|
4767
4844
|
`);
|
|
4768
|
-
postCheckRun(repo,
|
|
4845
|
+
postCheckRun(repo, activeSha, "success", []);
|
|
4769
4846
|
await postEventToBackend({
|
|
4770
4847
|
gatewayUrl,
|
|
4771
4848
|
apiKey: synkroApiKey,
|
|
4772
4849
|
repo,
|
|
4773
|
-
prNumber,
|
|
4774
|
-
sha,
|
|
4850
|
+
prNumber: activePrNumber,
|
|
4851
|
+
sha: activeSha,
|
|
4775
4852
|
findings: [],
|
|
4776
4853
|
filesScanned: 0,
|
|
4777
4854
|
totalLatencyMs: 0
|
|
@@ -4794,13 +4871,13 @@ async function scanPrCommand() {
|
|
|
4794
4871
|
console.log(`${files.length} files in PR, ${eligible.length} eligible for scan.
|
|
4795
4872
|
`);
|
|
4796
4873
|
if (eligible.length === 0) {
|
|
4797
|
-
postCheckRun(repo,
|
|
4874
|
+
postCheckRun(repo, activeSha, "success", []);
|
|
4798
4875
|
await postEventToBackend({
|
|
4799
4876
|
gatewayUrl,
|
|
4800
4877
|
apiKey: synkroApiKey,
|
|
4801
4878
|
repo,
|
|
4802
|
-
prNumber,
|
|
4803
|
-
sha,
|
|
4879
|
+
prNumber: activePrNumber,
|
|
4880
|
+
sha: activeSha,
|
|
4804
4881
|
findings: [],
|
|
4805
4882
|
filesScanned: 0,
|
|
4806
4883
|
totalLatencyMs: 0
|
|
@@ -4827,17 +4904,17 @@ Total: ${allFindings.length} finding(s) across ${eligible.length} file(s) in ${t
|
|
|
4827
4904
|
const review = await spawnOpusConsolidator(allFindings, claudeToken);
|
|
4828
4905
|
console.log(` \u2192 ${review.comments.length} review comment(s), severity: ${review.severity}`);
|
|
4829
4906
|
if (review.comments.length > 0) {
|
|
4830
|
-
postPrReview(repo,
|
|
4907
|
+
postPrReview(repo, activePrNumber, activeSha, review);
|
|
4831
4908
|
}
|
|
4832
4909
|
}
|
|
4833
4910
|
const conclusion = shouldFail(allFindings, failThreshold) ? "failure" : "success";
|
|
4834
|
-
postCheckRun(repo,
|
|
4911
|
+
postCheckRun(repo, activeSha, conclusion, allFindings);
|
|
4835
4912
|
await postEventToBackend({
|
|
4836
4913
|
gatewayUrl,
|
|
4837
4914
|
apiKey: synkroApiKey,
|
|
4838
4915
|
repo,
|
|
4839
|
-
prNumber,
|
|
4840
|
-
sha,
|
|
4916
|
+
prNumber: activePrNumber,
|
|
4917
|
+
sha: activeSha,
|
|
4841
4918
|
findings: allFindings,
|
|
4842
4919
|
filesScanned: eligible.length,
|
|
4843
4920
|
totalLatencyMs
|