Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
df22134
fix sigma compound modifier chains and |all quantifier
Polliog Jun 22, 2026
55b8266
add reservoir getServiceHealthStats with true window p95
Polliog Jun 22, 2026
02a873d
service map p95 sourced from raw spans via reservoir
Polliog Jun 22, 2026
f984ed8
changelog for sigma modifier chains and true p95
Polliog Jun 22, 2026
c6e19c8
Merge pull request #261 from logtide-dev/fix/sigma-modifiers-true-p95…
Polliog Jun 22, 2026
3704c7c
restyle trace and session id in log detail
Polliog Jun 25, 2026
aeb85a2
link error group logs to their trace
Polliog Jun 25, 2026
1b57a91
changelog for trace links and log detail restyle
Polliog Jun 25, 2026
8444c09
update esbuild version to 0.25.12 in package.json and pnpm-lock.yaml
Polliog Jun 25, 2026
ec4dcf1
fix log context dialog overflow and add metadata copy
Polliog Jun 25, 2026
5173d0e
search: metadata copy, breadcrumbs view, nested columns
Polliog Jun 25, 2026
63b54af
changelog for log detail copy, breadcrumbs and nested columns
Polliog Jun 25, 2026
b1cf325
fix redis leak: dont clobber bullmq cleanup defaults
Polliog Jun 25, 2026
6a038fd
sigma cron: only update existing rules, no auto alerts
Polliog Jun 25, 2026
0112cae
changelog for redis leak and sigma sync fixes
Polliog Jun 25, 2026
51017ad
fix error detail trend bars not rendering
Polliog Jun 25, 2026
2eac0c5
pin user-facing dates and numbers to en-US locale
Polliog Jun 25, 2026
80fd256
changelog for trend bars and en-US locale sweep
Polliog Jun 25, 2026
14aff44
mask pii in trace span attributes (fail-closed)
Polliog Jun 25, 2026
6cd3c1b
changelog for span pii masking
Polliog Jun 25, 2026
d5b9341
project overview: activity overview instead of logs timeline
Polliog Jun 25, 2026
f8a346a
multi-engine span timeseries for trace volume/latency panels
Polliog Jun 25, 2026
0d129f7
changelog for activity overview and span timeseries panels
Polliog Jun 25, 2026
0daf4dc
let platform admins read any org usage
Polliog Jun 25, 2026
445f13f
changelog for admin usage access fix
Polliog Jun 25, 2026
08225b7
enforce api-key org/project binding on dashboard endpoints
Polliog Jun 26, 2026
234b3e0
escape user-derived service names in service-map tooltip
Polliog Jun 26, 2026
fdef78c
changelog for dashboard authz and service-map xss fixes
Polliog Jun 26, 2026
af97a58
Merge pull request #262 from logtide-dev/fix/dashboard-authz-and-serv…
Polliog Jun 26, 2026
ecdf292
fix e2e trace link selector after log detail restyle
Polliog Jun 26, 2026
f6a8cdc
merge fix/e2e-trace-link into develop
Polliog Jun 26, 2026
34de2f2
validate redirect target in auth-free login/register
Polliog Jun 26, 2026
17d3e5e
sanitize otlp service.name at ingestion
Polliog Jun 26, 2026
ad9d64a
pin validated ip in safeFetch to block dns rebinding
Polliog Jun 26, 2026
2e9c17e
serialize first-admin promotion to close bootstrap race
Polliog Jun 26, 2026
43360ec
serialize capability count+create to prevent limit races
Polliog Jun 26, 2026
402a1c8
changelog for security hardening batch
Polliog Jun 26, 2026
d92d679
bound capability lock waiters with in-process mutex
Polliog Jun 26, 2026
56db418
credit kiberblick.de in security acknowledgments
Polliog Jun 26, 2026
24c6433
Merge pull request #263 from logtide-dev/fix/security-hardening-leads
Polliog Jun 26, 2026
acd8ef8
bump version to 1.0.3
Polliog Jun 26, 2026
6dbf347
cut 1.0.3 release in changelog
Polliog Jun 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@
<a href="https://codecov.io/gh/logtide-dev/logtide"><img src="https://codecov.io/gh/logtide-dev/logtide/branch/main/graph/badge.svg" alt="Coverage"></a>
<a href="https://hub.docker.com/r/logtide/backend"><img src="https://img.shields.io/docker/v/logtide/backend?label=docker&logo=docker" alt="Docker"></a>
<a href="https://artifacthub.io/packages/helm/logtide/logtide"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/logtide" alt="Artifact Hub"></a>
<img src="https://img.shields.io/badge/version-1.0.2-blue.svg" alt="Version">
<img src="https://img.shields.io/badge/version-1.0.3-blue.svg" alt="Version">
<img src="https://img.shields.io/badge/license-AGPLv3-blue.svg" alt="License">
<img src="https://img.shields.io/badge/status-beta-success.svg" alt="Status">
</div>

<br />

