All plugins

Dragon Checkout Guard

Payment-page script inventory, authorisation record, weekly tamper check and header baseline: PCI DSS 6.4.3 / 11.6.1 and SAQ A evidence. No account.

Payment-page script inventory, authorisation record, weekly tamper check with email alerts and header baseline: evidence for your PCI DSS 6.4.3 / 11.6.1 assessment and your SAQ A eligibility. No account, no telemetry to Dragon Core; the only requests it makes to anywhere but your own site (scans also fetch your own payment pages) are downloads of scripts your own payment page already loads from another host, to keep their content hashes current.

Why this exists

Nearly every WooCommerce store embeds its gateway's card fields in an iframe: Stripe, WooPayments, PayPal, Braintree or Adyen. Since 31 March 2025, SAQ A carries an eligibility criterion that the merchant has confirmed its site is not susceptible to attacks from scripts. PCI SSC FAQ 1588 (February 2025) applies that criterion to merchants embedding a provider's payment form and accepts two confirmations:

  1. techniques such as those in requirements 6.4.3 and 11.6.1, applied by the merchant to its own page, or
  2. written confirmation from the PCI DSS validated provider that its embedded form includes protections against script attacks when implemented to the provider's instructions.

Not every provider issues one, which leaves route 1. The iframe belongs to the provider; the page it sits in is yours, and a script injected into that page is what e-skimming (the Magecart style of attack) uses to read a shopper's details before the iframe ever sees them. Dragon Checkout Guard is the record of what runs on that page: an inventory, a justification per script, a content hash rechecked weekly, and a response-header baseline.

If your own page's scripts can touch card data, you are on SAQ A-EP or SAQ D and 6.4.3 and 11.6.1 apply in full: the same records are what those requirements ask for. If your customer is redirected to the provider's own page to pay, the criterion does not apply to you, though assessors still commonly expect the page that starts the payment to be monitored.

Claim boundary

Dragon Checkout Guard produces evidence supporting your assessment. It never claims your site "is compliant" or "is certified", it never claims a script's integrity "is assured" (only that an integrity observation was recorded, by the method you chose), and it does not determine which SAQ you complete. Your acquirer, QSA or internal assessor settles that; this plugin gives them something concrete to review.

It does not look inside the gateway's iframe, and is not meant to: that document is served by the provider from its own origin and is covered by the provider's own PCI DSS validation.

The inventory covers the scripts a page loads through <script> elements (external and inline, including those inside <template> content), <link rel="modulepreload"> and import maps. JavaScript in event-handler attributes (onclick, onerror and the like) and the documents in <iframe srcdoc> are not inventoried.

Requirements

WordPress 6.5+, PHP 8.0+. WooCommerce is optional but required for the built-in checkout / cart / order-pay / add-payment-method page keys; without it, only custom paths are in scope. No account, no third-party API, no telemetry to Dragon Core. The only outbound request it makes to anywhere but your own site (scans also fetch your own payment pages) keeps a script's content hash current: it downloads a script your payment page already loads from another host (Stripe.js, a gateway SDK, Google Tag Manager). A script on your own site is read from disk instead. That request is a plain HTTPS GET of a public URL and carries no data about your site, its configuration or its visitors. See "Data and privacy" below. Every screen, email and notice is translatable; community translations come from translate.wordpress.org.

