Skip to content

feat(security-contacts): emails reachability & noise filtering [CM-1324]#4353

Merged
mbani01 merged 13 commits into
mainfrom
feat/security_emails_reachability
Jul 17, 2026
Merged

feat(security-contacts): emails reachability & noise filtering [CM-1324]#4353
mbani01 merged 13 commits into
mainfrom
feat/security_emails_reachability

Conversation

@mbani01

@mbani01 mbani01 commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

This pull request introduces a comprehensive system for classifying and tracking the reachability of security contact email addresses. It adds new database fields, implements email reachability heuristics, and ensures that only reachable contacts are considered in package detail queries. The most important changes are grouped below:

Database schema updates:

  • Added reachable (boolean) and reachability_reason (text) columns to the security_contacts table to store email reachability status and the reason for unreachability.

Email reachability classification:

  • Implemented the classifyEmailReachability function in email.ts, which uses heuristics to determine if an email is reachable, and if not, provides a specific reason (e.g., invalid, noreply, bot, disposable, etc.).
  • Introduced the EmailReachabilityReason type and supporting constants for classifying email addresses. [1] [2] [3]

Contact processing and storage:

  • Modified the contact reconciliation process to classify each contact's reachability and include the result in the ScoredContact type and database writes. [1] [2] [3] [4] [5] [6]

Query logic update:

  • Updated the package detail query to only return security contacts that are marked as reachable.

@mbani01 mbani01 self-assigned this Jul 16, 2026
Copilot AI review requested due to automatic review settings July 16, 2026 08:43
@cursor

cursor Bot commented Jul 16, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes which security contacts are stored and exposed (reachability filtering) and alters batch persistence semantics; incorrect heuristics could hide legitimate contacts or mislabel reachability.

Overview
Adds email reachability for security contacts: new reachable / reachability_reason columns, heuristic classification in @crowd/common (classifyEmailReachability — noreply, bots, disposable domains, placeholders, local-machine, invalid), and persistence through reconcile and contact upserts. Package detail queries now only surface contacts with reachable = true.

Refactors the packages worker so extraction returns ProcessRepoResult and batch/on-demand paths persist in bulk (writeContactsBatch, chunked writes with per-chunk error isolation) instead of per-repo DB transactions during concurrent extraction—plus heartbeat through the write phase and an index on repos.contacts_last_refreshed for sweep eligibility.

Also derives GitHub handles from noreply emails for CDP resolution, tightens CDP email provenance, and adds emailSafeKnownBots plus a vendored disposable-domain blocklist.

Reviewed by Cursor Bugbot for commit b063400. Bugbot is set up for automated code reviews on this repo. Configure here.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

Comment thread services/libs/common/src/email.ts

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds email-reachability classification and filters unreachable security contacts from package details.

Changes:

  • Classifies invalid, automated, disposable, and placeholder emails.
  • Persists reachability metadata during reconciliation.
  • Filters package security contacts by reachability.

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
services/libs/data-access-layer/src/osspckgs/api.ts Filters unreachable contacts.
services/libs/common/src/email.ts Adds reachability heuristics.
services/libs/common/src/constants/index.ts Exports disposable domains.
services/apps/packages_worker/src/security-contacts/writeContacts.ts Persists classification results.
services/apps/packages_worker/src/security-contacts/types.ts Extends contact types.
services/apps/packages_worker/src/security-contacts/reconcile.ts Classifies reconciled contacts.
backend/src/osspckgs/migrations/V1784073600__security_contacts_reachability.sql Adds reachability columns.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread services/libs/common/src/email.ts Outdated
Comment thread services/libs/common/src/constants/index.ts
mbani01 added 4 commits July 16, 2026 12:35
Signed-off-by: Mouad BANI <mouad-mb@outlook.com>
Signed-off-by: Mouad BANI <mouad-mb@outlook.com>
Signed-off-by: Mouad BANI <mouad-mb@outlook.com>
Signed-off-by: Mouad BANI <mouad-mb@outlook.com>
@mbani01
mbani01 force-pushed the feat/security_emails_reachability branch from 1ec9d67 to 22339ca Compare July 16, 2026 11:36
Copilot AI review requested due to automatic review settings July 16, 2026 11:36
Signed-off-by: Mouad BANI <mouad-mb@outlook.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.

