diff --git a/.gitignore b/.gitignore index b7205fb0..71a9d48f 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,8 @@ stirling-pdf-*.png settings-*.png layout-*.png audit_report.md +apps/docs/SECURITY_AUDIT_REPORT.md +docs/QA_REPORT*.md .worktrees/ .release-version .models diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 31ab1058..00000000 --- a/CLAUDE.md +++ /dev/null @@ -1,162 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## What Is SnapOtter - -Open-source, self-hostable image manipulation suite (51 tools). Single Docker container, no external services. Dual-licensed AGPLv3 / commercial. - -## Monorepo Layout - -``` -apps/web/ # Main React SPA (port 1349) -apps/api/ # Fastify API server (port 13490) -apps/landing/ # Marketing site (Vite + React, deployed to Cloudflare Pages) -apps/docs/ # VitePress documentation site (deployed to Cloudflare Pages) -packages/shared/ # Constants, types, i18n, permissions -- consumed by all apps -packages/image-engine/ # Sharp-based image processing pipeline -packages/ai/ # Python sidecar bridge for ML models -``` - -## Tech Stack - -| Layer | Technology | -|-------|-----------| -| Frontend | React 19, Vite 6, Tailwind CSS 4, Zustand, react-router-dom v7 | -| Backend | Fastify 5, tsx (no compile step in dev), Sharp | -| Database | SQLite via Drizzle ORM (better-sqlite3) | -| AI/ML | Python sidecar (rembg, RealESRGAN, PaddleOCR, MediaPipe, LaMa) | -| Docs | VitePress | -| Testing | Vitest (unit/integration), Playwright (e2e) | -| CI/CD | GitHub Actions, semantic-release, Docker multi-arch | -| Linting | Biome (format + lint in one pass) | - -## Commands - -```bash -pnpm dev # Start all dev servers (web on :1349, api on :13490) -pnpm build # Build all workspaces (turbo, packages first) -pnpm typecheck # TypeScript check across monorepo -pnpm lint # Biome lint + format check -pnpm lint:fix # Biome auto-fix -pnpm test # All Vitest tests (unit + integration) -pnpm test:unit # Unit tests only -pnpm test:integration # Integration tests only (full API) -pnpm test:e2e # Playwright e2e tests (main app) -pnpm test:e2e:landing # Playwright e2e tests (landing site) -pnpm test:e2e:docs # Playwright e2e tests (docs site) -pnpm test:coverage # Tests with V8 coverage report - -# Run a single test file -pnpm vitest run tests/unit/my-test.test.ts -pnpm vitest run tests/integration/my-test.test.ts - -# Run a single e2e spec -pnpm playwright test tests/e2e/my-test.spec.ts - -# Database migrations (run from apps/api/) -cd apps/api && npx drizzle-kit generate # Generate from schema changes -cd apps/api && npx drizzle-kit migrate # Apply pending migrations -``` - -## Key Conventions - -- **Simplicity over complexity** -- do not over-engineer -- **Double quotes**, **semicolons**, **2-space indent** (enforced by Biome) -- **ES modules** in all workspaces (`"type": "module"`) -- Conventional commits for semantic-release (`feat:`, `fix:`, `refactor:`, `docs:`, `test:`, `chore:`) -- Zod for all API input validation -- i18n strings in `packages/shared/src/i18n/en.ts` (reference locale, ~1500 keys) -- All user-facing strings use `useTranslation()` hook from `apps/web/src/contexts/i18n-context.tsx` -- Use `t.section.key` for string references, `format(t.key, { var })` for interpolation, `plural(n, one, other)` for pluralization -- Tailwind uses logical properties for RTL support (`ms-` not `ml-`, `text-start` not `text-left`) -- 21 supported languages. Adding a new locale: create `packages/shared/src/i18n/.ts` typed as `TranslationKeys`, add to `SUPPORTED_LOCALES` in `index.ts` - -## Architecture: How a Tool Works - -A "tool" is defined in three places that share the same `toolId` string: - -1. **Shared metadata** (`packages/shared/src/constants.ts`) -- `TOOLS[]` array with id, name, description, category, icon, route. Single source of truth for tool metadata consumed by both frontend and backend. - -2. **API route** (`apps/api/src/routes/tools/.ts`) -- Processing logic. Most tools use the `createToolRoute` factory (`apps/api/src/routes/tool-factory.ts`), which handles multipart parsing, file validation, format decoding (HEIC/RAW/PSD), SVG sanitization, EXIF auto-orientation, Zod settings validation, output saving, preview generation, file versioning, and analytics. A simple tool is ~30 lines: a Zod schema + a process function that calls `@snapotter/image-engine`. Routes are registered in `apps/api/src/routes/tools/index.ts`. - -3. **Frontend settings** (`apps/web/src/components/tools/-settings.tsx`) -- UI component for the tool's settings panel. Registered in `apps/web/src/lib/tool-registry.tsx` with a `displayMode` (side-by-side, before-after, live-preview, interactive-crop, interactive-eraser, no-dropzone, custom-results). - -### Adding a New Tool - -1. Add tool definition to `TOOLS[]` in `packages/shared/src/constants.ts` -2. Create `apps/api/src/routes/tools/.ts` -- export a `register(app)` function that calls `createToolRoute(app, { toolId, settingsSchema, process })` -3. Add to the registration array in `apps/api/src/routes/tools/index.ts` -4. Create `apps/web/src/components/tools/-settings.tsx` -5. Add lazy import + registry entry in `apps/web/src/lib/tool-registry.tsx` -6. Add i18n strings in `packages/shared/src/i18n/en.ts` - -### Request Lifecycle - -**Standard tools**: Frontend sends `POST /api/v1/tools/:toolId` (multipart with file + JSON settings) via XHR with upload progress. The `createToolRoute` factory processes and returns `{jobId, downloadUrl, originalSize, processedSize}`. - -**AI tools**: Do NOT use `createToolRoute` for their HTTP route. They return `202 Accepted` immediately, process asynchronously via the Python sidecar, and stream progress via SSE (`EventSource` on `/api/v1/jobs/:jobId/progress`). They still register a sync wrapper via `registerToolProcessFn()` for pipeline/batch reuse. - -### Pipeline System - -Pipelines chain tools: each step's output buffer feeds the next step's input. The tool registry (`toolRegistry` Map) enables pipelines and batch processing to call any registered tool's process function without duplicating logic. CRUD stored in `pipelines` DB table as JSON. Batch processing uses `p-queue` concurrency and returns a ZIP via `archiver`. - -## AI Sidecar (packages/ai/) - -Two-tier Python execution: - -1. **Persistent dispatcher** (primary): Long-lived Python process (`packages/ai/python/dispatcher.py`) communicating via JSON over stdio. Node sends `{id, script, args}` on stdin, Python responds `{id, stdout, exitCode}` on stdout. Progress events stream on stderr as `{progress, stage}`. Pre-imports heavy ML libraries at startup to eliminate cold-start latency. Auto-restarts after 50 requests to prevent memory leaks. - -2. **Per-request fallback**: Fresh Python process per request if dispatcher is unavailable. Exponential backoff with crash recovery (5 crashes in 60s = permanent disable). - -AI tools require model bundles defined in `packages/shared/src/features.ts` (`FEATURE_BUNDLES`). The API checks `isToolInstalled()` before processing; the frontend shows an install prompt if not installed. - -## Image Engine (packages/image-engine/) - -`processImage(input, operations[], outputFormat?)` pipes a buffer through named operations. Each operation is a pure function `(Sharp, options) => Promise`. The `OPERATION_MAP` in `engine.ts` dispatches by string key. Individual operations are also exported for direct use by API routes. - -## Auth and Permissions - -Session-based auth with scrypt password hashing. Three built-in roles (`admin`, `editor`, `user`) with 14 granular permissions defined in `packages/shared/src/permissions.ts`. Custom roles stored in `roles` table. API keys (prefixed `si_`) carry optional scoped permissions that intersect with user role permissions. Auth can be disabled entirely (synthetic anonymous user with `user` role). - -## Frontend State - -14 Zustand stores in `apps/web/src/stores/`. The central one is `file-store.ts` managing `FileEntry[]` with blob URLs, processing status, and batch ZIP. Key hooks: `use-tool-processor.ts` (single-file XHR + batch + SSE progress) and `use-pipeline-processor.ts`. - -All page components are `lazy()`-loaded. The key route is `/:toolId` rendering `ToolPage` (`apps/web/src/pages/tool-page.tsx`), which looks up metadata from shared constants and the frontend tool registry. - -## Database - -SQLite via Drizzle ORM. Schema: `apps/api/src/db/schema.ts`. Migrations in `apps/api/drizzle/`. - -Tables: users, teams, sessions, settings (key-value), jobs, apiKeys, pipelines, auditLog, roles, userFiles. - -## Testing - -- `tests/unit/` -- Vitest unit tests -- `tests/integration/` -- Vitest integration tests (one per tool/feature, full API) -- `tests/e2e/` -- Playwright specs (Chromium primary, Firefox/WebKit for cross-browser) -- `tests/fixtures/` -- Small test images in various formats - -Vitest uses single-fork pool (shared SQLite connection), 30s timeouts, and injects test env vars. Playwright auth setup project logs in and saves storage state; spins up both dev servers with a fresh test DB. - -## Pre-commit Hooks - -Husky + lint-staged runs `biome check --write` on staged `*.{ts,tsx,js,jsx,json}` files before every commit. If a commit fails, fix the lint issues rather than bypassing the hook. - -## Do Not Modify Config Files - -Biome, TypeScript, and editor config files are protected by hooks. Fix the code to satisfy the linter/compiler, not the other way around. This prevents a common AI failure mode where rules get weakened instead of code getting fixed. - -## Model Routing for Subagents - -Always use **Opus 4.7, Max Effort, 1M context** for every agent and subagent. No exceptions. No model downgrading. - -## Strategic Compaction - -When context gets large, compact at logical phase boundaries: - -- **Good times to compact**: After research and before planning. After debugging and before implementing the fix. After completing a major feature. -- **Bad times to compact**: Mid-implementation. While actively debugging. During a multi-step refactor. -- **Survives compaction**: This CLAUDE.md, active tasks, git state, memory files -- **Lost on compaction**: Intermediate reasoning, file contents previously read, conversation flow diff --git a/SECURITY_AUDIT_REPORT.md b/SECURITY_AUDIT_REPORT.md deleted file mode 100644 index 8c6b54da..00000000 --- a/SECURITY_AUDIT_REPORT.md +++ /dev/null @@ -1,288 +0,0 @@ -# SnapOtter Security Audit Report - -**Date:** 2026-05-13 -**Version:** 1.16.0 -**Commit:** bc0cac4 (baseline) -**Auditor:** Claude (Principal Application Security Engineer) -**Scope:** Full codebase + production Docker container - -## Executive Summary - -- Total findings: 45 -- CRITICAL: 1 | HIGH: 10 | MEDIUM: 17 | LOW: 12 | INFO: 5 -- Fixed in this audit: 39 -- Deferred: 6 (with justification) -- Security tests added: 104 unit + 7 integration = 111 total -- SVG attack payload fixtures: 11 - -## Baseline Penetration Test - -Tested against production Docker container. 28 of 30 OWASP attack vectors blocked at baseline. - -| # | Attack | Baseline | After Hardening | -|---|--------|----------|-----------------| -| 10.1 | Path traversal (download) | PASS | PASS | -| 10.2 | Path traversal (upload filename) | PASS | PASS | -| 10.3 | IDOR on user files | PASS | PASS | -| 10.4 | Unauthenticated tool access | PASS | PASS | -| 10.5 | Privilege escalation via registration | PASS | PASS | -| 10.6 | Privilege escalation via self-update | PASS | PASS | -| 10.7 | Session token entropy | PASS | PASS | -| 10.8 | Password hash strength | PASS | PASS | -| 10.9 | SQL injection (login) | PASS | PASS | -| 10.10 | SQL injection (search) | PASS | PASS | -| 10.11 | Command injection (filename) | PASS | PASS | -| 10.12 | Header injection (CRLF) | PASS | PASS | -| 10.13 | Brute force login | FAIL | PASS | -| 10.14 | Account enumeration | PASS | PASS | -| 10.15 | Default credentials | PASS | PASS | -| 10.16 | Debug endpoints | PASS | PASS | -| 10.17 | Directory listing | PASS | PASS | -| 10.19 | Session fixation | PASS | PASS | -| 10.20 | Token reuse after logout | PASS | PASS | -| 10.21 | API key after revocation | PASS | PASS | -| 10.22 | Malicious file upload | PASS | PASS | -| 10.23 | Polyglot file | PASS | PASS | -| 10.24 | Audit log completeness | PASS | PASS | -| 10.25 | No sensitive data in logs | FAIL | PASS | -| 10.26 | SSRF cloud metadata | PASS | PASS | -| 10.27 | SSRF localhost | PASS | PASS | -| 10.28 | SSRF IP encoding bypasses | PASS | PASS | -| 10.29 | SVG XSS (script) | PASS | PASS | -| 10.30 | SVG XXE | PASS | PASS | -| 10.31 | SVG SSRF | PASS | PASS | -| 10.32 | SVG event handler | PASS | PASS | - -## Findings Detail - -### [CRITICAL] C1: drizzle-orm SQL Injection CVE - -- **File:** package.json -- **CWE:** CWE-89 -- **Status:** Fixed -- **Fix:** Updated drizzle-orm 0.38.4 to 0.45.2 - -### [HIGH] H1: Login Rate Limit Default Too High - -- **File:** apps/api/src/lib/env.ts:42 -- **CWE:** CWE-307 -- **Status:** Fixed -- **Impact:** 500 login attempts/min allowed brute force at 720K/day -- **Fix:** Default changed to 30/min - -### [HIGH] H2: Global Rate Limit Disabled - -- **File:** apps/api/src/lib/env.ts:23, apps/api/src/index.ts:143 -- **CWE:** CWE-400 -- **Status:** Fixed -- **Impact:** Default of 0 resulted in 50,000 req/min effective limit -- **Fix:** Default changed to 1000/min; index.ts uses `Math.max(env.RATE_LIMIT_PER_MIN, 1)` - -### [HIGH] H3: SVG Sanitizer Missing Protections - -- **File:** apps/api/src/lib/svg-sanitize.ts -- **CWE:** CWE-79, CWE-611 -- **Status:** Fixed -- **Impact:** CDATA bypass, entity-encoded javascript:, animation-based injection -- **Fix:** Added CDATA stripping, XML entity decoding, ``/``/`