Getting started

  1. Activate the plugin and open Tools > Checkout Guard.
  2. On the Pages tab, confirm which pages are in scope. Checkout, Order Pay and Add Payment Method are watched by default; Cart is off by default (enable it on the Settings tab). Add custom paths on the Settings tab for anything else, for example a non-Woo payment page.
  3. Populate an initial inventory either way:
    • Scan now loops back over every in-scope page server-side and records what it finds. This is the fastest way to cover every page at once, and what the weekly cron does automatically. The loopback request is always anonymous. WooCommerce redirects an empty-cart Checkout to the cart, so the scan's own Checkout request gets a cart holding one product, in that request's memory only: the lowest-id published, purchasable, in-stock simple product priced above zero among the first 100 simple products, or the product you choose with the dragoncheckoutguard_scan_cart_product filter (return 0 to turn this off). No WooCommerce session row, cookie, persistent cart, order or stock reservation is created, and no add-to-cart event fires. The request does still run WooCommerce's cart-total and checkout-page hooks, so a server-side analytics, conversion or abandoned-cart tool that records checkout views from those hooks may count one visit per scan (weekly by default) with the DragonCheckoutGuard/ user agent; filter that user agent out there. Browser analytics do not see it. Only a request carrying the scan's own single-use token gets the cart: the token is random, bound to the Checkout page, valid for 60 seconds and spent on first use, so an ordinary visitor, a forged token or a replayed one is redirected as usual. Checkout then renders the way it does for a shopper with an item in the cart, payment gateway scripts included, and the scan records it as read; the scripts it records, and their fingerprints, match a shopper's with a similar cart: inline script fingerprints ignore numbers and plain text such as prices, quantities and item names. An inline block whose structure depends on the cart (one entry per cart line, or a true/false flag such as whether the cart is virtual) fingerprints the same on every scan, since the scan always uses the same one-item cart, but can differ from shoppers whose carts have a different shape, as it already does between shoppers. If there is no suitable product, the scan is still redirected, records the page as redirected to the cart and says why. Order Pay is skipped by every scan, since it only exists for a real order. Add Payment Method needs a signed-in customer, so WooCommerce answers the logged-out scan with its login form: the scan recognises WooCommerce's login form and records the page as not read (it needs a signed-in customer) rather than inventorying the login page. That is not an error. Record its scripts with Open & capture while logged in. On a stock WooCommerce store with the default scope and at least one product for sale, a scan therefore reads Checkout and the run counts as a complete check: Order Pay (skipped) and Add Payment Method (needs a signed-in customer) are listed as not read, but neither makes the run Incomplete. Open & capture records those pages' scripts in the inventory, but it is not a scan and adds no row to the check record. Any page a scan could not cover is called out under the Scan now button on the Inventory tab and on that page's card on the Pages tab. The check record says the same: each run lists every page as read, skipped, needing a signed-in customer, redirected to the cart or not read, with the number of script hashes it verified, and a run that could not read a page it should have read is marked Incomplete rather than No change (Order Pay and a page needing a signed-in customer do not count against it). A run that read no page at all is not counted as a check by Site Health, so a failed scan never looks like a clean one.
    • Open & capture (per page, on the Pages tab) mints a single-use token and sends your own browser to that page, so the capture also sees scripts a tag manager injects at runtime, which a server-side loopback cannot observe. Order Pay has no "Open & capture" button: without a specific order id there is nothing to loop back to. Checkout needs an item in your own account's cart first, and the screen says so: WooCommerce redirects an empty-cart checkout page away before your capture ever runs. Add Payment Method has no such precondition; it is a My Account endpoint, not cart-dependent, and just needs you logged in with a gateway that supports saved payment methods. Because it runs in your logged-in browser, the page carries the admin bar; its scripts (and any dependency only the admin bar uses) are left out of the capture, since no shopper is ever sent them. For the same reason a passive pageview that shows the admin bar is not sampled at all.
  4. Review each row on the Inventory tab and authorise the scripts that belong there.
  5. Answer the three questions on the Assessment guide tab, so the printable report carries the context an assessor needs.

The tabs

  • Inventory: every script seen on an in-scope page, filterable by status (new, authorised, ignored, changed) plus unconfirmed browser observations, with the export buttons and Scan now.
  • Pages: every page key, whether it is enabled, its URL, when it was last covered, and the per-page Open & capture button.
  • Headers: the response-header baseline per page and any drift.
  • Assessment guide: the SAQ A route self-check.
  • Settings: scope, email alerts, capture, collector token, retention, redacted parameters, uninstall behaviour.

