Permanent guardrails#
These are doctrine, not per-task decisions. They bind every session and every tool, so the posture cannot drift. They are written as what not to do, because on a WordPress plugin the common compliance reflexes are the dangerous ones. They are codified verbatim in the project's own instruction file.
| Guardrail | The rule |
|---|---|
| Compliance layering | No server-level configuration (strict CSP, TLS headers) injected from plugin code; it breaks core and Gutenberg and is the wrong layer. No .env in a WordPress plugin; configuration is wp-config.php, options, or approved constants. WordPress core owns password hashing; a plugin never shadows the password store. |
| Secure by design | Every change keeps static analysis at its strictest level with zero errors and conforms to the WordPress-Extra lint ruleset. Sanitisation and nonce plus capability checks are mandatory at every entry point. No tree-wide automated fixes; compliance is verified surgically, per unit. No hardcoded secrets. |
| Honest documentation | Every GDPR, CCPA, CRA, or accessibility claim is defensible, source-cited, and mapped to a real status. Fabricated or generalised compliance statements are never written. The ecosystem compliance programme is Core-owned and counsel-gated; status is never asserted publicly before counsel validation. |
Compliance status#
"Partial" means a real baseline is enforced but a formal end-to-end audit has not been done. "Not assessed" means no claim is made yet. Nothing here is marked fully met, because nothing has been independently audited.
| Standard | Status | Basis |
|---|---|---|
| OWASP Top 10 | Partial | PHPStan at level 9 with zero new errors (the legacy mixed-type debt is isolated in a baseline; new code stays clean); WordPress-Extra plus PHPCompatibility lint; Plugin Check zero-errors is held as a release-gate invariant. Nonce plus a five-capability RBAC system, self-healed on every admin request, plus escaping and input-sanitisation discipline at every entry point. No third-party penetration test. |
| GDPR | Partial | Strong baseline: a click's IP is stored only as a one-way SHA-256 hash salted with the WordPress auth salt, never raw; per-field privacy toggles (IP, user agent, referer, country, sub-ID, variant, bot); configurable retention (30/90/180/365 days or forever) with monthly aggregation before deletion so totals survive without the row; country resolved from the Cloudflare header or a bundled GeoLite2 file, not raw-IP storage; a WordPress Privacy API exporter and eraser ship in the Privacy module. Not yet formally assessed: an end-to-end data-flow and retention review across the click log, the Google/GSC caches, and the Rank Tracker history. |
| CCPA / CPRA | Not assessed | Node is commercial software; selling the plugin is not what this right concerns. "Do Not Sell/Share" is about selling or sharing a site visitor's personal information, which Node does not do. A consumer opt-out surface is therefore likely not applicable, but this has not been formally confirmed and written down. |
| CRA | Partial | Phase 2 gap audit complete 2026-05-20 (see §Annex I gap audit): per-clause status with cited mechanisms is now public. Most rows are partial because the cite-your-mechanism discipline does not allow status inflation where a formal per-unit audit has not been done, Node's partial rows carry noticeably richer citations than Cairnstone's (per-field privacy toggles, SHA-256 IP hashing, RBAC system, asynchronous logging, encrypted service-account store) but the same discipline keeps the status honest. CycloneDX 1.5 SBOM landed (build/sbom-2.91.7.cdx.json; naturally excluded from the deploy payload by the GHA runtime allow-list). Phase 3, declared support period, EU DoC, CE marking, Annex VII technical documentation, is gated to product launch. |
| EAA / WCAG 2.2 AA | Partial | AA is the stated bar. The admin UI ships a non-negotiable light-and-dark theme system with paired contrast, focus rings, and hover/disabled states; tooltips are viewport-clamped so they never disappear off-screen. Honest open item: the interactive D3 SVG Content Graph is a hard AA surface; there is no full third-party WCAG audit and per-surface formal testing is incomplete. |
Data handling#
- Click log. Logging is asynchronous (scheduled, off the redirect path). The visitor IP is written only as a salted one-way SHA-256 hash; user agent, referer, country, sub-ID, variant, and bot flag each have an independent privacy toggle. Retention is configurable with monthly aggregation before deletion.
- Country resolution. Cloudflare header first, then a bundled GeoLite2 database; raw IP retention is not part of the documented design.
- Google data. Search Console and GA4 metrics are cached in schema-versioned custom tables; the service-account JSON is held in an encrypted on-disk store. The Rank Tracker is passive, it reads only the cached GSC data, with no third-party SERP API and no per-query billing.
- No phone-home. No activation telemetry and no anonymous usage statistics unless the site owner actively turns them on.
- Documentation site. This site self-hosts its webfont for GDPR; there is no third-party font CDN call.
Security measures#
- Static analysis at level 9, zero new errors after every change; new errors are regressions, not new debt.
- Lint: the WordPress-Extra ruleset plus PHPCompatibility for the supported PHP floor; new violations are not added to touched files.
- Input: sanitisation plus nonce plus a five-capability RBAC system (self-healed on every admin request) at every entry point.
- Output: every output goes through the appropriate escaping helper; Plugin Check zero-errors is a release-gate invariant that catches unescaped output.
- Headers and TLS: deliberately not handled by the plugin. This is correct, not a gap; the plugin layer is the wrong place for site-wide security headers.
- Secrets: none hardcoded in the plugin codebase.
Accessibility#
Conformance target is WCAG 2.2 AA. The admin surface ships a non-negotiable light-and-dark theme system: every rule has a paired counterpart, and contrast, focus rings, hover, and disabled states are required to read correctly in both modes before a surface is called done. Tooltips are clamped to the visible viewport so they never vanish under the admin menu or off the edge. Verification is by exercising the real admin surface in a browser, since a passing test does not prove a surface is usable. Open: the interactive D3 SVG Content Graph is a known hard AA surface; there is no full third-party audit and per-surface formal testing is incomplete.
Dependencies and SBOM#
- Minimal runtime surface. The bundled third-party runtime code is a small vendored set, the D3 v7 graph renderer, Chart.js, Select2, a QR-code library, and a GeoLite2 database reader, each GPLv2-or-later compatible. The entire development toolchain (test runner, static analyser, linter, Composer) is build-excluded and never ships.
- License audit. Every shipped third-party asset must be GPLv2-or-later compatible; new dependencies are checked before shipping.
- SBOM: implemented (Phase 2, 2026-05-20). CycloneDX 1.5 JSON at
build/sbom-2.91.7.cdx.json; lists the PHP platform, the vendored MaxMind DB Reader (Apache-2.0), the vendored frontend libraries (D3 v7, Chart.js, Select2), and the bundled GeoLite2 database. Regenerates on version bump; naturally excluded from the GHA deploy payload by the runtime allow-list. An automated dependency-vulnerability audit / scanning pipeline remains pending; tracked under "Not yet assessed".
Annex I gap audit (Phase 2)#
Phase 2 of the Core-owned CRA program: per-clause mapping of every Annex I requirement to Node's concrete mechanism, with a mandatory citation per row. Completed 2026-05-20 against the clause structure verified the same day from the consolidated OJ text (Gillish Core plans/CRA-PHASE2-PLAN.md §3.1). The status ladder is met / partial / not applicable / not assessed. No row may read met without an exercisable cited mechanism, partial rows reflect that a real baseline exists but a formal per-unit audit has not been done, which is the cite-your-source discipline the project enforces, not a defect. Node's partial rows carry noticeably richer citations than Cairnstone's did (per-field privacy toggles, SHA-256 IP hashing, RBAC system, asynchronous logging, encrypted service-account store), the same status, stronger basis.
Annex I Part I: cybersecurity requirements relating to product properties
| Clause | Status | Concrete mechanism (cited) |
|---|---|---|
| (1) Umbrella, appropriate cybersecurity based on risks | Partial | Permanent guardrails (§Permanent guardrails above) bind every change; build gates (composer test / stan / lint, Plugin Check release gate) are enforced. No formal documented risk assessment per CRA Art. 13 yet, Phase 3 deliverable. |
| (2)(a) No known exploitable vulnerabilities | Partial | PHPStan level 9 with zero new errors (legacy mixed-type debt isolated in phpstan-baseline.neon; new code stays clean); WordPress-Extra phpcs + PHPCompatibility; Plugin Check zero-errors release-gate invariant. No third-party penetration test; no CVE recorded against Node. |
| (2)(b) Secure-by-default + resettable | Partial | Safe defaults are wp.org-gate-mandated (Plugin Check rejects unsafe defaults); resettable state via uninstall.php, destructive cleanup is gated on the Settings → Link Management → Uninstall → "Keep data when the plugin is deleted" toggle being OFF (drops plugin-owned tables, removes gillish_node_ options + KNOWN_META_PREFIX post-meta, force-deletes every gillish_node CPT row, plugin transients). No per-module formal review of default values conducted. |
| (2)(c) Vulnerabilities addressable via security updates | Met | WordPress core's signed update channel (readme.txt Stable tag mechanism, wp.org distribution); studio's controlled Pro update path uses the same transport guarantees. |
| (2)(d) Protection from unauthorised access | Partial | Explicit five-capability RBAC system, self-healed on every admin request; nonces on every write path; capability-gated REST endpoints (src/Rest/Controllers/{Analytics,Links,Track}Controller.php registered via src/Rest/RestApi.php); single grep-able hook registry (src/Plugin.php, 53 registrations). No per-controller formal audit signed off, partial rather than met reflects the strict cite-your-mechanism discipline, not a defect. |
| (2)(e) Confidentiality of stored / transmitted / processed data | Partial | Visitor IP stored only as one-way SHA-256 hash salted with wp_salt('auth'), never raw; per-field privacy toggles for user agent, referer, country, sub-ID, variant, bot flag; Google service-account JSON held in an encrypted on-disk store; country resolved via Cloudflare header first, with the GeoLite2 fallback file read locally (no external network call). Open: end-to-end data-flow review across the click log + GSC caches + Rank Tracker history. |
| (2)(f) Integrity of data, commands, programs, configuration | Partial | declare(strict_types=1) in every PHP file; nonce verification on every write; outputs escaped via the appropriate esc_* helper at the boundary (Plugin Check zero-errors release gate catches unescaped output). No per-call-site formal audit. |
| (2)(g) Data minimisation | Partial | Per-field privacy toggles disable individual click-log fields; configurable retention (30 / 90 / 180 / 365 days, or forever) with monthly aggregation before deletion so visible totals survive without the per-row record; no raw IP retained (SHA-256 hash only); no telemetry without explicit opt-in. Open: formal end-to-end review (same item as (2)(e)). |
| (2)(h) Availability + DoS mitigation | Not applicable | Node is a WordPress plugin; availability sits in the site owner's hosting / WP-core layer. Click logging is asynchronous (scheduled, off the redirect path) so traffic spikes do not slow the redirect itself, but DoS resilience is the host's responsibility. No new DoS vectors introduced. |
| (2)(i) Minimise negative impact on other services | Met | Click logging is asynchronous and off the redirect path (scheduled), so click-traffic does not slow visitor-facing pages. GeoLite2 lookup runs locally against the bundled database file; no external HTTP request at click time. No phone-home telemetry. Node's runtime is process-local on the WP host. |
| (2)(j) Limit attack surfaces, incl. external interfaces | Partial | Single grep-able hook registry in src/Plugin.php (53 hook registrations from one file); REST surface is three named controllers + one umbrella REST API class, all in src/Rest/; admin pages, AJAX handlers, and REST endpoints all funnel through capability- and nonce-gated entry points. No formal external-interface inventory written down yet, Phase 3 deliverable. |
| (2)(k) Reduce impact of incidents (exploitation mitigation) | Partial | declare(strict_types=1) per file; PHPStan level 9 catches whole classes of latent type-confusion bugs; WordPress / host layer carries process-level mitigations. No formal exploit-mitigation features beyond established WP best-practice. |
| (2)(l) Security-related information recording / monitoring | Partial | The click-log table records redirect events (functional / analytics), useful for incident forensics but not a dedicated security audit log; authentication-event logging is inherited from WordPress core. Node does not implement an additional security-event log. |
| (2)(m) Secure permanent removal of data and settings | Partial | uninstall.php provides full destructive cleanup but is opt-in: gated on Settings → Link Management → Uninstall → "Keep data when the plugin is deleted" being OFF, because the default is to preserve months of click history across accidental deactivate-delete. The user can securely remove on a permanent basis; clearer UI surfacing of the opt-in would close this row to met. |
Annex I Part II: vulnerability-handling requirements
| Clause | Status | Concrete mechanism (cited) |
|---|---|---|
| (1) SBOM in a commonly used machine-readable format | Met (this commit) | CycloneDX 1.5 JSON at build/sbom-2.91.7.cdx.json; lists PHP platform, MaxMind DB Reader for PHP (Apache-2.0), and vendored frontend libraries (D3 v7, Chart.js, Select2) + bundled GeoLite2 data file; naturally excluded from the deploy payload via the GHA runtime allow-list; regenerates on version bump. |
| (2) Address vulnerabilities without delay | Met | Ecosystem CVD intake operational; security@gillish.com watched via Thunderbird IMAP (2026-05-19); standard wp.org update channel for rapid distribution. |
| (3) Effective and regular tests and reviews | Partial | Two PHPUnit testsuites (composer test unit + composer test-wp WordPress integration); PHPStan level 9 with isolated legacy baseline; WordPress-Extra phpcs + PHPCompatibility; Plugin Check release gate; runs after every PHP edit batch. Automated dependency-CVE audit, in-session pre-push gate: the agent runs composer audit --format=plain against the resolved lockfile before every git push origin main on Core / Cairnstone / Node / MCP; an open Packagist advisory holds the push until resolved. The gate moved from a GitHub-Actions workflow to an in-session pre-push step on 2026-05-24 alongside the broader auto-deploy retirement; same zero-tolerance threshold throughout. Feed and Mesh inherit when their toolchains stand up. No formal regular cadence for human-driven code review beyond per-change. |
| (4) Public disclosure of fixed vulnerabilities | Partial | CVD policy commits to structured public advisories (readme.txt changelog + docs site); format committed, channel live. No vulnerabilities have been fixed and disclosed yet, first execution awaits the first real report. |
| (5) Coordinated vulnerability disclosure policy | Met | Live at html.gillish.com/mesh/security/#cvd; canonical source gillish-core/plans/CRA-CVD-POLICY.md; security.txt published at /.well-known/security.txt (RFC 9116). |
| (6) Facilitate sharing of vulnerability information | Met | Monitored security@gillish.com mailbox + security.txt Contact line + the public CVD policy, three discoverable paths to a working contact. |
| (7) Secure update distribution | Met | WordPress core's signed update channel (wp.org Stable tag); Pro tier via the studio's controlled update path with the same transport guarantees. |
| (8) Free + timely security updates within declared support period | Partial, declared support period set in Phase 3 | CVD policy commits to free security updates within the support period; the support-period number itself is a Phase 3 deliverable. Proposed default 5 years from a version's release. Row moves to met when the support period lands in readme.txt and is declared in the EU Declaration of Conformity. |
Summary. Part I, 14 clauses: 2 met, 11 partial, 1 not applicable. Part II, 8 clauses: 5 met, 3 partial. Same numerical pattern as Cairnstone (the doctrine is the doctrine); Node's partial rows carry noticeably richer citations because Node has more documented data-handling mechanisms. The partial rows concentrate on items requiring formal per-unit audits or Phase 3 deliverables (declared support period, formal risk assessment, exploitation-mitigation statement), scheduled, not gaps in conduct.
Not yet assessed#
The honest gaps, listed so a reviewer can see exactly what is open:
- End-to-end data-flow and retention review across the click log, the Google/GSC caches, and the Rank Tracker history, for data minimisation.
- CCPA / CPRA applicability determination, written and signed off.
- An automated dependency-vulnerability audit / scanning pipeline. SBOM generation landed in Phase 2 (2026-05-20); the automated-audit side remains pending.
- Full third-party WCAG 2.2 AA audit across every surface, including the interactive Content Graph.
- A formal secure-development-lifecycle statement for CRA beyond the build gates already in place.
- Counsel validation of the manufacturer and scope determinations, which the Core-owned CRA programme treats as mandatory before any launch reliance.
Input on any of this goes back to the maintainer directly. The honest gap a reviewer can name is the most valuable thing on this page.