Skip to content
Merged
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
10 changes: 5 additions & 5 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ jobs:
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3.4.0
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
Expand All @@ -28,7 +28,7 @@ jobs:

# Build is skipped if Trivy exits with code 1
- name: Build and push Docker image
uses: docker/build-push-action@v6
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
with:
context: .
push: true
Expand All @@ -41,7 +41,7 @@ jobs:
REVISION=${{ github.sha }}

- name: Run Trivy image scan
uses: aquasecurity/trivy-action@v0.35.0
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # v0.35.0
with:
scan-type: 'image'
image-ref: 'ghcr.io/${{ github.repository_owner }}/episodeguard:${{ steps.date.outputs.tag }}'
Expand All @@ -52,7 +52,7 @@ jobs:
trivyignores: './.trivyignore'

- name: Delete old package versions (keep last 3)
uses: actions/delete-package-versions@v5
uses: actions/delete-package-versions@e5bc658cc4c965c472efe991f8beea3981499c55 # v5
with:
package-name: episodeguard
package-type: container
Expand Down
21 changes: 21 additions & 0 deletions .github/workflows/gitleaks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Secret Scan
on:
push:
branches: [ "main" ]
pull_request:
permissions:
contents: read
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0 # full history so gitleaks can scan past commits

- name: Run gitleaks
uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_CONFIG: .gitleaks.toml
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,3 @@ data/
.env
.env.local
npm-debug.log*
package-lock.json
66 changes: 66 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Gitleaks configuration for Episode Guard
# Scans commit history and the working tree for leaked secrets.
# Run locally: gitleaks dir . --config .gitleaks.toml
# Docs: https://github.com/gitleaks/gitleaks

title = "Episode Guard gitleaks config"

# Inherit gitleaks' built-in rule set (AWS, GitHub, Slack, private keys, JWTs,
# generic high-entropy keys, etc.), then layer the project-specific rules below.
[extend]
useDefault = true

# --- Project-specific rules ---

[[rules]]
id = "episodeguard-media-api-key"
description = "Tautulli / Sonarr / Jellyfin API key"
regex = '''(?i)\b(?:tautulli|sonarr|jellyfin)_api_key\s*[:=]\s*["']?([0-9a-f]{20,})["']?'''
secretGroup = 1
keywords = ["tautulli_api_key", "sonarr_api_key", "jellyfin_api_key"]
tags = ["api-key", "media-stack"]

[[rules]]
id = "episodeguard-secret-key"
description = "Episode Guard SECRET_KEY (session signing + OIDC encryption)"
regex = '''(?i)\bsecret_key\s*[:=]\s*["']?([A-Za-z0-9+/=_\-]{16,})["']?'''
secretGroup = 1
keywords = ["secret_key"]
tags = ["secret"]

[[rules]]
id = "episodeguard-oidc-client-secret"
description = "OIDC provider client secret"
regex = '''(?i)\bclient_secret\s*[:=]\s*["']?([A-Za-z0-9._\-]{16,})["']?'''
secretGroup = 1
keywords = ["client_secret"]
tags = ["oidc", "secret"]

# --- Allowlist ---
# Documented placeholders, env templates, and lockfile integrity hashes are not
# secrets. Kept as regexes (not whole-file ignores) where possible so a real leak
# in a docs file is still caught. Stopwords cover frontend JS expressions that
# look like secret assignments (e.g. client_secret:document.getElementById(...)).

[allowlist]
description = "Ignore documented placeholders, templates, and lockfile hashes"
paths = [
'''\.env\.example$''',
'''(^|/)package-lock\.json$''',
]
regexes = [
'''your_(?:tautulli|sonarr|jellyfin)_api_key''',
'''your_random_secret''',
'''change-me(?:-to-a-random-secret)?''',
'''\$\{[A-Z0-9_]+\}''',
'''<your secret>''',
]
stopwords = [
"example",
"placeholder",
"changeme",
"your_",
"getelementbyid",
"decrypt",
"encrypt",
]
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
FROM node:22-alpine AS builder
RUN apk add --no-cache python3 make g++
WORKDIR /app
COPY package.json ./
RUN npm install --omit=dev
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

# Stage 2: lean runtime image
FROM node:22-alpine
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,14 @@ The app header shows the current version (e.g. `v2.0.0`) next to the Episode Gua
docker build -t episodeguard .
```

## Security

Secrets (API keys, `SECRET_KEY`, OIDC client secrets) are never committed. A gitleaks scan runs in CI on every push and pull request (`.github/workflows/gitleaks.yml`) using the rules in `.gitleaks.toml`, and fails the build if anything leaks. To scan locally before pushing:

```bash
gitleaks dir . --config .gitleaks.toml
```

## Contributing

All changes go through a branch and pull request — nothing gets merged directly to `main`.
Expand Down
5 changes: 2 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ services:
TAUTULLI_API_KEY: ${TAUTULLI_API_KEY}
SONARR_URL: ${SONARR_URL}
SONARR_API_KEY: ${SONARR_API_KEY}
# ── Web UI auth (required) ───────────────────────────────────────────
WEB_USERNAME: ${WEB_USERNAME}
WEB_PASSWORD: ${WEB_PASSWORD}
# Signs session cookies and encrypts OIDC secrets. Generate: openssl rand -base64 32
SECRET_KEY: ${SECRET_KEY}
# ── Optional ────────────────────────────────────────────────────────
DATA_DIR: /data

Expand Down
Loading
Loading