Skip to content

v0.5.20

Choose a tag to compare

@github-actions github-actions released this 04 May 15:22
· 587 commits to main since this release

What's new in v0.5.20

v0.5.20 closes the runtime side of the ack workflow opened by v0.5.17. The CI side (TOML in repo, versioned, change via PR review) is now complemented by a daemon HTTP API (POST / DELETE / GET on /api/findings/{signature}/ack, GET /api/acks) backed by a JSONL append-only file at ~/.local/share/perf-sentinel/acks.jsonl. An SRE on call can now mute a finding without a PR cycle on the application repo, while the team-agreed permanent acks in the TOML baseline stay immutable from the runtime side. The two sources are unioned at query time with TOML winning on conflict, so an operator who tries to write a daemon ack on a signature already covered by the TOML baseline gets a 409 Conflict rather than a silent no-op.

A behavior change ships alongside: GET /api/findings now filters out acked findings by default (mirroring the CLI 0.5.17 --acknowledgments default). Pass ?include_acked=true to restore the pre-0.5.20 shape, with each acked entry annotated by acknowledged_by: { source: "toml" | "daemon", ... } so clients can render both sources distinctly. The two diagnostic endpoints /api/findings/{trace_id} and /api/export/report keep their previous shape: per-trace and full-report views may need to surface acked findings even on the default path.

The daemon ack store is hardened against local-filesystem attacks (Trojan Source defense extended with U+00AD and U+2060-U+2064, O_NOFOLLOW plus post-open mode-& 0o077 check on Unix, mode 0o600 on creation, atomic compaction via tmp + rename). Optional X-API-Key authentication on POST and DELETE (constant-time compare via subtle::ConstantTimeEq) protects deployments that bind the daemon to a non-loopback interface. The default 127.0.0.1 posture from previous releases is unchanged. Per-field caps (256 bytes on by, 1024 bytes on reason, 4 KiB per JSONL line, 64 MiB on the file, 10 000 active acks) bound disk and memory usage under adversarial input.

The release also clarifies the Report.warning_details lifecycle introduced in 0.5.19: cold_start is documented as transient (auto-clears on first batch), ingestion_drops as sticky (cumulative since daemon start, cleared only on restart). New transient-vs-sticky table in docs/METRICS.md plus operator workflow guidance in docs/RUNBOOK.md and the FR mirrors. This closes the third B3 lab followup investigated 2026-05-04, alongside two earlier followups (process collector cfg-gating, OTLP rejected reasons coverage) re-audited and confirmed as no action needed.

Helm chart 0.2.23 ships in lockstep, bumping the default daemon image tag to ghcr.io/robintra/perf-sentinel:0.5.20.

