Retention-Governance + Heartbeat-Wachhund (Pentest R183, R182-Rest)

R183-01: PUT /retention-policies/{id} nahm retentionDays 0 ohne Bestaetigung an
und protokollierte es als MEDIUM - waehrend cleanup, seal und rehash alle ein
Confirm-Gate haben und die Folge CRITICAL ist. Die geladene Waffe war ungegatet,
der Abzug gegatet. Fix: Absenken verlangt confirm SHORTEN, wird als CRITICAL mit
Vorher/Nachher protokolliert, Untergrenze 30 Tage fuer Authentication/AuditLog.

R183-02: Nach dem Cleanup meldete verify "Keine Manipulation" bei valid:false
und 3010 endgueltig geloeschten Anmeldeprotokollen - der Befund stand nur im
Feld, die Prosa beruhigte. Fix: ehrliche Formulierung bei Luecken, und das
Gegenbuch ruft /verify mit und wertet valid:false hart, egal wie der Text
klingt.

R183-03: verify warnte dauerhaft "Altbestand nicht versiegelt", waehrend
seal-backlog zu Recht ablehnte. Eine unaufloesbare Warnung lernt man zu
ignorieren. Fix: eigener Zustand nicht_noetig, echte Warnung nennt den Befehl.

Heartbeat-Wachhund: Bleibt ein Dienstkonto laenger still als
SERVICE_ACCOUNT_MAX_SILENCE_MINUTES (Standard 180), gibt es SUSPICIOUS/CRITICAL.
Ohne je gesehene Anmeldung wird geschwiegen statt geraten, pro Ausfall genau
eine Meldung. Verifiziert in allen drei Faellen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 10:01:32 +02:00
co-authored by Claude Opus 5
parent 41671cbb96
commit 910c94daa1
7 changed files with 246 additions and 10 deletions
+25 -2
View File
@@ -235,13 +235,20 @@ async function anmelden() {
zugangsToken = j.data.token;
}
async function hole(pfad) {
async function hole(pfad, methode = 'GET') {
// Fehler werden hier zu einer erklaerenden Zeile frueher flog ein
// Node-Stacktrace hoch, also genau die kryptische erste Zeile, die fuer
// git-Meldungen schon abgestellt war.
let r;
try {
r = await fetch(`${CRM_URL}${pfad}`, { headers: { Authorization: `Bearer ${zugangsToken}` } });
r = await fetch(`${CRM_URL}${pfad}`, {
method: methode,
headers: {
Authorization: `Bearer ${zugangsToken}`,
...(methode === 'POST' ? { 'content-type': 'application/json' } : {}),
},
...(methode === 'POST' ? { body: '{}' } : {}),
});
} catch (e) {
console.error(
`Das CRM ist nicht erreichbar (${CRM_URL}).\n` +
@@ -737,6 +744,22 @@ if (letzter) {
`jetzt maxId=${aktuell.maxId}. Es wurden Einträge entfernt.`,
);
}
// `valid: false` wird HART gewertet, unabhaengig von der erklaerenden Prosa
// (Pentest R183-02). Ein Cleanup mit abgesenkter Aufbewahrung kann tausende
// Eintraege endgueltig loeschen und die Luecken per Tombstone als „erklaert“
// ausweisen die Meldung liest sich dann harmlos. Fuer das Gegenbuch zaehlt
// das Feld, nicht der Satz.
const pruefung = await hole('/api/audit-logs/verify', 'POST');
if (pruefung && pruefung.valid === false) {
alarm(
'Die Prüfung im CRM meldet die Kette als NICHT unversehrt (valid: false).\n' +
` CRM-Text: ${pruefung.message}\n` +
'Auch wenn dieser Text harmlos klingt: Es wird nichts beglaubigt, solange die\n' +
'Prüfung nicht sauber ist. Häufigste Ursache ist ein Cleanup mit verkürzter\n' +
'Aufbewahrung dann wurden Einträge endgültig gelöscht.',
);
}
const rueck = await hole(`/api/audit-logs/checkpoint?atId=${letzter.maxId}`);
if (rueck.atHash === null) alarm(`Der beglaubigte Eintrag ${letzter.maxId} existiert nicht mehr.`);
if (rueck.atHash !== letzter.chainHead) {