From 8b8d31771c9777a340e8d17f64662edbfecc26da Mon Sep 17 00:00:00 2001 From: luciferlive112116 <291889058+luciferlive112116@users.noreply.github.com> Date: Sun, 5 Jul 2026 15:12:00 +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/ (case-insensitive) - 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 Pipedream rule ends at the host, so its terminator rejects every hostname-continuation char (`.`, `-`, alphanumeric) — a look-alike suffix host (`...m.pipedream.net.evil.com` or `...m.pipedream.net-evil.com`) is not matched, both asserted in the negative test. 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 | 46 +++++++++++++++++++ review-enrichment/test/secret-scan.test.ts | 42 +++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/review-enrichment/src/analyzers/secret-scan.ts b/review-enrichment/src/analyzers/secret-scan.ts index cf4e352a31..1f011c2a98 100644 --- a/review-enrichment/src/analyzers/secret-scan.ts +++ b/review-enrichment/src/analyzers/secret-scan.ts @@ -955,6 +955,52 @@ 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. + // Case-insensitive hex, since the UUID may be written in either case. + kind: "healthchecks_ping_url", + re: /https:\/\/hc-ping\.com\/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/, + confidence: "high", + }, + { + // Pipedream workflow trigger — the subdomain of `.m.pipedream.net` is the endpoint's secret address. The + // negative lookahead rejects ANY hostname-continuation char (`.`, `-`, or alphanumeric) so a look-alike + // suffix domain — `.m.pipedream.net.evil.com` OR `.m.pipedream.net-evil.com` — is not matched. + kind: "pipedream_webhook_url", + re: /https:\/\/[a-z0-9]+\.m\.pipedream\.net(?![A-Za-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..726de47e42 100644 --- a/review-enrichment/test/secret-scan.test.ts +++ b/review-enrichment/test/secret-scan.test.ts @@ -1573,3 +1573,45 @@ 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], + // Healthchecks accepts the UUID in upper case too — the rule matches it case-insensitively. + ["healthchecks_ping_url", "https://hc-ping.com/" + uuid.toUpperCase()], + ["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 — neither a dot- nor a hyphen-joined suffix. + "https://example.m.pipedream.net.evil.com/path", + "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}`); + } +});