Self-audit, May 2026

OWASP ASVS L2 self-review.

Static review of code, configuration, and infrastructure against OWASP Application Security Verification Standard Level 2 (v4.0.3). Per-control status, evidence, and an honest section on what a self-audit cannot prove. Dated 21 May 2026.

Scope

caltury.com.au, app + marketing site, the Supabase project lshifibopmomxkmebluz (Sydney), Stripe live-mode integration, OpenSanctions API, Resend, Anthropic, Sentry.

Method

Static review of source (apps/web), Postgres schema and RLS policies (supabase/migrations), CI config (.github/), and platform settings. No automated scanner output bundled into this version.

Reviewer

Founder Ben Horne, sole reviewer for this revision. External review by a CREST-accredited tester is scheduled (see Next external test).

Controls reviewed
41
Pass
34
Partial
6
Not applicable
1
V1

V1 Architecture, design and threat modelling.

Architecture choices and trust boundaries. Lightweight modelling rather than a formal STRIDE artefact at this revision.

V1.1 Secure development lifecycle is documented and followed
Partial
CLAUDE.md captures the per-feature workflow (author, dev-test on localhost:3000, build, push). No formal SSDLC document. Treated as Partial.
V1.2 Components and trust boundaries are documented
Pass
Sub-processor map at /sub-processors and the control inventory at /security name every boundary (Caltury app, Supabase, Stripe, OpenSanctions, Anthropic, Resend, Sentry).
V1.4 Sensitive data flows are documented
Pass
Privacy Policy v5 documents the data flow for every category of personal information; /security and /sub-processors corroborate.
V1.5 Threat model exists for the authentication flows
Partial
Auth flows and known threats described informally in code comments (apps/web/src/lib/auth/) and in the Supabase Auth + Caltury invite-token model. No formal STRIDE document.
V2

V2 Authentication.

Sign-in paths, password handling, MFA, account-recovery flows. Supabase Auth handles credential storage; Caltury controls the surrounding flows.

V2.1 Passwords are hashed with a memory-hard or adaptive algorithm
Pass
Supabase Auth stores bcrypt-hashed credentials per its documented implementation. Caltury never sees plaintext.
V2.2 Multi-factor authentication is supported
Pass
TOTP MFA available at /dashboard/settings/security via Supabase Auth Factors API.
V2.3 Account recovery uses verified single-use tokens
Pass
Password reset goes through Supabase Auth verified single-use tokens delivered over branded SMTP.
V2.4 Authentication errors do not enumerate accounts
Pass
Sign-in and reset paths return generic messaging; Supabase Auth's response shape does not differentiate unknown email vs wrong password to the client.
V2.5 Rate limiting and brute-force protection on credential paths
Pass
Per-IP throttle on sign-up backed by Upstash Redis when configured; Supabase Auth rate limits documented in the dashboard with anomaly logs.
V3

V3 Session management.

Session lifecycle, cookie flags, token storage, sign-out.

V3.1 Session tokens are unique, unpredictable, and bound to the user
Pass
Supabase Auth JWT + refresh-token rotation. Caltury does not mint its own session tokens.
V3.2 Session cookies are Secure, HttpOnly, SameSite
Pass
Cookies set by @supabase/ssr with Secure + HttpOnly + SameSite=Lax. Cookie configuration centralised in apps/web/src/lib/supabase/server.ts.
V3.3 Sign-out invalidates the session on the server side
Pass
Sign-out calls supabase.auth.signOut() which invalidates the refresh token; subsequent cookie use is rejected at the SSR boundary.
V3.4 Session timeout enforced on inactivity
Pass
Supabase Auth default access-token lifetime + refresh rotation; long-lived refresh tokens are rotated on each refresh and revocable from the dashboard.
V4

V4 Access control.

Authorisation enforcement at the database layer and the route layer, tenant isolation, privilege separation.

V4.1 The principle of least privilege is enforced
Pass
Row-level security on every public table. DELETE permission revoked on every audit-bearing table (migration 0037). Service-role gated for audit inserts only.
V4.2 Direct object references are protected against horizontal access
Pass
Postgres RLS policies are org-scoped; a code bug cannot leak across tenants because policy enforcement is below the application.
V4.3 Administrative interfaces are protected by additional controls
Pass
Supabase admin console requires founder account + TOTP MFA. No alternative admin path is exposed in the application.
V5

V5 Validation, sanitisation and encoding.

Input handling, output encoding, deserialisation safety.

V5.1 All user input is validated server-side against a positive schema
Pass
Server actions and route handlers parse via Zod schemas before any DB write; rejections return structured field-level errors.
V5.2 Output to HTML is context-aware encoded
Pass
React JSX auto-encodes. Markdown rendering uses an allow-list sanitiser; HTML body in auto_generated_guides is constrained by the generation prompt to a restricted tag set.
V5.3 CSV exports neutralise spreadsheet formula injection
Pass
Leading =, +, -, @, tab and CR characters prefixed with a single quote in apps/web/src/lib/csv.ts so a malicious customer-typed value cannot become a formula in an exported file.
V5.4 SQL is parameterised, never concatenated
Pass
All DB access via supabase-js or @supabase/ssr; raw SQL only in migration files which take no user input.
V7

V7 Error handling and logging.

Error visibility, log content, PII handling, audit trail.

V7.1 Errors do not leak sensitive information in responses
Pass
Server-action error envelopes return generic messages to the client; Sentry receives the rich error with PII scrubbed at SDK level.
V7.2 Audit trail captures security-relevant events
Pass
Append-only audit_log table with 25+ event types covering enrolment, KYC, sanctions, SMR / TTR / IFTI, training and reviewer workflows. Insert-only via service role.
V7.3 Logs are protected from tampering
Partial
DELETE and UPDATE revoked on audit_log via migration 0037. Inserts gated through service role. Cryptographic hash-chain on audit rows is a separate work-stream not landed at the date of this audit.
V7.4 PII is not logged in cleartext to monitoring
Pass
Sentry beforeSend recursively scrubs email, ABN, DOB, address, name, passport and drivers_licence from event extras, tags, contexts, breadcrumbs and request data.
V8

