Package sigmalite provides a parser and an execution engine
for the Sigma detection format.
rule, err := sigmalite.ParseRule([]byte(`
title: My example rule
detection:
keywords:
- foo
- bar
selection:
EventId: 1234
condition: keywords and selection
`))
if err != nil {
return err
}
entry := &sigmalite.LogEntry{
Message: "Hello foo",
Fields: map[string]string{
"EventId": "1234",
},
}
isMatch := rule.Detection.Matches(entry, nil)If you're collecting packets with tshark and export them with -T json,
use ParseTSharkJSON to turn the output into a slice of LogEntry values that can be matched against Sigma rules:
data, err := os.ReadFile("packets.json")
if err != nil {
return err
}
entries, err := sigmalite.ParseTSharkJSON(data)
if err != nil {
return err
}
for _, entry := range entries {
if rule.Detection.Matches(entry, nil) {
fmt.Println("matched packet", entry.Fields["frame.number"])
}
}go get github.com/runreveal/sigmaliteBuild the sigmalite command to evaluate Sigma rules against local log files:
go build ./cmd/sigmaliteRun the executable by supplying one or more -rule files and an -input log file. The
-input-format flag selects how the log file is parsed:
./sigmalite \
-rule rules/network.yaml \
-rule rules/process.yaml \
-input packets.json \
-input-format tshark-jsonEach match is emitted as a JSON object describing the rule, message, and flattened fields. Additional formats are available:
tshark-json(default): parse logs produced bytshark -T jsonjsonl: parse newline-delimited JSON objects where each object represents a log entryraw: treat each non-empty line as a message-only log entry
Rules are written in YAML format
and, at a minimum, must include a title and a detection:
title: My example rule
detection:
keywords:
- foo
- bar
selection:
EventId: 1234
condition: keywords and selectionThe condition field in the detection block is a logical expression
that joins other field selectors in the detection block.
In this example, this rule will match any log entry
that has an EventId field that is exactly 1234
and has "foo" or "bar" in its message.
Fields can also be matched using regular expressions:
title: My example rule with a timestamp
detection:
selection:
Timestamp|re: ^2024-06-01T(01|02|03):[0-5][0-9]:[0-5][0-9]$
condition: selectionAs well as CIDRs:
title: My example rule with IP addresses
detection:
local:
DestinationIp|cidr:
- "127.0.0.0/8"
- "10.0.0.0/8"
- "172.16.0.0/12"
- "192.168.0.0/16"
- "169.254.0.0/16"
- "::1/128" # IPv6 loopback
- "fe80::/10" # IPv6 link-local addresses
- "fc00::/7" # IPv6 private addresses
condition: not localMore information can be found in the official Sigma rules documentation.
This library supports the following field modifiers:
The FieldResolver interface extends the standard Sigma specification to support complex field lookup scenarios that go beyond simple key/value pairs. This allows you to implement custom field resolution logic for:
- Nested JSON structures: Access deeply nested fields using dot notation (e.g.,
event.process.user) - Array handling: Extract values from arrays or lists within log entries
- Wildcard matching: Support field patterns like
process.*.userornetwork[*].ip - Multiple field aggregation: Combine values from multiple related fields
- Case normalization: Handle field name variations and case sensitivity
- Complex data transformations: Apply custom logic before field matching
- External datasource lookups: Lookup field values from an external datasource.
type FieldResolver interface {
Resolve(fieldName string, entry *LogEntry) []string
}The Resolve method takes a field name from your Sigma rule and returns all matching values as a string slice. If no matches are found, return nil or an empty slice.
// CustomResolver demonstrates field resolution for structured logs
type CustomResolver struct{}
func (r *CustomResolver) Resolve(fieldName string, entry *sigma.LogEntry) []string {
switch fieldName {
case "process.users":
// Aggregate user fields from multiple sources
var users []string
if user, ok := entry.Fields["Event.Process.User"]; ok {
users = append(users, user)
}
if user, ok := entry.Fields["Event.Login.User"]; ok {
users = append(users, user)
}
if user, ok := entry.Fields["Event.Session.User"]; ok {
users = append(users, user)
}
return users
case "network.internal_ips":
// Extract all IP addresses from network-related fields
var ips []string
for fieldName, value := range entry.Fields {
if strings.Contains(strings.ToLower(fieldName), "ip") {
// Simple IP validation (in real usage, use proper validation)
if strings.Contains(value, ".") {
ips = append(ips, value)
}
}
}
return ips
default:
return nil
}
}
func matches(detection *sigmalite.Detection) bool {
opts := &sigmalite.MatchOptions{
FieldResolver: CustomResolver{},
},
entry := &sigmalite.LogEntry{
Message: string("Message Text"),
Fields: nil, // Using resolver so this can be empty
}
return detection.Matches(entry, opts)
}Field Resolvers work seamlessly with all field modifiers, allowing you to apply regex patterns, case-insensitive matching, and other transformations to the resolved values.