From 6e082ce58fed891aa82652740d44cac85f13d201 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 23:13:53 +0000 Subject: [PATCH] =?UTF-8?q?Stream=20D:=20Security=20hardening=20=E2=80=94?= =?UTF-8?q?=2010=20fixes=20from=20QA=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SEC-05: Fail fast if SECRET_KEY is default 'change-me-in-production' - SEC-06: CORS allow_credentials=False (Bearer tokens don't need it) - SEC-08: Input validation on auth schemas (min/max length, username pattern) - SEC-10: .dockerignore files (root, backend, mcp-connector) - SEC-11: Hardcode ALGORITHM='HS256' as constant, remove from settings - CQ-01: Gate crawl scheduler on CRAWL_ENABLED env var (default false) - OS-02: CONTRIBUTING.md - OS-03: CHANGELOG.md with v1.0.0 entry - setup.ps1: Add API_TOKEN generation (Windows parity with setup.sh) --- .dockerignore | 12 ++++++++---- CHANGELOG.md | 27 +++++++++++++++++++++++++++ CONTRIBUTING.md | 31 +++++++++++++++++++++++++++++++ backend/.dockerignore | 11 +++++++++++ backend/app/config.py | 2 +- backend/app/main.py | 27 +++++++++++++++++++-------- backend/app/schemas/auth.py | 12 ++++++------ backend/app/utils/auth.py | 3 ++- mcp-connector/.dockerignore | 4 ++++ setup.ps1 | 4 ++++ 10 files changed, 113 insertions(+), 20 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 backend/.dockerignore create mode 100644 mcp-connector/.dockerignore diff --git a/.dockerignore b/.dockerignore index 7a27d03..3ac26d2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,11 @@ .git .env +.env.* +!.env.example +__pycache__ +*.pyc content/ -*.md -LICENSE -setup.sh -setup.ps1 +node_modules/ +dist/ +*.log +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8da9836 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,27 @@ +# Changelog + +All notable changes to Diligence are documented here. + +## [1.0.0] — 2026-06-18 + +Initial open-source release. + +### Features +- Points economy with daily gate, weekly targets, weekly reset +- Science-based onboarding (PAR-Q+, TTM, BREQ-2 motivation profiling) +- Activity logging (manual + Strava OAuth + Polar OAuth sync) +- Food logging with Open Food Facts barcode scanning + USDA FoodData Central +- 90-day structured program tracking +- Configurable reward shop +- In-app support chat with Telegram notifications +- 14-tool MCP connector for AI agent integration +- Meal plan system with compliance tracking +- Dynamic integration configuration (11 providers) +- B2A discovery layer (llms.txt, agent-card.json, SKILL.md) + +### Security +- Fernet-encrypted credential storage (HKDF key derivation) +- Signed JWT OAuth state parameters +- MCP connector authentication via API_TOKEN +- First-user auto-admin grant +- Traceback suppression in error responses diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c14bc4a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,31 @@ +# Contributing to Diligence + +Thanks for your interest in contributing! + +## How to contribute + +1. **Open an issue** to discuss the change before starting work +2. **Fork the repo** and create a branch from `main` +3. **Test locally**: `docker compose up -d` and verify all 4 containers start healthy +4. **Submit a PR** with a clear description of what changed and why + +## Development setup + +```bash +git clone https://github.com/DiligenceWorks/Diligence.git +cd Diligence +./setup.sh # Mac/Linux +docker compose up -d +``` + +Open http://localhost and register. First user gets admin automatically. + +## Code style + +- Backend: Python 3.11+, FastAPI, async/await, type hints +- Frontend: React with hooks, no class components +- CSS: use CSS variables from `index.css`, never hardcode colors + +## Questions? + +Open a GitHub Discussion or reach out at hello@diligenceworks.online. diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..3ac26d2 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,11 @@ +.git +.env +.env.* +!.env.example +__pycache__ +*.pyc +content/ +node_modules/ +dist/ +*.log +.DS_Store diff --git a/backend/app/config.py b/backend/app/config.py index 502ebd2..3c2be33 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -10,7 +10,7 @@ class Settings(BaseSettings): # Auth secret_key: str = "change-me-in-production" - algorithm: str = "HS256" + crawl_enabled: bool = False # Set CRAWL_ENABLED=true to start program crawl scheduler access_token_expire_minutes: int = 1440 # 24 hours api_token: str = "" # MCP connector auth — generated by setup.sh diff --git a/backend/app/main.py b/backend/app/main.py index 55a3c36..b06d7c5 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import sys import logging from contextlib import asynccontextmanager from fastapi import FastAPI @@ -27,6 +28,13 @@ async def lifespan(app: FastAPI): logger.error("Could not initialize database after 10 attempts — starting without tables") + # SEC-05: Fail fast if SECRET_KEY not configured + from app.config import get_settings + _s = get_settings() + if _s.secret_key == "change-me-in-production": + logger.error("CRITICAL: SECRET_KEY not set. Run ./setup.sh or set SECRET_KEY in .env") + sys.exit(1) + # Run lightweight migrations for schema changes try: await run_migrations() @@ -41,14 +49,17 @@ async def lifespan(app: FastAPI): except Exception as e: logger.warning(f"Resource seeding failed (non-fatal): {e}") - # Start background crawl queue scheduler + # Start background crawl queue scheduler (gated on CRAWL_ENABLED) crawl_task = None - try: - from app.services.crawl_scheduler import crawl_queue_loop - crawl_task = asyncio.create_task(crawl_queue_loop()) - logger.info("Crawl queue scheduler started") - except Exception as e: - logger.warning(f"Crawl scheduler failed to start (non-fatal): {e}") + if _s.crawl_enabled: + try: + from app.services.crawl_scheduler import crawl_queue_loop + crawl_task = asyncio.create_task(crawl_queue_loop()) + logger.info("Crawl queue scheduler started") + except Exception as e: + logger.warning(f"Crawl scheduler failed to start (non-fatal): {e}") + else: + logger.info("Crawl scheduler disabled (set CRAWL_ENABLED=true to enable)") logger.info("Fitness Rewards backend started") yield @@ -68,7 +79,7 @@ app = FastAPI(title="Fitness Rewards", version="1.0.0", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], - allow_credentials=True, + allow_credentials=False # Bearer tokens don't need credentials mode, allow_methods=["*"], allow_headers=["*"], ) diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py index 5cbd802..9d2b238 100644 --- a/backend/app/schemas/auth.py +++ b/backend/app/schemas/auth.py @@ -1,17 +1,17 @@ from __future__ import annotations -from pydantic import BaseModel +from pydantic import BaseModel, Field class LoginRequest(BaseModel): - username: str - password: str + username: str = Field(min_length=3, max_length=50) + password: str = Field(min_length=8, max_length=128) class RegisterRequest(BaseModel): - username: str - password: str - display_name: str + username: str = Field(min_length=3, max_length=50, pattern=r'^[a-zA-Z0-9_-]+$') + password: str = Field(min_length=8, max_length=128) + display_name: str = Field(min_length=1, max_length=100) email: str | None = None diff --git a/backend/app/utils/auth.py b/backend/app/utils/auth.py index 41ae54a..74e9ba2 100644 --- a/backend/app/utils/auth.py +++ b/backend/app/utils/auth.py @@ -13,6 +13,7 @@ from app.config import get_settings from app.database import get_db settings = get_settings() +ALGORITHM = "HS256" # Hardcoded — not configurable for security oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login", auto_error=False) @@ -27,7 +28,7 @@ def verify_password(plain: str, hashed: str) -> bool: def create_access_token(user_id: str, expires_delta: timedelta | None = None) -> str: expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=settings.access_token_expire_minutes)) payload = {"sub": user_id, "exp": expire} - return jwt.encode(payload, settings.secret_key, algorithm=settings.algorithm) + return jwt.encode(payload, settings.secret_key, algorithm=ALGORITHM) async def get_current_user( diff --git a/mcp-connector/.dockerignore b/mcp-connector/.dockerignore new file mode 100644 index 0000000..dcd44cf --- /dev/null +++ b/mcp-connector/.dockerignore @@ -0,0 +1,4 @@ +.git +.env +__pycache__ +*.pyc diff --git a/setup.ps1 b/setup.ps1 index 98c429d..d9b7e3b 100644 --- a/setup.ps1 +++ b/setup.ps1 @@ -1,4 +1,8 @@ # Diligence — Windows Setup +# Generate API_TOKEN for MCP connector auth +$apiToken = -join ((48..57) + (65..90) + (97..122) | Get-Random -Count 32 | ForEach-Object {[char]$_}) +(Get-Content .env) -replace '^API_TOKEN=.*', "API_TOKEN=$apiToken" | Set-Content .env + Write-Host "`n`e[36m💪 Diligence — Setup`e[0m`n" if (Test-Path .env) {