V8 Data protection.

Encryption at rest and in transit, data classification, retention.

V8.1 Sensitive data is encrypted at rest
Pass
Postgres TDE on Supabase Pro (AES-256). Object storage inherits the same at-rest encryption.
V8.2 Sensitive data is encrypted in transit
Pass
TLS 1.3 on all client traffic, server-to-server, and sub-processor traffic. HSTS preload-ready header (max-age=63072000; includeSubDomains; preload).
V8.3 Identity documents not stored on Caltury
Pass
Stripe Identity holds raw ID images. Caltury stores the verification result and a Stripe reference only.
V8.4 Data retention is bounded and documented
Pass
7-year retention per AML/CTF Act ch 11. Each audit-bearing row carries retain_until; vault flags rows that cross the window. Documented at /security and /privacy.
V9

V9 Communication.

Channel security, certificate handling, header hardening.

V9.1 Only modern TLS versions are accepted
Pass
Vercel edge terminates TLS 1.3 (1.2 minimum). Supabase enforces TLS 1.2+ on direct DB connections.
V9.2 Sensitive headers are set
Pass
next.config.ts ships HSTS preload-ready, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, Permissions-Policy locked down.
V9.3 Content-Security-Policy locked to named upstreams
Pass
script-src, style-src, img-src, font-src and connect-src restricted to Stripe, Supabase, OpenSanctions, Anthropic, Resend. No wildcards, no inline-eval shortcuts.
V10

V10 Malicious code.

Build supply-chain, dependency hygiene, code provenance.

V10.1 Dependency updates are automated and reviewed
Pass
.github/dependabot.yml runs weekly against npm + GitHub Actions ecosystems; every Dependabot PR is reviewed in writing.
V10.2 Static analysis runs on every change
Partial
TypeScript strict mode + ESLint lint gates run on every change locally and in Vercel build. CodeQL static analysis requires GitHub Advanced Security on private repos and is not currently subscribed. Planned to land alongside the first external pen-test work or a GHAS upgrade, whichever sooner.
V10.3 Software bill of materials is generated
Partial
pnpm-lock.yaml is the authoritative dependency lockfile. A formal CycloneDX SBOM is not generated at this revision; planned to land alongside the first external pen-test work.
V10.4 Code signing of release artefacts
N/A
Caltury ships as a hosted web application, not installable binaries. No release-artefact signing surface to control.
V12

V12 Files and resources.

File upload handling, file storage isolation, malware controls.

V12.1 File uploads are size- and type-constrained
Pass
Upload paths (reviewer evidence, training certificates) validate MIME and enforce size caps server-side before the storage write.
V12.2 User uploads are isolated by tenant
Pass
Supabase Storage paths are scoped under org_id with RLS policies that mirror the database-layer org policies.
V12.3 Uploaded files are scanned or sandboxed
Partial
No automated malware scanner today. The accepted set is restricted to PDF and image MIME types and files are never executed server-side. AV scanning planned at first enterprise customer.
V13

V13 API and web services.

Public API surface, rate limits, schema enforcement.

V13.1 Public endpoints are documented
Pass
Public endpoints are limited to /api/health and /api/status; both documented inline in their route handlers and surfaced from /security/incident-response and /status.
V13.2 Webhook handlers verify signatures
Pass
Stripe billing and Stripe Identity webhooks verify the signature header against the signing secret in apps/web/src/app/api/stripe-billing and stripe-identity. Replays handled idempotently via a dedicated event-id ledger (migration 0043).
V13.3 Authenticated APIs enforce per-user authorisation
Pass
Server actions and authenticated route handlers wrap in requireUserAndOrg() which resolves the session, loads the org-scoped profile, and applies the RLS context.
Honest section

Limitations of this self-audit.

A self-audit is the lowest-confidence form of security assurance. The list below states what it cannot do so a procurement lead can weight the result accordingly.

  • Not externally validated
    No CREST-accredited tester has reviewed this revision. A clean self-audit row is the founder's assessment, not an independent finding.
  • No live attacker emulation
    Burp Suite, ZAP, Nuclei templates and similar dynamic tooling were not run as part of this revision. Application logic flaws that depend on traffic-level interaction will not have been surfaced.
  • Static review only
    Methodology is reading source code + Postgres schema + RLS policies + CI config + platform settings. No interactive testing of a running instance was part of the score.
  • Sole-reviewer bias
    The founder wrote the code and reviewed the code. Independent eyes catch what familiar eyes miss. An external review is scheduled (see Next external test).
  • ASVS L3 controls not in scope
    This pass targeted Level 2. L3 controls (formal threat modelling artefacts, signed software bill of materials, full cryptographic key custody documentation) are not part of this version.
Next external test

Independent CREST review scheduled.

Trigger
First enterprise customer
or 1 January 2027,
whichever sooner.

A CREST-accredited external penetration test is on the budget for the first enterprise customer engagement, or latest 1 January 2027 regardless of revenue. Scope will cover the same surface this self-audit reviewed plus authenticated application logic, with the report shared with that customer and published in summary form at /security. In the meantime, customer-initiated coordinated tests are welcomed at no charge through security@caltury.com.au.

Procurement-grade detail

Email support@caltury.com.au.

For the full ASVS work-paper, sub-processor DPAs, pen-test rules of engagement, or any specific control evidence. Founder replies in writing.