Files
pfsense2jsonl/pfjson2cim.go
2025-02-28 13:08:30 +01:00

185 lines
4.8 KiB
Go

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
}