Siegelwechsel ist ein Alarm, kein Hinweis (Pentest R185-01/-02)
R185-01 (MEDIUM): Die Flanke, die wir selbst gemeldet hatten, hat der
Tester live bestaetigt. seal-backlog war beim ZWEITEN Aufruf genauso
gegatet wie beim ersten ({"confirm":"SEAL"} -> 200), und das Ereignis
landete nur im Audit-Log, nicht im Alarmkanal. Sein Punkt: der
automatische Rueckhalt des Gegenbuchs haengt an `valid` - und `valid`
ueberlebt ein ersetzendes Siegel per Konstruktion. Angriff: Altzeile per
DB-Zugriff loeschen, neu siegeln, Luecke ist beglaubigt, valid wieder
true. Live reproduziert.
Zwei Schichten, in seiner Reihenfolge:
1. Alarmkanal. Neuer SecurityEventType AUDIT_SEAL_CHANGED (Migration
20260826120000). Erstes Siegeln HIGH, Ersetzen CRITICAL - geht damit
ueber sendPendingCriticalAlerts sofort per Mail raus. Die Details
halten Wurzel vorher/nachher und den vollstaendigen Vorbefund fest.
2. Gate. Steht bereits ein Siegel, verlangt der Endpunkt
{"confirm":"RESEAL"} statt SEAL, mit einem Text, der sagt, was dabei
verloren geht. Ein Austausch der Beweisgrundlage soll nicht dasselbe
Wort haben wie das Einrichten.
Und im Gegenbuch selbst: dort stand fuer den Wurzelwechsel ein
console.warn, waehrend der Rueckgabecode auf 0 blieb - also exakt das
Muster, das wir dem CRM zweimal angekreidet haben (R179, R183-02), im
Werkzeug, das dagegen gebaut wurde. Jetzt exit 2, mit alter und neuer
Wurzel samt Blattzahl; "10 Blaetter -> 9 Blaetter" zeigt die Loeschung
sofort. Auch die Erstsiegelung meldet sich, statt stillschweigend
uebernommen zu werden.
Aufloesbar gemacht: der Alarm bricht ab, BEVOR angehaengt wird - ohne
Bestaetigungsweg haette auch ein legitimes Siegeln fuer immer alarmiert
(R183-03-Falle). Neu ist NOTARY_SEAL_ACK, bewusst nicht "true", sondern
die Wurzel selbst (mind. 16 Zeichen): ein stehen gelassener Wert passt
beim naechsten Wechsel nicht mehr und kann keinen weiteren Austausch
durchwinken.
R185-02 (LOW): GET /api/audit-logs?action=<x> gab ungueltige Enum-Werte
roh an die Spalte -> 500. Zweifach schlecht: fehlende Validierung und
Fehler-Orakel (200 vs 500 verraet die Enum-Mitglieder). Jetzt 400 mit
der erlaubten Menge im Klartext. Mitgenommen: sensitivity, Datumsfelder,
Zahlenfelder, Textlaengen und ein Deckel auf limit (200), ueber den sich
sonst die ganze Tabelle an der Seitenlogik vorbei ziehen liess. Beide
Endpunkte.
Getestet ueber HTTP gegen eine Wegwerf-DB, inkl. echtem Gegenbuch-Lauf
mit SSH-signiertem lokalem Repo. Zusaetzlich nachgeholt, was der Tester
nicht herstellen konnte: vollstaendig unsigniertes Protokoll ->
kein_siegel statt der frueheren falschen Entwarnung nicht_noetig, und
seal-backlog nennt den fehlenden Schluessel als naechsten Schritt.
Gegenrichtung geprueft, R183-03 bleibt behoben.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,17 @@
|
|||||||
|
-- Neuer SecurityEventType AUDIT_SEAL_CHANGED (Pentest R185-01).
|
||||||
|
--
|
||||||
|
-- Das Setzen oder Ersetzen des Bestandssiegels veraendert die Grundlage, gegen
|
||||||
|
-- die spaeter Manipulation nachgewiesen wird. Bisher landete das ausschliesslich
|
||||||
|
-- als CRITICAL-Zeile im Audit-Log - also in einem Kanal, den ein Mensch lesen
|
||||||
|
-- muss. Der Alarmkanal ist ein separater Store; ohne eigenen Ereignistyp gab es
|
||||||
|
-- dort gar keinen Eintrag, und `valid` bleibt bei einem ersetzenden Siegel
|
||||||
|
-- konstruktionsbedingt `true`.
|
||||||
|
--
|
||||||
|
-- MODIFY COLUMN ist idempotent (setzt die Enum-Definition, mehrfach ausfuehrbar).
|
||||||
|
ALTER TABLE `SecurityEvent`
|
||||||
|
MODIFY COLUMN `type` ENUM(
|
||||||
|
'LOGIN_FAILED','LOGIN_SUCCESS','RATE_LIMIT_HIT','ACCESS_DENIED',
|
||||||
|
'SSRF_BLOCKED','PASSWORD_RESET_REQUEST','PASSWORD_RESET_CONFIRM',
|
||||||
|
'LOGOUT','TOKEN_REJECTED','PERMISSION_CHANGED','AUDIT_SEAL_CHANGED',
|
||||||
|
'SUSPICIOUS'
|
||||||
|
) NOT NULL;
|
||||||
@@ -1482,6 +1482,7 @@ enum SecurityEventType {
|
|||||||
LOGOUT // expliziter Logout
|
LOGOUT // expliziter Logout
|
||||||
TOKEN_REJECTED // ungültiger / abgelaufener / manipulierter JWT
|
TOKEN_REJECTED // ungültiger / abgelaufener / manipulierter JWT
|
||||||
PERMISSION_CHANGED // Admin hat Rolle/Permission geändert
|
PERMISSION_CHANGED // Admin hat Rolle/Permission geändert
|
||||||
|
AUDIT_SEAL_CHANGED // Bestandssiegel gesetzt oder ersetzt (Beweis-Grundlage)
|
||||||
SUSPICIOUS // generischer Catch-All
|
SUSPICIOUS // generischer Catch-All
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,66 @@ import { AuthRequest } from '../types/index.js';
|
|||||||
import * as auditService from '../services/audit.service.js';
|
import * as auditService from '../services/audit.service.js';
|
||||||
import { logChange } from '../services/audit.service.js';
|
import { logChange } from '../services/audit.service.js';
|
||||||
import { AuditAction, AuditSensitivity } from '@prisma/client';
|
import { AuditAction, AuditSensitivity } from '@prisma/client';
|
||||||
|
import { emit as emitSecurityEvent, contextFromRequest } from '../services/securityMonitor.service.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filterwerte aus der Query pruefen (Pentest R185-02).
|
||||||
|
*
|
||||||
|
* Vorher gingen `action` und `sensitivity` als roher String an die Enum-Spalte.
|
||||||
|
* Ein ungueltiger Wert liess Prisma auflaufen und der Handler antwortete 500.
|
||||||
|
* Das ist zweierlei: eine fehlende Validierung – und ein Fehler-Orakel. Wer
|
||||||
|
* 200 gegen 500 vergleicht, liest die Enum-Mitglieder aus, ohne sie zu kennen.
|
||||||
|
* Ein ungueltiger Filter ist eine schlechte ANFRAGE, keine Server-Panne: 400.
|
||||||
|
*
|
||||||
|
* Dasselbe gilt fuer Datumsangaben (`new Date('foo')` ergibt Invalid Date und
|
||||||
|
* sprengt die Query erst in der Datenbank) und fuer Zahlen (`parseInt('x')`
|
||||||
|
* ergibt NaN).
|
||||||
|
*/
|
||||||
|
class FilterFehler extends Error {}
|
||||||
|
|
||||||
|
const AUDIT_ACTIONS = Object.values(AuditAction) as string[];
|
||||||
|
const AUDIT_SENSITIVITIES = Object.values(AuditSensitivity) as string[];
|
||||||
|
|
||||||
|
function pruefeEnum<T extends string>(
|
||||||
|
wert: unknown, erlaubt: string[], feld: string,
|
||||||
|
): T | undefined {
|
||||||
|
if (wert === undefined || wert === '') return undefined;
|
||||||
|
if (typeof wert !== 'string' || !erlaubt.includes(wert)) {
|
||||||
|
// Die erlaubten Werte stehen ohnehin in der Oberflaeche und im Schema –
|
||||||
|
// sie zu nennen verraet nichts und erspart Rateversuche.
|
||||||
|
throw new FilterFehler(
|
||||||
|
`Ungültiger Wert für "${feld}". Erlaubt: ${erlaubt.join(', ')}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return wert as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruefeDatum(wert: unknown, feld: string): Date | undefined {
|
||||||
|
if (wert === undefined || wert === '') return undefined;
|
||||||
|
const d = new Date(wert as string);
|
||||||
|
if (Number.isNaN(d.getTime())) {
|
||||||
|
throw new FilterFehler(`Ungültiges Datum für "${feld}".`);
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruefeZahl(wert: unknown, feld: string): number | undefined {
|
||||||
|
if (wert === undefined || wert === '') return undefined;
|
||||||
|
const n = Number(wert);
|
||||||
|
if (!Number.isInteger(n) || n < 0) {
|
||||||
|
throw new FilterFehler(`Ungültige Zahl für "${feld}".`);
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Freitextfelder begrenzen – unbegrenzte LIKE-Muster sind teuer. */
|
||||||
|
function pruefeText(wert: unknown, feld: string, maxLaenge = 200): string | undefined {
|
||||||
|
if (wert === undefined || wert === '') return undefined;
|
||||||
|
if (typeof wert !== 'string' || wert.length > maxLaenge) {
|
||||||
|
throw new FilterFehler(`Ungültiger Wert für "${feld}" (max. ${maxLaenge} Zeichen).`);
|
||||||
|
}
|
||||||
|
return wert;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Audit-Logs mit Filtern abrufen
|
* Audit-Logs mit Filtern abrufen
|
||||||
@@ -26,23 +86,29 @@ export async function getAuditLogs(req: AuthRequest, res: Response) {
|
|||||||
} = req.query;
|
} = req.query;
|
||||||
|
|
||||||
const result = await auditService.searchAuditLogs({
|
const result = await auditService.searchAuditLogs({
|
||||||
userId: userId ? parseInt(userId as string) : undefined,
|
userId: pruefeZahl(userId, 'userId'),
|
||||||
customerId: customerId ? parseInt(customerId as string) : undefined,
|
customerId: pruefeZahl(customerId, 'customerId'),
|
||||||
dataSubjectId: dataSubjectId ? parseInt(dataSubjectId as string) : undefined,
|
dataSubjectId: pruefeZahl(dataSubjectId, 'dataSubjectId'),
|
||||||
action: action as AuditAction | undefined,
|
action: pruefeEnum<AuditAction>(action, AUDIT_ACTIONS, 'action'),
|
||||||
sensitivity: sensitivity as AuditSensitivity | undefined,
|
sensitivity: pruefeEnum<AuditSensitivity>(sensitivity, AUDIT_SENSITIVITIES, 'sensitivity'),
|
||||||
resourceType: resourceType as string | undefined,
|
resourceType: pruefeText(resourceType, 'resourceType', 100),
|
||||||
resourceId: resourceId as string | undefined,
|
resourceId: pruefeText(resourceId, 'resourceId', 100),
|
||||||
startDate: startDate ? new Date(startDate as string) : undefined,
|
startDate: pruefeDatum(startDate, 'startDate'),
|
||||||
endDate: endDate ? new Date(endDate as string) : undefined,
|
endDate: pruefeDatum(endDate, 'endDate'),
|
||||||
success: success !== undefined ? success === 'true' : undefined,
|
success: success !== undefined ? success === 'true' : undefined,
|
||||||
search: search as string | undefined,
|
search: pruefeText(search, 'search'),
|
||||||
page: page ? parseInt(page as string) : 1,
|
page: pruefeZahl(page, 'page') || 1,
|
||||||
limit: limit ? parseInt(limit as string) : 50,
|
// Deckel: sonst laesst sich ueber `limit` die gesamte Tabelle in einem
|
||||||
|
// Zug ziehen, an der Seitenlogik vorbei.
|
||||||
|
limit: Math.min(pruefeZahl(limit, 'limit') || 50, 200),
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ success: true, ...result });
|
res.json({ success: true, ...result });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof FilterFehler) {
|
||||||
|
res.status(400).json({ success: false, error: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.error('Fehler beim Abrufen der Audit-Logs:', error);
|
console.error('Fehler beim Abrufen der Audit-Logs:', error);
|
||||||
res.status(500).json({ success: false, error: 'Fehler beim Abrufen der Audit-Logs' });
|
res.status(500).json({ success: false, error: 'Fehler beim Abrufen der Audit-Logs' });
|
||||||
}
|
}
|
||||||
@@ -96,7 +162,7 @@ export async function getAuditLogsByCustomer(req: AuthRequest, res: Response) {
|
|||||||
*/
|
*/
|
||||||
export async function exportAuditLogs(req: AuthRequest, res: Response) {
|
export async function exportAuditLogs(req: AuthRequest, res: Response) {
|
||||||
try {
|
try {
|
||||||
const format = (req.query.format as 'json' | 'csv') || 'json';
|
const format = req.query.format === 'csv' ? 'csv' : 'json';
|
||||||
const {
|
const {
|
||||||
action,
|
action,
|
||||||
sensitivity,
|
sensitivity,
|
||||||
@@ -107,11 +173,11 @@ export async function exportAuditLogs(req: AuthRequest, res: Response) {
|
|||||||
|
|
||||||
const content = await auditService.exportAuditLogs(
|
const content = await auditService.exportAuditLogs(
|
||||||
{
|
{
|
||||||
action: action as AuditAction | undefined,
|
action: pruefeEnum<AuditAction>(action, AUDIT_ACTIONS, 'action'),
|
||||||
sensitivity: sensitivity as AuditSensitivity | undefined,
|
sensitivity: pruefeEnum<AuditSensitivity>(sensitivity, AUDIT_SENSITIVITIES, 'sensitivity'),
|
||||||
resourceType: resourceType as string | undefined,
|
resourceType: pruefeText(resourceType, 'resourceType', 100),
|
||||||
startDate: startDate ? new Date(startDate as string) : undefined,
|
startDate: pruefeDatum(startDate, 'startDate'),
|
||||||
endDate: endDate ? new Date(endDate as string) : undefined,
|
endDate: pruefeDatum(endDate, 'endDate'),
|
||||||
},
|
},
|
||||||
format
|
format
|
||||||
);
|
);
|
||||||
@@ -126,6 +192,10 @@ export async function exportAuditLogs(req: AuthRequest, res: Response) {
|
|||||||
res.json({ success: true, data: JSON.parse(content) })
|
res.json({ success: true, data: JSON.parse(content) })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof FilterFehler) {
|
||||||
|
res.status(400).json({ success: false, error: error.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
console.error('Fehler beim Exportieren der Audit-Logs:', error);
|
console.error('Fehler beim Exportieren der Audit-Logs:', error);
|
||||||
res.status(500).json({ success: false, error: 'Fehler beim Exportieren' });
|
res.status(500).json({ success: false, error: 'Fehler beim Exportieren' });
|
||||||
}
|
}
|
||||||
@@ -338,25 +408,90 @@ export async function getCheckpoint(req: AuthRequest, res: Response) {
|
|||||||
*/
|
*/
|
||||||
export async function sealBacklog(req: AuthRequest, res: Response) {
|
export async function sealBacklog(req: AuthRequest, res: Response) {
|
||||||
try {
|
try {
|
||||||
if (req.body?.confirm !== 'SEAL') {
|
// Erst nachsehen, ob schon ein Siegel steht (Pentest R185-01).
|
||||||
|
//
|
||||||
|
// Ein zweites Siegeln ist etwas grundlegend anderes als das erste: Es
|
||||||
|
// ERSETZT die Grundlage, gegen die Manipulation nachgewiesen wird. Wer den
|
||||||
|
// Altbestand per Datenbankzugriff beschneidet und danach neu siegelt,
|
||||||
|
// bekommt eine passende Wurzel und eine ueber den neuen Vorbefund
|
||||||
|
// beglaubigte Luecke - und `valid` steht wieder auf `true`. Deshalb
|
||||||
|
// verlangt das Ersetzen ein eigenes Wort und nicht dasselbe wie das
|
||||||
|
// Einrichten.
|
||||||
|
const vorher = await auditService.verifyIntegrity();
|
||||||
|
const siegelSteht =
|
||||||
|
vorher.backlogSealStatus === 'intakt' ||
|
||||||
|
vorher.backlogSealStatus === 'gebrochen' ||
|
||||||
|
vorher.backlogSealStatus === 'entfernt';
|
||||||
|
|
||||||
|
const erwartet = siegelSteht ? 'RESEAL' : 'SEAL';
|
||||||
|
if (req.body?.confirm !== erwartet) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error:
|
error: siegelSteht
|
||||||
'Versiegelt den aktuellen Stand des Altbestands. Zum Bestätigen ' +
|
? 'Es besteht bereits ein Bestandssiegel. Erneutes Siegeln ERSETZT die ' +
|
||||||
'{"confirm":"SEAL"} mitsenden.',
|
'bisherige Beweisgrundlage: bestehende Lücken werden neu beglaubigt und ' +
|
||||||
|
'ein Befund am Altbestand verschwindet aus der Prüfung. Das ist kein ' +
|
||||||
|
'Wartungsschritt. Zum Bestätigen {"confirm":"RESEAL"} mitsenden – und ' +
|
||||||
|
'vorher klären, warum das bisherige Siegel nicht mehr passt.'
|
||||||
|
: 'Versiegelt den aktuellen Stand des Altbestands. Zum Bestätigen ' +
|
||||||
|
'{"confirm":"SEAL"} mitsenden.',
|
||||||
|
aktuellerSiegelzustand: vorher.backlogSealStatus,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await auditService.sealBacklog({
|
const result = await auditService.sealBacklog({
|
||||||
userEmail: req.user?.email,
|
userEmail: req.user?.email,
|
||||||
ipAddress: req.ip || (req.socket as any)?.remoteAddress,
|
ipAddress: req.ip || (req.socket as any)?.remoteAddress,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// In den ALARMKANAL, nicht nur ins Audit-Log (Pentest R185-01).
|
||||||
|
//
|
||||||
|
// Die CRITICAL-Zeile im Audit-Log gab es schon - aber die muss jemand
|
||||||
|
// lesen, und genau das ist die R183-02/R184-01-Klasse. Der automatische
|
||||||
|
// Rueckhalt des Gegenbuchs haengt an `valid`, und `valid` ueberlebt ein
|
||||||
|
// ersetzendes Siegel per Konstruktion. Der einzige maschinell erkennbare
|
||||||
|
// Anker ist der Wechsel der Wurzel - der gehoert dorthin, wo etwas von
|
||||||
|
// selbst passiert.
|
||||||
|
const ctx = contextFromRequest(req);
|
||||||
|
emitSecurityEvent({
|
||||||
|
type: 'AUDIT_SEAL_CHANGED',
|
||||||
|
severity: siegelSteht ? 'CRITICAL' : 'HIGH',
|
||||||
|
message: siegelSteht
|
||||||
|
? `Bestandssiegel ERSETZT (${result.sealedCount} Alteinträge, id ${result.fromId}–` +
|
||||||
|
`${result.toId}). Die bisherige Beweisgrundlage gilt nicht mehr; der Zustand ` +
|
||||||
|
`davor war "${vorher.backlogSealStatus}". Wenn das keine geplante Maßnahme war, ` +
|
||||||
|
'ist es ein Befund.'
|
||||||
|
: `Bestandssiegel erstmals gesetzt (${result.sealedCount} Alteinträge, id ` +
|
||||||
|
`${result.fromId}–${result.toId}). Ab jetzt fallen Änderungen am Altbestand auf.`,
|
||||||
|
ipAddress: ctx.ipAddress,
|
||||||
|
userId: req.user?.userId,
|
||||||
|
userEmail: req.user?.email,
|
||||||
|
endpoint: ctx.endpoint,
|
||||||
|
details: {
|
||||||
|
ersetzt: siegelSteht,
|
||||||
|
zustandVorher: vorher.backlogSealStatus,
|
||||||
|
wurzelVorher: vorher.backlogSealRoot,
|
||||||
|
wurzelNachher: result.root,
|
||||||
|
befundVorher: {
|
||||||
|
manipuliert: vorher.tamperedEntries,
|
||||||
|
luecken: vorher.chainGaps,
|
||||||
|
altbestandVeraendert: vorher.backlogTampered,
|
||||||
|
altbestandFehlend: vorher.backlogMissing,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
data: result,
|
data: result,
|
||||||
message:
|
message:
|
||||||
`${result.sealedCount} Alteinträge (id ${result.fromId}–${result.toId}) versiegelt. ` +
|
`${result.sealedCount} Alteinträge (id ${result.fromId}–${result.toId}) versiegelt. ` +
|
||||||
'Spätere Änderungen an diesen Einträgen fallen ab sofort auf.',
|
'Spätere Änderungen an diesen Einträgen fallen ab sofort auf.' +
|
||||||
|
(siegelSteht
|
||||||
|
? ' ACHTUNG: Dies war ein ERSETZENDES Siegel – die vorherige Beweisgrundlage ' +
|
||||||
|
'gilt nicht mehr.'
|
||||||
|
: ''),
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fehler beim Versiegeln des Altbestands:', error);
|
console.error('Fehler beim Versiegeln des Altbestands:', error);
|
||||||
|
|||||||
@@ -97,6 +97,66 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ Erledigt
|
||||||
|
|
||||||
|
- [x] **🚨 Siegelwechsel ist ein Alarm, kein Hinweis + Filter-Validierung (Pentest R185)** (2026-08-26)
|
||||||
|
- **R185-01 (MEDIUM)** – Die Flanke, die wir dem Tester selbst gemeldet
|
||||||
|
hatten, hat er live bestätigt: `seal-backlog` war beim **zweiten** Aufruf
|
||||||
|
genauso gegatet wie beim ersten (`{"confirm":"SEAL"}` → 200), und das
|
||||||
|
Ereignis landete nur im Audit-Log, nicht im Alarmkanal. Sein Punkt: der
|
||||||
|
automatische Rückhalt des Gegenbuchs hängt an `valid` – und `valid`
|
||||||
|
überlebt ein ersetzendes Siegel **per Konstruktion**. Angriff: Altzeile
|
||||||
|
per DB-Zugriff löschen → neu siegeln → Lücke ist beglaubigt, `valid`
|
||||||
|
wieder `true`. **Live reproduziert.**
|
||||||
|
- Zwei Schichten, in seiner Reihenfolge:
|
||||||
|
1. **Alarmkanal.** Neuer `SecurityEventType` `AUDIT_SEAL_CHANGED` (Migration
|
||||||
|
`20260826120000`). Erstes Siegeln → HIGH, Ersetzen → **CRITICAL** (geht
|
||||||
|
damit über `sendPendingCriticalAlerts` sofort per Mail raus). Die
|
||||||
|
Ereignis-Details halten Wurzel vorher/nachher und den vollständigen
|
||||||
|
Vorbefund fest.
|
||||||
|
2. **Gate.** Steht bereits ein Siegel, verlangt der Endpunkt
|
||||||
|
`{"confirm":"RESEAL"}` statt `SEAL` – mit einem Text, der sagt, was
|
||||||
|
dabei verloren geht. Ein Austausch der Beweisgrundlage soll nicht
|
||||||
|
dasselbe Wort haben wie das Einrichten.
|
||||||
|
- **Und im Gegenbuch selbst:** Dort stand für den Wurzelwechsel ein
|
||||||
|
`console.warn`, während der Rückgabecode auf **0** blieb – also exakt das
|
||||||
|
Muster, das wir dem CRM zweimal angekreidet haben (R179/R183-02), im
|
||||||
|
Werkzeug, das dagegen gebaut wurde. Jetzt **exit 2**, mit alter und neuer
|
||||||
|
Wurzel samt Blattzahl (`10 Blätter → 9 Blätter` zeigt die Löschung
|
||||||
|
sofort). Auch die Erstsiegelung meldet sich, statt stillschweigend
|
||||||
|
übernommen zu werden.
|
||||||
|
- **Auflösbar gemacht:** Der Alarm bricht ab, *bevor* angehängt wird – ohne
|
||||||
|
Bestätigungsweg hätte auch ein legitimes Siegeln für immer alarmiert
|
||||||
|
(R183-03-Falle). Neu: `NOTARY_SEAL_ACK`. Bewusst **nicht** `true`, sondern
|
||||||
|
die **Wurzel selbst** (mind. 16 Zeichen): ein stehen gelassener Wert passt
|
||||||
|
beim nächsten Wechsel nicht mehr und kann keinen weiteren Austausch
|
||||||
|
durchwinken – der Unterschied zu `NOTARY_GENESIS_ACK`, wo genau diese
|
||||||
|
Falle dokumentiert werden musste.
|
||||||
|
- **R185-02 (LOW)** – `GET /api/audit-logs?action=<x>`: ungültige Enum-Werte
|
||||||
|
gingen roh an die Spalte → **500**. Zweifach schlecht: fehlende
|
||||||
|
Validierung *und* Fehler-Orakel (200 vs. 500 verrät die Enum-Mitglieder).
|
||||||
|
Jetzt 400 mit der erlaubten Menge im Klartext – die steht ohnehin in der
|
||||||
|
Oberfläche. Gleich mitgenommen: `sensitivity`, Datumsfelder
|
||||||
|
(`new Date('foo')` → Invalid Date → 500), Zahlenfelder (`parseInt` → NaN),
|
||||||
|
Textlängen, und ein Deckel auf `limit` (200), über den sich sonst die
|
||||||
|
ganze Tabelle an der Seitenlogik vorbei ziehen ließ. Beide Endpunkte
|
||||||
|
(`/audit-logs` und `/audit-logs/export`).
|
||||||
|
- **Getestet über HTTP gegen eine Wegwerf-DB**, inkl. echtem Gegenbuch-Lauf
|
||||||
|
mit SSH-signiertem lokalem Repo: Erstsiegeln mit `SEAL` → 200; zweites mit
|
||||||
|
`SEAL` → **400**; mit `RESEAL` → 200; beide Alarmkanal-Ereignisse mit
|
||||||
|
korrekter Severity vorhanden. Wäsche (Zeile 7 gelöscht → RESEAL) → CRM
|
||||||
|
meldet `valid:true`, **Gegenbuch exit=2**. Bestätigung: falsche Wurzel →
|
||||||
|
weiter exit 2, zu kurzer Wert → weiter exit 2, richtige Wurzel → exit 0
|
||||||
|
und beglaubigt, Folgelauf ruhig.
|
||||||
|
- **Nachgeholt, was der Tester nicht herstellen konnte:** vollständig
|
||||||
|
unsigniertes Protokoll (Platzhalter-`AUDIT_HMAC_KEY`) → `kein_siegel`
|
||||||
|
statt der früheren falschen Entwarnung `nicht_noetig`, und
|
||||||
|
`seal-backlog` nennt den fehlenden Schlüssel als nächsten Schritt – die
|
||||||
|
Warnung ist also auflösbar. Gegenrichtung (Log beginnt signiert) →
|
||||||
|
weiterhin `nicht_noetig`, R183-03 bleibt behoben.
|
||||||
|
- Dateien: `backend/src/controllers/auditLog.controller.ts`,
|
||||||
|
`backend/prisma/schema.prisma` + Migration, `tools/audit-notary/notary.mjs`,
|
||||||
|
`tools/audit-notary/{docker-compose.yml,.env.example,README.md}`,
|
||||||
|
`frontend/src/services/api.ts`, `frontend/src/pages/settings/Monitoring.tsx`
|
||||||
|
|
||||||
- [x] **👁️ Integritätsstatus in der Oberfläche (Einstellungen → Audit-Protokoll)** (2026-08-26)
|
- [x] **👁️ Integritätsstatus in der Oberfläche (Einstellungen → Audit-Protokoll)** (2026-08-26)
|
||||||
- Bisher war der Zustand der Hash-Kette nur per `POST /api/audit-logs/verify`
|
- Bisher war der Zustand der Hash-Kette nur per `POST /api/audit-logs/verify`
|
||||||
einsehbar – also praktisch nur für das Gegenbuch und für jemanden mit
|
einsehbar – also praktisch nur für das Gegenbuch und für jemanden mit
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const TYPE_OPTIONS: { value: SecurityEventType | ''; label: string }[] = [
|
|||||||
{ value: 'LOGOUT', label: 'Logout' },
|
{ value: 'LOGOUT', label: 'Logout' },
|
||||||
{ value: 'TOKEN_REJECTED', label: 'Token abgelehnt' },
|
{ value: 'TOKEN_REJECTED', label: 'Token abgelehnt' },
|
||||||
{ value: 'PERMISSION_CHANGED', label: 'Berechtigung geändert' },
|
{ value: 'PERMISSION_CHANGED', label: 'Berechtigung geändert' },
|
||||||
|
{ value: 'AUDIT_SEAL_CHANGED', label: 'Bestandssiegel gesetzt/ersetzt' },
|
||||||
{ value: 'SUSPICIOUS', label: 'Verdächtig (Threshold)' },
|
{ value: 'SUSPICIOUS', label: 'Verdächtig (Threshold)' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1872,7 +1872,7 @@ export interface EmailLog {
|
|||||||
export type SecurityEventType =
|
export type SecurityEventType =
|
||||||
| 'LOGIN_FAILED' | 'LOGIN_SUCCESS' | 'RATE_LIMIT_HIT' | 'ACCESS_DENIED'
|
| 'LOGIN_FAILED' | 'LOGIN_SUCCESS' | 'RATE_LIMIT_HIT' | 'ACCESS_DENIED'
|
||||||
| 'SSRF_BLOCKED' | 'PASSWORD_RESET_REQUEST' | 'PASSWORD_RESET_CONFIRM'
|
| 'SSRF_BLOCKED' | 'PASSWORD_RESET_REQUEST' | 'PASSWORD_RESET_CONFIRM'
|
||||||
| 'LOGOUT' | 'TOKEN_REJECTED' | 'PERMISSION_CHANGED' | 'SUSPICIOUS';
|
| 'LOGOUT' | 'TOKEN_REJECTED' | 'PERMISSION_CHANGED' | 'AUDIT_SEAL_CHANGED' | 'SUSPICIOUS';
|
||||||
|
|
||||||
export type SecuritySeverity = 'INFO' | 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
export type SecuritySeverity = 'INFO' | 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,17 @@ PROD_GENESIS_ACK=
|
|||||||
# geklärt hast, warum. Siehe README, Abschnitt "Wenn das Gedächtnis fehlt".
|
# geklärt hast, warum. Siehe README, Abschnitt "Wenn das Gedächtnis fehlt".
|
||||||
PROD_ADOPT_ACK=
|
PROD_ADOPT_ACK=
|
||||||
|
|
||||||
|
# Normalerweise leer lassen.
|
||||||
|
# Gültige Werte: die neue Siegelwurzel (mind. 16 Zeichen) | (leer)
|
||||||
|
# Das Gegenbuch schlägt Alarm, wenn sich die Wurzel des Bestandssiegels
|
||||||
|
# ändert – denn ein erneutes Siegeln ersetzt die Grundlage, gegen die
|
||||||
|
# Manipulation nachgewiesen wird. War der Wechsel gewollt, hier die Wurzel
|
||||||
|
# eintragen, die der Alarm nennt, einmal laufen lassen und wieder leeren.
|
||||||
|
# Bewusst KEIN "true": ein stehen gelassener Wert passt beim nächsten
|
||||||
|
# Wechsel nicht mehr und kann darum keinen weiteren stillschweigend
|
||||||
|
# durchwinken.
|
||||||
|
PROD_SEAL_ACK=
|
||||||
|
|
||||||
# Wo das Buch liegt – relativ zu diesem Verzeichnis.
|
# Wo das Buch liegt – relativ zu diesem Verzeichnis.
|
||||||
# DIESES VERZEICHNIS GEHÖRT INS BACKUP (enthält Buch und Signaturschlüssel).
|
# DIESES VERZEICHNIS GEHÖRT INS BACKUP (enthält Buch und Signaturschlüssel).
|
||||||
PROD_DIR=./data/prod
|
PROD_DIR=./data/prod
|
||||||
@@ -71,4 +82,5 @@ STAGING_CRM_PASSWORD=
|
|||||||
STAGING_INTERVAL=3600
|
STAGING_INTERVAL=3600
|
||||||
STAGING_GENESIS_ACK=
|
STAGING_GENESIS_ACK=
|
||||||
STAGING_ADOPT_ACK=
|
STAGING_ADOPT_ACK=
|
||||||
|
STAGING_SEAL_ACK=
|
||||||
STAGING_DIR=./data/staging
|
STAGING_DIR=./data/staging
|
||||||
|
|||||||
@@ -138,6 +138,45 @@ Sie verschwinden also nicht aus dem Bericht – sie zählen nur nicht mehr als
|
|||||||
offener Befund. Jede **neue** Lücke, jede veränderte oder entfernte Altzeile
|
offener Befund. Jede **neue** Lücke, jede veränderte oder entfernte Altzeile
|
||||||
und jedes gebrochene Siegel lösen weiterhin sofort Alarm aus.
|
und jedes gebrochene Siegel lösen weiterhin sofort Alarm aus.
|
||||||
|
|
||||||
|
### Wenn sich die Siegelwurzel ändert: Alarm, und warum
|
||||||
|
|
||||||
|
Erneutes Siegeln **ersetzt** die Grundlage, gegen die Manipulation nachgewiesen
|
||||||
|
wird. Wer eine Altzeile per Datenbankzugriff entfernt und danach neu siegelt,
|
||||||
|
bekommt eine passende Wurzel und eine beglaubigte Lücke — und die Prüfung im
|
||||||
|
CRM meldet wieder `valid: true`. Der Wechsel der Wurzel ist die einzige Spur
|
||||||
|
davon, die eine Maschine sehen kann.
|
||||||
|
|
||||||
|
Deshalb ist er ein **Alarm** (exit 2), kein Hinweis. Vorher stand hier eine
|
||||||
|
Zeile Prosa, während der Rückgabecode auf 0 blieb — also genau das Muster, das
|
||||||
|
wir dem CRM selbst zweimal angekreidet haben.
|
||||||
|
|
||||||
|
Der Alarm nennt die alte und die neue Wurzel samt Blattzahl. Ein Sprung von
|
||||||
|
`10 Blätter` auf `9 Blätter` sagt sofort, was passiert ist.
|
||||||
|
|
||||||
|
**War der Wechsel gewollt** (typisch: dein einmaliges Erstsiegeln), bestätigst
|
||||||
|
du ihn mit der Wurzel, die der Alarm ausgibt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# in der .env
|
||||||
|
PROD_SEAL_ACK=72062c88a8b6e2b53b30496b483885cd
|
||||||
|
|
||||||
|
docker compose up -d # ein Lauf – die neue Wurzel wird beglaubigt
|
||||||
|
PROD_SEAL_ACK= # danach wieder leeren
|
||||||
|
```
|
||||||
|
|
||||||
|
Bestätigt wird bewusst **nicht** mit `true`, sondern mit der Wurzel selbst.
|
||||||
|
Ein versehentlich stehen gelassener Wert passt beim nächsten Wechsel nicht mehr
|
||||||
|
und kann deshalb keinen weiteren Austausch stillschweigend durchwinken.
|
||||||
|
|
||||||
|
**War er nicht gewollt**, sieh im CRM nach: das Sicherheits-Ereignis
|
||||||
|
`AUDIT_SEAL_CHANGED` (Einstellungen → Monitoring) und die CRITICAL-Zeile zu
|
||||||
|
`/api/audit-logs/seal-backlog` im Audit-Protokoll nennen Konto, Zeitpunkt und
|
||||||
|
den Befund, der vor dem Siegeln galt.
|
||||||
|
|
||||||
|
Im CRM selbst ist erneutes Siegeln zusätzlich gegatet: es verlangt
|
||||||
|
`{"confirm":"RESEAL"}` statt `{"confirm":"SEAL"}` — ein Austausch der
|
||||||
|
Beweisgrundlage soll nicht dasselbe Wort haben wie das Einrichten.
|
||||||
|
|
||||||
### Zugang einrichten (das brauchst du vorher)
|
### Zugang einrichten (das brauchst du vorher)
|
||||||
|
|
||||||
Das Gegenbuch braucht ein **eigenes Benutzerkonto** im CRM – kein Token. Der
|
Das Gegenbuch braucht ein **eigenes Benutzerkonto** im CRM – kein Token. Der
|
||||||
@@ -442,7 +481,7 @@ Deshalb gilt jetzt:
|
|||||||
|---|---|
|
|---|---|
|
||||||
| 0 | alles in Ordnung, Checkpoint angehängt (bzw. Prüfung bestanden) |
|
| 0 | alles in Ordnung, Checkpoint angehängt (bzw. Prüfung bestanden) |
|
||||||
| 1 | Betriebsfehler (Konfiguration, Commit oder Push fehlgeschlagen) |
|
| 1 | Betriebsfehler (Konfiguration, Commit oder Push fehlgeschlagen) |
|
||||||
| 2 | **Befund** – Widerspruch zwischen CRM und Gegenbuch, oder ungültige Signatur |
|
| 2 | **Befund** – Widerspruch zwischen CRM und Gegenbuch, ungültige Signatur, oder die Wurzel des Bestandssiegels hat sich geändert (`NOTARY_SEAL_ACK`) |
|
||||||
| 5 | **Anker unvollständig** – die Kette ist gültig, aber `refs/notary/seq-N` fehlt. Reparierbar durch einen Notar-Schreiblauf |
|
| 5 | **Anker unvollständig** – die Kette ist gültig, aber `refs/notary/seq-N` fehlt. Reparierbar durch einen Notar-Schreiblauf |
|
||||||
| 4 | **Wächter-Gedächtnis fehlt** – Erstinbetriebnahme unbestätigt, oder Speicher nach der Etablierung verloren |
|
| 4 | **Wächter-Gedächtnis fehlt** – Erstinbetriebnahme unbestätigt, oder Speicher nach der Etablierung verloren |
|
||||||
| 3 | beglaubigter Stand nicht abschließend feststellbar – Remote fehlt/unerreichbar, erste Beobachtung, Zurückspulen nicht ausschließbar, **oder** Checkpoint erstellt aber nicht verankert |
|
| 3 | beglaubigter Stand nicht abschließend feststellbar – Remote fehlt/unerreichbar, erste Beobachtung, Zurückspulen nicht ausschließbar, **oder** Checkpoint erstellt aber nicht verankert |
|
||||||
@@ -491,6 +530,8 @@ an, bis er geklärt ist.
|
|||||||
| Untergeschobener Commit | Commit ohne gültige Signatur in der Historie |
|
| Untergeschobener Commit | Commit ohne gültige Signatur in der Historie |
|
||||||
| Nie gepushte lokale Commits | Abgleich gegen den Remote-Kopf |
|
| Nie gepushte lokale Commits | Abgleich gegen den Remote-Kopf |
|
||||||
| Bestandssiegel-Blätter entfernt | beglaubigte Blattzahl auf null gefallen |
|
| Bestandssiegel-Blätter entfernt | beglaubigte Blattzahl auf null gefallen |
|
||||||
|
| **Altzeile gelöscht und neu gesiegelt** (Wäsche) | **Wurzel des Bestandssiegels hat gewechselt – `valid` allein bleibt dabei `true`** |
|
||||||
|
| Erstmals gesiegelt, ohne dass es jemand veranlasst hat | vorher keine Wurzel beglaubigt, jetzt eine |
|
||||||
|
|
||||||
Bei jedem dieser Fälle bricht das Skript mit **Exit-Code 2** ab und **hängt
|
Bei jedem dieser Fälle bricht das Skript mit **Exit-Code 2** ab und **hängt
|
||||||
nichts an** – der manipulierte Zustand wird also nicht als neue Wahrheit
|
nichts an** – der manipulierte Zustand wird also nicht als neue Wahrheit
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ services:
|
|||||||
NOTARY_GENESIS_ACK: ${PROD_GENESIS_ACK:-}
|
NOTARY_GENESIS_ACK: ${PROD_GENESIS_ACK:-}
|
||||||
# Nur nach geklärtem Verlust des Beobachtungsspeichers, siehe README:
|
# Nur nach geklärtem Verlust des Beobachtungsspeichers, siehe README:
|
||||||
NOTARY_ADOPT_ACK: ${PROD_ADOPT_ACK:-}
|
NOTARY_ADOPT_ACK: ${PROD_ADOPT_ACK:-}
|
||||||
|
# Nur nach einem GEWOLLTEN Siegelwechsel, mit der neuen Wurzel:
|
||||||
|
NOTARY_SEAL_ACK: ${PROD_SEAL_ACK:-}
|
||||||
volumes:
|
volumes:
|
||||||
# Buch, Schlüssel, Beobachtungsspeicher und Statusdatei.
|
# Buch, Schlüssel, Beobachtungsspeicher und Statusdatei.
|
||||||
# Dieses Verzeichnis ist das Gegenbuch – es gehört ins Backup.
|
# Dieses Verzeichnis ist das Gegenbuch – es gehört ins Backup.
|
||||||
@@ -53,5 +55,6 @@ services:
|
|||||||
NOTAR_EMAIL: ${NOTAR_EMAIL:-gegenbuch@localhost}
|
NOTAR_EMAIL: ${NOTAR_EMAIL:-gegenbuch@localhost}
|
||||||
NOTARY_GENESIS_ACK: ${STAGING_GENESIS_ACK:-}
|
NOTARY_GENESIS_ACK: ${STAGING_GENESIS_ACK:-}
|
||||||
NOTARY_ADOPT_ACK: ${STAGING_ADOPT_ACK:-}
|
NOTARY_ADOPT_ACK: ${STAGING_ADOPT_ACK:-}
|
||||||
|
NOTARY_SEAL_ACK: ${STAGING_SEAL_ACK:-}
|
||||||
volumes:
|
volumes:
|
||||||
- ${STAGING_DIR:-./data/staging}:/gegenbuch
|
- ${STAGING_DIR:-./data/staging}:/gegenbuch
|
||||||
|
|||||||
@@ -135,6 +135,24 @@ if (!SIGN && process.env.NOTARY_INSECURE_ACK !== 'mir-ist-klar-dass-das-ungeschu
|
|||||||
const CRM_EMAIL = process.env.CRM_EMAIL;
|
const CRM_EMAIL = process.env.CRM_EMAIL;
|
||||||
const CRM_PASSWORD = process.env.CRM_PASSWORD;
|
const CRM_PASSWORD = process.env.CRM_PASSWORD;
|
||||||
|
|
||||||
|
// Bestaetigung fuer einen Wechsel der Siegelwurzel (Pentest R185-01).
|
||||||
|
//
|
||||||
|
// Ein Wechsel loest Alarm aus – und der Alarm bricht ab, BEVOR der neue Stand
|
||||||
|
// ins Gegenbuch kommt. Ohne Bestaetigungsweg wuerde deshalb auch ein voellig
|
||||||
|
// legitimes Siegeln von da an bei jedem Lauf erneut alarmieren, ohne dass der
|
||||||
|
// Betreiber es je aufloesen koennte. Genau daran stirbt eine Warnung
|
||||||
|
// (dieselbe Lehre wie R183-03).
|
||||||
|
//
|
||||||
|
// Bestaetigt wird deshalb nicht pauschal mit „true“, sondern mit der WURZEL,
|
||||||
|
// die man akzeptiert – mindestens 16 Zeichen. Ein versehentlich stehen
|
||||||
|
// gelassener Wert passt beim naechsten Wechsel nicht mehr und kann darum
|
||||||
|
// keinen weiteren Austausch stillschweigend durchwinken. Das ist der
|
||||||
|
// Unterschied zu NOTARY_GENESIS_ACK, wo genau diese Falle dokumentiert
|
||||||
|
// werden musste.
|
||||||
|
const SEAL_ACK = (process.env.NOTARY_SEAL_ACK || '').trim();
|
||||||
|
const siegelBestaetigt = (wurzel) =>
|
||||||
|
SEAL_ACK.length >= 16 && !!wurzel && wurzel.startsWith(SEAL_ACK);
|
||||||
|
|
||||||
if (!CRM_URL) {
|
if (!CRM_URL) {
|
||||||
console.error('CRM_URL muss gesetzt sein.');
|
console.error('CRM_URL muss gesetzt sein.');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -774,10 +792,59 @@ if (letzter) {
|
|||||||
if (letzter.sealLeafCount > 0 && aktuell.sealLeafCount === 0) {
|
if (letzter.sealLeafCount > 0 && aktuell.sealLeafCount === 0) {
|
||||||
alarm('Die Blattwerte des Bestandssiegels wurden entfernt.');
|
alarm('Die Blattwerte des Bestandssiegels wurden entfernt.');
|
||||||
}
|
}
|
||||||
|
// Wechsel der Siegelwurzel ist ein ALARM, kein Hinweis (Pentest R185-01).
|
||||||
|
//
|
||||||
|
// Vorher stand hier ein console.warn und der Rueckgabecode blieb 0 – also
|
||||||
|
// genau das Muster, das wir dem CRM zweimal angekreidet haben: die Warnung
|
||||||
|
// steht in der Prosa, die Maschine meldet „in Ordnung“. Das ist hier
|
||||||
|
// besonders teuer, weil `valid` ein ERSETZENDES Siegel per Konstruktion
|
||||||
|
// ueberlebt: Wer eine Altzeile per Datenbankzugriff entfernt und danach neu
|
||||||
|
// siegelt, bekommt eine passende Wurzel und eine beglaubigte Luecke. Die
|
||||||
|
// Vollpruefung sagt dann `true`. Der Wurzelwechsel ist der EINZIGE
|
||||||
|
// maschinell erkennbare Anker dagegen – und der gehoert in den Alarm.
|
||||||
|
//
|
||||||
|
// Ein legitimes Neu-Siegeln loest hier einmal aus. Das ist gewollt: ein
|
||||||
|
// Austausch der Beweisgrundlage soll einmal wehtun und bestaetigt werden,
|
||||||
|
// statt lautlos durchzulaufen.
|
||||||
if (letzter.sealRoot && aktuell.sealRoot && letzter.sealRoot !== aktuell.sealRoot) {
|
if (letzter.sealRoot && aktuell.sealRoot && letzter.sealRoot !== aktuell.sealRoot) {
|
||||||
console.warn(
|
if (siegelBestaetigt(aktuell.sealRoot)) {
|
||||||
`HINWEIS: Das Bestandssiegel wurde erneuert (${letzter.sealRoot.slice(0, 12)}… → ` +
|
console.log(
|
||||||
`${aktuell.sealRoot.slice(0, 12)}…). Legitim nach einem Retention-Lauf – sonst prüfen.`,
|
`Siegelwechsel bestätigt (NOTARY_SEAL_ACK): ${letzter.sealRoot.slice(0, 16)}… → ` +
|
||||||
|
`${aktuell.sealRoot.slice(0, 16)}…. Die neue Wurzel wird beglaubigt.\n` +
|
||||||
|
' NOTARY_SEAL_ACK danach wieder leeren.',
|
||||||
|
);
|
||||||
|
} else alarm(
|
||||||
|
'Das Bestandssiegel wurde ERSETZT – die beglaubigte Grundlage ist eine andere.\n' +
|
||||||
|
` beglaubigt: ${letzter.sealRoot.slice(0, 16)}… (${letzter.sealLeafCount} Blätter)\n` +
|
||||||
|
` jetzt : ${aktuell.sealRoot.slice(0, 16)}… (${aktuell.sealLeafCount} Blätter)\n` +
|
||||||
|
'Ein erneutes Siegeln schreibt den AKTUELLEN Stand des Altbestands fest. Wurden\n' +
|
||||||
|
'vorher Einträge entfernt, sind deren Lücken danach beglaubigt und die Prüfung im\n' +
|
||||||
|
'CRM meldet wieder valid:true – dieser Wurzelwechsel ist die einzige Spur davon.\n' +
|
||||||
|
'Wenn das keine geplante Maßnahme war: Im CRM das Ereignis AUDIT_SEAL_CHANGED und\n' +
|
||||||
|
'die CRITICAL-Zeile zu /api/audit-logs/seal-backlog ansehen (wer, wann, Vorbefund).\n' +
|
||||||
|
'War es geplant, den Wechsel bestätigen und danach wieder leeren:\n' +
|
||||||
|
` NOTARY_SEAL_ACK=${aktuell.sealRoot.slice(0, 32)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Erstsiegelung: vorher nichts beglaubigt, jetzt eine Wurzel. Das ist der
|
||||||
|
// eine legitime Einrichtungsschritt – aber auch das Fenster, in dem ein
|
||||||
|
// beschnittener Altbestand einmalig festgeschrieben werden koennte. Deshalb
|
||||||
|
// ebenfalls melden, mit eigenem Text statt stillschweigend zu uebernehmen.
|
||||||
|
if (!letzter.sealRoot && aktuell.sealRoot) {
|
||||||
|
if (siegelBestaetigt(aktuell.sealRoot)) {
|
||||||
|
console.log(
|
||||||
|
`Erstsiegelung bestätigt (NOTARY_SEAL_ACK): ${aktuell.sealRoot.slice(0, 16)}…. ` +
|
||||||
|
'Die Wurzel wird beglaubigt.\n NOTARY_SEAL_ACK danach wieder leeren.',
|
||||||
|
);
|
||||||
|
} else alarm(
|
||||||
|
'Erstmals ein Bestandssiegel gesetzt – bisher war keines beglaubigt.\n' +
|
||||||
|
` Wurzel: ${aktuell.sealRoot.slice(0, 16)}… (${aktuell.sealLeafCount} Blätter)\n` +
|
||||||
|
'War das dein Einrichtungsschritt, ist alles in Ordnung: Der nächste Lauf läuft\n' +
|
||||||
|
'wieder auf 0, sobald diese Wurzel im Gegenbuch steht.\n' +
|
||||||
|
'War es das NICHT, dann hat jemand den Stand des Altbestands festgeschrieben –\n' +
|
||||||
|
'samt aller Lücken, die zu diesem Zeitpunkt bestanden.\n' +
|
||||||
|
'Zum Bestätigen setzen und danach wieder leeren:\n' +
|
||||||
|
` NOTARY_SEAL_ACK=${aktuell.sealRoot.slice(0, 32)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user