Authorising a script

Each script gets:

  • Justification (required, 5+ characters): the evidence a reviewer actually reads.
  • Owner: who inside the business is accountable for it.
  • Provider: the third party serving it, if any.
  • Business purpose: why it is there.
  • Review due: a date to revisit the authorisation.
  • Integrity method: how you are monitoring this script for tampering.
    • Subresource Integrity (SRI): the script tag itself carries an integrity attribute the browser verifies.
    • Hash monitored by this plugin: records that you are relying on Dragon Checkout Guard's own content-hash comparison as this script's integrity control. The hashing is not gated on this setting (every new or authorised remote script is hashed on the same schedule regardless), but selecting this method is your statement, for the assessment, that a hash change is what you are counting on to catch tampering, rather than SRI or a vendor's own controls.
    • Vendor-managed integrity: the provider, for example a payment processor, is contractually responsible for the script's integrity.
    • Not feasible, justified: record why in Justification, for example a script outside your control that cannot be hashed or SRI-pinned.
    • None yet: the default for a newly discovered script, and not a valid end state for your assessment.

Revoking a script's authorisation puts it back to new so it surfaces for re-review.

Scripts are found by reading each page the way a browser does. On PHP 8.4 and later that uses PHP's own HTML5 parser, so a script counts wherever a browser would run it: inside <template>, under an SVG <desc> or <foreignObject>, after markup that closes a <noscript> or <title> early, with an HTML5 character reference such as &period; in its src, or with any JavaScript MIME type the HTML standard lists (legacy ones such as text/javascript1.5 and application/x-javascript included). A <base href> is honoured where a browser honours it, including inside <foreignObject> or a MathML <mi>. On PHP 8.0 to 8.3 the older libxml parser is used, and a few deliberately malformed constructs (an end tag hidden in an attribute of <title>, <noscript> or <textarea>, the <!--> comment, <script/src=...>) can hide a script from it, and a <base href> can be placed wrongly (after an <svg><p> break-out, or behind a </noscript> in an attribute); run PHP 8.4 or later for full coverage.

Inline blocks printed by WordPress are identified by handle and position, so a change to one is recorded as drift on the same row rather than appearing as a new script on every scan. A change counts when it alters the block's code, the host of a URL it names (an endpoint or a script it loads), the rest of that URL, a relative path to a .js or .mjs file, a string containing HTML markup (<) or // (such as the markup document.write() or .html() inserts), or a template literal. Numbers, long token-like values (nonces, order keys, hashes, including those inside a URL's path or query) and other string values such as tracking ids and labels are ignored, so a rotating nonce never registers as drift; an inline block with no handle is identified by the same fingerprint, so changing one of those values does not create a new row either.

After updating to 1.0.7, inline blocks that name a URL or a script path, contain markup or //, or contain a template literal are fingerprinted afresh, because the earlier fingerprint ignored every string. A block printed for a WordPress handle keeps its row: an authorised or ignored one is flagged once for re-review (its history says why), an unreviewed one is updated quietly. A block with no handle is identified by its fingerprint, so it appears once as a new, unreviewed row and its earlier row stops being seen; review the new row, after which the earlier one can be marked ignored. Inline previews are stored with sensitive values redacted.

Recognised providers

When a script matches a known provider, the authorise form offers Use suggested justification, which prefills provider, business purpose, justification and integrity method.

A script is matched by the host it is served from, by the plugin folder directly under your site's plugins directory that it is served from (on your site's own host and port), or by the handle WordPress itself registered it under, as recorded by the server-side capture of WordPress's script registry. That includes the inline data WordPress prints for a registered handle. A handle that only appears in the page's markup (an element id such as stripe-js-extra, or an importmap name) or in a shopper's browser is never used to suggest a provider, since anything injected into the page could claim one.

