Skip to content

Plugin Directory: Email committers the outcome of a security scan - #795

Draft
obenland wants to merge 6 commits into
WordPress:trunkfrom
obenland:feature/gandalf-scan-committer-notifications
Draft

Plugin Directory: Email committers the outcome of a security scan#795
obenland wants to merge 6 commits into
WordPress:trunkfrom
obenland:feature/gandalf-scan-committer-notifications

Conversation

@obenland

@obenland obenland commented Aug 12, 2026

Copy link
Copy Markdown
Member

Summary

Adds the author-facing half of the security scan pipeline: a completed scan callback now emails the plugin's committers its findings, alongside the existing reviewer surfaces (Slack alert, internal note).

Builds on #777 — its commit is included here until it lands; only the latest commit is new.

What it does

  • A new Email\Security_Scan_Findings (Markdown, multipart HTML + plain text) tells committers what was found: per finding the risk score, title, the file path linked to the plugins.trac.wordpress.org browser, the reported code snippet as a code block, and the finding's explanation. Findings render highest-risk first under a Findings subheader, bounded to ten; snippets and explanations reach the email unstripped since only the stored meta row needs bounding. The internal report URL is deliberately not shared.
  • The email reflects what happened. Blocked: the security review found issues severe enough to withhold the version from the update API, sites keep receiving the previous version, and the fix is in the author's hands — address the findings and release a new version, which escapes the block (Plugin Directory: Add an API to block a release from being served #785's per-release scoping). Advisory: please review the findings and address them in an upcoming release.
  • Whether to notify is adjustable by risk score: Plugin_Scan_Gandalf::NOTIFY_RISK_SCORE (default 8.0, the block threshold) via the wporg_plugins_security_scan_notify_risk_score filter — lower it to start advising authors below the block bar, raise it above 10 to disable the emails entirely.
  • Recipients are the plugin's committers minus bot and no-login accounts, matching the release-confirmation email in cli/class-import.php. Dedup mirrors the Slack alert: release blocks always email, advisory results deduplicate per verdict hash (month window), so a blocked outcome can never be silenced by an earlier advisory email of the same verdict.
  • Finding strings are treated as untrusted scanner output: angle brackets are entity-encoded rather than stripped (so text like <slug> or <?php survives as text while markup can't go live; the plain-text variant decodes them back), length is bounded, Markdown link/image syntax is neutralized so a hostile title can't smuggle a masquerading anchor into an email from plugins@wordpress.org, backticks become apostrophes to keep line-leading pairs out of Markdown::code_trick(), and snippets use indented code blocks, which Markdown escapes and which have no fence to break out of. Trac URLs are built from per-segment rawurlencode()d paths.

Testing

tests/Security_Scan_Notification_Test.php (10 tests) covers the blocked and advisory email content, the inclusive threshold and both filter directions (lowering and disabling), advisory dedup across scans vs. blocks always emailing, bot-account exclusion (against a real account, so only the filter excludes it), hostile-string neutralization (escaped markup in titles and snippets, Markdown links), and a finding carrying only the contractually required risk_score. Full plugin-directory suite passes (229 tests); both new files lint clean.

🤖 Generated with Claude Code

obenland and others added 6 commits August 13, 2026 13:48
… risk score.

Security scan callbacks now carry a max_risk_score and a bounded findings
array. A completed scan at or above the threshold blocks the scanned
release pending review — the previously served version keeps being
served — and the plugin review team receives the findings as context via
an internal note, a Slack alert, and a stored evidence snapshot. A verdict
for a version already served, or superseded by a newer release, can't
un-ship anything and stays advisory.

Scanner retries are acknowledged idempotently under a canonical digest:
an identical retry repeats no effects, and a completed verdict supersedes
an earlier failure report for the same scan. Finding fields beyond the
risk score are optional per the callback contract and read defensively.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A completed security scan whose maximum risk score reaches the
notification threshold (default 8.0, filterable via
wporg_plugins_security_scan_notify_risk_score) emails the plugin's
committers the findings: risk score, title, and a Trac browser link per
finding. A blocked release is reflected in the email, including that a
new version escapes the block; below the block threshold the email is
advisory. Release blocks always email, advisory results deduplicate per
verdict hash, and finding strings are treated as untrusted throughout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pending one.

The blocked email now leads with the cause and the fix: the review found
issues severe enough to block the version, address them and release a
new version.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…and linked paths.

Findings now carry their code snippet (an indented code block, escaped by
Markdown and immune to fence breakout) and explanation, passed to the
email unstripped since only the stored meta row needs bounding. Untrusted
prose is entity-encoded rather than tag-stripped so text like <slug> or
<?php survives, backticks become apostrophes to keep line-leading pairs
out of Markdown::code_trick(), and the plain-text variant decodes what
the Markdown source encodes. The file path now links to the Trac browser
instead of a separate bare URL, Findings became a subheader, and the
blocked wording drops the version-scope clause.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@obenland
obenland force-pushed the feature/gandalf-scan-committer-notifications branch from 8d5e9d7 to f51dd9e Compare August 13, 2026 18:49
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.

1 participant