@@ -0,0 +1,2 @@
ALTER TABLE security_contacts ADD COLUMN IF NOT EXISTS reachable BOOLEAN NOT NULL DEFAULT TRUE;
@@ -1,2 +1,3 @@
export * from './email-providers'
export * from './bots'
export * from './disposable-email-domains'
SELECT channel, value, role, confidence, score
FROM security_contacts
WHERE repo_id = pr.repo_id AND deleted_at IS NULL
WHERE repo_id = pr.repo_id AND deleted_at IS NULL AND reachable
Copilot AI review requested due to automatic review settings July 16, 2026 11:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.

@@ -0,0 +1,2 @@
ALTER TABLE security_contacts ADD COLUMN IF NOT EXISTS reachable BOOLEAN NOT NULL DEFAULT TRUE;
SELECT channel, value, role, confidence, score
FROM security_contacts
WHERE repo_id = pr.repo_id AND deleted_at IS NULL
WHERE repo_id = pr.repo_id AND deleted_at IS NULL AND reachable
Signed-off-by: Mouad BANI <mouad-mb@outlook.com>
Copilot AI review requested due to automatic review settings July 16, 2026 12:12
if (BOT_EXACT_LOCAL_PARTS.has(lp)) return true
// 'bot' as a delimited token: bot-*, *-bot, *.bot, bot_* ...
return /(^|[.\-_+])bot([.\-_+]|$)/.test(lp)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Role-prefixed bot local parts

Medium Severity

The bot heuristic treats any local part ending in -bot as unreachable, but role exemptions only apply to exact local parts like security. Addresses such as security-bot@company.com match the -bot token rule and are marked unreachable even though they are plausible human security contacts.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9db3785. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 8 changed files in this pull request and generated 4 comments.

@@ -0,0 +1,2 @@
ALTER TABLE security_contacts ADD COLUMN IF NOT EXISTS reachable BOOLEAN NOT NULL DEFAULT TRUE;
'example.com',
'example.org',
'example.net',
'example.edu',
'actions',
])

const emailDomain = (value: string): string => value.slice(value.indexOf('@') + 1)
SELECT channel, value, role, confidence, score
FROM security_contacts
WHERE repo_id = pr.repo_id AND deleted_at IS NULL
WHERE repo_id = pr.repo_id AND deleted_at IS NULL AND reachable
Signed-off-by: Mouad BANI <mouad-mb@outlook.com>
Copilot AI review requested due to automatic review settings July 16, 2026 13:55
Signed-off-by: Mouad BANI <mouad-mb@outlook.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 6 comments.

Comment on lines 226 to 228
} finally {
clearInterval(heartbeatTimer)
}
@@ -0,0 +1,2 @@
ALTER TABLE security_contacts ADD COLUMN IF NOT EXISTS reachable BOOLEAN NOT NULL DEFAULT TRUE;
Comment thread services/libs/common/src/email.ts Outdated
Copilot AI review requested due to automatic review settings July 16, 2026 14:03
…s-tag noreply detection

Signed-off-by: Mouad BANI <mouad-mb@outlook.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 5 comments.

