diff --git a/packages/loopover-engine/src/settings/contributor-blacklist.ts b/packages/loopover-engine/src/settings/contributor-blacklist.ts index 980d6ba4bb..d4c88828d1 100644 --- a/packages/loopover-engine/src/settings/contributor-blacklist.ts +++ b/packages/loopover-engine/src/settings/contributor-blacklist.ts @@ -5,9 +5,11 @@ // command-authorization.ts (normalize → typed policy + warnings). import type { ContributorBlacklistEntry } from "../types/manifest-deps-types.js"; -// GitHub logins: 1–39 chars, alphanumeric or single hyphens (not leading/trailing). Anything else is dropped so a -// malformed entry can never widen the match or break the close path. -const GITHUB_LOGIN = /^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$/; +// GitHub logins: 1–39 chars, alphanumeric or single hyphens (not leading/trailing), plus an optional trailing +// `[bot]` App-actor suffix (e.g. `dependabot[bot]`) so a maintainer can blacklist a repo-specific bot — the same +// login shape auto-close-exempt.ts already accepts for exemptions (#6190). Anything else is dropped so a malformed +// entry can never widen the match or break the close path. +const GITHUB_LOGIN = /^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}(?:\[bot\])?$/; const MAX_ENTRIES = 1000; const MAX_REASON_CHARS = 200; const MAX_EVIDENCE = 10; diff --git a/test/unit/contributor-blacklist-engine.test.ts b/test/unit/contributor-blacklist-engine.test.ts index 4fda94a2f0..783bb08abc 100644 --- a/test/unit/contributor-blacklist-engine.test.ts +++ b/test/unit/contributor-blacklist-engine.test.ts @@ -32,6 +32,21 @@ describe("normalizeContributorBlacklist (#1425) [engine]", () => { expect(entries.map((e) => e.login)).toEqual(["a-b", "user123", "a".repeat(39)]); }); + it("accepts a `[bot]`-suffixed App-actor login and enforces it against a matching author (#6190)", () => { + const { entries, warnings } = normalizeContributorBlacklist(["evilbot[bot]", "dependabot[bot]", "github-actions[bot]"]); + expect(entries.map((e) => e.login)).toEqual(["evilbot[bot]", "dependabot[bot]", "github-actions[bot]"]); + expect(warnings).toEqual([]); + expect(isAuthorBlacklisted("evilbot[bot]", entries)).toBe(true); + expect(isAuthorBlacklisted("EvilBot[bot]", entries)).toBe(true); + expect(findBlacklistEntry("dependabot[bot]", entries)?.login).toBe("dependabot[bot]"); + expect(isAuthorBlacklisted("innocent[bot]", entries)).toBe(false); + }); + + it("still drops malformed bot-ish logins (bare `[bot]`, doubled suffix, wrong-case or mid-string brackets)", () => { + const { entries } = normalizeContributorBlacklist(["[bot]", "weird[bot][bot]", "user[Bot]", "a[bot]b"]); + expect(entries).toEqual([]); + }); + it("de-duplicates by case-insensitive login, keeping the FIRST (richer) occurrence", () => { const { entries } = normalizeContributorBlacklist([{ login: "Mona", reason: "first" }, { login: "mona", reason: "second" }]); expect(entries).toEqual([{ login: "Mona", reason: "first" }]); diff --git a/test/unit/contributor-blacklist.test.ts b/test/unit/contributor-blacklist.test.ts index a75636b6a6..f6d14e83fa 100644 --- a/test/unit/contributor-blacklist.test.ts +++ b/test/unit/contributor-blacklist.test.ts @@ -67,6 +67,22 @@ describe("normalizeContributorBlacklist (#1425)", () => { expect(entries.map((e) => e.login)).toEqual(["a-b", "user123", "a".repeat(39)]); }); + it("accepts a `[bot]`-suffixed App-actor login and enforces it against a matching author (#6190)", () => { + const { entries, warnings } = normalizeContributorBlacklist(["evilbot[bot]", "dependabot[bot]", "github-actions[bot]"]); + expect(entries.map((e) => e.login)).toEqual(["evilbot[bot]", "dependabot[bot]", "github-actions[bot]"]); + expect(warnings).toEqual([]); + // enforcement path: a PR author with the blacklisted bot login is actually matched (case-insensitively) + expect(isAuthorBlacklisted("evilbot[bot]", entries)).toBe(true); + expect(isAuthorBlacklisted("EvilBot[bot]", entries)).toBe(true); + expect(findBlacklistEntry("dependabot[bot]", entries)?.login).toBe("dependabot[bot]"); + expect(isAuthorBlacklisted("innocent[bot]", entries)).toBe(false); + }); + + it("still drops malformed bot-ish logins (bare `[bot]`, doubled suffix, wrong-case or mid-string brackets)", () => { + const { entries } = normalizeContributorBlacklist(["[bot]", "weird[bot][bot]", "user[Bot]", "a[bot]b"]); + expect(entries).toEqual([]); + }); + it("de-duplicates by case-insensitive login, keeping the FIRST (richer) occurrence", () => { const { entries } = normalizeContributorBlacklist([{ login: "Mona", reason: "first" }, { login: "mona", reason: "second" }]); expect(entries).toEqual([{ login: "Mona", reason: "first" }]);