ProviderCategory
Stripe, PayPal, Braintree, Adyen, Klarna, Mollie, Square, Google Pay, Apple Pay, WooPaymentsPayments
Stripe Radar, ThreatMetrixFraud
WooCommerceWordPress
Google Tag ManagerTag manager
Google Analytics, Meta Pixel, HotjarAnalytics
Intercom, Zendesk, LiveChatChat

Anything outside the payment and WordPress categories carries a reviewer note on its row: a tag manager or pixel can pull further scripts onto a payment page, a chat widget is third-party code on a page you are attesting for, and session recording on a payment page records keystrokes unless it is masked or excluded. Unrecognised scripts are listed plainly for you to justify or remove.

The plugin's own collector script is recorded as authorised automatically, so it never sits in your queue as an unexplained script.

Headers tab

Every in-scope page's security-relevant response headers (Content-Security-Policy, X-Frame-Options, Strict-Transport-Security, Referrer-Policy, Permissions-Policy, the Cross-Origin trio, X-Content-Type-Options, Cache-Control and the CSP reporting headers) are captured, compared to an accepted baseline, and flagged when they drift. Accept a new baseline once you have reviewed a change; changes feed Dragon Activity Log if it is active.

Headers are read from each scan's own ordinary, logged-out request for the page - the response a shopper's browser receives - so headers your web server, proxy or CDN adds (Strict-Transport-Security and Content-Security-Policy often are) are included, and a header that stops being sent is flagged as removed. A page therefore has a baseline from its first scan, before any shopper arrives. A few details:

  • The scan makes a second request for this, without its capture token: the capture request sets cache-control and referrer-policy for itself, so its own response is never used for headers. For Checkout the second request carries a separate single-use token that arms nothing but the in-memory cart, so WooCommerce serves the page rather than redirecting it. Once a scan reads Checkout this way for the first time, headers your web server or CDN adds may show as changed against a baseline that came only from shoppers' pageviews: review them and accept the baseline. If that second request fails, the scan reports the page as failed rather than passing over the missing header evidence.
  • A shopper's pageview and Open & capture record scripts only, not headers: WordPress can only see the headers PHP set, not the ones added after it, so they would disagree with the scan about every server-set header.
  • A header that appears after a page's baseline was recorded is flagged as added, the same as one that changes or disappears; accept the baseline once you have reviewed it. (A page first baselined by a version before 1.0.7, whose scans left out cache-control and referrer-policy, adopts those two quietly on its first scan after the upgrade, once.)
  • A page the last scan could not reach - Order Pay, which needs a real order, or a Checkout that WooCommerce redirected to the cart because the scan found no product to put in its in-memory cart - gets its headers from logged-out shoppers' pageviews instead. Those only see the headers PHP set: a header that appears or changes there is flagged, but a header missing from the pageview is never reported as removed. Once a scan reaches the page again, the scan is the source. A logged-in pageview is never used for headers, because WordPress sends logged-in responses no-store, which is not what shoppers receive.
  • Add Payment Method answers every logged-out request, a scan's or a shopper's, with WooCommerce's login form. Nothing from that response is recorded: no scripts, inline blocks or headers. Its scripts come from Open & capture (or a signed-in customer's pageview), and it has no header baseline. Header rows earlier versions recorded for it (all from that login form) are removed on upgrade, and the removal, with the values removed, is recorded in the event log. The same applies to Checkout on a store that requires an account to buy (guest checkout off and no account creation at checkout): a logged-out shopper's view, or a scan that gets the login prompt, records nothing, and the scan lists Checkout as needing a signed-in customer.

A page you take out of scope keeps its header history, but its flags stop counting towards the outstanding-headers total on this tab, in Site Health and in the evidence record, since the tab no longer lists it for you to accept.

If a full-page cache serves your payment pages, the headers being compared may be the cache's own rather than your origin server's. Bypass the cache, or exclude payment URLs from full-page caching, for a meaningful headers comparison.

Browser collector and page caches

