Sammelcommit fuer den ueber mehrere Sitzungen gewachsenen, bisher nicht
eingecheckten Stand. Getrennte Commits waeren ehrlicher gewesen, sind es
nachtraeglich aber nicht mehr: BugRepo, changelog.json und die Doku tragen
Aenderungen aus allen Straengen gleichzeitig, und keine Zwischenstufe waere
lauffaehig.
ROADMAP IM DEPLOYMENTCENTER (2.7.0)
Planungsstand, Reihenfolge und Zustaendigkeit lagen in handgepflegten
Markdown-Dateien neben einem Bugtracker, der genau diese drei Dinge bereits
fuehrt. Zwei Pfleger fuer dasselbe Feld - Drift war eine Frage der Zeit.
Ab jetzt: Zustand im DC, Begruendung in der Datei. Kein Feld hat zwei Pfleger.
- Migration 016: epic, roadmap_rank, target_build, defer_trigger,
doc_anchor, Status "deferred", Tabelle roadmap_epics.
KEINE Roadmap-Tabelle - ein Eintrag ist ein bugtracker_item.
- Zugehoerigkeit ist ein Feld: Ein Item steht auf der Roadmap, wenn es ein
epic hat. Ein zweites Kennzeichen daneben koennte auseinanderlaufen.
- Kennung ist die Item-ID. Handvergebene Kuerzel ("A12") erzeugen die
schlimmste Fehlerart: Beide Seiten stimmig, Verschiedenes gemeint, weil
die Kennung nach einer Loeschung neu vergeben wurde.
- Zurueckstellen verlangt defer_trigger. Ohne benannten Anlass ist ein
Vorhaben nicht zurueckgestellt, sondern vergessen. Die Regel sitzt im
Repository, damit sie auf allen drei Schreibwegen gleich greift.
- Modules/Bugtracker/Roadmap.php, /api/bugtracker/v1/roadmap.php,
WebUI-Modul mit Filtern, cli/roadmap.php --write/--check,
dc_roadmap und dc_roadmap_set im MCP-Adapter.
- cli/roadmap.php --write ERZEUGT den Zustandsteil der Markdown-Datei;
--check prueft nur die Prosa. Was erzeugt wird, kann nicht driften.
- docs/ROADMAP_MIGRATION.md: Uebernahme fuer die anderen Projekte.
TESTINSTANZ UND TESTTOR
- dc-dev.mhdf.de: zweite Installation, eigene DB, eigene Schluessel,
Basic-Auth davor, Rocket.Chat aus.
- 124 Testfaelle in tests/e2e gegen API, Rechte, Migrationen, Sperre.
- scripts/deploy.py bringt vor jedem Upload erst den anstehenden Stand auf
die Testinstanz und laesst dann den Testlauf laufen. Erst bei
Rueckgabewert 0 geht etwas auf die Produktion (--ohne-test als Notausgang).
Andersherum prueft der Lauf aelteren Code als den, der gleich hochgeht.
- tests/ ist aus dem Produktiv-Deploy ausgeschlossen; testctl.php kann
nicht versehentlich auf dc.mhdf.de landen.
- Ein Lauf mit mehr uebersprungenen als geprueften Faellen endet mit
Rueckgabewert 2 statt 0. Nichts ist kaputt, geprueft wurde es aber auch
nicht - der Unterschied darf nicht am Rueckgabewert verschwinden.
SUPERVISOR, PROJEKTLANDSCHAFT, NACHPRUEFUNG (2.6.0/2.6.1)
- Migration 013: projektuebergreifend melden (dev_agent, evidence,
triage_external, affects).
- Migration 014: supervisor_findings, Katalog, fuenf serverseitige
Detektoren im Tick.
- Migration 015: Nachpruefung als Arbeitsschlange statt Benachrichtigung.
- DEV-Lizenzschluessel zur Selbstbedienung, dc-testkit fuer die Test-VM.
- MCP-Adapter: 15 Werkzeuge, ohne Fremdabhaengigkeiten, mit Abgrenzung
von Fremddaten gegen Prompt-Injektion.
BEHOBEN
- observe und status im Supervisor legten den Zustand des Befundes als
"status" in die Antwort. Http::ok() merged ['status' => 'success']
darunter, also gewann der Befundzustand - eine angenommene Meldung kam
als status="open" an. Heisst jetzt finding_status. War Item #59.
Migrationen 013-016 sind auf dem Produktivserver noch NICHT ausgefuehrt.
Ohne sie bricht das WebUI, weil die Roadmap-Ansicht bei jedem Seitenaufruf
gegen bugtracker_items.epic abfragt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
175 lines
6.6 KiB
Python
175 lines
6.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Zugangssperre und Webroot.
|
|
|
|
Diese Gruppe prueft nicht die Anwendung, sondern das, was vor ihr steht: die
|
|
.htaccess. Sie steht am Anfang jedes Laufs, weil eine offene Testinstanz ein
|
|
groesseres Problem waere als jeder Befund darin - und weil ein Deployment die
|
|
.htaccess neu schreibt, also jedes Mal die Gelegenheit hat, sie kaputt zu
|
|
machen.
|
|
"""
|
|
|
|
import base64
|
|
|
|
from harness import test, erwarte, erwarte_status, erwarte_fehler
|
|
|
|
|
|
@test('sperre', 'Startseite ohne Zugangsdaten abgewiesen')
|
|
def _(ctx):
|
|
"""Ohne Basic-Auth darf die Testinstanz nichts ausliefern."""
|
|
r = ctx.client.get('/', basic=False, token=None)
|
|
erwarte_status(r, 401)
|
|
|
|
|
|
@test('sperre', 'Startseite mit falschem Passwort abgewiesen')
|
|
def _(ctx):
|
|
"""Ein falsches Passwort darf nicht durchrutschen - sonst waere die Datei kaputt."""
|
|
falsch = base64.b64encode(b'deploy:definitiv-falsch').decode()
|
|
r = ctx.client.get('/', basic=False, token=None,
|
|
headers={'Authorization': 'Basic ' + falsch})
|
|
erwarte_status(r, 401)
|
|
|
|
|
|
@test('sperre', 'Startseite mit Zugangsdaten erreichbar')
|
|
def _(ctx):
|
|
"""Mit den richtigen Daten muss das WebUI antworten, sonst sperrt die Datei zu viel."""
|
|
r = ctx.client.get('/', token=None)
|
|
erwarte_status(r, 200, 302)
|
|
|
|
|
|
@test('sperre', 'Konfiguration nicht abrufbar')
|
|
def _(ctx):
|
|
"""config/config.php enthaelt Zugangsdaten der Instanz."""
|
|
for pfad in ('/config/config.php', '/config/.htpasswd', '/config/config.example.php'):
|
|
r = ctx.client.get(pfad, token=None)
|
|
erwarte_status(r, 403)
|
|
|
|
|
|
@test('sperre', 'Anwendungscode nicht abrufbar')
|
|
def _(ctx):
|
|
"""src/, sql/ und cli/ sind kein Webinhalt."""
|
|
for pfad in ('/src/Core/Config.php', '/src/bootstrap.php',
|
|
'/sql/schema.sql', '/sql/migrations/016_roadmap.sql',
|
|
'/cli/tick.php', '/cli/roadmap.php'):
|
|
r = ctx.client.get(pfad, token=None)
|
|
erwarte_status(r, 403)
|
|
|
|
|
|
@test('sperre', 'Laufzeitverzeichnis nicht abrufbar')
|
|
def _(ctx):
|
|
"""In var/log/ stehen Fehlermeldungen samt Pfaden."""
|
|
for pfad in ('/var/log/', '/var/.htaccess'):
|
|
r = ctx.client.get(pfad, token=None)
|
|
erwarte_status(r, 403)
|
|
|
|
|
|
@test('sperre', 'Serverdaten und Deployment-Konfiguration nicht vorhanden')
|
|
def _(ctx):
|
|
"""Was gar nicht erst hochgeladen wird, kann auch nicht gelesen werden."""
|
|
for pfad in ('/Serverdaten.txt', '/scripts/deploy_config.json',
|
|
'/scripts/deploy_dev_config.json', '/tests/server/testctl.php'):
|
|
r = ctx.client.get(pfad, token=None)
|
|
erwarte_status(r, 403, 404)
|
|
|
|
|
|
@test('sperre', 'Interne Dokumentation nicht abrufbar')
|
|
def _(ctx):
|
|
"""
|
|
docs/ im Repository ist nicht public/docs/. Dort stehen interne Adressen
|
|
und Wege, die Instanz zurueckzusetzen. Erreichbar ist es nicht, weil
|
|
/docs/... auf public/docs/ umgeschrieben wird - eine Wirkung, die eine
|
|
Aenderung an den Routen versehentlich aufheben koennte.
|
|
"""
|
|
for pfad in ('/docs/TESTINSTANZ.md', '/docs/TESTUMGEBUNG.md', '/docs/ROADMAP.md'):
|
|
r = ctx.client.get(pfad, token=None)
|
|
erwarte_status(r, 403, 404)
|
|
|
|
|
|
@test('sperre', 'Oeffentliche Dokumentation weiterhin erreichbar')
|
|
def _(ctx):
|
|
"""Die Gegenprobe: public/docs/ soll die Sperre nicht mitgesperrt haben."""
|
|
r = ctx.client.get('/docs/bugtracker.md', token=None)
|
|
erwarte_status(r, 200)
|
|
|
|
|
|
@test('sperre', 'Kein Verzeichnisindex')
|
|
def _(ctx):
|
|
"""Options -Indexes: Verzeichnisse duerfen ihren Inhalt nicht auflisten."""
|
|
for pfad in ('/public/assets/', '/public/docs/release-template/'):
|
|
r = ctx.client.get(pfad, token=None)
|
|
erwarte(r.status in (403, 404) or 'Index of' not in (r.body or ''),
|
|
"%s liefert einen Verzeichnisindex" % pfad)
|
|
|
|
|
|
@test('sperre', 'API ohne Token bleibt hinter der Sperre')
|
|
def _(ctx):
|
|
"""Die Ausnahme in der .htaccess greift nur mit Token-Header, nie ohne."""
|
|
r = ctx.client.get('/api/health', basic=False, token=None)
|
|
erwarte_status(r, 401)
|
|
erwarte('WWW-Authenticate' in ' '.join(r.headers.keys()) or 'www-authenticate' in r.headers,
|
|
"401 ohne WWW-Authenticate - das ist die Anwendung, nicht die Sperre")
|
|
|
|
|
|
@test('sperre', 'API mit Token-Header wird durchgelassen')
|
|
def _(ctx):
|
|
"""Mit X-Agent-Token darf ein SDK die API ohne Basic-Auth erreichen."""
|
|
r = ctx.client.get('/api/health', basic=False)
|
|
erwarte(r.status != 401 or r.fehlercode is not None,
|
|
"Die .htaccess laesst /api/ mit Token-Header nicht durch (HTTP %d)" % r.status)
|
|
erwarte_status(r, 200, 503)
|
|
|
|
|
|
@test('sperre', 'API mit Bearer wird durchgelassen')
|
|
def _(ctx):
|
|
"""Der zweite Weg, ein Token zu uebergeben - vom .NET-SDK genutzt."""
|
|
r = ctx.client.get('/api/health', basic=False, bearer=True)
|
|
erwarte_status(r, 200, 503)
|
|
|
|
|
|
@test('sperre', 'Token-Header oeffnet nur /api/')
|
|
def _(ctx):
|
|
"""Ein Token-Header darf nicht das WebUI oder die Doku freischalten."""
|
|
for pfad in ('/', '/index.php', '/login.php', '/docs/', '/install_db.php', '/testctl.php'):
|
|
r = ctx.client.get(pfad, basic=False)
|
|
erwarte_status(r, 401)
|
|
|
|
|
|
@test('sperre', 'Sicherheitskopfzeilen gesetzt')
|
|
def _(ctx):
|
|
"""nosniff, Frame-Options und Referrer-Policy kommen aus der .htaccess."""
|
|
r = ctx.client.get('/api/health')
|
|
erwarte(r.headers.get('x-content-type-options') == 'nosniff',
|
|
"X-Content-Type-Options fehlt oder ist %r" % r.headers.get('x-content-type-options'))
|
|
r2 = ctx.client.get('/', token=None)
|
|
fehlend = [h for h in ('x-frame-options', 'referrer-policy')
|
|
if h not in r2.headers]
|
|
erwarte(not fehlend, "Fehlende Kopfzeilen auf der Startseite: %s" % ', '.join(fehlend))
|
|
|
|
|
|
@test('sperre', 'testctl ohne Shared Key abgewiesen')
|
|
def _(ctx):
|
|
"""Die Basic-Auth allein darf nicht genuegen, um Tabellen zu loeschen."""
|
|
r = ctx.client.post('/testctl.php', body={'action': 'status'}, token=None)
|
|
erwarte_fehler(r, http=401, code='unauthorized')
|
|
|
|
|
|
@test('sperre', 'testctl mit falschem Shared Key abgewiesen')
|
|
def _(ctx):
|
|
r = ctx.client.post('/testctl.php', body={'action': 'status'}, token='dc_master_falsch')
|
|
erwarte_fehler(r, http=401, code='unauthorized')
|
|
|
|
|
|
@test('sperre', 'testctl loescht nicht ohne Bestaetigung')
|
|
def _(ctx):
|
|
"""Ohne den Namen der Datenbank in confirm darf nichts verschwinden."""
|
|
r = ctx.testctl('wipe')
|
|
erwarte_fehler(r, http=400, code='confirmation_required')
|
|
|
|
r2 = ctx.testctl('wipe', confirm='irgendwas-anderes')
|
|
erwarte_fehler(r2, http=400, code='confirmation_required')
|
|
|
|
# Und die Tabellen stehen noch.
|
|
r3 = ctx.testctl('status')
|
|
erwarte(r3.feld('tables', default=0) > 20,
|
|
"Nach den abgewiesenen Loeschversuchen fehlen Tabellen: %s" % r3.kurz(200))
|