From 5adf0d628dcc853589c4979b73cbd70ad04802e5 Mon Sep 17 00:00:00 2001 From: Ben Brooks Date: Wed, 21 Aug 2024 10:06:43 +0100 Subject: [PATCH] CBG-4188: Add audit logging metrics (#7083) * Add audit stats placeholders * Add stat incrs * Add stat assertions to existing TestAuditLoggingFields * Stat descriptions --- base/logger_audit.go | 3 +++ base/stats.go | 34 ++++++++++++++++++++++++++++++++++ base/stats_descriptions.go | 7 +++++++ rest/audit_test.go | 24 +++++++++++++++++++++++- 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/base/logger_audit.go b/base/logger_audit.go index c2cd2e6907..d2130eadab 100644 --- a/base/logger_audit.go +++ b/base/logger_audit.go @@ -147,6 +147,7 @@ func Audit(ctx context.Context, id AuditID, additionalData AuditFields) { } logger.logf(fieldsJSON) + SyncGatewayStats.GlobalStats.AuditStat.NumAuditsLogged.Add(1) } // IsAuditEnabled checks if auditing is enabled for the SG node @@ -285,6 +286,7 @@ func shouldLogAuditEventForUserAndRole(logCtx *LogContext) bool { Domain: string(logCtx.UserDomain), Name: logCtx.Username, }]; isDisabled { + SyncGatewayStats.GlobalStats.AuditStat.NumAuditsFilteredByUser.Add(1) return false } } @@ -295,6 +297,7 @@ func shouldLogAuditEventForUserAndRole(logCtx *LogContext) bool { Domain: string(logCtx.UserDomain), Name: role, }]; isDisabled { + SyncGatewayStats.GlobalStats.AuditStat.NumAuditsFilteredByRole.Add(1) return false } } diff --git a/base/stats.go b/base/stats.go index d94968bbdc..4b23f66870 100644 --- a/base/stats.go +++ b/base/stats.go @@ -32,6 +32,7 @@ const ( NamespaceKey = "sgw" ResourceUtilizationSubsystem = "resource_utilization" ConfigSubsystem = "config" + AuditSubsystem = "audit" SubsystemCacheKey = "cache" SubsystemDatabaseKey = "database" @@ -168,6 +169,7 @@ func (s *SgwStats) String() string { type GlobalStat struct { ResourceUtilization *ResourceUtilization `json:"resource_utilization"` ConfigStat *ConfigStat `json:"config"` + AuditStat *AuditStat `json:"audit"` } func newGlobalStat() (*GlobalStat, error) { @@ -180,6 +182,10 @@ func newGlobalStat() (*GlobalStat, error) { if err != nil { return nil, err } + err = g.initAuditStats() + if err != nil { + return nil, err + } return g, nil } @@ -198,6 +204,25 @@ func (g *GlobalStat) initConfigStats() error { return nil } +func (g *GlobalStat) initAuditStats() error { + auditStat := &AuditStat{} + var err error + auditStat.NumAuditsLogged, err = NewIntStat(AuditSubsystem, "num_audits_logged", StatUnitNoUnits, NumAuditsLoggedDesc, StatAddedVersion3dot2dot1, StatDeprecatedVersionNotDeprecated, StatStabilityCommitted, nil, nil, prometheus.CounterValue, 0) + if err != nil { + return err + } + auditStat.NumAuditsFilteredByUser, err = NewIntStat(AuditSubsystem, "num_audits_filtered_by_user", StatUnitNoUnits, NumAuditsFilteredByUserDesc, StatAddedVersion3dot2dot1, StatDeprecatedVersionNotDeprecated, StatStabilityCommitted, nil, nil, prometheus.CounterValue, 0) + if err != nil { + return err + } + auditStat.NumAuditsFilteredByRole, err = NewIntStat(AuditSubsystem, "num_audits_filtered_by_role", StatUnitNoUnits, NumAuditsFilteredByRoleDesc, StatAddedVersion3dot2dot1, StatDeprecatedVersionNotDeprecated, StatStabilityCommitted, nil, nil, prometheus.CounterValue, 0) + if err != nil { + return err + } + g.AuditStat = auditStat + return nil +} + func (g *GlobalStat) initResourceUtilizationStats() error { var err error resUtil := &ResourceUtilization{} @@ -365,6 +390,15 @@ type ConfigStat struct { DatabaseRollbackCollectionCollisions *SgwIntStat `json:"database_config_rollback_collection_collisions"` } +type AuditStat struct { + // The number of times an audit event was created/emitted/logged. + NumAuditsLogged *SgwIntStat `json:"num_audits_logged"` + // The number of times an audit event was filtered by username. + NumAuditsFilteredByUser *SgwIntStat `json:"num_audits_filtered_by_user"` + // The number of times an audit event was filtered by role. + NumAuditsFilteredByRole *SgwIntStat `json:"num_audits_filtered_by_role"` +} + type DbStats struct { dbName string CacheStats *CacheStats `json:"cache,omitempty"` diff --git a/base/stats_descriptions.go b/base/stats_descriptions.go index a36ebf670f..7bbc8acf2e 100644 --- a/base/stats_descriptions.go +++ b/base/stats_descriptions.go @@ -75,6 +75,13 @@ const ( DatabaseCollectionConflictDesc = "The total number of times a database config is rolled back to an invalid state (collection conflicts)." ) +// audit stat +const ( + NumAuditsLoggedDesc = "The total number of audit events logged." + NumAuditsFilteredByUserDesc = "The total number of audit events filtered by user." + NumAuditsFilteredByRoleDesc = "The total number of audit events filtered by role." +) + // cache stats descriptions const ( AbandonedSequencesDesc = "The total number of skipped sequences that were not found after 60 minutes and were abandoned." diff --git a/rest/audit_test.go b/rest/audit_test.go index 48c9ab11d4..c9f311cc04 100644 --- a/rest/audit_test.go +++ b/rest/audit_test.go @@ -132,7 +132,9 @@ func TestAuditLoggingFields(t *testing.T) { // auditableAction is a function that performs an action that should've been audited auditableAction func(t testing.TB) // expectedAuditEvents is a list of expected audit events and their fields for the given action... can be more than one event produced for a given action - expectedAuditEventFields map[base.AuditID]base.AuditFields + expectedAuditEventFields map[base.AuditID]base.AuditFields + expectedStatNumAuditEventsFilteredByUser int64 + expectedStatNumAuditEventsFilteredByRole int64 }{ { @@ -377,12 +379,14 @@ func TestAuditLoggingFields(t *testing.T) { auditableAction: func(t testing.TB) { RequireStatus(t, rt.SendUserRequest(http.MethodGet, "/db/", "", filteredPublicUsername), http.StatusOK) }, + expectedStatNumAuditEventsFilteredByUser: 3, // http, auth, read db }, { name: "filtered public role request", auditableAction: func(t testing.TB) { RequireStatus(t, rt.SendUserRequest(http.MethodGet, "/db/", "", filteredPublicRoleUsername), http.StatusOK) }, + expectedStatNumAuditEventsFilteredByRole: 3, // http, auth, read db }, { name: "filtered admin request", @@ -395,6 +399,7 @@ func TestAuditLoggingFields(t *testing.T) { } RequireStatus(t, rt.SendAdminRequestWithAuth(http.MethodGet, "/db/", "", filteredAdminUsername, RestTesterDefaultUserPassword), http.StatusOK) }, + expectedStatNumAuditEventsFilteredByUser: 3, // http, auth, read db }, { name: "filtered admin role request", @@ -407,6 +412,7 @@ func TestAuditLoggingFields(t *testing.T) { } RequireStatus(t, rt.SendAdminRequestWithAuth(http.MethodGet, "/db/", "", filteredAdminRoleUsername, RestTesterDefaultUserPassword), http.StatusOK) }, + expectedStatNumAuditEventsFilteredByRole: 3, // http, auth, read db }, { name: "authed admin request role filtered on different bucket", @@ -480,7 +486,23 @@ func TestAuditLoggingFields(t *testing.T) { } for _, testCase := range testCases { rt.Run(testCase.name, func(t *testing.T) { + numAuditEventsStatBefore := base.SyncGatewayStats.GlobalStats.AuditStat.NumAuditsLogged.Value() + numAuditEventsFilteredByUserStatBefore := base.SyncGatewayStats.GlobalStats.AuditStat.NumAuditsFilteredByUser.Value() + numAuditEventsFilteredByRoleStatBefore := base.SyncGatewayStats.GlobalStats.AuditStat.NumAuditsFilteredByRole.Value() + output := base.AuditLogContents(t, testCase.auditableAction) + + numAuditEventsStatAfter := base.SyncGatewayStats.GlobalStats.AuditStat.NumAuditsLogged.Value() + numAuditEventsFilteredByUserStatAfter := base.SyncGatewayStats.GlobalStats.AuditStat.NumAuditsFilteredByUser.Value() + numAuditEventsFilteredByRoleStatAfter := base.SyncGatewayStats.GlobalStats.AuditStat.NumAuditsFilteredByRole.Value() + + numAuditEventsStat := numAuditEventsStatAfter - numAuditEventsStatBefore + assert.Equal(t, int64(len(testCase.expectedAuditEventFields)), numAuditEventsStat) + numAuditEventsFilteredByUserStat := numAuditEventsFilteredByUserStatAfter - numAuditEventsFilteredByUserStatBefore + assert.Equal(t, testCase.expectedStatNumAuditEventsFilteredByUser, numAuditEventsFilteredByUserStat) + numAuditEventsFilteredByRoleStat := numAuditEventsFilteredByRoleStatAfter - numAuditEventsFilteredByRoleStatBefore + assert.Equal(t, testCase.expectedStatNumAuditEventsFilteredByRole, numAuditEventsFilteredByRoleStat) + events := jsonLines(t, output) assert.Equalf(t, len(testCase.expectedAuditEventFields), len(events), "expected exactly %d audit events, got %d", len(testCase.expectedAuditEventFields), len(events))