The optional browser collector (Settings tab, on by default) reports scripts that only exist once a real shopper's page is running: the ones a tag manager injects at runtime, which never appear in the server-rendered HTML a loopback scan can see. Reports land as observations you confirm before they join the inventory; nothing is added automatically, and an unconfirmed observation is never integrity-checked. Dismiss them one at a time, or use Dismiss all on the Inventory tab, or wp dragon-checkout-guard dismiss.

The report endpoint is guarded by a per-install token printed in the page source of in-scope pages, not by a nonce (a nonce means nothing for an anonymous visitor). Rotating the token (Settings, tick "Issue a new token when saving") invalidates the old one immediately, but if a full-page cache is serving your payment pages, visitors keep receiving cached HTML with the old token baked in until the cache is cleared, and their reports are rejected in the meantime. Clear your page cache immediately after rotating.

The collector enforces its limits before anything is written: see "Limits" below. If the hourly or total observation limit is reached, the collector pauses for an hour and an admin notice tells you, with a collector.tripped entry in the event log (and in Dragon Activity Log, if active).

A total limit that has been reached would otherwise keep the collector shut off until the rows aged out 30 days later, so the daily maintenance clears a flood on its own: once the table holds 4000 rows in total it deletes the most recent unconfirmed observations, up to the number needed to bring the table down to 2500, and records how many it took. Confirmed observations are never touched, so a table held up by confirmed rows is left as it is. Nothing genuine is lost - a script that really runs on the page is reported again by the next shopper's browser.

Scan scheduling

A full scan runs automatically once a week via WP-Cron, and can be triggered on demand from the admin screen ("Scan now") or WP-CLI. On-demand scans are scheduled as a single cron event rather than run inline, so the request that clicks the button returns immediately; an atomic lock prevents the weekly cron, the on-demand trigger and WP-CLI overlapping.

Every scan rehashes every pending script, with no per-run row cap, and records an evidence event describing the run. A partial scan (one where some page could not be covered) merges into the same summary rather than replacing it, and a detected change whose database write failed is reported as a failed scan rather than a clean one.

Email alerts