Added

  • Three new daemon endpoints on the existing HTTP query API: POST /api/findings/{signature}/ack to acknowledge a finding at runtime, DELETE /api/findings/{signature}/ack to revoke, and GET /api/acks to list active runtime acks. The signature uses the same canonical <finding_type>:<service>:<sanitized_endpoint>:<sha256-prefix> format as the CI TOML workflow, so an operator can copy a value from analyze --format json | jq '.findings[].signature' straight into a curl call.
  • JSONL append-only persistence at ~/.local/share/perf-sentinel/acks.jsonl by default (configurable via [daemon.ack] storage_path). Each ack and each unack is one line, tail -f-able for live audit. The file is replayed and atomically rewritten via tmp + rename at every daemon restart, so an ack / unack churn loop cannot accumulate forever.
  • ResolvedTomlAck wrapper in daemon::query_api pre-parses the TOML expires_at: Option<String> into Option<DateTime<Utc>> once at startup. The hot-path lookup_ack now does a single datetime comparison per finding instead of a chrono::NaiveDate::parse_from_str per match.
  • AckStore::snapshot_active returns Arc<HashMap<String, AckEntry>> (a single atomic refcount inc, no data copy). The active map sits behind RwLock<Arc<HashMap>>, so concurrent GET /api/findings polls take a cheap clone of the Arc and never block writers. Writers pay an O(N) HashMap clone outside the write lock to keep readers wait-free.
  • Optional API key authentication via [daemon.ack] api_key. When set, POST and DELETE require an X-API-Key header that matches the configured secret (constant-time compared via subtle::ConstantTimeEq). GET /api/acks and GET /api/findings stay unauthenticated by design (loopback reads). Empty strings and keys shorter than 12 characters are rejected at config load.
  • Audit by field resolved with priority order on every write: X-User-Id HTTP header, then JSON body by, then "anonymous" fallback. BiDi and invisible Unicode characters are stripped before persistence (crate::report::sarif::strip_bidi_and_invisible), with the sanitization set extended to cover U+00AD (soft hyphen) and U+2060-U+2064 (word joiner plus invisible operators).
  • ?include_acked=true query parameter on GET /api/findings. When passed, returns the full set with each acked entry annotated by acknowledged_by: { source, ... }. The source discriminant is "toml" for CI baseline acks and "daemon" for runtime acks.
  • TOML and JSONL interop: the daemon reads .perf-sentinel-acknowledgments.toml (path configurable via [daemon.ack] toml_path) at startup as immutable baseline and unions with the JSONL store. TOML wins on conflict. A POST on a signature already covered by an active TOML ack returns 409 Conflict with a clear message pointing the operator at the TOML file rather than silently writing a JSONL line that would be shadowed on read.
  • Hardened file open on Unix: O_NOFOLLOW plus a tokio::fs::symlink_metadata pre-check (SymlinkRefused typed error rather than the kernel's ELOOP), mode 0o600 on creation, and a post-open permissions().mode() & 0o077 check that rejects pre-existing weak-mode files. The startup compaction unconditionally rewrites the file with mode 0o600, eliminating any weak-permission window an attacker could plant before daemon launch.
  • Hard caps: 64 MiB on the JSONL file, 4 KiB per line (allocation-bounded via byte-by-byte read, not just post-allocation check), 10 000 active acks, 1024 bytes on the signature, 256 bytes on by, 1024 bytes on reason, with UTF-8-boundary truncation shared with the existing span-event sanitization (crate::event::truncate_field).
  • 20 unit tests in crates/sentinel-core/src/daemon/ack.rs covering replay, compaction, expiry, BiDi stripping, signature validation, file-size caps, parse errors with line numbers, concurrent-write integrity, symlink refusal, and weak-permission reset at startup.
  • 7 integration tests in crates/sentinel-core/src/daemon/query_api.rs covering POST / DELETE / GET, the ?include_acked flag, TOML-wins conflict resolution (now returning 409), the X-API-Key auth path, and a regression test that pins the JSON shape of FindingResponse against future serde-flatten collisions.
  • 3 config tests covering the new [daemon.ack] section and the api_key length validator (empty rejected, < 12 chars rejected, 12+ accepted).
  • docs/QUERY-API.md and docs/FR/QUERY-API-FR.md: full reference for the three new endpoints with curl examples, request and response shapes, status code matrix, and a "TOML and JSONL interop" section that documents the conflict resolution rules.
  • docs/RUNBOOK.md and docs/FR/RUNBOOK-FR.md "Acknowledging findings at runtime" section with three curl examples (no-auth, auth via TOML config, churn-and-compaction), plus the transient-vs-sticky lifecycle clarification on cold_start and ingestion_drops.
  • docs/METRICS.md and docs/FR/METRICS-FR.md: transient-vs-sticky Report.warning_details table.
  • docs/CONFIGURATION.md and docs/FR/CONFIGURATION-FR.md: new [daemon.ack] section with field reference and rationale (why no /tmp fallback, why mode 0600, what the api_key length floor means).
  • .gitleaks.toml allowlist for the documented finding-signature pattern, anchored on the actual FindingType discriminants. Matches against the full source line via regexTarget = "line", never on the bare 16-hex tail in isolation, so a real high-entropy secret nearby is still detected.

Changed

  • GET /api/findings default behavior: filters out acked findings by default (CI TOML plus daemon JSONL union). Pass ?include_acked=true to restore the pre-0.5.20 behavior. Aligned with the CLI 0.5.17 --acknowledgments default for consistency. The per-trace /api/findings/{trace_id} and /api/export/report endpoints keep their previous shape.
  • crate::acknowledgments::is_ack_active promoted to pub(crate) so the daemon side reuses the same TOML expiry check without duplicating the chrono::NaiveDate parse logic.
  • crate::event::truncate_field promoted to pub(crate) and reused by daemon::ack for the by and reason field caps. Single source of truth for byte-and-char-boundary truncation across the crate.
  • init_ack_resources error policy split by source: when the operator explicitly set [daemon.ack] storage_path or [daemon.ack] toml_path, a load failure on that path is fatal at startup with a typed DaemonError::AckTomlLoad { path, source } or DaemonError::AckStoreInit { path, source }. When the path was resolved from the default (dirs::data_local_dir() or .perf-sentinel-acknowledgments.toml in CWD), failures are logged at WARN and the daemon stays up so OTLP ingestion, /metrics and /health keep serving. The three ack endpoints return 503 Service Unavailable until the operator fixes the default path and restarts.
  • Helm chart 0.2.22 to 0.2.23, appVersion 0.5.19 to 0.5.20, default daemon image tag points at ghcr.io/robintra/perf-sentinel:0.5.20. The artifacthub.io/images annotation is updated in lockstep.

Behavior

  • GET /api/findings shape change is opt-out, not opt-in. Existing clients that scrape this endpoint and alert on the count will silently miss critical findings if those findings are acked. Audit your monitoring after the upgrade and either pass ?include_acked=true or rely on the new acknowledged_by annotation to render both sets.
  • Backward compatibility on the other endpoints. /api/findings/{trace_id}, /api/explain/{trace_id}, /api/correlations, /api/status, /api/export/report keep their pre-0.5.20 JSON shape byte-for-byte.
  • Default-path ack init failure is non-fatal. A daemon launched without [daemon.ack] storage_path whose default location cannot be resolved (missing HOME, denied write perms on ~/.local/share) logs a WARN and continues without an ack store. Operator-supplied paths still fail loud at startup, the right place for typo and permission diagnosis.
  • TOML wins on conflict, with a loud signal. A POST on a TOML-acked signature returns 409 Conflict instead of writing a no-op JSONL line. Pre-0.5.20 the daemon-side write would have been silently shadowed by the TOML baseline on read.
  • [daemon.ack] api_key shorter than 12 characters is rejected at config load. Pre-0.5.20 the shipped behavior was warn-only at <16. The daemon does not rate-limit POST /ack, so a co-resident attacker on the loopback interface could brute-force shorter keys in a tractable window. 12 characters of [a-z0-9-] puts the brute-force horizon past 10^17 attempts, well beyond any realistic deployment.
  • Process metrics, OTLP rejected counter, structured warnings from 0.5.19 are unchanged. The transient-vs-sticky doc clarification is documentation-only.

Documentation

  • New "Acknowledging findings at runtime" section in docs/RUNBOOK.md and docs/FR/RUNBOOK-FR.md with three curl examples and a TOML-vs-JSONL audit-source comparison table.
  • New [daemon.ack] section in docs/CONFIGURATION.md and docs/FR/CONFIGURATION-FR.md with field reference, rationale for the no-/tmp fallback, and the mode 0o600 discipline.
  • Three new endpoints documented in docs/QUERY-API.md and docs/FR/QUERY-API-FR.md with request and response shapes, status code matrix, and the TOML and JSONL interop section.
  • Transient-vs-sticky table for Report.warning_details in docs/METRICS.md and docs/FR/METRICS-FR.md, with operator workflow text in the corresponding RUNBOOK entries.

Install

Prebuilt binaries (Linux amd64 / arm64, macOS arm64, Windows amd64):

curl -LO https://github.com/robintra/perf-sentinel/releases/download/v0.5.20/perf-sentinel-linux-amd64
chmod +x perf-sentinel-linux-amd64
sudo mv perf-sentinel-linux-amd64 /usr/local/bin/perf-sentinel

Linux binaries are statically linked against musl and run on any distribution (Alpine, Debian, RHEL, Ubuntu any version) regardless of glibc version, and inside FROM scratch images.

From crates.io:

cargo install perf-sentinel --version 0.5.20

Docker:

docker run --rm -p 4317:4317 -p 4318:4318 \
  ghcr.io/robintra/perf-sentinel:0.5.20 watch --listen-address 0.0.0.0

Helm chart 0.2.23 ships alongside, see the matching chart-v0.2.23 release for the chart-side details.

What's Changed

  • chore(ci)(deps): bump SonarSource/sonarqube-scan-action from 7.1.0 to 8.0.0 in the other-actions group by @dependabot[bot] in #13
  • chore(ci)(deps): bump taiki-e/install-action from 2.75.22 to 2.75.30 in the ci-actions group by @dependabot[bot] in #11
  • chore(ci)(deps): bump github/codeql-action from 4.35.2 to 4.35.3 in the security-actions group across 1 directory by @dependabot[bot] in #12

Full Changelog: v0.5.19...v0.5.20