diff --git a/pfjson2cim.go b/pfjson2cim.go new file mode 100644 index 0000000..1a5e63b --- /dev/null +++ b/pfjson2cim.go @@ -0,0 +1,184 @@ +package main + +import ( + "bufio" + "encoding/json" + "fmt" + "os" + "strconv" + "strings" +) + +func main() { + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + line := scanner.Text() + if len(strings.TrimSpace(line)) == 0 { + continue + } + + // Unmarshal the JSON event + var event map[string]interface{} + if err := json.Unmarshal([]byte(line), &event); err != nil { + fmt.Fprintf(os.Stderr, "Error parsing JSON: %v\n", err) + continue + } + + // Transform the event into a flat CIM structure. + cimEvent := transformToCIM(event) + + // Marshal the new event and output it + out, err := json.Marshal(cimEvent) + if err != nil { + fmt.Fprintf(os.Stderr, "Error marshaling CIM event: %v\n", err) + continue + } + fmt.Println(string(out)) + } + + if err := scanner.Err(); err != nil { + fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err) + os.Exit(1) + } +} + +// transformToCIM maps the nested pfSense log structure to a flat CIM structure. +func transformToCIM(event map[string]interface{}) map[string]interface{} { + cim := make(map[string]interface{}) + + // Timestamp: preserve it as is. + if ts, ok := event["timestamp"]; ok { + cim["timestamp"] = ts + } + + // Optionally copy log_level if available. + if lvl, ok := event["log_level"]; ok { + cim["log_level"] = lvl + } + + // Our pfSense log is expected to have a "log" object. + logObj, ok := event["log"].(map[string]interface{}) + if !ok { + // If not, return the original event. + return event + } + + // Extract common fields from "base" + base, _ := logObj["base"].(map[string]interface{}) + if base != nil { + if act, ok := base["action"].(string); ok && act != "" { + cim["action"] = strings.ToLower(act) + } + if dir, ok := base["direction"].(string); ok && dir != "" { + cim["direction"] = strings.ToLower(dir) + } + if rn, ok := base["rule_number"]; ok { + // try converting rule_number to integer if possible. + if rnStr, ok := rn.(string); ok { + if rnInt, err := strconv.Atoi(rnStr); err == nil { + cim["rule_number"] = rnInt + } else { + cim["rule_number"] = rnStr + } + } else { + cim["rule_number"] = rn + } + } + if reason, ok := base["reason"].(string); ok && reason != "" { + cim["reason"] = reason + } + } + + // Extract IP data: source, destination, and packet length + ipData, _ := logObj["ip_data"].(map[string]interface{}) + if ipData != nil { + if src, ok := ipData["source_address"].(string); ok && src != "" { + cim["src"] = src + } + if dest, ok := ipData["destination_address"].(string); ok && dest != "" { + cim["dest"] = dest + } + if length, ok := ipData["length"].(string); ok && length != "" { + if l, err := strconv.Atoi(length); err == nil { + cim["packet_length"] = l + } else { + cim["packet_length"] = length + } + } + } + + // Determine transport protocol from ip_specific_data. + ipSpec, _ := logObj["ip_specific_data"].(map[string]interface{}) + var protocol string + if ipSpec != nil { + if ipVersion, ok := base["ip_version"].(string); ok { + if ipVersion == "4" { + if ipv4, ok := ipSpec["ipv4_header"].(map[string]interface{}); ok { + if pt, ok := ipv4["protocol_text"].(string); ok { + protocol = strings.ToLower(pt) + } + } + } else if ipVersion == "6" { + if ipv6, ok := ipSpec["ipv6_header"].(map[string]interface{}); ok { + if pt, ok := ipv6["protocol_text"].(string); ok { + protocol = strings.ToLower(pt) + } + } + } + } + } + if protocol != "" { + cim["transport"] = protocol + } + + // Map protocol-specific data for TCP and UDP. + protoSpec, _ := logObj["protocol_specific_data"].(map[string]interface{}) + if protoSpec != nil { + // For TCP/UDP, map ports and data length. + if protocol == "tcp" || protocol == "udp" { + if sp, ok := protoSpec["source_port"].(string); ok && sp != "" { + if port, err := strconv.Atoi(sp); err == nil { + cim["src_port"] = port + } else { + cim["src_port"] = sp + } + } + if dp, ok := protoSpec["destination_port"].(string); ok && dp != "" { + if port, err := strconv.Atoi(dp); err == nil { + cim["dest_port"] = port + } else { + cim["dest_port"] = dp + } + } + if dl, ok := protoSpec["data_length"].(string); ok && dl != "" { + if d, err := strconv.Atoi(dl); err == nil { + // Map to bytes_in or bytes_out based on direction. + if dstr, ok := cim["direction"].(string); ok { + if dstr == "in" { + cim["bytes_in"] = d + } else if dstr == "out" { + cim["bytes_out"] = d + } else { + cim["data_length"] = d + } + } else { + cim["data_length"] = d + } + } else { + cim["data_length"] = dl + } + } + } else { + // For non-TCP/UDP protocols, if ports exist in the raw data, include them. + if sp, ok := protoSpec["source_port"].(string); ok && sp != "" { + cim["src_port"] = sp + } + if dp, ok := protoSpec["destination_port"].(string); ok && dp != "" { + cim["dest_port"] = dp + } + } + } + + // Return the new flat event with only CIM fields. + return cim +}