mirror of
https://github.com/SigNoz/signoz.git
synced 2026-11-12 19:31:13 +00:00
Compare commits
5 Commits
feat/input
...
feat/audit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32fc779641 | ||
|
|
ef8171dc9f | ||
|
|
9cd8df5190 | ||
|
|
5c528f8bdc | ||
|
|
79f0720768 |
2
.github/workflows/e2eci.yaml
vendored
2
.github/workflows/e2eci.yaml
vendored
@@ -88,7 +88,7 @@ jobs:
|
||||
postgres:15 \
|
||||
clickhouse/clickhouse-server:25.12.5 \
|
||||
clickhouse/clickhouse-keeper:25.12.5 \
|
||||
signoz/signoz-schema-migrator:v0.144.9 \
|
||||
signoz/signoz-otel-collector:v0.144.16-rc.1 \
|
||||
wiremock/wiremock:2.35.1-1 \
|
||||
| xargs -P 0 -n 1 docker pull -q
|
||||
- name: python
|
||||
|
||||
7
.github/workflows/integrationci.yaml
vendored
7
.github/workflows/integrationci.yaml
vendored
@@ -40,6 +40,7 @@ jobs:
|
||||
suite:
|
||||
- alertmanager
|
||||
- alertmanagerrotation
|
||||
- auditquerier
|
||||
- basepath
|
||||
- callbackauthn
|
||||
- cloudintegrations
|
||||
@@ -83,8 +84,8 @@ jobs:
|
||||
- wal
|
||||
clickhouse-version:
|
||||
- 25.12.5
|
||||
schema-migrator-version:
|
||||
- v0.144.6
|
||||
signoz-otel-collector-version:
|
||||
- v0.144.16-rc.1
|
||||
postgres-version:
|
||||
- 15
|
||||
if: |
|
||||
@@ -155,4 +156,4 @@ jobs:
|
||||
--sqlite-mode ${{matrix.sqlite-mode}} \
|
||||
--postgres-version ${{matrix.postgres-version}} \
|
||||
--clickhouse-version ${{matrix.clickhouse-version}} \
|
||||
--schema-migrator-version ${{matrix.schema-migrator-version}}
|
||||
--signoz-otel-collector-version ${{matrix.signoz-otel-collector-version}}
|
||||
|
||||
@@ -256,7 +256,7 @@ Tests can be configured using pytest options:
|
||||
- `--sqlite-mode` — SQLite journal mode: `delete` or `wal` (default: `delete`). Only relevant when `--sqlstore-provider=sqlite`.
|
||||
- `--postgres-version` — PostgreSQL version (default: `15`)
|
||||
- `--clickhouse-version` — ClickHouse version, also used for ClickHouse Keeper (default: `25.12.5`)
|
||||
- `--schema-migrator-version` — SigNoz schema migrator version (default: `v0.144.9`)
|
||||
- `--signoz-otel-collector-version`: SigNoz OTel collector version, used to run the ClickHouse migrations (default: `v0.144.16-rc.1`)
|
||||
- `--with-web` — Build the frontend into the SigNoz image (required for e2e)
|
||||
|
||||
Example:
|
||||
|
||||
@@ -114,8 +114,8 @@ def pytest_addoption(parser: pytest.Parser):
|
||||
help="clickhouse version",
|
||||
)
|
||||
parser.addoption(
|
||||
"--schema-migrator-version",
|
||||
"--signoz-otel-collector-version",
|
||||
action="store",
|
||||
default="v0.144.9", # todo(nikhil): change to 0.144.10
|
||||
help="schema migrator version",
|
||||
default="v0.144.16-rc.1",
|
||||
help="signoz-otel-collector version, used to run clickhouse migrations",
|
||||
)
|
||||
|
||||
86
tests/fixtures/audit.py
vendored
86
tests/fixtures/audit.py
vendored
@@ -11,6 +11,12 @@ from ksuid import KsuidMs
|
||||
from fixtures import types
|
||||
from fixtures.fingerprint import LogsOrTracesFingerprint
|
||||
|
||||
ALICE_CREATED_DASHBOARD = "alice@acme.com (user-001) created dashboard (dash-001)"
|
||||
ALICE_UPDATED_DASHBOARD = "alice@acme.com (user-001) updated dashboard (dash-001)"
|
||||
VIEWER_FAILED_DELETE_DASHBOARD = "viewer@acme.com (user-002) failed to delete dashboard (dash-001): forbidden"
|
||||
SA_CREATED_ALERT_RULE = "sa-001 created alert-rule (alert-001)"
|
||||
ALICE_UPDATED_USER = "alice@acme.com (user-001) updated user (user-020)"
|
||||
|
||||
|
||||
class AuditResource(ABC):
|
||||
labels: str
|
||||
@@ -180,3 +186,83 @@ def insert_audit_logs(
|
||||
cluster = clickhouse.env["SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_CLUSTER"]
|
||||
for table in ["logs", "logs_resource"]:
|
||||
clickhouse.conn.query(f"TRUNCATE TABLE signoz_audit.{table} ON CLUSTER '{cluster}' SYNC")
|
||||
|
||||
|
||||
def sample_audit_logs(now: datetime.datetime) -> list[AuditLog]:
|
||||
return [
|
||||
AuditLog(
|
||||
timestamp=now - datetime.timedelta(seconds=5),
|
||||
resources={"service.name": "signoz", "signoz.resource.kind": "dashboard", "signoz.resource.id": "dash-001"},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-001",
|
||||
"signoz.principal.email": "alice@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "create",
|
||||
"signoz.audit.outcome": "success",
|
||||
"signoz.audit.fields_changed": 3,
|
||||
},
|
||||
body=ALICE_CREATED_DASHBOARD,
|
||||
event_name="signoz.audit.create",
|
||||
scope_version="1.0.0",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - datetime.timedelta(seconds=4),
|
||||
resources={"service.name": "signoz", "signoz.resource.kind": "dashboard", "signoz.resource.id": "dash-001"},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-001",
|
||||
"signoz.principal.email": "alice@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.outcome": "success",
|
||||
"signoz.audit.fields_changed": 8,
|
||||
},
|
||||
body=ALICE_UPDATED_DASHBOARD,
|
||||
event_name="signoz.audit.update",
|
||||
scope_version="1.0.0",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - datetime.timedelta(seconds=3),
|
||||
resources={"service.name": "signoz", "signoz.resource.kind": "dashboard", "signoz.resource.id": "dash-001"},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-002",
|
||||
"signoz.principal.email": "viewer@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "delete",
|
||||
"signoz.audit.outcome": "failure",
|
||||
"exception.type": "forbidden",
|
||||
},
|
||||
body=VIEWER_FAILED_DELETE_DASHBOARD,
|
||||
event_name="signoz.audit.delete",
|
||||
severity_text="ERROR",
|
||||
scope_version="1.0.0",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - datetime.timedelta(seconds=2),
|
||||
resources={"service.name": "signoz", "signoz.resource.kind": "alert-rule", "signoz.resource.id": "alert-001"},
|
||||
attributes={
|
||||
"signoz.principal.id": "sa-001",
|
||||
"signoz.principal.type": "service_account",
|
||||
"signoz.audit.verb": "create",
|
||||
"signoz.audit.outcome": "success",
|
||||
"signoz.audit.fields_changed": 12,
|
||||
},
|
||||
body=SA_CREATED_ALERT_RULE,
|
||||
event_name="signoz.audit.create",
|
||||
scope_version="1.0.0",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - datetime.timedelta(seconds=1),
|
||||
resources={"service.name": "signoz", "signoz.resource.kind": "user", "signoz.resource.id": "user-020"},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-001",
|
||||
"signoz.principal.email": "alice@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.action_category": "access_control",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body=ALICE_UPDATED_USER,
|
||||
event_name="signoz.audit.update",
|
||||
scope_version="2.0.0",
|
||||
),
|
||||
]
|
||||
|
||||
2
tests/fixtures/metrics.py
vendored
2
tests/fixtures/metrics.py
vendored
@@ -990,7 +990,7 @@ def insert_reduced_metrics_to_clickhouse(
|
||||
) -> None:
|
||||
"""Insert reduced series into distributed_time_series_v4_reduced and 60s
|
||||
buckets into the reduced samples tables. These tables exist only when
|
||||
the schema migrator version includes the metrics cardinality-control
|
||||
the signoz-otel-collector version includes the metrics cardinality-control
|
||||
migration."""
|
||||
if time_series:
|
||||
conn.insert(
|
||||
|
||||
39
tests/fixtures/migrator.py
vendored
39
tests/fixtures/migrator.py
vendored
@@ -19,37 +19,28 @@ def create_migrator( # pylint: disable=too-many-arguments,too-many-positional-a
|
||||
"""
|
||||
Factory function for running schema migrations. Accepts optional
|
||||
env_overrides to customize the migrator environment; the release comes
|
||||
from the --schema-migrator-version option.
|
||||
from the --signoz-otel-collector-version option.
|
||||
"""
|
||||
|
||||
def create() -> None:
|
||||
migrator_version = request.config.getoption("--schema-migrator-version")
|
||||
collector_version = request.config.getoption("--signoz-otel-collector-version")
|
||||
client = docker.from_env()
|
||||
|
||||
environment = dict(env_overrides) if env_overrides else {}
|
||||
environment = {
|
||||
"SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN": clickhouse.env["SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN"],
|
||||
"SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER": "cluster",
|
||||
"SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_REPLICATION": "true",
|
||||
"SIGNOZ_OTEL_COLLECTOR_TIMEOUT": "10m",
|
||||
**(env_overrides or {}),
|
||||
}
|
||||
|
||||
container = client.containers.run(
|
||||
image=f"signoz/signoz-schema-migrator:{migrator_version}",
|
||||
command=f"sync --replication=true --cluster-name=cluster --up= --dsn={clickhouse.env['SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN']}",
|
||||
detach=True,
|
||||
auto_remove=False,
|
||||
network=network.id,
|
||||
environment=environment,
|
||||
)
|
||||
|
||||
result = container.wait()
|
||||
|
||||
if result["StatusCode"] != 0:
|
||||
logs = container.logs().decode(encoding="utf-8")
|
||||
container.remove()
|
||||
print(logs)
|
||||
raise RuntimeError("failed to run migrations on clickhouse")
|
||||
|
||||
container.remove()
|
||||
|
||||
container = client.containers.run(
|
||||
image=f"signoz/signoz-schema-migrator:{migrator_version}",
|
||||
command=f"async --replication=true --cluster-name=cluster --up= --dsn={clickhouse.env['SIGNOZ_TELEMETRYSTORE_CLICKHOUSE_DSN']}",
|
||||
image=f"signoz/signoz-otel-collector:{collector_version}",
|
||||
entrypoint="/bin/sh",
|
||||
command=[
|
||||
"-c",
|
||||
"/signoz-otel-collector migrate bootstrap && /signoz-otel-collector migrate sync up && /signoz-otel-collector migrate async up",
|
||||
],
|
||||
detach=True,
|
||||
auto_remove=False,
|
||||
network=network.id,
|
||||
|
||||
@@ -1,457 +0,0 @@
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.audit import AuditLog
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.querier import (
|
||||
BuilderQuery,
|
||||
build_aggregation,
|
||||
build_order_by,
|
||||
build_scalar_query,
|
||||
make_query_request,
|
||||
)
|
||||
|
||||
|
||||
def test_audit_list_all(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_audit_logs: Callable[[list[AuditLog]], None],
|
||||
) -> None:
|
||||
"""List audit events across multiple resource types — verify count, ordering, and fields."""
|
||||
now = datetime.now(tz=UTC)
|
||||
insert_audit_logs(
|
||||
[
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=3),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.resource.kind": "alert-rule",
|
||||
"signoz.resource.id": "alert-001",
|
||||
},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-010",
|
||||
"signoz.principal.email": "ops@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "create",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="ops@acme.com (user-010) created alert-rule (alert-001)",
|
||||
event_name="signoz.audit.create",
|
||||
severity_text="INFO",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=2),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.resource.kind": "saved-view",
|
||||
"signoz.resource.id": "view-001",
|
||||
},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-010",
|
||||
"signoz.principal.email": "ops@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="ops@acme.com (user-010) updated saved-view (view-001)",
|
||||
event_name="signoz.audit.update",
|
||||
severity_text="INFO",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=1),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.resource.kind": "user",
|
||||
"signoz.resource.id": "user-020",
|
||||
},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-010",
|
||||
"signoz.principal.email": "ops@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.action_category": "access_control",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="ops@acme.com (user-010) updated user (user-020)",
|
||||
event_name="signoz.audit.update",
|
||||
severity_text="INFO",
|
||||
),
|
||||
]
|
||||
)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
now = datetime.now(tz=UTC)
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((now - timedelta(seconds=30)).timestamp() * 1000),
|
||||
end_ms=int(now.timestamp() * 1000),
|
||||
queries=[
|
||||
BuilderQuery(
|
||||
signal="logs",
|
||||
source="audit",
|
||||
limit=100,
|
||||
order=[build_order_by("timestamp"), build_order_by("id")],
|
||||
).to_dict()
|
||||
],
|
||||
request_type="raw",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
rows = response.json()["data"]["data"]["results"][0]["rows"]
|
||||
assert len(rows) == 3
|
||||
|
||||
# Most recent first
|
||||
assert [row["data"]["event_name"] for row in rows] == ["signoz.audit.update", "signoz.audit.update", "signoz.audit.create"]
|
||||
assert [row["data"]["body"]["message"] for row in rows] == [
|
||||
"ops@acme.com (user-010) updated user (user-020)",
|
||||
"ops@acme.com (user-010) updated saved-view (view-001)",
|
||||
"ops@acme.com (user-010) created alert-rule (alert-001)",
|
||||
]
|
||||
|
||||
# Verify event_name and body are present
|
||||
assert rows[0]["data"]["body"] == {"message": "ops@acme.com (user-010) updated user (user-020)"}
|
||||
assert rows[0]["data"]["severity_text"] == "INFO"
|
||||
|
||||
assert rows[0]["data"]["resource"] == {
|
||||
"attributes": {
|
||||
"service.name": "signoz",
|
||||
"signoz.resource.kind": "user",
|
||||
"signoz.resource.id": "user-020",
|
||||
}
|
||||
}
|
||||
assert rows[0]["data"]["attributes"]["signoz.audit.action_category"] == "access_control"
|
||||
assert rows[0]["data"]["scope"]["name"] == "signoz.audit"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filter_expression,expected_count,expected_event_names",
|
||||
[
|
||||
pytest.param(
|
||||
"signoz.principal.id = 'user-001'",
|
||||
3,
|
||||
{"signoz.audit.update", "signoz.audit.create"},
|
||||
id="filter_by_principal_id",
|
||||
),
|
||||
pytest.param(
|
||||
"signoz.audit.outcome = 'failure'",
|
||||
1,
|
||||
{"signoz.audit.delete"},
|
||||
id="filter_by_outcome_failure",
|
||||
),
|
||||
pytest.param(
|
||||
"signoz.resource.kind = 'dashboard' AND signoz.resource.id = 'dash-001'",
|
||||
3,
|
||||
{"signoz.audit.delete", "signoz.audit.update", "signoz.audit.create"},
|
||||
id="filter_by_resource_kind_and_id",
|
||||
),
|
||||
pytest.param(
|
||||
"signoz.principal.type = 'service_account'",
|
||||
1,
|
||||
{"signoz.audit.create"},
|
||||
id="filter_by_principal_type",
|
||||
),
|
||||
pytest.param(
|
||||
"signoz.resource.kind = 'dashboard' AND signoz.audit.verb = 'delete'",
|
||||
1,
|
||||
{"signoz.audit.delete"},
|
||||
id="filter_by_resource_kind_and_action",
|
||||
),
|
||||
pytest.param(
|
||||
"resource.signoz.resource.id = 'sa-001'",
|
||||
1,
|
||||
{"signoz.audit.create"},
|
||||
id="filter_by_resource_prefix",
|
||||
),
|
||||
pytest.param(
|
||||
"body CONTAINS 'failed'",
|
||||
1,
|
||||
{"signoz.audit.delete"},
|
||||
id="filter_by_body_contains",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_audit_filter(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_audit_logs: Callable[[list[AuditLog]], None],
|
||||
filter_expression: str,
|
||||
expected_count: int,
|
||||
expected_event_names: set,
|
||||
) -> None:
|
||||
"""Parametrized audit filter tests covering the documented query patterns."""
|
||||
now = datetime.now(tz=UTC)
|
||||
insert_audit_logs(
|
||||
[
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=5),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.resource.kind": "dashboard",
|
||||
"signoz.resource.id": "dash-001",
|
||||
},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-001",
|
||||
"signoz.principal.email": "alice@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "create",
|
||||
"signoz.audit.action_category": "configuration_changes",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="alice@acme.com created dashboard",
|
||||
event_name="signoz.audit.create",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=4),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.resource.kind": "dashboard",
|
||||
"signoz.resource.id": "dash-001",
|
||||
},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-001",
|
||||
"signoz.principal.email": "alice@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.action_category": "configuration_changes",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="alice@acme.com updated dashboard",
|
||||
event_name="signoz.audit.update",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=3),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.resource.kind": "dashboard",
|
||||
"signoz.resource.id": "dash-001",
|
||||
},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-002",
|
||||
"signoz.principal.email": "viewer@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "delete",
|
||||
"signoz.audit.action_category": "configuration_changes",
|
||||
"signoz.audit.outcome": "failure",
|
||||
"exception.type": "forbidden",
|
||||
},
|
||||
body="viewer@acme.com failed to delete dashboard",
|
||||
event_name="signoz.audit.delete",
|
||||
severity_text="ERROR",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=2),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.resource.kind": "serviceaccount",
|
||||
"signoz.resource.id": "sa-001",
|
||||
},
|
||||
attributes={
|
||||
"signoz.principal.id": "sa-001",
|
||||
"signoz.principal.type": "service_account",
|
||||
"signoz.audit.verb": "create",
|
||||
"signoz.audit.action_category": "access_control",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="sa-001 created serviceaccount",
|
||||
event_name="signoz.audit.create",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=1),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.resource.kind": "session",
|
||||
"signoz.resource.id": "*",
|
||||
},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-001",
|
||||
"signoz.principal.email": "alice@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "create",
|
||||
"signoz.audit.action_category": "access_control",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="alice@acme.com created session",
|
||||
event_name="signoz.audit.create",
|
||||
),
|
||||
]
|
||||
)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
now = datetime.now(tz=UTC)
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((now - timedelta(seconds=30)).timestamp() * 1000),
|
||||
end_ms=int(now.timestamp() * 1000),
|
||||
queries=[
|
||||
BuilderQuery(
|
||||
signal="logs",
|
||||
source="audit",
|
||||
limit=100,
|
||||
filter_expression=filter_expression,
|
||||
order=[build_order_by("timestamp"), build_order_by("id")],
|
||||
).to_dict()
|
||||
],
|
||||
request_type="raw",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
rows = response.json()["data"]["data"]["results"][0]["rows"]
|
||||
assert len(rows) == expected_count
|
||||
|
||||
actual_event_names = {row["data"]["event_name"] for row in rows}
|
||||
assert actual_event_names == expected_event_names
|
||||
|
||||
|
||||
def test_audit_scalar_count_failures(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_audit_logs: Callable[[list[AuditLog]], None],
|
||||
) -> None:
|
||||
"""Alert query — count multiple failures from different principals."""
|
||||
now = datetime.now(tz=UTC)
|
||||
insert_audit_logs(
|
||||
[
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=3),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.resource.kind": "dashboard",
|
||||
"signoz.resource.id": "dash-100",
|
||||
},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-050",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "delete",
|
||||
"signoz.audit.outcome": "failure",
|
||||
},
|
||||
body="user-050 failed to delete dashboard",
|
||||
event_name="signoz.audit.delete",
|
||||
severity_text="ERROR",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=2),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.resource.kind": "alert-rule",
|
||||
"signoz.resource.id": "alert-200",
|
||||
},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-060",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.outcome": "failure",
|
||||
},
|
||||
body="user-060 failed to update alert-rule",
|
||||
event_name="signoz.audit.update",
|
||||
severity_text="ERROR",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=1),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.resource.kind": "dashboard",
|
||||
"signoz.resource.id": "dash-100",
|
||||
},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-050",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="user-050 updated dashboard",
|
||||
event_name="signoz.audit.update",
|
||||
),
|
||||
]
|
||||
)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
now = datetime.now(tz=UTC)
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((now - timedelta(seconds=30)).timestamp() * 1000),
|
||||
end_ms=int(now.timestamp() * 1000),
|
||||
queries=[
|
||||
build_scalar_query(
|
||||
name="A",
|
||||
signal="logs",
|
||||
source="audit",
|
||||
aggregations=[build_aggregation("count()")],
|
||||
filter_expression="signoz.audit.outcome = 'failure'",
|
||||
)
|
||||
],
|
||||
request_type="scalar",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
scalar_data = response.json()["data"]["data"]["results"][0].get("data", [])
|
||||
assert len(scalar_data) == 1
|
||||
assert scalar_data[0][0] == 2
|
||||
|
||||
|
||||
def test_audit_does_not_leak_into_logs(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_audit_logs: Callable[[list[AuditLog]], None],
|
||||
) -> None:
|
||||
"""A single audit event in signoz_audit must not appear in regular log queries."""
|
||||
now = datetime.now(tz=UTC)
|
||||
insert_audit_logs(
|
||||
[
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=1),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.resource.kind": "organization",
|
||||
"signoz.resource.id": "org-999",
|
||||
},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-admin",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="user-admin updated organization (org-999)",
|
||||
event_name="signoz.audit.update",
|
||||
),
|
||||
]
|
||||
)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
now = datetime.now(tz=UTC)
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((now - timedelta(seconds=30)).timestamp() * 1000),
|
||||
end_ms=int(now.timestamp() * 1000),
|
||||
queries=[
|
||||
BuilderQuery(
|
||||
signal="logs",
|
||||
limit=100,
|
||||
order=[build_order_by("timestamp"), build_order_by("id")],
|
||||
).to_dict()
|
||||
],
|
||||
request_type="raw",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
rows = response.json()["data"]["data"]["results"][0].get("rows") or []
|
||||
|
||||
audit_bodies = [row["data"]["body"] for row in rows if "signoz.audit" in row["data"].get("attributes_string", {}).get("signoz.audit.verb", "")]
|
||||
assert len(audit_bodies) == 0
|
||||
177
tests/integration/tests/auditquerier/01_list.py
Normal file
177
tests/integration/tests/auditquerier/01_list.py
Normal file
@@ -0,0 +1,177 @@
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.audit import AuditLog
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.querier import BuilderQuery, build_order_by, make_query_request
|
||||
|
||||
|
||||
def test_audit_list_all(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_audit_logs: Callable[[list[AuditLog]], None],
|
||||
) -> None:
|
||||
"""List audit events across multiple resource types — verify count, ordering, and fields."""
|
||||
now = datetime.now(tz=UTC)
|
||||
insert_audit_logs(
|
||||
[
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=3),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.resource.kind": "alert-rule",
|
||||
"signoz.resource.id": "alert-001",
|
||||
},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-010",
|
||||
"signoz.principal.email": "ops@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "create",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="ops@acme.com (user-010) created alert-rule (alert-001)",
|
||||
event_name="signoz.audit.create",
|
||||
severity_text="INFO",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=2),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.resource.kind": "saved-view",
|
||||
"signoz.resource.id": "view-001",
|
||||
},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-010",
|
||||
"signoz.principal.email": "ops@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="ops@acme.com (user-010) updated saved-view (view-001)",
|
||||
event_name="signoz.audit.update",
|
||||
severity_text="INFO",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=1),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.resource.kind": "user",
|
||||
"signoz.resource.id": "user-020",
|
||||
},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-010",
|
||||
"signoz.principal.email": "ops@acme.com",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.action_category": "access_control",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="ops@acme.com (user-010) updated user (user-020)",
|
||||
event_name="signoz.audit.update",
|
||||
severity_text="INFO",
|
||||
),
|
||||
]
|
||||
)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
now = datetime.now(tz=UTC)
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((now - timedelta(seconds=30)).timestamp() * 1000),
|
||||
end_ms=int(now.timestamp() * 1000),
|
||||
queries=[
|
||||
BuilderQuery(
|
||||
signal="logs",
|
||||
source="audit",
|
||||
limit=100,
|
||||
order=[build_order_by("timestamp"), build_order_by("id")],
|
||||
).to_dict()
|
||||
],
|
||||
request_type="raw",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
rows = response.json()["data"]["data"]["results"][0]["rows"]
|
||||
assert len(rows) == 3
|
||||
|
||||
# Most recent first
|
||||
assert [row["data"]["event_name"] for row in rows] == ["signoz.audit.update", "signoz.audit.update", "signoz.audit.create"]
|
||||
assert [row["data"]["body"]["message"] for row in rows] == [
|
||||
"ops@acme.com (user-010) updated user (user-020)",
|
||||
"ops@acme.com (user-010) updated saved-view (view-001)",
|
||||
"ops@acme.com (user-010) created alert-rule (alert-001)",
|
||||
]
|
||||
|
||||
# Verify event_name and body are present
|
||||
assert rows[0]["data"]["body"] == {"message": "ops@acme.com (user-010) updated user (user-020)"}
|
||||
assert rows[0]["data"]["severity_text"] == "INFO"
|
||||
|
||||
assert rows[0]["data"]["resource"] == {
|
||||
"attributes": {
|
||||
"service.name": "signoz",
|
||||
"signoz.resource.kind": "user",
|
||||
"signoz.resource.id": "user-020",
|
||||
}
|
||||
}
|
||||
assert rows[0]["data"]["attributes"]["signoz.audit.action_category"] == "access_control"
|
||||
assert rows[0]["data"]["scope"]["name"] == "signoz.audit"
|
||||
|
||||
|
||||
def test_audit_does_not_leak_into_logs(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_audit_logs: Callable[[list[AuditLog]], None],
|
||||
) -> None:
|
||||
"""A single audit event in signoz_audit must not appear in regular log queries."""
|
||||
now = datetime.now(tz=UTC)
|
||||
insert_audit_logs(
|
||||
[
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=1),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.resource.kind": "organization",
|
||||
"signoz.resource.id": "org-999",
|
||||
},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-admin",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="user-admin updated organization (org-999)",
|
||||
event_name="signoz.audit.update",
|
||||
),
|
||||
]
|
||||
)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
now = datetime.now(tz=UTC)
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((now - timedelta(seconds=30)).timestamp() * 1000),
|
||||
end_ms=int(now.timestamp() * 1000),
|
||||
queries=[
|
||||
BuilderQuery(
|
||||
signal="logs",
|
||||
limit=100,
|
||||
order=[build_order_by("timestamp"), build_order_by("id")],
|
||||
).to_dict()
|
||||
],
|
||||
request_type="raw",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
|
||||
rows = response.json()["data"]["data"]["results"][0].get("rows") or []
|
||||
|
||||
audit_bodies = [row["data"]["body"] for row in rows if "signoz.audit" in row["data"].get("attributes_string", {}).get("signoz.audit.verb", "")]
|
||||
assert len(audit_bodies) == 0
|
||||
214
tests/integration/tests/auditquerier/03_aggregations.py
Normal file
214
tests/integration/tests/auditquerier/03_aggregations.py
Normal file
@@ -0,0 +1,214 @@
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.audit import AuditLog
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.querier import (
|
||||
RequestType,
|
||||
build_aggregation,
|
||||
build_group_by_field,
|
||||
build_scalar_query,
|
||||
get_all_series,
|
||||
index_series_by_label,
|
||||
make_query_request,
|
||||
)
|
||||
|
||||
|
||||
def test_audit_scalar_count_failures(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_audit_logs: Callable[[list[AuditLog]], None],
|
||||
) -> None:
|
||||
"""Alert query — count multiple failures from different principals."""
|
||||
now = datetime.now(tz=UTC)
|
||||
insert_audit_logs(
|
||||
[
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=3),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.resource.kind": "dashboard",
|
||||
"signoz.resource.id": "dash-100",
|
||||
},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-050",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "delete",
|
||||
"signoz.audit.outcome": "failure",
|
||||
},
|
||||
body="user-050 failed to delete dashboard",
|
||||
event_name="signoz.audit.delete",
|
||||
severity_text="ERROR",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=2),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.resource.kind": "alert-rule",
|
||||
"signoz.resource.id": "alert-200",
|
||||
},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-060",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.outcome": "failure",
|
||||
},
|
||||
body="user-060 failed to update alert-rule",
|
||||
event_name="signoz.audit.update",
|
||||
severity_text="ERROR",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=1),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.resource.kind": "dashboard",
|
||||
"signoz.resource.id": "dash-100",
|
||||
},
|
||||
attributes={
|
||||
"signoz.principal.id": "user-050",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="user-050 updated dashboard",
|
||||
event_name="signoz.audit.update",
|
||||
),
|
||||
]
|
||||
)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
now = datetime.now(tz=UTC)
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((now - timedelta(seconds=30)).timestamp() * 1000),
|
||||
end_ms=int(now.timestamp() * 1000),
|
||||
queries=[
|
||||
build_scalar_query(
|
||||
name="A",
|
||||
signal="logs",
|
||||
source="audit",
|
||||
aggregations=[build_aggregation("count()")],
|
||||
filter_expression="signoz.audit.outcome = 'failure'",
|
||||
)
|
||||
],
|
||||
request_type="scalar",
|
||||
)
|
||||
|
||||
assert response.status_code == HTTPStatus.OK
|
||||
assert response.json()["status"] == "success"
|
||||
|
||||
scalar_data = response.json()["data"]["data"]["results"][0].get("data", [])
|
||||
assert len(scalar_data) == 1
|
||||
assert scalar_data[0][0] == 2
|
||||
|
||||
|
||||
def test_audit_scalar_distinct_principals_by_resource_kind(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_audit_logs: Callable[[list[AuditLog]], None],
|
||||
) -> None:
|
||||
"""count_distinct(principal) grouped by a resource field: dashboards touched by two principals, alert rules by one."""
|
||||
now = datetime.now(tz=UTC)
|
||||
insert_audit_logs(
|
||||
[
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=seconds_ago),
|
||||
resources={"service.name": "signoz", "signoz.resource.kind": kind, "signoz.resource.id": resource_id},
|
||||
attributes={"signoz.principal.id": principal, "signoz.principal.type": "user", "signoz.audit.verb": verb, "signoz.audit.outcome": "success"},
|
||||
body=f"{principal} {verb} {kind} ({resource_id})",
|
||||
event_name=f"signoz.audit.{verb}",
|
||||
)
|
||||
for seconds_ago, kind, resource_id, principal, verb in [
|
||||
(5, "dashboard", "dash-001", "user-001", "update"),
|
||||
(4, "dashboard", "dash-001", "user-001", "update"),
|
||||
(3, "dashboard", "dash-002", "user-002", "delete"),
|
||||
(2, "alert-rule", "alert-001", "user-001", "create"),
|
||||
(1, "alert-rule", "alert-002", "user-001", "update"),
|
||||
]
|
||||
]
|
||||
)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((now - timedelta(seconds=30)).timestamp() * 1000),
|
||||
end_ms=int(now.timestamp() * 1000),
|
||||
queries=[
|
||||
build_scalar_query(
|
||||
name="A",
|
||||
signal="logs",
|
||||
source="audit",
|
||||
aggregations=[build_aggregation("count_distinct(signoz.principal.id)")],
|
||||
group_by=[build_group_by_field("signoz.resource.kind", "string", "resource")],
|
||||
)
|
||||
],
|
||||
request_type=RequestType.SCALAR,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
rows = response.json()["data"]["data"]["results"][0]["data"]
|
||||
assert {row[0]: row[-1] for row in rows} == {"dashboard": 2, "alert-rule": 1}
|
||||
|
||||
|
||||
def test_audit_time_series_count_by_verb(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_audit_logs: Callable[[list[AuditLog]], None],
|
||||
) -> None:
|
||||
"""count() per minute grouped by verb: two updates two minutes ago, one update and one delete a minute ago."""
|
||||
now = datetime.now(tz=UTC).replace(second=0, microsecond=0)
|
||||
one_minute_ago = now - timedelta(minutes=1)
|
||||
two_minutes_ago = now - timedelta(minutes=2)
|
||||
insert_audit_logs(
|
||||
[
|
||||
AuditLog(
|
||||
timestamp=timestamp,
|
||||
resources={"service.name": "signoz", "signoz.resource.kind": "dashboard", "signoz.resource.id": resource_id},
|
||||
attributes={"signoz.principal.id": principal, "signoz.principal.type": "user", "signoz.audit.verb": verb, "signoz.audit.outcome": "success"},
|
||||
body=f"{principal} {verb} dashboard ({resource_id})",
|
||||
event_name=f"signoz.audit.{verb}",
|
||||
)
|
||||
for timestamp, resource_id, principal, verb in [
|
||||
(two_minutes_ago + timedelta(seconds=10), "dash-001", "user-001", "update"),
|
||||
(two_minutes_ago + timedelta(seconds=20), "dash-002", "user-001", "update"),
|
||||
(one_minute_ago + timedelta(seconds=10), "dash-001", "user-002", "update"),
|
||||
(one_minute_ago + timedelta(seconds=30), "dash-002", "user-002", "delete"),
|
||||
]
|
||||
]
|
||||
)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((now - timedelta(minutes=5)).timestamp() * 1000),
|
||||
end_ms=int((now + timedelta(seconds=5)).timestamp() * 1000),
|
||||
queries=[
|
||||
build_scalar_query(
|
||||
name="A",
|
||||
signal="logs",
|
||||
source="audit",
|
||||
aggregations=[build_aggregation("count()")],
|
||||
group_by=[build_group_by_field("signoz.audit.verb", "string", "attribute")],
|
||||
)
|
||||
],
|
||||
request_type=RequestType.TIME_SERIES,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
series_by_verb = index_series_by_label(get_all_series(response.json(), "A"), "signoz.audit.verb")
|
||||
counts_by_verb = {verb: {point["timestamp"]: point["value"] for point in series["values"] if point["value"]} for verb, series in series_by_verb.items()}
|
||||
|
||||
two_minutes_ago_ms = int(two_minutes_ago.timestamp() * 1000)
|
||||
one_minute_ago_ms = int(one_minute_ago.timestamp() * 1000)
|
||||
assert counts_by_verb == {
|
||||
"update": {two_minutes_ago_ms: 2, one_minute_ago_ms: 1},
|
||||
"delete": {one_minute_ago_ms: 1},
|
||||
}
|
||||
74
tests/integration/tests/auditquerier/04_filter_operators.py
Normal file
74
tests/integration/tests/auditquerier/04_filter_operators.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.audit import (
|
||||
ALICE_CREATED_DASHBOARD,
|
||||
ALICE_UPDATED_DASHBOARD,
|
||||
ALICE_UPDATED_USER,
|
||||
SA_CREATED_ALERT_RULE,
|
||||
VIEWER_FAILED_DELETE_DASHBOARD,
|
||||
AuditLog,
|
||||
sample_audit_logs,
|
||||
)
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.querier import BuilderQuery, build_order_by, make_query_request
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filter_expression,expected_bodies",
|
||||
[
|
||||
pytest.param("signoz.principal.id = 'user-001'", {ALICE_CREATED_DASHBOARD, ALICE_UPDATED_DASHBOARD, ALICE_UPDATED_USER}, id="equal"),
|
||||
pytest.param("signoz.audit.verb != 'update'", {ALICE_CREATED_DASHBOARD, VIEWER_FAILED_DELETE_DASHBOARD, SA_CREATED_ALERT_RULE}, id="not_equal"),
|
||||
pytest.param("signoz.audit.verb IN ['create', 'delete']", {ALICE_CREATED_DASHBOARD, VIEWER_FAILED_DELETE_DASHBOARD, SA_CREATED_ALERT_RULE}, id="in_list"),
|
||||
pytest.param("signoz.resource.kind NOT IN ['dashboard']", {SA_CREATED_ALERT_RULE, ALICE_UPDATED_USER}, id="not_in_list"),
|
||||
pytest.param("signoz.resource.kind = 'dashboard' AND signoz.resource.id = 'dash-001'", {ALICE_CREATED_DASHBOARD, ALICE_UPDATED_DASHBOARD, VIEWER_FAILED_DELETE_DASHBOARD}, id="and"),
|
||||
pytest.param("signoz.audit.outcome = 'failure' OR signoz.principal.type = 'service_account'", {VIEWER_FAILED_DELETE_DASHBOARD, SA_CREATED_ALERT_RULE}, id="or"),
|
||||
pytest.param("NOT signoz.resource.kind = 'dashboard'", {SA_CREATED_ALERT_RULE, ALICE_UPDATED_USER}, id="not"),
|
||||
pytest.param("signoz.resource.kind = 'dashboard' AND (signoz.audit.verb = 'create' OR signoz.audit.verb = 'delete')", {ALICE_CREATED_DASHBOARD, VIEWER_FAILED_DELETE_DASHBOARD}, id="and_with_grouped_or"),
|
||||
pytest.param("exception.type EXISTS", {VIEWER_FAILED_DELETE_DASHBOARD}, id="exists"),
|
||||
pytest.param("exception.type NOT EXISTS", {ALICE_CREATED_DASHBOARD, ALICE_UPDATED_DASHBOARD, SA_CREATED_ALERT_RULE, ALICE_UPDATED_USER}, id="not_exists"),
|
||||
pytest.param("body CONTAINS 'failed'", {VIEWER_FAILED_DELETE_DASHBOARD}, id="contains"),
|
||||
pytest.param("body NOT CONTAINS 'dashboard'", {SA_CREATED_ALERT_RULE, ALICE_UPDATED_USER}, id="not_contains"),
|
||||
pytest.param("body LIKE '%failed%'", {VIEWER_FAILED_DELETE_DASHBOARD}, id="like"),
|
||||
pytest.param("body ILIKE '%ALERT-RULE%'", {SA_CREATED_ALERT_RULE}, id="ilike"),
|
||||
pytest.param("body REGEXP '^sa-001'", {SA_CREATED_ALERT_RULE}, id="regexp"),
|
||||
pytest.param("signoz.audit.fields_changed > 5", {ALICE_UPDATED_DASHBOARD, SA_CREATED_ALERT_RULE}, id="gt_number"),
|
||||
pytest.param("signoz.audit.fields_changed <= 8", {ALICE_CREATED_DASHBOARD, ALICE_UPDATED_DASHBOARD}, id="lte_number"),
|
||||
],
|
||||
)
|
||||
def test_audit_filter_operators(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_audit_logs: Callable[[list[AuditLog]], None],
|
||||
filter_expression: str,
|
||||
expected_bodies: set[str],
|
||||
) -> None:
|
||||
now = datetime.now(tz=UTC)
|
||||
insert_audit_logs(sample_audit_logs(now))
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((now - timedelta(seconds=30)).timestamp() * 1000),
|
||||
end_ms=int(now.timestamp() * 1000),
|
||||
queries=[
|
||||
BuilderQuery(
|
||||
signal="logs",
|
||||
source="audit",
|
||||
limit=100,
|
||||
filter_expression=filter_expression,
|
||||
order=[build_order_by("timestamp"), build_order_by("id")],
|
||||
).to_dict()
|
||||
],
|
||||
request_type="raw",
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
rows = response.json()["data"]["data"]["results"][0].get("rows") or []
|
||||
assert {row["data"]["body"]["message"] for row in rows} == expected_bodies
|
||||
78
tests/integration/tests/auditquerier/05_time_range.py
Normal file
78
tests/integration/tests/auditquerier/05_time_range.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.audit import AuditLog
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.querier import (
|
||||
BuilderQuery,
|
||||
RequestType,
|
||||
build_aggregation,
|
||||
build_order_by,
|
||||
build_scalar_query,
|
||||
make_query_request,
|
||||
)
|
||||
|
||||
|
||||
def test_audit_time_range_is_start_inclusive_end_exclusive(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_audit_logs: Callable[[list[AuditLog]], None],
|
||||
) -> None:
|
||||
"""Events at start, inside, and at end minus one second are returned; one second before start, at end, and two hours earlier are not. The same window bounds a scalar count."""
|
||||
end = datetime.now(tz=UTC).replace(microsecond=0)
|
||||
start = end - timedelta(minutes=1)
|
||||
insert_audit_logs(
|
||||
[
|
||||
AuditLog(
|
||||
timestamp=timestamp,
|
||||
resources={"service.name": "signoz", "signoz.resource.kind": "dashboard", "signoz.resource.id": "dash-001"},
|
||||
attributes={"signoz.principal.id": "user-001", "signoz.principal.type": "user", "signoz.audit.verb": "update", "signoz.audit.outcome": "success"},
|
||||
body=body,
|
||||
event_name="signoz.audit.update",
|
||||
)
|
||||
for timestamp, body in [
|
||||
(start - timedelta(seconds=1), "one second before start"),
|
||||
(start, "at start"),
|
||||
(start + timedelta(seconds=30), "inside"),
|
||||
(end - timedelta(seconds=1), "one second before end"),
|
||||
(end, "at end"),
|
||||
(end - timedelta(hours=2), "two hours earlier"),
|
||||
]
|
||||
]
|
||||
)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
start_ms = int(start.timestamp() * 1000)
|
||||
end_ms = int(end.timestamp() * 1000)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=start_ms,
|
||||
end_ms=end_ms,
|
||||
queries=[
|
||||
BuilderQuery(
|
||||
signal="logs",
|
||||
source="audit",
|
||||
limit=100,
|
||||
order=[build_order_by("timestamp"), build_order_by("id")],
|
||||
).to_dict()
|
||||
],
|
||||
request_type=RequestType.RAW,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
rows = response.json()["data"]["data"]["results"][0].get("rows") or []
|
||||
assert [row["data"]["body"]["message"] for row in rows] == ["one second before end", "inside", "at start"]
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=start_ms,
|
||||
end_ms=end_ms,
|
||||
queries=[build_scalar_query(name="A", signal="logs", source="audit", aggregations=[build_aggregation("count()")])],
|
||||
request_type=RequestType.SCALAR,
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
assert response.json()["data"]["data"]["results"][0]["data"][0][0] == 3
|
||||
99
tests/integration/tests/auditquerier/06_relations.py
Normal file
99
tests/integration/tests/auditquerier/06_relations.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.audit import AuditLog
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.querier import BuilderQuery, build_order_by, make_query_request
|
||||
|
||||
ATTACHED_ROLE_001_TO_USER_020 = "ops@acme.com (user-010) attached role (role-001) to user (user-020)"
|
||||
DETACHED_ROLE_001_FROM_USER_021 = "ops@acme.com (user-010) detached role (role-001) from user (user-021)"
|
||||
ATTACHED_ROLE_002_TO_USER_020 = "ops@acme.com (user-010) attached role (role-002) to user (user-020)"
|
||||
UPDATED_ROLE_001 = "ops@acme.com (user-010) updated role (role-001)"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filter_expression,expected_bodies",
|
||||
[
|
||||
pytest.param("event_name = 'signoz.audit.attach'", {ATTACHED_ROLE_001_TO_USER_020, ATTACHED_ROLE_002_TO_USER_020}, id="attach_events"),
|
||||
pytest.param("event_name = 'signoz.audit.detach'", {DETACHED_ROLE_001_FROM_USER_021}, id="detach_events"),
|
||||
pytest.param("signoz.resource.target.id = 'user-020'", {ATTACHED_ROLE_001_TO_USER_020, ATTACHED_ROLE_002_TO_USER_020}, id="by_target_id"),
|
||||
pytest.param("signoz.resource.id = 'role-001' AND signoz.resource.target.kind = 'user'", {ATTACHED_ROLE_001_TO_USER_020, DETACHED_ROLE_001_FROM_USER_021}, id="by_resource_and_target_kind"),
|
||||
pytest.param("signoz.resource.target.kind EXISTS", {ATTACHED_ROLE_001_TO_USER_020, DETACHED_ROLE_001_FROM_USER_021, ATTACHED_ROLE_002_TO_USER_020}, id="with_target"),
|
||||
pytest.param("signoz.resource.target.kind NOT EXISTS", {UPDATED_ROLE_001}, id="without_target"),
|
||||
],
|
||||
)
|
||||
def test_audit_relation_filters(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_audit_logs: Callable[[list[AuditLog]], None],
|
||||
filter_expression: str,
|
||||
expected_bodies: set[str],
|
||||
) -> None:
|
||||
"""Attach and detach events carry the counterpart as target attributes on the record; a plain update on the same resource carries none."""
|
||||
now = datetime.now(tz=UTC)
|
||||
principal = {"signoz.principal.id": "user-010", "signoz.principal.email": "ops@acme.com", "signoz.principal.type": "user", "signoz.audit.outcome": "success"}
|
||||
insert_audit_logs(
|
||||
[
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=4),
|
||||
resources={"service.name": "signoz", "signoz.resource.kind": "role", "signoz.resource.id": "role-001"},
|
||||
attributes={**principal, "signoz.audit.verb": "attach", "signoz.resource.target.kind": "user", "signoz.resource.target.id": "user-020"},
|
||||
body=ATTACHED_ROLE_001_TO_USER_020,
|
||||
event_name="signoz.audit.attach",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=3),
|
||||
resources={"service.name": "signoz", "signoz.resource.kind": "role", "signoz.resource.id": "role-001"},
|
||||
attributes={**principal, "signoz.audit.verb": "detach", "signoz.resource.target.kind": "user", "signoz.resource.target.id": "user-021"},
|
||||
body=DETACHED_ROLE_001_FROM_USER_021,
|
||||
event_name="signoz.audit.detach",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=2),
|
||||
resources={"service.name": "signoz", "signoz.resource.kind": "role", "signoz.resource.id": "role-002"},
|
||||
attributes={**principal, "signoz.audit.verb": "attach", "signoz.resource.target.kind": "user", "signoz.resource.target.id": "user-020"},
|
||||
body=ATTACHED_ROLE_002_TO_USER_020,
|
||||
event_name="signoz.audit.attach",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=1),
|
||||
resources={"service.name": "signoz", "signoz.resource.kind": "role", "signoz.resource.id": "role-001"},
|
||||
attributes={**principal, "signoz.audit.verb": "update"},
|
||||
body=UPDATED_ROLE_001,
|
||||
event_name="signoz.audit.update",
|
||||
),
|
||||
]
|
||||
)
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((now - timedelta(seconds=30)).timestamp() * 1000),
|
||||
end_ms=int(now.timestamp() * 1000),
|
||||
queries=[
|
||||
BuilderQuery(
|
||||
signal="logs",
|
||||
source="audit",
|
||||
limit=100,
|
||||
filter_expression=filter_expression,
|
||||
order=[build_order_by("timestamp"), build_order_by("id")],
|
||||
).to_dict()
|
||||
],
|
||||
request_type="raw",
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
rows = response.json()["data"]["data"]["results"][0].get("rows") or []
|
||||
assert {row["data"]["body"]["message"] for row in rows} == expected_bodies
|
||||
for row in rows:
|
||||
if row["data"]["event_name"] == "signoz.audit.update":
|
||||
assert "signoz.resource.target.kind" not in row["data"]["attributes"]
|
||||
else:
|
||||
assert row["data"]["attributes"]["signoz.resource.target.kind"] == "user"
|
||||
assert row["data"]["resource"]["attributes"]["signoz.resource.kind"] == "role"
|
||||
73
tests/integration/tests/auditquerier/07_field_contexts.py
Normal file
73
tests/integration/tests/auditquerier/07_field_contexts.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from collections.abc import Callable
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from http import HTTPStatus
|
||||
|
||||
import pytest
|
||||
|
||||
from fixtures import types
|
||||
from fixtures.audit import (
|
||||
ALICE_CREATED_DASHBOARD,
|
||||
ALICE_UPDATED_DASHBOARD,
|
||||
ALICE_UPDATED_USER,
|
||||
SA_CREATED_ALERT_RULE,
|
||||
VIEWER_FAILED_DELETE_DASHBOARD,
|
||||
AuditLog,
|
||||
sample_audit_logs,
|
||||
)
|
||||
from fixtures.auth import USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD
|
||||
from fixtures.querier import BuilderQuery, build_order_by, make_query_request
|
||||
|
||||
ALL_SAMPLE_BODIES = {ALICE_CREATED_DASHBOARD, ALICE_UPDATED_DASHBOARD, VIEWER_FAILED_DELETE_DASHBOARD, SA_CREATED_ALERT_RULE, ALICE_UPDATED_USER}
|
||||
BARE_SCOPE_KEYS_SKIP = pytest.mark.skip(reason="bare scope_name and scope_version resolve to attributes in the audit schema; only the scope. prefix reaches the scope column")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filter_expression,expected_bodies",
|
||||
[
|
||||
pytest.param("signoz.resource.id = 'alert-001'", {SA_CREATED_ALERT_RULE}, id="resource_key_bare"),
|
||||
pytest.param("resource.signoz.resource.id = 'alert-001'", {SA_CREATED_ALERT_RULE}, id="resource_key_prefixed"),
|
||||
pytest.param("signoz.principal.email = 'alice@acme.com'", {ALICE_CREATED_DASHBOARD, ALICE_UPDATED_DASHBOARD, ALICE_UPDATED_USER}, id="attribute_key_bare"),
|
||||
pytest.param("attribute.signoz.principal.email = 'alice@acme.com'", {ALICE_CREATED_DASHBOARD, ALICE_UPDATED_DASHBOARD, ALICE_UPDATED_USER}, id="attribute_key_prefixed"),
|
||||
pytest.param("severity_text = 'ERROR'", {VIEWER_FAILED_DELETE_DASHBOARD}, id="intrinsic_key_bare"),
|
||||
pytest.param("log.severity_text = 'ERROR'", {VIEWER_FAILED_DELETE_DASHBOARD}, id="intrinsic_key_prefixed"),
|
||||
pytest.param("event_name = 'signoz.audit.create'", {ALICE_CREATED_DASHBOARD, SA_CREATED_ALERT_RULE}, id="intrinsic_event_name"),
|
||||
pytest.param("scope.name = 'signoz.audit'", ALL_SAMPLE_BODIES, id="scope_name_prefixed"),
|
||||
pytest.param("scope.name = 'other'", set(), id="scope_name_prefixed_no_match"),
|
||||
pytest.param("scope.version = '2.0.0'", {ALICE_UPDATED_USER}, id="scope_version_prefixed"),
|
||||
pytest.param("scope_name = 'signoz.audit'", ALL_SAMPLE_BODIES, marks=BARE_SCOPE_KEYS_SKIP, id="scope_name_bare"),
|
||||
pytest.param("scope_version = '2.0.0'", {ALICE_UPDATED_USER}, marks=BARE_SCOPE_KEYS_SKIP, id="scope_version_bare"),
|
||||
],
|
||||
)
|
||||
def test_audit_filter_by_field_context(
|
||||
signoz: types.SigNoz,
|
||||
create_user_admin: None, # pylint: disable=unused-argument
|
||||
get_token: Callable[[str, str], str],
|
||||
insert_audit_logs: Callable[[list[AuditLog]], None],
|
||||
filter_expression: str,
|
||||
expected_bodies: set[str],
|
||||
) -> None:
|
||||
"""A bare key resolves to the resource column for the keys the audit writer emits, to a built-in column for intrinsics, and to attributes otherwise; a context prefix pins it."""
|
||||
now = datetime.now(tz=UTC)
|
||||
insert_audit_logs(sample_audit_logs(now))
|
||||
token = get_token(USER_ADMIN_EMAIL, USER_ADMIN_PASSWORD)
|
||||
|
||||
response = make_query_request(
|
||||
signoz,
|
||||
token,
|
||||
start_ms=int((now - timedelta(seconds=30)).timestamp() * 1000),
|
||||
end_ms=int(now.timestamp() * 1000),
|
||||
queries=[
|
||||
BuilderQuery(
|
||||
signal="logs",
|
||||
source="audit",
|
||||
limit=100,
|
||||
filter_expression=filter_expression,
|
||||
order=[build_order_by("timestamp"), build_order_by("id")],
|
||||
).to_dict()
|
||||
],
|
||||
request_type="raw",
|
||||
)
|
||||
assert response.status_code == HTTPStatus.OK, response.text
|
||||
|
||||
rows = response.json()["data"]["data"]["results"][0].get("rows") or []
|
||||
assert {row["data"]["body"]["message"] for row in rows} == expected_bodies
|
||||
Reference in New Issue
Block a user