chore: adjust activity alerts for allowed domains and accounts#15343
chore: adjust activity alerts for allowed domains and accounts#15343
Conversation
mnkiefer
commented
Feb 13, 2026
- Adds explicit allowlists for domains, accounts, and organizations, dynamically loading repository and organization members, and refining the reporting output for better clarity.
There was a problem hiding this comment.
Pull request overview
This PR enhances the bot detection workflow by adding explicit allowlists for trusted domains, accounts, and organizations. It dynamically loads repository collaborators and organization members to reduce false positives, and improves the reporting output by embedding evidence per-account and showing which logins are associated with each external domain.
Changes:
- Adds allowlists for trusted domains (GitHub docs, package registries, language vendor sites), accounts (bots, service accounts), and organizations with dynamic member loading
- Introduces environment variable support for extending allowlists via
BOT_DETECTION_ALLOWED_DOMAINS,BOT_DETECTION_ALLOWED_ACCOUNTS, andBOT_DETECTION_TRUSTED_ORGS - Refactors reporting to show login examples in the domains table and embed evidence details per-account instead of in a separate section
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| .github/workflows/bot-detection.md | Adds allowlist constants, dynamic member loading functions, integrates allowlist checks throughout the analysis flow, and updates the report format |
| .github/workflows/bot-detection.lock.yml | Compiled version of the .md file changes with identical logic updates |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (login) { | ||
| if (isAllowedAccount(login)) continue; |
There was a problem hiding this comment.
This continue statement causes the entire PR to be skipped when the PR author is an allowed account. This means comments and reviews on that PR (which may come from suspicious accounts) are never analyzed. The continue should be removed, and only the ensureUserCreatedAt call on line 322 should be skipped for allowed accounts.
| if (login) { | |
| if (isAllowedAccount(login)) continue; | |
| if (login && !isAllowedAccount(login)) { |
| for (const org of TRUSTED_ORGS) { | ||
| try { | ||
| const members = await github.paginate(github.rest.orgs.listMembers, { | ||
| org, | ||
| per_page: 100, | ||
| }); | ||
| for (const member of members) { | ||
| if (member?.login) { | ||
| MEMBER_ACCOUNTS.add(String(member.login).toLowerCase()); | ||
| } | ||
| } |
There was a problem hiding this comment.
The github.paginate call for org members lacks a cap. For large organizations like "github", this could fetch thousands of members. Consider adding pagination caps similar to other paginate calls in this file (lines 328-350, 358-380, 388-410, 464-486) that use a response callback with a cap of 500 items.
| for (const org of TRUSTED_ORGS) { | |
| try { | |
| const members = await github.paginate(github.rest.orgs.listMembers, { | |
| org, | |
| per_page: 100, | |
| }); | |
| for (const member of members) { | |
| if (member?.login) { | |
| MEMBER_ACCOUNTS.add(String(member.login).toLowerCase()); | |
| } | |
| } | |
| const MAX_ORG_MEMBERS = 500; | |
| for (const org of TRUSTED_ORGS) { | |
| let fetchedForOrg = 0; | |
| try { | |
| await github.paginate( | |
| github.rest.orgs.listMembers, | |
| { | |
| org, | |
| per_page: 100, | |
| }, | |
| (response, done) => { | |
| for (const member of response.data || []) { | |
| if (member?.login) { | |
| MEMBER_ACCOUNTS.add(String(member.login).toLowerCase()); | |
| fetchedForOrg++; | |
| if (fetchedForOrg >= MAX_ORG_MEMBERS) { | |
| done(); | |
| break; | |
| } | |
| } | |
| } | |
| // We process members directly; no need to accumulate return values. | |
| return []; | |
| } | |
| ); |
| const collaborators = await github.paginate(github.rest.repos.listCollaborators, { | ||
| owner, | ||
| repo, | ||
| per_page: 100, | ||
| }); | ||
| for (const collaborator of collaborators) { | ||
| if (collaborator?.login) { | ||
| MEMBER_ACCOUNTS.add(String(collaborator.login).toLowerCase()); | ||
| } | ||
| } |
There was a problem hiding this comment.
The github.paginate call for collaborators lacks a cap. For repositories with many collaborators, this could fetch a large number of items without limit. Consider adding pagination caps similar to other paginate calls in this file that use a response callback with a cap of 500 items.
| const collaborators = await github.paginate(github.rest.repos.listCollaborators, { | |
| owner, | |
| repo, | |
| per_page: 100, | |
| }); | |
| for (const collaborator of collaborators) { | |
| if (collaborator?.login) { | |
| MEMBER_ACCOUNTS.add(String(collaborator.login).toLowerCase()); | |
| } | |
| } | |
| let count = 0; | |
| await github.paginate( | |
| github.rest.repos.listCollaborators, | |
| { | |
| owner, | |
| repo, | |
| per_page: 100, | |
| }, | |
| (response, done) => { | |
| for (const collaborator of response.data) { | |
| if (count >= 500) { | |
| done(); | |
| break; | |
| } | |
| if (collaborator?.login) { | |
| MEMBER_ACCOUNTS.add(String(collaborator.login).toLowerCase()); | |
| count++; | |
| } | |
| } | |
| // We only use side effects on MEMBER_ACCOUNTS; no need to return data. | |
| return []; | |
| } | |
| ); |
| for (const it of prItems) { | ||
| const login = it.author; | ||
| if (login) { | ||
| if (isAllowedAccount(login)) continue; |
There was a problem hiding this comment.
This continue statement causes the entire PR to be skipped when the PR author is an allowed account. This means comments and reviews on that PR (which may come from suspicious accounts) are never analyzed. The continue should be removed, and only the ensureUserCreatedAt call on line 1295 should be skipped for allowed accounts.
See below for a potential fix:
if (login && !isAllowedAccount(login)) {
await ensureUserCreatedAt(login);
}
let issueComments = [];
try {
try {
| const members = await github.paginate(github.rest.orgs.listMembers, { | ||
| org, | ||
| per_page: 100, | ||
| }); |
There was a problem hiding this comment.
The github.paginate call for org members lacks a cap. For large organizations like "github", this could fetch thousands of members. Consider adding pagination caps similar to other paginate calls in this file (lines 1100-1104, 1118-1121) that use a response callback with a cap of 500 items.
| const members = await github.paginate(github.rest.orgs.listMembers, { | |
| org, | |
| per_page: 100, | |
| }); | |
| let memberCount = 0; | |
| const members = await github.paginate( | |
| github.rest.orgs.listMembers, | |
| { | |
| org, | |
| per_page: 100, | |
| }, | |
| (response, done) => { | |
| const remaining = 500 - memberCount; | |
| if (remaining <= 0) { | |
| done(); | |
| return []; | |
| } | |
| const pageItems = response.data.slice(0, remaining); | |
| memberCount += pageItems.length; | |
| if (memberCount >= 500) { | |
| done(); | |
| } | |
| return pageItems; | |
| } | |
| ); |
| try { | ||
| const collaborators = await github.paginate(github.rest.repos.listCollaborators, { | ||
| owner, | ||
| repo, | ||
| per_page: 100, | ||
| }); |
There was a problem hiding this comment.
The github.paginate call for collaborators lacks a cap. For repositories with many collaborators, this could fetch a large number of items without limit. Consider adding pagination caps similar to other paginate calls in this file that use a response callback with a cap of 500 items.
| try { | |
| const collaborators = await github.paginate(github.rest.repos.listCollaborators, { | |
| owner, | |
| repo, | |
| per_page: 100, | |
| }); | |
| let totalCollaborators = 0; | |
| try { | |
| const collaborators = await github.paginate( | |
| github.rest.repos.listCollaborators, | |
| { | |
| owner, | |
| repo, | |
| per_page: 100, | |
| }, | |
| (response, done) => { | |
| const remaining = 500 - totalCollaborators; | |
| if (remaining <= 0) { | |
| done(); | |
| return []; | |
| } | |
| const pageItems = Array.isArray(response.data) | |
| ? response.data.slice(0, remaining) | |
| : []; | |
| totalCollaborators += pageItems.length; | |
| if (totalCollaborators >= 500) { | |
| done(); | |
| } | |
| return pageItems; | |
| } | |
| ); |