Comment on lines +86 to +91
const PLACEHOLDER_EMAIL_DOMAINS = new Set([
'example.com',
'example.org',
'example.net',
'example.edu',
])
Comment on lines 226 to 228
} finally {
clearInterval(heartbeatTimer)
}
@@ -0,0 +1,2 @@
ALTER TABLE security_contacts ADD COLUMN IF NOT EXISTS reachable BOOLEAN NOT NULL DEFAULT TRUE;
Comment on lines 142 to +144
const scored: ScoredContact[] = merged.map((c) => {
const contact = { ...c, provenance: dedupeProvenance(c.provenance) }
return { ...contact, ...scoreContact(contact, now) }
return { ...contact, ...scoreContact(contact, now), ...classifyReachability(contact) }
Comment on lines +1 to +2
ALTER TABLE security_contacts ADD COLUMN IF NOT EXISTS reachable BOOLEAN NOT NULL DEFAULT TRUE;
ALTER TABLE security_contacts ADD COLUMN IF NOT EXISTS reachability_reason TEXT;
Copilot AI review requested due to automatic review settings July 16, 2026 14:13
// 2-minute heartbeat timeout just like extraction can, and letting the timer stop early would
// let Temporal retry this activity while the original attempt is still writing.
const writeStartedAt = Date.now()
await writeContactsBatch(qx, outcomes)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cancel drops extracted batch work

Medium Severity

Batch processing now collects every processRepo outcome and only calls writeContactsBatch after all concurrent extractions finish. If the activity is cancelled (or mapWithConcurrency aborts) during extraction, none of the completed repos are persisted and markReposAttempted never runs for extractor failures, unlike the previous per-repo writes.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d1e868b. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 3 comments.

Comment on lines +119 to +120
const lp = localPart.toLowerCase()
if (ROLE_LOCAL_PARTS.has(lp)) return false
@@ -0,0 +1,2 @@
ALTER TABLE security_contacts ADD COLUMN IF NOT EXISTS reachable BOOLEAN NOT NULL DEFAULT TRUE;
Comment on lines +190 to +193
for (let i = 0; i < ok.length; i += WRITE_CHUNK_SIZE) {
const chunk = ok.slice(i, i + WRITE_CHUNK_SIZE)
try {
await writeContactsChunk(qx, chunk)
Signed-off-by: Mouad BANI <mouad-mb@outlook.com>
Copilot AI review requested due to automatic review settings July 17, 2026 12:48
export async function processRepo(
target: RepoTarget,
baseDeps: Omit<ExtractorDeps, 'repoTree'>,
qx: QueryExecutor,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sweep ignores reachable-only contacts

Medium Severity

The batch sweep treats any non-deleted security_contacts row as “has contacts” for weekly refresh, but this change persists email rows with reachable = false and package reads only return reachable contacts. Repos whose only stored rows are unreachable emails can look enriched to the sweep while the API shows no contacts, so they skip the daily retry path for up to a week.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8de3130. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 5 comments.

Comment on lines +24 to +25
const handle = parseGitHubNoreplyEmail(c.value)
if (!handle) continue
@@ -0,0 +1,2 @@
ALTER TABLE security_contacts ADD COLUMN IF NOT EXISTS reachable BOOLEAN NOT NULL DEFAULT TRUE;
Comment on lines 142 to +144
const scored: ScoredContact[] = merged.map((c) => {
const contact = { ...c, provenance: dedupeProvenance(c.provenance) }
return { ...contact, ...scoreContact(contact, now) }
return { ...contact, ...scoreContact(contact, now), ...classifyReachability(contact) }
Comment on lines +3 to +4
CREATE INDEX IF NOT EXISTS repos_contacts_last_refreshed_idx
ON repos (contacts_last_refreshed);
SELECT channel, value, role, confidence, score
FROM security_contacts
WHERE repo_id = pr.repo_id AND deleted_at IS NULL
WHERE repo_id = pr.repo_id AND deleted_at IS NULL AND reachable
Signed-off-by: Mouad BANI <mouad-mb@outlook.com>
Copilot AI review requested due to automatic review settings July 17, 2026 12:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 11 comments.

@@ -0,0 +1,2 @@
ALTER TABLE security_contacts ADD COLUMN IF NOT EXISTS reachable BOOLEAN NOT NULL DEFAULT TRUE;
Comment on lines +85 to +90
const PLACEHOLDER_EMAIL_DOMAINS = new Set([
'example.com',
'example.org',
'example.net',
'example.edu',
])
'actions',
])

const emailDomain = (value: string): string => value.slice(value.indexOf('@') + 1)
Comment on lines 142 to +144
const scored: ScoredContact[] = merged.map((c) => {
const contact = { ...c, provenance: dedupeProvenance(c.provenance) }
return { ...contact, ...scoreContact(contact, now) }
return { ...contact, ...scoreContact(contact, now), ...classifyReachability(contact) }
Comment on lines +23 to +25
if (c.channel !== 'email') continue
const handle = parseGitHubNoreplyEmail(c.value)
if (!handle) continue
SELECT channel, value, role, confidence, score
FROM security_contacts
WHERE repo_id = pr.repo_id AND deleted_at IS NULL
WHERE repo_id = pr.repo_id AND deleted_at IS NULL AND reachable
Comment on lines +1 to +2
ALTER TABLE security_contacts ADD COLUMN IF NOT EXISTS reachable BOOLEAN NOT NULL DEFAULT TRUE;
ALTER TABLE security_contacts ADD COLUMN IF NOT EXISTS reachability_reason TEXT;
Comment on lines +190 to +193
for (let i = 0; i < ok.length; i += WRITE_CHUNK_SIZE) {
const chunk = ok.slice(i, i + WRITE_CHUNK_SIZE)
try {
await writeContactsChunk(qx, chunk)
Comment on lines +65 to +67
if (c.channel !== 'email') return { reachable: true, reachabilityReason: null }
const r = classifyEmailReachability(c.value)
return { reachable: r.reachable, reachabilityReason: r.reason }
@@ -1,2 +1,3 @@
export * from './email-providers'
export * from './bots'
export * from './disposable-email-domains'
Signed-off-by: Mouad BANI <mouad-mb@outlook.com>
Copilot AI review requested due to automatic review settings July 17, 2026 15:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 6 comments.

Comment on lines +77 to +79
handle: contact.value,
tier: contact.tier,
provenance: [{ source, sourceTier: contact.tier, fetchedAt }],
provenance: [...contact.provenance, { source, sourceTier: contact.tier, fetchedAt }],
@@ -0,0 +1,2 @@
ALTER TABLE security_contacts ADD COLUMN IF NOT EXISTS reachable BOOLEAN NOT NULL DEFAULT TRUE;
Comment on lines +3 to +4
CREATE INDEX IF NOT EXISTS repos_contacts_last_refreshed_idx
ON repos (contacts_last_refreshed);
Comment on lines +1 to +2
ALTER TABLE security_contacts ADD COLUMN IF NOT EXISTS reachable BOOLEAN NOT NULL DEFAULT TRUE;
ALTER TABLE security_contacts ADD COLUMN IF NOT EXISTS reachability_reason TEXT;
@@ -1,2 +1,3 @@
export * from './email-providers'
export * from './bots'
export * from './disposable-email-domains'
SELECT channel, value, role, confidence, score
FROM security_contacts
WHERE repo_id = pr.repo_id AND deleted_at IS NULL
WHERE repo_id = pr.repo_id AND deleted_at IS NULL AND reachable
Copilot AI review requested due to automatic review settings July 17, 2026 16:19
@mbani01
mbani01 merged commit 70a96d7 into main Jul 17, 2026
14 checks passed
@mbani01
mbani01 deleted the feat/security_emails_reachability branch July 17, 2026 16:20

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 4 total unresolved issues (including 3 from previous reviews).

Fix All in Cursor

Reviewed by Cursor Bugbot for commit b063400. Configure here.

],
})
}
return derived

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitLab noreply handles not derived

Medium Severity

deriveGithubHandlesFromNoreplyEmails only parses GitHub noreply addresses. GitLab @users.noreply.gitlab.com emails are classified unreachable elsewhere but never get a derived handle for CDP resolution, so GitLab-sourced noreply contacts miss the recovery path GitHub noreply contacts receive.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b063400. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 6 comments.

contacts = contacts.filter((c) => c.channel !== 'github-pvr')
}

contacts.push(...deriveGithubHandlesFromNoreplyEmails(contacts))
@@ -0,0 +1,2 @@
ALTER TABLE security_contacts ADD COLUMN IF NOT EXISTS reachable BOOLEAN NOT NULL DEFAULT TRUE;
Comment on lines +1 to +2
ALTER TABLE security_contacts ADD COLUMN IF NOT EXISTS reachable BOOLEAN NOT NULL DEFAULT TRUE;
ALTER TABLE security_contacts ADD COLUMN IF NOT EXISTS reachability_reason TEXT;
import validator from 'validator'

import { noreplyEmailProviders } from './constants'
import { disposableEmailDomains, emailSafeKnownBots, noreplyEmailProviders } from './constants'
Comment on lines +209 to +212
// Extraction is collected here and persisted afterward in one batched call (below) rather
// than per-repo, since per-repo writes meant up to CONCURRENCY concurrent transactions
// against a packages-db pool sized for far fewer connections — the actual sweep bottleneck.
outcomes = await mapWithConcurrency(targets, CONCURRENCY, async (target) => {
SELECT channel, value, role, confidence, score
FROM security_contacts
WHERE repo_id = pr.repo_id AND deleted_at IS NULL
WHERE repo_id = pr.repo_id AND deleted_at IS NULL AND reachable
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants