Beglaubigte Alt-Luecken: Dauer-Alarm im Gegenbuch beendet
Das Gegenbuch auf Prod meldete stuendlich exit=2, weil die CRM-Pruefung
wegen 6 struktureller Luecken valid:false lieferte (IDs 33, 44, 45, 922,
1434, 2583).
Diagnose: harmlos. Jede Luecke liegt innerhalb eines Schwungs von
Eintraegen mit identischer Sekunde, betrifft nur /login und /refresh,
kein Eintrag fehlt, tamperedEntries ist leer. Das ist die Signatur der
Race-Condition, die am 19.08. mit AuditChainLock geschlossen wurde
(R166-01). Die Zeilen datieren ihren eigenen Code: Eintrag 2583 stammt
vom 21.08., ist aber noch hashVersion=1.
Das eigentliche Problem war nicht die Luecke, sondern der Dauer-Alarm.
Diese Luecken sind nicht heilbar - die Verkettung ist gebrochen, die
Inhalte sind unversehrt. Ohne Aenderung haette das Gegenbuch fuer immer
Alarm gemeldet, und ein Signal, das immer schreit, warnt nicht mehr.
Dieselbe Klasse wie R162, R174, R179, R182, R183-02.
Loesung: Beglaubigung statt Unterdrueckung. Eine Luecke zaehlt nicht mehr
als offener Befund, wenn sie im versiegelten Bereich liegt UND im
Vorbefund des Siegel-Markers steht - also im Zustand, den der Betreiber
beim Siegeln ausdruecklich festgeschrieben hat. Der Vorbefund liegt in
changesBefore und ist ab Version 3 mitgehasht; ohne AUDIT_HMAC_KEY laesst
sich die Liste nicht nachtraeglich erweitern (gleiche Absicherung wie
beim Loeschungs-Manifest, R171-01). Beglaubigt heisst nicht verschwunden:
die Luecken bleiben in chainGaps, stehen zusaetzlich in attestedGaps und
werden im Bericht ausdruecklich benannt.
Nebenbefund derselben Klasse mitbehoben: backlogSealStatus meldete
"nicht_noetig" ("Es gibt keine unsignierten Alteintraege"), wenn
v3FromId === null - das bedeutet aber das Gegenteil, naemlich dass
ueberhaupt nichts signiert ist, etwa weil AUDIT_HMAC_KEY fehlt. Ein
vollstaendig unsigniertes Log bekam damit Entwarnung fuer genau den
Zustand mit der geringsten Beweiskraft. Die Bedingung haengt jetzt an der
Zahl der unsignierten Zeilen; R183-03 bleibt behoben (Regressionstest).
Getestet ueber HTTP gegen eine eigene Wegwerf-Datenbank, nicht am Service
vorbei: Prod-Zustand nachgebaut (10 v1-Zeilen, Bruch bei id 5) ->
valid:false; nach seal-backlog -> valid:true, attestedGaps:[5]. Drei
Gegenproben: neue Luecke nach dem Siegeln -> valid:false; gesiegelte
Altzeile veraendert -> Siegel gebrochen, nichts mehr beglaubigt;
Beglaubigungsliste im Marker gefaelscht (DB-Schreibrecht, kein
Schluessel) -> Marker ungueltig, Status entfernt, valid:false.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -149,16 +149,38 @@ export async function verifyIntegrity(req: AuthRequest, res: Response) {
|
||||
const tampered = result.tamperedEntries.length;
|
||||
const gaps = result.chainGaps.length;
|
||||
const unexplained = result.unexplainedGaps.length;
|
||||
// Beglaubigte Alt-Luecken sind kein offener Befund mehr, verschwinden aber
|
||||
// auch nicht aus dem Bericht - sie werden eigens benannt.
|
||||
const beglaubigt = result.attestedGaps.length;
|
||||
const offeneGaps = gaps - beglaubigt;
|
||||
const offeneUnexplained = result.unexplainedGaps.filter(
|
||||
(id) => !result.attestedGaps.includes(id),
|
||||
).length;
|
||||
|
||||
const luecken = gaps > 0
|
||||
? `${gaps} strukturelle Lücken` +
|
||||
(unexplained === 0
|
||||
const luecken = offeneGaps > 0
|
||||
? `${offeneGaps} strukturelle Lücke${offeneGaps === 1 ? '' : 'n'}` +
|
||||
(offeneUnexplained === 0
|
||||
? ' (alle durch protokollierte Löschungen erklärt)'
|
||||
: unexplained < gaps
|
||||
? `, davon ${unexplained} ohne dokumentierte Löschung`
|
||||
: offeneUnexplained < offeneGaps
|
||||
? `, davon ${offeneUnexplained} ohne dokumentierte Löschung`
|
||||
: ' ohne dokumentierte Löschung')
|
||||
: '';
|
||||
|
||||
// Der Satz erscheint IMMER, wenn beglaubigte Luecken existieren - auch
|
||||
// neben einem Befund. Wer den Bericht liest, soll nie den Eindruck
|
||||
// bekommen, die Kette sei lueckenlos, wenn sie es nicht ist.
|
||||
const weitere = tampered > 0 || offeneGaps > 0 ? 'weitere ' : '';
|
||||
const beglaubigtText =
|
||||
beglaubigt === 0
|
||||
? ''
|
||||
: beglaubigt === 1
|
||||
? ` Eine ${weitere}Lücke stammt aus der Zeit vor dem Bestandssiegel und ist darin als ` +
|
||||
`Vorbefund beglaubigt (ID ${result.attestedGaps[0]}); der betroffene Eintrag selbst ` +
|
||||
'ist unverändert.'
|
||||
: ` ${beglaubigt} ${weitere}Lücken stammen aus der Zeit vor dem Bestandssiegel und sind ` +
|
||||
`darin als Vorbefund beglaubigt (IDs ${result.attestedGaps.join(', ')}); ` +
|
||||
'die betroffenen Einträge selbst sind unverändert.';
|
||||
|
||||
const unverifiable = result.unverifiableEntries.length;
|
||||
const keinSchluessel = unverifiable > 0
|
||||
? ` ${unverifiable} Einträge sind HMAC-signiert und ohne konfigurierten AUDIT_HMAC_KEY nicht prüfbar.`
|
||||
@@ -207,14 +229,17 @@ export async function verifyIntegrity(req: AuthRequest, res: Response) {
|
||||
// stand nur noch im Feld `valid`. Wer die Prosa liest statt des Felds,
|
||||
// klickt genau das weg, was ihn haette warnen sollen.
|
||||
const message = tampered > 0
|
||||
? `${tampered} MANIPULIERTE Einträge gefunden` + (gaps > 0 ? ` (zusätzlich ${luecken})` : '')
|
||||
? `${tampered} MANIPULIERTE Einträge gefunden` + (offeneGaps > 0 ? ` (zusätzlich ${luecken})` : '')
|
||||
: siegelProblem
|
||||
? 'Die Kette selbst ist rechnerisch stimmig, ABER:'
|
||||
: gaps > 0
|
||||
: offeneGaps > 0
|
||||
? `Die Kette ist nicht mehr lückenlos: ${luecken}. Die verbliebenen Inhalte sind ` +
|
||||
'unverändert – aber gelöschte Einträge lassen sich naturgemäß nicht mehr prüfen. ' +
|
||||
'Dokumentierte Löschungen sind erwartbar; unerwartete gehören nachgegangen.'
|
||||
: 'Alle Einträge sind unverändert und lückenlos verkettet';
|
||||
: beglaubigt > 0
|
||||
? 'Alle Einträge sind unverändert. Seit dem Bestandssiegel ist keine neue Lücke ' +
|
||||
'entstanden.'
|
||||
: 'Alle Einträge sind unverändert und lückenlos verkettet.';
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -228,8 +253,10 @@ export async function verifyIntegrity(req: AuthRequest, res: Response) {
|
||||
chainGaps: result.chainGaps,
|
||||
// Nur Lücken ohne protokollierte Löschung sind erklärungsbedürftig.
|
||||
unexplainedGaps: result.unexplainedGaps,
|
||||
// Alt-Lücken, die das Bestandssiegel als bereits vorhanden beglaubigt.
|
||||
attestedGaps: result.attestedGaps,
|
||||
tampered: tampered > 0,
|
||||
message: message + keinSchluessel + siegel + mehrfach,
|
||||
message: message + beglaubigtText + keinSchluessel + siegel + mehrfach,
|
||||
unverifiableEntries: result.unverifiableEntries,
|
||||
// Zustand des Bestandssiegels ueber den nicht signierbaren Altbestand.
|
||||
backlogSealStatus: result.backlogSealStatus,
|
||||
|
||||
@@ -873,7 +873,11 @@ export async function sealBacklog(
|
||||
export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
|
||||
valid: boolean;
|
||||
checkedCount: number;
|
||||
/** Alle beanstandeten Zeilen (tampered + chainGaps) – Abwaertskompatibilitaet. */
|
||||
/**
|
||||
* Alle noch offenen Beanstandungen: veraenderte Zeilen plus Luecken, die
|
||||
* NICHT vom Bestandssiegel beglaubigt sind. Nur an dieser Liste haengt
|
||||
* `valid` – beglaubigte Alt-Luecken stehen in `attestedGaps`.
|
||||
*/
|
||||
invalidEntries: number[];
|
||||
/**
|
||||
* ERNST: Der Inhalt der Zeile passt nicht mehr zu ihrem Hash – jemand hat
|
||||
@@ -898,6 +902,13 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
|
||||
* eine fehlende Konfiguration ein Fehlalarm ueber das gesamte Log.
|
||||
*/
|
||||
unverifiableEntries: number[];
|
||||
/**
|
||||
* Teilmenge von `chainGaps`, die beim Versiegeln des Altbestands bereits
|
||||
* bestand und im signierten Siegel-Marker als Vorbefund festgehalten ist.
|
||||
* Diese Luecken sind BEGLAUBIGT: sie bleiben sichtbar, kippen `valid` aber
|
||||
* nicht mehr (siehe ausfuehrliche Begruendung an der Berechnung unten).
|
||||
*/
|
||||
attestedGaps: number[];
|
||||
/**
|
||||
* Zustand des Bestandssiegels ueber den nicht signierbaren Altbestand.
|
||||
* `kein_siegel` = nie erstellt. `entfernt` = Blaetter vorhanden, aber kein
|
||||
@@ -1048,6 +1059,8 @@ 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[] = [];
|
||||
// Luecken, die das Siegel als bereits vorhanden beglaubigt (siehe unten).
|
||||
const beglaubigteLuecken = new Set<number>();
|
||||
let backlogSealStatus: 'kein_siegel' | 'intakt' | 'gebrochen' | 'entfernt' | 'nicht_noetig' = 'kein_siegel';
|
||||
let backlogSealRoot: string | null = null;
|
||||
|
||||
@@ -1092,14 +1105,24 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
|
||||
// Deshalb gilt jetzt: Blaetter vorhanden, aber kein gueltiger Marker =
|
||||
// Siegel ENTFERNT und damit ein Befund – nicht „nie versiegelt“.
|
||||
const blattAnzahl = await prisma.auditBacklogSeal.count();
|
||||
const unsignierteAnzahl = await prisma.auditLog.count({ where: { hashVersion: { lt: 3 } } });
|
||||
if (!siegel && blattAnzahl > 0 && siegelSchluessel.length > 0) {
|
||||
backlogSealStatus = 'entfernt';
|
||||
} else if (!siegel && blattAnzahl === 0 && (v3FromId === null || v3FromId <= 1)) {
|
||||
} else if (!siegel && blattAnzahl === 0 && unsignierteAnzahl === 0) {
|
||||
// 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.
|
||||
//
|
||||
// Die Bedingung haengt bewusst an der Zahl der UNSIGNIERTEN Zeilen und
|
||||
// nicht mehr an `v3FromId === null`. Letzteres bedeutet naemlich das
|
||||
// genaue Gegenteil: dass ueberhaupt nichts signiert ist – etwa weil
|
||||
// AUDIT_HMAC_KEY fehlt. Ein vollstaendig unsigniertes Log meldete damit
|
||||
// „Bestandssiegel nicht noetig“, also Entwarnung fuer genau den Zustand
|
||||
// mit der geringsten Beweiskraft. Jetzt bleibt es bei „kein_siegel“; die
|
||||
// Fehlermeldung von seal-backlog nennt den fehlenden Schluessel als
|
||||
// naechsten Schritt, die Warnung ist also aufloesbar.
|
||||
backlogSealStatus = 'nicht_noetig';
|
||||
}
|
||||
|
||||
@@ -1140,6 +1163,43 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
|
||||
backlogTampered.length === 0 && backlogMissing.length === 0 && wurzelJetzt === meta.root
|
||||
? 'intakt'
|
||||
: 'gebrochen';
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Beglaubigte Alt-Luecken
|
||||
//
|
||||
// Die Race-Luecken aus der Zeit vor dem Sperr-Fix lassen sich nicht
|
||||
// mehr heilen: die Verkettung ist gebrochen, die Inhalte sind aber
|
||||
// unversehrt. Wuerden sie `valid` dauerhaft auf false halten, meldete
|
||||
// das Gegenbuch stuendlich Alarm, ohne dass es je etwas zu tun gaebe -
|
||||
// und genau daran stirbt jede Warnung. Ein Signal, das immer schreit,
|
||||
// warnt nicht mehr.
|
||||
//
|
||||
// Deshalb gilt eine Luecke als BEGLAUBIGT, wenn beides zutrifft:
|
||||
// 1. sie liegt im versiegelten Bereich (id <= toId), und
|
||||
// 2. sie steht im Vorbefund des Siegel-Markers, also im Zustand, den
|
||||
// der Betreiber beim Siegeln ausdruecklich festgeschrieben hat.
|
||||
// Der Vorbefund liegt in `changesBefore` und ist ab Version 3 mit-
|
||||
// gehasht - die Liste laesst sich also ohne Schluessel nicht nachtraeg-
|
||||
// lich erweitern (gleiche Absicherung wie beim Manifest, R171-01).
|
||||
//
|
||||
// Beglaubigt heisst NICHT verschwunden: die Luecken bleiben in
|
||||
// `chainGaps` und werden weiter berichtet. Sie zaehlen nur nicht mehr
|
||||
// als offener Befund. Alles andere schlaegt unveraendert an - eine
|
||||
// NEUE Luecke, eine veraenderte oder entfernte Altzeile, ein gebroche-
|
||||
// nes Siegel. Bei nicht intaktem Siegel wird gar nichts beglaubigt.
|
||||
if (backlogSealStatus === 'intakt') {
|
||||
try {
|
||||
const roh = siegel.changesEncrypted
|
||||
? decrypt(siegel.changesBefore || '')
|
||||
: siegel.changesBefore || '{}';
|
||||
const vorbefund = JSON.parse(roh);
|
||||
for (const id of vorbefund?.befund?.ketten_luecken || []) {
|
||||
if (typeof id === 'number' && id <= bis) beglaubigteLuecken.add(id);
|
||||
}
|
||||
} catch {
|
||||
// Unlesbarer Vorbefund beglaubigt nichts - die sichere Richtung.
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
backlogSealStatus = 'gebrochen';
|
||||
}
|
||||
@@ -1252,7 +1312,13 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
|
||||
|
||||
// Eskalierte Luecken stehen sowohl in tamperedEntries als auch in chainGaps;
|
||||
// ohne Entdopplung zaehlte dieselbe Zeile zweimal (Pentest R171-03).
|
||||
const invalidEntries = [...new Set([...tamperedEntries, ...chainGaps])].sort((a, b) => a - b);
|
||||
//
|
||||
// Beglaubigte Alt-Luecken zaehlen NICHT als offener Befund (siehe die
|
||||
// Begruendung an `beglaubigteLuecken`). Sie bleiben in `chainGaps`
|
||||
// sichtbar und stehen zusaetzlich in `attestedGaps`.
|
||||
const attestedGaps = chainGaps.filter((id) => beglaubigteLuecken.has(id));
|
||||
const offeneLuecken = chainGaps.filter((id) => !beglaubigteLuecken.has(id));
|
||||
const invalidEntries = [...new Set([...tamperedEntries, ...offeneLuecken])].sort((a, b) => a - b);
|
||||
|
||||
// Ein gebrochenes oder entferntes Siegel muss `valid` kippen, auch wenn keine
|
||||
// einzelne Zeile beanstandet ist – sonst bliebe der stille Anker-Verlust
|
||||
@@ -1270,6 +1336,7 @@ export async function verifyIntegrity(fromId?: number, toId?: number): Promise<{
|
||||
chainGaps,
|
||||
unexplainedGaps,
|
||||
unverifiableEntries,
|
||||
attestedGaps,
|
||||
backlogSealStatus,
|
||||
backlogTampered,
|
||||
backlogMissing,
|
||||
|
||||
@@ -97,6 +97,54 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
||||
|
||||
## ✅ Erledigt
|
||||
|
||||
- [x] **🧾 Beglaubigte Alt-Lücken: Dauer-Alarm im Gegenbuch beendet** (2026-08-26)
|
||||
- **Ausgangslage.** Das Gegenbuch auf Prod meldete stündlich `exit=2`. Die
|
||||
CRM-Prüfung lieferte `valid: false` wegen **6 struktureller Lücken**
|
||||
(IDs 33, 44, 45, 922, 1434, 2583).
|
||||
- **Diagnose: harmlos.** Jede Lücke liegt innerhalb eines Schwungs von
|
||||
Einträgen mit **identischer Sekunde**, betrifft nur `/login` und
|
||||
`/refresh`, kein Eintrag fehlt (kein 404), `tamperedEntries` leer. Das ist
|
||||
die Signatur der Race-Condition, die am 19.08. mit `AuditChainLock`
|
||||
geschlossen wurde (R166-01). Bestätigt durch die Zeilen selbst: Eintrag
|
||||
2583 stammt vom 21.08., ist aber noch `hashVersion=1` – auf Prod lief zu
|
||||
dem Zeitpunkt also der alte Stand.
|
||||
- **Das eigentliche Problem war nicht die Lücke, sondern der Dauer-Alarm.**
|
||||
Diese Lücken sind nicht heilbar: die Verkettung ist gebrochen, die Inhalte
|
||||
sind unversehrt. Ohne Änderung hätte das Gegenbuch für immer Alarm gemeldet
|
||||
– und ein Signal, das immer schreit, warnt nicht mehr. Dieselbe Klasse wie
|
||||
R162, R174, R179, R182, R183-02.
|
||||
- **Lösung: Beglaubigung statt Unterdrückung.** Eine Lücke zählt nicht mehr
|
||||
als offener Befund, wenn beides gilt: sie liegt im **versiegelten Bereich**
|
||||
UND steht im **Vorbefund des Siegel-Markers**, also im Zustand, den der
|
||||
Betreiber beim Siegeln ausdrücklich festgeschrieben hat. Der Vorbefund
|
||||
liegt in `changesBefore` und ist ab Version 3 mitgehasht – die Liste lässt
|
||||
sich ohne `AUDIT_HMAC_KEY` nicht nachträglich erweitern (gleiche
|
||||
Absicherung wie beim Löschungs-Manifest, R171-01).
|
||||
- Beglaubigt heißt **nicht verschwunden**: die Lücken bleiben in `chainGaps`,
|
||||
stehen zusätzlich in neuem Feld `attestedGaps` und werden im Bericht
|
||||
ausdrücklich benannt.
|
||||
- **Nebenbefund derselben Klasse mitbehoben:** `backlogSealStatus` meldete
|
||||
`nicht_noetig` ("Es gibt keine unsignierten Alteinträge"), wenn
|
||||
`v3FromId === null` – das bedeutet aber das **Gegenteil**: dass überhaupt
|
||||
nichts signiert ist, etwa weil `AUDIT_HMAC_KEY` fehlt. Ein vollständig
|
||||
unsigniertes Log bekam damit Entwarnung für genau den Zustand mit der
|
||||
geringsten Beweiskraft. Die Bedingung hängt jetzt an der Zahl der
|
||||
unsignierten Zeilen. R183-03 (unauflösbare Warnung bei frisch signiertem
|
||||
Log) bleibt behoben – per Regressionstest geprüft.
|
||||
- **Getestet über HTTP gegen eine eigene Wegwerf-Datenbank**, nicht am
|
||||
Service vorbei: Prod-Zustand nachgebaut (10 v1-Zeilen, Bruch bei id 5) →
|
||||
`valid:false`; nach `seal-backlog` → `valid:true`, `attestedGaps:[5]`.
|
||||
Drei Gegenproben: **neue Lücke** nach dem Siegeln → `valid:false`;
|
||||
**gesiegelte Altzeile verändert** → Siegel `gebrochen`, nichts mehr
|
||||
beglaubigt; **Beglaubigungsliste im Marker gefälscht** (DB-Schreibrecht,
|
||||
kein Schlüssel) → Marker ungültig, Status `entfernt`, `valid:false`.
|
||||
- Doku: Abschnitt „Wenn der erste Lauf `exit=2` meldet" in
|
||||
`tools/audit-notary/README.md` – inklusive der Warnung, **vor** dem
|
||||
Siegeln zu prüfen, was man da festschreibt.
|
||||
- Dateien: `backend/src/services/audit.service.ts`,
|
||||
`backend/src/controllers/auditLog.controller.ts`,
|
||||
`frontend/src/services/api.ts`, `tools/audit-notary/README.md`
|
||||
|
||||
- [x] **🔓 Dienstkonto-Flag: Gate in beide Richtungen, richtige Rechte-Domaene (Pentest R184-01/-02)** (2026-08-24)
|
||||
- **R184-01** – Setzen war gegatet, **Entfernen nicht**. Und das Entfernen ist
|
||||
der gefaehrlichere Weg: Der Heartbeat-Wachhund fragt `isServiceAccount:
|
||||
|
||||
@@ -1752,7 +1752,7 @@ export const auditLogApi = {
|
||||
return res.data;
|
||||
},
|
||||
verifyIntegrity: async () => {
|
||||
const res = await api.post<ApiResponse<{ valid: boolean; checkedCount: number; invalidEntries: number[]; tamperedEntries: number[]; chainGaps: number[]; unexplainedGaps: number[]; unverifiableEntries: number[]; tampered: boolean; message: string }>>('/audit-logs/verify');
|
||||
const res = await api.post<ApiResponse<{ valid: boolean; checkedCount: number; invalidEntries: number[]; tamperedEntries: number[]; chainGaps: number[]; unexplainedGaps: number[]; attestedGaps: number[]; unverifiableEntries: number[]; tampered: boolean; message: string }>>('/audit-logs/verify');
|
||||
return res.data;
|
||||
},
|
||||
rehash: async () => {
|
||||
|
||||
@@ -86,6 +86,58 @@ soll nicht versehentlich passieren.
|
||||
getrennte Dienste mit getrennten Verzeichnissen und getrennten Schlüsseln.
|
||||
Welche laufen, steuert `COMPOSE_PROFILES` in der `.env`.
|
||||
|
||||
### Wenn der erste Lauf `exit=2` meldet: den Altbestand versiegeln
|
||||
|
||||
Ein CRM, das schon länger läuft, hat fast immer einen **Altbestand** – Einträge
|
||||
aus der Zeit, bevor das Protokoll signiert wurde. Solange der nicht versiegelt
|
||||
ist, meldet die Prüfung `valid: false`, und das Gegenbuch schlägt zu Recht
|
||||
Alarm. Typischerweise steht dann im Log:
|
||||
|
||||
Hinweis: Der Altbestand ist nicht versiegelt – Änderungen daran wären
|
||||
nicht erkennbar.
|
||||
|
||||
Das ist **einmalig** zu erledigen, im CRM, nicht hier:
|
||||
|
||||
```bash
|
||||
TOKEN=$(curl -s -X POST https://<crm>/api/auth/login \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"email":"…","password":"…"}' | jq -r '.data.token')
|
||||
|
||||
curl -s -X POST https://<crm>/api/audit-logs/seal-backlog \
|
||||
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
|
||||
-d '{"confirm":"SEAL"}' | jq
|
||||
```
|
||||
|
||||
Dafür braucht es das Recht `audit:admin` – das Gegenbuch-Dienstkonto hat es
|
||||
absichtlich **nicht**. Nimm dein Administratorkonto.
|
||||
|
||||
**Zuerst nachsehen, was versiegelt wird.** Das Siegel schreibt den aktuellen
|
||||
Zustand fest, samt aller vorhandenen Lücken. Wer blind siegelt, beglaubigt
|
||||
gegebenenfalls auch eine Lücke, die von einer Löschung stammt. Deshalb vorher:
|
||||
|
||||
```bash
|
||||
curl -s -X POST https://<crm>/api/audit-logs/verify \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
| jq '.data | {valid, chainGaps, unexplainedGaps, tamperedEntries}'
|
||||
```
|
||||
|
||||
Stehen dort Lücken, sieh dir die betroffenen IDs und ihre Nachbarn an
|
||||
(`GET /api/audit-logs/<id>`). Liegen sie jeweils **innerhalb einer Sekunde**
|
||||
zusammen mit ihren Nachbarn und fehlt kein Eintrag (kein `404`), sind es
|
||||
Schreibkollisionen aus paralleln Anfragen – harmlos. Fehlt dagegen ein Eintrag
|
||||
oder steht etwas in `tamperedEntries`, **erst klären, dann siegeln**.
|
||||
|
||||
Nach dem Siegeln meldet die Prüfung wieder `valid: true`, und die bekannten
|
||||
Alt-Lücken erscheinen als **beglaubigt**:
|
||||
|
||||
Alle Einträge sind unverändert. Seit dem Bestandssiegel ist keine neue
|
||||
Lücke entstanden. 6 Lücken stammen aus der Zeit vor dem Bestandssiegel
|
||||
und sind darin als Vorbefund beglaubigt (IDs …).
|
||||
|
||||
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
|
||||
und jedes gebrochene Siegel lösen weiterhin sofort Alarm aus.
|
||||
|
||||
### Zugang einrichten (das brauchst du vorher)
|
||||
|
||||
Das Gegenbuch braucht ein **eigenes Benutzerkonto** im CRM – kein Token. Der
|
||||
|
||||
Reference in New Issue
Block a user