PCI DSS 11.6.1 asks for a mechanism that alerts personnel to unauthorised changes to payment page scripts and security-impacting headers. Dragon Checkout Guard sends that alert by email.

  • When: after every completed check (the weekly scan, Scan now, or wp dragon-checkout-guard scan) that finds something since the previous check. A check that finds nothing sends nothing. Alerts follow the checks, so how soon you hear about a change depends on how often checks run: weekly at minimum, and Scan now runs one at any time.
  • What it lists: new scripts (with the page they were found on), scripts whose content changed (with the same plain-language reason the script's history shows, for example that the file's contents differ from the version you authorised on a given date), scripts that the last two checks did not see on a page they both read in full although an earlier check saw them there, and pages whose monitored response headers differ from the accepted baseline (added, removed and changed header names). Each section lists up to 25 entries and counts the rest. A script a shopper's pageview recorded between scans is in the next check's alert, including one recorded after installing and before the first check. The first check's email says it is the first check and lists the scripts recorded on the payment pages so far, for you to review and authorise. Inline fingerprints flagged only by the 1.0.7 format upgrade are not listed.
  • Who gets it: on by default, sent to the site administration email address (Settings, General). List your own recipients under Settings > Email alerts, one per line or separated by commas, up to 20; an entry that is not an email address, or a list containing < or >, is refused and nothing is saved. Each recipient gets a separate email, so no one sees the others' addresses. Untick Change alerts to stop them.
  • Delivery: through WordPress's own wp_mail(), so it uses your site's mail setup (your host's mail service or an SMTP plugin). Use Send test alert on the Settings tab to check delivery. A send that does not reach every recipient is shown at the top of the plugin's screen, with the reason WordPress reported (a mail plugin that throws an error counts as a failed send, with its message), until a later check has nothing to report, a later alert reaches every recipient, or a test alert does. The Settings tab shows the last alert's outcome and when the warning was cleared.
  • Evidence: every check record (the 11.6.1 CSV, the printable record and the WP-CLI export) has an alert column: sent, partial (some recipients only), failed, none (nothing to report), off (alerts switched off), no_recipients, or addon (the separate add-on was active, so alerts were left to it). A record written before 1.1.0 has it empty. Each delivered or failed alert is also kept in the event log as alert_sent or alert_failed, never pruned.
  • Content: script names and addresses as the inventory shows them (sensitive query values redacted) and response-header names, never anything about customers or orders. Nothing from a payment page goes into the subject line, and line breaks in page-supplied text are removed.

Removed scripts are judged only from the checks' own page loads, never from shoppers' pageviews, and a script is reported as removed only after two checks in a row read the page in full without it. A script that only some shoppers load (signed in, or with certain products in the cart) is therefore not reported as removed, and one check that happens to miss a script does not report it either. A script that disappears and stays gone is reported by the second check after it went, once. A script that the checks' own page loads see only some of the time (an A/B test, for example) can still be reported as removed after two misses in a row, and then reappear without a "new" entry, since it is already in the inventory.

Exports and the evidence report

All three are on the Inventory tab.

  • Export inventory (CSV): one row per script. Columns: script, type, party, pages, source, provider, owner, business_purpose, justification, status, changed_since_authorised, authorised_by, authorised_at, integrity_method, current_hash, last_checked, review_due, first_seen, last_seen, integrity_error (why no hash). Every coded column (type, party, pages, source, status, integrity method, integrity error) holds the raw stored code rather than a label, so the file reads the same whatever the site's language; the printable report shows readable labels.
  • Evidence record (CSV): the 11.6.1 record of the checks that ran, one row per check. Columns: checked at (UTC), trigger, pages in run, scripts seen, new scripts, changes found by this check, scripts still flagged, headers still flagged, outcome, pages read, pages not read in full, script hashes verified, email alert. The outcome is no change, changes recorded (scripts), header changes recorded (a monitored response header was added, removed or changed by this check), script and header changes recorded, or incomplete. Headers still flagged from an earlier check do not change a later check's outcome; records written before header changes were counted keep the outcome they had.
  • Print evidence report: a standalone printable "Payment page script integrity record" combining both, the header baselines, and your Assessment guide answers as "Assessment context". It opens in a new tab for the browser's print dialog.

CSV cells are neutralised against spreadsheet formula injection, since the values originate outside your site. Both exports are available from WP-CLI as CSV or JSON.

Site Health

Three tests appear under Tools > Site Health, in the Payments section:

  • Payment page check record: whether a check that read at least one payment page has run in the last 8 days. Scans that could not read any page are named as such rather than counted.
  • Scripts on your payment pages: whether any script is unauthorised or has changed since it was authorised.
  • Payment page response headers: whether any header drift is outstanding.

WP-CLI

bash
wp dragon-checkout-guard scan                      # scan every enabled page key
wp dragon-checkout-guard scan --page=checkout      # scan one page key

wp dragon-checkout-guard pages                     # every page key: enabled? URL?
wp dragon-checkout-guard pages --format=json

wp dragon-checkout-guard list                                  # inventory (dcg_scripts)
wp dragon-checkout-guard list --status=browser                 # unconfirmed browser observations
wp dragon-checkout-guard list --status=changed --format=csv    # scripts whose content hash changed

wp dragon-checkout-guard authorise 12 \
    --justification="Stripe.js payment element" \
    --method=vendor_managed --provider=Stripe --user=admin

wp dragon-checkout-guard dismiss 42                # one unconfirmed observation
wp dragon-checkout-guard dismiss --page=checkout   # every unconfirmed observation for a page
wp dragon-checkout-guard dismiss --all-unconfirmed --yes

wp dragon-checkout-guard export --type=inventory > inventory.csv
wp dragon-checkout-guard export --type=evidence --format=json

wp dragon-checkout-guard prune --days=90           # trim old events + unconfirmed observations
  • scan takes --page=<key>: checkout, cart, order_pay, add_payment_method or a custom:<hash> key.
  • pages takes --format=table|json|csv and lists every recognised page key, whether it is enabled, and its canonical URL where one exists.
  • list accepts --status=new|authorised|ignored|changed|browser and --format=table|json|csv, and warns rather than silently truncating if a result set exceeds its 500-row cap: narrow with --status.
  • authorise accepts --method=sri|hash_monitored|vendor_managed|not_feasible_justified|none, plus --owner, --purpose and --provider. Run it with WP-CLI's global --user=<id|login> so the authorisation is attributed to a real actor rather than recorded anonymously.
  • dismiss takes exactly one of <id>, --page=<key> or --all-unconfirmed (with --yes to skip the prompt). A confirmed observation is part of the audit trail and is never deleted here.
  • export takes --type=inventory|evidence and --format=csv|json, with the same columns as the admin exports. The evidence export covers the last 90 days and warns if it stopped short.
  • prune takes --days=<n>, applied to both events and unconfirmed observations; 0 clears everything eligible now. Without it, events use your configured retention and observations use 30 days, the same defaults the daily maintenance cron uses.

Limits

LimitValue
Passive capture intervalOne sample per page per interval, in minutes (setting, default 10). Passive sampling reads the script registry; it does not buffer the page.
Buffered capture size5 MB (Server_Capture::MAX_BUFFER_BYTES) for a scan or Open & capture; a larger page is passed through untouched and the capture is recorded as failed.
Remote hash fetches200 per UTC day (Integrity::DAILY_BUDGET).
Per-host backoff3 failures pause that host for 1 hour.
SchemeOnly https script URLs are fetched, with the connection pinned to the validated address where the site's HTTP transport supports it (curl).
Response size hashed2 MB (Integrity::MAX_BODY_BYTES).
Stale scriptsA script not seen on any page for 30 days stops being refetched and shows "Not seen recently".
Collector requests30 per minute per address (IPv6 grouped by /64).
Collector report size300 rows and 64 KB per report.
Collector trip500 new observations in an hour, or 5000 unconfirmed in total, pause the collector for an hour with an admin notice.
Flood reliefAt 4000 rows in total the daily maintenance deletes the most recent unconfirmed observations, up to the number needed to bring the table down to 2500, and records an observations_cleared event with the number it took. Confirmed observations are never deleted.
Observation retentionUnconfirmed observations are pruned after 30 days.
Event retention30 to 3650 days (setting, default 365).
Export sizeExports carry at most 5000 rows and say so when they stop short.
Custom paths50 paths, 255 characters each, site-relative only.
Alert recipients20 addresses; each alert section lists 25 entries and counts the rest.
ConcurrencyOne scan at a time, enforced by a lock.

Event retention prunes only the low-value kinds: discovered, scan run, scan failed, collector tripped and settings changed. Everything else, including the evidence, authorised, revoked, changed, confirmed, headers changed, baseline accepted and browser observations cleared records, is kept regardless of the setting, because that is the record.

A script that could not be hashed says why, on its row and in the inventory export: host did not resolve, host resolves to a private address, host temporarily backed off after failures, daily fetch budget exhausted (retried tomorrow), fetch failed, response over 2 MB, local file not found, or unsupported file type.

Settings are validated rather than silently accepted: a custom path that points off-site or is malformed is refused with a reason and your typed value is handed back, and a numeric value outside its range is clamped and reported as adjusted.

If the plugin's tables could not be created on activation, the schema version is not recorded, an admin notice says so, and the install is retried automatically every 10 minutes or immediately on re-activation.

Dragon Activity Log

If Dragon Activity Log is active, Dragon Checkout Guard records script.authorised, script.revoked, script.changed, headers.changed and collector.tripped to it. script.changed and collector.tripped are recorded at the higher severity. The plugin's own event log works with or without it.

Data and privacy

The plugin stores its script inventory, headers baseline and forensic event log in its own database tables. None of it identifies a site visitor: there is no IP address, user-agent or session column anywhere in the schema, and the browser collector's per-address rate limit uses a salted hash of the address, kept in a transient for two minutes; on sites without a persistent object cache that transient lives in the options table for that time. Inline previews are stored with sensitive values redacted, and query parameters you list under Redacted query parameters are masked in the src shown throughout the admin.

External requests. The only outbound request the plugin makes to anywhere but your own site (scans also fetch your own payment pages) is wp_safe_remote_get() downloading a script your payment page already loads from another host, to keep its content hash current for drift detection. A script on your own site is read from disk instead. The request is a plain HTTPS GET of a public URL, sent with the user agent Mozilla/5.0 (compatible; DragonCheckoutGuard/<version>; +https://dragoncore.ltd/plugins/dragon-checkout-guard): no cookies and no data about your site, its configuration or its visitors are attached to it, and nothing is ever sent to Dragon Core. There is no account and no telemetry.

Email. Alerts are sent with wp_mail() through your site's own mail setup, to the addresses on the Settings tab (or the site administration email address). They carry script names and addresses and response-header names from your payment pages, nothing about customers or orders.

A suggested privacy-policy paragraph describing exactly what is recorded is registered with WordPress's own Settings > Privacy tool (wp_add_privacy_policy_content), so you can add it to your site's privacy policy in one click.

Uninstall

Deleting the plugin keeps all its data by default, so a reinstall picks up where you left off. To remove everything on uninstall, tick Delete this plugin's tables, options and evidence when it is uninstalled in Settings first; this sets the dragoncheckoutguard_delete_data_on_uninstall option, which uninstall.php checks before dropping anything.

For developers

Actions:

HookFires when
dragoncheckoutguard_script_discoveredA script is recorded for the first time.
dragoncheckoutguard_script_changedA script's content hash drifted from the authorised one.
dragoncheckoutguard_script_authorisedA script is authorised.
dragoncheckoutguard_script_revokedAn authorisation is revoked.
dragoncheckoutguard_headers_changedA page's response headers drifted from the accepted baseline.
dragoncheckoutguard_scan_finishedA scan completed, with its summary.
dragoncheckoutguard_scan_failedA scan failed, or could not cover a page.
dragoncheckoutguard_collector_reportA browser collector report was accepted.
dragoncheckoutguard_collector_trippedThe collector paused on an observation limit.
dragoncheckoutguard_script_rekeyedThe 1.0.7 upgrade rewrote a stored src into its canonical form (default port or missing path, IDN host) and re-keyed the row in place.
dragoncheckoutguard_settings_savedSettings were saved.
dragoncheckoutguard_admin_tab_<slug>Renders a tab added through dragoncheckoutguard_admin_tabs.

Filters:

HookFilters
dragoncheckoutguard_in_scopeThe resolved page key for the current request, or null when it is out of scope.
dragoncheckoutguard_page_keysThe recognised page keys and their labels.
dragoncheckoutguard_admin_tabsThe admin tab list (slug => label).
dragoncheckoutguard_settings_fieldsExtra fields rendered in the "Additional settings" block.
dragoncheckoutguard_capture_intervalThe passive capture interval, in seconds.
dragoncheckoutguard_scan_cart_productThe product id the scan puts in its in-memory cart to read Checkout (default: the lowest-id eligible simple product among the first 100, or 0). Return 0 to leave the cart empty. The product must be published, purchasable, in stock and priced above zero.
dragoncheckoutguard_monitored_headersThe response headers captured and compared.
dragoncheckoutguard_redacted_paramsThe query parameter names masked for display.
dragoncheckoutguard_authorised_sourcesThe authorised-script hosts, shaped for a CSP script-src allowlist (inline hashes are passed as the second argument).