logstash-codec-cef 6.1.2-java → 6.2.0-java

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b911dab6fca1f37b8edd22e8ea97759674df3301bb0afafacf14343c84237088
4
- data.tar.gz: ebad9548b63d7a5f6f3bc92e74166700599b75e7425df1fd471cfa111399b098
3
+ metadata.gz: 25264d450cdfa027ac9758a05972c0c87119a635238b9bacfc746e4daec40dff
4
+ data.tar.gz: 63dbd8558c231e61c9e55962484042a52c28177cba5b4adcf4d49291aace491a
5
5
  SHA512:
6
- metadata.gz: b2cea62a7689f4d15791338167a5f105a5770121a0069f52607e28bb7fb1e7081eb039d4916d78dbe18f288bdc4c49a11cb69945e1721709ae5b0905c1497c0c
7
- data.tar.gz: 648f0c5fd204ddc940f8d7d53cfd3bbd4d7493b607dc78058b5a11bfc5cdbeda898cd6860743be84af7c1ea5260327983300b1bbaecd314901a923c4c489acfe
6
+ metadata.gz: 62ac45d798abaf3008f99357b578506237bddc024904951cab64bdb36fd61f1c91894e4e70f2ce6a911fde46c4d1a15c69a1368b2a0dd69a6f509e09e8ee8192
7
+ data.tar.gz: 46dd75f39b72ae788dfcd29f21810be7086bb16cb1984f81f50a786682d66f06f7e6a2ee543f3e88a3e8951da5680f99fdcd45d28e606c16171d34bc30ceb4a4
data/CHANGELOG.md CHANGED
@@ -1,3 +1,6 @@
1
+ ## 6.2.0
2
+ - Introduce ECS Compatibility mode [#83](https://github.com/logstash-plugins/logstash-codec-cef/pull/83).
3
+
1
4
  ## 6.1.2
2
5
  - Added error log with full payload when something bad happens in decoding a message[#84](https://github.com/logstash-plugins/logstash-codec-cef/pull/84)
3
6
 
data/docs/index.asciidoc CHANGED
@@ -27,14 +27,53 @@ https://community.saas.hpe.com/dcvta86296/attachments/dcvta86296/connector-docum
27
27
  If this codec receives a payload from an input that is not a valid CEF message, then it will
28
28
  produce an event with the payload as the 'message' field and a '_cefparsefailure' tag.
29
29
 
30
+ ==== Compatibility with the Elastic Common Schema (ECS)
31
+
32
+ This plugin can be used to decode CEF events _into_ the Elastic Common Schema, or to encode ECS-compatible events into CEF.
33
+ It can also be used _without_ ECS, encoding and decoding events using only CEF-defined field names and keys.
34
+
35
+ The ECS Compatibility mode for a specific plugin instance can be controlled by setting <<plugins-{type}s-{plugin}-ecs_compatibility>> when defining the codec:
36
+
37
+ [source,sh]
38
+ -----
39
+ input {
40
+ tcp {
41
+ # ...
42
+ codec => cef {
43
+ ecs_compatibility => v1
44
+ }
45
+ }
46
+ }
47
+ -----
48
+
49
+ If left unspecified, the value of the `pipeline.ecs_compatibility` setting is used.
50
+
51
+ ===== Timestamps and ECS Compatiblity
52
+
53
+ When running in ECS Compatibility Mode, timestamp-type fields are parsed and normalized
54
+ to specific points on the timeline.
55
+
56
+ Because the CEF format allows ambiguous timestamp formats, some reasonable assumptions are made:
57
+
58
+ - When the timestamp does not include a year, we assume it happened in the recent past
59
+ (or _very_ near future to accommodate out-of-sync clocks and timezone offsets).
60
+ - When the timestamp does not include UTC-offset information, we use the event's
61
+ timezone (`dtz` or `deviceTimeZone` field), or fall through to this plugin's
62
+ <<plugins-{type}s-{plugin}-default_timezone>>.
63
+ - Localized timestamps are parsed using the provided <<plugins-{type}s-{plugin}-locale>>.
64
+
30
65
  [id="plugins-{type}s-{plugin}-options"]
31
66
  ==== Cef Codec Configuration Options
32
67
 
33
68
  [cols="<,<,<",options="header",]
34
69
  |=======================================================================
35
70
  |Setting |Input type|Required
71
+ | <<plugins-{type}s-{plugin}-default_timezone>> |<<string,string>>|No
36
72
  | <<plugins-{type}s-{plugin}-delimiter>> |<<string,string>>|No
73
+ | <<plugins-{type}s-{plugin}-device>> |<<string,string>>|No
74
+ | <<plugins-{type}s-{plugin}-ecs_compatibility>> |<<string,string>>|No
37
75
  | <<plugins-{type}s-{plugin}-fields>> |<<array,array>>|No
76
+ | <<plugins-{type}s-{plugin}-locale>> |<<string,string>>|No
38
77
  | <<plugins-{type}s-{plugin}-name>> |<<string,string>>|No
39
78
  | <<plugins-{type}s-{plugin}-product>> |<<string,string>>|No
40
79
  | <<plugins-{type}s-{plugin}-reverse_mapping>> |<<boolean,boolean>>|No
@@ -46,6 +85,21 @@ produce an event with the payload as the 'message' field and a '_cefparsefailure
46
85
 
47
86
  &nbsp;
48
87
 
88
+ [id="plugins-{type}s-{plugin}-default_timezone"]
89
+ ===== `default_timezone`
90
+
91
+ * Value type is <<string,string>>
92
+ * Supported values are:
93
+ ** https://en.wikipedia.org/wiki/List_of_tz_database_time_zones[Timezone names] (such as `Europe/Moscow`, `America/Argentina/Buenos_Aires`)
94
+ ** UTC Offsets (such as `-08:00`, `+03:00`)
95
+ * The default value is your system time zone
96
+ * This option has no effect when _encoding_.
97
+
98
+ When parsing timestamp fields in ECS mode and encountering timestamps that
99
+ do not contain UTC-offset information, the `deviceTimeZone` (`dtz`) field
100
+ from the CEF payload is used to interpret the given time. If the event does
101
+ not include timezone information, this `default_timezone` is used instead.
102
+
49
103
  [id="plugins-{type}s-{plugin}-delimiter"]
50
104
  ===== `delimiter`
51
105
 
@@ -69,21 +123,71 @@ This setting allows the following character sequences to have special meaning:
69
123
  * `\\r` (backslash "r") - means carriage return (ASCII 0x0D)
70
124
  * `\\n` (backslash "n") - means newline (ASCII 0x0A)
71
125
 
126
+ [id="plugins-{type}s-{plugin}-device"]
127
+ ===== `device`
128
+
129
+ * Value type is <<string,string>>
130
+ * Supported values are:
131
+ ** `observer`: indicates that device-specific fields represent the device used to _observe_ the event.
132
+ ** `host`: indicates that device-specific fields represent the device on which the event _occurred_.
133
+ * The default value for this setting is `observer`.
134
+ * Option has no effect when <<plugins-{type}s-{plugin}-ecs_compatibility,`ecs_compatibility => disabled`>>.
135
+ * Option has no effect when _encoding_
136
+
137
+ Defines a set of device-specific CEF fields as either representing the device on which an
138
+ event _occurred_, or merely the device from which the event was _observed_.
139
+ This causes the relevant fields to be routed to either the `host` or the `observer`
140
+ top-level groupings.
141
+
142
+ If the codec handles data from a variety of sources, the ECS recommendation is to use `observer`.
143
+
144
+ [id="plugins-{type}s-{plugin}-ecs_compatibility"]
145
+ ===== `ecs_compatibility`
146
+
147
+ * Value type is <<string,string>>
148
+ * Supported values are:
149
+ ** `disabled`: uses CEF-defined field names in the event (e.g., `bytesIn`, `sourceAddress`)
150
+ ** `v1`: supports ECS-compatible event fields (e.g., `[source][bytes]`, `[source][ip]`)
151
+ * Default value depends on which version of Logstash is running:
152
+ ** When Logstash provides a `pipeline.ecs_compatibility` setting, its value is used as the default
153
+ ** Otherwise, the default value is `disabled`.
154
+
155
+ Controls this plugin's compatibility with the
156
+ {ecs-ref}[Elastic Common Schema (ECS)]
157
+ (ECS)].
158
+
72
159
  [id="plugins-{type}s-{plugin}-fields"]
73
160
  ===== `fields`
74
161
 
75
162
  * Value type is <<array,array>>
76
163
  * Default value is `[]`
164
+ * Option has no effect when _decoding_
165
+
166
+ When this codec is used in an Output Plugin, a list of fields can be provided to be included in CEF extensions part as key/value pairs.
77
167
 
78
- Fields to be included in CEV extension part as key/value pairs
168
+ [id="plugins-{type}s-{plugin}-locale"]
169
+ ===== `locale`
170
+
171
+ * Value type is <<string,string>>
172
+ * Supported values are:
173
+ ** Abbreviated language_COUNTRY format (e.g., `en_GB`, `pt_BR`)
174
+ ** Valid https://tools.ietf.org/html/bcp47[IETF BCP 47] language tag (e.g., `zh-cmn-Hans-CN`)
175
+ * The default value is your system locale
176
+ * Option has no effect when _encoding_
177
+
178
+ When parsing timestamp fields in ECS mode and encountering timestamps in
179
+ a localized format, this `locale` is used to interpret locale-specific strings
180
+ such as month abbreviations.
79
181
 
80
182
  [id="plugins-{type}s-{plugin}-name"]
81
183
  ===== `name`
82
184
 
83
185
  * Value type is <<string,string>>
84
186
  * Default value is `"Logstash"`
187
+ * Option has no effect when _decoding_
85
188
 
86
- Name field in CEF header. The new value can include `%{foo}` strings
189
+ When this codec is used in an Output Plugin, this option can be used to specify the
190
+ value of the name field in the CEF header. The new value can include `%{foo}` strings
87
191
  to help you build a new value from other parts of the event.
88
192
 
89
193
  [id="plugins-{type}s-{plugin}-product"]
@@ -91,8 +195,10 @@ to help you build a new value from other parts of the event.
91
195
 
92
196
  * Value type is <<string,string>>
93
197
  * Default value is `"Logstash"`
198
+ * Option has no effect when _decoding_
94
199
 
95
- Device product field in CEF header. The new value can include `%{foo}` strings
200
+ When this codec is used in an Output Plugin, this option can be used to specify the
201
+ value of the device product field in CEF header. The new value can include `%{foo}` strings
96
202
  to help you build a new value from other parts of the event.
97
203
 
98
204
 
@@ -101,6 +207,7 @@ to help you build a new value from other parts of the event.
101
207
 
102
208
  * Value type is <<boolean,boolean>>
103
209
  * Default value is `false`
210
+ * Option has no effect when _decoding_
104
211
 
105
212
  Set to true to adhere to the specifications and encode using the CEF key name (short name) for the CEF field names.
106
213
 
@@ -109,8 +216,10 @@ Set to true to adhere to the specifications and encode using the CEF key name (s
109
216
 
110
217
  * Value type is <<string,string>>
111
218
  * Default value is `"6"`
219
+ * Option has no effect when _decoding_
112
220
 
113
- Severity field in CEF header. The new value can include `%{foo}` strings
221
+ When this codec is used in an Output Plugin, this option can be used to specify the
222
+ value of the severity field in CEF header. The new value can include `%{foo}` strings
114
223
  to help you build a new value from other parts of the event.
115
224
 
116
225
  Defined as field of type string to allow sprintf. The value will be validated
@@ -122,8 +231,10 @@ All invalid values will be mapped to the default of 6.
122
231
 
123
232
  * Value type is <<string,string>>
124
233
  * Default value is `"Logstash"`
234
+ * Option has no effect when _decoding_
125
235
 
126
- Signature ID field in CEF header. The new value can include `%{foo}` strings
236
+ When this codec is used in an Output Plugin, this option can be used to specify the
237
+ value of the signature ID field in CEF header. The new value can include `%{foo}` strings
127
238
  to help you build a new value from other parts of the event.
128
239
 
129
240
  [id="plugins-{type}s-{plugin}-vendor"]
@@ -131,8 +242,10 @@ to help you build a new value from other parts of the event.
131
242
 
132
243
  * Value type is <<string,string>>
133
244
  * Default value is `"Elasticsearch"`
245
+ * Option has no effect when _decoding_
134
246
 
135
- Device vendor field in CEF header. The new value can include `%{foo}` strings
247
+ When this codec is used in an Output Plugin, this option can be used to specify the
248
+ value of the device vendor field in CEF header. The new value can include `%{foo}` strings
136
249
  to help you build a new value from other parts of the event.
137
250
 
138
251
  [id="plugins-{type}s-{plugin}-version"]
@@ -140,8 +253,9 @@ to help you build a new value from other parts of the event.
140
253
 
141
254
  * Value type is <<string,string>>
142
255
  * Default value is `"1.0"`
256
+ * Option has no effect when _decoding_
143
257
 
144
- Device version field in CEF header. The new value can include `%{foo}` strings
258
+ When this codec is used in an Output Plugin, this option can be used to specify the
259
+ value of the device version field in CEF header. The new value can include `%{foo}` strings
145
260
  to help you build a new value from other parts of the event.
146
261
 
147
-
@@ -3,6 +3,9 @@ require "logstash/util/buftok"
3
3
  require "logstash/util/charset"
4
4
  require "logstash/codecs/base"
5
5
  require "json"
6
+ require "time"
7
+
8
+ require 'logstash/plugin_mixins/ecs_compatibility_support'
6
9
 
7
10
  # Implementation of a Logstash codec for the ArcSight Common Event Format (CEF)
8
11
  # Based on Revision 20 of Implementing ArcSight CEF, dated from June 05, 2013
@@ -13,6 +16,10 @@ require "json"
13
16
  class LogStash::Codecs::CEF < LogStash::Codecs::Base
14
17
  config_name "cef"
15
18
 
19
+ include LogStash::PluginMixins::ECSCompatibilitySupport(:disabled, :v1)
20
+
21
+ InvalidTimestamp = Class.new(StandardError)
22
+
16
23
  # Device vendor field in CEF header. The new value can include `%{foo}` strings
17
24
  # to help you build a new value from other parts of the event.
18
25
  config :vendor, :validate => :string, :default => "Elasticsearch"
@@ -68,106 +75,24 @@ class LogStash::Codecs::CEF < LogStash::Codecs::Base
68
75
  # * `\\n` (backslash "n") - means newline (ASCII 0x0A)
69
76
  config :delimiter, :validate => :string
70
77
 
78
+ # When parsing timestamps that do not include a UTC offset in payloads that do not
79
+ # include the device's timezone, the default timezone is used.
80
+ # If none is provided the system timezone is used.
81
+ config :default_timezone, :validate => :string
82
+
83
+ # The locale is used to parse abbreviated month names from some CEF timestamp
84
+ # formats.
85
+ # If none is provided, the system default is used.
86
+ config :locale, :validate => :string
87
+
71
88
  # If raw_data_field is set, during decode of an event an additional field with
72
89
  # the provided name is added, which contains the raw data.
73
90
  config :raw_data_field, :validate => :string
74
91
 
75
- HEADER_FIELDS = ['cefVersion','deviceVendor','deviceProduct','deviceVersion','deviceEventClassId','name','severity']
76
-
77
- # Translating and flattening the CEF extensions with known field names as documented in the Common Event Format whitepaper
78
- MAPPINGS = {
79
- "act" => "deviceAction",
80
- "app" => "applicationProtocol",
81
- "c6a1" => "deviceCustomIPv6Address1",
82
- "c6a1Label" => "deviceCustomIPv6Address1Label",
83
- "c6a2" => "deviceCustomIPv6Address2",
84
- "c6a2Label" => "deviceCustomIPv6Address2Label",
85
- "c6a3" => "deviceCustomIPv6Address3",
86
- "c6a3Label" => "deviceCustomIPv6Address3Label",
87
- "c6a4" => "deviceCustomIPv6Address4",
88
- "c6a4Label" => "deviceCustomIPv6Address4Label",
89
- "cat" => "deviceEventCategory",
90
- "cfp1" => "deviceCustomFloatingPoint1",
91
- "cfp1Label" => "deviceCustomFloatingPoint1Label",
92
- "cfp2" => "deviceCustomFloatingPoint2",
93
- "cfp2Label" => "deviceCustomFloatingPoint2Label",
94
- "cfp3" => "deviceCustomFloatingPoint3",
95
- "cfp3Label" => "deviceCustomFloatingPoint3Label",
96
- "cfp4" => "deviceCustomFloatingPoint4",
97
- "cfp4Label" => "deviceCustomFloatingPoint4Label",
98
- "cn1" => "deviceCustomNumber1",
99
- "cn1Label" => "deviceCustomNumber1Label",
100
- "cn2" => "deviceCustomNumber2",
101
- "cn2Label" => "deviceCustomNumber2Label",
102
- "cn3" => "deviceCustomNumber3",
103
- "cn3Label" => "deviceCustomNumber3Label",
104
- "cnt" => "baseEventCount",
105
- "cs1" => "deviceCustomString1",
106
- "cs1Label" => "deviceCustomString1Label",
107
- "cs2" => "deviceCustomString2",
108
- "cs2Label" => "deviceCustomString2Label",
109
- "cs3" => "deviceCustomString3",
110
- "cs3Label" => "deviceCustomString3Label",
111
- "cs4" => "deviceCustomString4",
112
- "cs4Label" => "deviceCustomString4Label",
113
- "cs5" => "deviceCustomString5",
114
- "cs5Label" => "deviceCustomString5Label",
115
- "cs6" => "deviceCustomString6",
116
- "cs6Label" => "deviceCustomString6Label",
117
- "dhost" => "destinationHostName",
118
- "dmac" => "destinationMacAddress",
119
- "dntdom" => "destinationNtDomain",
120
- "dpid" => "destinationProcessId",
121
- "dpriv" => "destinationUserPrivileges",
122
- "dproc" => "destinationProcessName",
123
- "dpt" => "destinationPort",
124
- "dst" => "destinationAddress",
125
- "duid" => "destinationUserId",
126
- "duser" => "destinationUserName",
127
- "dvc" => "deviceAddress",
128
- "dvchost" => "deviceHostName",
129
- "dvcpid" => "deviceProcessId",
130
- "end" => "endTime",
131
- "fname" => "fileName",
132
- "fsize" => "fileSize",
133
- "in" => "bytesIn",
134
- "msg" => "message",
135
- "out" => "bytesOut",
136
- "outcome" => "eventOutcome",
137
- "proto" => "transportProtocol",
138
- "request" => "requestUrl",
139
- "rt" => "deviceReceiptTime",
140
- "shost" => "sourceHostName",
141
- "smac" => "sourceMacAddress",
142
- "sntdom" => "sourceNtDomain",
143
- "spid" => "sourceProcessId",
144
- "spriv" => "sourceUserPrivileges",
145
- "sproc" => "sourceProcessName",
146
- "spt" => "sourcePort",
147
- "src" => "sourceAddress",
148
- "start" => "startTime",
149
- "suid" => "sourceUserId",
150
- "suser" => "sourceUserName",
151
- "ahost" => "agentHostName",
152
- "art" => "agentReceiptTime",
153
- "at" => "agentType",
154
- "aid" => "agentId",
155
- "_cefVer" => "cefVersion",
156
- "agt" => "agentAddress",
157
- "av" => "agentVersion",
158
- "atz" => "agentTimeZone",
159
- "dtz" => "destinationTimeZone",
160
- "slong" => "sourceLongitude",
161
- "slat" => "sourceLatitude",
162
- "dlong" => "destinationLongitude",
163
- "dlat" => "destinationLatitude",
164
- "catdt" => "categoryDeviceType",
165
- "mrt" => "managerReceiptTime",
166
- "amac" => "agentMacAddress"
167
- }
168
-
169
- # Reverse mapping of CEF full field names to CEF extensions field names for encoding into a CEF event for output.
170
- REVERSE_MAPPINGS = MAPPINGS.invert
92
+ # Defines whether a set of device-specific CEF fields represent the _observer_,
93
+ # or the actual `host` on which the event occurred. If this codec handles a mix,
94
+ # it is safe to use the default `observer`.
95
+ config :device, :validate => %w(observer host), :default => 'observer'
171
96
 
172
97
  # A CEF Header is a sequence of zero or more:
173
98
  # - backslash-escaped pipes; OR
@@ -255,6 +180,12 @@ class LogStash::Codecs::CEF < LogStash::Codecs::Base
255
180
  @delimiter = @delimiter.gsub("\\r", "\r").gsub("\\n", "\n")
256
181
  @buffer = FileWatch::BufferedTokenizer.new(@delimiter)
257
182
  end
183
+
184
+ require_relative 'cef/timestamp_normalizer'
185
+ @timestamp_normalzer = TimestampNormalizer.new(locale: @locale, timezone: @default_timezone)
186
+
187
+ generate_header_fields!
188
+ generate_mappings!
258
189
  end
259
190
 
260
191
  public
@@ -286,7 +217,7 @@ class LogStash::Codecs::CEF < LogStash::Codecs::Base
286
217
 
287
218
  # Use a scanning parser to capture the HEADER_FIELDS
288
219
  unprocessed_data = data
289
- HEADER_FIELDS.each do |field_name|
220
+ @header_fields.each do |field_name|
290
221
  match_data = HEADER_SCANNER.match(unprocessed_data)
291
222
  break if match_data.nil? # missing fields
292
223
 
@@ -304,22 +235,24 @@ class LogStash::Codecs::CEF < LogStash::Codecs::Base
304
235
  message = unprocessed_data
305
236
 
306
237
  # Try and parse out the syslog header if there is one
307
- if (cef_version = event.get('cefVersion')).include?(' ')
238
+ cef_version_field = @header_fields[0]
239
+ if (cef_version = event.get(cef_version_field)).include?(' ')
308
240
  split_cef_version = cef_version.rpartition(' ')
309
- event.set('syslog', split_cef_version[0])
310
- event.set('cefVersion', split_cef_version[2])
241
+ event.set(@syslog_header, split_cef_version[0])
242
+ event.set(cef_version_field, split_cef_version[2])
311
243
  end
312
244
 
313
245
  # Get rid of the CEF bit in the version
314
- event.set('cefVersion', delete_cef_prefix(event.get('cefVersion')))
246
+ event.set(cef_version_field, delete_cef_prefix(event.get(cef_version_field)))
315
247
 
316
248
  # Use a scanning parser to capture the Extension Key/Value Pairs
317
249
  if message && message.include?('=')
318
250
  message = message.strip
251
+ extension_fields = {}
319
252
 
320
253
  message.scan(EXTENSION_KEY_VALUE_SCANNER) do |extension_field_key, raw_extension_field_value|
321
254
  # expand abbreviated extension field keys
322
- extension_field_key = MAPPINGS.fetch(extension_field_key, extension_field_key)
255
+ extension_field_key = @decode_mapping.fetch(extension_field_key, extension_field_key)
323
256
 
324
257
  # convert extension field name to strict legal field_reference, fixing field names with ambiguous array-like syntax
325
258
  extension_field_key = extension_field_key.sub(EXTENSION_KEY_ARRAY_CAPTURE, '[\1]\2') if extension_field_key.end_with?(']')
@@ -327,7 +260,21 @@ class LogStash::Codecs::CEF < LogStash::Codecs::Base
327
260
  # process legal extension field value escapes
328
261
  extension_field_value = raw_extension_field_value.gsub(EXTENSION_VALUE_ESCAPE_CAPTURE, '\1')
329
262
 
330
- event.set(extension_field_key, extension_field_value)
263
+ extension_fields[extension_field_key] = extension_field_value
264
+ end
265
+
266
+ # in ECS mode, normalize timestamps including timezone.
267
+ if ecs_compatibility != :disabled
268
+ device_timezone = extension_fields['[event][timezone]']
269
+ @timestamp_fields.each do |timestamp_field_name|
270
+ raw_timestamp = extension_fields.delete(timestamp_field_name) or next
271
+ value = normalize_timestamp(raw_timestamp, device_timezone)
272
+ event.set(timestamp_field_name, value)
273
+ end
274
+ end
275
+
276
+ extension_fields.each do |field_key, field_value|
277
+ event.set(field_key, field_value)
331
278
  end
332
279
  end
333
280
 
@@ -368,6 +315,234 @@ class LogStash::Codecs::CEF < LogStash::Codecs::Base
368
315
 
369
316
  private
370
317
 
318
+ def generate_header_fields!
319
+ # @header_fields is an _ordered_ set of fields.
320
+ @header_fields = [
321
+ ecs_select[disabled: 'cefVersion', v1: '[cef][version]'],
322
+ ecs_select[disabled: 'deviceVendor', v1: '[observer][vendor]'],
323
+ ecs_select[disabled: 'deviceProduct', v1: '[observer][product]'],
324
+ ecs_select[disabled: 'deviceVersion', v1: '[observer][version]'],
325
+ ecs_select[disabled: 'deviceEventClassId', v1: '[event][code]'],
326
+ ecs_select[disabled: 'name', v1: '[cef][name]'],
327
+ ecs_select[disabled: 'severity', v1: '[event][severity]']
328
+ ].map(&:freeze).freeze
329
+ # the @syslog_header is the field name used when a syslog header preceeds the CEF Version.
330
+ @syslog_header = ecs_select[disabled:'syslog',v1:'[log][syslog][header]']
331
+ end
332
+
333
+ class CEFField
334
+ ##
335
+ # @param name [String]: the full CEF name of a field
336
+ # @param key [String] (optional): an abbreviated CEF key to use when encoding a value with `reverse_mapping => true`
337
+ # when left unspecified, the `key` is the field's `name`.
338
+ # @param ecs_field [String] (optional): an ECS-compatible field reference to use, with square-bracket syntax.
339
+ # when left unspecified, the `ecs_field` is the field's `name`.
340
+ # @param legacy [String] (optional): a legacy CEF name to support in pass-through.
341
+ # in decoding mode without ECS, field name will be used as-provided.
342
+ # in encoding mode without ECS when provided to `fields` and `reverse_mapping => false`,
343
+ # field name will be used as-provided.
344
+ # @param priority [Integer] (optional): when multiple fields resolve to the same ECS field name, the field with the
345
+ # highest `prioriry` will be used by the encoder.
346
+ def initialize(name, key: name, ecs_field: name, legacy:nil, priority:0, normalize:nil)
347
+ @name = name
348
+ @key = key
349
+ @ecs_field = ecs_field
350
+ @legacy = legacy
351
+ @priority = priority
352
+ @normalize = normalize
353
+ end
354
+ attr_reader :name
355
+ attr_reader :key
356
+ attr_reader :ecs_field
357
+ attr_reader :legacy
358
+ attr_reader :priority
359
+ attr_reader :normalize
360
+ end
361
+
362
+ def generate_mappings!
363
+ encode_mapping = Hash.new
364
+ decode_mapping = Hash.new
365
+ timestamp_fields = Set.new
366
+ [
367
+ CEFField.new("agentAddress", key: "agt", ecs_field: "[agent][ip]"),
368
+ CEFField.new("agentDnsDomain", ecs_field: "[cef][agent][registered_domain]", priority: 10),
369
+ CEFField.new("agentHostName", key: "ahost", ecs_field: "[agent][name]"),
370
+ CEFField.new("agentId", key: "aid", ecs_field: "[agent][id]"),
371
+ CEFField.new("agentMacAddress", key: "amac", ecs_field: "[agent][mac]"),
372
+ CEFField.new("agentNtDomain", ecs_field: "[cef][agent][registered_domain]"),
373
+ CEFField.new("agentReceiptTime", key: "art", ecs_field: "[event][created]", normalize: :timestamp),
374
+ CEFField.new("agentTimeZone", key: "atz", ecs_field: "[cef][agent][timezone]"),
375
+ CEFField.new("agentTranslatedAddress", ecs_field: "[cef][agent][nat][ip]"),
376
+ CEFField.new("agentTranslatedZoneExternalID", ecs_field: "[cef][agent][translated_zone][external_id]"),
377
+ CEFField.new("agentTranslatedZoneURI", ecs_field: "[cef][agent][translated_zone][uri]"),
378
+ CEFField.new("agentType", key: "at", ecs_field: "[agent][type]"),
379
+ CEFField.new("agentVersion", key: "av", ecs_field: "[agent][version]"),
380
+ CEFField.new("agentZoneExternalID", ecs_field: "[cef][agent][zone][external_id]"),
381
+ CEFField.new("agentZoneURI", ecs_field: "[cef][agent][zone][uri]"),
382
+ CEFField.new("applicationProtocol", key: "app", ecs_field: "[network][protocol]"),
383
+ CEFField.new("baseEventCount", key: "cnt", ecs_field: "[cef][base_event_count]"),
384
+ CEFField.new("bytesIn", key: "in", ecs_field: "[source][bytes]"),
385
+ CEFField.new("bytesOut", key: "out", ecs_field: "[destination][bytes]"),
386
+ CEFField.new("categoryDeviceType", key: "catdt", ecs_field: "[cef][device_type]"),
387
+ CEFField.new("customerExternalID", ecs_field: "[organization][id]"),
388
+ CEFField.new("customerURI", ecs_field: "[organization][name]"),
389
+ CEFField.new("destinationAddress", key: "dst", ecs_field: "[destination][ip]"),
390
+ CEFField.new("destinationDnsDomain", ecs_field: "[destination][registered_domain]", priority: 10),
391
+ CEFField.new("destinationGeoLatitude", key: "dlat", ecs_field: "[destination][geo][location][lat]", legacy: "destinationLatitude"),
392
+ CEFField.new("destinationGeoLongitude", key: "dlong", ecs_field: "[destination][geo][location][lon]", legacy: "destinationLongitude"),
393
+ CEFField.new("destinationHostName", key: "dhost", ecs_field: "[destination][domain]"),
394
+ CEFField.new("destinationMacAddress", key: "dmac", ecs_field: "[destination][mac]"),
395
+ CEFField.new("destinationNtDomain", key: "dntdom", ecs_field: "[destination][registered_domain]"),
396
+ CEFField.new("destinationPort", key: "dpt", ecs_field: "[destination][port]"),
397
+ CEFField.new("destinationProcessId", key: "dpid", ecs_field: "[destination][process][pid]"),
398
+ CEFField.new("destinationProcessName", key: "dproc", ecs_field: "[destination][process][name]"),
399
+ CEFField.new("destinationServiceName", ecs_field: "[destination][service][name]"),
400
+ CEFField.new("destinationTranslatedAddress", ecs_field: "[destination][nat][ip]"),
401
+ CEFField.new("destinationTranslatedPort", ecs_field: "[destination][nat][port]"),
402
+ CEFField.new("destinationTranslatedZoneExternalID", ecs_field: "[cef][destination][translated_zone][external_id]"),
403
+ CEFField.new("destinationTranslatedZoneURI", ecs_field: "[cef][destination][translated_zone][uri]"),
404
+ CEFField.new("destinationUserId", key: "duid", ecs_field: "[destination][user][id]"),
405
+ CEFField.new("destinationUserName", key: "duser", ecs_field: "[destination][user][name]"),
406
+ CEFField.new("destinationUserPrivileges", key: "dpriv", ecs_field: "[destination][user][group][name]"),
407
+ CEFField.new("destinationZoneExternalID", ecs_field: "[cef][destination][zone][external_id]"),
408
+ CEFField.new("destinationZoneURI", ecs_field: "[cef][destination][zone][uri]"),
409
+ CEFField.new("deviceAction", key: "act", ecs_field: "[event][action]"),
410
+ CEFField.new("deviceAddress", key: "dvc", ecs_field: "[#{@device}][ip]"),
411
+ CEFField.new("deviceCustomFloatingPoint1", key: "cfp1", ecs_field: "[cef][device_custom_floating_point_1][value]"),
412
+ CEFField.new("deviceCustomFloatingPoint1Label", key: "cfp1Label", ecs_field: "[cef][device_custom_floating_point_1][label]"),
413
+ CEFField.new("deviceCustomFloatingPoint2", key: "cfp2", ecs_field: "[cef][device_custom_floating_point_2][value]"),
414
+ CEFField.new("deviceCustomFloatingPoint2Label", key: "cfp2Label", ecs_field: "[cef][device_custom_floating_point_2][label]"),
415
+ CEFField.new("deviceCustomFloatingPoint3", key: "cfp3", ecs_field: "[cef][device_custom_floating_point_3][value]"),
416
+ CEFField.new("deviceCustomFloatingPoint3Label", key: "cfp3Label", ecs_field: "[cef][device_custom_floating_point_3][label]"),
417
+ CEFField.new("deviceCustomFloatingPoint4", key: "cfp4", ecs_field: "[cef][device_custom_floating_point_4][value]"),
418
+ CEFField.new("deviceCustomFloatingPoint4Label", key: "cfp4Label", ecs_field: "[cef][device_custom_floating_point_4][label]"),
419
+ CEFField.new("deviceCustomIPv6Address1", key: "c6a1", ecs_field: "[cef][device_custom_ipv6_address_1][value]"),
420
+ CEFField.new("deviceCustomIPv6Address1Label", key: "c6a1Label", ecs_field: "[cef][device_custom_ipv6_address_1][label]"),
421
+ CEFField.new("deviceCustomIPv6Address2", key: "c6a2", ecs_field: "[cef][device_custom_ipv6_address_2][value]"),
422
+ CEFField.new("deviceCustomIPv6Address2Label", key: "c6a2Label", ecs_field: "[cef][device_custom_ipv6_address_2][label]"),
423
+ CEFField.new("deviceCustomIPv6Address3", key: "c6a3", ecs_field: "[cef][device_custom_ipv6_address_3][value]"),
424
+ CEFField.new("deviceCustomIPv6Address3Label", key: "c6a3Label", ecs_field: "[cef][device_custom_ipv6_address_3][label]"),
425
+ CEFField.new("deviceCustomIPv6Address4", key: "c6a4", ecs_field: "[cef][device_custom_ipv6_address_4][value]"),
426
+ CEFField.new("deviceCustomIPv6Address4Label", key: "c6a4Label", ecs_field: "[cef][device_custom_ipv6_address_4][label]"),
427
+ CEFField.new("deviceCustomNumber1", key: "cn1", ecs_field: "[cef][device_custom_number_1][value]"),
428
+ CEFField.new("deviceCustomNumber1Label", key: "cn1Label", ecs_field: "[cef][device_custom_number_1][label]"),
429
+ CEFField.new("deviceCustomNumber2", key: "cn2", ecs_field: "[cef][device_custom_number_2][value]"),
430
+ CEFField.new("deviceCustomNumber2Label", key: "cn2Label", ecs_field: "[cef][device_custom_number_2][label]"),
431
+ CEFField.new("deviceCustomNumber3", key: "cn3", ecs_field: "[cef][device_custom_number_3][value]"),
432
+ CEFField.new("deviceCustomNumber3Label", key: "cn3Label", ecs_field: "[cef][device_custom_number_3][label]"),
433
+ CEFField.new("deviceCustomString1", key: "cs1", ecs_field: "[cef][device_custom_string_1][value]"),
434
+ CEFField.new("deviceCustomString1Label", key: "cs1Label", ecs_field: "[cef][device_custom_string_1][label]"),
435
+ CEFField.new("deviceCustomString2", key: "cs2", ecs_field: "[cef][device_custom_string_2][value]"),
436
+ CEFField.new("deviceCustomString2Label", key: "cs2Label", ecs_field: "[cef][device_custom_string_2][label]"),
437
+ CEFField.new("deviceCustomString3", key: "cs3", ecs_field: "[cef][device_custom_string_3][value]"),
438
+ CEFField.new("deviceCustomString3Label", key: "cs3Label", ecs_field: "[cef][device_custom_string_3][label]"),
439
+ CEFField.new("deviceCustomString4", key: "cs4", ecs_field: "[cef][device_custom_string_4][value]"),
440
+ CEFField.new("deviceCustomString4Label", key: "cs4Label", ecs_field: "[cef][device_custom_string_4][label]"),
441
+ CEFField.new("deviceCustomString5", key: "cs5", ecs_field: "[cef][device_custom_string_5][value]"),
442
+ CEFField.new("deviceCustomString5Label", key: "cs5Label", ecs_field: "[cef][device_custom_string_5][label]"),
443
+ CEFField.new("deviceCustomString6", key: "cs6", ecs_field: "[cef][device_custom_string_6][value]"),
444
+ CEFField.new("deviceCustomString6Label", key: "cs6Label", ecs_field: "[cef][device_custom_string_6][label]"),
445
+ CEFField.new("deviceDirection", ecs_field: "[network][direction]"),
446
+ CEFField.new("deviceDnsDomain", ecs_field: "[#{@device}][registered_domain]", priority: 10),
447
+ CEFField.new("deviceEventCategory", key: "cat", ecs_field: "[cef][category]"),
448
+ CEFField.new("deviceExternalId", ecs_field: (@device == 'host' ? "[host][id]" : "[observer][name]")),
449
+ CEFField.new("deviceFacility", ecs_field: "[log][syslog][facility][code]"),
450
+ CEFField.new("deviceHostName", key: "dvchost", ecs_field: (@device == 'host' ? '[host][name]' : '[observer][hostname]')),
451
+ CEFField.new("deviceInboundInterface", ecs_field: "[observer][ingress][interface][name]"),
452
+ CEFField.new("deviceMacAddress", key: "dvcmac", ecs_field: "[@device][mac]"),
453
+ CEFField.new("deviceNtDomain", ecs_field: "[cef][nt_domain]"),
454
+ CEFField.new("deviceOutboundInterface", ecs_field: "[observer][egress][interface][name]"),
455
+ CEFField.new("devicePayloadId", ecs_field: "[cef][payload_id]"),
456
+ CEFField.new("deviceProcessId", key: "dvcpid", ecs_field: "[process][pid]"),
457
+ CEFField.new("deviceProcessName", ecs_field: "[process][name]"),
458
+ CEFField.new("deviceReceiptTime", key: "rt", ecs_field: "@timestamp", normalize: :timestamp),
459
+ CEFField.new("deviceTimeZone", key: "dtz", ecs_field: "[event][timezone]", legacy: "destinationTimeZone"),
460
+ CEFField.new("deviceTranslatedAddress", ecs_field: "[host][nat][ip]"),
461
+ CEFField.new("deviceTranslatedZoneExternalID", ecs_field: "[cef][translated_zone][external_id]"),
462
+ CEFField.new("deviceTranslatedZoneURI", ecs_field: "[cef][translated_zone][uri]"),
463
+ CEFField.new("deviceVersion", ecs_field: "[observer][version]"),
464
+ CEFField.new("deviceZoneExternalID", ecs_field: "[cef][zone][external_id]"),
465
+ CEFField.new("deviceZoneURI", ecs_field: "[cef][zone][uri]"),
466
+ CEFField.new("endTime", key: "end", ecs_field: "[event][end]", normalize: :timestamp),
467
+ CEFField.new("eventId", ecs_field: "[event][id]"),
468
+ CEFField.new("eventOutcome", key: "outcome", ecs_field: "[event][outcome]"),
469
+ CEFField.new("externalId", ecs_field: "[cef][external_id]"),
470
+ CEFField.new("fileCreateTime", ecs_field: "[file][created]"),
471
+ CEFField.new("fileHash", ecs_field: "[file][hash]]"),
472
+ CEFField.new("fileId", ecs_field: "[file][inode]"),
473
+ CEFField.new("fileModificationTime", ecs_field: "[file][mtime]", normalize: :timestamp),
474
+ CEFField.new("fileName", key: "fname", ecs_field: "[file][name]"),
475
+ CEFField.new("filePath", ecs_field: "[file][path]"),
476
+ CEFField.new("filePermission", ecs_field: "[file][group]"),
477
+ CEFField.new("fileSize", key: "fsize", ecs_field: "[file][size]"),
478
+ CEFField.new("fileType", ecs_field: "[file][extension]"),
479
+ CEFField.new("managerReceiptTime", key: "mrt", ecs_field: "[event][ingested]", normalize: :timestamp),
480
+ CEFField.new("message", key: "msg", ecs_field: "[message]"),
481
+ CEFField.new("oldFileCreateTime", ecs_field: "[cef][old_file][created]", normalize: :timestamp),
482
+ CEFField.new("oldFileHash", ecs_field: "[cef][old_file][hash]"),
483
+ CEFField.new("oldFileId", ecs_field: "[cef][old_file][inode]"),
484
+ CEFField.new("oldFileModificationTime", ecs_field: "[cef][old_file][mtime]", normalize: :timestamp),
485
+ CEFField.new("oldFileName", ecs_field: "[cef][old_file][name]"),
486
+ CEFField.new("oldFilePath", ecs_field: "[cef][old_file][path]"),
487
+ CEFField.new("oldFilePermission", ecs_field: "[cef][old_file][group]"),
488
+ CEFField.new("oldFileSize", ecs_field: "[cef][old_file][size]"),
489
+ CEFField.new("oldFileType", ecs_field: "[cef][old_file][extension]"),
490
+ CEFField.new("rawEvent", ecs_field: "[event][original]"),
491
+ CEFField.new("Reason", key: "reason", ecs_field: "[event][reason]"),
492
+ CEFField.new("requestClientApplication", ecs_field: "[user_agent][original]"),
493
+ CEFField.new("requestContext", ecs_field: "[http][request][referrer]"),
494
+ CEFField.new("requestCookies", ecs_field: "[cef][request][cookies]"),
495
+ CEFField.new("requestMethod", ecs_field: "[http][request][method]"),
496
+ CEFField.new("requestUrl", key: "request", ecs_field: "[url][original]"),
497
+ CEFField.new("sourceAddress", key: "src", ecs_field: "[source][ip]"),
498
+ CEFField.new("sourceDnsDomain", ecs_field: "[source][registered_domain]", priority: 10),
499
+ CEFField.new("sourceGeoLatitude", key: "slat", ecs_field: "[source][geo][location][lat]", legacy: "sourceLatitude"),
500
+ CEFField.new("sourceGeoLongitude", key: "slong", ecs_field: "[source][geo][location][lon]", legacy: "sourceLongitude"),
501
+ CEFField.new("sourceHostName", key: "shost", ecs_field: "[source][domain]"),
502
+ CEFField.new("sourceMacAddress", key: "smac", ecs_field: "[source][mac]"),
503
+ CEFField.new("sourceNtDomain", key: "sntdom", ecs_field: "[source][registered_domain]"),
504
+ CEFField.new("sourcePort", key: "spt", ecs_field: "[source][port]"),
505
+ CEFField.new("sourceProcessId", key: "spid", ecs_field: "[source][process][pid]"),
506
+ CEFField.new("sourceProcessName", key: "sproc", ecs_field: "[source][process][name]"),
507
+ CEFField.new("sourceServiceName", ecs_field: "[source][service][name]"),
508
+ CEFField.new("sourceTranslatedAddress", ecs_field: "[source][nat][ip]"),
509
+ CEFField.new("sourceTranslatedPort", ecs_field: "[source][nat][port]"),
510
+ CEFField.new("sourceTranslatedZoneExternalID", ecs_field: "[cef][source][translated_zone][external_id]"),
511
+ CEFField.new("sourceTranslatedZoneURI", ecs_field: "[cef][source][translated_zone][uri]"),
512
+ CEFField.new("sourceUserId", key: "suid", ecs_field: "[source][user][id]"),
513
+ CEFField.new("sourceUserName", key: "suser", ecs_field: "[source][user][name]"),
514
+ CEFField.new("sourceUserPrivileges", key: "spriv", ecs_field: "[source][user][group][name]"),
515
+ CEFField.new("sourceZoneExternalID", ecs_field: "[cef][source][zone][external_id]"),
516
+ CEFField.new("sourceZoneURI", ecs_field: "[cef][source][zone][uri]"),
517
+ CEFField.new("startTime", key: "start", ecs_field: "[event][start]", normalize: :timestamp),
518
+ CEFField.new("transportProtocol", key: "proto", ecs_field: "[network][transport]"),
519
+ CEFField.new("type", ecs_field: "[cef][type]"),
520
+ ].sort_by(&:priority).each do |cef|
521
+ field_name = ecs_select[disabled:cef.name, v1:cef.ecs_field]
522
+
523
+ # whether the source is a cef_key or cef_name, normalize to field_name
524
+ decode_mapping[cef.key] = field_name
525
+ decode_mapping[cef.name] = field_name
526
+
527
+ # whether source is a cef_name or a field_name, normalize to target
528
+ normalized_encode_target = @reverse_mapping ? cef.key : cef.name
529
+ encode_mapping[field_name] = normalized_encode_target
530
+ encode_mapping[cef.name] = normalized_encode_target unless cef.name == field_name
531
+
532
+ # if a field has an alias, normalize pass-through
533
+ if cef.legacy
534
+ decode_mapping[cef.legacy] = ecs_select[disabled:cef.legacy, v1:cef.ecs_field]
535
+ encode_mapping[cef.legacy] = @reverse_mapping ? cef.key : cef.legacy
536
+ end
537
+
538
+ timestamp_fields << field_name if ecs_compatibility != :disabled && cef.normalize == :timestamp
539
+ end
540
+
541
+ @decode_mapping = decode_mapping.dup.freeze
542
+ @encode_mapping = encode_mapping.dup.freeze
543
+ @timestamp_fields = timestamp_fields.dup.freeze
544
+ end
545
+
371
546
  # Escape pipes and backslashes in the header. Equal signs are ok.
372
547
  # Newlines are forbidden.
373
548
  def sanitize_header_field(value)
@@ -392,17 +567,23 @@ class LogStash::Codecs::CEF < LogStash::Codecs::Base
392
567
  .gsub(EXTENSION_VALUE_SANITIZER_PATTERN, EXTENSION_VALUE_SANITIZER_MAPPING)
393
568
  end
394
569
 
570
+ def normalize_timestamp(value, device_timezone_name)
571
+ value = @timestamp_normalzer.normalize(value, device_timezone_name).iso8601(9)
572
+
573
+ LogStash::Timestamp.new(value)
574
+ rescue => e
575
+ @logger.error("Failed to parse CEF timestamp value `#{value}` (#{e.message})")
576
+ raise InvalidTimestamp.new("Not a valid CEF timestamp: `#{value}`")
577
+ end
578
+
395
579
  def get_value(fieldname, event)
396
580
  val = event.get(fieldname)
397
581
 
398
582
  return nil if val.nil?
399
583
 
400
- key = sanitize_extension_key(fieldname)
401
-
402
- if @reverse_mapping
403
- key = REVERSE_MAPPINGS[key] || key
404
- end
405
-
584
+ key = @encode_mapping.fetch(fieldname, fieldname)
585
+ key = sanitize_extension_key(key)
586
+
406
587
  case val
407
588
  when Array, Hash
408
589
  return "#{key}=#{sanitize_extension_val(val.to_json)}"