Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions review-enrichment/src/analyzers/secret-scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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_<id>/<token>`; 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-<id>?key=<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/<deployment-id>/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-----/,
Expand Down
42 changes: 42 additions & 0 deletions review-enrichment/test/secret-scan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}
});
Loading