commit 3009b4977a8492241b4a197f7329a3e24b3ac852 Author: root Date: Tue Nov 10 21:52:04 2026 +0100 Initial Debian security mirror diff --git a/.gitea/workflows/update.yml b/.gitea/workflows/update.yml new file mode 100644 index 0000000..c976aa8 --- /dev/null +++ b/.gitea/workflows/update.yml @@ -0,0 +1,41 @@ +name: Debian Security Tracker Mirror +on: + workflow_dispatch: + schedule: + - cron: "43 */6 * * *" +permissions: + contents: write +concurrency: + group: vuln-debian-mirror + cancel-in-progress: false +jobs: + mirror: + runs-on: vuln-runner + timeout-minutes: 120 + steps: + - name: Sync official Debian data using persistent checkout + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + CACHE_ROOT: /var/lib/act_runner/cache/vuln-debian + REPO_URL: https://git.apex-project.de/VULN/vuln-debian.git + run: | + set -Eeuo pipefail + umask 077 + mkdir -p "$CACHE_ROOT" + if [ ! -f "$CACHE_ROOT/repo/scripts/run-cached.sh" ]; then + ASKPASS="$(mktemp)" + trap 'rm -f "$ASKPASS"' EXIT + cat > "$ASKPASS" <<'EOF' + #!/bin/sh + case "$1" in + *Username*) printf 'oauth2\n' ;; + *Password*) printf '%s\n' "$GITEA_TOKEN" ;; + *) exit 1 ;; + esac + EOF + chmod 700 "$ASKPASS" + GIT_ASKPASS="$ASKPASS" GIT_TERMINAL_PROMPT=0 git clone --depth 1 --branch main "$REPO_URL" "$CACHE_ROOT/repo" + rm -f "$ASKPASS" + trap - EXIT + fi + bash "$CACHE_ROOT/repo/scripts/run-cached.sh" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f8791a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +__pycache__/ +*.pyc +*.tmp +*.lock +.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..6777917 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# vuln-debian – Gitea Security Tracker Mirror + +Spiegelt den **offiziellen Debian Security Tracker JSON-Export** als Rohdatei nach Gitea. + +Quelle: https://security-tracker.debian.org/tracker/data/json + +## Funktionsumfang + +- Vollständiger JSON-Export, unverändert unter `data/tracker.json`. +- Paketnamen, CVEs, Releases, Status, `fixed_version`, `urgency` bleiben erhalten. +- `ETag` / `Last-Modified` werden verwendet, falls der Debian-Server sie liefert. +- SHA256-Vergleich verhindert unnötige Commits. +- Dauerhaftes Git-Arbeitsverzeichnis auf demselben Host wie Gitea. +- Alle sechs Stunden zur Minute 43 UTC, plus manueller Workflow-Start. +- Gitea-Job-Token: `${{ secrets.GITEA_TOKEN }}`; kein eigener PAT nötig, sofern Push-Berechtigung vorhanden. + +## Installation + +1. Leeres Repository `VULN/vuln-debian` erstellen. +2. Dateien dieses Verzeichnisses hochladen: + ```bash + git init + git branch -M main + git add . + git commit -m 'Initial Debian mirror' + git remote add origin https://git.apex-project.de/VULN/vuln-debian.git + git push -u origin main + ``` +3. Auf der Gitea-VM: + ```bash + sudo apt update && sudo apt install -y git python3 util-linux ca-certificates + sudo install -d -o act_runner -g act_runner -m 700 /var/lib/act_runner/cache/vuln-debian + ``` +4. In Gitea die Actions für das Repository aktivieren und den Schreibzugriff des Job-Tokens erlauben. +5. Unter `VULN/vuln-debian/actions` den Workflow manuell starten. + +## Cache + +`/var/lib/act_runner/cache/vuln-debian/repo` enthält einen persistenten Checkout, `/var/lib/act_runner/cache/vuln-debian/.sync.lock` verhindert gleichzeitige Synchronisationen. Das Repository wird nur beim ersten Mal vollständig geklont, danach mit `git fetch --depth 1` aktualisiert. + +## Quelle und Grenzen + +Das Debian-JSON ist **kein Archiv nur ab 2017**. Es enthält auch ältere CVEs. Das ist beabsichtigt, da ein Filter die Rohdaten verändern würde; die spätere SQLite-Buildstufe filtert nach Jahr und gewünschter Release-Unterstützung. + +Der Feed wird als eine große JSON-Datei versioniert. Das ist vollständig und verlustfrei, kann die Git-Historie aber bei häufigen Änderungen vergrößern. Die Inhalte werden nicht zu separaten CVE-Dateien aufgeteilt. + +Der Feed ist eine abgeleitete Ansicht der Debian-Sicherheitsdaten. Das Schema ist nicht garantiert stabil; bei strukturellen Änderungen bricht die Validierung ab statt stillschweigend Daten zu verlieren. Hinweise zu Redistributon/Nutzungsbedingungen vor öffentlicher Veröffentlichung prüfen. + +## Diagnose + +```bash +sudo journalctl -u act_runner -f +sudo -u act_runner git -C /var/lib/act_runner/cache/vuln-debian/repo status --short +sudo du -sh /var/lib/act_runner/cache/vuln-debian/repo +``` + +Getestet: Python-Syntax, Shell-Syntax, lokale Testdaten. Live-Gitea-Push und großer Debian-Download sind nicht durchgetestet. diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/run-cached.sh b/scripts/run-cached.sh new file mode 100644 index 0000000..fb9eebe --- /dev/null +++ b/scripts/run-cached.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -Eeuo pipefail +umask 077 +: "${GITEA_TOKEN:?GITEA_TOKEN fehlt}" +CACHE_ROOT="${CACHE_ROOT:-/var/lib/act_runner/cache/vuln-debian}" +REPO_URL="${REPO_URL:-https://git.apex-project.de/VULN/vuln-debian.git}" +mkdir -p "$CACHE_ROOT" +exec 9>"$CACHE_ROOT/.sync.lock" +flock -w 300 9 || { echo 'Ein anderer Lauf ist noch aktiv' >&2; exit 1; } +# Credentials are supplied only on demand; never embedded in the remote URL. +ASKPASS="$(mktemp)" +trap 'rm -f "$ASKPASS"' EXIT +cat > "$ASKPASS" <<'EOF' +#!/bin/sh +case "$1" in + *Username*) printf 'oauth2\n' ;; + *Password*) printf '%s\n' "$GITEA_TOKEN" ;; + *) exit 1 ;; +esac +EOF +chmod 700 "$ASKPASS" +export GIT_ASKPASS="$ASKPASS" GIT_TERMINAL_PROMPT=0 +if [[ ! -d "$CACHE_ROOT/repo/.git" ]]; then + git clone --depth 1 --branch main "$REPO_URL" "$CACHE_ROOT/repo" +fi +cd "$CACHE_ROOT/repo" +git remote set-url origin "$REPO_URL" +git fetch --depth 1 origin main +git reset --hard FETCH_HEAD +python3 scripts/sync.py +python3 scripts/validate.py +git add data/tracker.json state/sync.json +if git diff --cached --quiet; then + echo 'Keine neuen Daten für Commit'; exit 0 +fi +git config user.name 'Vulnerability Bot' +git config user.email 'vulnerability-bot@users.noreply.local' +git commit -m "Debian Security Tracker update $(date -u +%FT%TZ)" +git push origin HEAD:main diff --git a/scripts/sync.py b/scripts/sync.py new file mode 100644 index 0000000..024d059 --- /dev/null +++ b/scripts/sync.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Mirror the official Debian Security Tracker JSON export without changing its contents.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +from datetime import datetime, timezone +from urllib.request import Request, urlopen +from urllib.error import HTTPError + +URL = 'https://security-tracker.debian.org/tracker/data/json' + +def sync(root: Path, url: str, timeout: int = 240): + data = root / 'data' / 'tracker.json' + state = root / 'state' / 'sync.json' + meta = {} + if state.exists(): + meta = json.loads(state.read_text(encoding='utf-8')) + headers = {'User-Agent': 'apex-project-vuln-debian-mirror/1.0', 'Accept': 'application/json'} + if data.exists(): + if meta.get('etag'): headers['If-None-Match'] = meta['etag'] + if meta.get('last_modified'): headers['If-Modified-Since'] = meta['last_modified'] + try: + with urlopen(Request(url, headers=headers), timeout=timeout) as response: + raw = response.read() + etag = response.headers.get('ETag') + modified = response.headers.get('Last-Modified') + except HTTPError as exc: + if exc.code == 304 and data.exists(): + print('HTTP 304: keine Änderungen am Debian-Export') + return False + raise + if len(raw) < 100: raise RuntimeError('Antwort ist unerwartet kurz') + parsed = json.loads(raw) + validate(parsed) + digest = hashlib.sha256(raw).hexdigest() + if data.exists() and hashlib.sha256(data.read_bytes()).hexdigest() == digest: + print('Inhalt unverändert; kein Commit erforderlich') + return False + data.parent.mkdir(parents=True, exist_ok=True) + state.parent.mkdir(parents=True, exist_ok=True) + temp = data.with_suffix('.json.tmp') + temp.write_bytes(raw) + os.replace(temp, data) + state_temp = state.with_suffix('.json.tmp') + state_temp.write_text(json.dumps({'source': url, 'sha256': digest, 'etag':etag, + 'last_modified':modified, 'downloaded_at':datetime.now(timezone.utc).isoformat(), + 'package_count':len(parsed)}, indent=2, ensure_ascii=False)+'\n', encoding='utf-8') + os.replace(state_temp, state) + print(f'Debian Export gespeichert: {len(raw):,} Bytes / {len(parsed):,} Pakete') + return True + +def validate(parsed): + if not isinstance(parsed, dict) or not parsed: + raise ValueError('Erwartet nichtleeres JSON-Objekt mit Paketnamen als Keys') + for package, cves in list(parsed.items())[:200]: + if not isinstance(package, str) or not isinstance(cves, dict): + raise ValueError(f'Unerwartetes Debian-JSON-Schema bei {package!r}') + +def main(): + p = argparse.ArgumentParser() + p.add_argument('--root', type=Path, default=Path(__file__).resolve().parent.parent) + p.add_argument('--url', default=URL) + p.add_argument('--timeout', type=int, default=240) + args = p.parse_args() + sync(args.root, args.url, args.timeout) + +if __name__ == '__main__': main() diff --git a/scripts/validate.py b/scripts/validate.py new file mode 100644 index 0000000..4e8ef72 --- /dev/null +++ b/scripts/validate.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +import hashlib +import json +from pathlib import Path +from sync import validate +root = Path(__file__).resolve().parent.parent +p = root / 'data/tracker.json' +s = root / 'state/sync.json' +raw = p.read_bytes() +parsed = json.loads(raw) +validate(parsed) +meta = json.loads(s.read_text()) +assert hashlib.sha256(raw).hexdigest() == meta['sha256'], 'SHA256 stimmt nicht' +print(f'OK: {len(parsed):,} Debian-Pakete; SHA256 geprüft') diff --git a/state/.gitkeep b/state/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_sync.py b/tests/test_sync.py new file mode 100644 index 0000000..779b182 --- /dev/null +++ b/tests/test_sync.py @@ -0,0 +1,13 @@ +import json +import tempfile +import unittest +from pathlib import Path +from sys import path +path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts')) +from sync import validate +class TestSync(unittest.TestCase): + def test_example(self): + validate({'openssl': {'CVE-2024-0001': {'releases': {'bookworm': {'status': 'resolved', 'fixed_version': '1.2'}}}}}) + def test_bad(self): + with self.assertRaises(ValueError): validate([]) +if __name__ == '__main__': unittest.main()