Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Database Configuration
DATABASE_URL=postgresql://user:password@localhost:5432/fireform
# Example: postgresql://fireform_user:secure_pass@db:5432/fireform_prod

# LLM Service Configuration
LLM_API_KEY=your_ollama_api_key_here
LLM_TIMEOUT=30
LLM_BASE_URL=http://localhost:11434

# Application Configuration
SECRET_KEY=your_secret_key_here_min_32_chars
# Generate with: python -c "import secrets; print(secrets.token_urlsafe(32))"

TEMPLATE_DIR=./templates
UPLOAD_DIR=./uploads

# Logging Configuration
LOG_LEVEL=INFO
SQL_ECHO=false

# CORS Configuration (comma-separated origins)
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080

# Rate Limiting
RATE_LIMIT_ENABLED=true
RATE_LIMIT_PER_MINUTE=60

# Security Headers
HSTS_ENABLED=true
FRAME_DENY=true
61 changes: 61 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Cache pip dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest

- name: Run tests
env:
PYTHONPATH: ${{ github.workspace }}
run: |
pytest tests/ -v --tb=short

lint:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install linter
run: |
python -m pip install --upgrade pip
pip install ruff

- name: Run linter
run: |
ruff check . --select=E,F,I --ignore=E501
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,42 @@ The result is hours of time saved per shift, per firefighter.

Open-Source (DPG): Built 100% with open-source tools to be a true Digital Public Good, freely available for any department to adopt and modify.

## 🚀 Getting Started

### Prerequisites
- Python 3.12+
- Docker (recommended)
- Ollama (for local LLM)

### Local Development

1. Clone the repository:
```bash
git clone https://github.com/fireform-core/FireForm.git
cd FireForm
```

2. Copy the environment template:
```bash
cp .env.example .env
```

3. Generate a secure secret key:
```bash
python -c "import secrets; print(secrets.token_urlsafe(32))"
```

4. Fill in the required values in `.env`

5. Start with Docker:
```bash
docker compose up
```

⚠️ **Never commit `.env` to Git** — it contains secrets.

For production deployment and secrets rotation, see [SECRETS.md](./SECRETS.md).

## 🤝 Code of Conduct

We are committed to providing a friendly, safe, and welcoming environment for all. Please see our [Code of Conduct](CODE_OF_CONDUCT.md) for more information.
Expand Down
167 changes: 167 additions & 0 deletions SECRETS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Secrets Management Guide

This document outlines best practices for managing secrets in FireForm.

## Environment Variables

All secrets are managed via environment variables. **Never commit secrets to Git.**

### Required Variables

See `.env.example` for the complete list of required environment variables.

### Local Development

1. Copy the example file:
```bash
cp .env.example .env
```

2. Fill in actual values:
```bash
# Generate a secure SECRET_KEY
python -c "import secrets; print(secrets.token_urlsafe(32))"
```

3. **Never commit `.env`** — it's in `.gitignore`

---

## Secrets Rotation

### Database Credentials

**When to rotate:** Every 90 days or immediately after suspected compromise

**Steps:**
1. Create new database user with same permissions
2. Update `DATABASE_URL` in production environment
3. Restart application (zero downtime with health checks)
4. Verify new credentials work
5. Revoke old database user

**Example:**
```sql
-- Create new user
CREATE USER fireform_user_new WITH PASSWORD 'new_secure_password';
GRANT ALL PRIVILEGES ON DATABASE fireform TO fireform_user_new;

-- After rotation succeeds
DROP USER fireform_user_old;
```

---

### LLM API Keys

**When to rotate:** Every 90 days or when API key is exposed

**Steps:**
1. Generate new API key from LLM provider dashboard
2. Update `LLM_API_KEY` in production environment
3. Restart application
4. Verify extraction still works
5. Revoke old API key

---

### Application Secret Key

**When to rotate:** Every 180 days or after security incident

**Impact:** Rotating `SECRET_KEY` invalidates all active sessions

**Steps:**
1. Generate new secret key:
```bash
python -c "import secrets; print(secrets.token_urlsafe(32))"
```
2. Update `SECRET_KEY` in production
3. Restart application
4. Notify users they'll need to re-authenticate

---

## Production Deployment

### Environment-Specific Secrets

Use separate secrets for each environment:

- **Development:** `.env` (local only, never committed)
- **Staging:** Staging secrets manager (AWS Secrets Manager, Vault, etc.)
- **Production:** Production secrets manager with audit logging

### CAL FIRE Deployment Checklist

- [ ] Database credentials rotated within last 90 days
- [ ] LLM API key rotated within last 90 days
- [ ] `SECRET_KEY` is cryptographically random (min 32 chars)
- [ ] All secrets stored in approved secrets manager
- [ ] Secrets rotation schedule documented
- [ ] Audit logging enabled for secrets access
- [ ] No secrets in application logs (verified via `api/utils/secrets.py`)

---

## Secrets in Logs

The application automatically redacts secrets from logs using `api/utils/secrets.py`.

**Redacted patterns:**
- API keys
- Passwords
- Tokens
- Database credentials
- Bearer tokens

**Example:**
```python
from api.utils.secrets import sanitize_log_message

message = "Failed to connect with api_key=sk_test_12345"
sanitized, _ = sanitize_log_message(message)
# Output: "Failed to connect with api_key=***REDACTED***"
```

---

## Git Protection

### Pre-commit Hook (Recommended)

Create `.git/hooks/pre-commit`:

```bash
#!/bin/bash
# Prevent committing .env files

if git diff --cached --name-only | grep -q "^.env$"; then
echo "ERROR: Attempting to commit .env file"
echo "Secrets should never be committed to Git"
exit 1
fi
```

Make it executable:
```bash
chmod +x .git/hooks/pre-commit
```

---

## Compliance

### Federal Standards

- **NIST SP 800-53 (AC-2):** Access control for credentials
- **FEMA IS-700:** Incident management security requirements
- **OWASP Top 10:** Sensitive Data Exposure prevention

### Audit Trail

For CAL FIRE production:
- Log all secrets rotation events
- Maintain 1-year audit history
- Alert on rotation failures
- Monthly compliance review
8 changes: 7 additions & 1 deletion api/db/database.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import os
from sqlmodel import create_engine, Session

DATABASE_URL = "sqlite:///./fireform.db"

# SQL echo is opt-in via environment variable — defaults to off in production
# to prevent every SQL query from being printed to stdout.
SQL_ECHO = os.getenv("SQL_ECHO", "false").lower() == "true"

engine = create_engine(
DATABASE_URL,
echo=True,
echo=SQL_ECHO,
connect_args={"check_same_thread": False},
)


def get_session():
with Session(engine) as session:
yield session
7 changes: 4 additions & 3 deletions api/db/models.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
from sqlmodel import SQLModel, Field
from sqlalchemy import Column, JSON
from datetime import datetime
from datetime import datetime, timezone


class Template(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
fields: dict = Field(sa_column=Column(JSON))
pdf_path: str
created_at: datetime = Field(default_factory=datetime.utcnow)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))


class FormSubmission(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
template_id: int
input_text: str
output_pdf_path: str
created_at: datetime = Field(default_factory=datetime.utcnow)
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
5 changes: 4 additions & 1 deletion api/errors/base.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
class AppError(Exception):
"""Base application error with an HTTP status code."""

def __init__(self, message: str, status_code: int = 400):
self.message = message
self.status_code = status_code
self.status_code = status_code
super().__init__(message)
Loading