Skip to content

feat: network actor blocking, intel column, post-CRITICAL moment, Upgrade wiring - #46

Merged
cport1 merged 4 commits into
mainfrom
feat/p1-actor-intel
Jul 23, 2026
Merged

feat: network actor blocking, intel column, post-CRITICAL moment, Upgrade wiring#46
cport1 merged 4 commits into
mainfrom
feat/p1-actor-intel

Conversation

@cport1

@cport1 cport1 commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

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

State Actor feed Intel column Post-CRITICAL notice Enrichment
Unconnected cron never scheduled → no request static lock glyph + title, no request static "Connect" copy, no request no request
Connected, Free cron not scheduled (needs features.actor_feed) one batched, 1h-cached POST /sdk/detections/intel per render for uncached IPs; redacted teaser intel lookup for the one IP (deferred to admin render) gated off when synced entitlements say features.enrichment false
Connected, Pro+ hourly GET /sdk/actor-feed?since= while entitled same batched call, full fields same, with the upgrade/confirmation copy allowed when entitled

The feed handler re-checks connected and entitled on every run and unschedules itself otherwise; a 403 upgrade_required unschedules quietly. Queueing the CRITICAL moment writes only local state — the personalising intel lookup happens later, in the connected admin context.

Work items

  1. Actor feed + network blockingincludes/class-webdecoy-actor-feed.php. Hourly cron, cursor persisted in an option, pagination via next_since/complete. Inserts through the existing blocked-IPs table as created_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.
  2. Actor intel column — new column between MITRE Tactic and Source in detections-page.php, backed by includes/class-webdecoy-actor-intel.php (batched, per-IP 1h cache, redaction-aware, em-dashes on failure).
  3. Post-CRITICAL momentincludes/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.
  4. Upgrade wiring + enrichment gateWebDecoy_Cloud_Connect::wp_upgrade_url() for the billing/wordpress?organization_id=..&plan=wp_pro link; WebDecoy_IP_Enrichment gated behind features.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

  • Rows this feature owns carry 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.
  • Before inserting, each IP is run through WebDecoy_Blocker::is_allowlisted() (exact + CIDR), so an allowlisted IP is never network-blocked.
  • Refresh sets blocked_at/expires_at for live feed IPs; the existing hourly cleanup_expired cron reaps them 48h after they last appeared. Cap enforcement deletes only network rows, oldest blocked_at first (blocked_at proxies last-seen since it's refreshed each sync).

Throttle design

WebDecoy_Critical_Moment stores the last-queued epoch in the webdecoy_critical_moment_last option; the pure should_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) with is-dismissible.

Tests / checks

  • php tests/run.php66 passed, 0 failed (added coverage for feed pagination/cap/allowlist-exclusion, intel normalization/redaction, and the notice-throttle/variant logic).
  • php -l clean on every touched file. Escaping/sanitize/unslash/nonce + phpcs suppressions follow the existing codebase style (class-webdecoy-blocker.php file-level DB disables); no new superglobal reads.

Notes / left undone

  • Among the deception paths, the canary-credential replay is the CRITICAL-severity insert that fires the moment; honeytoken/decoy-served record HIGH and intentionally don't trip the higher-bar CRITICAL moment (the moment also fires from any other insert recorded at CRITICAL, e.g. a client score ≥75).
  • enforce_cap uses blocked_at as the last-seen proxy to avoid a schema migration (documented in-code); if precise last_seen eviction is ever required it'd need a new column.
  • The feed's per-IP existence check is one query per IP inside the hourly cron (off the request path) — fine at the 2000-row cap; could be bulk-loaded later if needed.
  • phpcs/Plugin Check binary isn't installed in this environment; validation was manual against the reviewed patterns.

cport1 added 4 commits July 23, 2026 09:59
- 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.
@cport1
cport1 merged commit ad331bb into main Jul 23, 2026
3 checks passed
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.

Network actor blocking, intel column, post-CRITICAL moment, Upgrade wiring

1 participant