audit-go/internal/audit/api/api_legacy_converter.go
Mathias Koehrer (EXT) 84c49f2690 Merged PR 889470: feat: Add more validation to proto schema
Made the initiator.email optional and added a new validation.
Added a new regex pattern to string fields to prevent them from consisting only of whitespace.

Security-concept-update-needed: false

JIRA Work Item: [STACKITRMA-677](https://jira.schwarz/browse/STACKITRMA-677)
2025-11-27 14:52:27 +00:00

310 lines
10 KiB
Go

package api
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"time"
"google.golang.org/protobuf/encoding/protojson"
auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1"
pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common"
)
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
switch {
case strings.HasSuffix(event.LogName, string(pkgAuditCommon.EventTypeAdminActivity)):
eventType = "ADMIN_ACTIVITY"
case strings.HasSuffix(event.LogName, string(pkgAuditCommon.EventTypeSystemEvent)):
eventType = "SYSTEM_EVENT"
case strings.HasSuffix(event.LogName, string(pkgAuditCommon.EventTypePolicyDenied)):
eventType = "POLICY_DENIED"
case strings.HasSuffix(event.LogName, string(pkgAuditCommon.EventTypeDataAccess)):
return nil, pkgAuditCommon.ErrUnsupportedEventTypeDataAccess
default:
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
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{}
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{}
if event.ProtoPayload.Request != nil {
body = event.ProtoPayload.Request.AsMap()
}
var headers map[string]interface{}
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: &parameters,
Body: &body,
Headers: &headers,
}
}
if routableEvent.ObjectIdentifier == nil {
return nil, pkgAuditCommon.ErrObjectIdentifierNil
}
// Context and event type
var messageContext *LegacyAuditEventContext
switch routableEvent.ObjectIdentifier.Type {
case string(pkgAuditCommon.ObjectTypeProject):
messageContext = &LegacyAuditEventContext{
OrganizationId: nil,
FolderId: nil,
ProjectId: &routableEvent.ObjectIdentifier.Identifier,
}
case string(pkgAuditCommon.ObjectTypeFolder):
messageContext = &LegacyAuditEventContext{
OrganizationId: nil,
FolderId: &routableEvent.ObjectIdentifier.Identifier,
ProjectId: nil,
}
case string(pkgAuditCommon.ObjectTypeOrganization):
messageContext = &LegacyAuditEventContext{
OrganizationId: &routableEvent.ObjectIdentifier.Identifier,
FolderId: nil,
ProjectId: nil,
}
case string(pkgAuditCommon.ObjectTypeSystem):
messageContext = nil
default:
return nil, pkgAuditCommon.ErrUnsupportedObjectIdentifierType
}
var visibility string
switch routableEvent.Visibility {
case auditV1.Visibility_VISIBILITY_PUBLIC:
visibility = "PUBLIC"
case auditV1.Visibility_VISIBILITY_PRIVATE:
visibility = "PRIVATE"
case auditV1.Visibility_VISIBILITY_UNSPECIFIED:
visibility = ""
}
// 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,
auditV1.LogSeverity_LOG_SEVERITY_DEBUG,
auditV1.LogSeverity_LOG_SEVERITY_INFO,
auditV1.LogSeverity_LOG_SEVERITY_NOTICE,
auditV1.LogSeverity_LOG_SEVERITY_WARNING:
severity = "INFO"
case auditV1.LogSeverity_LOG_SEVERITY_ERROR,
auditV1.LogSeverity_LOG_SEVERITY_CRITICAL,
auditV1.LogSeverity_LOG_SEVERITY_ALERT,
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"`
}