235 lines
7.0 KiB
Go
235 lines
7.0 KiB
Go
package main
|
||
|
||
import (
|
||
"bufio"
|
||
"encoding/csv"
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
)
|
||
|
||
func main() {
|
||
// Create a scanner to read lines from standard input.
|
||
scanner := bufio.NewScanner(os.Stdin)
|
||
for scanner.Scan() {
|
||
line := scanner.Text()
|
||
// Expect exactly 4 tab-separated columns:
|
||
// 1. Timestamp
|
||
// 2. Log level (e.g. "Informational")
|
||
// 3. The literal "filterlog" (optionally with a colon)
|
||
// 4. The CSV log data.
|
||
parts := strings.Split(line, "\t")
|
||
if len(parts) < 4 {
|
||
fmt.Fprintf(os.Stderr, "Skipping malformed line: %s\n", line)
|
||
continue
|
||
}
|
||
|
||
timestamp := strings.TrimSpace(parts[0])
|
||
logLevel := strings.TrimSpace(parts[1])
|
||
filterlogField := strings.TrimSpace(parts[2])
|
||
// In case the CSV data was split over multiple tab columns, join them.
|
||
csvData := strings.TrimSpace(strings.Join(parts[3:], "\t"))
|
||
|
||
// Validate that the third column is "filterlog" (with optional colon).
|
||
if filterlogField != "filterlog" && filterlogField != "filterlog:" {
|
||
fmt.Fprintf(os.Stderr, "Skipping line due to invalid filterlog field: %s\n", line)
|
||
continue
|
||
}
|
||
|
||
// Parse the CSV log data.
|
||
r := csv.NewReader(strings.NewReader(csvData))
|
||
fields, err := r.Read()
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Error parsing CSV: %v\n", err)
|
||
continue
|
||
}
|
||
|
||
// Trim whitespace from each CSV field.
|
||
for i, f := range fields {
|
||
fields[i] = strings.TrimSpace(f)
|
||
}
|
||
|
||
// Verify at least 9 fields (the common fields).
|
||
if len(fields) < 9 {
|
||
fmt.Fprintf(os.Stderr, "Not enough CSV fields in log-data: %s\n", line)
|
||
continue
|
||
}
|
||
|
||
// Map the common fields as defined in the BNF:
|
||
// <rule-number>, <sub-rule-number>, <anchor>, <tracker>, <real-interface>,
|
||
// <reason>, <action>, <direction>, <ip-version>
|
||
base := map[string]interface{}{
|
||
"rule_number": fields[0],
|
||
"sub_rule_number": fields[1],
|
||
"anchor": fields[2],
|
||
"tracker": fields[3],
|
||
"real_interface": fields[4],
|
||
"reason": fields[5],
|
||
"action": fields[6],
|
||
"direction": fields[7],
|
||
"ip_version": fields[8],
|
||
}
|
||
|
||
ipVersion := fields[8]
|
||
idx := 9
|
||
|
||
// We'll build two objects: one for the IP-specific header and one for the IP-data block.
|
||
ipSpecificData := map[string]interface{}{}
|
||
ipData := map[string]interface{}{}
|
||
protocolSpecificData := map[string]interface{}{}
|
||
var protoSpecific []string
|
||
|
||
if ipVersion == "4" {
|
||
// For IPv4, expect 8 fields for IPv4-specific data.
|
||
if len(fields) < idx+8+3 {
|
||
fmt.Fprintf(os.Stderr, "Not enough fields for IPv4 in line: %s\n", line)
|
||
continue
|
||
}
|
||
ipv4Header := map[string]interface{}{
|
||
"tos": fields[idx],
|
||
"ecn": fields[idx+1],
|
||
"ttl": fields[idx+2],
|
||
"id": fields[idx+3],
|
||
"offset": fields[idx+4],
|
||
"flags": fields[idx+5],
|
||
"protocol_id": fields[idx+6],
|
||
"protocol_text": fields[idx+7],
|
||
}
|
||
ipSpecificData["ipv4_header"] = ipv4Header
|
||
idx += 8
|
||
|
||
// The ip-data block: <length>, <source-address>, <destination-address>.
|
||
ipData = map[string]interface{}{
|
||
"length": fields[idx],
|
||
"source_address": fields[idx+1],
|
||
"destination_address": fields[idx+2],
|
||
}
|
||
idx += 3
|
||
} else if ipVersion == "6" {
|
||
// For IPv6, expect 5 fields for IPv6-specific data.
|
||
if len(fields) < idx+5+3 {
|
||
fmt.Fprintf(os.Stderr, "Not enough fields for IPv6 in line: %s\n", line)
|
||
continue
|
||
}
|
||
ipv6Header := map[string]interface{}{
|
||
"class": fields[idx],
|
||
"flow_label": fields[idx+1],
|
||
"hop_limit": fields[idx+2],
|
||
"protocol_text": fields[idx+3],
|
||
"protocol_id": fields[idx+4],
|
||
}
|
||
ipSpecificData["ipv6_header"] = ipv6Header
|
||
idx += 5
|
||
|
||
ipData = map[string]interface{}{
|
||
"length": fields[idx],
|
||
"source_address": fields[idx+1],
|
||
"destination_address": fields[idx+2],
|
||
}
|
||
idx += 3
|
||
} else {
|
||
// Unknown IP version – capture remainder as raw.
|
||
ipSpecificData["raw"] = fields[idx:]
|
||
idx = len(fields)
|
||
}
|
||
|
||
// Any fields remaining are protocol-specific.
|
||
if len(fields) > idx {
|
||
protoSpecific = fields[idx:]
|
||
}
|
||
|
||
// Determine protocol from the header's protocol_text.
|
||
var protocol string
|
||
if ipVersion == "4" {
|
||
if hdr, ok := ipSpecificData["ipv4_header"].(map[string]interface{}); ok {
|
||
protocol = hdr["protocol_text"].(string)
|
||
}
|
||
} else if ipVersion == "6" {
|
||
if hdr, ok := ipSpecificData["ipv6_header"].(map[string]interface{}); ok {
|
||
protocol = hdr["protocol_text"].(string)
|
||
}
|
||
}
|
||
|
||
// Structure protocol-specific data if possible.
|
||
switch strings.ToLower(protocol) {
|
||
case "tcp":
|
||
// Expect 9 fields for TCP:
|
||
// <source-port>, <destination-port>, <data-length>, <tcp-flags>,
|
||
// <sequence-number>, <ack-number>, <tcp-window>, <urg>, <tcp-options>
|
||
if len(protoSpecific) >= 9 {
|
||
protocolSpecificData = map[string]interface{}{
|
||
"source_port": protoSpecific[0],
|
||
"destination_port": protoSpecific[1],
|
||
"data_length": protoSpecific[2],
|
||
"tcp_flags": protoSpecific[3],
|
||
"sequence_number": protoSpecific[4],
|
||
"ack_number": protoSpecific[5],
|
||
"tcp_window": protoSpecific[6],
|
||
"urg": protoSpecific[7],
|
||
"tcp_options": protoSpecific[8],
|
||
}
|
||
} else {
|
||
protocolSpecificData["raw"] = protoSpecific
|
||
}
|
||
case "udp":
|
||
// Expect 3 fields for UDP: <source-port>, <destination-port>, <data-length>
|
||
if len(protoSpecific) >= 3 {
|
||
protocolSpecificData = map[string]interface{}{
|
||
"source_port": protoSpecific[0],
|
||
"destination_port": protoSpecific[1],
|
||
"data_length": protoSpecific[2],
|
||
}
|
||
} else {
|
||
protocolSpecificData["raw"] = protoSpecific
|
||
}
|
||
case "icmp":
|
||
// ICMP data may vary; output raw fields.
|
||
protocolSpecificData["raw"] = protoSpecific
|
||
case "carp":
|
||
// Expect 6 fields for CARP: <carp-type>, <carp-ttl>, <vhid>, <version>, <advbase>, <advskew>
|
||
if len(protoSpecific) >= 6 {
|
||
protocolSpecificData = map[string]interface{}{
|
||
"carp_type": protoSpecific[0],
|
||
"carp_ttl": protoSpecific[1],
|
||
"vhid": protoSpecific[2],
|
||
"version": protoSpecific[3],
|
||
"advbase": protoSpecific[4],
|
||
"advskew": protoSpecific[5],
|
||
}
|
||
} else {
|
||
protocolSpecificData["raw"] = protoSpecific
|
||
}
|
||
default:
|
||
// For unknown protocols, return raw protocol-specific fields.
|
||
if len(protoSpecific) > 0 {
|
||
protocolSpecificData["raw"] = protoSpecific
|
||
}
|
||
}
|
||
|
||
// Assemble the final JSON object.
|
||
result := map[string]interface{}{
|
||
"timestamp": timestamp,
|
||
"log_level": logLevel,
|
||
"log": map[string]interface{}{
|
||
"base": base,
|
||
"ip_specific_data": ipSpecificData,
|
||
"ip_data": ipData,
|
||
"protocol_specific_data": protocolSpecificData,
|
||
},
|
||
}
|
||
|
||
jsonData, err := json.Marshal(result)
|
||
if err != nil {
|
||
fmt.Fprintf(os.Stderr, "Error marshaling JSON: %v\n", err)
|
||
continue
|
||
}
|
||
fmt.Println(string(jsonData))
|
||
}
|
||
|
||
if err := scanner.Err(); err != nil {
|
||
fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err)
|
||
os.Exit(1)
|
||
}
|
||
}
|