diff --git a/review-enrichment/src/analyzers/secret-scan.ts b/review-enrichment/src/analyzers/secret-scan.ts index cf4e352a31..da3d77b0e4 100644 --- a/review-enrichment/src/analyzers/secret-scan.ts +++ b/review-enrichment/src/analyzers/secret-scan.ts @@ -955,6 +955,50 @@ const RULES: Rule[] = [ re: /https:\/\/chat\.googleapis\.com\/v1\/spaces\/[A-Za-z0-9_-]+\/messages\?[^\s"']*key=/, confidence: "high", }, + { + // Netlify build-hook URL — the trailing id triggers a production build, like the webhook rules above. + kind: "netlify_build_hook_url", + re: /https:\/\/api\.netlify\.com\/build_hooks\/[0-9a-f]{24}/, + confidence: "high", + }, + { + // Vercel deploy-hook URL — `.../deploy/prj_/`; the trailing token triggers a deployment. + kind: "vercel_deploy_hook_url", + re: /https:\/\/api\.vercel\.com\/v1\/integrations\/deploy\/prj_[A-Za-z0-9]+\/[A-Za-z0-9]+/, + confidence: "high", + }, + { + // Render deploy-hook URL — `.../deploy/srv-?key=`; the `key` query param triggers a deployment. + kind: "render_deploy_hook_url", + re: /https:\/\/api\.render\.com\/deploy\/srv-[A-Za-z0-9]+\?key=[A-Za-z0-9_-]+/, + confidence: "high", + }, + { + // Healthchecks.io ping URL — the UUID is the check's secret address; a leak lets anyone suppress its alerts. + kind: "healthchecks_ping_url", + re: /https:\/\/hc-ping\.com\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/, + confidence: "high", + }, + { + // Pipedream workflow trigger — the subdomain of `.m.pipedream.net` is the endpoint's secret address. The + // negative lookahead stops the host matching a look-alike suffix domain (`.m.pipedream.net.evil.com`). + kind: "pipedream_webhook_url", + re: /https:\/\/[a-z0-9]+\.m\.pipedream\.net(?![a-z0-9.])/, + confidence: "high", + }, + { + // Azure Logic App callback URL — the `sig=` query param is a SAS that authorizes triggering the workflow. + kind: "azure_logic_app_url", + re: /https:\/\/[a-z0-9.-]+\.logic\.azure\.com(?::443)?\/workflows\/[^\s"']*sig=/, + confidence: "high", + }, + { + // Google Apps Script web-app URL — the `/macros/s//exec` id is a capability address for the + // deployed script (anyone with the URL can invoke it). + kind: "google_apps_script_url", + re: /https:\/\/script\.google\.com\/macros\/s\/[A-Za-z0-9_-]{30,}\/exec/, + confidence: "high", + }, { kind: "private_key", re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/, diff --git a/review-enrichment/test/secret-scan.test.ts b/review-enrichment/test/secret-scan.test.ts index 1b7248e2e7..7bad135172 100644 --- a/review-enrichment/test/secret-scan.test.ts +++ b/review-enrichment/test/secret-scan.test.ts @@ -1573,3 +1573,42 @@ test("scanPatch does not flag near-misses of the webhook-URL and token formats", assert.equal(findings.length, 0, `near-miss should not match: ${nm}`); } }); + +test("scanPatch flags deployment-trigger and webhook URLs that embed a secret", () => { + const uuid = [hex(8), hex(4), hex(4), hex(4), hex(12)].join("-"); + const cases = [ + ["netlify_build_hook_url", "https://api.netlify.com/build_hooks/" + hex(24)], + ["vercel_deploy_hook_url", "https://api.vercel.com/v1/integrations/deploy/prj_" + b62(12) + "/" + b62(24)], + ["render_deploy_hook_url", "https://api.render.com/deploy/srv-" + b62(20) + "?key=" + b62(20)], + ["healthchecks_ping_url", "https://hc-ping.com/" + uuid], + ["pipedream_webhook_url", "https://" + b62(12).toLowerCase() + ".m.pipedream.net"], + [ + "azure_logic_app_url", + "https://prod-01.westus.logic.azure.com/workflows/" + hex(32) + "/triggers/manual/paths/invoke?sig=" + b62(24), + ], + ["google_apps_script_url", "https://script.google.com/macros/s/" + b62(40) + "/exec"], + ]; + for (const [kind, secret] of cases) { + const findings = scanPatch("src/config.ts", hunk([`const c = "${secret}";`])); + assert.equal(findings.length, 1, `${kind}: expected exactly one finding, got ${JSON.stringify(findings)}`); + assert.equal(findings[0].kind, kind, `${kind}: wrong kind`); + assert.equal(findings[0].confidence, "high", `${kind}: wrong confidence`); + } +}); + +test("scanPatch does not flag near-misses of the deployment-hook URL formats", () => { + const nearMisses = [ + // Vendor API/docs/dashboard URLs that carry no secret path segment — nothing to leak. + "https://api.netlify.com/api/v1/sites", + "https://vercel.com/docs/deploy-hooks", + "https://render.com/docs/deploy-hooks", + // A Render deploy URL WITHOUT the `key` query param is not the triggerable secret URL. + "https://api.render.com/deploy/srv-" + b62(20), + // A look-alike host is not the real Pipedream endpoint host. + "https://example.m.pipedream.net.evil.com/path", + ]; + for (const nm of nearMisses) { + const findings = scanPatch("src/config.ts", hunk([`const c = "${nm}";`])); + assert.equal(findings.length, 0, `near-miss should not match: ${nm}`); + } +});