mirror of
https://github.com/SigNoz/signoz.git
synced 2026-11-07 17:00:41 +00:00
Compare commits
1 Commits
fix/genera
...
keystone-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1effc948a |
1
go.mod
1
go.mod
@@ -10,6 +10,7 @@ require (
|
||||
github.com/SigNoz/clickhouse-go-mock v0.14.0
|
||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
|
||||
github.com/SigNoz/signoz-otel-collector v0.144.6
|
||||
github.com/SigNoz/signoz-semantic-conventions/semconv/go v0.0.0-20261106053310-1c638e9f15fe
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1
|
||||
github.com/antonmedv/expr v1.15.3
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
|
||||
2
go.sum
2
go.sum
@@ -102,6 +102,8 @@ github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd h1:Bk43AsDYe0fhkb
|
||||
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd/go.mod h1:nxRcH/OEdM8QxzH37xkGzomr1O0JpYBRS6pwjsWW6Pc=
|
||||
github.com/SigNoz/signoz-otel-collector v0.144.6 h1:AgblCIDlLKGtssS4ywvyEByH9fbJ7hFqZvPvUVJKwp8=
|
||||
github.com/SigNoz/signoz-otel-collector v0.144.6/go.mod h1:YRQw6ETaq7fm8ts4Itgb6wC0bVhc0+nC6PFI0MaiH9k=
|
||||
github.com/SigNoz/signoz-semantic-conventions/semconv/go v0.0.0-20261106053310-1c638e9f15fe h1:gPy02emReD47zTba0k7F2Xf9yyica2S+MGygLAASoWM=
|
||||
github.com/SigNoz/signoz-semantic-conventions/semconv/go v0.0.0-20261106053310-1c638e9f15fe/go.mod h1:sHbNUFRlBAJP2z9naC5HkczucmE4nZCvwy4+EQbav70=
|
||||
github.com/Yiling-J/theine-go v0.6.2 h1:1GeoXeQ0O0AUkiwj2S9Jc0Mzx+hpqzmqsJ4kIC4M9AY=
|
||||
github.com/Yiling-J/theine-go v0.6.2/go.mod h1:08QpMa5JZ2pKN+UJCRrCasWYO1IKCdl54Xa836rpmDU=
|
||||
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c=
|
||||
|
||||
@@ -23,7 +23,7 @@ func newTestSettings() factory.ScopedProviderSettings {
|
||||
func newTestEvent(resource coretypes.Resource, action coretypes.Verb) audittypes.AuditEvent {
|
||||
return audittypes.AuditEvent{
|
||||
Timestamp: time.Now(),
|
||||
EventName: audittypes.NewEventName(resource.Kind(), action),
|
||||
EventName: audittypes.NewEventName(action),
|
||||
AuditAttributes: audittypes.AuditAttributes{
|
||||
Action: action,
|
||||
Outcome: audittypes.OutcomeSuccess,
|
||||
|
||||
@@ -109,10 +109,9 @@ func (middleware *Audit) emitAuditEvent(req *http.Request, writer responseCaptur
|
||||
statusCode := writer.StatusCode()
|
||||
span := trace.SpanFromContext(req.Context())
|
||||
|
||||
var errorType, errorCode string
|
||||
var errorType string
|
||||
if statusCode >= 400 {
|
||||
errorType = render.ErrorTypeFromStatusCode(statusCode)
|
||||
errorCode = render.ErrorCodeFromBody(writer.BodyBytes())
|
||||
}
|
||||
|
||||
extractorCtx := coretypes.ExtractorContext{Request: req, ResponseBody: writer.BodyBytes()}
|
||||
@@ -122,8 +121,12 @@ func (middleware *Audit) emitAuditEvent(req *http.Request, writer responseCaptur
|
||||
continue
|
||||
}
|
||||
|
||||
resource.ResolveResponse(extractorCtx)
|
||||
verb, category := resource.Verb(), resource.Category()
|
||||
if !audittypes.IsAudited(verb) {
|
||||
continue
|
||||
}
|
||||
|
||||
resource.ResolveResponse(extractorCtx)
|
||||
|
||||
switch typed := resource.(type) {
|
||||
case coretypes.ResolvedResourceWithTargetResource:
|
||||
@@ -138,15 +141,13 @@ func (middleware *Audit) emitAuditEvent(req *http.Request, writer responseCaptur
|
||||
),
|
||||
}
|
||||
|
||||
// Sibling peers are symmetric, so mirror the event from the target's side too.
|
||||
if !typed.IsParentChild() {
|
||||
attributesList = append(attributesList, audittypes.NewRelatedResourceAttributes(
|
||||
typed.TargetResource(),
|
||||
targetID,
|
||||
typed.SourceResource(),
|
||||
sourceID,
|
||||
))
|
||||
}
|
||||
// A relation is recorded once from each side.
|
||||
attributesList = append(attributesList, audittypes.NewRelatedResourceAttributes(
|
||||
typed.TargetResource(),
|
||||
targetID,
|
||||
typed.SourceResource(),
|
||||
sourceID,
|
||||
))
|
||||
|
||||
for _, attributes := range attributesList {
|
||||
middleware.auditor.Audit(req.Context(), audittypes.NewAuditEventFromHTTPRequest(
|
||||
@@ -160,7 +161,6 @@ func (middleware *Audit) emitAuditEvent(req *http.Request, writer responseCaptur
|
||||
claims,
|
||||
attributes,
|
||||
errorType,
|
||||
errorCode,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -180,7 +180,6 @@ func (middleware *Audit) emitAuditEvent(req *http.Request, writer responseCaptur
|
||||
claims,
|
||||
attributes,
|
||||
errorType,
|
||||
errorCode,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -45,18 +44,6 @@ func Success(rw http.ResponseWriter, httpCode int, data interface{}) {
|
||||
_, _ = rw.Write(body)
|
||||
}
|
||||
|
||||
func ErrorCodeFromBody(body []byte) string {
|
||||
code := gjson.GetBytes(body, "error.code").String()
|
||||
|
||||
// This should never return empty since we only call this function on responses that were generated by us.
|
||||
// If it does return empty, the codebase has failed to use render package for error responses somewhere, and we should fix that instead of trying to handle it here.
|
||||
if code == "" {
|
||||
return errors.CodeUnset.String()
|
||||
}
|
||||
|
||||
return code
|
||||
}
|
||||
|
||||
func ErrorTypeFromStatusCode(statusCode int) string {
|
||||
// We are losing the exact type information here, but we can at least capture the error code and message for better observability.
|
||||
// To get the exact type, we would need some changes in the render package to include the error type in the response, which we can consider in the future if there is a need for it.
|
||||
|
||||
@@ -60,31 +60,6 @@ func TestSuccess(t *testing.T) {
|
||||
assert.Equal(t, expected, actual)
|
||||
}
|
||||
|
||||
func TestErrorCodeFromBody(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
body []byte
|
||||
wantCode string
|
||||
}{
|
||||
{
|
||||
name: "ValidErrorResponse",
|
||||
body: []byte(`{"status":"error","error":{"code":"authz_forbidden","message":"only admins can access this resource"}}`),
|
||||
wantCode: "authz_forbidden",
|
||||
},
|
||||
{
|
||||
name: "InvalidJSON",
|
||||
body: []byte(`not json`),
|
||||
wantCode: "unset",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.wantCode, ErrorCodeFromBody(testCase.body))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
listener, err := net.Listen("tcp", "localhost:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -34,16 +34,15 @@ func auditFieldKeyMap() map[string][]*telemetrytypes.TelemetryFieldKey {
|
||||
|
||||
return map[string][]*telemetrytypes.TelemetryFieldKey{
|
||||
"service.name": {key("service.name", res, str, false)},
|
||||
"signoz.audit.action": {key("signoz.audit.action", attr, str, true)},
|
||||
"signoz.audit.verb": {key("signoz.audit.verb", attr, str, true)},
|
||||
"signoz.audit.outcome": {key("signoz.audit.outcome", attr, str, true)},
|
||||
"signoz.audit.principal.email": {key("signoz.audit.principal.email", attr, str, true)},
|
||||
"signoz.audit.principal.id": {key("signoz.audit.principal.id", attr, str, true)},
|
||||
"signoz.audit.principal.type": {key("signoz.audit.principal.type", attr, str, true)},
|
||||
"signoz.audit.resource.kind": {key("signoz.audit.resource.kind", res, str, false)},
|
||||
"signoz.audit.resource.id": {key("signoz.audit.resource.id", res, str, false)},
|
||||
"signoz.principal.email": {key("signoz.principal.email", attr, str, true)},
|
||||
"signoz.principal.id": {key("signoz.principal.id", attr, str, true)},
|
||||
"signoz.principal.type": {key("signoz.principal.type", attr, str, true)},
|
||||
"signoz.resource.kind": {key("signoz.resource.kind", res, str, false)},
|
||||
"signoz.resource.id": {key("signoz.resource.id", res, str, false)},
|
||||
"signoz.audit.action_category": {key("signoz.audit.action_category", attr, str, false)},
|
||||
"signoz.audit.error.type": {key("signoz.audit.error.type", attr, str, false)},
|
||||
"signoz.audit.error.code": {key("signoz.audit.error.code", attr, str, false)},
|
||||
"exception.type": {key("exception.type", attr, str, false)},
|
||||
"http.request.method": {key("http.request.method", attr, str, false)},
|
||||
"http.response.status_code": {key("http.response.status_code", attr, i64, false)},
|
||||
}
|
||||
@@ -87,12 +86,12 @@ func TestStatementBuilder(t *testing.T) {
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Source: telemetrytypes.SourceAudit,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "signoz.audit.principal.id = '019a-1234-abcd-5678'",
|
||||
Expression: "signoz.principal.id = '019a-1234-abcd-5678'",
|
||||
},
|
||||
Limit: 100,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM signoz_audit.distributed_logs WHERE (`attribute_string_signoz$$audit$$principal$$id` = ? AND `attribute_string_signoz$$audit$$principal$$id_exists` = true) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM signoz_audit.distributed_logs WHERE (`attribute_string_signoz$$principal$$id` = ? AND `attribute_string_signoz$$principal$$id_exists` = true) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"019a-1234-abcd-5678", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
|
||||
},
|
||||
},
|
||||
@@ -121,13 +120,13 @@ func TestStatementBuilder(t *testing.T) {
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Source: telemetrytypes.SourceAudit,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "signoz.audit.resource.kind = 'dashboard' AND signoz.audit.resource.id = '019b-5678-efgh-9012'",
|
||||
Expression: "signoz.resource.kind = 'dashboard' AND signoz.resource.id = '019b-5678-efgh-9012'",
|
||||
},
|
||||
Limit: 100,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE ((simpleJSONExtractString(labels, 'signoz.audit.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?) AND (simpleJSONExtractString(labels, 'signoz.audit.resource.id') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"dashboard", "%signoz.audit.resource.kind%", "%signoz.audit.resource.kind\":\"dashboard%", "019b-5678-efgh-9012", "%signoz.audit.resource.id%", "%signoz.audit.resource.id\":\"019b-5678-efgh-9012%", uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE ((simpleJSONExtractString(labels, 'signoz.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?) AND (simpleJSONExtractString(labels, 'signoz.resource.id') = ? AND labels LIKE ? AND labels LIKE ?)) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"dashboard", "%signoz.resource.kind%", "%signoz.resource.kind\":\"dashboard%", "019b-5678-efgh-9012", "%signoz.resource.id%", "%signoz.resource.id\":\"019b-5678-efgh-9012%", uint64(1747945619), uint64(1747983448), "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
|
||||
},
|
||||
},
|
||||
// List: all dashboard deletions (compliance — resource.kind + action AND)
|
||||
@@ -138,13 +137,13 @@ func TestStatementBuilder(t *testing.T) {
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Source: telemetrytypes.SourceAudit,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "signoz.audit.resource.kind = 'dashboard' AND signoz.audit.action = 'delete'",
|
||||
Expression: "signoz.resource.kind = 'dashboard' AND signoz.audit.verb = 'delete'",
|
||||
},
|
||||
Limit: 100,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE (simpleJSONExtractString(labels, 'signoz.audit.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (`attribute_string_signoz$$audit$$action` = ? AND `attribute_string_signoz$$audit$$action_exists` = true) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"dashboard", "%signoz.audit.resource.kind%", "%signoz.audit.resource.kind\":\"dashboard%", uint64(1747945619), uint64(1747983448), "delete", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
|
||||
Query: "WITH __resource_filter AS (SELECT fingerprint FROM signoz_audit.distributed_logs_resource WHERE (simpleJSONExtractString(labels, 'signoz.resource.kind') = ? AND labels LIKE ? AND labels LIKE ?) AND seen_at_ts_bucket_start >= ? AND seen_at_ts_bucket_start <= ? GROUP BY fingerprint) SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM signoz_audit.distributed_logs WHERE resource_fingerprint GLOBAL IN (SELECT fingerprint FROM __resource_filter) AND (`attribute_string_signoz$$audit$$verb` = ? AND `attribute_string_signoz$$audit$$verb_exists` = true) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"dashboard", "%signoz.resource.kind%", "%signoz.resource.kind\":\"dashboard%", uint64(1747945619), uint64(1747983448), "delete", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
|
||||
},
|
||||
},
|
||||
// List: all actions by service accounts (materialized principal.type)
|
||||
@@ -155,12 +154,12 @@ func TestStatementBuilder(t *testing.T) {
|
||||
Signal: telemetrytypes.SignalLogs,
|
||||
Source: telemetrytypes.SourceAudit,
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "signoz.audit.principal.type = 'service_account'",
|
||||
Expression: "signoz.principal.type = 'service_account'",
|
||||
},
|
||||
Limit: 100,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM signoz_audit.distributed_logs WHERE (`attribute_string_signoz$$audit$$principal$$type` = ? AND `attribute_string_signoz$$audit$$principal$$type_exists` = true) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Query: "SELECT timestamp, id, trace_id, span_id, trace_flags, severity_text, severity_number, scope_name, scope_version, body, event_name, attributes_string, attributes_number, attributes_bool, resource, scope_string FROM signoz_audit.distributed_logs WHERE (`attribute_string_signoz$$principal$$type` = ? AND `attribute_string_signoz$$principal$$type_exists` = true) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? LIMIT ?",
|
||||
Args: []any{"service_account", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 100},
|
||||
},
|
||||
},
|
||||
@@ -173,14 +172,14 @@ func TestStatementBuilder(t *testing.T) {
|
||||
Source: telemetrytypes.SourceAudit,
|
||||
StepInterval: qbtypes.Step{Duration: 60 * time.Second},
|
||||
Filter: &qbtypes.Filter{
|
||||
Expression: "signoz.audit.outcome = 'failure' AND signoz.audit.action = 'update'",
|
||||
Expression: "signoz.audit.outcome = 'failure' AND signoz.audit.verb = 'update'",
|
||||
},
|
||||
Aggregations: []qbtypes.LogAggregation{
|
||||
{Expression: "count()"},
|
||||
},
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "SELECT count() AS __result_0 FROM signoz_audit.distributed_logs WHERE ((`attribute_string_signoz$$audit$$outcome` = ? AND `attribute_string_signoz$$audit$$outcome_exists` = true) AND (`attribute_string_signoz$$audit$$action` = ? AND `attribute_string_signoz$$audit$$action_exists` = true)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY __result_0 DESC",
|
||||
Query: "SELECT count() AS __result_0 FROM signoz_audit.distributed_logs WHERE ((`attribute_string_signoz$$audit$$outcome` = ? AND `attribute_string_signoz$$audit$$outcome_exists` = true) AND (`attribute_string_signoz$$audit$$verb` = ? AND `attribute_string_signoz$$audit$$verb_exists` = true)) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? ORDER BY __result_0 DESC",
|
||||
Args: []any{"failure", "update", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
|
||||
},
|
||||
},
|
||||
@@ -199,12 +198,12 @@ func TestStatementBuilder(t *testing.T) {
|
||||
Expression: "signoz.audit.outcome = 'failure'",
|
||||
},
|
||||
GroupBy: []qbtypes.GroupByKey{
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "signoz.audit.principal.email"}},
|
||||
{TelemetryFieldKey: telemetrytypes.TelemetryFieldKey{Name: "signoz.principal.email"}},
|
||||
},
|
||||
Limit: 5,
|
||||
},
|
||||
expected: qbtypes.Statement{
|
||||
Query: "WITH __limit_cte AS (SELECT toString(multiIf(`attribute_string_signoz$$audit$$principal$$email_exists` = true, `attribute_string_signoz$$audit$$principal$$email`, NULL)) AS `signoz.audit.principal.email`, count() AS __result_0 FROM signoz_audit.distributed_logs WHERE (`attribute_string_signoz$$audit$$outcome` = ? AND `attribute_string_signoz$$audit$$outcome_exists` = true) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `signoz.audit.principal.email` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toString(multiIf(`attribute_string_signoz$$audit$$principal$$email_exists` = true, `attribute_string_signoz$$audit$$principal$$email`, NULL)) AS `signoz.audit.principal.email`, count() AS __result_0 FROM signoz_audit.distributed_logs WHERE (`attribute_string_signoz$$audit$$outcome` = ? AND `attribute_string_signoz$$audit$$outcome_exists` = true) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`signoz.audit.principal.email`) GLOBAL IN (SELECT `signoz.audit.principal.email` FROM __limit_cte) GROUP BY ts, `signoz.audit.principal.email`",
|
||||
Query: "WITH __limit_cte AS (SELECT toString(multiIf(`attribute_string_signoz$$principal$$email_exists` = true, `attribute_string_signoz$$principal$$email`, NULL)) AS `signoz.principal.email`, count() AS __result_0 FROM signoz_audit.distributed_logs WHERE (`attribute_string_signoz$$audit$$outcome` = ? AND `attribute_string_signoz$$audit$$outcome_exists` = true) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? GROUP BY `signoz.principal.email` ORDER BY __result_0 DESC LIMIT ?) SELECT toStartOfInterval(fromUnixTimestamp64Nano(timestamp), INTERVAL 60 SECOND) AS ts, toString(multiIf(`attribute_string_signoz$$principal$$email_exists` = true, `attribute_string_signoz$$principal$$email`, NULL)) AS `signoz.principal.email`, count() AS __result_0 FROM signoz_audit.distributed_logs WHERE (`attribute_string_signoz$$audit$$outcome` = ? AND `attribute_string_signoz$$audit$$outcome_exists` = true) AND timestamp >= ? AND ts_bucket_start >= ? AND timestamp < ? AND ts_bucket_start <= ? AND (`signoz.principal.email`) GLOBAL IN (SELECT `signoz.principal.email` FROM __limit_cte) GROUP BY ts, `signoz.principal.email`",
|
||||
Args: []any{"failure", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448), 5, "failure", "1747947419000000000", uint64(1747945619), "1747983448000000000", uint64(1747983448)},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package audittypes
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
sigsemconv "github.com/SigNoz/signoz-semantic-conventions/semconv/go"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
@@ -16,10 +19,9 @@ type AuditAttributes struct {
|
||||
Action coretypes.Verb // guaranteed to be present
|
||||
ActionCategory coretypes.ActionCategory // guaranteed to be present
|
||||
Outcome Outcome // guaranteed to be present
|
||||
IdentNProvider authtypes.IdentNProvider
|
||||
}
|
||||
|
||||
func NewAuditAttributesFromHTTP(statusCode int, action coretypes.Verb, category coretypes.ActionCategory, claims authtypes.Claims) AuditAttributes {
|
||||
func NewAuditAttributesFromHTTP(statusCode int, action coretypes.Verb, category coretypes.ActionCategory) AuditAttributes {
|
||||
outcome := OutcomeFailure
|
||||
if statusCode >= 200 && statusCode < 400 {
|
||||
outcome = OutcomeSuccess
|
||||
@@ -29,15 +31,13 @@ func NewAuditAttributesFromHTTP(statusCode int, action coretypes.Verb, category
|
||||
Action: action,
|
||||
ActionCategory: category,
|
||||
Outcome: outcome,
|
||||
IdentNProvider: claims.IdentNProvider,
|
||||
}
|
||||
}
|
||||
|
||||
func (attributes AuditAttributes) Put(dest pcommon.Map) {
|
||||
dest.PutStr("signoz.audit.action", attributes.Action.StringValue())
|
||||
dest.PutStr("signoz.audit.action_category", attributes.ActionCategory.StringValue())
|
||||
dest.PutStr("signoz.audit.outcome", attributes.Outcome.StringValue())
|
||||
putStrIfNotEmpty(dest, "signoz.audit.identn_provider", attributes.IdentNProvider.StringValue())
|
||||
dest.PutStr(sigsemconv.SigNozAuditVerbKey, attributes.Action.StringValue())
|
||||
dest.PutStr(sigsemconv.SigNozAuditActionCategoryKey, attributes.ActionCategory.StringValue())
|
||||
dest.PutStr(sigsemconv.SigNozAuditOutcomeKey, attributes.Outcome.StringValue())
|
||||
}
|
||||
|
||||
// Audit attributes — Principal (Who).
|
||||
@@ -50,6 +50,9 @@ type PrincipalAttributes struct {
|
||||
|
||||
func NewPrincipalAttributesFromClaims(claims authtypes.Claims) PrincipalAttributes {
|
||||
principalID, _ := valuer.NewUUID(claims.UserID)
|
||||
if claims.Principal == authtypes.PrincipalServiceAccount {
|
||||
principalID, _ = valuer.NewUUID(claims.ServiceAccountID)
|
||||
}
|
||||
principalEmail, _ := valuer.NewEmail(claims.Email)
|
||||
principalOrgID, _ := valuer.NewUUID(claims.OrgID)
|
||||
|
||||
@@ -62,14 +65,16 @@ func NewPrincipalAttributesFromClaims(claims authtypes.Claims) PrincipalAttribut
|
||||
}
|
||||
|
||||
func (attributes PrincipalAttributes) Put(dest pcommon.Map) {
|
||||
dest.PutStr("signoz.audit.principal.id", attributes.PrincipalID.StringValue())
|
||||
dest.PutStr("signoz.audit.principal.email", attributes.PrincipalEmail.String())
|
||||
dest.PutStr("signoz.audit.principal.type", attributes.PrincipalType.StringValue())
|
||||
dest.PutStr("signoz.audit.principal.org_id", attributes.PrincipalOrgID.StringValue())
|
||||
if !attributes.PrincipalID.IsZero() {
|
||||
dest.PutStr(sigsemconv.SigNozPrincipalIDKey, attributes.PrincipalID.StringValue())
|
||||
putStrIfNotEmpty(dest, sigsemconv.SigNozPrincipalTypeKey, attributes.PrincipalType.StringValue())
|
||||
}
|
||||
putStrIfNotEmpty(dest, sigsemconv.SigNozPrincipalEmailKey, attributes.PrincipalEmail.String())
|
||||
dest.PutStr(sigsemconv.SigNozOrganizationIDKey, attributes.PrincipalOrgID.StringValue())
|
||||
}
|
||||
|
||||
// Audit attributes — Resource (On What).
|
||||
// These are OTel resource attributes (placed on the Resource, not event attributes).
|
||||
// The resource goes on the OTel Resource; the target goes on the log record.
|
||||
type ResourceAttributes struct {
|
||||
Resource coretypes.Resource // guaranteed to be present
|
||||
ResourceID string
|
||||
@@ -102,40 +107,42 @@ func NewRelatedResourceAttributes(resource coretypes.Resource, resourceID string
|
||||
// These are resource-level attributes (stored in the resource JSON column),
|
||||
// not event-level attributes (stored in attributes_string).
|
||||
func (attributes ResourceAttributes) PutResource(orgID valuer.UUID, dest pcommon.Map) {
|
||||
putStrIfNotEmpty(dest, "signoz.audit.resource.kind", attributes.Resource.Kind().String())
|
||||
putStrIfNotEmpty(dest, "signoz.audit.resource.id", attributes.ResourceID)
|
||||
putStrIfNotEmpty(dest, sigsemconv.SigNozResourceKindKey, attributes.Resource.Kind().String())
|
||||
putStrIfNotEmpty(dest, sigsemconv.SigNozResourceIDKey, attributes.ResourceID)
|
||||
if attributes.ResourceID != "" {
|
||||
putStrIfNotEmpty(dest, "signoz.audit.resource.object", attributes.Resource.Object(orgID, attributes.ResourceID))
|
||||
putStrIfNotEmpty(dest, sigsemconv.SigNozResourceObjectKey, attributes.Resource.Object(orgID, attributes.ResourceID))
|
||||
}
|
||||
}
|
||||
|
||||
// Put writes the target and the ingestion key the resource belongs to on the log record.
|
||||
func (attributes ResourceAttributes) Put(orgID valuer.UUID, dest pcommon.Map) {
|
||||
if attributes.TargetResource != nil {
|
||||
putStrIfNotEmpty(dest, "signoz.audit.resource.target.kind", attributes.TargetResource.Kind().String())
|
||||
putStrIfNotEmpty(dest, "signoz.audit.resource.target.id", attributes.TargetResourceID)
|
||||
putStrIfNotEmpty(dest, sigsemconv.SigNozResourceTargetKindKey, attributes.TargetResource.Kind().String())
|
||||
putStrIfNotEmpty(dest, sigsemconv.SigNozResourceTargetIDKey, attributes.TargetResourceID)
|
||||
if attributes.TargetResourceID != "" {
|
||||
putStrIfNotEmpty(dest, "signoz.audit.resource.target.object", attributes.TargetResource.Object(orgID, attributes.TargetResourceID))
|
||||
putStrIfNotEmpty(dest, sigsemconv.SigNozResourceTargetObjectKey, attributes.TargetResource.Object(orgID, attributes.TargetResourceID))
|
||||
}
|
||||
}
|
||||
|
||||
putStrIfNotEmpty(dest, sigsemconv.SigNozWorkspaceKeyIDKey, attributes.ingestionKeyID())
|
||||
}
|
||||
|
||||
// Audit attributes — Error (When outcome is failure)
|
||||
// Error messages are intentionally excluded to avoid leaking sensitive or
|
||||
// PII data into audit logs. The error type and code are sufficient for
|
||||
// filtering and alerting; investigators can correlate via trace ID.
|
||||
// PII data into audit logs. The error type is sufficient for filtering and
|
||||
// alerting; investigators can correlate via trace ID.
|
||||
type ErrorAttributes struct {
|
||||
ErrorType string
|
||||
ErrorCode string
|
||||
}
|
||||
|
||||
func NewErrorAttributes(errorType, errorCode string) ErrorAttributes {
|
||||
func NewErrorAttributes(errorType string) ErrorAttributes {
|
||||
return ErrorAttributes{
|
||||
ErrorType: errorType,
|
||||
ErrorCode: errorCode,
|
||||
}
|
||||
}
|
||||
|
||||
func (attributes ErrorAttributes) Put(dest pcommon.Map) {
|
||||
putStrIfNotEmpty(dest, "signoz.audit.error.type", attributes.ErrorType)
|
||||
putStrIfNotEmpty(dest, "signoz.audit.error.code", attributes.ErrorCode)
|
||||
putStrIfNotEmpty(dest, string(semconv.ExceptionTypeKey), attributes.ErrorType)
|
||||
}
|
||||
|
||||
// Audit attributes — Transport Context (Where/How).
|
||||
@@ -148,6 +155,7 @@ type TransportAttributes struct {
|
||||
HTTPStatusCode int
|
||||
URLPath string
|
||||
ClientAddress string
|
||||
ClientPort int
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
@@ -157,15 +165,18 @@ func NewTransportAttributesFromHTTP(req *http.Request, route string, statusCode
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
clientAddress, clientPort := splitHostPort(req.RemoteAddr)
|
||||
|
||||
return TransportAttributes{
|
||||
NetworkProtocolName: "http",
|
||||
NetworkProtocolVersion: req.Proto,
|
||||
NetworkProtocolVersion: networkProtocolVersion(req),
|
||||
URLScheme: scheme,
|
||||
HTTPMethod: req.Method,
|
||||
HTTPRoute: route,
|
||||
HTTPStatusCode: statusCode,
|
||||
URLPath: req.URL.Path,
|
||||
ClientAddress: req.RemoteAddr,
|
||||
ClientAddress: clientAddress,
|
||||
ClientPort: clientPort,
|
||||
UserAgent: req.UserAgent(),
|
||||
}
|
||||
}
|
||||
@@ -181,9 +192,48 @@ func (attributes TransportAttributes) Put(dest pcommon.Map) {
|
||||
}
|
||||
putStrIfNotEmpty(dest, string(semconv.URLPathKey), attributes.URLPath)
|
||||
putStrIfNotEmpty(dest, string(semconv.ClientAddressKey), attributes.ClientAddress)
|
||||
if attributes.ClientPort != 0 {
|
||||
dest.PutInt(string(semconv.ClientPortKey), int64(attributes.ClientPort))
|
||||
}
|
||||
putStrIfNotEmpty(dest, string(semconv.UserAgentOriginalKey), attributes.UserAgent)
|
||||
}
|
||||
|
||||
func (attributes ResourceAttributes) ingestionKeyID() string {
|
||||
if attributes.Resource.Kind() == coretypes.KindIngestionKey {
|
||||
return attributes.ResourceID
|
||||
}
|
||||
|
||||
if attributes.Resource.Kind() == coretypes.KindIngestionLimit && attributes.TargetResource != nil && attributes.TargetResource.Kind() == coretypes.KindIngestionKey {
|
||||
return attributes.TargetResourceID
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// networkProtocolVersion drops the HTTP/ prefix, and the .0 of HTTP/2 and later.
|
||||
func networkProtocolVersion(req *http.Request) string {
|
||||
if req.ProtoMajor >= 2 && req.ProtoMinor == 0 {
|
||||
return strconv.Itoa(req.ProtoMajor)
|
||||
}
|
||||
|
||||
return strconv.Itoa(req.ProtoMajor) + "." + strconv.Itoa(req.ProtoMinor)
|
||||
}
|
||||
|
||||
// splitHostPort returns addr unchanged with port 0 when it has no valid port.
|
||||
func splitHostPort(addr string) (string, int) {
|
||||
host, portStr, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return addr, 0
|
||||
}
|
||||
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil {
|
||||
return addr, 0
|
||||
}
|
||||
|
||||
return host, port
|
||||
}
|
||||
|
||||
func putStrIfNotEmpty(attrs pcommon.Map, key, value string) {
|
||||
if value != "" {
|
||||
attrs.PutStr(key, value)
|
||||
@@ -227,9 +277,13 @@ func newBody(auditAttributes AuditAttributes, principalAttributes PrincipalAttri
|
||||
b.WriteString(")")
|
||||
}
|
||||
|
||||
// Target (attach/detach context): " · target kind (id)" or " · target kind".
|
||||
// Target (attach/detach context): " to target kind (id)" or " from target kind" for detach.
|
||||
if resourceAttributes.TargetResource != nil {
|
||||
b.WriteString(" to ")
|
||||
if auditAttributes.Action == coretypes.VerbDetach {
|
||||
b.WriteString(" from ")
|
||||
} else {
|
||||
b.WriteString(" to ")
|
||||
}
|
||||
b.WriteString(resourceAttributes.TargetResource.Kind().String())
|
||||
if resourceAttributes.TargetResourceID != "" {
|
||||
b.WriteString(" (")
|
||||
@@ -238,25 +292,10 @@ func newBody(auditAttributes AuditAttributes, principalAttributes PrincipalAttri
|
||||
}
|
||||
}
|
||||
|
||||
// Error suffix (failure only): ": type (code)" or ": type" or ": (code)" or omitted.
|
||||
if auditAttributes.Outcome == OutcomeFailure {
|
||||
errorType := errorAttributes.ErrorType
|
||||
errorCode := errorAttributes.ErrorCode
|
||||
if errorType != "" || errorCode != "" {
|
||||
b.WriteString(": ")
|
||||
if errorType != "" && errorCode != "" {
|
||||
b.WriteString(errorType)
|
||||
b.WriteString(" (")
|
||||
b.WriteString(errorCode)
|
||||
b.WriteString(")")
|
||||
} else if errorType != "" {
|
||||
b.WriteString(errorType)
|
||||
} else {
|
||||
b.WriteString("(")
|
||||
b.WriteString(errorCode)
|
||||
b.WriteString(")")
|
||||
}
|
||||
}
|
||||
// Error suffix (failure only): ": type" or omitted.
|
||||
if auditAttributes.Outcome == OutcomeFailure && errorAttributes.ErrorType != "" {
|
||||
b.WriteString(": ")
|
||||
b.WriteString(errorAttributes.ErrorType)
|
||||
}
|
||||
|
||||
return b.String()
|
||||
|
||||
@@ -3,15 +3,12 @@ package audittypes
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewAuditAttributesFromHTTP_OutcomeBoundary(t *testing.T) {
|
||||
claims := authtypes.Claims{IdentNProvider: authtypes.IdentNProviderTokenizer}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
statusCode int
|
||||
@@ -36,7 +33,7 @@ func TestNewAuditAttributesFromHTTP_OutcomeBoundary(t *testing.T) {
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
attrs := NewAuditAttributesFromHTTP(testCase.statusCode, coretypes.VerbUpdate, coretypes.ActionCategoryConfigurationChange, claims)
|
||||
attrs := NewAuditAttributesFromHTTP(testCase.statusCode, coretypes.VerbUpdate, coretypes.ActionCategoryConfigurationChange)
|
||||
assert.Equal(t, testCase.expectedOutcome, attrs.Outcome)
|
||||
})
|
||||
}
|
||||
@@ -154,9 +151,8 @@ func TestNewBody(t *testing.T) {
|
||||
},
|
||||
errorAttributes: ErrorAttributes{
|
||||
ErrorType: "forbidden",
|
||||
ErrorCode: "authz_forbidden",
|
||||
},
|
||||
expectedBody: "viewer@acme.com (019aaaaa-bbbb-7000-8000-cccc00000002) failed to update dashboard (019b-5678): forbidden (authz_forbidden)",
|
||||
expectedBody: "viewer@acme.com (019aaaaa-bbbb-7000-8000-cccc00000002) failed to update dashboard (019b-5678): forbidden",
|
||||
},
|
||||
{
|
||||
name: "Failure_ErrorTypeOnly",
|
||||
@@ -193,6 +189,39 @@ func TestNewBody(t *testing.T) {
|
||||
errorAttributes: ErrorAttributes{},
|
||||
expectedBody: "test@acme.com (019a1234-abcd-7000-8000-567800000001) failed to create dashboard (019b-5678)",
|
||||
},
|
||||
{
|
||||
name: "Success_Attach",
|
||||
auditAttributes: AuditAttributes{
|
||||
Action: coretypes.VerbAttach,
|
||||
Outcome: OutcomeSuccess,
|
||||
},
|
||||
principalAttributes: PrincipalAttributes{},
|
||||
resourceAttributes: NewRelatedResourceAttributes(coretypes.ResourceRole, "role-1", coretypes.ResourceServiceAccount, "sa-1"),
|
||||
errorAttributes: ErrorAttributes{},
|
||||
expectedBody: "attached role (role-1) to serviceaccount (sa-1)",
|
||||
},
|
||||
{
|
||||
name: "Success_Detach",
|
||||
auditAttributes: AuditAttributes{
|
||||
Action: coretypes.VerbDetach,
|
||||
Outcome: OutcomeSuccess,
|
||||
},
|
||||
principalAttributes: PrincipalAttributes{},
|
||||
resourceAttributes: NewRelatedResourceAttributes(coretypes.ResourceRole, "role-1", coretypes.ResourceServiceAccount, "sa-1"),
|
||||
errorAttributes: ErrorAttributes{},
|
||||
expectedBody: "detached role (role-1) from serviceaccount (sa-1)",
|
||||
},
|
||||
{
|
||||
name: "Failure_Detach",
|
||||
auditAttributes: AuditAttributes{
|
||||
Action: coretypes.VerbDetach,
|
||||
Outcome: OutcomeFailure,
|
||||
},
|
||||
principalAttributes: PrincipalAttributes{},
|
||||
resourceAttributes: NewRelatedResourceAttributes(coretypes.ResourceRole, "role-1", coretypes.ResourceServiceAccount, "sa-1"),
|
||||
errorAttributes: ErrorAttributes{},
|
||||
expectedBody: "failed to detach role (role-1) from serviceaccount (sa-1)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
|
||||
@@ -57,11 +57,10 @@ func NewAuditEventFromHTTPRequest(
|
||||
claims authtypes.Claims,
|
||||
resourceAttributes ResourceAttributes,
|
||||
errorType string,
|
||||
errorCode string,
|
||||
) AuditEvent {
|
||||
auditAttributes := NewAuditAttributesFromHTTP(statusCode, action, actionCategory, claims)
|
||||
auditAttributes := NewAuditAttributesFromHTTP(statusCode, action, actionCategory)
|
||||
principalAttributes := NewPrincipalAttributesFromClaims(claims)
|
||||
errorAttributes := NewErrorAttributes(errorType, errorCode)
|
||||
errorAttributes := NewErrorAttributes(errorType)
|
||||
transportAttributes := NewTransportAttributesFromHTTP(req, route, statusCode)
|
||||
|
||||
return AuditEvent{
|
||||
@@ -69,7 +68,7 @@ func NewAuditEventFromHTTPRequest(
|
||||
TraceID: traceID,
|
||||
SpanID: spanID,
|
||||
Body: newBody(auditAttributes, principalAttributes, resourceAttributes, errorAttributes),
|
||||
EventName: NewEventName(resourceAttributes.Resource.Kind(), auditAttributes.Action),
|
||||
EventName: NewEventName(auditAttributes.Action),
|
||||
AuditAttributes: auditAttributes,
|
||||
PrincipalAttributes: principalAttributes,
|
||||
ResourceAttributes: resourceAttributes,
|
||||
@@ -148,6 +147,8 @@ func (event AuditEvent) ToLogRecord(dest plog.LogRecord) {
|
||||
// Resource attributes are set at the ResourceLogs level, not per-LogRecord.
|
||||
// See NewPLogsFromAuditEvents.
|
||||
|
||||
event.ResourceAttributes.Put(event.PrincipalAttributes.PrincipalOrgID, attrs)
|
||||
|
||||
// Error attributes
|
||||
event.ErrorAttributes.Put(attrs)
|
||||
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
package audittypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
import (
|
||||
sigsemconv "github.com/SigNoz/signoz-semantic-conventions/semconv/go"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
)
|
||||
|
||||
var eventNames = map[coretypes.Verb]EventName{
|
||||
coretypes.VerbCreate: {s: sigsemconv.SigNozAuditCreateEventName},
|
||||
coretypes.VerbUpdate: {s: sigsemconv.SigNozAuditUpdateEventName},
|
||||
coretypes.VerbDelete: {s: sigsemconv.SigNozAuditDeleteEventName},
|
||||
coretypes.VerbAttach: {s: sigsemconv.SigNozAuditAttachEventName},
|
||||
coretypes.VerbDetach: {s: sigsemconv.SigNozAuditDetachEventName},
|
||||
}
|
||||
|
||||
// EventName is a typed wrapper for audit event names, ensuring not every
|
||||
// string qualifies as an event name.
|
||||
@@ -8,15 +19,16 @@ type EventName struct {
|
||||
s string
|
||||
}
|
||||
|
||||
// NewEventName derives the audit event name from a resource kind and action.
|
||||
// Format: {resource_kind}.{pastTense(action)}
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// NewEventName("dashboard", coretypes.VerbCreate) → "dashboard.created"
|
||||
// NewEventName("dashboard", coretypes.VerbUpdate) → "dashboard.updated"
|
||||
func NewEventName(resourceKind coretypes.Kind, action coretypes.Verb) EventName {
|
||||
return EventName{s: resourceKind.String() + "." + action.PastTense()}
|
||||
// NewEventName returns the audit event name of an action, signoz.audit.{action}.
|
||||
// It is zero for an action that is not audited; see IsAudited.
|
||||
func NewEventName(action coretypes.Verb) EventName {
|
||||
return eventNames[action]
|
||||
}
|
||||
|
||||
// IsAudited reports whether an action records an audit event.
|
||||
func IsAudited(action coretypes.Verb) bool {
|
||||
_, ok := eventNames[action]
|
||||
return ok
|
||||
}
|
||||
|
||||
// String returns the string representation of the event name.
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/coretypes"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.opentelemetry.io/collector/pdata/plog"
|
||||
oteltrace "go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
@@ -31,7 +33,6 @@ func TestNewAuditEvent(t *testing.T) {
|
||||
resource coretypes.Resource
|
||||
resourceID string
|
||||
errorType string
|
||||
errorCode string
|
||||
expectedOutcome Outcome
|
||||
expectedBody string
|
||||
}{
|
||||
@@ -61,9 +62,8 @@ func TestNewAuditEvent(t *testing.T) {
|
||||
resource: testDashboardResource,
|
||||
resourceID: "019b-5678-efgh-9012",
|
||||
errorType: "forbidden",
|
||||
errorCode: "authz_forbidden",
|
||||
expectedOutcome: OutcomeFailure,
|
||||
expectedBody: "viewer@acme.com (019aaaaa-bbbb-7000-8000-cccc00000002) failed to update dashboard (019b-5678-efgh-9012): forbidden (authz_forbidden)",
|
||||
expectedBody: "viewer@acme.com (019aaaaa-bbbb-7000-8000-cccc00000002) failed to update dashboard (019b-5678-efgh-9012): forbidden",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@ func TestNewAuditEvent(t *testing.T) {
|
||||
testCase.claims,
|
||||
NewResourceAttributes(testCase.resource, testCase.resourceID),
|
||||
testCase.errorType,
|
||||
testCase.errorCode,
|
||||
)
|
||||
|
||||
assert.Equal(t, testCase.expectedOutcome, event.AuditAttributes.Outcome)
|
||||
@@ -97,7 +96,6 @@ func TestNewAuditEvent(t *testing.T) {
|
||||
assert.Equal(t, traceID, event.TraceID)
|
||||
assert.Equal(t, spanID, event.SpanID)
|
||||
assert.Equal(t, testCase.errorType, event.ErrorAttributes.ErrorType)
|
||||
assert.Equal(t, testCase.errorCode, event.ErrorAttributes.ErrorCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -105,7 +103,7 @@ func TestNewAuditEvent(t *testing.T) {
|
||||
func newTestEvent(resource coretypes.Resource, resourceID string, action coretypes.Verb) AuditEvent {
|
||||
return AuditEvent{
|
||||
Body: resource.Kind().String() + "." + action.PastTense(),
|
||||
EventName: NewEventName(resource.Kind(), action),
|
||||
EventName: NewEventName(action),
|
||||
AuditAttributes: AuditAttributes{
|
||||
Action: action,
|
||||
ActionCategory: coretypes.ActionCategoryConfigurationChange,
|
||||
@@ -210,11 +208,11 @@ func TestNewPLogsFromAuditEvents(t *testing.T) {
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, "0.90.0", serviceVersion.Str())
|
||||
|
||||
kind, exists := resourceAttrs.Get("signoz.audit.resource.kind")
|
||||
kind, exists := resourceAttrs.Get("signoz.resource.kind")
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, testCase.expectedResourceKinds[i], kind.Str())
|
||||
|
||||
id, exists := resourceAttrs.Get("signoz.audit.resource.id")
|
||||
id, exists := resourceAttrs.Get("signoz.resource.id")
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, testCase.expectedResourceIDs[i], id.Str())
|
||||
|
||||
@@ -225,8 +223,8 @@ func TestNewPLogsFromAuditEvents(t *testing.T) {
|
||||
|
||||
for j := 0; j < resourceLogs.ScopeLogs().At(0).LogRecords().Len(); j++ {
|
||||
recordAttrs := resourceLogs.ScopeLogs().At(0).LogRecords().At(j).Attributes()
|
||||
_, hasKind := recordAttrs.Get("signoz.audit.resource.kind")
|
||||
_, hasID := recordAttrs.Get("signoz.audit.resource.id")
|
||||
_, hasKind := recordAttrs.Get("signoz.resource.kind")
|
||||
_, hasID := recordAttrs.Get("signoz.resource.id")
|
||||
assert.False(t, hasKind, "resource.kind must not be in log record attributes")
|
||||
assert.False(t, hasID, "resource.id must not be in log record attributes")
|
||||
}
|
||||
@@ -234,3 +232,158 @@ func TestNewPLogsFromAuditEvents(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewEventName(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
action coretypes.Verb
|
||||
expected string
|
||||
expectedAudited bool
|
||||
}{
|
||||
{name: "Create", action: coretypes.VerbCreate, expected: "signoz.audit.create", expectedAudited: true},
|
||||
{name: "Detach", action: coretypes.VerbDetach, expected: "signoz.audit.detach", expectedAudited: true},
|
||||
{name: "Read_NotAudited", action: coretypes.VerbRead, expected: "", expectedAudited: false},
|
||||
{name: "List_NotAudited", action: coretypes.VerbList, expected: "", expectedAudited: false},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.expected, NewEventName(testCase.action).String())
|
||||
assert.Equal(t, testCase.expectedAudited, IsAudited(testCase.action))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuditEventToLogRecord(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
claims authtypes.Claims
|
||||
resourceAttributes ResourceAttributes
|
||||
statusCode int
|
||||
errorType string
|
||||
remoteAddr string
|
||||
proto string
|
||||
protoMajor int
|
||||
protoMinor int
|
||||
expectedAttributes map[string]any
|
||||
absentAttributes []string
|
||||
}{
|
||||
{
|
||||
name: "Success_User_HTTP11",
|
||||
claims: authtypes.Claims{UserID: "019a1234-abcd-7000-8000-567800000001", Principal: authtypes.PrincipalUser, Email: "alice@acme.com", OrgID: "019a1234-abcd-7000-8000-567800000002"},
|
||||
resourceAttributes: NewResourceAttributes(testDashboardResource, "d-001"),
|
||||
statusCode: http.StatusOK,
|
||||
remoteAddr: "10.0.0.1:52341",
|
||||
proto: "HTTP/1.1",
|
||||
protoMajor: 1,
|
||||
protoMinor: 1,
|
||||
expectedAttributes: map[string]any{
|
||||
"signoz.audit.verb": "create",
|
||||
"signoz.audit.action_category": "configuration_changes",
|
||||
"signoz.audit.outcome": "success",
|
||||
"signoz.principal.id": "019a1234-abcd-7000-8000-567800000001",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.principal.email": "alice@acme.com",
|
||||
"signoz.organization.id": "019a1234-abcd-7000-8000-567800000002",
|
||||
"network.protocol.version": "1.1",
|
||||
"client.address": "10.0.0.1",
|
||||
"client.port": int64(52341),
|
||||
"url.path": "/api/v1/resource",
|
||||
},
|
||||
absentAttributes: []string{"exception.type", "signoz.workspace.key.id", "signoz.resource.target.kind", "signoz.audit.identn_provider"},
|
||||
},
|
||||
{
|
||||
name: "ServiceAccount_HTTP2",
|
||||
claims: authtypes.Claims{UserID: "00000000-0000-0000-0000-000000000000", ServiceAccountID: "019a1234-abcd-7000-8000-567800000003", Principal: authtypes.PrincipalServiceAccount, OrgID: "019a1234-abcd-7000-8000-567800000002"},
|
||||
resourceAttributes: NewResourceAttributes(testDashboardResource, "d-002"),
|
||||
statusCode: http.StatusOK,
|
||||
remoteAddr: "10.0.0.3:443",
|
||||
proto: "HTTP/2.0",
|
||||
protoMajor: 2,
|
||||
protoMinor: 0,
|
||||
expectedAttributes: map[string]any{
|
||||
"signoz.principal.id": "019a1234-abcd-7000-8000-567800000003",
|
||||
"signoz.principal.type": "service_account",
|
||||
"network.protocol.version": "2",
|
||||
},
|
||||
absentAttributes: []string{"signoz.principal.email"},
|
||||
},
|
||||
{
|
||||
name: "Failure_Anonymous_IngestionKey_AddressWithoutPort",
|
||||
claims: authtypes.Claims{OrgID: "019a1234-abcd-7000-8000-567800000002"},
|
||||
resourceAttributes: NewResourceAttributes(coretypes.ResourceMetaResourceIngestionKey, "key-001"),
|
||||
statusCode: http.StatusForbidden,
|
||||
errorType: "forbidden",
|
||||
remoteAddr: "10.0.0.2",
|
||||
proto: "HTTP/1.1",
|
||||
protoMajor: 1,
|
||||
protoMinor: 1,
|
||||
expectedAttributes: map[string]any{
|
||||
"signoz.audit.outcome": "failure",
|
||||
"exception.type": "forbidden",
|
||||
"signoz.workspace.key.id": "key-001",
|
||||
"client.address": "10.0.0.2",
|
||||
},
|
||||
absentAttributes: []string{"client.port", "signoz.principal.id", "signoz.principal.type", "signoz.principal.email"},
|
||||
},
|
||||
{
|
||||
name: "Attach_IngestionLimitToKey",
|
||||
claims: authtypes.Claims{UserID: "019a1234-abcd-7000-8000-567800000001", Principal: authtypes.PrincipalUser, OrgID: "019a1234-abcd-7000-8000-567800000002"},
|
||||
resourceAttributes: NewRelatedResourceAttributes(coretypes.ResourceMetaResourceIngestionLimit, "limit-001", coretypes.ResourceMetaResourceIngestionKey, "key-002"),
|
||||
statusCode: http.StatusOK,
|
||||
remoteAddr: "10.0.0.4:1234",
|
||||
proto: "HTTP/1.1",
|
||||
protoMajor: 1,
|
||||
protoMinor: 1,
|
||||
expectedAttributes: map[string]any{
|
||||
"signoz.resource.target.kind": "ingestion-key",
|
||||
"signoz.resource.target.id": "key-002",
|
||||
"signoz.workspace.key.id": "key-002",
|
||||
},
|
||||
absentAttributes: []string{"signoz.resource.kind", "signoz.resource.id"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/resource", nil)
|
||||
req.RemoteAddr = testCase.remoteAddr
|
||||
req.Proto, req.ProtoMajor, req.ProtoMinor = testCase.proto, testCase.protoMajor, testCase.protoMinor
|
||||
|
||||
event := NewAuditEventFromHTTPRequest(req, "/api/v1/resource", testCase.statusCode, oteltrace.TraceID{}, oteltrace.SpanID{}, coretypes.VerbCreate, coretypes.ActionCategoryConfigurationChange, testCase.claims, testCase.resourceAttributes, testCase.errorType)
|
||||
|
||||
record := plog.NewLogRecord()
|
||||
event.ToLogRecord(record)
|
||||
attrs := record.Attributes().AsRaw()
|
||||
|
||||
for key, value := range testCase.expectedAttributes {
|
||||
assert.Equal(t, value, attrs[key], key)
|
||||
}
|
||||
|
||||
for _, key := range testCase.absentAttributes {
|
||||
assert.NotContains(t, attrs, key)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewPLogsFromAuditEvents_TargetPerRecord(t *testing.T) {
|
||||
first := NewAuditEventFromHTTPRequest(httptest.NewRequest(http.MethodPost, "/", nil), "/", http.StatusOK, oteltrace.TraceID{}, oteltrace.SpanID{}, coretypes.VerbAttach, coretypes.ActionCategoryConfigurationChange, authtypes.Claims{}, NewRelatedResourceAttributes(testDashboardResource, "d-001", coretypes.ResourceMetaResourceRule, "r-001"), "")
|
||||
second := NewAuditEventFromHTTPRequest(httptest.NewRequest(http.MethodPost, "/", nil), "/", http.StatusOK, oteltrace.TraceID{}, oteltrace.SpanID{}, coretypes.VerbAttach, coretypes.ActionCategoryConfigurationChange, authtypes.Claims{}, NewRelatedResourceAttributes(testDashboardResource, "d-001", coretypes.ResourceMetaResourceRule, "r-002"), "")
|
||||
|
||||
logs := NewPLogsFromAuditEvents([]AuditEvent{first, second}, "signoz", "0.90.0", "signoz.audit")
|
||||
require.Equal(t, 1, logs.ResourceLogs().Len())
|
||||
|
||||
resourceLogs := logs.ResourceLogs().At(0)
|
||||
assert.NotContains(t, resourceLogs.Resource().Attributes().AsRaw(), "signoz.resource.target.id")
|
||||
|
||||
records := resourceLogs.ScopeLogs().At(0).LogRecords()
|
||||
require.Equal(t, 2, records.Len())
|
||||
|
||||
targetIDs := []any{}
|
||||
for i := 0; i < records.Len(); i++ {
|
||||
targetIDs = append(targetIDs, records.At(i).Attributes().AsRaw()["signoz.resource.target.id"])
|
||||
}
|
||||
|
||||
assert.Equal(t, []any{"r-001", "r-002"}, targetIDs)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package audittypes
|
||||
|
||||
import (
|
||||
sigsemconv "github.com/SigNoz/signoz-semantic-conventions/semconv/go"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"go.opentelemetry.io/collector/pdata/plog"
|
||||
)
|
||||
@@ -13,8 +14,8 @@ type Outcome struct {
|
||||
}
|
||||
|
||||
var (
|
||||
OutcomeSuccess = Outcome{valuer.NewString("success"), plog.SeverityNumberInfo, "INFO"}
|
||||
OutcomeFailure = Outcome{valuer.NewString("failure"), plog.SeverityNumberError, "ERROR"}
|
||||
OutcomeSuccess = Outcome{valuer.NewString(sigsemconv.SigNozAuditOutcomeSuccess), plog.SeverityNumberInfo, "INFO"}
|
||||
OutcomeFailure = Outcome{valuer.NewString(sigsemconv.SigNozAuditOutcomeFailure), plog.SeverityNumberError, "ERROR"}
|
||||
)
|
||||
|
||||
func (Outcome) Enum() []any {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package coretypes
|
||||
|
||||
import "github.com/SigNoz/signoz/pkg/valuer"
|
||||
import (
|
||||
sigsemconv "github.com/SigNoz/signoz-semantic-conventions/semconv/go"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
var (
|
||||
ActionCategoryAccessControl = ActionCategory{valuer.NewString("access_control")}
|
||||
ActionCategoryConfigurationChange = ActionCategory{valuer.NewString("configuration_change")}
|
||||
ActionCategoryAccessControl = ActionCategory{valuer.NewString(sigsemconv.SigNozAuditActionCategoryAccessControl)}
|
||||
ActionCategoryConfigurationChange = ActionCategory{valuer.NewString(sigsemconv.SigNozAuditActionCategoryConfigurationChanges)}
|
||||
ActionCategoryDataAccess = ActionCategory{valuer.NewString("data_access")}
|
||||
ActionCategorySystemEvent = ActionCategory{valuer.NewString("system_event")}
|
||||
)
|
||||
|
||||
@@ -30,55 +30,55 @@ def test_audit_list_all(
|
||||
timestamp=now - timedelta(seconds=3),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "alert-rule",
|
||||
"signoz.audit.resource.id": "alert-001",
|
||||
"signoz.resource.kind": "alert-rule",
|
||||
"signoz.resource.id": "alert-001",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-010",
|
||||
"signoz.audit.principal.email": "ops@acme.com",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "create",
|
||||
"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="alert-rule.created",
|
||||
event_name="signoz.audit.create",
|
||||
severity_text="INFO",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=2),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "saved-view",
|
||||
"signoz.audit.resource.id": "view-001",
|
||||
"signoz.resource.kind": "saved-view",
|
||||
"signoz.resource.id": "view-001",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-010",
|
||||
"signoz.audit.principal.email": "ops@acme.com",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "update",
|
||||
"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="saved-view.updated",
|
||||
event_name="signoz.audit.update",
|
||||
severity_text="INFO",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=1),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "user",
|
||||
"signoz.audit.resource.id": "user-020",
|
||||
"signoz.resource.kind": "user",
|
||||
"signoz.resource.id": "user-020",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-010",
|
||||
"signoz.audit.principal.email": "ops@acme.com",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "update",
|
||||
"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="user.role.changed",
|
||||
event_name="signoz.audit.update",
|
||||
severity_text="INFO",
|
||||
),
|
||||
]
|
||||
@@ -109,9 +109,12 @@ def test_audit_list_all(
|
||||
assert len(rows) == 3
|
||||
|
||||
# Most recent first
|
||||
assert rows[0]["data"]["event_name"] == "user.role.changed"
|
||||
assert rows[1]["data"]["event_name"] == "saved-view.updated"
|
||||
assert rows[2]["data"]["event_name"] == "alert-rule.created"
|
||||
assert [row["data"]["event_name"] for row in rows] == ["signoz.audit.update", "signoz.audit.update", "signoz.audit.create"]
|
||||
assert [row["data"]["body"] 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"] == "ops@acme.com (user-010) updated user (user-020)"
|
||||
@@ -122,33 +125,33 @@ def test_audit_list_all(
|
||||
"filter_expression,expected_count,expected_event_names",
|
||||
[
|
||||
pytest.param(
|
||||
"signoz.audit.principal.id = 'user-001'",
|
||||
"signoz.principal.id = 'user-001'",
|
||||
3,
|
||||
{"session.login", "dashboard.updated", "dashboard.created"},
|
||||
{"signoz.audit.update", "signoz.audit.create"},
|
||||
id="filter_by_principal_id",
|
||||
),
|
||||
pytest.param(
|
||||
"signoz.audit.outcome = 'failure'",
|
||||
1,
|
||||
{"dashboard.deleted"},
|
||||
{"signoz.audit.delete"},
|
||||
id="filter_by_outcome_failure",
|
||||
),
|
||||
pytest.param(
|
||||
"signoz.audit.resource.kind = 'dashboard' AND signoz.audit.resource.id = 'dash-001'",
|
||||
"signoz.resource.kind = 'dashboard' AND signoz.resource.id = 'dash-001'",
|
||||
3,
|
||||
{"dashboard.deleted", "dashboard.updated", "dashboard.created"},
|
||||
{"signoz.audit.delete", "signoz.audit.update", "signoz.audit.create"},
|
||||
id="filter_by_resource_kind_and_id",
|
||||
),
|
||||
pytest.param(
|
||||
"signoz.audit.principal.type = 'service_account'",
|
||||
"signoz.principal.type = 'service_account'",
|
||||
1,
|
||||
{"serviceaccount.apikey.created"},
|
||||
{"signoz.audit.create"},
|
||||
id="filter_by_principal_type",
|
||||
),
|
||||
pytest.param(
|
||||
"signoz.audit.resource.kind = 'dashboard' AND signoz.audit.action = 'delete'",
|
||||
"signoz.resource.kind = 'dashboard' AND signoz.audit.verb = 'delete'",
|
||||
1,
|
||||
{"dashboard.deleted"},
|
||||
{"signoz.audit.delete"},
|
||||
id="filter_by_resource_kind_and_action",
|
||||
),
|
||||
],
|
||||
@@ -170,94 +173,92 @@ def test_audit_filter(
|
||||
timestamp=now - timedelta(seconds=5),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "dashboard",
|
||||
"signoz.audit.resource.id": "dash-001",
|
||||
"signoz.resource.kind": "dashboard",
|
||||
"signoz.resource.id": "dash-001",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-001",
|
||||
"signoz.audit.principal.email": "alice@acme.com",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "create",
|
||||
"signoz.audit.action_category": "configuration_change",
|
||||
"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="dashboard.created",
|
||||
event_name="signoz.audit.create",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=4),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "dashboard",
|
||||
"signoz.audit.resource.id": "dash-001",
|
||||
"signoz.resource.kind": "dashboard",
|
||||
"signoz.resource.id": "dash-001",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-001",
|
||||
"signoz.audit.principal.email": "alice@acme.com",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "update",
|
||||
"signoz.audit.action_category": "configuration_change",
|
||||
"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="dashboard.updated",
|
||||
event_name="signoz.audit.update",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=3),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "dashboard",
|
||||
"signoz.audit.resource.id": "dash-001",
|
||||
"signoz.resource.kind": "dashboard",
|
||||
"signoz.resource.id": "dash-001",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-002",
|
||||
"signoz.audit.principal.email": "viewer@acme.com",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "delete",
|
||||
"signoz.audit.action_category": "configuration_change",
|
||||
"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",
|
||||
"signoz.audit.error.type": "forbidden",
|
||||
"signoz.audit.error.code": "authz_forbidden",
|
||||
"exception.type": "forbidden",
|
||||
},
|
||||
body="viewer@acme.com failed to delete dashboard",
|
||||
event_name="dashboard.deleted",
|
||||
event_name="signoz.audit.delete",
|
||||
severity_text="ERROR",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=2),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "serviceaccount",
|
||||
"signoz.audit.resource.id": "sa-001",
|
||||
"signoz.resource.kind": "serviceaccount",
|
||||
"signoz.resource.id": "sa-001",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "sa-001",
|
||||
"signoz.audit.principal.email": "",
|
||||
"signoz.audit.principal.type": "service_account",
|
||||
"signoz.audit.action": "create",
|
||||
"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="serviceaccount.apikey.created",
|
||||
event_name="signoz.audit.create",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=1),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "session",
|
||||
"signoz.audit.resource.id": "*",
|
||||
"signoz.resource.kind": "session",
|
||||
"signoz.resource.id": "*",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-001",
|
||||
"signoz.audit.principal.email": "alice@acme.com",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "login",
|
||||
"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 login session",
|
||||
event_name="session.login",
|
||||
body="alice@acme.com created session",
|
||||
event_name="signoz.audit.create",
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -304,51 +305,51 @@ def test_audit_scalar_count_failures(
|
||||
timestamp=now - timedelta(seconds=3),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "dashboard",
|
||||
"signoz.audit.resource.id": "dash-100",
|
||||
"signoz.resource.kind": "dashboard",
|
||||
"signoz.resource.id": "dash-100",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-050",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "delete",
|
||||
"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="dashboard.deleted",
|
||||
event_name="signoz.audit.delete",
|
||||
severity_text="ERROR",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=2),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "alert-rule",
|
||||
"signoz.audit.resource.id": "alert-200",
|
||||
"signoz.resource.kind": "alert-rule",
|
||||
"signoz.resource.id": "alert-200",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-060",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "update",
|
||||
"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="alert-rule.updated",
|
||||
event_name="signoz.audit.update",
|
||||
severity_text="ERROR",
|
||||
),
|
||||
AuditLog(
|
||||
timestamp=now - timedelta(seconds=1),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "dashboard",
|
||||
"signoz.audit.resource.id": "dash-100",
|
||||
"signoz.resource.kind": "dashboard",
|
||||
"signoz.resource.id": "dash-100",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-050",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "update",
|
||||
"signoz.principal.id": "user-050",
|
||||
"signoz.principal.type": "user",
|
||||
"signoz.audit.verb": "update",
|
||||
"signoz.audit.outcome": "success",
|
||||
},
|
||||
body="user-050 updated dashboard",
|
||||
event_name="dashboard.updated",
|
||||
event_name="signoz.audit.update",
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -394,17 +395,17 @@ def test_audit_does_not_leak_into_logs(
|
||||
timestamp=now - timedelta(seconds=1),
|
||||
resources={
|
||||
"service.name": "signoz",
|
||||
"signoz.audit.resource.kind": "organization",
|
||||
"signoz.audit.resource.id": "org-999",
|
||||
"signoz.resource.kind": "organization",
|
||||
"signoz.resource.id": "org-999",
|
||||
},
|
||||
attributes={
|
||||
"signoz.audit.principal.id": "user-admin",
|
||||
"signoz.audit.principal.type": "user",
|
||||
"signoz.audit.action": "update",
|
||||
"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="organization.updated",
|
||||
event_name="signoz.audit.update",
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -430,5 +431,5 @@ def test_audit_does_not_leak_into_logs(
|
||||
|
||||
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.action", "")]
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user