-
Notifications
You must be signed in to change notification settings - Fork 27
Expand file tree
/
Copy pathplaceholder_map.go
More file actions
132 lines (113 loc) · 4.75 KB
/
Copy pathplaceholder_map.go
File metadata and controls
132 lines (113 loc) · 4.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
// or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
// https://securityonion.net/license; you may not use this file except in compliance with the
// Elastic License 2.0.
package playbook
import (
"regexp"
"github.com/security-onion-solutions/securityonion-soc/model"
"github.com/apex/log"
"gopkg.in/yaml.v3"
)
// eventDataPrefix is where a Sigma (Elastalert) alert document nests the original triggering event.
// Alert-level metadata (rule.uuid, soc_id) sits at the top level; the fired event's own
// fields live under event_data.*
const eventDataPrefix = "event_data."
var placeholderUseRe = regexp.MustCompile(`%([^%\s]+)%`)
// extractPlaceholders returns the set of resolvable %token% names used across the given
// query strings, skipping escaped \%name%\.
func extractPlaceholders(queries ...string) map[string]bool {
used := map[string]bool{}
for _, q := range queries {
for _, idx := range placeholderUseRe.FindAllStringSubmatchIndex(q, -1) {
if idx[0] > 0 && q[idx[0]-1] == '\\' {
continue // escaped \%name% is literal text, not a placeholder
}
used[q[idx[2]:idx[3]]] = true
}
}
return used
}
// lookupEventValue resolves a field path against the alert document, trying the
// event_data.-nested original-event location first, then the bare alert-level path, then
// the document-id bridge (the SOC _id lives on EventRecord.Id, not in Payload). A
// present-but-null field is treated as absent, so it degrades to the NODATA fallback
// instead of injecting a null value (some sources ship explicit nulls)
func lookupEventValue(event *model.EventRecord, key string) (interface{}, bool) {
if event == nil {
return nil, false
}
if v, ok := event.Payload[eventDataPrefix+key]; ok && v != nil {
return v, true
}
if v, ok := event.Payload[key]; ok && v != nil {
return v, true
}
if key == socIdPayloadKey && event.Id != "" {
return event.Id, true
}
return nil, false
}
// mergePlaceholderMaps overlays the user map on top of the global (shipped) map, returning a
// new combined map.
func mergePlaceholderMaps(global, user map[string]string) map[string]string {
merged := make(map[string]string, len(global)+len(user))
for token, field := range global {
merged[token] = field
}
for token, field := range user {
merged[token] = field
}
return merged
}
// missingValueFallback fills a mapped placeholder whose value is absent from
// the event, so `sigma convert` doesn't raise on an unresolved placeholder.
// Unmapped placeholders get no var and still fail.
const missingValueFallback = "NODATA"
// loadPlaceholderMap reads one placeholder map YAML file (Sigma placeholder name -> event field
// path) into a map. Each file is an optional layer of the combined map (see mergePlaceholderMaps),
// so a missing, malformed, or empty file is non-fatal and simply contributes no tokens.
func (pdm *PlaybookDiskManager) loadPlaceholderMap(path string) map[string]string {
m := map[string]string{}
raw, err := pdm.ReadFile(path)
if err != nil {
log.WithError(err).WithField("path", path).Debug("no playbook placeholder map at path; treating as an empty layer")
return m
}
if err := yaml.Unmarshal(raw, &m); err != nil {
log.WithError(err).WithField("path", path).Warn("unable to parse playbook placeholder map; treating as an empty layer")
return map[string]string{}
}
return m
}
// socIdPayloadKey is the field name %document_id% resolves to (lookupEventValue bridges it
// to EventRecord.Id). Matches the soc_id name the case/escalation handlers give event.Id.
const socIdPayloadKey = "soc_id"
// buildVarsFromEvent builds the `vars:` block for `sigma convert` from the alert event.
//
// (1) Every declared token (the combined placeholder map = global map + user map) resolves to
// its event value via lookupEventValue, else the NODATA fallback
// (2) A token USED in a query but undeclared is tried as a direct field name; a hit
// covers "named the placeholder after a flat field", a miss is left ABSENT so
// value_placeholders fails rather than silently resolving to the fallback.
//
// List values pass through as-is; the backend renders them as an OR-list.
func (pdm *PlaybookDiskManager) buildVarsFromEvent(event *model.EventRecord, bindings map[string]string, used map[string]bool) map[string]interface{} {
vars := make(map[string]interface{}, len(bindings)+len(used))
for token, field := range bindings {
if v, ok := lookupEventValue(event, field); ok {
vars[token] = v
} else {
vars[token] = missingValueFallback
}
}
for token := range used {
if _, declared := bindings[token]; declared {
continue
}
if v, ok := lookupEventValue(event, token); ok {
vars[token] = v
}
}
return vars
}