package api import ( "encoding/json" "errors" "fmt" "net/url" "strings" "time" auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1" "google.golang.org/protobuf/encoding/protojson" ) var ErrUnsupportedSeverity = errors.New("unsupported severity level") // convertAndSerializeIntoLegacyFormat converts the protobuf events into the json serialized legacy audit log format func convertAndSerializeIntoLegacyFormat( event *auditV1.AuditLogEntry, routableEvent *auditV1.RoutableAuditEvent, ) ([]byte, error) { // Event type var eventType string if strings.HasSuffix(event.LogName, string(EventTypeAdminActivity)) { eventType = "ADMIN_ACTIVITY" } else if strings.HasSuffix(event.LogName, string(EventTypeSystemEvent)) { eventType = "SYSTEM_EVENT" } else if strings.HasSuffix(event.LogName, string(EventTypePolicyDenied)) { eventType = "POLICY_DENIED" } else if strings.HasSuffix(event.LogName, string(EventTypeDataAccess)) { return nil, ErrUnsupportedEventTypeDataAccess } else { return nil, errors.New("unsupported event type") } // Source IP & User agent var sourceIpAddress string var userAgent string if event.ProtoPayload == nil || event.ProtoPayload.RequestMetadata == nil { sourceIpAddress = "0.0.0.0" userAgent = "none" } else { sourceIpAddress = event.ProtoPayload.RequestMetadata.CallerIp userAgent = event.ProtoPayload.RequestMetadata.CallerSuppliedUserAgent } // Principals var serviceAccountDelegationInfo *LegacyAuditEventServiceAccountDelegationInfo = nil if len(event.ProtoPayload.AuthenticationInfo.ServiceAccountDelegationInfo) > 0 { var principals []LegacyAuditEventPrincipal for _, principal := range event.ProtoPayload.AuthenticationInfo.ServiceAccountDelegationInfo { switch principalValue := principal.Authority.(type) { case *auditV1.ServiceAccountDelegationInfo_IdpPrincipal_: principals = append(principals, LegacyAuditEventPrincipal{ Id: principalValue.IdpPrincipal.PrincipalId, Email: &principalValue.IdpPrincipal.PrincipalEmail, }) case *auditV1.ServiceAccountDelegationInfo_SystemPrincipal_: principals = append(principals, LegacyAuditEventPrincipal{ Id: "system", }) default: return nil, errors.New("unsupported principal type") } } serviceAccountDelegationInfo = &LegacyAuditEventServiceAccountDelegationInfo{Principals: principals} } var request LegacyAuditEventRequest if event.ProtoPayload.RequestMetadata.RequestAttributes == nil { request = LegacyAuditEventRequest{ Endpoint: "none", } } else { var parameters map[string]interface{} = nil if event.ProtoPayload.RequestMetadata.RequestAttributes.Path != "" && event.ProtoPayload.RequestMetadata.RequestAttributes.Query != nil && *event.ProtoPayload.RequestMetadata.RequestAttributes.Query != "" { parameters = map[string]interface{}{} unescapedQuery, err := url.QueryUnescape(*event.ProtoPayload.RequestMetadata.RequestAttributes.Query) if err != nil { return nil, err } parsedUrl, err := url.Parse(fmt.Sprintf("%s?%s", event.ProtoPayload.RequestMetadata.RequestAttributes.Path, unescapedQuery)) if err != nil { return nil, err } for k, v := range parsedUrl.Query() { parameters[k] = v } } var body map[string]interface{} = nil if event.ProtoPayload.Request != nil { body = event.ProtoPayload.Request.AsMap() } var headers map[string]interface{} = nil if event.ProtoPayload.RequestMetadata.RequestAttributes.Headers != nil { headers = map[string]interface{}{} for key, value := range event.ProtoPayload.RequestMetadata.RequestAttributes.Headers { headers[key] = value } } request = LegacyAuditEventRequest{ Endpoint: event.ProtoPayload.RequestMetadata.RequestAttributes.Path, Parameters: ¶meters, Body: &body, Headers: &headers, } } if routableEvent.ObjectIdentifier == nil { return nil, ErrObjectIdentifierNil } // Context and event type var messageContext *LegacyAuditEventContext switch routableEvent.ObjectIdentifier.Type { case string(ObjectTypeProject): messageContext = &LegacyAuditEventContext{ OrganizationId: nil, FolderId: nil, ProjectId: &routableEvent.ObjectIdentifier.Identifier, } case string(ObjectTypeFolder): messageContext = &LegacyAuditEventContext{ OrganizationId: nil, FolderId: &routableEvent.ObjectIdentifier.Identifier, ProjectId: nil, } case string(ObjectTypeOrganization): messageContext = &LegacyAuditEventContext{ OrganizationId: &routableEvent.ObjectIdentifier.Identifier, FolderId: nil, ProjectId: nil, } case string(ObjectTypeSystem): messageContext = nil default: return nil, ErrUnsupportedObjectIdentifierType } var visibility string switch routableEvent.Visibility { case auditV1.Visibility_VISIBILITY_PUBLIC: visibility = "PUBLIC" case auditV1.Visibility_VISIBILITY_PRIVATE: visibility = "PRIVATE" } // Details serializedRequestAttributes, err := protojson.Marshal(event.ProtoPayload.Metadata) if err != nil { return nil, err } var details map[string]interface{} err = json.Unmarshal(serializedRequestAttributes, &details) if err != nil { return nil, err } // Result var result = event.ProtoPayload.Response.AsMap() // Severity var severity string switch event.Severity { case auditV1.LogSeverity_LOG_SEVERITY_DEFAULT: fallthrough case auditV1.LogSeverity_LOG_SEVERITY_DEBUG: fallthrough case auditV1.LogSeverity_LOG_SEVERITY_INFO: fallthrough case auditV1.LogSeverity_LOG_SEVERITY_NOTICE: fallthrough case auditV1.LogSeverity_LOG_SEVERITY_WARNING: severity = "INFO" case auditV1.LogSeverity_LOG_SEVERITY_ERROR: fallthrough case auditV1.LogSeverity_LOG_SEVERITY_CRITICAL: fallthrough case auditV1.LogSeverity_LOG_SEVERITY_ALERT: fallthrough case auditV1.LogSeverity_LOG_SEVERITY_EMERGENCY: severity = "ERROR" default: return nil, ErrUnsupportedSeverity } // Instantiate the legacy event - missing values are filled with defaults legacyAuditEvent := LegacyAuditEvent{ Severity: severity, Visibility: visibility, EventType: eventType, EventTimeStamp: event.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime(), EventName: event.ProtoPayload.OperationName, SourceIpAddress: sourceIpAddress, UserAgent: userAgent, Initiator: LegacyAuditEventPrincipal{ Id: event.ProtoPayload.AuthenticationInfo.PrincipalId, Email: &event.ProtoPayload.AuthenticationInfo.PrincipalEmail, }, ServiceAccountDelegationInfo: serviceAccountDelegationInfo, Request: request, Context: messageContext, ResourceName: &event.ProtoPayload.ResourceName, CorrelationId: event.CorrelationId, Result: &result, Details: &details, } bytes, err := json.Marshal(legacyAuditEvent) if err != nil { return nil, err } return bytes, nil } // LegacyAuditEvent has the format as follows: /* { "severity": "INFO", "visibility": "PUBLIC", "eventType": "ADMIN_ACTIVITY", "eventTimeStamp": "2019-08-24T14:15:22Z", "eventName": "Create organization", "sourceIpAddress": "127.0.0.1", "userAgent": "CLI", "initiator": { "id": "string", "email": "user@example.com" }, "serviceAccountDelegationInfo": { "principals": [ { "id": "string", "email": "user@example.com" } ] }, "request": { "endpoint": "string", "parameters": {}, "body": {}, "headers": { "Content-Type": "application/json" } }, "context": { "organizationId": "string", "folderId": "string", "projectId": "string" }, "resourceId": "string", "resourceName": "string", "correlationId": "string", "result": {}, "details": {} } */ type LegacyAuditEvent struct { Severity string `json:"severity"` Visibility string `json:"visibility"` EventType string `json:"eventType"` EventTimeStamp time.Time `json:"eventTimeStamp"` EventName string `json:"eventName"` SourceIpAddress string `json:"sourceIpAddress"` UserAgent string `json:"userAgent"` Initiator LegacyAuditEventPrincipal `json:"initiator"` Request LegacyAuditEventRequest `json:"request"` ServiceAccountDelegationInfo *LegacyAuditEventServiceAccountDelegationInfo `json:"serviceAccountDelegationInfo"` Context *LegacyAuditEventContext `json:"context"` ResourceId *string `json:"resourceId"` ResourceName *string `json:"resourceName"` CorrelationId *string `json:"correlationId"` Result *map[string]interface{} `json:"result"` Details *map[string]interface{} `json:"details"` } // LegacyAuditEventPrincipal is a representation for a principal's id (+optional email) information. type LegacyAuditEventPrincipal struct { Id string `json:"id"` Email *string `json:"email"` } // LegacyAuditEventServiceAccountDelegationInfo contains information about service account delegation. type LegacyAuditEventServiceAccountDelegationInfo struct { Principals []LegacyAuditEventPrincipal `json:"principals"` } // LegacyAuditEventRequest contains request information, which mirrors the action of the user and // the resulting changes within the system. type LegacyAuditEventRequest struct { Endpoint string `json:"endpoint"` Parameters *map[string]interface{} `json:"parameters"` Body *map[string]interface{} `json:"body"` Headers *map[string]interface{} `json:"headers"` } // LegacyAuditEventContext contains optional context information. type LegacyAuditEventContext struct { OrganizationId *string `json:"organizationId"` FolderId *string `json:"folderId"` ProjectId *string `json:"projectId"` }