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:
@@ -15,6 +15,12 @@ export async function logChange(opts: {
|
||||
label: string; // Menschenlesbares Label z.B. "Vollmacht für Stefan Hacker widerrufen"
|
||||
details?: Record<string, unknown>; // Zusätzliche Details z.B. { vorher: 'erteilt', nachher: 'widerrufen' }
|
||||
customerId?: number;
|
||||
/// Ueberschreibt die aus dem Ressourcentyp abgeleitete Stufe. Noetig, wenn
|
||||
/// eine Aktion gefaehrlicher ist als ihr Typ vermuten laesst - etwa das
|
||||
/// Verkuerzen einer Aufbewahrungsfrist, dessen Folge CRITICAL ist
|
||||
/// (Pentest R183-01).
|
||||
sensitivity?: AuditSensitivity;
|
||||
before?: Record<string, unknown>;
|
||||
}) {
|
||||
try {
|
||||
const user = opts.req?.user;
|
||||
@@ -32,6 +38,8 @@ export async function logChange(opts: {
|
||||
httpMethod: opts.req?.method || '',
|
||||
ipAddress: opts.req?.socket?.remoteAddress || opts.req?.headers?.['x-forwarded-for'] || 'unknown',
|
||||
dataSubjectId: opts.customerId,
|
||||
sensitivity: opts.sensitivity,
|
||||
changesBefore: opts.before,
|
||||
changesAfter: opts.details,
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -895,7 +903,7 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
|
||||
* `kein_siegel` = nie erstellt. `entfernt` = Blaetter vorhanden, aber kein
|
||||
* gueltiger Marker mehr – der Anker wurde herausgeloest (Pentest R174-01).
|
||||
*/
|
||||
backlogSealStatus: 'kein_siegel' | 'intakt' | 'gebrochen' | 'entfernt';
|
||||
backlogSealStatus: 'kein_siegel' | 'intakt' | 'gebrochen' | 'entfernt' | 'nicht_noetig';
|
||||
/** Altbestands-Zeilen, deren Inhalt vom Siegel abweicht. */
|
||||
backlogTampered: number[];
|
||||
/** Gesiegelte Zeilen, die nicht mehr existieren – Beweismaterial entfernt. */
|
||||
@@ -1040,7 +1048,7 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
|
||||
// ueber denselben Weg faelschbar wie zuvor die Manifeste (R171-01).
|
||||
const backlogTampered: number[] = [];
|
||||
const backlogMissing: number[] = [];
|
||||
let backlogSealStatus: 'kein_siegel' | 'intakt' | 'gebrochen' | 'entfernt' = 'kein_siegel';
|
||||
let backlogSealStatus: 'kein_siegel' | 'intakt' | 'gebrochen' | 'entfernt' | 'nicht_noetig' = 'kein_siegel';
|
||||
let backlogSealRoot: string | null = null;
|
||||
|
||||
const siegelKandidaten = await prisma.auditLog.findMany({
|
||||
@@ -1086,6 +1094,13 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
|
||||
const blattAnzahl = await prisma.auditBacklogSeal.count();
|
||||
if (!siegel && blattAnzahl > 0 && siegelSchluessel.length > 0) {
|
||||
backlogSealStatus = 'entfernt';
|
||||
} else if (!siegel && blattAnzahl === 0 && (v3FromId === null || v3FromId <= 1)) {
|
||||
// Es gibt gar keinen Altbestand: Entweder ist alles signiert, oder das Log
|
||||
// beginnt erst mit der Signierung. Dann ist „nicht versiegelt“ kein Mangel
|
||||
// (Pentest R183-03) – die bisherige Warnung liess sich nicht aufloesen,
|
||||
// weil seal-backlog zu Recht ablehnte. Eine Warnung, die der Betreiber
|
||||
// nicht beheben kann, lernt er zu ignorieren.
|
||||
backlogSealStatus = 'nicht_noetig';
|
||||
}
|
||||
|
||||
if (siegel) {
|
||||
@@ -1243,7 +1258,9 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
|
||||
// einzelne Zeile beanstandet ist – sonst bliebe der stille Anker-Verlust
|
||||
// unsichtbar (R174-01).
|
||||
const siegelInOrdnung =
|
||||
backlogSealStatus === 'intakt' || backlogSealStatus === 'kein_siegel';
|
||||
backlogSealStatus === 'intakt' ||
|
||||
backlogSealStatus === 'kein_siegel' ||
|
||||
backlogSealStatus === 'nicht_noetig';
|
||||
|
||||
return {
|
||||
valid: invalidEntries.length === 0 && siegelInOrdnung,
|
||||
|
||||
Reference in New Issue
Block a user