Initial Debian security mirror
This commit is contained in:
@@ -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"
|
||||
@@ -0,0 +1,5 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.tmp
|
||||
*.lock
|
||||
.env
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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')
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user