feat: network actor blocking, intel column, post-CRITICAL moment, Upgrade wiring - #46
Merged
Conversation
- Add WebDecoy_Cloud_Connect::wp_upgrade_url() building the WordPress-channel checkout link (app.webdecoy.com/billing/wordpress?organization_id=..&plan=wp_pro, contract §4); Cloud tab Upgrade button now uses it. - Gate WebDecoy_IP_Enrichment remote calls behind features.enrichment: fail silently to no-enrichment when a synced entitlements cache says unentitled; never-synced (manual-key) installs still defer to the server's own 403.
…iring)
New controllers, all self-guarding to make ZERO external calls unless the site
is connected (and, for the feed, entitled):
- WebDecoy_Actor_Feed: hourly cron scheduled only when connected AND
features.actor_feed; fetches GET /sdk/actor-feed?since={cursor} (Bearer key,
follows next_since/complete pagination, persists the cursor). Mirrors feed IPs
into the existing blocked-IPs table as created_by='webdecoy-network' with a
rolling +48h expiry (refreshed each sync; lapses via cleanup when an actor
leaves the feed). Never touches non-network rows, never inserts an allowlisted
IP (exact + CIDR), caps at 2000 rows evicting oldest-seen first. 403
upgrade_required unschedules quietly; downgrade/disconnect unschedules on the
next admin load.
- WebDecoy_Actor_Intel: batched POST /sdk/detections/intel for <=50 unique IPs,
per-IP 1h transient cache, honors the server redacted flag (never surfaces
Pro-only fields on a free-connected teaser), fails silently to no-intel.
- WebDecoy_Critical_Moment: one-shot, 7-day-throttled admin notice queued from
the CRITICAL detection insert sites (canary/decoy). Queue writes local state
only; the intel lookup that personalises the copy is deferred to the admin
render, and unconnected installs show static Connect copy with no fetch.
Wire all three into load_includes(); queue the moment from the three
CRITICAL-capable insert sites; clear the new cron + options on
deactivate/uninstall.
- Blocked IPs: show a small 'Network' badge on created_by='webdecoy-network' rows (reuses the existing badge styles). - Detections: new 'Actor Intel' column between MITRE Tactic and Source. Unconnected renders a static lock glyph with a 'Connect to see actor intelligence' title and makes NO request. Connected resolves the page's unique IPs in one batched, cached lookup and renders known/count/first-seen (+ tor/ vpn/abuse chips when the response is non-redacted); em-dashes on any miss. - Styles for the network/known badges, intel chips, and the critical-moment notice.
Dependency-free tests (php tests/run.php) for the WordPress-free helpers: - feed pagination-continue, page normalization, cross-page IP merge, allowlist exclusion, and cap/eviction ordering; - intel response normalization incl. the free-connected redaction guarantee and unique/validated/capped IP batching; - the 7-day notice throttle, days-since-first-seen, and copy-variant selection.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Plugin side of the P1 Cloud-monetization mechanic. Closes #45. Part of WebDecoy/app#306.
Ships later as 2.4.0 — no version bump in this PR.
Zero-external-calls guarantee, per state
features.actor_feed)POST /sdk/detections/intelper render for uncached IPs; redacted teaserfeatures.enrichmentfalseGET /sdk/actor-feed?since=while entitledThe feed handler re-checks connected and entitled on every run and unschedules itself otherwise; a
403 upgrade_requiredunschedules quietly. Queueing the CRITICAL moment writes only local state — the personalising intel lookup happens later, in the connected admin context.Work items
includes/class-webdecoy-actor-feed.php. Hourly cron, cursor persisted in an option, pagination vianext_since/complete. Inserts through the existing blocked-IPs table ascreated_by='webdecoy-network', reason"WebDecoy network: known attacker (actor <id>)", rolling +48h expiry. Never inserts an allowlisted IP (exact + CIDR), never touches non-network rows, caps 2000 rows evicting oldest-seen first. Blocked-IPs page shows a Network badge.detections-page.php, backed byincludes/class-webdecoy-actor-intel.php(batched, per-IP 1h cache, redaction-aware, em-dashes on failure).includes/class-webdecoy-critical-moment.php, queued from the CRITICAL detection insert sites, one dismissible admin notice throttled to one per 7 days; connected+known → "first seen N days ago… on Pro it would have been blocked", or the Pro+ confirmation copy; unconnected → static Connect invite.WebDecoy_Cloud_Connect::wp_upgrade_url()for thebilling/wordpress?organization_id=..&plan=wp_prolink;WebDecoy_IP_Enrichmentgated behindfeatures.enrichment.Feed membership / cursor
Membership is server-derived (high-confidence actors with HIGH/CRITICAL detections) — the plugin consumes whatever the endpoint returns. The persisted cursor only ever moves forward (
next_since > requested), so a stalled/zero cursor never regresses or loops.How network rows coexist with user blocks
created_by='webdecoy-network'. Insert/refresh/evict operate only on that marker — a human/system/rate-limiter row for the same IP is read once (to detect ownership) and otherwise left completely untouched.WebDecoy_Blocker::is_allowlisted()(exact + CIDR), so an allowlisted IP is never network-blocked.blocked_at/expires_atfor live feed IPs; the existing hourlycleanup_expiredcron reaps them 48h after they last appeared. Cap enforcement deletes only network rows, oldestblocked_atfirst (blocked_atproxies last-seen since it's refreshed each sync).Throttle design
WebDecoy_Critical_Momentstores the last-queued epoch in thewebdecoy_critical_moment_lastoption; the pureshould_queue(last, now, window)gates queuing to at most once per 7 days. The pending notice lives in a 7-day transient (webdecoy_critical_moment) and is consumed on first admin render (one-shot) withis-dismissible.Tests / checks
php tests/run.php→ 66 passed, 0 failed (added coverage for feed pagination/cap/allowlist-exclusion, intel normalization/redaction, and the notice-throttle/variant logic).php -lclean on every touched file. Escaping/sanitize/unslash/nonce + phpcs suppressions follow the existing codebase style (class-webdecoy-blocker.phpfile-level DB disables); no new superglobal reads.Notes / left undone
enforce_capusesblocked_atas the last-seen proxy to avoid a schema migration (documented in-code); if preciselast_seeneviction is ever required it'd need a new column.phpcs/Plugin Check binary isn't installed in this environment; validation was manual against the reviewed patterns.