diff --git a/README.md b/README.md index 25a6932..a24180b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,45 @@ +# scan.run + +**scan.run** is a production-oriented SaaS platform for safe, authorized OWASP-focused web vulnerability scanning. + +This repository currently contains the reference architecture, data model, API design, and implementation blueprint for building scan.run from MVP through enterprise scale. + +## What is included + +- Full-stack architecture and service boundaries +- Recommended technology stack +- Multi-tenant database schema (PostgreSQL) +- Public API and admin API design +- Scan queue and worker orchestration design +- Billing and subscription lifecycle +- Domain ownership verification flow +- Role/permission model +- Dashboard/module composition design +- Report data model (management + technical views) +- Deployment architecture and scaling strategy +- Security controls for safe scanning +- Extensible monorepo codebase structure + +## Core pricing model + +1. **Single Scan**: `$5` one-time purchase for one scan credit. +2. **Subscription**: `$10/month` with `5 scans/month`, minimum `12-month` commitment. + +## Product principles + +- Scan only verified/authorized targets +- Isolated scan execution with strict egress controls +- Strong abuse prevention and auditability +- Clear separation of reusable UI templates and business logic +- Modular architecture for adding engines, plans, and enterprise features + +## Documents + +- [`docs/architecture.md`](docs/architecture.md) +- [`docs/database_schema.sql`](docs/database_schema.sql) +- [`docs/api.md`](docs/api.md) +- [`docs/codebase-structure.md`](docs/codebase-structure.md) + # Scan Run Web App A lightweight, production-minded starter web application with: diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..f00b764 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,122 @@ +# scan.run API Design (v1) + +Base URL: `https://api.scan.run/v1` + +## Auth + +- `POST /auth/register` +- `POST /auth/login` +- `POST /auth/refresh` +- `POST /auth/logout` +- `POST /auth/mfa/verify` + +## Tenant and membership + +- `GET /me` +- `GET /tenants` +- `POST /tenants` +- `GET /tenants/{tenantId}/members` +- `POST /tenants/{tenantId}/members` +- `PATCH /tenants/{tenantId}/members/{userId}` +- `DELETE /tenants/{tenantId}/members/{userId}` + +## Projects and domains + +- `GET /tenants/{tenantId}/projects` +- `POST /tenants/{tenantId}/projects` +- `PATCH /tenants/{tenantId}/projects/{projectId}` +- `DELETE /tenants/{tenantId}/projects/{projectId}` +- `GET /tenants/{tenantId}/domains` +- `POST /tenants/{tenantId}/domains` +- `GET /tenants/{tenantId}/domains/{domainId}/verification` +- `POST /tenants/{tenantId}/domains/{domainId}/verification/start` +- `POST /tenants/{tenantId}/domains/{domainId}/verification/complete` + +## Scans + +- `POST /tenants/{tenantId}/scans` + - Validates verified domain + available credits + - Creates scan job and reserve credit ledger entry +- `GET /tenants/{tenantId}/scans` +- `GET /tenants/{tenantId}/scans/{scanId}` +- `POST /tenants/{tenantId}/scans/{scanId}/cancel` +- `POST /tenants/{tenantId}/scans/{scanId}/retry` + +## Reports + +- `GET /tenants/{tenantId}/scans/{scanId}/report/management` +- `GET /tenants/{tenantId}/scans/{scanId}/report/technical` +- `POST /tenants/{tenantId}/scans/{scanId}/report/export` + - body: `{ "format": "pdf" | "json" }` +- `GET /tenants/{tenantId}/exports/{exportId}` + +## Billing + +- `GET /tenants/{tenantId}/billing/summary` +- `POST /tenants/{tenantId}/billing/checkout/single-scan` +- `POST /tenants/{tenantId}/billing/checkout/subscription` +- `GET /tenants/{tenantId}/billing/invoices` +- `GET /tenants/{tenantId}/billing/ledger` +- `POST /tenants/{tenantId}/billing/subscription/cancel` +- `POST /webhooks/stripe` + +## API keys + +- `GET /tenants/{tenantId}/api-keys` +- `POST /tenants/{tenantId}/api-keys` +- `DELETE /tenants/{tenantId}/api-keys/{keyId}` + +## Internal admin API + +- `GET /internal/users` +- `GET /internal/subscriptions` +- `GET /internal/scans` +- `GET /internal/queue/health` +- `POST /internal/scans/{scanId}/retry` +- `GET /internal/billing/payments` +- `GET /internal/taxonomy` +- `PUT /internal/taxonomy` +- `GET /internal/rate-limit/policies` +- `PUT /internal/rate-limit/policies` + +--- + +## Example: create scan + +`POST /tenants/{tenantId}/scans` + +```json +{ + "projectId": "4cf8a75a-8b47-4d21-a4c8-5ad0639f4a11", + "domainId": "8d7fc1e2-b011-4fc4-b0d5-2650d7cb8843", + "scanProfile": "owasp-baseline", + "priority": "normal" +} +``` + +Response: + +```json +{ + "scanId": "0f7aa0f6-21ac-4bc8-8cbc-024f355c2a75", + "status": "queued", + "creditReservation": { + "ledgerEntryId": "6a6a9e75-ef85-4680-9f57-8e16e80b9e33", + "quantity": -1 + } +} +``` + +--- + +## Webhook events to consume + +- `invoice.paid` +- `invoice.payment_failed` +- `checkout.session.completed` +- `customer.subscription.created` +- `customer.subscription.updated` +- `customer.subscription.deleted` + +Idempotency requirement: store processed webhook event IDs and reject duplicates. + diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..25ca524 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,258 @@ +# scan.run Architecture Blueprint + +## 1) Recommended full-stack architecture + +scan.run follows a modular, service-oriented architecture with clear bounded contexts: + +- **Web App (Frontend BFF client)** +- **API Gateway / Edge** +- **Core Platform API** (auth, projects, scans, reports, billing state) +- **Scan Orchestrator Service** (job lifecycle + policy checks) +- **Queue + Worker Fleet** (isolated scanners) +- **Report Service** (materialized report projections + export generation) +- **Admin API** (internal operations) +- **Billing Integrations** (Stripe webhooks + entitlements) +- **Observability + Audit pipeline** + +### Logical flow + +1. User authenticates and verifies domain ownership. +2. User starts scan (credit/subscription entitlement validated). +3. Orchestrator enqueues a scan job and reserves credit. +4. Worker executes in sandbox, stores raw evidence and normalized findings. +5. Report service generates management/technical projections. +6. Dashboard streams status updates and renders report modules. +7. Billing cycle replenishes monthly credits for active subscriptions. + +--- + +## 2) Suggested technology stack + +### Frontend +- **Next.js (App Router) + TypeScript** +- **Tailwind CSS + shadcn/ui** for modular design system +- **TanStack Query** for data fetching/state +- **Zod** for runtime schema validation +- **ECharts/Recharts** for risk visualizations + +### Backend +- **Node.js (NestJS) + TypeScript** +- **PostgreSQL** (primary relational store) +- **Redis** (queue, rate limits, idempotency, cache) +- **BullMQ** (job orchestration) +- **Object Storage (S3-compatible)** for evidence/report artifacts +- **NATS/Kafka** optional for event-driven scale stage + +### Scanning runtime +- **Containerized scanner workers** (e.g., Nuclei, OWASP ZAP automation, custom modules) +- **Kubernetes Jobs** or dedicated worker deployments +- **gVisor/Kata/Firecracker-style isolation** at higher security levels + +### Billing/Auth/Infra +- **Stripe** for subscriptions + one-time payments +- **OIDC-compatible auth** (self-hosted or managed) + JWT/refresh token strategy +- **Terraform** for infrastructure as code +- **OpenTelemetry + Prometheus + Grafana + Loki** for observability + +--- + +## 3) Multi-tenant architecture considerations + +- Every tenant-scoped table includes `tenant_id`. +- Tenant isolation at API policy layer and query layer. +- Optional PostgreSQL RLS in enterprise tier. +- Per-tenant usage/rate-limit quotas. +- Per-tenant encryption context for sensitive artifacts. + +--- + +## 4) Background job architecture + +### Queues +- `scan.requested` +- `scan.dispatch` +- `scan.execute` +- `scan.retry` +- `scan.finalize` +- `report.generate` +- `billing.reconcile` +- `notifications.send` + +### Job states +`queued -> validating -> running -> partial_failure|succeeded|failed -> report_generating -> complete` + +### Retry policy +- Exponential backoff with jitter +- max attempts configurable by failure class +- hard-stop on policy violations (unverified domain, abuse score) + +### Idempotency +- API uses idempotency keys for scan creation and payments +- workers write through immutable execution IDs + +--- + +## 5) Billing/subscription flow + +### Single scan ($5) +1. User purchases `1` credit. +2. Stripe payment success webhook confirms transaction. +3. Credit ledger entry created (`credit +1`). +4. Credit consumed when scan enters `running`. + +### Subscription ($10/month, 5 scans/month, 12-month minimum) +1. User starts subscription checkout. +2. Contract metadata stores minimum term end date. +3. Monthly renewal webhook triggers ledger refill (`+5` monthly credits). +4. Unused monthly credits can be configured as non-rollover (default) or capped rollover. +5. Early cancellation requests marked pending until term end. + +### Credit ledger model +- Never mutate balances directly. +- Derive current balance from immutable ledger entries: + - `grant_subscription_monthly` + - `grant_one_time_purchase` + - `reserve_scan` + - `consume_scan` + - `release_scan` + - `adjustment_admin` + +--- + +## 6) Domain verification flow + +Allowed verification methods: +- **DNS TXT token** (preferred) +- **HTTP file challenge** (`/.well-known/scan-run-verification.txt`) +- **Meta tag challenge** + +Flow: +1. User adds domain/project. +2. System generates signed verification token and challenge instructions. +3. Verification worker validates challenge from multiple resolvers/regions. +4. Domain marked `verified` with verification evidence snapshot. +5. Re-verification required periodically or upon ownership risk signal. + +Policy: scans are blocked unless domain status is `verified` or has explicit admin-approved authorization exception. + +--- + +## 7) User roles & permissions + +### Tenant roles +- **Owner**: full tenant access + billing/admin within tenant +- **Admin**: manage members/projects/scans, no ownership transfer +- **Security Analyst**: run scans/view technical reports/export +- **Manager**: view management reports only, limited exports +- **Billing Manager**: subscriptions/invoices/payment methods +- **Viewer**: read-only dashboard access + +### Internal roles +- **Platform Admin**: all internal controls +- **Support**: read + limited remedial actions +- **Ops**: queue/worker/retry controls +- **Finance Ops**: billing state tools + +Enforce RBAC + optional ABAC policies (resource ownership, environment tags). + +--- + +## 8) Dashboard structure (modular templates) + +- `/dashboard/overview` + - scan utilization, credits, risk trend, recent jobs +- `/dashboard/projects` + - domains, verification status, project settings +- `/dashboard/scans` + - scan list, status timeline, filters +- `/dashboard/reports/:scanId` + - **Management View** module set + - **Technical View** module set +- `/dashboard/billing` + - plan, term end, invoices, payment methods, credit ledger +- `/dashboard/settings` + - members, API keys, notifications +- `/internal/*` + - user/subscription activity, queue health, policy config, taxonomy editor + +UI modularity: +- Widget registry + report section schema +- Renderers separated from API data loaders +- Feature flags for progressive rollout + +--- + +## 9) Report model (management + technical) + +### Management view includes +- overall risk score and rating (Critical/High/Medium/Low) +- top prioritized issues +- business impact summary +- remediation roadmap by effort/impact +- trend vs previous scans + +### Technical view includes +- finding ID and fingerprint +- affected asset/URL and endpoint metadata +- request/response evidence pointers +- OWASP Top 10 mapping +- CWE/CVSS mapping and vectors +- severity, confidence, exploitability +- remediation guidance and references + +### Export formats +- JSON (full machine-readable) +- PDF (management-friendly + technical appendix) + +--- + +## 10) Deployment architecture for scale + +### Core runtime +- Kubernetes across multiple node pools: + - web/api pool + - background services pool + - isolated scanner worker pool +- Managed PostgreSQL with read replicas +- Redis cluster/sentinel +- Object storage with lifecycle + immutability controls + +### Networking & security +- WAF + API gateway at edge +- strict worker egress policies (allowlist + DNS controls) +- secret manager + short-lived credentials +- private service mesh mTLS + +### Scaling strategy +- Horizontal pod autoscaling by queue depth and CPU/memory +- worker autoscaling by `scan.execute` backlog +- partition queues by priority/tenant tier + +--- + +## 11) Security considerations (safe scanning) + +- Mandatory domain verification or explicit documented authorization +- Target allow/deny policy engine (block internal/private CIDRs by default) +- SSRF and lateral movement controls in scanner runtime +- Isolated per-job execution context and ephemeral filesystems +- Sanitization of captured evidence and secrets detection/redaction +- Signed artifact URLs with short TTL +- Full audit logs for user/admin/worker actions +- Abuse detection (velocity, anomaly, suspicious targets) +- Rate limiting per IP, user, tenant, and endpoint class +- Legal safeguards: terms acceptance + scanning authorization attestations + +--- + +## 12) MVP -> Growth -> Enterprise path + +### MVP +- Single region, one primary scan engine, essential RBAC, Stripe billing + +### Growth +- multiple scan engines, event bus, richer reporting templates, multi-region read scaling + +### Enterprise +- SSO/SAML, SCIM, custom retention, dedicated workers/VPC, RLS, policy packs, SIEM integrations + diff --git a/docs/codebase-structure.md b/docs/codebase-structure.md new file mode 100644 index 0000000..d7258aa --- /dev/null +++ b/docs/codebase-structure.md @@ -0,0 +1,82 @@ +# scan.run Recommended Codebase Structure + +```text +scan.run/ + apps/ + web/ # Next.js frontend + api/ # NestJS public API + BFF endpoints + admin/ # Internal admin UI (can be merged with web + route guards) + worker/ # Scan worker runtime entrypoints + scheduler/ # Cron-like scheduled jobs (credit refills, re-verification) + + packages/ + ui/ # Reusable component library + dashboard templates + config/ # Shared lint/tsconfig/build presets + auth/ # Auth utilities, JWT, RBAC policy helpers + billing/ # Stripe adapters + entitlement logic + scanning-core/ # Scan orchestration contracts and engine adapters + reporting/ # Report projections, scoring, export renderers + taxonomy/ # OWASP/CWE/CVSS mapping rules + database/ # Prisma/SQL migrations + repository helpers + observability/ # logging, tracing, metrics wrappers + shared-types/ # zod schemas + TS types for API contracts + + infrastructure/ + terraform/ + environments/ + dev/ + staging/ + prod/ + modules/ + k8s/ + postgres/ + redis/ + storage/ + networking/ + kubernetes/ + base/ + overlays/ + dev/ + staging/ + prod/ + + docs/ + architecture.md + database_schema.sql + api.md + runbooks/ + incident-response.md + scanner-failure-retry.md + billing-reconciliation.md + + .github/ + workflows/ + ci.yml + cd.yml + security.yml + + scripts/ + bootstrap.sh + seed-dev.ts + rotate-keys.ts +``` + +## Modularity and extensibility patterns + +1. **Scanner plugin interface** in `packages/scanning-core`: + - each engine implements `prepare`, `execute`, `normalizeFindings`, `collectEvidence`. +2. **Report template registry** in `packages/reporting`: + - register management/technical widget templates via metadata. +3. **Dashboard widget composition** in `packages/ui`: + - route-level configs define active widgets, data source bindings, and access policy. +4. **Entitlement engine** in `packages/billing`: + - plan rules are data-driven to support future tiers. +5. **Policy enforcement middleware** in API: + - centralized checks for verification, limits, and role permissions. + +## CI/CD recommendations + +- PR checks: lint, unit tests, integration tests, migration checks, SAST, dependency audit +- Deploy gates: staging smoke scans, synthetic transaction checks, queue health checks +- Progressive rollouts: canary release for orchestrator and worker components + diff --git a/docs/database_schema.sql b/docs/database_schema.sql new file mode 100644 index 0000000..3186a5b --- /dev/null +++ b/docs/database_schema.sql @@ -0,0 +1,217 @@ +-- scan.run reference PostgreSQL schema + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE TABLE tenants ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + name TEXT NOT NULL, + slug TEXT UNIQUE NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + email CITEXT UNIQUE NOT NULL, + password_hash TEXT, + auth_provider TEXT NOT NULL DEFAULT 'local', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + last_login_at TIMESTAMPTZ +); + +CREATE TABLE tenant_members ( + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + role TEXT NOT NULL CHECK (role IN ('owner','admin','security_analyst','manager','billing_manager','viewer')), + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY (tenant_id, user_id) +); + +CREATE TABLE projects ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + name TEXT NOT NULL, + created_by UUID REFERENCES users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE domains ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + hostname TEXT NOT NULL, + verification_status TEXT NOT NULL DEFAULT 'pending' CHECK (verification_status IN ('pending','verified','failed','revoked')), + verification_method TEXT CHECK (verification_method IN ('dns_txt','http_file','meta_tag','manual_exception')), + verified_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (tenant_id, hostname) +); + +CREATE TABLE domain_verification_challenges ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + domain_id UUID NOT NULL REFERENCES domains(id) ON DELETE CASCADE, + method TEXT NOT NULL CHECK (method IN ('dns_txt','http_file','meta_tag')), + token TEXT NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + validated_at TIMESTAMPTZ, + validation_evidence JSONB, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE subscriptions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + provider TEXT NOT NULL DEFAULT 'stripe', + provider_customer_id TEXT NOT NULL, + provider_subscription_id TEXT, + plan_code TEXT NOT NULL CHECK (plan_code IN ('single_scan','monthly_5_commit_12m')), + status TEXT NOT NULL CHECK (status IN ('trialing','active','past_due','canceled','incomplete','paused')), + contract_start_at TIMESTAMPTZ, + minimum_term_end_at TIMESTAMPTZ, + current_period_start TIMESTAMPTZ, + current_period_end TIMESTAMPTZ, + cancel_at_period_end BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE credit_ledger ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + subscription_id UUID REFERENCES subscriptions(id), + scan_id UUID, + entry_type TEXT NOT NULL CHECK (entry_type IN ( + 'grant_subscription_monthly', + 'grant_one_time_purchase', + 'reserve_scan', + 'consume_scan', + 'release_scan', + 'adjustment_admin' + )), + quantity INTEGER NOT NULL, + metadata JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE payments ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + provider TEXT NOT NULL DEFAULT 'stripe', + provider_payment_id TEXT NOT NULL, + amount_cents INTEGER NOT NULL, + currency TEXT NOT NULL DEFAULT 'USD', + status TEXT NOT NULL, + metadata JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE scan_jobs ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, + domain_id UUID NOT NULL REFERENCES domains(id) ON DELETE CASCADE, + requested_by UUID REFERENCES users(id), + status TEXT NOT NULL CHECK (status IN ( + 'queued','validating','running','retrying','partial_failure','succeeded','failed','canceled','report_generating','complete' + )), + engine TEXT NOT NULL, + priority SMALLINT NOT NULL DEFAULT 5, + queued_at TIMESTAMPTZ NOT NULL DEFAULT now(), + started_at TIMESTAMPTZ, + finished_at TIMESTAMPTZ, + failure_reason TEXT, + metadata JSONB NOT NULL DEFAULT '{}'::jsonb +); + +CREATE TABLE scan_executions ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + scan_job_id UUID NOT NULL REFERENCES scan_jobs(id) ON DELETE CASCADE, + attempt_number INTEGER NOT NULL, + worker_id TEXT, + status TEXT NOT NULL CHECK (status IN ('running','succeeded','failed','timed_out','aborted')), + started_at TIMESTAMPTZ NOT NULL DEFAULT now(), + finished_at TIMESTAMPTZ, + runtime_metadata JSONB NOT NULL DEFAULT '{}'::jsonb, + UNIQUE (scan_job_id, attempt_number) +); + +CREATE TABLE findings ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + scan_job_id UUID NOT NULL REFERENCES scan_jobs(id) ON DELETE CASCADE, + fingerprint TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT, + severity TEXT NOT NULL CHECK (severity IN ('critical','high','medium','low','info')), + confidence TEXT CHECK (confidence IN ('high','medium','low')), + owasp_category TEXT, + cwe_id TEXT, + cvss_vector TEXT, + cvss_score NUMERIC(3,1), + exploitability_score NUMERIC(3,1), + affected_url TEXT, + evidence_ref TEXT, + remediation TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (scan_job_id, fingerprint) +); + +CREATE TABLE report_snapshots ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + scan_job_id UUID NOT NULL REFERENCES scan_jobs(id) ON DELETE CASCADE, + management_report JSONB NOT NULL, + technical_report JSONB NOT NULL, + generated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + UNIQUE (scan_job_id) +); + +CREATE TABLE report_exports ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + scan_job_id UUID NOT NULL REFERENCES scan_jobs(id) ON DELETE CASCADE, + format TEXT NOT NULL CHECK (format IN ('pdf','json')), + artifact_uri TEXT NOT NULL, + checksum_sha256 TEXT, + created_by UUID REFERENCES users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE api_keys ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE, + name TEXT NOT NULL, + key_prefix TEXT NOT NULL, + key_hash TEXT NOT NULL, + scopes TEXT[] NOT NULL DEFAULT '{}', + last_used_at TIMESTAMPTZ, + expires_at TIMESTAMPTZ, + created_by UUID REFERENCES users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE audit_logs ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + tenant_id UUID REFERENCES tenants(id) ON DELETE SET NULL, + actor_user_id UUID REFERENCES users(id) ON DELETE SET NULL, + actor_type TEXT NOT NULL CHECK (actor_type IN ('user','system','worker','admin')), + action TEXT NOT NULL, + resource_type TEXT NOT NULL, + resource_id TEXT, + ip_address INET, + user_agent TEXT, + metadata JSONB NOT NULL DEFAULT '{}'::jsonb, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE TABLE rate_limit_counters ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + scope TEXT NOT NULL, + scope_key TEXT NOT NULL, + window_start TIMESTAMPTZ NOT NULL, + request_count INTEGER NOT NULL, + UNIQUE (scope, scope_key, window_start) +); + +CREATE INDEX idx_scan_jobs_tenant_status ON scan_jobs (tenant_id, status); +CREATE INDEX idx_findings_scan_job ON findings (scan_job_id, severity); +CREATE INDEX idx_credit_ledger_tenant_created ON credit_ledger (tenant_id, created_at DESC); +CREATE INDEX idx_audit_logs_tenant_created ON audit_logs (tenant_id, created_at DESC);