Gegenbuch: externe Notarisierung der Audit-Kette
Abschluss der Anker-Kette. Alle bisherigen Schutzebenen liegen in derselben Datenbank, die sie absichern sollen - der Pentest hat das ueber mehrere Runden Schicht fuer Schicht gezeigt, zuletzt in R174-01 am Siegel-Marker selbst. Aufteilung nach der Analyse des Pentesters (der Schutz kommt vom Ort, nicht von der Signatur): Das CRM liefert nur einen lesbaren Kontrollwert ohne Geheimnisse (GET /api/audit-logs/checkpoint, audit:read). Signiert, zeitgestempelt und angehaengt wird auf einem anderen Rechner - Schluessel und Push-Recht liegen nicht in den Deploy-Secrets des CRM. Ohne diese Trennung waere es D1 nochmal, nur schlimmer: sieht nach doppeltem Boden aus, tut still nichts. Der Kontrollwert enthaelt bewusst maxId. Ein blosser Kopf-Hash erkennt Umschreiben, aber kein Abschneiden am Ende - genau die R174-01-Klasse, eine Ebene hoeher. atId erlaubt der Gegenstelle, einen frueher beglaubigten Kopf erneut abzufragen und nachzurechnen. Gegenstelle: tools/audit-notary/notary.mjs (Cron auf zweitem Rechner, privates Git-Repo als Append-only-Ablage, signierte Commits). Prueft vor dem Anhaengen und bricht bei Widerspruch mit Exit-Code 2 ab, ohne zu schreiben. Verifiziert gegen eine CRM-Attrappe mit echter DB: beglaubigte Zeile veraendert -> Alarm; am Ende abgeschnitten (maxId 5->4) -> Alarm; Gegenbuch selbst gekuerzt (seq-Luecke) -> Alarm; in allen Faellen nichts angehaengt. Ehrlich dokumentiert: Restfenster zwischen zwei Laeufen bleibt und ist inhaerent; ein stiller Cron-Ausfall erzeugt im CRM keine Warnung und muss auf dem Gegenbuch-Rechner ueberwacht werden; Force-Push muss serverseitig gesperrt sein, sonst ist Append-only nur geliehen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -269,6 +269,29 @@ export async function rehashAll(req: AuthRequest, res: Response) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kontrollwert fuer das externe Gegenbuch (Notarisierung).
|
||||||
|
*
|
||||||
|
* Bewusst nur lesend und ohne Geheimnisse - die Gegenstelle braucht keinerlei
|
||||||
|
* Schreibrecht auf dieses System, und dieses System kennt den Schluessel der
|
||||||
|
* Gegenstelle nicht. Genau diese Trennung ist der Schutz.
|
||||||
|
*/
|
||||||
|
export async function getCheckpoint(req: AuthRequest, res: Response) {
|
||||||
|
try {
|
||||||
|
const roh = req.query.atId as string | undefined;
|
||||||
|
const atId = roh !== undefined ? parseInt(roh, 10) : undefined;
|
||||||
|
if (roh !== undefined && Number.isNaN(atId as number)) {
|
||||||
|
res.status(400).json({ success: false, error: 'atId muss eine Zahl sein' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const data = await auditService.getCheckpoint(atId);
|
||||||
|
res.json({ success: true, data });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fehler beim Erstellen des Kontrollwerts:', error);
|
||||||
|
res.status(500).json({ success: false, error: 'Fehler beim Erstellen des Kontrollwerts' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Altbestand versiegeln (Pentest R171-02).
|
* Altbestand versiegeln (Pentest R171-02).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ router.get('/export', requirePermission('audit:read'), auditLogController.export
|
|||||||
router.get('/retention-policies', requirePermission('audit:admin'), auditLogController.getRetentionPolicies);
|
router.get('/retention-policies', requirePermission('audit:admin'), auditLogController.getRetentionPolicies);
|
||||||
router.put('/retention-policies/:id', requirePermission('audit:admin'), auditLogController.updateRetentionPolicy);
|
router.put('/retention-policies/:id', requirePermission('audit:admin'), auditLogController.updateRetentionPolicy);
|
||||||
|
|
||||||
|
// Kontrollwert für das externe Gegenbuch (nur lesend, ohne Geheimnisse)
|
||||||
|
router.get('/checkpoint', requirePermission('audit:read'), auditLogController.getCheckpoint);
|
||||||
|
|
||||||
// Audit-Logs für einen Kunden (DSGVO)
|
// Audit-Logs für einen Kunden (DSGVO)
|
||||||
router.get('/customer/:customerId', requirePermission('audit:read'), auditLogController.getAuditLogsByCustomer);
|
router.get('/customer/:customerId', requirePermission('audit:read'), auditLogController.getAuditLogsByCustomer);
|
||||||
|
|
||||||
|
|||||||
@@ -676,6 +676,69 @@ const BACKLOG_SEAL_RESOURCE = 'AuditBacklogSeal';
|
|||||||
* dann nur als harmlose Luecke. Zeilen ab `v3FromId` deckt die Versionsgrenze
|
* dann nur als harmlose Luecke. Zeilen ab `v3FromId` deckt die Versionsgrenze
|
||||||
* ab (sie MUESSEN V3 sein), es bleibt also keine Naht dazwischen.
|
* ab (sie MUESSEN V3 sein), es bleibt also keine Naht dazwischen.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Kontrollwert fuer die externe Notarisierung (Gegenbuch).
|
||||||
|
*
|
||||||
|
* Alle bisherigen Anker leben IM System: Hash, Versionsgrenze, Manifest,
|
||||||
|
* Bestandssiegel, Siegel-Marker. Wer Schreibzugriff auf die Datenbank hat,
|
||||||
|
* sitzt am Ende immer schon auf der Ebene, die beweisen soll – zuletzt gezeigt
|
||||||
|
* in R174-01. Ein Gegenbuch an einem fremden Ort durchbricht das.
|
||||||
|
*
|
||||||
|
* Der Wert enthaelt bewusst `maxId`: Ein blosser Kopf-Hash erkennt zwar
|
||||||
|
* Umschreiben, aber kein ABSCHNEIDEN am Ende. Mit ankertem `maxId` ist
|
||||||
|
* „juengster beglaubigter maxId > aktueller maxId“ der Beweis, dass Zeilen
|
||||||
|
* verschwunden sind.
|
||||||
|
*
|
||||||
|
* `atId` erlaubt der Gegenstelle, einen FRUEHER beglaubigten Kopf erneut
|
||||||
|
* abzufragen und zu reproduzieren – ohne das koennte sie nur den aktuellen
|
||||||
|
* Stand sehen und nichts nachrechnen.
|
||||||
|
*
|
||||||
|
* Enthaelt keine Geheimnisse: nur Hashes und Zaehlstaende.
|
||||||
|
*/
|
||||||
|
export async function getCheckpoint(atId?: number): Promise<{
|
||||||
|
ts: string;
|
||||||
|
maxId: number | null;
|
||||||
|
chainHead: string | null;
|
||||||
|
sealRoot: string | null;
|
||||||
|
sealStatus: string;
|
||||||
|
atId?: number;
|
||||||
|
atHash?: string | null;
|
||||||
|
}> {
|
||||||
|
const kopf = await prisma.auditLog.findFirst({
|
||||||
|
orderBy: { id: 'desc' },
|
||||||
|
select: { id: true, hash: true },
|
||||||
|
});
|
||||||
|
const pruefung = await verifyIntegrity();
|
||||||
|
|
||||||
|
const ergebnis: {
|
||||||
|
ts: string;
|
||||||
|
maxId: number | null;
|
||||||
|
chainHead: string | null;
|
||||||
|
sealRoot: string | null;
|
||||||
|
sealStatus: string;
|
||||||
|
atId?: number;
|
||||||
|
atHash?: string | null;
|
||||||
|
} = {
|
||||||
|
ts: new Date().toISOString(),
|
||||||
|
maxId: kopf?.id ?? null,
|
||||||
|
chainHead: kopf?.hash ?? null,
|
||||||
|
sealRoot: pruefung.backlogSealRoot,
|
||||||
|
sealStatus: pruefung.backlogSealStatus,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (atId !== undefined && Number.isFinite(atId)) {
|
||||||
|
const zeile = await prisma.auditLog.findUnique({
|
||||||
|
where: { id: atId },
|
||||||
|
select: { hash: true },
|
||||||
|
});
|
||||||
|
ergebnis.atId = atId;
|
||||||
|
// null bedeutet: die beglaubigte Zeile existiert nicht mehr.
|
||||||
|
ergebnis.atHash = zeile?.hash ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ergebnis;
|
||||||
|
}
|
||||||
|
|
||||||
export async function sealBacklog(
|
export async function sealBacklog(
|
||||||
ausgeloestVon?: { userEmail?: string; ipAddress?: string },
|
ausgeloestVon?: { userEmail?: string; ipAddress?: string },
|
||||||
): Promise<{ sealedCount: number; fromId: number; toId: number; root: string }> {
|
): Promise<{ sealedCount: number; fromId: number; toId: number; root: string }> {
|
||||||
|
|||||||
@@ -97,6 +97,38 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ Erledigt
|
||||||
|
|
||||||
|
- [x] **⚖️ Gegenbuch: externe Notarisierung der Audit-Kette** (2026-08-18)
|
||||||
|
- Abschluss der Anker-Kette. Alle bisherigen Schutzebenen liegen in
|
||||||
|
derselben Datenbank, die sie absichern sollen – der Pentest hat das ueber
|
||||||
|
mehrere Runden Schicht fuer Schicht gezeigt, zuletzt in R174-01 am Siegel-
|
||||||
|
Marker selbst. Ein Gegenbuch an einem fremden Ort durchbricht das.
|
||||||
|
- Aufteilung nach der Analyse des Pentesters („der Schutz kommt vom ORT,
|
||||||
|
nicht von der Signatur“): Das CRM liefert nur einen **lesbaren
|
||||||
|
Kontrollwert ohne Geheimnisse** (`GET /api/audit-logs/checkpoint`,
|
||||||
|
`audit:read`). Signiert, zeitstempelt und angehaengt wird auf einem
|
||||||
|
**anderen Rechner** – Schluessel und Push-Recht liegen NICHT in den
|
||||||
|
Deploy-Secrets des CRM. Ohne diese Trennung waere es „D1 nochmal, nur
|
||||||
|
schlimmer“: sieht nach doppeltem Boden aus, tut still nichts.
|
||||||
|
- Der Kontrollwert enthaelt bewusst `maxId`. Ein blosser Kopf-Hash erkennt
|
||||||
|
Umschreiben, aber kein **Abschneiden am Ende** – genau die R174-01-Klasse,
|
||||||
|
eine Ebene hoeher. `atId` erlaubt der Gegenstelle, einen frueher
|
||||||
|
beglaubigten Kopf erneut abzufragen und nachzurechnen.
|
||||||
|
- Gegenstelle: `tools/audit-notary/notary.mjs` (Cron auf zweitem Rechner,
|
||||||
|
privates Git-Repo als Append-only-Ablage, signierte Commits). Prueft VOR
|
||||||
|
dem Anhaengen und bricht bei Widerspruch mit Exit-Code 2 ab, **ohne zu
|
||||||
|
schreiben** – der manipulierte Zustand wird nicht als neue Wahrheit
|
||||||
|
festgeschrieben.
|
||||||
|
- Verifiziert gegen eine CRM-Attrappe mit echter DB: beglaubigte Zeile
|
||||||
|
veraendert → Alarm; am Ende abgeschnitten (maxId 5→4) → Alarm; Gegenbuch
|
||||||
|
selbst gekuerzt (seq-Luecke) → Alarm; in allen Faellen nichts angehaengt.
|
||||||
|
Wegwerf-DB und Test-Gegenbuch danach geloescht.
|
||||||
|
- **Ehrlich dokumentiert** (README): Restfenster zwischen zwei Laeufen bleibt
|
||||||
|
und ist inhaerent; ein stiller Cron-Ausfall erzeugt im CRM KEINE Warnung
|
||||||
|
und muss auf dem Gegenbuch-Rechner ueberwacht werden; Force-Push muss
|
||||||
|
serverseitig gesperrt sein, sonst ist Append-only nur geliehen.
|
||||||
|
- **Betrieb:** Einrichtung liegt beim Betreiber (privates Repo, Cron,
|
||||||
|
Signaturschluessel auf dem zweiten Rechner).
|
||||||
|
|
||||||
- [x] **🚨 Siegel-Entfernung wird erkannt (Pentest R174-01, HIGH)** (2026-08-18)
|
- [x] **🚨 Siegel-Entfernung wird erkannt (Pentest R174-01, HIGH)** (2026-08-18)
|
||||||
- Fund: Der Siegelzustand hing **ausschliesslich** am Marker im Audit-Log –
|
- Fund: Der Siegelzustand hing **ausschliesslich** am Marker im Audit-Log –
|
||||||
und den kann ein DB-Schreiber **ohne Schluessel** loeschen. Danach meldete
|
und den kann ein DB-Schreiber **ohne Schluessel** loeschen. Danach meldete
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
# Gegenbuch (externe Notarisierung)
|
||||||
|
|
||||||
|
Alle Schutzmechanismen im CRM selbst – Hash-Kette, Versionsgrenze,
|
||||||
|
Löschungs-Manifest, Bestandssiegel – liegen in **derselben Datenbank**, die sie
|
||||||
|
absichern sollen. Wer dort schreiben kann, sitzt am Ende immer schon auf der
|
||||||
|
Ebene, die den Beweis führt. Genau das hat der Pentest über mehrere Runden
|
||||||
|
Schicht für Schicht gezeigt.
|
||||||
|
|
||||||
|
Das Gegenbuch durchbricht das: Ein zweiter Rechner holt regelmäßig einen kurzen
|
||||||
|
Kontrollwert vom CRM, prüft ihn gegen seine eigene Historie und hängt ihn
|
||||||
|
signiert an ein privates Repository an.
|
||||||
|
|
||||||
|
## Die eine nicht verhandelbare Bedingung
|
||||||
|
|
||||||
|
**Der Signaturschlüssel und das Push-Recht dürfen nicht in den Deploy-Secrets
|
||||||
|
des CRM liegen.** Sonst ist der zweite Schlüssel im selben Zugriffsbereich wie
|
||||||
|
der erste – es sieht nach doppeltem Boden aus und ist keiner. Das CRM braucht
|
||||||
|
für dieses Verfahren **gar nichts** zu wissen: Es liefert nur einen lesbaren
|
||||||
|
Kontrollwert, der keine Geheimnisse enthält.
|
||||||
|
|
||||||
|
## Einrichten
|
||||||
|
|
||||||
|
Auf einem **anderen** Rechner als dem CRM-Server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone git@host:dein/audit-gegenbuch.git
|
||||||
|
cd audit-gegenbuch
|
||||||
|
|
||||||
|
export CRM_URL=https://crm.example.de
|
||||||
|
export CRM_TOKEN=<Access-Token eines Benutzers mit audit:read>
|
||||||
|
|
||||||
|
node /pfad/zu/notary.mjs
|
||||||
|
```
|
||||||
|
|
||||||
|
Als Cronjob, stündlich:
|
||||||
|
|
||||||
|
```cron
|
||||||
|
0 * * * * cd /pfad/zum/gegenbuch && CRM_URL=https://crm.example.de \
|
||||||
|
CRM_TOKEN=... node /pfad/notary.mjs >> notary.log 2>&1
|
||||||
|
```
|
||||||
|
|
||||||
|
**Force-Push serverseitig sperren.** Ohne das ist die Append-only-Eigenschaft
|
||||||
|
nur geliehen – wer Schreibrecht auf das Repository erlangt, schreibt die
|
||||||
|
Historie sonst einfach um. Bei GitHub/GitLab: Branch-Protection auf `main`.
|
||||||
|
|
||||||
|
## Was das Skript erkennt
|
||||||
|
|
||||||
|
| Angriff | Erkennung |
|
||||||
|
|---|---|
|
||||||
|
| Beglaubigter Eintrag nachträglich verändert | Hash an der beglaubigten Position stimmt nicht mehr |
|
||||||
|
| Einträge am Ende abgeschnitten | aktuelle höchste ID kleiner als die beglaubigte |
|
||||||
|
| Bestandssiegel verschwunden | vorher beglaubigt, jetzt nicht mehr vorhanden |
|
||||||
|
| Gegenbuch selbst gekürzt | Lücke in der fortlaufenden Nummer |
|
||||||
|
|
||||||
|
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
|
||||||
|
festgeschrieben. Für Cron heißt das: Exit-Code überwachen, sonst geht der Alarm
|
||||||
|
im Log unter.
|
||||||
|
|
||||||
|
## Was es nicht leistet – ehrlich
|
||||||
|
|
||||||
|
- **Restfenster:** Einträge zwischen dem letzten Kontrollwert und jetzt sind
|
||||||
|
noch nicht beglaubigt und am Ende fälsch- oder kürzbar. Häufigerer Lauf
|
||||||
|
verkleinert das Fenster, beseitigt es aber nicht. Das gilt für jedes
|
||||||
|
periodische Verfahren dieser Art.
|
||||||
|
- **Stiller Ausfall:** Läuft der Cronjob nicht mehr, entsteht keine Warnung im
|
||||||
|
CRM – dort ist ja nichts konfiguriert. Der Ausfall muss auf dem
|
||||||
|
Gegenbuch-Rechner überwacht werden (z. B. Alarm, wenn der jüngste Commit
|
||||||
|
älter als zwei Intervalle ist).
|
||||||
|
- **`NOTARY_SIGN=false`** existiert nur für Tests ohne hinterlegten Schlüssel.
|
||||||
|
Im Betrieb niemals setzen.
|
||||||
Executable
+137
@@ -0,0 +1,137 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* OpenCRM – Gegenbuch (externe Notarisierung)
|
||||||
|
* ===========================================
|
||||||
|
*
|
||||||
|
* WICHTIG: Dieses Skript laeuft NICHT auf dem CRM-Server. Es gehoert auf einen
|
||||||
|
* zweiten Rechner. Der Signaturschluessel und das Push-Recht auf das private
|
||||||
|
* Repository leben dort – nicht in den Deploy-Secrets des CRM.
|
||||||
|
*
|
||||||
|
* Der Grund: Alle Anker im CRM selbst (Hash-Kette, Versionsgrenze, Manifest,
|
||||||
|
* Bestandssiegel) sitzen in derselben Datenbank, die sie schuetzen sollen. Wer
|
||||||
|
* dort schreiben kann, kommt am Ende an jeden davon heran. Ein Gegenbuch an
|
||||||
|
* einem fremden Ort durchbricht das – aber NUR, solange der Schluessel dieses
|
||||||
|
* Ortes nicht im selben Zugriffsbereich liegt. Liegt er es doch, sieht es nach
|
||||||
|
* doppeltem Boden aus und ist keiner.
|
||||||
|
*
|
||||||
|
* Ablauf je Lauf:
|
||||||
|
* 1. Kontrollwert beim CRM abholen (nur lesend)
|
||||||
|
* 2. GEGEN die eigene Historie pruefen, BEVOR etwas angehaengt wird
|
||||||
|
* 3. Nur bei Unauffaelligkeit anhaengen, signiert committen, pushen
|
||||||
|
* 4. Bei Widerspruch: Exit-Code 2 und nichts schreiben
|
||||||
|
*
|
||||||
|
* Aufruf (Beispiel-Cron, stuendlich):
|
||||||
|
* 0 * * * * cd /pfad/zum/gegenbuch && CRM_URL=https://crm.example.de \
|
||||||
|
* CRM_TOKEN=... node /pfad/notary.mjs >> notary.log 2>&1
|
||||||
|
*/
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
|
const CRM_URL = process.env.CRM_URL;
|
||||||
|
const CRM_TOKEN = process.env.CRM_TOKEN;
|
||||||
|
const LEDGER = resolve(process.env.LEDGER_FILE || 'checkpoints.jsonl');
|
||||||
|
const PUSH = process.env.NOTARY_PUSH !== 'false';
|
||||||
|
// NUR fuer Tests ohne hinterlegten Schluessel. Im Betrieb NIEMALS setzen: ohne
|
||||||
|
// Signatur kann jeder, der Schreibrecht auf das Repository erlangt, die
|
||||||
|
// Checkpoint-Reihe faelschen - dann ist das Gegenbuch wertlos.
|
||||||
|
const SIGN = process.env.NOTARY_SIGN !== 'false';
|
||||||
|
|
||||||
|
if (!CRM_URL || !CRM_TOKEN) {
|
||||||
|
console.error('CRM_URL und CRM_TOKEN müssen gesetzt sein.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const git = (...args) => execFileSync('git', args, { encoding: 'utf8' }).trim();
|
||||||
|
|
||||||
|
async function hole(pfad) {
|
||||||
|
const r = await fetch(`${CRM_URL}${pfad}`, {
|
||||||
|
headers: { Authorization: `Bearer ${CRM_TOKEN}` },
|
||||||
|
});
|
||||||
|
if (!r.ok) throw new Error(`${pfad} -> HTTP ${r.status}`);
|
||||||
|
const j = await r.json();
|
||||||
|
if (!j.success) throw new Error(`${pfad} -> ${j.error}`);
|
||||||
|
return j.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ledgerLesen() {
|
||||||
|
if (!existsSync(LEDGER)) return [];
|
||||||
|
return readFileSync(LEDGER, 'utf8')
|
||||||
|
.split('\n')
|
||||||
|
.filter((z) => z.trim())
|
||||||
|
.map((z) => JSON.parse(z));
|
||||||
|
}
|
||||||
|
|
||||||
|
const bisher = ledgerLesen();
|
||||||
|
|
||||||
|
// --- seq lueckenlos und monoton pruefen -------------------------------------
|
||||||
|
// Ohne diese Pruefung koennte jemand die Checkpoint-Reihe selbst hinten
|
||||||
|
// kuerzen; die Luecke faellt dann nicht auf.
|
||||||
|
for (let i = 0; i < bisher.length; i++) {
|
||||||
|
if (bisher[i].seq !== i + 1) {
|
||||||
|
console.error(`ALARM: Lücke in der Checkpoint-Reihe an Position ${i + 1} (seq=${bisher[i].seq}).`);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const letzter = bisher[bisher.length - 1];
|
||||||
|
const aktuell = await hole('/api/audit-logs/checkpoint');
|
||||||
|
|
||||||
|
// --- Abgleich VOR dem Anhaengen ---------------------------------------------
|
||||||
|
if (letzter) {
|
||||||
|
// 1) Abschneiden am Ende: der beglaubigte Stand darf nie schrumpfen.
|
||||||
|
if (aktuell.maxId !== null && aktuell.maxId < letzter.maxId) {
|
||||||
|
console.error(
|
||||||
|
`ALARM: Das Audit-Log ist geschrumpft. Beglaubigt war maxId=${letzter.maxId}, ` +
|
||||||
|
`jetzt maxId=${aktuell.maxId}. Es wurden Einträge entfernt.`,
|
||||||
|
);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Umschreiben: der frueher beglaubigte Kopf muss reproduzierbar bleiben.
|
||||||
|
const rueck = await hole(`/api/audit-logs/checkpoint?atId=${letzter.maxId}`);
|
||||||
|
if (rueck.atHash === null) {
|
||||||
|
console.error(`ALARM: Der beglaubigte Eintrag ${letzter.maxId} existiert nicht mehr.`);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
if (rueck.atHash !== letzter.chainHead) {
|
||||||
|
console.error(
|
||||||
|
`ALARM: Der Eintrag ${letzter.maxId} wurde nachträglich verändert.\n` +
|
||||||
|
` beglaubigt: ${letzter.chainHead}\n jetzt : ${rueck.atHash}`,
|
||||||
|
);
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Bestandssiegel darf nicht klammheimlich verschwinden.
|
||||||
|
if (letzter.sealRoot && !aktuell.sealRoot) {
|
||||||
|
console.error('ALARM: Das Bestandssiegel ist verschwunden (vorher beglaubigt vorhanden).');
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
if (letzter.sealRoot && aktuell.sealRoot && letzter.sealRoot !== aktuell.sealRoot) {
|
||||||
|
console.warn(
|
||||||
|
`HINWEIS: Das Bestandssiegel wurde erneuert (${letzter.sealRoot.slice(0, 12)}… → ` +
|
||||||
|
`${aktuell.sealRoot.slice(0, 12)}…). Legitim nach einem Retention-Lauf – sonst prüfen.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Anhaengen, signieren, pushen -------------------------------------------
|
||||||
|
const eintrag = {
|
||||||
|
seq: bisher.length + 1,
|
||||||
|
ts: aktuell.ts,
|
||||||
|
maxId: aktuell.maxId,
|
||||||
|
chainHead: aktuell.chainHead,
|
||||||
|
sealRoot: aktuell.sealRoot,
|
||||||
|
sealStatus: aktuell.sealStatus,
|
||||||
|
};
|
||||||
|
writeFileSync(LEDGER, [...bisher, eintrag].map((e) => JSON.stringify(e)).join('\n') + '\n');
|
||||||
|
|
||||||
|
git('add', LEDGER);
|
||||||
|
// -S signiert den Commit mit dem Schluessel DIESES Rechners.
|
||||||
|
const commitArgs = ['commit', ...(SIGN ? ['-S'] : []), '-m',
|
||||||
|
`Checkpoint ${eintrag.seq} – maxId ${eintrag.maxId} – ${eintrag.ts}`];
|
||||||
|
if (!SIGN) console.warn('WARNUNG: Commit wird NICHT signiert (NOTARY_SIGN=false).');
|
||||||
|
git(...commitArgs);
|
||||||
|
if (PUSH) git('push');
|
||||||
|
|
||||||
|
console.log(`OK: Checkpoint ${eintrag.seq} beglaubigt (maxId ${eintrag.maxId}).`);
|
||||||
Reference in New Issue
Block a user