Skip to content

Commit

Permalink
Feat/refactor (#31)
Browse files Browse the repository at this point in the history
* Bump go version for testing

* Refactored implementation
  • Loading branch information
pcktdmp authored Oct 1, 2024
1 parent 4bb7093 commit 441e32f
Show file tree
Hide file tree
Showing 8 changed files with 270 additions and 94 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,4 @@ jobs:
fi
- name: Test
run: go test -v
run: cd cefevent && go test -v
34 changes: 21 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,20 @@ Go Package for ArcSight's Common Event Format

# Motivation

Learning Go, help people who need to generate CEF events in Golang.
Learning Go, help people who need to process CEF events in Golang.

## TL;DR

`cefevent` is a [loose implementation](#Not-implemented) of the Common Event Format, the one who processes events
needs to handle the [known](https://www.microfocus.com/documentation/arcsight/arcsight-smartconnectors-8.4/pdfdoc/cef-implementation-standard/cef-implementation-standard.pdf) field limits.

### Install the package

```bash
$ go get github.com/pcktdmp/cef/cefevent
```

### cef.go
### examples.go

```go
package main
Expand Down Expand Up @@ -44,8 +47,7 @@ func main() {
Extensions: f,
}

cef, _ := event.Generate()
fmt.Println(cef)
fmt.Println(event.String())

// send a CEF event as log message to stdout
event.Log()
Expand All @@ -57,23 +59,29 @@ func main() {
if err != nil {
fmt.Println("Need to handle this.")
}

// if you want read a CEF event from a line
eventLine := "CEF:0|Cool Vendor|Cool Product|1.0|COOL_THING|Something cool happened.|Unknown|src=127.0.0.1"
newEvent := cefevent.CefEvent{}
newEvent.Read(eventLine)


// if you want read a CEF event from a line
eventLine := "CEF:0|Cool Vendor|Cool Product|1.0|COOL_THING|Something cool happened.|Unknown|src=127.0.0.1"
newEvent := cefevent.CefEvent{}
newEvent.Read(eventLine)
eventString, err := newEvent.String()
if err != nil {
fmt.Println("Need to handle this.")
}
fmt.Println(eventString)

}

```
### Example output

```bash
$ go run cef.go
$ go run examples.go
CEF:0|Cool Vendor|Cool Product|1.0|FLAKY_EVENT|Something flaky happened.|3|requestClientApplication=Go-http-client/1.1 src=127.0.0.1
2020/03/12 21:28:19 CEF:0|Cool Vendor|Cool Product|1.0|FLAKY_EVENT|Something flaky happened.|3|requestClientApplication=Go-http-client/1.1 src=127.0.0.1
2020/03/12 21:28:19 CEF:0|Cool Vendor|Cool Product|1.0|FLAKY_EVENT|Something flaky happened.|3|requestClientApplication=Go-http-client/1.1 src=127.0.0.1
```

## Not yet implemented
## Not implemented

* Field limits according to format standard for [known](https://community.microfocus.com/t5/ArcSight-Connectors/ArcSight-Common-Event-Format-CEF-Implementation-Standard/ta-p/1645557?attachment-id=68077) CEF fields
* Field limits according to format standard for CEF fields
3 changes: 0 additions & 3 deletions TODO

This file was deleted.

178 changes: 128 additions & 50 deletions cefevent/cefevent.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cefevent

import (
"encoding/json"
"errors"
"fmt"
"log"
Expand All @@ -12,27 +13,29 @@ import (
)

// CefEventer defines the interface for handling Common Event Format (CEF) events.
// It includes methods to generate, validate, read, and log CEF events.
// It includes methods to create (String()), Validate(), Read(), and Log() CEF events.
type CefEventer interface {
Generate() (string, error) // Generate constructs and returns a CEF message string if all the mandatory fields are set.
Validate() bool // Validate checks whether all mandatory fields in the CefEvent struct are set.
Validate() error // Validate if the CEF message is according to the specification.
String() (string, error) // String constructs and returns a CEF message string.
Build() (CefEvent, error) // Build constructs and returns a CEF message according to CefEvent.
Read(line string) (CefEvent, error) // Read parses a CEF message string and populates the CefEvent struct with the extracted data.
Log() (bool, error) // Log attempts to generate a CEF message from the current CefEvent and logs it to the standard output.
Log() error // Log attempts to generate a CEF message from the current CefEvent and logs it to the standard output.
escapeEventData() error // escapeEventData will try to escape all data properly in the struct according the Common Event Format.
}

// CefEvent represents a Common Event Format (CEF) event.
// It includes fields for CEF version, device vendor, device product, device version,
// device event class ID, event name, event severity, and additional extensions.
type CefEvent struct {
// defaults to 0 which is also the first CEF version.
Version int
DeviceVendor string
DeviceProduct string
DeviceVersion string
DeviceEventClassId string
Name string
Severity string
Extensions map[string]string
Version int `json:"Version" yaml:"Version" toml:"Version" xml:"Version" header:"CEF Version" comment:"The version of the CEF specification that the event conforms to."`
DeviceVendor string `json:"DeviceVendor" yaml:"DeviceVendor" toml:"DeviceVendor" xml:"DeviceVendor" header:"Device Vendor" comment:"The name of the device vendor."`
DeviceProduct string `json:"DeviceProduct" yaml:"DeviceProduct" toml:"DeviceProduct" xml:"DeviceProduct" header:"Device Product" comment:"The name of the device product."`
DeviceVersion string `json:"DeviceVersion" yaml:"DeviceVersion" toml:"DeviceVersion" xml:"DeviceVersion" header:"Device Version" comment:"The version of the device product."`
DeviceEventClassId string `json:"DeviceEventClassId" yaml:"DeviceEventClassId" toml:"DeviceEventClassId" xml:"DeviceEventClassId" header:"Device Event Class ID" comment:"The ID of the event class that the event conforms to."`
Name string `json:"Name" yaml:"Name" toml:"Name" xml:"Name" header:"Name" comment:"The name of the event."`
Severity string `json:"Severity" yaml:"Severity" toml:"Severity" xml:"Severity" header:"Severity" comment:"The severity of the event."`
Extensions map[string]string `json:"Extensions,omitempty" yaml:"Extensions" toml:"Extensions" xml:"Extensions" header:"Extensions" comment:"Additional extensions to the CEF message."`
}

// cefEscapeField escapes special characters in a given string that are used in CEF (Common Event Format) fields.
Expand Down Expand Up @@ -82,16 +85,53 @@ func cefEscapeExtension(field string) string {
return replacer.Replace(field)
}

// escapeEventData processes and escapes all necessary fields within the CefEvent struct according
// to the Common Event Format (CEF) specifications. It ensures that fields such as DeviceVendor,
// DeviceProduct, DeviceVersion, DeviceEventClassId, Name, Severity, and Extensions have their
// special characters escaped properly to maintain the integrity of the CEF message.
//
// This function performs the following steps:
// - Escapes special characters in fields like DeviceVendor, DeviceProduct, DeviceVersion,
// DeviceEventClassId, Name, and Severity using the cefEscapeField helper function.
// - Iterates over the Extensions map and escapes both the keys and values using the
// cefEscapeExtension helper function, ensuring no duplicated keys in the resulting map.
//
// Returns:
// - An error if there is any issue during the escaping process; otherwise, returns nil.
func (event *CefEvent) escapeEventData() error {

event.DeviceVendor = cefEscapeField(event.DeviceVendor)
event.DeviceProduct = cefEscapeField(event.DeviceProduct)
event.DeviceVersion = cefEscapeField(event.DeviceVersion)
event.DeviceEventClassId = cefEscapeField(event.DeviceEventClassId)
event.Name = cefEscapeField(event.Name)
event.Severity = cefEscapeField(event.Severity)

// TODO: memory usage improvement
// simple method to make sure escaped strings are not duped in the map keys
escapedExtensions := make(map[string]string)

if len(event.Extensions) > 0 {
for k, v := range event.Extensions {
escapedExtensions[cefEscapeExtension(k)] = cefEscapeExtension(v)
}
}

event.Extensions = escapedExtensions

return nil
}

// Validate verifies whether all mandatory fields in the CefEvent struct are set.
// It checks if the fields Version, DeviceVendor, DeviceProduct, DeviceVersion,
// DeviceEventClassId, Name, and Severity are populated and returns true if they are,
// otherwise, it returns false.
// DeviceEventClassId, Name, and Severity are populated and returns nil if they are,
// otherwise, it returns an error.
//
// This method uses reflection to loop over the mandatory fields and check their values.
//
// Returns:
// - A boolean indicating whether all mandatory fields are set (true) or not (false).
func (event *CefEvent) Validate() bool {
// - An error message indicating whether all mandatory fields are set (err) or not (nil).
func (event *CefEvent) Validate() error {

assertEvent := reflect.ValueOf(event).Elem()

Expand All @@ -113,12 +153,11 @@ func (event *CefEvent) Validate() bool {
for _, field := range mandatoryFields {

if assertEvent.FieldByName(field).String() == "" {
return false
return errors.New("not all mandatory CEF fields are set")
}
}

return true

return nil
}

// Log attempts to generate a CEF message from the current CefEvent
Expand All @@ -131,25 +170,42 @@ func (event *CefEvent) Validate() bool {
// it logs an error message to stderr.
//
// Returns:
// - A boolean indicating whether the logging operation succeeded (true) or failed (false).
// - An error if the logging operation could not be completed due to a failure in generating the CEF message.
func (event *CefEvent) Log() (bool, error) {
// - An error indicating whether the logging operation succeeded (nil) or failed (err).
func (event *CefEvent) Log() error {

logMessage, err := event.Generate()
logMessage, err := event.String()

if err != nil {
log.SetOutput(os.Stderr)
errMsg := "unable to generate and thereby log the CEF message"
errMsg := "unable to create and thereby log the CEF message"
log.Println(errMsg)
return false, errors.New(errMsg)
return errors.New(errMsg)
}

log.SetOutput(os.Stdout)
log.Println(logMessage)
return true, nil
return nil
}

// Generate constructs and returns a CEF (Common Event Format) message string if all the mandatory
// Build constructs and returns a CEF (Common Event Format) message just as String() but then as CefEvent type.
//
// Returns:
// - A CefEvent type representing the CEF message.
// - An error if any mandatory field is missing or if there are other issues during generation.
func (event *CefEvent) Build() (CefEvent, error) {

if event.Validate() != nil {
return CefEvent{}, errors.New("not all mandatory CEF fields are set")
}

if event.escapeEventData() != nil {
return CefEvent{}, errors.New("unable to escape CEF event data")
}

return *event, nil
}

// String constructs and returns a CEF (Common Event Format) message string if all the mandatory
// fields are set in the CefEvent. If any mandatory field is missing, it returns an error.
//
// A CEF message follows the format:
Expand All @@ -158,20 +214,17 @@ func (event *CefEvent) Log() (bool, error) {
// Each field is escaped to ensure that special characters do not interfere with the CEF format.
//
// Returns:
// - A string representing the generated CEF message.
// - A string representing the CEF message.
// - An error if any mandatory field is missing or if there are other issues during generation.
func (event CefEvent) Generate() (string, error) {
func (event *CefEvent) String() (string, error) {

if !CefEventer.Validate(&event) {
if CefEventer.Validate(event) != nil {
return "", errors.New("not all mandatory CEF fields are set")
}

event.DeviceVendor = cefEscapeField(event.DeviceVendor)
event.DeviceProduct = cefEscapeField(event.DeviceProduct)
event.DeviceVersion = cefEscapeField(event.DeviceVersion)
event.DeviceEventClassId = cefEscapeField(event.DeviceEventClassId)
event.Name = cefEscapeField(event.Name)
event.Severity = cefEscapeField(event.Severity)
if event.escapeEventData() != nil {
return "", errors.New("unable to escape CEF event data")
}

var p strings.Builder

Expand All @@ -185,8 +238,8 @@ func (event CefEvent) Generate() (string, error) {
for _, k := range sortedExtensions {
p.WriteString(fmt.Sprintf(
"%s=%s ",
cefEscapeExtension(k),
cefEscapeExtension(event.Extensions[k])),
k,
event.Extensions[k]),
)
}

Expand Down Expand Up @@ -220,7 +273,7 @@ func (event CefEvent) Generate() (string, error) {
// Returns:
// - A CefEvent struct populated with the parsed CEF message data.
// - An error if the CEF message is improperly formatted or if any mandatory field is missing.
func (event CefEvent) Read(eventLine string) (CefEvent, error) {
func (event *CefEvent) Read(eventLine string) (CefEvent, error) {
if strings.HasPrefix(eventLine, "CEF:") {
eventSlashed := strings.Split(strings.TrimPrefix(eventLine, "CEF:"), "|")

Expand All @@ -230,6 +283,7 @@ func (event CefEvent) Read(eventLine string) (CefEvent, error) {
return CefEvent{}, err
}

event.Version = cefVersion
parsedExtensions := make(map[string]string)

// each extension k,v is separated by a " ".
Expand All @@ -244,22 +298,46 @@ func (event CefEvent) Read(eventLine string) (CefEvent, error) {
}
}

eventParsed := CefEvent{
Version: cefVersion,
DeviceVendor: eventSlashed[1],
DeviceProduct: eventSlashed[2],
DeviceVersion: eventSlashed[3],
DeviceEventClassId: eventSlashed[4],
Name: eventSlashed[5],
Severity: eventSlashed[6],
Extensions: parsedExtensions,
event.DeviceVendor = eventSlashed[1]
event.DeviceProduct = eventSlashed[2]
event.DeviceVersion = eventSlashed[3]
event.DeviceEventClassId = eventSlashed[4]
event.Name = eventSlashed[5]
event.Severity = eventSlashed[6]
event.Extensions = parsedExtensions

if event.escapeEventData() != nil {
return CefEvent{}, errors.New("could not escape CEF event data")
}

if !CefEventer.Validate(&eventParsed) {
if CefEventer.Validate(event) != nil {
return CefEvent{}, errors.New("not all mandatory CEF fields are set")
}

return eventParsed, nil
return *event, nil
}
return CefEvent{}, errors.New("not a valid CEF message")
}

// ToJSON converts the CefEvent instance to a JSON string.
//
// This method first validates the CefEvent to ensure all mandatory fields are set,
// and then attempts to marshal the event into a JSON formatted string.
//
// Returns:
// - A JSON string representation of the CefEvent if successful.
// - An error if the CefEvent is not valid or if there is an error during the JSON marshaling process.
func (event *CefEvent) ToJSON() (string, error) {
// Validate the event before converting to JSON
if err := event.Validate(); err != nil {
return "", err
}

// Attempt to convert the event to JSON
jsonData, err := json.Marshal(event)
if err != nil {
return "", err
}

return string(jsonData), nil
}
Loading

0 comments on commit 441e32f

Please sign in to comment.