|
| 1 | +// Copyright (c) F5, Inc. |
| 2 | +// |
| 3 | +// This source code is licensed under the Apache License, Version 2.0 license found in the |
| 4 | +// LICENSE file in the root directory of this source tree. |
| 5 | + |
| 6 | +package securityviolationsprocessor |
| 7 | + |
| 8 | +import ( |
| 9 | + "encoding/csv" |
| 10 | + "strconv" |
| 11 | + "strings" |
| 12 | + |
| 13 | + events "github.com/nginx/agent/v3/api/grpc/events/v1" |
| 14 | +) |
| 15 | + |
| 16 | +// Mapping of CSV field positions to their corresponding keys |
| 17 | +var fieldOrder = []string{ |
| 18 | + "blocking_exception_reason", |
| 19 | + "dest_port", |
| 20 | + "ip_client", |
| 21 | + "is_truncated_bool", |
| 22 | + "method", |
| 23 | + "policy_name", |
| 24 | + "protocol", |
| 25 | + "request_status", |
| 26 | + "response_code", |
| 27 | + "severity", |
| 28 | + "sig_cves", |
| 29 | + "sig_set_names", |
| 30 | + "src_port", |
| 31 | + "sub_violations", |
| 32 | + "support_id", |
| 33 | + "threat_campaign_names", |
| 34 | + "violation_rating", |
| 35 | + "vs_name", |
| 36 | + "x_forwarded_for_header_value", |
| 37 | + "outcome", |
| 38 | + "outcome_reason", |
| 39 | + "violations", |
| 40 | + "violation_details", |
| 41 | + "bot_signature_name", |
| 42 | + "bot_category", |
| 43 | + "bot_anomalies", |
| 44 | + "enforced_bot_anomalies", |
| 45 | + "client_class", |
| 46 | + "client_application", |
| 47 | + "client_application_version", |
| 48 | + "transport_protocol", |
| 49 | + "uri", |
| 50 | + "request", |
| 51 | +} |
| 52 | + |
| 53 | +// parseCSVLog parses comma-separated syslog messages where fields are in a |
| 54 | +// order : blocking_exception_reason,dest_port,ip_client,is_truncated_bool,method,policy_name,protocol,request_status,response_code,severity,sig_cves,sig_set_names,src_port,sub_violations,support_id,threat_campaign_names,violation_rating,vs_name,x_forwarded_for_header_value,outcome,outcome_reason,violations,violation_details,bot_signature_name,bot_category,bot_anomalies,enforced_bot_anomalies,client_class,client_application,client_application_version,transport_protocol,uri,request (secops_dashboard-log profile format). |
| 55 | +// versions when key-value logging isn't enabled. |
| 56 | +// |
| 57 | +//nolint:lll //long test string kept for log profile readability |
| 58 | +func parseCSVLog(message string) map[string]string { |
| 59 | + fieldValueMap := make(map[string]string, 33) |
| 60 | + |
| 61 | + // Remove the "ASM:" prefix if present so we only process the values |
| 62 | + message = strings.TrimPrefix(message, "ASM:") |
| 63 | + |
| 64 | + reader := csv.NewReader(strings.NewReader(message)) |
| 65 | + reader.LazyQuotes = true |
| 66 | + fields, err := reader.Read() |
| 67 | + if err != nil { |
| 68 | + // fallback: return empty map if parsing fails |
| 69 | + return fieldValueMap |
| 70 | + } |
| 71 | + |
| 72 | + for i, field := range fields { |
| 73 | + if i >= len(fieldOrder) { |
| 74 | + break |
| 75 | + } |
| 76 | + fieldValueMap[fieldOrder[i]] = strings.TrimSpace(field) |
| 77 | + } |
| 78 | + |
| 79 | + // combine multiple values separated by '::' |
| 80 | + if combined, ok := fieldValueMap["sig_cves"]; ok { |
| 81 | + parts := strings.SplitN(combined, "::", maxSplitParts) |
| 82 | + fieldValueMap["sig_ids"] = parts[0] |
| 83 | + if len(parts) > 1 { |
| 84 | + fieldValueMap["sig_names"] = parts[1] |
| 85 | + } |
| 86 | + } |
| 87 | + |
| 88 | + if combined, ok := fieldValueMap["sig_set_names"]; ok { |
| 89 | + parts := strings.SplitN(combined, "::", maxSplitParts) |
| 90 | + fieldValueMap["sig_set_names"] = parts[0] |
| 91 | + if len(parts) > 1 { |
| 92 | + fieldValueMap["sig_cves"] = parts[1] |
| 93 | + } |
| 94 | + } |
| 95 | + |
| 96 | + return fieldValueMap |
| 97 | +} |
| 98 | + |
| 99 | +// parseOutcome converts string outcome to RequestOutcome enum |
| 100 | +func parseOutcome(outcome string) events.RequestOutcome { |
| 101 | + switch strings.ToLower(strings.TrimSpace(outcome)) { |
| 102 | + case "passed": |
| 103 | + return events.RequestOutcome_REQUEST_OUTCOME_PASSED |
| 104 | + case "rejected": |
| 105 | + return events.RequestOutcome_REQUEST_OUTCOME_REJECTED |
| 106 | + default: |
| 107 | + return events.RequestOutcome_REQUEST_OUTCOME_UNKNOWN |
| 108 | + } |
| 109 | +} |
| 110 | + |
| 111 | +// parseIsTruncated converts string to boolean |
| 112 | +func parseIsTruncated(value string) bool { |
| 113 | + switch strings.ToLower(strings.TrimSpace(value)) { |
| 114 | + case "true": |
| 115 | + return true |
| 116 | + default: |
| 117 | + return false |
| 118 | + } |
| 119 | +} |
| 120 | + |
| 121 | +// parseSeverity converts string severity to Severity enum |
| 122 | +func parseSeverity(severity string) events.Severity { |
| 123 | + switch strings.ToLower(strings.TrimSpace(severity)) { |
| 124 | + case "informational": |
| 125 | + return events.Severity_SEVERITY_INFORMATIONAL |
| 126 | + case "low": |
| 127 | + return events.Severity_SEVERITY_LOW |
| 128 | + case "medium": |
| 129 | + return events.Severity_SEVERITY_MEDIUM |
| 130 | + case "high": |
| 131 | + return events.Severity_SEVERITY_HIGH |
| 132 | + case "critical": |
| 133 | + return events.Severity_SEVERITY_CRITICAL |
| 134 | + default: |
| 135 | + return events.Severity_SEVERITY_UNKNOWN |
| 136 | + } |
| 137 | +} |
| 138 | + |
| 139 | +// parseRequestOutcomeReason converts string outcome reason to RequestOutcomeReason enum |
| 140 | +func parseRequestOutcomeReason(reason string) events.RequestOutcomeReason { |
| 141 | + switch strings.ToUpper(strings.TrimSpace(reason)) { |
| 142 | + case "SECURITY_WAF_OK": |
| 143 | + return events.RequestOutcomeReason_SECURITY_WAF_OK |
| 144 | + case "SECURITY_WAF_VIOLATION": |
| 145 | + return events.RequestOutcomeReason_SECURITY_WAF_VIOLATION |
| 146 | + case "SECURITY_WAF_FLAGGED": |
| 147 | + return events.RequestOutcomeReason_SECURITY_WAF_FLAGGED |
| 148 | + case "SECURITY_WAF_VIOLATION_TRANSPARENT": |
| 149 | + return events.RequestOutcomeReason_SECURITY_WAF_VIOLATION_TRANSPARENT |
| 150 | + default: |
| 151 | + return events.RequestOutcomeReason_SECURITY_WAF_UNKNOWN |
| 152 | + } |
| 153 | +} |
| 154 | + |
| 155 | +// parseRequestStatus converts string request status to RequestStatus enum |
| 156 | +func parseRequestStatus(status string) events.RequestStatus { |
| 157 | + switch strings.ToLower(strings.TrimSpace(status)) { |
| 158 | + case "blocked": |
| 159 | + return events.RequestStatus_REQUEST_STATUS_BLOCKED |
| 160 | + case "alerted": |
| 161 | + return events.RequestStatus_REQUEST_STATUS_ALERTED |
| 162 | + case "passed": |
| 163 | + return events.RequestStatus_REQUEST_STATUS_PASSED |
| 164 | + default: |
| 165 | + return events.RequestStatus_REQUEST_STATUS_UNKNOWN |
| 166 | + } |
| 167 | +} |
| 168 | + |
| 169 | +// parseUint32 converts string to uint32 |
| 170 | +func parseUint32(value string) uint32 { |
| 171 | + if val, err := strconv.ParseUint(strings.TrimSpace(value), 10, 32); err == nil { |
| 172 | + return uint32(val) |
| 173 | + } |
| 174 | + return 0 |
| 175 | +} |
| 176 | + |
| 177 | +func mapKVToSecurityViolationEvent(log *events.SecurityViolationEvent, |
| 178 | + kvMap map[string]string, |
| 179 | +) { |
| 180 | + log.PolicyName = kvMap["policy_name"] |
| 181 | + log.SupportId = kvMap["support_id"] |
| 182 | + log.RequestOutcome = parseOutcome(kvMap["outcome"]) |
| 183 | + log.RequestOutcomeReason = parseRequestOutcomeReason(kvMap["outcome_reason"]) |
| 184 | + log.BlockingExceptionReason = kvMap["blocking_exception_reason"] |
| 185 | + log.Method = kvMap["method"] |
| 186 | + log.Protocol = kvMap["protocol"] |
| 187 | + log.XffHeaderValue = kvMap["x_forwarded_for_header_value"] |
| 188 | + log.Uri = kvMap["uri"] |
| 189 | + log.Request = kvMap["request"] |
| 190 | + log.IsTruncated = parseIsTruncated(kvMap["is_truncated_bool"]) |
| 191 | + log.RequestStatus = parseRequestStatus(kvMap["request_status"]) |
| 192 | + log.ResponseCode = parseUint32(kvMap["response_code"]) |
| 193 | + log.ServerAddr = kvMap["server_addr"] |
| 194 | + log.VsName = kvMap["vs_name"] |
| 195 | + log.RemoteAddr = kvMap["ip_client"] |
| 196 | + log.DestinationPort = parseUint32(kvMap["dest_port"]) |
| 197 | + log.ServerPort = parseUint32(kvMap["src_port"]) |
| 198 | + log.Violations = kvMap["violations"] |
| 199 | + log.SubViolations = kvMap["sub_violations"] |
| 200 | + log.ViolationRating = parseUint32(kvMap["violation_rating"]) |
| 201 | + log.SigSetNames = kvMap["sig_set_names"] |
| 202 | + log.SigCves = kvMap["sig_cves"] |
| 203 | + log.ClientClass = kvMap["client_class"] |
| 204 | + log.ClientApplication = kvMap["client_application"] |
| 205 | + log.ClientApplicationVersion = kvMap["client_application_version"] |
| 206 | + log.Severity = parseSeverity(kvMap["severity"]) |
| 207 | + log.ThreatCampaignNames = kvMap["threat_campaign_names"] |
| 208 | + log.BotAnomalies = kvMap["bot_anomalies"] |
| 209 | + log.BotCategory = kvMap["bot_category"] |
| 210 | + log.EnforcedBotAnomalies = kvMap["enforced_bot_anomalies"] |
| 211 | + log.BotSignatureName = kvMap["bot_signature_name"] |
| 212 | + log.DisplayName = kvMap["display_name"] |
| 213 | + |
| 214 | + if log.GetRemoteAddr() == "" { |
| 215 | + log.RemoteAddr = kvMap["remote_addr"] |
| 216 | + } |
| 217 | + if log.GetDestinationPort() == 0 { |
| 218 | + log.DestinationPort = parseUint32(kvMap["remote_port"]) |
| 219 | + } |
| 220 | +} |
0 commit comments