Integrate reviewed audit recovery guidance repair

Preserve the exact reviewed specialist commit and its documentation and signer controls alongside the current canonical frontier.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot] 2026-10-01 20:42:05 +01:00
commit db35048aca
10 changed files with 448 additions and 36 deletions

View file

@ -4,6 +4,11 @@ Pulse's audit log records security-relevant events with tamper-evident signature
**Requires:** Pro, legacy Pro+, or Cloud license with the `audit_logging` capability to query, export, and verify events via the API. Events are recorded on all plans, but the API endpoints are license-gated.
An active licence alone does not enable the audit UI/API on the public
community runtime. If the panel says **Pulse Pro runtime required**, follow
**Download Pulse Pro** to the private runtime for your deployment. Do not buy
a second licence or reset the data directory to clear this gate.
For plan details, see [PULSE_PRO.md](PULSE_PRO.md). For API endpoints, see [API Reference](API.md#-audit-log-pro).
---
@ -116,7 +121,9 @@ sensitive data; keep it outside shared repositories and issue attachments.
## Tamper Detection
Every audit event is cryptographically signed at creation time. You can verify that an event has not been modified:
When signing is available, Pulse signs audit events at creation time. Events
captured without a signer remain unsigned. Use **Verify** in the signed-in
Audit Log panel, or the authenticated read below, to check a stored signature:
```bash
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
@ -132,7 +139,49 @@ Response:
}
```
If `verified` is `false`, the event data has been tampered with since it was recorded.
`verified: true` means the signature matches the verifier's key and supported
signature format. It does not prove that the history is complete or that no
events were deleted. `verified: false` means the check failed, not that Pulse
has established why it failed. Keep that result as evidence.
### Verification failures and safe recovery
| Result | What it establishes | Safe next step |
| --- | --- | --- |
| **Not checked** | No verification result has been obtained. | Use **Verify** for the relevant event. |
| **Unsigned** | No signature was stored. | Check whether signing was available when the event was captured. Later enabling signing cannot authenticate it retroactively. |
| **Failed** / `verified: false` | The stored event and signature do not verify with the current key and supported format. | Preserve the evidence; a different key, missing or damaged signature, unsupported format or modified signed fields can cause this. |
| **Unavailable** / `available: false` | The current logger cannot offer verification. | Check the audit capability/runtime gate and bounded startup logs for persistent-store initialisation failures. |
| **Error** / non-successful HTTP response | The verification request did not return a usable result. | Keep the HTTP status and redacted error; it is not proof that the event failed verification. |
Start with the event's time, the last known working version and whether the
data mount, service account, runtime or backup changed. Inspect a bounded
startup log excerpt locally; distinguish encryption/signing initialisation
errors from an audit database/query error. Do not print the key files or post
whole audit exports, service environments or backups. For an empty panel,
clear filters and check the selected organisation before assuming events were
lost. See [Getting Help](TROUBLESHOOTING.md#-getting-help) for safe evidence.
**Preserve before changing anything.** Keep a consistent private backup of the
current data directory, including the audit store and its signing and
encryption keys. Use your established snapshot/backup procedure for a running
instance; copying a live `.db` file alone is not a consistent backup. For an
offline filesystem backup, stop Pulse first and retain the complete store,
including any SQLite sidecar files, with its original ownership and access.
Do not delete or regenerate a key, replace a live signing key with an older
one, edit audit rows or re-sign old events to make verification pass. A newly
generated signing key does not verify events signed with the previous key.
The encrypted signing key also needs its matching `.encryption.key`; restoring
only one member is not a recovery. Swapping keys can break verification of
newer events while concealing the original problem.
If a matching historical backup exists, restore and compare it in an isolated
test instance using the corresponding Pulse runtime/version, without
replacing the live data or contacting monitored systems and notification
destinations. Keep the current instance and both backups intact. If the
original key is lost or an event was unsigned, do not claim that a restart,
new key or successful check on a different event authenticated that history.
---
@ -160,16 +209,28 @@ See [Multi-Tenant Organizations](MULTI_TENANT.md) for details.
| Export | License-gated (402) | Available |
| `persistentLogging` API flag | `false` | `true` |
On all plans, audit events are written to the SQLite database. However, the query, verify, and export API endpoints require the `audit_logging` license feature and return `402 Payment Required` without it. The `persistentLogging` flag in API responses indicates whether the licensed query capabilities are available.
Pulse attempts SQLite-backed capture on all plans; failure to initialise the
store can leave console-only logging. The query, verify and export endpoints
also require the `audit_logging` capability and a compatible runtime. A
licence/runtime gate is not proof that events were never captured. For an
authorised query, `persistentLogging` describes the active logger, not a count
of historical events or proof that their signatures are valid.
---
## Storage
Audit events are stored in a SQLite database in the Pulse data directory:
The default audit store lives in the Pulse data directory:
- **Single-tenant:** `{data-dir}/audit/audit.db`
- **Multi-tenant:** `{data-dir}/orgs/{org-id}/audit/audit.db`
The default encrypted signing key is `.audit-signing.key` **inside the audit
store directory**, alongside `audit.db`, not at the data-directory root. Its
matching `.encryption.key` is in the data directory (or the organisation's data
directory for a non-default organisation). Keep both with the corresponding
history. A runtime may supply a different audit directory or managed signing
key; use that deployment's actual configuration rather than guessing a path.
Data directory locations:
- systemd: `/etc/pulse/`
- Docker/Kubernetes: `/data/`

View file

@ -6,7 +6,7 @@ Pulse uses a split-configuration model to ensure security and flexibility.
| ------ | --------- | ---------------- |
| `.env` | Authentication & Secrets | 🔒 **Critical** (Read-only by owner) |
| `.encryption.key` | Encryption key for `.enc` files | 🔒 **Critical** |
| `.audit-signing.key` | Audit log signing key (Pro/legacy Pro+/Cloud, encrypted) | 🔒 **Sensitive** |
| `audit/.audit-signing.key` | Encrypted signing key for the default audit store; preserve with its history and matching encryption key | 🔒 **Sensitive** |
| `system.json` | General Settings | 📝 Standard |
| `nodes.enc` | Node Credentials | 🔒 **Encrypted** (AES-256-GCM) |
| `alerts.json` | Alert Rules | 📝 Standard |
@ -35,7 +35,7 @@ Pulse uses a split-configuration model to ensure security and flexibility.
| `sessions.json` | Persistent sessions (includes OIDC refresh tokens) | 🔒 **Sensitive** |
| `update-history.jsonl` | Update history log (in-app updates) | 📝 Standard |
| `metrics.db` | Persistent metrics history (SQLite) | 📝 Standard |
| `audit.db` | Audit log database (Pro/legacy Pro+/Cloud, SQLite) | 🔒 **Sensitive** |
| `audit/audit.db` | Default audit database; capture on all plans, licensed query/export | 🔒 **Sensitive** |
| `baselines.json` | AI baseline data for anomaly detection | 📝 Standard |
| `ai_correlations.json` | AI correlation analysis cache | 📝 Standard |
| `ai_patterns.json` | AI pattern detection data | 📝 Standard |
@ -45,7 +45,11 @@ Pulse uses a split-configuration model to ensure security and flexibility.
Guest metadata entries are keyed by the canonical guest ID format `instance:node:vmid` (for example, `pve1:node1:100`). Legacy dash-separated keys are migrated automatically.
All files are located in `/etc/pulse/` (Systemd) or `/data/` (Docker/Kubernetes) by default.
Paths are relative to `/etc/pulse/` (Systemd) or `/data/` (Docker/Kubernetes) by
default. The audit database and default signing key are in the `audit/`
subdirectory. Runtime-specific audit storage and non-default organisation
paths can differ; see [Audit storage and safe recovery](AUDIT_LOGGING.md#storage).
Do not reset signing or encryption keys as a troubleshooting step.
Path overrides:
- `PULSE_DATA_DIR` sets the base directory for `system.json`, encrypted files, and the bootstrap token.

View file

@ -25,7 +25,7 @@ Pulse uses a split config model:
- **Local auth and secrets**: `.env` (managed by Quick Security Setup or environment overrides, not shown in the UI)
- **Encryption key**: `.encryption.key` (required to decrypt `.enc` files)
- **Audit signing key**: `.audit-signing.key` (Pro/legacy Pro+/Cloud, encrypted)
- **Audit signing key**: `audit/.audit-signing.key` (default store, encrypted; preserve with its matching encryption key and history)
- **System settings**: `system.json` (editable in the UI unless locked by env)
- **Nodes and credentials**: `nodes.enc` (encrypted)
- **Notification config**: `email.enc`, `webhooks.enc`, `apprise.enc` (encrypted)
@ -40,7 +40,7 @@ Pulse uses a split config model:
- **AI pattern data**: `ai_patterns.json`
- **AI remediation data**: `ai_remediations.json`
- **AI incident tracking**: `ai_incidents.json`
- **Audit log database**: `audit.db` (Pro/legacy Pro+/Cloud, SQLite)
- **Audit log database**: `audit/audit.db` (default store; persistent capture on all plans, licensed query/export)
- **Relay/Pro/legacy Pro+/Cloud license**: `license.enc` (encrypted)
- **Host metadata**: `host_metadata.json`
- **Docker metadata**: `docker_metadata.json`
@ -64,6 +64,12 @@ Path mapping:
- systemd/LXC: `/etc/pulse/*`
- Docker/Helm: `/data/*`
The audit paths above are the default store, not a licence-dependent guarantee
that storage initialised successfully. Runtime-specific storage can differ;
see [Audit storage and safe recovery](AUDIT_LOGGING.md#storage) before restoring
history or keys. Do not replace keys in a live instance to clear a verification
failure.
Enterprise/internal multi-org layout:
- Default org uses the root data dir for backward compatibility.
- Non-default orgs use `/orgs/<org-id>/`.

View file

@ -109,19 +109,40 @@ only the relevant port numbers and a redacted error if help is needed.
- If another admin can log in, use `POST /api/security/reset-lockout` to clear the lockout for your username or IP.
#### Audit Log verification shows unsigned events
- **Symptom**: Audit Log entries show “Unsigned” or verification fails in the UI.
- **Root cause**: Audit signing is disabled (crypto manager unavailable), so events are stored without signatures.
- **Fix**: Ensure `.encryption.key` is present and Pro/legacy Pro+/Cloud audit logging is enabled, then restart Pulse to regenerate `.audit-signing.key`. Newly created events will be signed; existing unsigned events remain unsigned.
**Unsigned** means no signature was stored for that event; it is not the same
as **Failed** verification or a request **Error**. Signing can be unavailable
when Pulse cannot initialise its encryption manager. Check a bounded startup
log excerpt and the persistent data mount and access for the service account,
without printing key contents. Do not delete or regenerate `.encryption.key`
or an audit signing key to make the warning disappear. Restoring signing for
new events cannot authenticate an old unsigned event.
See [Audit verification and safe recovery](AUDIT_LOGGING.md#verification-failures-and-safe-recovery)
for the different results and evidence to retain.
#### Audit Log is empty
- **Symptom**: Audit Log shows zero events or "Console Logging Only."
- **Root cause**: Community plan uses console logging only, or Pro/legacy Pro+/Cloud audit logging is not enabled.
- **Fix**: Use Pro, legacy Pro+, or Cloud with audit logging enabled, then generate new audit events (logins, token creation, password changes).
Clear the event, user, date and success filters, and check the selected
organisation first. A query error is not an empty history. **Pulse Pro runtime
required** means an active licence is running on the public community runtime;
follow the panel's **Download Pulse Pro** link rather than buying another
licence or resetting storage. Without the audit capability, reads and exports
are gated, but Pulse still attempts to capture events persistently on all
plans. **Console Logging Only** can also reflect unavailable persistent
storage: inspect the bounded startup logs for audit initialisation errors.
Do not change passwords or create tokens merely to populate the panel.
#### Audit Log verification fails for older events
- **Symptom**: Older events fail verification while newer events pass.
- **Root cause**: The audit signing key changed (for example, `.audit-signing.key` was regenerated), so signatures no longer match.
- **Fix**: Restore the previous `.audit-signing.key` from backup to verify older events. If rotated intentionally, expect older events to fail verification.
A failed signature check does not by itself prove tampering. An event signed
with a different key can fail even when its contents are unchanged; missing
signatures and an unsupported or damaged signature format also cannot verify.
Keep the failure as evidence. Do not swap an old key into the live instance,
edit audit rows or re-sign old events. Preserve the current data and keys
privately before any recovery; compare a matching backup only in an isolated
restore, not by overwriting today's history. Follow
[safe audit recovery](AUDIT_LOGGING.md#verification-failures-and-safe-recovery).
### Monitoring Data

View file

@ -4,6 +4,11 @@ Pulse's audit log records security-relevant events with tamper-evident signature
**Requires:** Pro, legacy Pro+, or Cloud license with the `audit_logging` capability to query, export, and verify events via the API. Events are recorded on all plans, but the API endpoints are license-gated.
An active licence alone does not enable the audit UI/API on the public
community runtime. If the panel says **Pulse Pro runtime required**, follow
**Download Pulse Pro** to the private runtime for your deployment. Do not buy
a second licence or reset the data directory to clear this gate.
For plan details, see [PULSE_PRO.md](PULSE_PRO.md). For API endpoints, see [API Reference](API.md#-audit-log-pro).
---
@ -116,7 +121,9 @@ sensitive data; keep it outside shared repositories and issue attachments.
## Tamper Detection
Every audit event is cryptographically signed at creation time. You can verify that an event has not been modified:
When signing is available, Pulse signs audit events at creation time. Events
captured without a signer remain unsigned. Use **Verify** in the signed-in
Audit Log panel, or the authenticated read below, to check a stored signature:
```bash
curl --disable --fail-with-body --header "@$HOME/.config/pulse/api-header" \
@ -132,7 +139,49 @@ Response:
}
```
If `verified` is `false`, the event data has been tampered with since it was recorded.
`verified: true` means the signature matches the verifier's key and supported
signature format. It does not prove that the history is complete or that no
events were deleted. `verified: false` means the check failed, not that Pulse
has established why it failed. Keep that result as evidence.
### Verification failures and safe recovery
| Result | What it establishes | Safe next step |
| --- | --- | --- |
| **Not checked** | No verification result has been obtained. | Use **Verify** for the relevant event. |
| **Unsigned** | No signature was stored. | Check whether signing was available when the event was captured. Later enabling signing cannot authenticate it retroactively. |
| **Failed** / `verified: false` | The stored event and signature do not verify with the current key and supported format. | Preserve the evidence; a different key, missing or damaged signature, unsupported format or modified signed fields can cause this. |
| **Unavailable** / `available: false` | The current logger cannot offer verification. | Check the audit capability/runtime gate and bounded startup logs for persistent-store initialisation failures. |
| **Error** / non-successful HTTP response | The verification request did not return a usable result. | Keep the HTTP status and redacted error; it is not proof that the event failed verification. |
Start with the event's time, the last known working version and whether the
data mount, service account, runtime or backup changed. Inspect a bounded
startup log excerpt locally; distinguish encryption/signing initialisation
errors from an audit database/query error. Do not print the key files or post
whole audit exports, service environments or backups. For an empty panel,
clear filters and check the selected organisation before assuming events were
lost. See [Getting Help](TROUBLESHOOTING.md#-getting-help) for safe evidence.
**Preserve before changing anything.** Keep a consistent private backup of the
current data directory, including the audit store and its signing and
encryption keys. Use your established snapshot/backup procedure for a running
instance; copying a live `.db` file alone is not a consistent backup. For an
offline filesystem backup, stop Pulse first and retain the complete store,
including any SQLite sidecar files, with its original ownership and access.
Do not delete or regenerate a key, replace a live signing key with an older
one, edit audit rows or re-sign old events to make verification pass. A newly
generated signing key does not verify events signed with the previous key.
The encrypted signing key also needs its matching `.encryption.key`; restoring
only one member is not a recovery. Swapping keys can break verification of
newer events while concealing the original problem.
If a matching historical backup exists, restore and compare it in an isolated
test instance using the corresponding Pulse runtime/version, without
replacing the live data or contacting monitored systems and notification
destinations. Keep the current instance and both backups intact. If the
original key is lost or an event was unsigned, do not claim that a restart,
new key or successful check on a different event authenticated that history.
---
@ -160,16 +209,28 @@ See [Multi-Tenant Organizations](MULTI_TENANT.md) for details.
| Export | License-gated (402) | Available |
| `persistentLogging` API flag | `false` | `true` |
On all plans, audit events are written to the SQLite database. However, the query, verify, and export API endpoints require the `audit_logging` license feature and return `402 Payment Required` without it. The `persistentLogging` flag in API responses indicates whether the licensed query capabilities are available.
Pulse attempts SQLite-backed capture on all plans; failure to initialise the
store can leave console-only logging. The query, verify and export endpoints
also require the `audit_logging` capability and a compatible runtime. A
licence/runtime gate is not proof that events were never captured. For an
authorised query, `persistentLogging` describes the active logger, not a count
of historical events or proof that their signatures are valid.
---
## Storage
Audit events are stored in a SQLite database in the Pulse data directory:
The default audit store lives in the Pulse data directory:
- **Single-tenant:** `{data-dir}/audit/audit.db`
- **Multi-tenant:** `{data-dir}/orgs/{org-id}/audit/audit.db`
The default encrypted signing key is `.audit-signing.key` **inside the audit
store directory**, alongside `audit.db`, not at the data-directory root. Its
matching `.encryption.key` is in the data directory (or the organisation's data
directory for a non-default organisation). Keep both with the corresponding
history. A runtime may supply a different audit directory or managed signing
key; use that deployment's actual configuration rather than guessing a path.
Data directory locations:
- systemd: `/etc/pulse/`
- Docker/Kubernetes: `/data/`

View file

@ -6,7 +6,7 @@ Pulse uses a split-configuration model to ensure security and flexibility.
| ------ | --------- | ---------------- |
| `.env` | Authentication & Secrets | 🔒 **Critical** (Read-only by owner) |
| `.encryption.key` | Encryption key for `.enc` files | 🔒 **Critical** |
| `.audit-signing.key` | Audit log signing key (Pro/legacy Pro+/Cloud, encrypted) | 🔒 **Sensitive** |
| `audit/.audit-signing.key` | Encrypted signing key for the default audit store; preserve with its history and matching encryption key | 🔒 **Sensitive** |
| `system.json` | General Settings | 📝 Standard |
| `nodes.enc` | Node Credentials | 🔒 **Encrypted** (AES-256-GCM) |
| `alerts.json` | Alert Rules | 📝 Standard |
@ -35,7 +35,7 @@ Pulse uses a split-configuration model to ensure security and flexibility.
| `sessions.json` | Persistent sessions (includes OIDC refresh tokens) | 🔒 **Sensitive** |
| `update-history.jsonl` | Update history log (in-app updates) | 📝 Standard |
| `metrics.db` | Persistent metrics history (SQLite) | 📝 Standard |
| `audit.db` | Audit log database (Pro/legacy Pro+/Cloud, SQLite) | 🔒 **Sensitive** |
| `audit/audit.db` | Default audit database; capture on all plans, licensed query/export | 🔒 **Sensitive** |
| `baselines.json` | AI baseline data for anomaly detection | 📝 Standard |
| `ai_correlations.json` | AI correlation analysis cache | 📝 Standard |
| `ai_patterns.json` | AI pattern detection data | 📝 Standard |
@ -45,7 +45,11 @@ Pulse uses a split-configuration model to ensure security and flexibility.
Guest metadata entries are keyed by the canonical guest ID format `instance:node:vmid` (for example, `pve1:node1:100`). Legacy dash-separated keys are migrated automatically.
All files are located in `/etc/pulse/` (Systemd) or `/data/` (Docker/Kubernetes) by default.
Paths are relative to `/etc/pulse/` (Systemd) or `/data/` (Docker/Kubernetes) by
default. The audit database and default signing key are in the `audit/`
subdirectory. Runtime-specific audit storage and non-default organisation
paths can differ; see [Audit storage and safe recovery](AUDIT_LOGGING.md#storage).
Do not reset signing or encryption keys as a troubleshooting step.
Path overrides:
- `PULSE_DATA_DIR` sets the base directory for `system.json`, encrypted files, and the bootstrap token.

View file

@ -25,7 +25,7 @@ Pulse uses a split config model:
- **Local auth and secrets**: `.env` (managed by Quick Security Setup or environment overrides, not shown in the UI)
- **Encryption key**: `.encryption.key` (required to decrypt `.enc` files)
- **Audit signing key**: `.audit-signing.key` (Pro/legacy Pro+/Cloud, encrypted)
- **Audit signing key**: `audit/.audit-signing.key` (default store, encrypted; preserve with its matching encryption key and history)
- **System settings**: `system.json` (editable in the UI unless locked by env)
- **Nodes and credentials**: `nodes.enc` (encrypted)
- **Notification config**: `email.enc`, `webhooks.enc`, `apprise.enc` (encrypted)
@ -40,7 +40,7 @@ Pulse uses a split config model:
- **AI pattern data**: `ai_patterns.json`
- **AI remediation data**: `ai_remediations.json`
- **AI incident tracking**: `ai_incidents.json`
- **Audit log database**: `audit.db` (Pro/legacy Pro+/Cloud, SQLite)
- **Audit log database**: `audit/audit.db` (default store; persistent capture on all plans, licensed query/export)
- **Relay/Pro/legacy Pro+/Cloud license**: `license.enc` (encrypted)
- **Host metadata**: `host_metadata.json`
- **Docker metadata**: `docker_metadata.json`
@ -64,6 +64,12 @@ Path mapping:
- systemd/LXC: `/etc/pulse/*`
- Docker/Helm: `/data/*`
The audit paths above are the default store, not a licence-dependent guarantee
that storage initialised successfully. Runtime-specific storage can differ;
see [Audit storage and safe recovery](AUDIT_LOGGING.md#storage) before restoring
history or keys. Do not replace keys in a live instance to clear a verification
failure.
Enterprise/internal multi-org layout:
- Default org uses the root data dir for backward compatibility.
- Non-default orgs use `/orgs/<org-id>/`.

View file

@ -109,19 +109,40 @@ only the relevant port numbers and a redacted error if help is needed.
- If another admin can log in, use `POST /api/security/reset-lockout` to clear the lockout for your username or IP.
#### Audit Log verification shows unsigned events
- **Symptom**: Audit Log entries show “Unsigned” or verification fails in the UI.
- **Root cause**: Audit signing is disabled (crypto manager unavailable), so events are stored without signatures.
- **Fix**: Ensure `.encryption.key` is present and Pro/legacy Pro+/Cloud audit logging is enabled, then restart Pulse to regenerate `.audit-signing.key`. Newly created events will be signed; existing unsigned events remain unsigned.
**Unsigned** means no signature was stored for that event; it is not the same
as **Failed** verification or a request **Error**. Signing can be unavailable
when Pulse cannot initialise its encryption manager. Check a bounded startup
log excerpt and the persistent data mount and access for the service account,
without printing key contents. Do not delete or regenerate `.encryption.key`
or an audit signing key to make the warning disappear. Restoring signing for
new events cannot authenticate an old unsigned event.
See [Audit verification and safe recovery](AUDIT_LOGGING.md#verification-failures-and-safe-recovery)
for the different results and evidence to retain.
#### Audit Log is empty
- **Symptom**: Audit Log shows zero events or "Console Logging Only."
- **Root cause**: Community plan uses console logging only, or Pro/legacy Pro+/Cloud audit logging is not enabled.
- **Fix**: Use Pro, legacy Pro+, or Cloud with audit logging enabled, then generate new audit events (logins, token creation, password changes).
Clear the event, user, date and success filters, and check the selected
organisation first. A query error is not an empty history. **Pulse Pro runtime
required** means an active licence is running on the public community runtime;
follow the panel's **Download Pulse Pro** link rather than buying another
licence or resetting storage. Without the audit capability, reads and exports
are gated, but Pulse still attempts to capture events persistently on all
plans. **Console Logging Only** can also reflect unavailable persistent
storage: inspect the bounded startup logs for audit initialisation errors.
Do not change passwords or create tokens merely to populate the panel.
#### Audit Log verification fails for older events
- **Symptom**: Older events fail verification while newer events pass.
- **Root cause**: The audit signing key changed (for example, `.audit-signing.key` was regenerated), so signatures no longer match.
- **Fix**: Restore the previous `.audit-signing.key` from backup to verify older events. If rotated intentionally, expect older events to fail verification.
A failed signature check does not by itself prove tampering. An event signed
with a different key can fail even when its contents are unchanged; missing
signatures and an unsupported or damaged signature format also cannot verify.
Keep the failure as evidence. Do not swap an old key into the live instance,
edit audit rows or re-sign old events. Preserve the current data and keys
privately before any recovery; compare a matching backup only in an isolated
restore, not by overwriting today's history. Follow
[safe audit recovery](AUDIT_LOGGING.md#verification-failures-and-safe-recovery).
### Monitoring Data

View file

@ -0,0 +1,142 @@
package audit_test
import (
"bytes"
"encoding/base64"
"os"
"path/filepath"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/crypto"
"github.com/rcourtman/pulse-go-rewrite/pkg/audit"
)
// These controls exercise the recovery guide's key/evidence distinctions with
// the real signer and encryption manager. They use only synthetic, private
// temporary directories: no SQLite store, installed instance or external call.
func TestSignerRecoveryGuidance(t *testing.T) {
dataDir := t.TempDir()
writeEncryptionKey := func(dir string, value byte) *crypto.CryptoManager {
t.Helper()
key := base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{value}, 32))
if err := os.WriteFile(filepath.Join(dir, ".encryption.key"), []byte(key), 0o600); err != nil {
t.Fatalf("write synthetic encryption key: %v", err)
}
manager, err := crypto.NewCryptoManagerAt(dir)
if err != nil {
t.Fatalf("load synthetic encryption manager: %v", err)
}
return manager
}
manager := writeEncryptionKey(dataDir, 'A')
auditDir := filepath.Join(dataDir, "audit")
signer, err := audit.NewSigner(auditDir, manager)
if err != nil {
t.Fatalf("create original signer: %v", err)
}
event := audit.Event{
ID: "synthetic-recovery-evidence",
Timestamp: time.Date(2026, 10, 1, 12, 0, 0, 0, time.UTC),
EventType: "login",
User: "synthetic-user",
Path: "/api/login",
Success: true,
Details: "synthetic recovery control",
}
event.Signature = signer.Sign(event)
if !signer.Verify(event) {
t.Fatal("original event must verify before recovery comparisons")
}
storedKey, err := os.ReadFile(filepath.Join(auditDir, ".audit-signing.key"))
if err != nil {
t.Fatalf("read synthetic encrypted key: %v", err)
}
t.Run("reload preserves verification without regeneration", func(t *testing.T) {
reloaded, err := audit.NewSigner(auditDir, manager)
if err != nil || !reloaded.Verify(event) {
t.Fatalf("reload failed to verify unchanged event: %v", err)
}
currentKey, err := os.ReadFile(filepath.Join(auditDir, ".audit-signing.key"))
if err != nil || !bytes.Equal(storedKey, currentKey) {
t.Fatal("reload must not replace the stored signing key")
}
})
t.Run("new signing key fails unchanged historical event", func(t *testing.T) {
freshSigner, err := audit.NewSigner(filepath.Join(t.TempDir(), "audit"), manager)
if err != nil {
t.Fatalf("create unrelated signer: %v", err)
}
if freshSigner.Verify(event) {
t.Fatal("a new key must not verify an unchanged old event")
}
if !signer.Verify(event) {
t.Fatal("failed check with another key must not alter the original evidence")
}
})
t.Run("signing key alone cannot restore with different encryption key", func(t *testing.T) {
restoreDir := t.TempDir()
otherManager := writeEncryptionKey(restoreDir, 'B')
restoreAuditDir := filepath.Join(restoreDir, "audit")
if err := os.MkdirAll(restoreAuditDir, 0o700); err != nil {
t.Fatalf("create isolated restore directory: %v", err)
}
keyPath := filepath.Join(restoreAuditDir, ".audit-signing.key")
if err := os.WriteFile(keyPath, storedKey, 0o600); err != nil {
t.Fatalf("copy synthetic encrypted key: %v", err)
}
if _, err := audit.NewSigner(restoreAuditDir, otherManager); err == nil {
t.Fatal("wrong encryption key must not silently recover the signer")
}
after, err := os.ReadFile(keyPath)
if err != nil || !bytes.Equal(after, storedKey) {
t.Fatal("a failed restore must preserve the supplied encrypted key")
}
})
t.Run("matching key pair verifies in isolated restore", func(t *testing.T) {
restoreDir := t.TempDir()
restoredManager := writeEncryptionKey(restoreDir, 'A')
restoreAuditDir := filepath.Join(restoreDir, "audit")
if err := os.MkdirAll(restoreAuditDir, 0o700); err != nil {
t.Fatalf("create isolated restore directory: %v", err)
}
if err := os.WriteFile(filepath.Join(restoreAuditDir, ".audit-signing.key"), storedKey, 0o600); err != nil {
t.Fatalf("copy synthetic encrypted key: %v", err)
}
restoredSigner, err := audit.NewSigner(restoreAuditDir, restoredManager)
if err != nil || !restoredSigner.Verify(event) {
t.Fatalf("matching isolated key pair failed to verify old event: %v", err)
}
if !signer.Verify(event) {
t.Fatal("isolated restore must leave the original verifier usable")
}
})
t.Run("unsigned is not retrospectively authenticated", func(t *testing.T) {
unsigned := event
unsigned.Signature = ""
if signer.Verify(unsigned) {
t.Fatal("working signer must not authenticate an unsigned old event")
}
})
t.Run("altered data and unsupported format remain failures", func(t *testing.T) {
altered := event
altered.Details = "altered synthetic evidence"
if signer.Verify(altered) {
t.Fatal("changed signed content must still fail verification")
}
unsupported := event
unsupported.Signature = "v99:" + event.Signature
if signer.Verify(unsupported) {
t.Fatal("unsupported signature format must not be accepted")
}
if !signer.Verify(event) {
t.Fatal("negative comparisons must preserve the original evidence")
}
})
}

View file

@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""Keep audit help from treating a failed check as a diagnosis or a reset task.
Signer/key behaviour is exercised separately by TestSignerRecoveryGuidance;
these checks bind those distinctions to the actual canonical and shipped help.
"""
from pathlib import Path
import re
import unittest
ROOT = Path(__file__).resolve().parents[2]
GUIDES = ("AUDIT_LOGGING", "TROUBLESHOOTING", "CONFIGURATION", "DEPLOYMENT_MODELS")
def read_guide(name: str) -> str:
return " ".join((ROOT / "docs" / f"{name}.md").read_text(encoding="utf-8").split())
class AuditRecoveryDocsTest(unittest.TestCase):
def test_shipped_guides_match_the_canonical_evidence(self):
for name in GUIDES:
with self.subTest(guide=name):
self.assertEqual((ROOT / "docs" / f"{name}.md").read_bytes(),
(ROOT / "frontend-modern/public/docs" / f"{name}.md").read_bytes())
def test_verification_results_are_not_conflated(self):
guide = read_guide("AUDIT_LOGGING")
for result in ("**Not checked**", "**Unsigned**", "**Failed**", "**Unavailable**", "**Error**"):
with self.subTest(result=result):
self.assertIn(result, guide)
self.assertIn("not that Pulse has established why it failed", guide)
self.assertIn("not proof that the event failed verification", guide)
self.assertIn("does not prove that the history is complete", guide)
self.assertNotIn("the event data has been tampered with since it was recorded", guide)
def test_recovery_preserves_history_and_both_keys(self):
guide = read_guide("AUDIT_LOGGING")
for boundary in ("consistent private backup", "copying a live `.db` file alone",
"including any SQLite sidecar files", "matching `.encryption.key`",
"isolated test instance", "without replacing the live data",
"or contacting monitored systems and notification destinations",
"Do not delete or regenerate a key", "edit audit rows or re-sign old events"):
with self.subTest(boundary=boundary):
self.assertIn(boundary, guide)
self.assertIn("newly generated signing key does not verify events signed with the previous key", guide)
self.assertIn("Later enabling signing cannot authenticate it retroactively", guide)
def test_troubleshooting_does_not_offer_a_live_key_swap(self):
guide = read_guide("TROUBLESHOOTING")
self.assertNotIn("restart Pulse to regenerate", guide)
self.assertNotIn("Restore the previous `.audit-signing.key` from backup to verify", guide)
self.assertIn("Do not swap an old key into the live instance", guide)
self.assertIn("cannot authenticate an old unsigned event", guide)
self.assertIn("AUDIT_LOGGING.md#verification-failures-and-safe-recovery", guide)
def test_empty_and_gated_are_not_missing_history(self):
for name in ("TROUBLESHOOTING", "AUDIT_LOGGING"):
guide = read_guide(name)
with self.subTest(guide=name):
self.assertIn("Pulse Pro runtime required", guide)
self.assertIn("Download Pulse Pro", guide)
self.assertIn("filters", guide)
self.assertIn("organisation", guide)
short = read_guide("TROUBLESHOOTING")
self.assertIn("A query error is not an empty history", short)
self.assertIn("Do not change passwords or create tokens merely to populate the panel", short)
self.assertNotIn("Community plan uses console logging only", short)
def test_storage_paths_match_the_default_store_not_the_old_root_path(self):
for name in ("CONFIGURATION", "DEPLOYMENT_MODELS"):
guide = read_guide(name)
with self.subTest(guide=name):
self.assertIn("`audit/audit.db`", guide)
self.assertIn("`audit/.audit-signing.key`", guide)
self.assertIn("AUDIT_LOGGING.md#storage", guide)
self.assertNotRegex(guide, r"(?:\| |\*\*: )`\.audit-signing\.key`")
guide = read_guide("AUDIT_LOGGING")
self.assertIn("**inside the audit store directory**", guide)
self.assertIn("organisation's data directory", guide)
self.assertIn("runtime may supply a different audit directory or managed signing key", guide)
if __name__ == "__main__":
unittest.main()