Security test plan
Status: draft, pending review. Covers this repository (backend planes, console, WooCommerce/Magento plugin). This is item 1 of two asks — a test plan and pentest case catalog. SAST/DAST tooling…
Generated from docs/SECURITY-TEST-PLAN.md in the Sarvex repository — edit the source there, not this page.
Sections about the marketing
websitedescribe a separate repository that is not part of this release; they are kept for context.
Status: draft, pending review. Covers this repository (backend planes, console, WooCommerce/Magento plugin). This is item 1 of two asks — a test plan and pentest case catalog. SAST/DAST tooling recommendations are in §5; nothing here has been executed yet, no scans have been run, and no findings are claimed. Treat every “expected” behavior below as a hypothesis to verify, not a confirmed fact.
Grounded in a read-only sweep of the current tree (internal/planes/*, internal/pkg/middlewares, internal/planes/control/auth, web/console, plugin/{woocommerce,magento}, website/server/api, .github/workflows) done 2026-09-26. File:line references below point at real code, not assumed shapes.
0. Scope & rules of engagement
In scope:
website— Nuxt/Cloudflare Worker marketing site, its two Nitro server routes (server/api/contact.post.ts,server/api/subscribe.post.ts), and the static/prerendered surface.web/console(@sarvex/console) — the Vue 3 SPA, its Kinde PKCE auth flow, and its typed API client.internal/planes/control/api— the console/operator REST API (Kinde bearer + HMAC connector auth, persarvex-v1-control-plane.yaml).internal/planes/control/realtime— the operator WebSocket surface.internal/planes/sync—/connector/*HMAC-authenticated ingest, catalog import, order-intent, sweep.internal/planes/serving— the public, unauthenticated/.well-known/ucp, catalog, negotiation, and checkout endpoints (checkout is the one path that calls a merchant backend live, per architecture §A10.2).plugin/woocommerceandplugin/magento— treated as attacker-adjacent code: it runs on merchant infrastructure Sarvex doesn’t control.
Out of scope for this pass (per CLAUDE.md §0/§6 — flag, don’t build): anything on the V1 out-of-scope list, the frozen Shopify/BigCommerce/Commercetools connectors (test only enough to confirm they’re inert, not their business logic), infrastructure the repo doesn’t declare (Kinde’s own hosted login UI, Stripe’s hosted pages, cluster and Cloudflare account access outside deployments/selfhost).
Rules of engagement:
- All dynamic/DAST testing runs against a dedicated staging/sandbox deployment with test Stripe keys, a sandboxed Kinde tenant, and simulator merchant stores (
cmd/sim,cmd/ucp-sim-equivalent) — never against production, and never against a real merchant’s live WooCommerce/Magento install. Checkout is the one endpoint that calls a merchant backend live; that merchant backend must be the simulator, not a real store. - No destructive testing (data deletion, DoS payloads) without a dedicated disposable environment.
- Credential/secret material (Kinde client secrets, HMAC pairing codes, Vault tokens and Transit keys, Stripe keys) is never exercised against real values in test scripts or commit history.
- Findings are triaged by the invariants in
CLAUDE.md§2 first — a violation there is a stop-and-report condition regardless of exploitability.
1. Attack surface map
| Surface | AuthN | Reachability | Key files |
|---|---|---|---|
| Website pages (static/prerendered) | none | public internet | website/pages, website/content |
Website /api/contact, /api/subscribe |
Turnstile captcha (contact only) + bearer token to downstream (server-side) | public internet | website/server/api/*.post.ts |
| Console SPA | Kinde PKCE (browser) | public internet (static SPA) | web/console/src/auth |
Control-plane REST (/api/*) |
Kinde JWT bearer (or dev sim resolver) | public internet, behind CORS allow-list to one origin | internal/planes/control/api, auth/bearer.go |
| Control-plane WebSocket | same bearer, presumably re-checked at upgrade | public internet | internal/planes/control/realtime |
Sync-plane /connector/* |
HMAC (X-Sarvex-Signature/-Timestamp/-Sequence/-Credential) |
public internet (merchant plugin calls in) | auth/hmac.go, sync/connector |
Serving-plane /.well-known/ucp, catalog, negotiation |
none by design (public agent surface) | public internet | internal/planes/serving |
| Serving-plane checkout | none inbound, but calls merchant backend live | public internet in, live outbound call out | checkout_service.go |
WooCommerce plugin ProfileProxy.php |
none (proxies public profile) | merchant’s own domain | plugin/woocommerce/src/ProfileProxy.php |
WooCommerce plugin SignalSender.php |
HMAC outbound to Sarvex | merchant’s own domain → Sarvex | plugin/woocommerce/src/SignalSender.php |
Magento plugin (Controller/Wellknown, Observer/*) |
same pattern, Magento module | merchant’s own domain | plugin/magento/* |
2. Threat model — tie every test to an invariant
CLAUDE.md §2 lists eight non-negotiable invariants; the ones with real attack-surface implications:
- Plane isolation — a control-plane bug that lets an attacker reach sync/serving internals directly (not via bus/Mongo read) is a structural break, not just a bug. Test: fuzz for any HTTP route that isn’t in
sarvex-v1-control-plane.yamlbut resolves anyway (route confusion, path traversal into another plane’s router if they ever share a process/port). - Discovery/read serves only the cache; checkout is the one live-call exception — test that no serving-plane read path can be coerced into a live store call (e.g. via a crafted
store_idor cache-miss fallback), and that checkout’s live call path can’t be reached from any other route. - One writer per store — not directly attacker-testable, but a tenant-isolation bypass that lets one store’s connector write another store’s catalog would violate it; covered by tenancy tests below.
- Two-level tenant isolation, resolved server-side only — the single highest-value target.
merchant_id/store_idmust never be trusted from URL path/query/body; they must come from the verified JWT claim, the HMAC credential, or operator scope. This is a full IDOR test class (§4). - Kinde is the only auth issuer —
NewSimBearerResolver(auth/bearer.go:89) trusts unsigned tokens (header.payload.anysig, no signature check) and is wired in wheneverKINDE_ISSUER_URLis unset (control/api/entry.go:858-859). This is intentional for local dev, but its blast radius if it ever activates in a real deployment is total auth bypass. Test: confirm every non-local deployment target indeployments/setsKINDE_ISSUER_URL, and add a startup guard/CI check that refuses to boot the sim resolver when a production-shaped env var (e.g.ENV=production) is set. - Idempotency at every external edge — checkout mutations, order recording, and connector signal ingestion all claim idempotency; test replay behavior explicitly (§4).
- Spec version is data — lower security relevance, skip for pentest, keep for correctness testing.
- Payment-credential boundary is structural — the checkout schema must have no field able to hold a raw credential; Sarvex only ever sees
handler_id+ opaque references. Test: attempt to smuggle a PAN/CVV-shaped value into any checkout/order field and confirm the schema/validator rejects it, and grep the schema for any string field wide enough to hold one.
3. Test plan by target
3.1 Website
- Input handling:
contact.post.tsandsubscribe.post.tsdo their own validation (email regex, length caps) before any downstream call — test bypasses (unicode homoglyphs in email,storeUrl/messagelength-boundary values, header injection viaemail/storeUrlreflected into the Resendtextbody — confirm no CRLF injection into the outbound email). - SSRF via
storeUrl: the field is free text forwarded into an email body only today — confirm it’s never later passed to a server-side fetch ($fetch) without validation if that code path grows. - Captcha bypass: replay a used Turnstile token, omit
remoteip, or race two submissions with the same token. - Secrets in client bundle: confirm
turnstileSecretKey/resendApiKey/contactApiTokennever leak into the client-side runtime config (Nuxtpublicvs private runtimeConfig split) — grep the built.output/publicbundle for the literal secret strings. - Headers: verify CSP,
X-Frame-Options/frame-ancestors,Strict-Transport-Security,X-Content-Type-Optionsare set (Nitro/Cloudflare config) — none were found innuxt.config.tsduring recon; likely a gap (§9). - Rate limiting / abuse: neither
/api/contactnor/api/subscribeshow rate limiting in the handler — confirm Cloudflare-level rate limiting exists, or add it; otherwise both are open to volumetric abuse (email-bombing the sales inbox, exhausting Resend quota) and to spamming the newsletter backend, which the code forwards tocontactApiUrlunauthenticated-by-source. - Dependency/build supply chain:
pnpm-lock.yamlintegrity,postinstall: nuxt prepareand thedocssubsite’s ownnpm install(two separate lockfiles/ecosystems in one repo — confirm both get scanned).
3.2 Console (Vue SPA)
- Token handling: where does Kinde PKCE store the access/refresh token (memory vs
localStorage/sessionStorage)?localStorageis XSS-exfiltratable; confirm@kinde-oss/kinde-auth-pkce-js’s default and whether the app overrides it. - XSS surface: any
v-htmlusage, any merchant-controlled data (catalog titles/descriptions pulled from a real WooCommerce store) rendered into the console — a malicious merchant catalog is attacker-controlled input reaching an authenticated console session (stored XSS via product data is a real, high-value path given the ingestion pipeline). Grepweb/console/srcforv-html,innerHTML,dangerouslySetInnerHTML-equivalents. - Client-side authorization is not authorization: confirm every view/route guard is backed by a real server-side check — the console’s role/merchant checks are UX only; the actual gate must be in
internal/planes/control/apimiddleware. Test by calling the API directly (bypassing the SPA) with a valid-but-wrong-scope token. - CORS:
WithCORS(middlewares/withCors.go) allow-lists exactly onedashboardUrivia exact-match after trimming trailing slash — test with scheme confusion (httpvshttps), subdomain variants, and trailing-path variants to confirm the exact-match really is exact and not a prefix/suffix match. - Mock mode leakage:
src/api/mocks.tsexists formake console(mock, no backend) — confirm build tooling can never accidentally ship the mock backend (with any seeded fake data/tokens) in a production build. - Dependency/SCA:
npm audit/osv-scanneragainstweb/console/package.json(axios, vue, kinde SDK, wrangler) — console CI currently runs build+tests only, no SCA (§5).
3.3 Control-plane API (internal/planes/control/api)
- AuthN correctness (
auth/bearer.go): algorithm confusion (forcealg: noneor HS256-with-public-key-as-secret against the JWKS-based verifier), expired/not-yet-valid token handling, audience/issuer mismatch, missingmerchant_id/roleclaim handling (already errors — confirm it errors closed, not open), token replay across environments (does a staging-issued token verify against production’s issuer/audience?). - Tenant isolation / IDOR — the core test class, per
docs/TENANCY-PATTERNS.md(ADR-2, query-level fencing mandatory): for every endpoint underapi/{account,store,connector,catalog,task,checklist,journey,validation,monitoring,billing,operator,facts,domain}.go, authenticate as merchant A and request/mutate a resource ID (store_id,connector_id, order/checkout id, task id) belonging to merchant B. Expected: 404/403, never data. Specifically targetpayment_config.go,ledger.go,checkout_sessions_test.go’s underlying handler, andinsights.go— anywhere a lookup is “by ID” rather than “by ID scoped to my merchant” is a finding. - Operator scope escalation:
withAdmin.go/operator role — confirm a merchant-role token can never reach operator-only routes (operator/service,EnterAsMerchantin the console implies an operator-impersonation feature — test that impersonation itself is scoped/audited and can’t be self-granted). - HMAC connector auth (
auth/hmac.go): replay beyond the sequence check — the function checksX-Sarvex-Sequenceagainst a Redis dedup set but theX-Sarvex-Timestampheader is read and folded into the signed message, yet nowhere in the read code does it appear to be checked for staleness (a signature computed once with a fixed, far-future or far-past timestamp would still verify if the sequence hasn’t been seen). Confirm whether a timestamp-window check exists elsewhere in the call chain; if not, this is a real replay-window widening finding — test by replaying a captured request with a new/unused sequence number and the original timestamp far outside any reasonable clock-skew window. Also test: Redis unavailable → does dedup check fail closed (return error, confirmed in code — good) or does the “mark sequence as seen” write failure silently allow a later replay (confirmed: write failure is logged but not fatal — a second replay in the same request lifetime before the key is set is a narrow race worth confirming isn’t exploitable). - HMAC credential/connector confusion:
<store_id>:<connector_id>credential format — test with aconnector_idthat exists but belongs to a differentstore_idthan claimed (code checks this — confirm the check can’t be bypassed by encoding tricks in the credential string, e.g. extra colons, sinceSplitN(credential, ":", 2)takes only the first split). - Rate limiting:
WithRateLimiting(internal/planes/serving/middleware.go) is in-memory, per-process, keyed byRealIP()— test (a) multi-instance bypass (each pod gets its own bucket), (b)RealIP()spoofing viaX-Forwarded-Forif the proxy/LB config trusts client-supplied headers, (c) whether login/token-verification-heavy routes have tighter limits than the blanket 50rps/10burst. - Body limits / request validation:
withBodyLimit.go,withRequestValidation.go— oversized payload DoS, malformed JSON handling, schema-validation bypass (extra/unknown fields — mass assignment into domain structs). - Injection: Mongo query construction anywhere user input reaches a filter directly (NoSQL injection via operator injection if any handler builds filters from raw request maps rather than typed structs) — grep for
bson.M{built from request bodies. - Payment-credential boundary: attempt to POST a full PAN/CVV-shaped string into any checkout/payment field across
payment_config.goand the serving-plane checkout schema; confirm rejection and confirm no logging path (reqlog.go,withRequestLogger.go) would capture it if it were ever sent (log scrubbing test). - WebSocket (
realtime/): confirm the upgrade handshake re-validates the bearer token (not just “was authenticated once, trust the socket forever”), and that messages on the socket are still tenant-scoped per §2.4.
3.4 Sync-plane connector ingest
- Same HMAC test class as 3.3, but this is the plugin-facing edge reachable from any merchant’s WordPress/Magento install — a compromised or malicious merchant is a legitimate, authenticated actor here, so also test:
- Malicious catalog content: a merchant pushes a product with a script-payload title/description/image-URL. Trace it through
ingestion.go→mapping.go→catalog_mongo.go→ serving cache → console rendering. Stored XSS via merchant-controlled catalog data reaching the console is the most realistic high-severity finding in this system. - SSRF via connector pull: the “thin events, fat pull” model means Sarvex calls back into the merchant’s store API (
GET /connector/profile-style pulls) using a URL derived from store registration data — test whether that URL can be pointed at internal/metadata addresses (169.254.169.254,localhost, internal service DNS) at store-registration time. - Sweep/job server DoS (
sweep_handler.go,job_server.go): a merchant that signals at high frequency or with oversized payloads — confirm backpressure/rate limiting per-connector, not just global. - Catalog import (
catalog_import.go): zip-bomb/oversized-feed handling if imports accept bulk file uploads.
3.5 Serving-plane public API
- No inbound auth by design — confirm nothing here silently trusts a header value as identity (there should be none to trust).
- Cache key tenancy: confirm every Redis cache key (
cache.go,cache_redis.go) is namespaced bystore_idsuch that no request for store A can ever read store B’s cached profile/catalog (a cache-key collision here is a direct cross-tenant data leak, more severe than a typical API IDOR since it’s unauthenticated). - Checkout idempotency (
checkout_idempotency_test.goexists — confirm real coverage, not just a stub): replay a checkout mutation with the same idempotency key and confirm no double-charge/double-order; replay with same key but different body and confirm rejection, not silent overwrite. - Session store (
session_redis.go,session_memory.go): session fixation, session ID entropy, expiry enforcement, and whethersession_memory.go(presumably a fallback/dev store) could ever be selected in a multi-instance production deployment (a per-instance memory session store behind a load balancer breaks session continuity and may fail open). - Abandonment sweep (
abandonment_sweep.go): confirm it can’t be triggered/timed by an external actor to cancel another party’s in-flight checkout. - Webhook endpoint (
webhook.go): if this receives inbound callbacks (Stripe or connector), confirm signature verification is mandatory and fails closed.
3.6 Merchant plugin (WooCommerce/Magento)
ProfileProxy.php: must serve/.well-known/ucpwith Sarvex’s bytes verbatim,200only, never a redirect (perCLAUDE.md§4) — test cache-poisoning of the “last-known-good” local cache (can an attacker who compromises the merchant’s WP admin, or a MITM before TLS, seed a bad cached profile that persists past a legitimate fix?), and confirm it holds no private keys (grep plugin storage/options tables for anything secret-shaped).SignalSender.php/ HMAC.php: pairing-code storage on the merchant’s own DB (WordPress options table / Magento config) — is it encrypted at rest, or plaintext in a table an SQLi elsewhere on the WP site could read? Confirm signal payloads truly carry ids only (per §4) — grep for any field that could carry PII/payment data.PairingManager.php: the pairing/registration flow is the bootstrap trust anchor — test whether the pairing code can be brute-forced, replayed, or re-used after rotation, and whether re-pairing requires re-proving control of the store.- WordPress-specific: nonce/CSRF protection on any admin-facing action, capability checks (
current_user_can) on plugin admin pages, no raw$wpdb->querystring concatenation (SQLi), output escaping in the plugin’s own admin UI. - Magento-specific: ACL (
etc/acl.xml) correctly restricts the plugin’s admin routes;Observer/*classes don’t trust event data as already-validated.
4. Concrete pentest case catalog
Each case: precondition → action → expected secure result. IDs group by target.
| ID | Target | Case |
|---|---|---|
| WEB-01 | Website | Submit /api/contact with a captcha token reused from a prior successful submission → expect rejection |
| WEB-02 | Website | Submit /api/contact with email containing \r\nBcc: attacker@evil.com → expect no header injection into the outbound Resend email |
| WEB-03 | Website | Submit /api/subscribe 1000x/min from one IP → expect throttling (Cloudflare or app-level) |
| WEB-04 | Website | Fetch the built client bundle, grep for resendApiKey/turnstileSecretKey/contactApiToken literal values → expect no match |
| WEB-05 | Website | Request any page over plain HTTP or with a wrong Host → expect HSTS redirect / rejection, not reflection |
| CON-01 | Console | Inspect browser storage post-login for raw JWT/refresh token in localStorage → assess XSS blast radius |
| CON-02 | Console | Search web/console/src for v-html sinks fed by catalog/product data → expect none, or expect DOMPurify-equivalent sanitization |
| CON-03 | Console | Call a control-plane endpoint directly (curl, valid token, store the console UI never lets you reach) for a resource outside your merchant → expect 403/404 |
| CON-04 | Console | Send a CORS preflight from https://evil.example and from https://console.sarvex.example.com.evil.com → expect rejection by WithCORS’s exact-match check |
| API-01 | Control API | Token for merchant A, request /api/stores/{storeB_id} (and equivalent for connector/task/checklist/journey/insights/ledger/billing/payment_config) → expect 403/404 on every one, not just some |
| API-02 | Control API | Forged JWT with alg: none or HS256-signed-with-JWKS-public-key against the RS256 verifier → expect rejection |
| API-03 | Control API | Valid merchant-role token against an operator-only route → expect 403 |
| API-04 | Control API | Confirm NewSimBearerResolver cannot activate when the deployment is not local dev (check every deployments/ env for KINDE_ISSUER_URL) |
| API-05 | Control API | Replay a captured HMAC-signed connector request with a new sequence number but the original (old) timestamp → expect rejection if a timestamp-freshness check exists; if it doesn’t, this is a finding — recommend adding one |
| API-06 | Control API | HMAC credential string storeA:connX:extra (extra colon) → expect the SplitN parsing can’t be abused to attribute the request to a different store than intended |
| API-07 | Control API | Flood one endpoint from many source IPs (simulating a botnet) against the in-memory per-IP rate limiter → expect a documented mitigation (WAF/LB-level) beyond the app-level limiter, since the app-level one is per-process |
| API-08 | Control API | Oversized JSON body (>configured limit) and deeply nested JSON → expect clean rejection, no resource exhaustion |
| API-09 | Control API | POST a 16-digit numeric string into any checkout/payment-adjacent field → expect schema rejection; grep logs after the attempt for the literal value → expect absent |
| API-10 | Control API (WebSocket) | Open a WebSocket with a valid token for merchant A, then attempt to subscribe to/receive merchant B’s channel/events → expect rejection |
| SYNC-01 | Sync ingest | Signal a product with title <img src=x onerror=alert(1)> through the full ingestion→mapping→catalog pipeline, view it in the console → expect encoded output, not execution |
| SYNC-02 | Sync ingest | Register a store whose backend URL resolves to 169.254.169.254 or 127.0.0.1 → expect the connector pull step to refuse (SSRF guard) |
| SYNC-03 | Sync ingest | Send HMAC-signed signals at high frequency from one connector → expect per-connector backpressure, not just global rate limiting |
| SYNC-04 | Sync ingest | Send a signal with a store_id in the payload that doesn’t match the HMAC credential’s store_id → expect rejection |
| SRV-01 | Serving plane | Request store A’s cached profile/catalog using any manipulated identifier that could collide with store B’s cache key → expect no cross-tenant data returned |
| SRV-02 | Serving plane | Submit the same checkout idempotency key twice with different line items → expect rejection of the second, not silent overwrite or double order |
| SRV-03 | Serving plane | Submit the same checkout idempotency key twice concurrently (race) → expect exactly one order created |
| SRV-04 | Serving plane | Hit the checkout endpoint (which calls the merchant backend live) at high volume → expect the merchant-backend call path has its own timeout/circuit-breaker (resilience package) so a flood can’t be used to DoS the merchant’s store through Sarvex |
| SRV-05 | Serving plane | Inspect webhook.go — send an unsigned/wrongly-signed inbound webhook → expect rejection, not best-effort processing |
| PLG-01 | WooCommerce plugin | MITM or compromise the “last-known-good” cache used by ProfileProxy.php → assess whether a stale/poisoned profile can persist after the upstream is fixed, and for how long |
| PLG-02 | WooCommerce plugin | Inspect WordPress options table / DB for the HMAC pairing secret → expect it’s stored no more readably than other WP secrets, ideally encrypted |
| PLG-03 | WooCommerce plugin | Attempt to re-run the pairing flow without proving control of the store (e.g. replay an old pairing code) → expect rejection |
| PLG-04 | Magento plugin | Access Controller/Wellknown and Observer/* as an unauthenticated/low-privilege user → expect ACL (etc/acl.xml) enforcement matches WooCommerce’s equivalent guarantees |
This table is a starting catalog, not exhaustive — expand it once the §1 sweep on deployments/ (env-per-environment config) and the actual sarvex-v1-control-plane.yaml endpoint list are cross-referenced endpoint-by-endpoint (there are more endpoints than are listed individually above; API-01 should be run against literally every merchant-scoped GET/PATCH/DELETE, not just the sample named).
5. SAST / DAST / SCA — what applies, and what’s already there vs. missing
Status: implemented in this PR, unless noted “still open” below.
.golangci.ymlrunsgosec,bodyclose,noctx,sqlclosecheck,rowserrcheck,depguard(architectural import-boundary enforcement — this is the automated check for the plane-isolation invariant) as part ofgo-lint.yml, but scoped to--new-from-rev(only new lines in a PR diff) — the existing backlog is not gated. Unchanged; still open (the backlog needs a separate paydown pass before removing the scoping)..github/workflows/security.yml(new):govulncheck,gosec(SARIF), CodeQL(go) with.github/codeql/codeql-config.yml(new, scoped tointernal/cmd/main.go), Bearer, and.semgrep/tenancy.yml(new custom rules, see below) — all whole-tree, not diff-scoped, plusgitleaksfor secret scanning. Semgrep runscontinue-on-error: truefor now (new custom rule set, first findings need triage before it blocks merges — same patterngo-test.ymlalready uses for its own new conformance job)..semgrep/tenancy.yml(new) mechanically enforces two of the invariants a generic SAST ruleset can’t reach:merchant-id-from-url-param: flagsmerchant_idsourced fromc.Param/c.QueryParam/c.FormValueoutsideoperator.go(the one sanctioned “authorized operator scope” exception, gated byrequireOperatorinhandler.go). Deliberately does not flagstore_id-from-URL — that’s the normal, blessed REST addressing pattern perdocs/TENANCY-PATTERNS.md(resource ID in the path,merchant_idfrom the principal fences the query) — an earlier draft of this rule flagged it and produced 59 false positives against exactly that pattern before being narrowed.checkout-credential-bearing-field: flags a raw card-number/CVV/PAN-shaped field in Sarvex’s own checkout/payment-config schema. First run found 3 matches (CVC stringininternal/pkg/ucp/types/{v20260408,v20260825}/payment.go) — investigated rather than auto-fixed, per the stop-and-report instinct these invariants demand: those packages pin the external ucp.dev wire spec (CLAUDE.md §3) and must model the fullCardCredentialshape to represent what a handler’s raw-card path can carry on the wire; the invariant is actually enforced one layer up, ininternal/planes/serving/api/checkout_contract.go’sOpaquePaymentCredential, whoseUnmarshalJSONdeliberately extracts onlyhandler_id+tokenand silently drops anyCardCredential-shaped payload. Confirmed viagrep(no persistence, logging, or Mongo write touches.Card/.CVCanywhere) — no violation, and the rule is now scoped away frominternal/pkg/ucp/types/**to avoid re-flagging this legitimate spec mirror. Both rules validated against the full tree: 0 findings post-fix.- Still open: a
depguard-style rule for plane-import isolation isn’t needed as a new rule —depguardin.golangci.ymlalready covers it (see comment there); a Semgrep rule for it would be redundant.
console-ci.yml: added anscajob (npm audit --audit-level=high),continue-on-error: trueuntil the first backlog is triaged.dependabot.yml(sarvex.cp): addednpmforweb/consoleandgithub-actions, alongside the existinggomod.- website repo: added
.github/dependabot.yml(npm for both the rootpnpmtree and the separately-installeddocs/subsite),.github/workflows/security.yml(pnpm audit+npm auditfordocs/+gitleaks), and baseline response headers innuxt.config.ts(X-Content-Type-Options,X-Frame-Options,Content-Security-Policy: frame-ancestors 'none',Referrer-Policy,Permissions-Policy, HSTS) addressing WEB-05.
Still open (not done in this pass — needs its own follow-up):
| Layer | Tool | Why it’s separate |
|---|---|---|
| PHP (woocommerce, magento) | PHPStan/Psalm + roave/security-advisories/composer audit |
WooCommerce plugin has no composer.json (Magento does) — needs its dependency model confirmed first (open question in §7), and neither repo’s CI currently touches PHP at all, so this is new pipeline plumbing, not an addition to an existing one |
| IaC | kubeconform/checkov against deployments/selfhost (Helm charts, Cloudflare Terraform) |
Not run in CI yet |
| DAST | OWASP ZAP baseline against staging console+control-API; manual/Burp testing for the §4 IDOR/tenancy cases | Needs the staging environment question in §7 answered first — no environment to point a scanner at yet |
Full CSP (script-src/style-src/etc.) on the website |
— | The frame-ancestors directive shipped here is safe to add blind (it only restricts being framed); a full CSP risks breaking Turnstile’s script+iframe, Nuxt’s inline hydration/JSON-LD, or a Cloudflare-injected analytics beacon, and needs verifying against a live preview deploy rather than guessed |
Container/deploy image scanning (trivy) |
— | Didn’t confirm whether deploy.yml builds images in this pass |
Applicability judgment for “if applicable” in the ask: yes, broadly applicable, and the bulk of it is now wired up — the codebase is a JWT/HMAC-authenticated multi-tenant Go backend plus a Vue SPA plus PHP plugins reachable from merchant infrastructure; every layer benefits from at least SAST+SCA. The tenancy/IDOR class in §4 remains the one place tooling has limited reach — the two Semgrep rules above catch the mechanical version of it, but the actual cross-merchant IDOR cases (API-01) still need human-driven authenticated testing, not generic tooling.
6. Sequencing
- Agree this plan and the case catalog (this document).
- Wire up SAST/SCA/secret-scanning (§5) — fast, low-risk, catches low-hanging fruit before manual effort.
- Triage SAST/SCA output; fix or explicitly accept each finding.
- Run the manual/DAST pentest case catalog (§4) against staging, prioritizing API-01/API-04/API-05 (tenant isolation, sim-resolver gating, HMAC replay) and SYNC-01 (stored XSS via catalog) as highest-severity-if-true.
- Write up findings with severity, reproduction steps, and fix recommendation; re-test after fixes.
7. Open questions to confirm before executing
- Is there a staging environment with test Stripe keys and a sandboxed Kinde tenant available for dynamic testing, or does one need to be stood up first?
- Does
X-Sarvex-Timestampget a freshness check anywhere outsidehmac.go(worth confirming before treating API-05 as a live finding rather than a documentation gap)? - What’s the WooCommerce plugin’s dependency/build model (no
composer.jsonwas found next toplugin/woocommerce, unlike Magento) — confirms which PHP SCA tool applies. - Who is the intended executor of the manual/DAST phase — internal team, or does this need external pentest engagement sign-off/authorization paperwork given it touches a live checkout path (even against simulators)?