Skip to content

Commit 8de3130

Browse files
committed
fix: resolve GitHub handles from noreply emails, fix bot false positives
Signed-off-by: Mouad BANI <mouad-mb@outlook.com>
1 parent d1e868b commit 8de3130

4 files changed

Lines changed: 82 additions & 6 deletions

File tree

services/apps/packages_worker/src/security-contacts/processBatch.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { extractSecurityMd } from './extractors/securityMd'
1616
import { extractSecurityTxt } from './extractors/securityTxt'
1717
import { githubApiGet } from './githubToken'
1818
import { reconcile } from './reconcile'
19-
import { resolveCdpEmails } from './resolveCdpEmails'
19+
import { deriveGithubHandlesFromNoreplyEmails, resolveCdpEmails } from './resolveCdpEmails'
2020
import {
2121
Extractor,
2222
ExtractorDeps,
@@ -162,6 +162,8 @@ export async function processRepo(
162162
contacts = contacts.filter((c) => c.channel !== 'github-pvr')
163163
}
164164

165+
contacts.push(...deriveGithubHandlesFromNoreplyEmails(contacts))
166+
165167
const handleContacts = contacts.filter((c) => c.channel === 'github-handle')
166168
if (handleContacts.length > 0) {
167169
try {

services/apps/packages_worker/src/security-contacts/resolveCdpEmails.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { parseGitHubNoreplyEmail } from '@crowd/common'
12
import {
23
findMembersByGithubHandles,
34
findResolvableEmailsForMembers,
@@ -13,6 +14,28 @@ function latestTimestamp(provenance: ProvenanceEntry[]): string {
1314
: times.reduce((a, b) => (new Date(b).getTime() > new Date(a).getTime() ? b : a))
1415
}
1516

17+
// A GitHub noreply email already encodes its owner's handle — derive it so the handle can be
18+
// resolved to a real address the same way any other github-handle contact is. The noreply email
19+
// itself is kept as the provenance source, so the resolved contact's origin stays traceable.
20+
export function deriveGithubHandlesFromNoreplyEmails(contacts: RawContact[]): RawContact[] {
21+
const derived: RawContact[] = []
22+
for (const c of contacts) {
23+
if (c.channel !== 'email') continue
24+
const handle = parseGitHubNoreplyEmail(c.value)
25+
if (!handle) continue
26+
derived.push({
27+
channel: 'github-handle',
28+
value: handle,
29+
role: c.role,
30+
tier: c.tier,
31+
provenance: [
32+
{ source: c.value, sourceTier: c.tier, fetchedAt: latestTimestamp(c.provenance) },
33+
],
34+
})
35+
}
36+
return derived
37+
}
38+
1639
export async function resolveCdpEmails(
1740
cdpQx: QueryExecutor,
1841
handleContacts: RawContact[],
@@ -51,8 +74,9 @@ export async function resolveCdpEmails(
5174
channel: 'email' as const,
5275
value: email,
5376
role: contact.role,
77+
handle: contact.value,
5478
tier: contact.tier,
55-
provenance: [{ source, sourceTier: contact.tier, fetchedAt }],
79+
provenance: [...contact.provenance, { source, sourceTier: contact.tier, fetchedAt }],
5680
}))
5781
})
5882
})

services/libs/common/src/constants/bots.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,54 @@ export const knownBots = new Set([
7979
'totesmessenger',
8080
'vredditdownloader',
8181
])
82+
83+
/**
84+
* Subset of knownBots safe for matching against email local-parts — excludes community
85+
* chat-bot usernames (Discord/Reddit) that collide with real personal names (e.g. craig, maki).
86+
*/
87+
export const emailSafeKnownBots = new Set([
88+
'allcontributors',
89+
'better-code-hub',
90+
'bors',
91+
'bors-servo',
92+
'brewtestbot',
93+
'codeclimate',
94+
'codepilot',
95+
'coderabbit',
96+
'copilot',
97+
'dependabot',
98+
'easycla',
99+
'fabbot',
100+
'facebook-github-bot',
101+
'github-merge-queue',
102+
'goreleaserbot',
103+
'harness-ci',
104+
'hashibot',
105+
'homebrewbumpbot',
106+
'homu',
107+
'jenkins',
108+
'mergify',
109+
'msftbot',
110+
'opentelemetrybot',
111+
'pep8speaks',
112+
'prombot',
113+
'pullrequest',
114+
'renovate',
115+
'reviewdog',
116+
'rust-highfive',
117+
'rustbot',
118+
'scala-steward',
119+
'scrutinizer-auto-fixer',
120+
'sonatype-lift',
121+
'sphinx-bot',
122+
'teamcity',
123+
'tensorflower',
124+
'web-flow',
125+
'weblate',
126+
'zuul',
127+
'codecov',
128+
'deepsource',
129+
'gitguardian',
130+
'snyk',
131+
'whitesource',
132+
])

services/libs/common/src/email.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import validator from 'validator'
22

3-
import { isKnownBot } from './bot'
4-
import { disposableEmailDomains, noreplyEmailProviders } from './constants'
3+
import { disposableEmailDomains, emailSafeKnownBots, noreplyEmailProviders } from './constants'
54

65
export const isValidEmail = (value: string): boolean => {
76
return validator.isEmail(value)
@@ -118,7 +117,7 @@ const emailDomain = (value: string): string => value.slice(value.indexOf('@') +
118117
const looksLikeBot = (localPart: string): boolean => {
119118
const lp = localPart.toLowerCase()
120119
if (ROLE_LOCAL_PARTS.has(lp)) return false
121-
if (isKnownBot(lp)) return true
120+
if (emailSafeKnownBots.has(lp)) return true
122121
if (lp.endsWith('[bot]')) return true
123122
if (BOT_EXACT_LOCAL_PARTS.has(lp)) return true
124123
// 'bot' as a delimited token: bot-*, *-bot, *.bot, bot_* ...
@@ -151,7 +150,7 @@ export const classifyEmailReachability = (email: string): EmailReachability => {
151150
const localPart = getEmailLocalPart(value)
152151
const baseLocalPart = localPart.split('+')[0]
153152
if (NOREPLY_LOCAL_PARTS.has(baseLocalPart)) return fail('noreply')
154-
if (looksLikeBot(localPart)) return fail('bot')
153+
if (looksLikeBot(baseLocalPart)) return fail('bot')
155154
if (disposableEmailDomains.has(emailDomain(value))) return fail('disposable')
156155

157156
return { email, reachable: true, reason: null }

0 commit comments

Comments
 (0)