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

Sign up to get free protection for your applications and to get access to all the features.
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)}"