Skip to content

Plugin Directory: Block a release when a security scan reports a high risk score#729

Draft
obenland wants to merge 3 commits into
WordPress:trunkfrom
obenland:feature/gandalf-risk-score-block
Draft

Plugin Directory: Block a release when a security scan reports a high risk score#729
obenland wants to merge 3 commits into
WordPress:trunkfrom
obenland:feature/gandalf-risk-score-block

Conversation

@obenland

Copy link
Copy Markdown
Member

Summary

Adds the scan-driven half of the release block: when a Gandalf scan reports a high risk_score
for a plugin version still inside its release cooldown, that version is held out of the update
API indefinitely — the previously served version keeps being served — until a reviewer
force-releases it.

This is the second half of #720, which is now split in two. It builds on #728, which adds the
blocking mechanism itself and the reviewer control.

Important

Stacked on #728. GitHub can't base a fork PR on another fork branch, so this PR's diff
includes #728's commits. Review only the last commitBlock a release when a security scan reports a high risk score. This should merge after #728.

How it works

  • ThresholdPlugin_Scan_Gandalf::RISK_SCORE_BLOCK_THRESHOLD (default 8). A completed
    scan at or above it blocks; below, or absent, it does not.
  • Precedence — block (risk ≥ threshold) → findings alert (existing) → clean.
  • Only within cooldown — a verdict for a version that is already live, was superseded by a
    newer commit mid-scan, or had no delay can't un-ship anything; it's escalated to Slack for
    manual action instead of held.
  • Attributionforce_release() and the Controls metabox name the risk score when a scan
    set the block, rather than describing it as a reviewer's.
  • Notifications — the findings and block Slack alerts now share one sender, and their
    user-facing wording says "security scan" rather than naming the scanner.

What ships inert

Auto-blocking on a risk score is disabled. A high score alerts Slack; a human decides. The
call is one commented-out line in handle_callback() to restore.

Gandalf doesn't send risk_score yet either, so the field is read defensively and the path stays
inert until both change.

Tests

  • Gandalf_Risk_Score_Block_Test drives handle_callback() directly for the handler's branches.
  • Gandalf_Risk_Score_Block_Callback_Test covers the full chain over the REST route — auth,
    routing, the write to update_source, force-release closing the loop, the block outlasting
    the cooldown.
  • Fixtures build on Plugin Directory: Let reviewers block a release from being served #728's Release_Block_Test_Case via Gandalf_Scan_Test_Case
    Gandalf_Callback_Test_Case.

190 tests pass locally, 8 skipped.

Note

Because auto-blocking is off, the 8 tests that assert a hold are skipped, and several of the
remaining ..._does_not_block tests pass vacuously — nothing blocks, so "it didn't block"
is not evidence the threshold logic works. Real coverage of this path only exists once the
commented-out call is restored. Flagging it so the green checkmark isn't read as more than
it is.

Notes for reviewers

🤖 Generated with Claude Code

obenland and others added 3 commits July 22, 2026 13:43
The test environment was missing three things the development environment
already had, which between them made whole classes of test impossible:

- `update_source` was never created, so anything touching the update API's
  table failed.
- `wp-content/env-bin` was not mapped, so `database-tables.sql` was not
  reachable from the test container to import.
- `PLUGINS_TABLE_PREFIX` was undefined, so `API\Base::load_routes()` fatalled
  and no `plugins/v1` route was registered.

Shared verbatim with WordPress#717; whichever of the two PRs lands second should drop
this commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UNnLdnE7etihGNvsWY1qGE
Reviewers could already skip the release cooldown, but had no way to hold a
release back short of closing the whole plugin. Add a Block control to the
Controls metabox, alongside the force-release one and sharing a single reason
field, so a version still inside its cooldown can be held out of `update_source`
until it's force-released.

- API_Update_Updater::block_release() is the counterpart to force_release(): it
  records a `release_block` on the release row, and a single gate in
  update_single_plugin() honours it, so both the deferred cooldown cron and the
  backup reconciliation cron keep the version out of `update_source`.
- A version that's already live can't be un-shipped, so a block on one is
  refused rather than recorded as a hold that never takes effect.
- force_release() clears the block and notes the override in the audit log.

Holding a version back means update_single_plugin() can't rewrite the row at
all, since every version-specific column describes the held version. That was
already true of the cooldown, and it means a status change never reached sites
while one was in flight: closing a plugin whose next version was deferred left
`update_source` serving the current version with `available = 1`. A block makes
that unbounded rather than a few hours, so sync_availability() now writes the
availability and closure fields on their own and leaves the served version
alone.

Covered by Reviewer_Release_Block_Test, which exercises the block through the
same path the metabox uses, including closing and re-opening a plugin while a
version is held. Its fixture lives in Release_Block_Test_Case, resolved by a
small autoloader in the test bootstrap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… risk score.

Builds on the reviewer release block: when a Gandalf scan reports a high
`risk_score` for a plugin version still inside its release cooldown, that
version is held out of the update API indefinitely — the previously served
version keeps being served — until a reviewer force-releases it.

- Threshold: Plugin_Scan_Gandalf::RISK_SCORE_BLOCK_THRESHOLD (default 8). A
  completed scan at or above it blocks; below, or absent, it does not.
- Precedence: block (risk >= threshold) -> findings alert (existing) -> clean.
- Only within cooldown: a verdict for a version that is already live, was
  superseded by a newer commit mid-scan, or had no delay can't un-ship
  anything. It's escalated to Slack for manual action instead of held.
- force_release() and the Controls metabox name the risk score when a scan set
  the block, rather than describing it as a reviewer's.
- The findings and block Slack alerts now share one sender, and their
  user-facing wording says "security scan" rather than naming the scanner.

Auto-blocking on a risk score is left disabled, so this ships independently of
any upstream Gandalf work: a high score alerts Slack, but a human decides. The
tests covering that path are skipped rather than removed, and the call is one
line to restore.

Gandalf does not send a `risk_score` yet either, so the field is read
defensively and the feature stays inert until it does.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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