Releases: logtide-dev/logtide
Release list
v1.1.0
The first feature minor since the 1.0.x line. Headline additions are inbound webhook receivers (push events into LogTide from CI/CD, uptime monitors and arbitrary tools through tokenized per-receiver endpoints) and project soft-delete with a 30-day recoverable grace window. Alongside them, five community-reported dashboard and query fixes (#271 to #275) tighten how totals, filter dropdowns and the activity/top-services widgets behave. Database migrations 050, 051 and 052 run on upgrade.
Added
- Inbound webhook receivers (#155): external systems (CI/CD, uptime monitors, arbitrary tools) can now push events into LogTide through per-receiver tokenized endpoints (
POST /api/v1/receivers/:id/:token,lr_-prefixed tokens stored as SHA-256 hashes, timing-safe compare; the URL is the credential since GitHub/Uptime Robot cannot send custom headers). Three adapters normalize payloads into log entries: GitHub (workflow_run and deployment_status events with conclusion-to-level mapping; ping and unsupported events marked skipped), Uptime (shape-detects Uptime Robot alertType and Better Stack incident payloads), and Generic JSON (optional dot-path field mapping with levelMap and defaults, validated by a shared Zod schema; full payload always preserved in metadata). The endpoint answers 202 and processing is asynchronous via a newreceiver-eventsqueue job (queue abstraction, both backends), which runs the adapter, validates againstlogSchemaand ingests throughingestionService.ingestLogs, so PII masking (fail-closed), usage quotas, Sigma detection, pipelines, metering and live tail all apply automatically; the outcome (processed/skipped/failed, normalized output, error) is recorded on areceiver_eventsrow pruned to the last 100 per receiver. Management: project-scoped CRUD + recent-events API under/api/v1/projects/:projectId/receivers(session auth, audit-logged asreceiver.created/receiver.deleted), a newreceivers.maxcapability limit enforced with the canonical withLimitLock pattern (4-case + race tests), and a "Webhook Receivers" section in project settings (create dialog with adapter picker and generic field-mapping inputs, one-time URL reveal with copy, enable/disable switch, recent-events viewer with raw/normalized JSON). Migration 052 (receivers,receiver_events); public docs indocs/receivers.md - Soft-delete for projects with a 30-day grace window: deleting a project now moves it to a recoverable "trash" state (
deleted_at) instead of removing it immediately. Its logs, traces and metrics stay queryable and the project stays viewable read-only during the window (the project detail layout loads withincludeDeleted, shows a "deleted (read-only)" banner with the permanent-deletion date, and hides the Settings tab); a "Restore" action brings it back. A daily purge worker (3 AM) hard-deletes projects past the grace window, callingreservoir.purgeProject()to clear logs/spans/metrics from every storage engine before deleting the row. Name and slug uniqueness moved to partial unique indexes (active projects only) so a deleted project's name/slug can be reused; restoring into a name or slug a new active project has since taken is rejected with a 409 instead of a raw constraint error. Soft-deleted projects are excluded from listings, data-availability widgets and API-key verification (ingestion is refused immediately, with the key-verification cache invalidated on delete). NewPOST /api/v1/projects/:id/restoreandGET /api/v1/projects?includeDeleted=true. Migrations 050 (soft-delete column + partial indexes) and 051 (dropON DELETE CASCADEfrom logs/spans/metrics so an out-of-band project delete cannot bulk-wipe data; the purge worker clears reservoir data explicitly first)
Fixed
- Log total no longer disagrees with the visible logs on TimescaleDB (#271): the displayed result count came from the Postgres query planner's row estimate, which is unreliable at small scale (stale
ANALYZEstats, few rows per chunk) and could be wrong in both directions, so the header showed a total that did not match the listed logs.countEstimatenow falls back to an exactCOUNTbelow a 50k-row estimate and keeps the fast planner estimate above it, so small and moderate deployments see an accurate total while large datasets stay performant. - Dashboard editing over plain HTTP on a LAN IP (#272): adding a panel called
crypto.randomUUID(), which only exists in a secure context (HTTPS or localhost), so it threw when LogTide was accessed over plain HTTP by IP. Panel IDs are now generated through auuid()helper that falls back tocrypto.getRandomValues()(available in non-secure contexts) whenrandomUUIDis missing. - Hosts missing from the filter dropdown (#273):
getDistinctHostnamesclamped its lookup window to 6 hours regardless of the selected range, so hosts that had only logged 6-24h ago were dropped from the hosts dropdown even though their logs were still visible in the (24h) logs view. The window now honors the requested range (default 24h, matching the log query default) so the dropdown lists every host with logs in the same window; hostnames stay cached for 5 minutes to bound the JSONB distinct cost. - Activity Overview chart stuck 1-2h behind live data (#274): on TimescaleDB the panel read its logs/spans/detections series entirely from continuous aggregates, whose refresh policy (
end_offset = 1 bucket, running once per bucket) never materializes the most recent 1-2 buckets, so the chart's latest edge showed empty while logs were still arriving (the raw-backed logs view stayed current, which read like a timezone offset). The panel now reads the cagg only up to a bucket-aligned cutoff two buckets back and backfills the recent tail live from the raw tables, keeping the fast aggregate for the bulk of the window while the latest buckets track real time. ClickHouse/MongoDB are unaffected (no continuous aggregates); alerts already read raw. - Top Services table window was unlabeled (#275): the "Top Services by Volume" widget covers a fixed 7-day window (and its total is a 7-day total), but it sat next to the 24h-framed activity chart with no window label, so its service list and total looked inconsistent with the logs page (which follows the selected range, 24h by default) and read like stale/stuck data. The widget title now states its window ("Last 7 Days") so the different counts are self-explanatory; the 7-day window itself is intentional (daily continuous aggregate for the historical span) and unchanged.
Full Changelog: v1.0.3...v1.1.0
v1.0.3
A security-focused release. It resolves a batch of privately reported issues (coordinated disclosure via KIberblick.de): cross-tenant read on the dashboard API endpoints, stored XSS via OTLP service.name in the service map, an open redirect on the auth-free login/register path, a DNS-rebinding gap in the SSRF guard's HTTP path, a first-admin bootstrap promotion race, and a capability-limit check-then-act race; trace span attributes are now PII-masked as well, and service.name is sanitized at ingestion as defense in depth. No database migrations; drop-in upgrade. Alongside the security work: two correctness follow-ups from the multi-engine bug-hunt sweep (issue #255): Sigma detection now honors full SigmaHQ field-modifier chains, and the service-map p95 is a true window percentile on every storage engine. The storage-layer change was validated against real ClickHouse, MongoDB and TimescaleDB. This line also fixes two operational bugs: a Redis memory leak where completed/failed BullMQ jobs were never evicted, and a nightly SigmaHQ sync that re-imported the whole catalog as enabled and auto-created alert rules. Plus a few frontend touch-ups: theme-aware trace/session IDs in the log detail, per-occurrence trace links on the error page, metadata copy buttons, a breadcrumbs timeline and nested metadata columns in log search.
Security
- Cross-tenant read on the dashboard API endpoints (fixed): the five dashboard endpoints (
/api/v1/dashboard/stats,/timeseries,/top-services,/timeline-events,/recent-errors) plus the newer/activity-overviewtookorganizationIdfrom the query string and only ran the organization-membership check behindif (request.user?.id), which is set for session auth only. For API-key authrequireFullAccesslets any non-write key through without settingrequest.user, so the membership check was skipped and the org was read from the attacker-supplied query rather than the key's boundrequest.organizationId; whenprojectIdwas omitted the project-in-org check was skipped too. A holder of any full-access API key (bound to org A) could read another organization's dashboard data by passing that org's id. All six handlers now route through a sharedresolveDashboardScopethat, for API-key auth, requires the requested org to match the key's bound org and the requested project to match (defaulting to the key's bound project when omitted), mirroringresolveQueryProjectIdwhich already protects the query and traces routes. Session auth keeps the org-membership and project-in-org checks. Reported privately via KIberblick.de - Stored XSS via OTLP
service.namein the service map (fixed):service.namefrom ingested traces passed throughsanitizeForPostgres, which only strips null bytes, so< > " 'survived intospan.service_nameand were served verbatim by the service-map API. InServiceMap.sveltethe EChartstooltip.formatterreturned a raw HTML string built fromparams.name/params.data.source/params.data.target, and ECharts renders tooltip output as HTML, so aservice.namelike<img src=x onerror=...>executed in the browser of any operator who opened the project's service map and hovered the node or edge. User-derived tooltip values are now HTML-escaped via a sharedescapeHtmlutil (also adopted by the SIEM HTML report builder, replacing its private copy). Stored data is left raw on purpose (escaping at the sink, not the store, avoids double-encoding and keeps the JSON API correct). Reported privately via KIberblick.de - Open redirect on auth-free login/register (fixed): in
authMode === 'none'deployments the login and register pages forwarded the user-suppliedredirectquery parameter viagoto(redirectUrl)with no validation, while the normal submit path already checked it. The check now lives in a sharedisSafeInternalPath/safeRedirecthelper used by both the auth-free and normal paths on both pages; it requires a single-leading-slash path and rejects protocol-relative forms including the backslash variant (/\evil.com) that some browsers normalize to//. Reported privately via KIberblick.de - SSRF guard now pins the validated IP (DNS rebinding hardening):
safeFetchresolved and validated the target host, then letfetch()re-resolve at connect time, leaving a resolve-then-connect window where a hostname could rebind to an internal address between check and connect (the TCP monitor path already pinned, the HTTP path did not). The HTTP(S) path now connects through a per-request undici dispatcher whose lookup is pinned to the already-validated address, so the socket reaches the exact IP that passed validation; TLS SNI and certificate validation still use the original hostname. Reported privately via KIberblick.de - First-admin bootstrap race (fixed):
createUserdecided the automatic first-admin promotion with a non-atomichasAnyAdmin()check followed by a separate insert, so concurrent registrations in the zero-admin window could each observe "no admin yet" and all be promoted. The check-then-insert now runs inside a transaction holding a Postgres advisory lock, so at most one registration wins the promotion. Only reachable before the first admin exists (and closed entirely whenINITIAL_ADMIN_*is set). Reported privately via KIberblick.de - Capability limit check-then-act race (fixed): resource-creating routes (api keys, custom dashboards, alert rules, sigma rule import/enable, notification channels) ran
COUNT -> assertWithinLimit -> insertwithout serialization, so parallel requests could each read a count under the limit and then all insert, exceeding a configured finite cap (a quota bypass, not a tenant boundary; the OSS default has no finite limits). The count+create now runs through a sharedwithLimitLockhelper that takes a per-organization, per-capability transaction-scoped advisory lock, so concurrent creators of the same resource type serialize and the cap holds. Reported privately via KIberblick.de - Defense-in-depth on OTLP
service.nameat ingestion: complementing the service-map output-encoding fix above, ingestedservice.name(for logs, spans and metrics) is now run through a sharedsanitizeServiceNamethat strips control characters (C0/DEL/C1, including null bytes) and caps the length, while deliberately preserving otherwise legitimate characters (escaping still happens at each sink). This keeps a raw payload from resurfacing through a sink that is added later or forgets to encode. Suggested by KIberblick.de - PII masking now also covers trace span attributes: masking was wired only into log ingestion, so trace spans were stored with their attributes verbatim. Spans routinely carry
http.request_body/http.response_body(plaintext credentials, JWTs),net.peer.ipand user agents, all persisted unmasked.tracesService.ingestSpansnow runs the same org/project masking rules over each span'sattributes,resourceAttributesand event/link attributes before storage, and drops (fail-closed) any span whose masking throws. Because request/response bodies are opaque stringified JSON that field-name rules can't see into, those body attributes are deep-masked (parse the JSON, maskpassword/token/email inside, re-serialize) with full redaction as a fallback when the value is not parseable JSON. Metric attributes are not yet masked (tracked separately)
Added
- Per-occurrence trace links on the error detail page: each log in an error group's Logs tab now shows a "View Trace" action when that log carries a trace context, opening the existing trace timeline. The error-group logs endpoint (
GET /api/v1/error-groups/:id/logs) now surfaces thetraceIdit already loaded from storage and previously discarded; no schema change, no migration - Copy buttons on metadata blocks: the log search expanded detail and the Log Context dialog now have a one-click copy on each metadata block (with copied feedback), so a log's metadata JSON can be grabbed without selecting it by hand
- Breadcrumbs timeline in the log search detail: when a log carries
metadata.breadcrumbs, the expanded row now renders a collapsible "Breadcrumbs (N)" timeline (the sameBreadcrumbTimelineview already used in the Log Context dialog) instead of leaving them buried in the raw metadata JSON - Nested metadata columns: custom metadata columns in log search now accept dot-notation paths (e.g.
sdk.name) to read into nested objects. Exact top-level keys still win first, so flat keys that contain dots (e.g.debug.trace_id) keep resolving; object/array values render as compact JSON, and the full value is available on hover
Fixed
- Admin usage page returned 403 for organizations the admin wasn't a member of: the metering endpoints (
/usage,/usage/breakdown,/usage/storage,/usage/capabilities) gated solely on org membership, so the platform Admin > Usage page (which lists every organization) got "Forbidden" whenever a selected org wasn't one the admin personally belonged to. Platform admins (is_admin) now bypass the membership check on these read endpoints; the queries stay filtered by the requestedorganizationId, so tenant scoping is unchanged - Trace volume / trace latency dashboard panels were empty on ClickHouse and MongoDB: both panel fetchers read span data straight from the Postgres
spanshypertable and its continuous aggregates and short-circuited to an empty series whenreservoir.getEngineType() !== 'timescale', so on ClickHouse/MongoDB deployments (where spans live in those engines) the panels returned no data. Added a multi-enginereservoir.getSpanTimeseries(time-bucketed span volume + true window p50/p95/p99 from raw spans:percentile_conton TimescaleDB,quantileon ClickHouse,$percentileon MongoDB) and switched both fetchers to it. The ClickHouse and MongoDB paths mirror the validatedgetServiceHealthStatspercentile approa...
v1.0.2
A frontend correctness and security release from a comprehensive multi-agent frontend bug hunt (UI, logic, reactivity, leaks and security), plus a hardening of how the browser authenticates the live-streaming endpoints. The headline item is single-use stream tickets: the session token no longer travels in WebSocket/SSE URLs (where reverse proxies log it). One additive database migration (049_stream_tickets); otherwise a drop-in upgrade.
Security
- Session token no longer placed in WebSocket/SSE URLs: browser
WebSocketandEventSourcecannot send anAuthorizationheader, so the log live-tail (/api/v1/logs/ws), the SIEM events stream (/api/v1/siem/events) and the trace live-tail (/api/v1/traces/stream) previously carried the long-lived session token in the URL query string, where it is logged by proxies and servers. The client now mints a short-lived, single-use stream ticket via an authenticatedPOST /api/v1/stream-ticketsand passes that ticket instead. Tickets live in the relational database (not Redis, so the mechanism is portable across the BullMQ and graphile queue backends), expire in 30s and are consumed on first use. The legacy?token=path still works for backward compatibility - Webhook channel secrets no longer rehydrated into the DOM: editing a notification channel no longer pre-fills the bearer token / basic-auth password inputs with the stored secret; the fields stay empty with a "leave blank to keep current" hint and are only sent when the user types a new value
- OIDC callback strips the session token from the URL after reading it, so it no longer lingers in browser history, the referrer or logs
- Admin pages enforce a client-side admin guard: several admin views (user detail, usage, organization detail) loaded and could mutate data on mount without checking the admin role; they now redirect non-admins, and the admin section layout has a guard of its own
- Removed a debug
console.logthat leaked log message content and api-key metadata to the browser console on the error-detail page
Added
- Global 401 handler: a single fetch interceptor installed at app startup clears local auth state and redirects to the login page (preserving the current path so the user lands back there after signing in) on any authenticated
/api/v1401 that is not an auth endpoint. Previously a revoked or expired session was only detected on a full dashboard remount, so a logged-out user could keep clicking around getting silent failures POST /api/v1/stream-ticketsendpoint andstream_ticketstable (migration 049) backing the stream-ticket auth described above
Fixed
- Stale-response races: overlapping loads triggered by fast filter/pagination changes could let an older in-flight response overwrite fresher results. Added local request-sequence guards on the log search, traces list, error groups, SIEM incidents, monitor detail/list, custom-dashboard panels and alert-preview views
- API client error handling: error-branch
response.json()calls are guarded so a non-JSON error body (reverse-proxy 502/HTML, empty204) no longer throws aSyntaxErrorthat masks the real HTTP failure (auth, admin and exceptions clients) - Locale-stable formatting: user-facing dates and numbers now use explicit
en-USformatting across the status pages, members, project settings, traces, metrics, search and notification-channel views; alert-history timestamps no longer label UTC values as if they were local time - Lifecycle and memory leaks: component store subscriptions are auto-managed, a first-run shortcut-hint
setTimeoutis cleared on unmount, and chart instances are disposed, so navigating away no longer leaves timers, listeners or subscriptions behind - Svelte 5 reactivity and assorted UI fixes: the trace detail page reloads when navigating between traces; the api-key DSN preserves an
http://scheme for non-TLS deployments; the "View Error Group" action navigates with a param the target page actually reads; SigmaSync no longer crashes when a commit hash is absent; the toaster follows the app theme; the delete-organization confirm is disabled while in flight; PII masking rules require a regex or field names; numeric monitor inputs guard againstNaN; and the audit-log resource cell no longer renders a literal escape sequence - ClickHouse: materialized-view backfills now run once instead of on every startup
Notes
- Left intentionally unchanged: storing the session token in
localStorage(a disputed, low-severity finding). Moving it to an httpOnly cookie would trade XSS token-theft for CSRF surface and a full auth-model overhaul without a clear net win; the high-leverage XSS defenses (CSP, output sanitization, auditing the few{@html}sites) are tracked separately
v1.0.1
A security and correctness release from a comprehensive, multi-engine bug audit of the 1.0 line. The headline items are two cross-tenant data-exposure fixes that were live in 1.0.0, alongside a broad sweep of detection, ingestion, storage, alerting and frontend correctness fixes. No database migrations; this is a drop-in upgrade. The storage-layer fixes were validated against real ClickHouse and MongoDB (and TimescaleDB), and CI now runs the MongoDB reservoir integration suite.
Security
- Cross-tenant log exposure via the WebSocket live-tail (
GET /api/v1/logs/ws): the handler validated only the session token and then streamed whateverprojectIdthe client passed, never checking project/organization membership, so any authenticated user could live-tail any project's logs by supplying its id. It now enforces the sameverifyProjectAccessmembership check as the REST query routes (regression test added). Was live on the 1.0.0 line - Cross-tenant leak of notification-channel secrets:
GET /notification-channels/alert-rules/:idand/sigma-rules/:idreturned the channelconfigverbatim (including webhookauthtokens/passwords) with no membership check. They now resolve the rule's organization, require membership, and scope the read to that org - PII masking fail-open:
maskTextskipped all content rules for pure-alphanumeric strings, so a separator-less credit-card number (e.g.4111111111111111) was stored unmasked. The all-alphanumeric early-exit is removed; only too-short strings are skipped - OIDC account-takeover: linking an external identity to an existing local account by email now requires the provider to assert the email is verified (
email_verified === true), preventing takeover via an unverified IdP email; LDAP is treated as authoritative - Cross-tenant
?projectId/ association gaps closed: the correlation batch-identifiers endpoint now intersects the requested project with the caller's accessible projects (and requires read access); status-incident and scheduled-maintenance creation, organization-default channels, and alert/sigma/monitor channel associations now validate that the project/channels belong to the caller's organization - Session invalidation: disabling a user, resetting their password, and changing a user's admin role now invalidate the cached session (previously the cached profile stayed valid for up to the cache TTL); the frontend logout now revokes the server-side session token instead of only clearing local state
- Hardening: emails are stored and compared case-insensitively across registration/login/profile-update; the global
CACHE_TTLoverride no longer clamps semantic TTLs (sessions, OIDC state, settings); the webhook envelope id is validated as a UUID; SigmaHQ category sync matches on a directory boundary; OTLP trace/span ids of invalid length are rejected
Security
- Dependency security updates, third wave (Dependabot): two further advisories resolved to their patched releases. The direct dependency
nodemaileris bumped from^8.0.9to^9.0.1(GHSA-p6gq-j5cr-w38f, HIGH): the message-levelrawoption bypasseddisableFileAccess/disableUrlAccess, enabling arbitrary file read and full-response SSRF in the delivered message; our SMTP senders only use standardcreateTransport/sendMailfields and never passraw, but the dependency is patched regardless. The transitiveundici(pulled in only byjsdomin the frontend test toolchain) is pinned via the root pnpmoverridesto>=7.28.0 <8(GHSA-vmh5-mc38-953g HIGH, TLS certificate validation bypass via droppedrequestTlsin the SOCKS5ProxyAgent; GHSA-pr7r-676h-xcf6 MEDIUM, cross-user information disclosure via shared-cache whitespace bypass), resolving to7.28.0and staying on the 7.x linejsdom@29expects. No vulnerable version remains inpnpm-lock.yaml
Fixed
- ClickHouse "Query with id = ... is already running" under concurrent queries (#213 regression): the request-context propagation derived the ClickHouse
query_iddeterministically fromrequestId + operation, so two same-operation queries running concurrently within one request reused the same id and ClickHouse rejected the second. Thequery_idnow keeps the request id + operation as a readable prefix (correlation is also carried in the SQLlog_comment) and appends a random suffix so every query is unique. ClickHouse-only - Sigma condition operator precedence:
parseExpressionfolded AND/OR strictly left-to-right, soa or b and cevaluated as(a or b) and c. AND now binds tighter than OR, matching the Sigma spec - Infinite loop on ingestion: a custom identifier pattern that can match the empty string (e.g.
\d*) no longer hangs the worker (zero-width-match guard plus a forced global flag) - Trace summary races:
upsertTraceis now race-free on TimescaleDB (single atomicINSERT ... ON CONFLICT) and ClickHouse (the summary is recomputed from the spans table instead of read-modify-write), so concurrent span batches no longer lose span counts - ClickHouse: added the missing
session_idcolumn (was written and filtered on but did not exist), parse zone-less datetimes as UTC, report the true latest value in the metrics overview (was the average) with NaN guards - MongoDB:
deleteMetricsByTimeRangeonly deletes the matched metrics' exemplars (was all exemplars in the window); the metrics overview sorts before$last; metadatadistinctno longer drops numeric/boolean values;ingestReturningreturns the rows that did land on a partial bulk-write failure - Rate-of-change alerts: the baseline now applies the rule's metadata filters (previously only the current rate did, deflating the deviation ratio), and a
minBaselineValueof 0 is honored instead of defaulting to 10 - Dashboards: personal dashboards are scoped to their creator (no longer readable/editable by any org member);
metric_statsums across buckets for sum/count aggregations; top-error percentages are computed against the true total; the capability usage page renders a zero limit as fully restricted, not unlimited - Pagination/counts: admin organization/project search counts apply the search filter; correlation lookups report the true total; the ClickHouse trace error-rate uses a consistent denominator;
getTopServiceshonors thetobound - Live tail: SSE polling bypasses the 60s query cache and guards against overlapping polls; the live-tail WebSocket URL works behind a reverse proxy; the traces and search live tails no longer corrupt pagination or index-keyed row state
- Infra/correctness: BullMQ and graphile queue retry-attempt defaults are aligned; webhook deliveries can only be replayed from the terminal
deadstate (no double delivery); the audit flush buffer is bounded on persistent DB failure; alert-notification jobs are deduplicated by history id; admin org-role changes are restricted consistently with member removal; tenant-table updates in the log-pipeline and service-dependency queries are project-scoped
Changed
- The reservoir storage abstraction gains
getTraceServices(distinct trace services with no result cap) implemented on all three engines and used by the traces service, replacing the previous 10k-trace paging - CI now provisions a MongoDB service for the reservoir integration suite, so all three storage engines are exercised on every run
Notes
- Deferred to a follow-up (#255): Sigma compound field modifiers (
field|base64|containschains) and a true service-map p95 (needs a mergeable t-digest sketch in the continuous aggregate)
Full Changelog: v1.0.0...v1.0.1
v1.0.0
First stable release of the 1.0 line. The headline work since 0.9.7 is the tenant data isolation audit that hardens every backend data-access path, and the metering + capability system that gives every organization usage measurement, feature gates and enforceable limits/quotas (ingestion, spans, storage) without changing OSS behavior. A typed lifecycle hooks surface and a reusable outbound webhook delivery system (HMAC signing, retry/backoff, DLQ, centralized SSRF) land alongside.
See CHANGELOG.md for the full, detailed record.
Security
- Tenant data isolation audit (#219, #228): closed authenticated cross-tenant log and trace/span reads via unvalidated
?projectId, swept application-layer scoping gaps on tenant-table queries, and added an isolation test suite plus CI tripwires (check:tenant-scoping). Reservoir log query params now requireprojectIdwith an explicitGLOBAL_SCOPEsentinel. - PII masking is fail-closed at ingestion: records whose masking fails are rejected before storage and reported in the ingest response
rejected[]; no unmasked data can reach any storage engine. - Two waves of dependency advisory fixes (12 advisories): vitest, esbuild, shell-quote, nodemailer, vite, js-yaml, protobufjs, form-data and @opentelemetry/core upgraded to patched releases; no vulnerable version remains in the lockfile.
Added
- Capability system (#214): per-organization feature gates, static limits and metered usage quotas, enforced across alerts, sigma rules, notification channels, API keys, dashboards and ingestion/storage/span quotas. OSS defaults stay unlimited.
- Resource usage metering (#212): storage-agnostic per-org/project consumption tracking with a Usage dashboard, plus span and storage-snapshot recording sites and capability-usage vs plan-limit progress bars.
- Lifecycle hooks (#216): typed before-/after- extension points for ingestion, query, alert evaluation and webhook dispatch; no-op in OSS, configurable via
HOOKS_MODULES. - Generic outbound webhook delivery (#218): HMAC-SHA256 signing, exponential-backoff retry, a dead-letter queue, per-org concurrency limiting and centralized SSRF protection, with every sender migrated onto it and a unified event envelope.
- Audit log primitive (#217): typed actions/actors/outcomes, per-org retention, and audit coverage for API-key access and failed logins.
- Request context propagation (#213): AsyncLocalStorage-backed context across HTTP, jobs and the DB layer.
Changed
- BREAKING: unified webhook event envelope (#218): every outbound delivery serializes to one
{ id, type, version, occurredAt, organizationId, projectId, data }envelope with anX-Logtide-Event-Version: 1header. - OTLP log metadata shape: resource attributes now land under
metadata.resource; structured bodies preserved undermetadata['otel.body'].
Fixed
- Sigma search by MITRE technique/tactic/tag (text[] vs jsonb), a migration prefix collision that could break production migrate, and assorted silent-failure and status-code issues across the admin, monitoring and sigma surfaces.
Notes
- Scheduled email digest reports (#154) are merged as groundwork but disabled in this release pending completion.
Full Changelog: v0.9.7...v1.0.0
v1.0.0-rc1
What's Changed
- Generic outbound webhook delivery infrastructure (#218) by @Polliog in #242
- ingestion safety: fail-closed pii masking + health visibility by @Polliog in #243
- capability enforcement: sigma, channels, api keys, dashboards by @Polliog in #244
- unified webhook event envelope + after lifecycle hooks by @Polliog in #245
- traces/otlp gap closure: metadata shape, frontend tests, e2e by @Polliog in #246
- test debt: drop all coverage exclusions, shared schemas, ci wiring by @Polliog in #247
- audit primitive: typed actions, actor types, outcomes, per-org retention by @Polliog in #248
Full Changelog: v1.0.0b-beta2...v1.0.0-rc1
v1.0.0b-beta2
What's Changed
- Capability system: feature gates, limits and usage quotas (#214) by @Polliog in #238
- Span and storage metering recording sites (#212 follow-up) by @Polliog in #239
- lifecycle hooks at ingestion, query, alert evaluation and webhook dispatch (#216) by @Polliog in #241
Full Changelog: v1.0.0b-beta1...v1.0.0b-beta2
v1.0.0b-beta1
What's Changed
- scheduled email digest by @DEVAANSH001 in #209
- Request context propagation across HTTP, jobs, and DB layer (#213) by @Polliog in #222
- Tenant data isolation audit (#219) by @Polliog in #228
- tenant isolation follow-ups: migration prefix collision + 404 semantics by @Polliog in #229
- resource usage metering (#212) by @Polliog in #237
New Contributors
- @DEVAANSH001 made their first contribution in #209
Full Changelog: v0.9.7...v1.0.0b-beta1
v0.9.7
Security
- SSRF in alert/Sigma webhook delivery via the legacy delivery path (authenticated) (GHSA-7v53-pw6r-99vj, CWE-918): the 0.9.6 SSRF hardening added the centralized
utils/ssrf-guard.tsguard and wired it into the HTTP/TCP monitors and theWebhookProvider, but the actual alert/Sigma webhook delivery path was left on the old inline filter.sendWebhookNotificationinqueue/jobs/alert-notification.ts(reached by thealert-notificationsBullMQ worker for threshold, rate-of-change and Sigma-rule alerts) still ran a barefetch(webhook_url, …)guarded only by a string-basedisPrivateIP(). That check was bypassable: any non-dotted-quad hostname returnedfalse(no DNS resolution), so a domain whose A record points at127.0.0.1/169.254.169.254/ an internal host passed the filter and the resolved internal address was then connected; the barefetchused the defaultredirect: 'follow', so a public host that302s to an internal URL was followed straight there; and CGNAT (100.64.0.0/10), IPv6, IPv4-mapped IPv6,0.0.0.0/8and198.18.0.0/15were not covered. An authenticated org owner/admin who can create a webhook notification channel could therefore use the backend as a blind SSRF probe against internal services and cloud metadata, with a partial read-back oracle since a non-2xx internal response body was spliced into the alert-history error message. The guarded "Test" button already blocked the same URLs, confirming this as an incomplete-fix sibling-gap. Fix:sendWebhookNotificationnow routes delivery throughsafeFetch(url, init, { allowPrivate: config.MONITOR_ALLOW_PRIVATE_TARGETS }), exactly asWebhookProviderdoes (DNS resolution + per-redirect-hop revalidation + full IPv4/IPv6 private/reserved range coverage), mappingSsrfBlockedErrorto the existing "private/internal addresses are not allowed" error. The inlineisPrivateIP/BLOCKED_HOSTSfilter and the barefetchare removed, and blocked targets are now rejected before the response body is read (closing the read-back oracle). TheMONITOR_ALLOW_PRIVATE_TARGETSopt-in still lets self-hosted deployments target internal endpoints. Reported by tonghuaroot
Full Changelog: v0.9.6...v0.9.7
v0.9.6
Changed
- Frontend now runs as a full SPA on
adapter-nodeinstead of doing SSR with client hydration. A singleexport const ssr = falsein the new root+layout.tscascades to every route, so the server only ships the empty app shell and the client renders from scratch. Eliminates the entire class of hydration mismatch bugs that had been accumulating across login, register, invite, public status page, the/auth/callbackpage and the various dashboard subpages, each previously patched with its own per-routessr = false. Server load functions and the Node runtime are untouched (the adapter, API proxying, env vars, BullMQ etc. keep working exactly as before); there are no+page.server.ts/+layout.server.tsfiles in the repo today so nothing had to change semantically on the server side. UX tradeoff: the public status page at/status/[orgSlug]/[projectSlug]now flashes the empty shell for one paint before the JS hydrates, which is acceptable for an authenticated-by-default product but should be revisited if SEO on the status page becomes a goal (a single-line overrideexport const ssr = truein that page's+page.tsputs it back on SSR without affecting anything else) - Removed 22 redundant route-level
ssr = falsedeclarations (dashboard/+layout.ts,dashboard/admin/+layout.tsand 20+page.tsfiles across landing, login, register, onboarding, invite, status, auth callback and the dashboard search / metrics / alerts / monitoring / projects / sessions / settings subtrees) that had been added one by one as each page hit a hydration bug. The new root-level cascade makes them all dead config; deleting them removes the temptation to copy the pattern into new routes
Fixed
- Infinite skeleton spinner on
/dashboard/search,/dashboard/tracesand/dashboard/metricswhen no project in the org had its data-availability flag set, typically right after a user deleted the only projects that had been ingesting. The filter logic atsearch/+page.svelte:441-444(and the identical pattern attraces/+page.svelte:142-145andmetrics/+page.svelte:90-93) readconst logsProjectIds = availability?.logsand then branched onlogsProjectIds ? filter : fallback. WhengetProjectDataAvailabilitylegitimately returned{ logs: [] }the empty array took the truthy branch ([]is truthy in JS) and[].includes(p.id)excluded every project, so the displayed project list was empty,loadLogs()never fired, andhasLoadedOncestayedfalseso theSkeletonTablerendered forever. Fix: guard the truthy branch withlogsProjectIds && logsProjectIds.length > 0so an empty availability response falls back to "show all projects" exactly like the API-failure path (.catch(() => null)) already does. The 0.9.4 backend optimization that introduced the cached availability flags is unaffected; this is purely a frontend null-vs-empty conflation. Logs of already-hard-deleted projects on the TimescaleDB engine remain unrecoverable due toON DELETE CASCADEonlogs.project_id(tracked separately under the soft-delete projects epic) - OIDC login failed against issuer-identifying providers (e.g. Authelia) because the
isscallback parameter was dropped (#233, #234): the OIDC callback handler extracted onlycodeandstatefrom the provider redirect and rebuilt the callback URL from just those two, discarding everything else. Providers implementing RFC 9207 (OAuth 2.0 Authorization Server Issuer Identification) appendissto the redirect andopenid-client'sauthorizationCodeGrant()validates it, so the token exchange failed with "issuer parameter missing" and login never completed. The Fastify callback route now forwards the fullrequest.querythroughhandleOidcCallbackinto the provider, which replays every parameter onto the callback URL handed to the token exchange (soiss,session_state, etc. survive); the requiredcode/stateare always re-asserted from the validated values. Duplicated/array-valued query params are collapsed to a single value withsearchParams.setinstead of being appended, since OIDC authorization-response params are single-valued per RFC 6749 / RFC 9207, avoiding a malformed URL with duplicateiss/codereachingauthorizationCodeGrant. Covered by new tests across the route, service and provider layers, including the array-collapse and undefined-param branches
Security
- Cross-tenant project access via unvalidated
projectId(authenticated): several routes accepted both anorganizationIdand aprojectId, verified the caller was a member of the organization, but never verified that the supplied project actually belonged to that organization. Since project UUIDs are normal identifiers that appear in dashboard URLs and client API calls, a user could pair their ownorganizationId(membership check passes) with a victim's knownprojectIdand reach another tenant's data. Confirmed on four handlers:POST /api/v1/alerts/previewreturnedsampleLogs(time, service, level, message, trace ID) from the foreign project;POST /api/v1/alertsandPOST /api/v1/monitorslet a rule/monitor be scoped to a foreign project (the monitor case also surfaces on the victim's public status page, which renders byproject_id); andGET/DELETE /api/v1/sourcemapslet a member list or delete another tenant's source maps. Fix: a sharedprojectsService.projectBelongsToOrg(projectId, organizationId)helper (singleprojectslookup filtered on bothidandorganization_id) is now enforced right after the existing membership check on each of those routes, returning403when the project is foreign. The alert/monitor update paths don't accept aprojectIdand the custom-dashboards panel pipeline already had an equivalentensureProjectInOrgguard at its choke point, so no change was needed there. Regression tests cover each handler - Server-side request forgery (SSRF) and internal port scanning via monitors and webhooks (authenticated): HTTP/TCP uptime monitors and webhook delivery executed user-supplied targets from the backend's network with no meaningful destination validation. Monitor creation only checked that an HTTP target started with
http(s)://and that a TCP target contained:;checker.tsthen calledfetch(target, { redirect: 'follow' })andcreateConnection({ host, port }), so a registered user could point a monitor athttp://169.254.169.254/…,http://127.0.0.1,10.0.0.0/8, etc. and use the sanitized up/down result and timing to probe internal services. The webhook provider had only literal-string private-IP filtering (no DNS resolution, incomplete IPv6, followed redirects), leaving DNS- and redirect-based bypasses open. Fix: a centralized outbound guard (utils/ssrf-guard.ts) resolves hostnames and rejects loopback, private, link-local (incl.169.254.169.254cloud metadata), CGNAT (100.64.0.0/10), multicast and reserved IPv4/IPv6 ranges (including IPv4-mapped IPv6 and ULA/fc00::/7, link-localfe80::/10). TCP checks resolve-then-pin the socket to the validated address (closing DNS-rebinding between validation and connect); HTTP checks and webhook delivery follow redirects manually and revalidate every hop instead ofredirect: 'follow'. The guard runs both at monitor create/update time (immediate400feedback) and at execution time (authoritative, returns ablockedresult). Private/internal targets are denied by default; self-hosted deployments that legitimately monitor internal services can opt back in withMONITOR_ALLOW_PRIVATE_TARGETS=true, which also governs webhook delivery. Note: HTTPS does not yet pin the connected address against a custom dispatcher, so a narrow DNS-rebinding window remains for HTTP monitors (tracked for a follow-up); the reported direct-target and redirect-to-internal vectors are closed
New Contributors
- @outcast292 made their first contribution in #233
Full Changelog: v0.9.5...v0.9.6