From 17c0506b9c0fd911c6349ce7ba7bfaef22c4e26e Mon Sep 17 00:00:00 2001 From: luciferlive112116 <291889058+luciferlive112116@users.noreply.github.com> Date: Sun, 5 Jul 2026 14:45:36 +0800 Subject: [PATCH] feat(enrichment): flag deployment-trigger and webhook URLs in secret-scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add seven high-confidence RULES for deployment-trigger and webhook URLs that embed a secret, following the existing URL-credential rules — a committed URL of this shape leaks a capability (trigger a deploy, suppress a monitor's alerts). - netlify_build_hook_url: api.netlify.com/build_hooks/ - vercel_deploy_hook_url: api.vercel.com/v1/integrations/deploy/prj_/ - render_deploy_hook_url: api.render.com/deploy/srv-?key= - healthchecks_ping_url: hc-ping.com/ - pipedream_webhook_url: .m.pipedream.net - azure_logic_app_url: .logic.azure.com/workflows/...?sig= - google_apps_script_url: script.google.com/macros/s//exec Each match is a full vendor URL (host + secret path/param), so an ordinary string cannot trip it and there is no token length to guess. The negative test asserts non-secret vendor URLs, a param-less Render URL, and a look-alike suffix host (...m.pipedream.net.evil.com) produce no finding; the Pipedream rule uses a negative-lookahead terminator to reject that suffix. All kinds are new and inserted before the generic-assignment rule. SecretFinding.kind is a plain string, so there is no types/render/metadata change. --- .../src/analyzers/secret-scan.ts | 44 +++++++++++++++++++ review-enrichment/test/secret-scan.test.ts | 39 ++++++++++++++++ 2 files changed, 83 insertions(+) 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}`); + } +});