> **🌊 LogTide 1.0.2 (public beta):** unified **Logs, Traces & Metrics** with a built-in **SIEM**, multi-engine storage (TimescaleDB / ClickHouse / MongoDB), uptime monitoring, parsing pipelines, and custom dashboards.
> **🌊 LogTide 1.0.3 (public beta):** unified **Logs, Traces & Metrics** with a built-in **SIEM**, multi-engine storage (TimescaleDB / ClickHouse / MongoDB), uptime monitoring, parsing pipelines, and custom dashboards.

---

Expand Down Expand Up @@ -124,7 +124,7 @@ We host it for you. Perfect for testing. [**Sign up at logtide.dev**](https://lo

---

## ✨ Core Features (v1.0.2)
## ✨ Core Features (v1.0.3)

### Monitoring, Pipelines & Dashboards
* 🩺 **Uptime Monitoring & Status Pages:** HTTP/TCP/heartbeat monitors with configurable thresholds, auto-created SIEM incidents on failure, scheduled maintenances, and public Uptime-Kuma-style status pages per project.
Expand Down
1 change: 1 addition & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ We thank the following researchers for responsibly disclosing security issues:

- **Bertie** — cross-tenant authorization gaps in project-scoped routes (alert preview/creation, monitor creation, source map list/delete) and SSRF / internal port-scanning via HTTP/TCP monitors and webhook delivery. Fixed in 0.9.6.
- **tonghuaroot** — SSRF in the alert/Sigma webhook delivery path, which still used the bypassable inline filter instead of the centralized `safeFetch` guard (incomplete-fix sibling-gap of the 0.9.6 hardening). Fixed in 0.9.7. (GHSA-7v53-pw6r-99vj)
- **KIberblick.de** ([kiberblick.de](https://kiberblick.de)) — cross-tenant read on the dashboard API endpoints (organization taken from the attacker-supplied query string under API-key auth) and stored XSS via OTLP `service.name` in the service map; plus 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. Fixed in 1.0.3.

## Supported Versions

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"name": "logtide",
"version": "1.0.2",
"version": "1.0.3",
"private": true,
"description": "LogTide - Self-hosted log management platform",
"author": "LogTide Team",
"license": "AGPL-3.0",
"pnpm": {
"overrides": {
"esbuild": ">=0.28.1",
"esbuild": ">=0.25.0 <0.26.0",
"shell-quote": ">=1.8.4",
"form-data": ">=4.0.6",
"vite": ">=6.4.3",
Expand Down
3 changes: 2 additions & 1 deletion packages/backend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@logtide/backend",
"version": "1.0.2",
"version": "1.0.3",
"private": true,
"description": "LogTide Backend API",
"type": "module",
Expand Down Expand Up @@ -70,6 +70,7 @@
"safe-regex2": "^5.0.0",
"source-map": "^0.7.6",
"tsx": "^4.21.0",
"undici": "^7.28.0",
"zod": "^3.25.76"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/capabilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ export {

export { quotaFlagCache } from './quota-cache.js';
export { QuotaEvaluator } from './quota-evaluator.js';
export { withLimitLock } from './limit-lock.js';
73 changes: 73 additions & 0 deletions packages/backend/src/capabilities/limit-lock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { sql } from 'kysely';
import { db } from '../database/connection.js';

// Namespace for the (int4, int4) Postgres advisory lock keyspace used by
// capability limit enforcement, kept distinct from other advisory locks.
const CAP_LOCK_NAMESPACE = 0x4c54; // 'LT'

/** Deterministic 32-bit signed hash (FNV-1a) for advisory lock keys. */
function hash32(input: string): number {
let h = 2166136261;
for (let i = 0; i < input.length; i++) {
h ^= input.charCodeAt(i);
h = Math.imul(h, 16777619);
}
return h | 0; // coerce to signed int32 for pg_advisory_xact_lock(int4, int4)
}

// In-process serialization tails, keyed by org+capability. This bounds the
// number of callers that block on the DB advisory lock to ONE per key per
// process, so concurrent requests don't each hold a transaction connection
// while waiting (which would exhaust the pool).
const localTails = new Map<string, Promise<void>>();

async function runExclusiveInProcess<T>(key: string, fn: () => Promise<T>): Promise<T> {
const prevTail = localTails.get(key) ?? Promise.resolve();
let release!: () => void;
const tail = new Promise<void>((resolve) => {
release = resolve;
});
localTails.set(key, tail);
await prevTail; // wait for the previous holder of this key
try {
return await fn();
} finally {
release();
// Drop the entry once we are the last in line, to keep the map bounded.
if (localTails.get(key) === tail) localTails.delete(key);
}
}

/**
* Serialize a "count current usage -> assert under limit -> create" sequence
* against concurrent callers for the same (organization, capability).
*
* The enforcement pattern (COUNT -> assertWithinLimit -> insert) is a
* check-then-act: without serialization, parallel requests can each read a count
* below the limit and then all insert, pushing usage past a finite cap.
*
* Serialization happens at two levels: an in-process mutex per org+capability
* (so within one backend instance only one such create runs at a time, and
* waiters do not hold a database connection while queued), wrapping a
* transaction-scoped Postgres advisory lock on the same key (so the guarantee
* also holds across multiple backend instances sharing the database). The
* advisory lock is released automatically on commit/rollback.
*
* Different organizations and different capabilities use distinct keys and do
* not contend. When no finite limit is configured (the OSS default) the only
* added cost is one advisory lock/unlock round-trip.
*/
export async function withLimitLock<T>(
organizationId: string,
capabilityKey: string,
fn: () => Promise<T>,
): Promise<T> {
const key = `${organizationId}:${capabilityKey}`;
const key2 = hash32(key);
return runExclusiveInProcess(key, () =>
db.transaction().execute(async (trx) => {
await sql`SELECT pg_advisory_xact_lock(${CAP_LOCK_NAMESPACE}, ${key2})`.execute(trx);
return fn();
}),
);
}
41 changes: 21 additions & 20 deletions packages/backend/src/modules/alerts/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { OrganizationsService } from '../organizations/service.js';
import { projectsService } from '../projects/service.js';
import { notificationChannelsService } from '../notification-channels/index.js';
import { auditLogService } from '../audit-log/index.js';
import { assertWithinLimit } from '../../capabilities/index.js';
import { assertWithinLimit, withLimitLock } from '../../capabilities/index.js';

const organizationsService = new OrganizationsService();

Expand Down Expand Up @@ -184,26 +184,27 @@ export async function alertsRoutes(fastify: FastifyInstance) {
// Session-auth requests don't populate request.organizationId in the ALS
// context, so we establish a scoped system context with the org from the
// validated body. Same pattern as otlp/trace-routes.ts.
// Note: count -> insert is not atomic; a concurrent create can briefly
// exceed the cap by one. Acceptable for user-initiated rule creation.
await context.runAsSystem('alerts:create-limit-check', async () => {
await context.with({ organizationId: body.organizationId }, async () => {
const currentRuleCount = await alertsService.countAlertRules(body.organizationId);
await assertWithinLimit('alerts.max_rules', currentRuleCount);
});
});

// The count -> insert is serialized per org via withLimitLock so concurrent
// creates can't race past the cap.
const { channelIds, alertType, baselineType, deviationMultiplier, minBaselineValue, cooldownMinutes, sustainedMinutes, metadataFilters, ...alertData } = body;
const alertRule = await alertsService.createAlertRule({
...alertData,
alertType: alertType || 'threshold',
baselineType: baselineType || null,
deviationMultiplier: deviationMultiplier ?? null,
minBaselineValue: minBaselineValue ?? null,
cooldownMinutes: cooldownMinutes ?? null,
sustainedMinutes: sustainedMinutes ?? null,
emailRecipients: alertData.emailRecipients || [],
metadataFilters: metadataFilters ?? [],
const alertRule = await withLimitLock(body.organizationId, 'alerts.max_rules', async () => {
await context.runAsSystem('alerts:create-limit-check', async () => {
await context.with({ organizationId: body.organizationId }, async () => {
const currentRuleCount = await alertsService.countAlertRules(body.organizationId);
await assertWithinLimit('alerts.max_rules', currentRuleCount);
});
});
return alertsService.createAlertRule({
...alertData,
alertType: alertType || 'threshold',
baselineType: baselineType || null,
deviationMultiplier: deviationMultiplier ?? null,
minBaselineValue: minBaselineValue ?? null,
cooldownMinutes: cooldownMinutes ?? null,
sustainedMinutes: sustainedMinutes ?? null,
emailRecipients: alertData.emailRecipients || [],
metadataFilters: metadataFilters ?? [],
});
});

// Associate channels with the alert rule
Expand Down
24 changes: 13 additions & 11 deletions packages/backend/src/modules/api-keys/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { apiKeysService } from './service.js';
import { authenticate } from '../auth/middleware.js';
import { projectsService } from '../projects/service.js';
import { auditLogService } from '../audit-log/index.js';
import { assertWithinLimit } from '../../capabilities/index.js';
import { assertWithinLimit, withLimitLock } from '../../capabilities/index.js';
import { CapabilityError } from '../../capabilities/errors.js';

const createApiKeySchema = z.object({
Expand Down Expand Up @@ -72,18 +72,20 @@ export async function apiKeysRoutes(fastify: FastifyInstance) {
});
}

await context.runAsSystem('apikeys:create-limit-check', async () => {
await context.with({ organizationId: project.organizationId }, async () => {
const count = await apiKeysService.countKeysForOrg(project.organizationId);
await assertWithinLimit('apikeys.max', count);
const result = await withLimitLock(project.organizationId, 'apikeys.max', async () => {
await context.runAsSystem('apikeys:create-limit-check', async () => {
await context.with({ organizationId: project.organizationId }, async () => {
const count = await apiKeysService.countKeysForOrg(project.organizationId);
await assertWithinLimit('apikeys.max', count);
});
});
});

const result = await apiKeysService.createApiKey({
projectId,
name: body.name,
type: body.type,
allowedOrigins: body.allowedOrigins ?? null,
return apiKeysService.createApiKey({
projectId,
name: body.name,
type: body.type,
allowedOrigins: body.allowedOrigins ?? null,
});
});

await auditLogService.record({
Expand Down
Loading
Loading