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
26 changes: 26 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Coverage.py configuration for the unit test suite.
# Measures only first-party code: third-party venvs, the native standalone
# builds, screenshots and the test tree itself are excluded so the percentage
# reflects the application, not its dependencies.
[run]
branch = True
source = .
omit =
test/*
.venv/*
.venv-windows/*
native-build/*
scripts/*
screenshot/*
dist/*
query/*
*/__pycache__/*

[report]
show_missing = True
skip_covered = False
precision = 1
exclude_also =
if __name__ == .__main__.:
raise NotImplementedError
if TYPE_CHECKING:
1 change: 1 addition & 0 deletions .github/workflows/build-arm-intel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jobs:
(github.event.pull_request.draft == false &&
github.event.pull_request.head.repo.full_name == github.repository)
runs-on: ubuntu-latest
permissions: {}
outputs:
image: ${{ steps.repo_name.outputs.image }}
tags: ${{ steps.tags.outputs.tags }}
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/lint-line-endings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ on:
pull_request:
branches: ["**"]

permissions:
contents: read

jobs:
check-crlf:
runs-on: ubuntu-latest
Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/lint-mypy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Type Check (mypy)

# Static type-checking scoped (via mypy.ini) to small, self-contained core
# modules so the gate passes today and can be widened as modules gain hints.
# Independent of the test/build workflows.

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

permissions:
contents: read

jobs:
mypy:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

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

- name: Install mypy
run: |
python -m pip install --upgrade pip
pip install mypy

- name: Run mypy (scoped to core modules via mypy.ini)
run: mypy --config-file mypy.ini
122 changes: 122 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,125 @@ jobs:
requirements/cpu.txt
requirements/gpu.txt
ignore-vulns: ${{ steps.cve_allowlist.outputs.ids }}

# Image / supply-chain layer: Trivy scans the container DEFINITION (Dockerfile
# + deployment manifests) for misconfigurations and the working tree for leaked
# secrets -- coverage pip-audit (Python CVEs) and Bandit (Python SAST) do not
# provide. The config scan (Dockerfile/manifest misconfig) is report-only
# (exit-code 0); the filesystem scan GATES (exit-code 1) so HIGH/CRITICAL
# dependency CVEs or leaked secrets turn this check red, matching pip-audit's
# fail-on-CVE behaviour. A red check does NOT block merge unless this workflow
# is added to branch protection's required checks. Findings are uploaded as
# SARIF to code scanning so the detailed table lives in the private Security
# tab (write/maintain/admin only) instead of the public Actions run log. A
# full built-image OS scan can later be hooked into the image-publish pipeline.
trivy:
runs-on: ubuntu-latest
# security-events:write lets the SARIF upload publish to the Security tab.
permissions:
contents: read
security-events: write
actions: read

steps:
- uses: actions/checkout@v4

- name: Install Trivy
run: |
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
| sh -s -- -b /usr/local/bin

# Each scan emits native JSON once: it feeds both a numeric recap on the run
# summary (0 shown when clean) and, via `trivy convert`, the SARIF uploaded
# to the Security tab -- one scan, no second pass over the vuln DB.

# Accepted / not-applicable findings are listed in .trivyignore (the Trivy
# counterpart of the pip-audit allowlist above). Config scan is report-only.
- name: Trivy config scan (Dockerfiles + deployment manifests)
if: always()
run: |
trivy config --severity HIGH,CRITICAL --exit-code 0 \
--ignorefile .trivyignore \
--format json --output trivy-config.json .

- name: Convert Trivy config report to SARIF
if: always()
run: trivy convert --format sarif --output trivy-config.sarif trivy-config.json

- name: Upload Trivy config SARIF to code scanning
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-config.sarif
category: trivy-config

# Filesystem scan GATES on HIGH/CRITICAL. --file-patterns adds the shipped
# requirements/*.txt to the pip vuln scan (Trivy's pip scanner otherwise only
# reads files named exactly requirements.txt); the -noavx2 legacy stacks and
# the dev-only test/requirements.txt are skipped. The detailed finding table
# reaches the Security tab via the converted SARIF (kept out of the public run
# log); only the counts are public. The step still exits non-zero on findings
# so the check goes red.
- name: Trivy filesystem scan (dependency CVEs + leaked secrets, fails on HIGH/CRITICAL)
run: |
trivy fs --scanners vuln,secret --severity HIGH,CRITICAL --exit-code 1 \
--ignorefile .trivyignore \
--file-patterns "pip:requirements/.*\.txt" \
--skip-files requirements/common-noavx2.txt,requirements/cpu-noavx2.txt,test/requirements.txt \
--format json --output trivy-fs.json \
--skip-dirs .venv,.venv-windows,native-build,test/songs,model,dist .

- name: Convert Trivy filesystem report to SARIF
if: always()
run: trivy convert --format sarif --output trivy-fs.sarif trivy-fs.json

- name: Upload Trivy filesystem SARIF to code scanning
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-fs.sarif
category: trivy-fs

# Counts on the run summary (0 when clean), mirroring the Bandit summary.
- name: Trivy job summary
if: always()
run: |
python3 - <<'PY'
import json, os

def load(path):
try:
with open(path) as fh:
return json.load(fh)
except (FileNotFoundError, json.JSONDecodeError):
return {}

def tally(report, key):
total = high = crit = 0
for res in (report.get("Results") or []):
for item in (res.get(key) or []):
total += 1
sev = (item.get("Severity") or "").upper()
if sev == "HIGH":
high += 1
elif sev == "CRITICAL":
crit += 1
return total, high, crit

cfg = load("trivy-config.json")
fs = load("trivy-fs.json")
rows = [
("Config misconfig (Dockerfiles / manifests)",) + tally(cfg, "Misconfigurations"),
("Dependency CVEs (filesystem)",) + tally(fs, "Vulnerabilities"),
("Leaked secrets (filesystem)",) + tally(fs, "Secrets"),
]
total = sum(r[1] for r in rows)
lines = ["## Trivy (HIGH / CRITICAL)", "",
"Total findings: %d" % total, "",
"| Scan | Findings | HIGH | CRITICAL |", "|---|---|---|---|"]
for name, t, h, c in rows:
lines.append("| %s | %d | %d | %d |" % (name, t, h, c))
lines += ["", "Detail for any non-zero row: Security tab -> Code scanning -> trivy-config / trivy-fs."]
with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as fh:
fh.write("\n".join(lines) + "\n")
PY
29 changes: 27 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,31 @@ jobs:
python -m pip install --upgrade pip
pip install -r test/requirements.txt

- name: Run unit tests
- name: Run unit tests with coverage
run: |
pytest test/unit/ -v --tb=short
pytest test/unit/ -v --tb=short \
--cov --cov-report=term-missing --cov-report=xml

- name: Coverage summary
if: always()
run: |
python - <<'PY'
import xml.etree.ElementTree as ET, os
try:
rate = float(ET.parse("coverage.xml").getroot().get("line-rate", 0)) * 100
except Exception:
rate = 0.0
summary = os.environ.get("GITHUB_STEP_SUMMARY")
line = f"## Unit test coverage: {rate:.1f}% of lines\n"
if summary:
open(summary, "a").write(line)
print(line)
PY

- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage.xml
retention-days: 7
33 changes: 33 additions & 0 deletions .trivyignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Trivy ignore list -- accepted / not-applicable HIGH/CRITICAL findings.
# This is the Trivy counterpart of the pip-audit allowlist in
# .github/workflows/security.yml. Format: put the human reason on a '#' line,
# then the bare CVE/GHSA id on the next line. Add an id here ONLY when the
# finding does not impact AudioMuse (unreachable path) or has no fixed release
# yet; PREFER bumping the dependency when a fix exists.

# transformers X-CLIP checkpoint-conversion deserialization RCE -- unreachable:
# AudioMuse never converts X-CLIP checkpoints nor loads untrusted checkpoints.
# Mirrors PYSEC-2025-217 in the pip-audit allowlist; no fix in the 4.x line.
CVE-2025-14929
GHSA-8jfx-5878-hv4v
# transformers Trainer torch.load RCE -- unreachable: AudioMuse does not use the
# transformers Trainer class. Mirrors CVE-2026-1839 in the pip-audit allowlist;
# fixed only in 5.0.0rc3 (a pre-release we do not pin to).
CVE-2026-1839
GHSA-69w3-r845-3855

# The onnx model-loading CVEs are intentionally NOT excluded here because they
# are already fixed by onnx==1.21.0 in requirements/common.txt (the version Trivy
# now scans). They are recorded below, commented out, only to document why they
# would be non-applicable should a future onnx pin ever regress (AudioMuse loads
# ONLY its own app-bundled models via onnx.load(); it never loads attacker-
# supplied models or calls save_external_data on untrusted data):
#
# # onnx path traversal via external-data load -- only trusted bundled models
# CVE-2026-27489
# # onnx untrusted-model-repo warning suppression -- models are vendored, not fetched at runtime
# CVE-2026-28500
# # onnx malicious-model DoS / info disclosure -- only trusted bundled models
# CVE-2026-34445
# # onnx TOCTOU in save_external_data -- never called on untrusted data
# GHSA-q56x-g2fj-4rj6
2 changes: 1 addition & 1 deletion config.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def _compute_headers():
}

# --- General Constants (Read from Environment Variables where applicable) ---
APP_VERSION = "v2.3.1"
APP_VERSION = "v2.3.2"
MAX_DISTANCE = float(os.environ.get("MAX_DISTANCE", "0.5"))
MAX_SONGS_PER_CLUSTER = int(os.environ.get("MAX_SONGS_PER_CLUSTER", "0"))
MAX_SONGS_PER_ARTIST = int(os.getenv("MAX_SONGS_PER_ARTIST", "3")) # Max songs per artist in similarity results and clustering
Expand Down
2 changes: 1 addition & 1 deletion database.py
Original file line number Diff line number Diff line change
Expand Up @@ -1028,7 +1028,7 @@ def init_db():
""", (query, 1.0, rank))

logger.info(f"Inserted {len(default_queries)} default DCLAP search queries")

db.commit()
# Release the advisory lock acquired at the top of init_db().
finally:
Expand Down
3 changes: 3 additions & 0 deletions lyrics/lyrics_transcriber.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,9 @@ def _fetch_from_configured_api(
import socket as _socket
import urllib.parse as _up
_parsed_tpl = _up.urlparse(url_template)
if _parsed_tpl.scheme not in ('http', 'https'):
logger.warning('Lyrics API slot %s blocked: non-http(s) scheme %r', slot, _parsed_tpl.scheme)
return None
_host = _parsed_tpl.hostname or ''
_host_l = _host.strip().lower()
if _host_l in ('localhost', '') or _host_l.endswith('.localhost') or _host_l.endswith('.local'):
Expand Down
26 changes: 26 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Static type-checking, scoped to small, self-contained core modules so the
# check is a real gate that passes today. Widen `files` as more modules gain
# type hints. The bulk of the codebase is untyped, hence the lenient defaults
# (no disallow_untyped_defs) -- this catches concrete type errors, not missing
# annotations.
[mypy]
python_version = 3.11
ignore_missing_imports = True
# Errors are reported only for the modules listed in `files`; imported-but-not
# scoped modules are followed for type info but their own errors are suppressed.
follow_imports = silent
warn_redundant_casts = True
no_implicit_optional = True
warn_unused_ignores = False
show_error_codes = True
files =
config.py,
sanitization.py,
ssrf_guard.py,
tz_helper.py,
proxy_prefix.py,
error/

# Tests are not type-checked.
[mypy-test.*]
ignore_errors = True
2 changes: 1 addition & 1 deletion requirements/common.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ ftfy==6.3.1
flasgger==0.9.7.1
sqlglot==30.6.0
google-genai==1.57.0
mistralai>=1.11.1,<2.0.0
mistralai==1.12.4
umap-learn==0.5.12
av==13.1.0
psutil==7.2.2
Expand Down
5 changes: 3 additions & 2 deletions requirements/windows.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ pgserver==0.1.4
waitress

# Windows-specific: pywin32 for process management and named mutex.
# mcp 1.27.0 requires >=310 on Windows; pin to the minimum supported.
pywin32>=310
# mcp 1.27.0 requires >=310 on Windows; pin to the minimum supported. 310 ships
# both win_amd64 (Intel/AMD) and win_arm64 wheels.
pywin32==310

# Windows tray (notification-area) menu app -- the counterpart to the macOS
# menu-bar agent. pystray needs Pillow to load the .ico.
Expand Down
9 changes: 9 additions & 0 deletions static/menu.css
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,15 @@ html.sidebar-open .sidebar {
background-color: #2563EB;
}

/* Keyboard focus ring inside the dark sidebar: white reads better than blue,
and a negative offset keeps it from being clipped by the sidebar's overflow. */
.sidebar-nav li a:focus-visible,
.sidebar-nav li button:focus-visible,
.submenu li a:focus-visible {
outline: 2px solid #ffffff;
outline-offset: -2px;
}

/* Dark mode toggle button styling */
#dark-mode-toggle,
#logout-btn {
Expand Down
Loading
Loading