Compare commits

...

1 Commits

Author SHA1 Message Date
Jarian
9d3f8e46a0 feat: Docker deployment, nginx security config, audit report (#11, #14-#22)
- Add Dockerfile with MediaInfo, non-root user, slim base
- Add docker-compose.yml with env_file and volume mounts
- Add nginx.conf with security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options)
- Hide server version (server_tokens off)
- Add SECURITY.md with full audit report and deployment guide
- Add .dockerignore for clean builds

Closes #11, #14, #21, #22
2026-07-05 08:10:04 +00:00
5 changed files with 186 additions and 0 deletions

12
.dockerignore Normal file
View File

@ -0,0 +1,12 @@
.git
.gitignore
*.md
__pycache__
*.pyc
src/.tvdb_cache
config.json
.env
.dockerignore
Dockerfile
docker-compose.yml
nginx.conf

29
Dockerfile Normal file
View File

@ -0,0 +1,29 @@
FROM python:3.12-slim AS base
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
libmediainfo0v \
zlib1g-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN python -m compileall src/
ARG USER=epmatcher
ARG UID=1000
ARG GID=1000
RUN groupadd -g "$GID" "$USER" && \
useradd -u "$UID" -g "$GID" -m -s /bin/bash "$USER" && \
mkdir -p /app/src/.tvdb_cache && \
chown -R "$USER":"$USER" /app
USER $USER
ENTRYPOINT ["python"]
CMD ["episode_matcher.py"]

60
SECURITY.md Normal file
View File

@ -0,0 +1,60 @@
# Security Audit Report — Episode Matcher
## Fixed Issues
### Critical
- **[CWE-22] Path Traversal in TVDB Cache (#2)** — Sanitized series names, validated cache paths stay within cache directory.
### High
- **[CWE-798] Hardcoded API Key (#3, #4, #5)** — TVDB_API_KEY now read from environment variable. config.json removed from git history, added to .gitignore.
- **[CWE-22] Incomplete .gitignore (#6)** — Added config.json, __pycache__, .env, build artifacts.
- **[CWE-22] Unsafe File Deletion (#8)** — `--auto-delete-duplicates` now requires `--force` flag. Deletions logged to recovery manifest.
### Medium
- **[CWE-561] sys.path Manipulation (#7)** — Added pyproject.toml for proper packaging with editable install support.
- **[CWE-20] Duplicate Detection (#9)** — Added SHA-256 hash comparison alongside duration for duplicate detection.
### Low
- **[CWE-346] Single Format Support (#12)** — Now supports .mkv, .mp4, .avi, .mov, .wmv, .flv, .webm, .m4v.
- **Hardcoded Fallback Ratio (#13)** — MediaInfo fallback ratio configurable via `duration_minutes_per_gb` in config.json.
## Web Security (tv.home.ms / Jellyfin)
The following issues affect the Jellyfin web interface served at tv.home.ms.
Fixes are in `nginx.conf` (reverse proxy configuration) or require Jellyfin upstream changes.
### Fixed via nginx.conf
| Issue | Severity | Fix |
|-------|----------|-----|
| #14 Missing security headers | High | CSP, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy added |
| #21 Server version exposed | Low | `server_tokens off` hides nginx version |
| #22 Response time leaked | Low | `more_clear_headers` directive (requires headers-more module) |
### Requires Jellyfin Upstream Fix
| Issue | Severity | Type | Notes |
|-------|----------|------|-------|
| #15 Console error (scroll behavior) | Medium | BROKEN | Jellyfin JS bundle bug — report to Jellyfin |
| #16 Buttons missing labels | Medium | A11Y | Jellyfin UI — needs aria-label on icon buttons |
| #17 Heading order incorrect | Medium | A11Y | Jellyfin HTML structure — H3 before H1 |
| #18 Inputs lack labels | Medium | A11Y | Jellyfin login form — needs `<label>` elements |
| #19 No skip navigation | Low | A11Y | Jellyfin UI — needs skip-to-content link |
| #20 Remember Me default | Low | SEC | Jellyfin login form — should default to unchecked |
### Issue #1 (Original)
The DP algorithm not finding optimal episode matches. Addressed by the strategies module
(`src/Matcher/strategies.py`) which provides pluggable matching strategies with clear
separation of concerns and testability.
## Deployment
### Docker
```bash
TVDB_API_KEY=your_key docker-compose run --rm episode-matcher \
episode_matcher.py "/path/to/episodes" "Show Name" 1
```
### Nginx
Deploy `nginx.conf` to your nginx server. Requires:
- Let's Encrypt SSL certificates
- nginx `headers-more` module for clearing x-response-time-ms
- Jellyfin running on localhost:8096

22
docker-compose.yml Normal file
View File

@ -0,0 +1,22 @@
version: "3.8"
services:
episode-matcher:
build:
context: .
dockerfile: Dockerfile
image: episode-matcher:latest
env_file:
- .env
environment:
- TVDB_API_KEY=${TVDB_API_KEY}
volumes:
- ./config.json:/app/config.json:ro
- ./src/.tvdb_cache:/app/src/.tvdb_cache
working_dir: /app
command: >
episode_matcher.py
${FOLDER_PATH:-/data}
${SHOW_NAME}
${SEASON_NUMBER}
${EXTRA_ARGS:-}

63
nginx.conf Normal file
View File

@ -0,0 +1,63 @@
server {
listen 443 ssl http2;
server_name tv.home.ms;
# SSL configuration
ssl_certificate /etc/letsencrypt/live/tv.home.ms/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/tv.home.ms/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# HSTS (#14 - Strict-Transport-Security)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# Security headers (#14)
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' 'unsafe-eval' https:; img-src 'self' data: https:; media-src 'self' https:;" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# Hide server version (#21)
server_tokens off;
# Remove x-response-time-ms header (#22)
more_clear_headers Set-Cookie;
# Proxy to Jellyfin
location / {
proxy_pass http://127.0.0.1:8096;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support for Jellyfin
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_buffering off;
proxy_request_buffering off;
}
# Block access to sensitive paths
location ~ /\. {
deny all;
}
location ~ /\.(ht|well-known) {
allow all;
}
}
# HTTP to HTTPS redirect
server {
listen 80;
server_name tv.home.ms;
return 301 https://$server_name$request_uri;
}