Skip to content

Commit 342ca51

Browse files
authored
Merge pull request #963 from MISP/codex/cef-export-standard
Fix CEF export serialization and standard compliance
2 parents 9016a44 + 8dfd67d commit 342ca51

5 files changed

Lines changed: 442 additions & 74 deletions

File tree

‎documentation/README.md‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3510,8 +3510,7 @@ Module to export a MISP event in CEF format.
35103510
[[source code](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/export_mod/cef_export.py)]
35113511
35123512
- **features**:
3513-
>The module takes a MISP event in input, to look every attribute. Each attribute matching with some predefined types is then exported in Common Event Format.
3514-
>Thus, there is no particular feature concerning MISP Events since any event can be exported. However, 4 configuration parameters recognized by CEF format are required and should be provided by users before exporting data: the device vendor, product and version, as well as the default severity of data.
3513+
>Exports supported event and object attributes as UTF-8 CEF file records, with standard escaping and field validation. IPv6 address records use CEF:1; other records use CEF:0. Device settings and severity have defaults. See the [usage guide](https://github.com/MISP/misp-modules/blob/main/documentation/export_mod/cef_export.md) for mappings and compatibility changes.
35153514
35163515
- **config**:
35173516
> - Default_Severity
@@ -3520,13 +3519,13 @@ Module to export a MISP event in CEF format.
35203519
> - Device_Version
35213520
35223521
- **input**:
3523-
>MISP Event attributes
3522+
>MISP events and object attributes in standard, bare or legacy export format.
35243523
35253524
- **output**:
35263525
>Common Event Format file
35273526
35283527
- **references**:
3529-
>https://community.softwaregrp.com/t5/ArcSight-Connectors/ArcSight-Common-Event-Format-CEF-Guide/ta-p/1589306?attachment-id=65537
3528+
>https://www.microfocus.com/documentation/arcsight/arcsight-smartconnectors-24.3/pdfdoc/cef-implementation-standard/cef-implementation-standard.pdf
35303529
35313530
-----
35323531
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# CEF export
2+
3+
The exporter follows the [OpenText ArcSight CEF Implementation Standard, version 27](https://www.microfocus.com/documentation/arcsight/arcsight-smartconnectors-24.3/pdfdoc/cef-implementation-standard/cef-implementation-standard.pdf), especially the header, file output, character encoding and extension dictionary sections (pages 5–10).
4+
5+
It produces a base64-encoded UTF-8 `.cef` file, with one line per supported attribute. It accepts a single event or a list of events: bare events, `{"Event": {...}}` wrappers, and legacy exports with `Attribute` or `Object` alongside `Event` metadata. Object attributes are included. As before, all supplied supported attributes are exported regardless of `to_ids`; unsupported types are ignored. Apply any desired filtering before export.
6+
7+
## Configuration
8+
9+
Missing configuration keys use these defaults. Explicit invalid values return a module error with no file.
10+
11+
| Setting | Default | Accepted values |
12+
| --- | --- | --- |
13+
| `Device_Vendor` | `MISP` | Non-empty string, at most 63 characters |
14+
| `Device_Product` | `MISP` | Non-empty string, at most 63 characters |
15+
| `Device_Version` | `1` | Non-empty string or integer, at most 31 characters |
16+
| `Default_Severity` | `1` | Integer 0–10, numeric string 0–10, or `Unknown`, `Low`, `Medium`, `High`, `Very-High` |
17+
18+
Integer device settings are converted to strings for compatibility with existing configuration. Header values cannot contain line breaks or other ASCII control characters. Overlong fields are rejected rather than truncated. Length limits apply to the original field values before CEF escaping.
19+
20+
## Record mapping
21+
22+
The header is `CEF:Version|Device Vendor|Device Product|Device Version|Device Event Class ID|Name|Severity|`.
23+
24+
The event-class ID is the MISP attribute type, so `ip-src`, `ip-dst`, and each hash algorithm remain distinguishable. The name is the attribute category (at most 512 characters), falling back to the type when the category is absent. The category is also exported as `cat`.
25+
26+
| MISP attribute type | CEF extension key | Limit |
27+
| --- | --- | --- |
28+
| `ip-src` | `src` | Single IPv4 or IPv6 address |
29+
| `ip-dst` | `dst` | Single IPv4 or IPv6 address |
30+
| `hostname`, `domain` | `dhost` | 1023 characters |
31+
| `md5`, `sha1`, `sha256` | `fileHash` | 255 characters |
32+
| `url` | `request` | 1023 characters |
33+
34+
IPv4 and non-address records use `CEF:0`. IPv6 address records use `CEF:1`, since CEF specification 1.x permits IPv6 in the standard address fields. Address values are normalized; CIDR networks and scoped IPv6 addresses cannot be represented as a single CEF address and return errors. String values retain their content, including Unicode, spaces and extension pipes. Hash, hostname and URL syntax validation remains the responsibility of the input producer; the exporter validates their CEF string type and length.
35+
36+
When an attribute has a `timestamp`, its non-negative Unix seconds are converted to milliseconds in `deviceCustomDate1`, labeled `MISP attribute timestamp` by `deviceCustomDate1Label`. This retains the year and avoids server timezone and locale dependence. A MISP attribute modification timestamp does not establish the time of an observed incident, so it is not mapped to `start`, `end`, or `rt`. Missing timestamps omit these two fields.
37+
38+
Header backslashes and pipes are escaped. Extension backslashes and equals signs are escaped, and CR/LF become literal `\r`/`\n` sequences. Pipes and spaces in extensions remain literal, as required by CEF. Invalid requests, control characters, unsupported header values, malformed addresses and overlong fields return `{"error": "..."}` without a partial file.
39+
40+
## Example
41+
42+
Submit this JSON to the misp-modules `/query` endpoint:
43+
44+
```json
45+
{
46+
"module": "cef_export",
47+
"config": {"Default_Severity": "Medium"},
48+
"data": [{"Attribute": [{
49+
"type": "url",
50+
"category": "Network activity",
51+
"timestamp": "1700000000",
52+
"value": "https://example.org/?a=1&b=2"
53+
}]}]
54+
}
55+
```
56+
57+
The decoded `data` is:
58+
59+
```text
60+
CEF:0|MISP|MISP|1|url|Network activity|Medium|request=https://example.org/?a\=1&b\=2 cat=Network activity deviceCustomDate1=1700000000000 deviceCustomDate1Label=MISP attribute timestamp
61+
```
62+
63+
## Compatibility changes
64+
65+
Version 2 removes the fabricated `MMM dd HH:mm:ss host` syslog prefix. The standard's file export procedure starts each line with `CEF:` (page 8); an actual syslog sender should supply its own transport prefix. Collectors configured for the old prefix must be updated.
66+
67+
Rules matching the event-class ID must use the attribute type instead of the category. Rules matching the name can continue using the category. Consumers of IPv6 address records need CEF 1.x support. Invalid severity, header, address or string lengths now fail the export explicitly instead of producing malformed output. The `.cef` extension and base64 response envelope are retained; the advertised MIME type is corrected to `text/plain`.
68+
69+
These checks cover file serialization against the published specification. They do not constitute ArcSight certification or a live collector integration test.

‎documentation/mkdocs/export_mod.md‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ Module to export a MISP event in CEF format.
55
[[source code](https://github.com/MISP/misp-modules/tree/main/misp_modules/modules/export_mod/cef_export.py)]
66

77
- **features**:
8-
>The module takes a MISP event in input, to look every attribute. Each attribute matching with some predefined types is then exported in Common Event Format.
9-
>Thus, there is no particular feature concerning MISP Events since any event can be exported. However, 4 configuration parameters recognized by CEF format are required and should be provided by users before exporting data: the device vendor, product and version, as well as the default severity of data.
8+
>Exports supported event and object attributes as UTF-8 CEF file records, with standard escaping and field validation. IPv6 address records use CEF:1; other records use CEF:0. Device settings and severity have defaults. See the [usage guide](https://github.com/MISP/misp-modules/blob/main/documentation/export_mod/cef_export.md) for mappings and compatibility changes.
109
1110
- **config**:
1211
> - Default_Severity
@@ -15,13 +14,13 @@ Module to export a MISP event in CEF format.
1514
> - Device_Version
1615
1716
- **input**:
18-
>MISP Event attributes
17+
>MISP events and object attributes in standard, bare or legacy export format.
1918
2019
- **output**:
2120
>Common Event Format file
2221
2322
- **references**:
24-
>https://community.softwaregrp.com/t5/ArcSight-Connectors/ArcSight-Common-Event-Format-CEF-Guide/ta-p/1589306?attachment-id=65537
23+
>https://www.microfocus.com/documentation/arcsight/arcsight-smartconnectors-24.3/pdfdoc/cef-implementation-standard/cef-implementation-standard.pdf
2524
2625
-----
2726

‎misp_modules/modules/export_mod/cef_export.py‎

Lines changed: 127 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
11
import base64
2-
import datetime
2+
import ipaddress
33
import json
44

55
misperrors = {"error": "Error"}
66

77
# possible module-types: 'expansion', 'hover' or both
88
moduleinfo = {
9-
"version": "1",
9+
"version": "2",
1010
"author": "Hannah Ward",
1111
"description": "Module to export a MISP event in CEF format.",
1212
"module-type": ["export"],
1313
"name": "CEF Export",
1414
"logo": "",
1515
"requirements": [],
1616
"features": (
17-
"The module takes a MISP event in input, to look every attribute. Each attribute matching with some predefined"
18-
" types is then exported in Common Event Format.\nThus, there is no particular feature concerning MISP Events"
19-
" since any event can be exported. However, 4 configuration parameters recognized by CEF format are required"
20-
" and should be provided by users before exporting data: the device vendor, product and version, as well as the"
21-
" default severity of data."
17+
"Exports supported event and object attributes as UTF-8 CEF file records, with standard escaping and field"
18+
" validation. IPv6 address records use CEF:1; other records use CEF:0. Device settings and severity have"
19+
" defaults. See the [usage"
20+
" guide](https://github.com/MISP/misp-modules/blob/main/documentation/export_mod/cef_export.md) for mappings"
21+
" and compatibility changes."
2222
),
2323
"references": [
24-
"https://community.softwaregrp.com/t5/ArcSight-Connectors/ArcSight-Common-Event-Format-CEF-Guide/ta-p/1589306?attachment-id=65537"
24+
"https://www.microfocus.com/documentation/arcsight/arcsight-smartconnectors-24.3/pdfdoc/cef-implementation-standard/cef-implementation-standard.pdf"
2525
],
26-
"input": "MISP Event attributes",
26+
"input": "MISP events and object attributes in standard, bare or legacy export format.",
2727
"output": "Common Event Format file",
2828
}
2929

@@ -43,71 +43,132 @@
4343

4444
mispattributes = {"input": list(cefmapping.keys())}
4545
outputFileExtension = "cef"
46-
responseType = "application/txt"
46+
responseType = "text/plain"
47+
inputSource = ["event", "event-collection"]
48+
DEFAULT_CONFIG = {
49+
"Default_Severity": 1,
50+
"Device_Vendor": "MISP",
51+
"Device_Product": "MISP",
52+
"Device_Version": 1,
53+
}
54+
55+
56+
def _text(value, name, limit, header=False):
57+
if not isinstance(value, str) or not value.strip() or len(value) > limit:
58+
raise ValueError(f"{name} must be a non-empty string of at most {limit} characters.")
59+
value.encode("utf-8")
60+
if any(ord(char) < 32 and (header or char not in "\r\n\t") or ord(char) == 127 for char in value):
61+
raise ValueError(f"{name} contains an unsupported control character.")
62+
return value
63+
64+
65+
def _header(value, name, limit):
66+
if type(value) is int:
67+
value = str(value)
68+
value = _text(value, name, limit, header=True)
69+
return value.replace("\\", "\\\\").replace("|", "\\|")
70+
71+
72+
def _extension(value):
73+
return value.replace("\\", "\\\\").replace("=", "\\=").replace("\n", "\\n").replace("\r", "\\r")
74+
75+
76+
def _severity(value):
77+
if type(value) is int and 0 <= value <= 10:
78+
return str(value)
79+
if isinstance(value, str):
80+
if value in ("Unknown", "Low", "Medium", "High", "Very-High"):
81+
return value
82+
if value.isascii() and value.isdecimal() and 0 <= int(value) <= 10:
83+
return str(int(value))
84+
raise ValueError("Default_Severity must be an integer from 0 to 10 or Unknown, Low, Medium, High, Very-High.")
85+
86+
87+
def _records(container, key):
88+
records = container.get(key, [])
89+
if not isinstance(records, list) or any(not isinstance(record, dict) for record in records):
90+
raise ValueError(f"{key} must be a list of objects.")
91+
return records
92+
93+
94+
def _attributes(data):
95+
if isinstance(data, dict):
96+
data = [data]
97+
if not isinstance(data, list) or any(not isinstance(event, dict) for event in data):
98+
raise ValueError("data must be a MISP event or a list of events.")
99+
for envelope in data:
100+
event = envelope.get("Event", envelope)
101+
if not isinstance(event, dict):
102+
raise ValueError("Event must be an object.")
103+
# Legacy exports place collections alongside the Event metadata.
104+
collections = dict(event, **envelope)
105+
yield from _records(collections, "Attribute")
106+
for obj in _records(collections, "Object"):
107+
yield from _records(obj, "Attribute")
108+
109+
110+
def _record(attr, device, severity):
111+
kind = attr.get("type")
112+
if not isinstance(kind, str) or kind not in cefmapping:
113+
return None
114+
key = cefmapping[kind]
115+
value = _text(attr.get("value"), kind, 255 if key == "fileHash" else 1023)
116+
cef_version = 0
117+
if key in ("src", "dst"):
118+
address = ipaddress.ip_address(value)
119+
if "%" in value:
120+
raise ValueError("Scoped IPv6 addresses cannot be represented in a CEF address field.")
121+
value = str(address)
122+
cef_version = int(address.version == 6)
123+
category = _text(attr.get("category", kind), "category", 512, header=True)
124+
name = _header(category, "category", 512)
125+
extension = [(key, value), ("cat", category)]
126+
if "timestamp" in attr:
127+
timestamp = attr["timestamp"]
128+
if not (type(timestamp) is int and timestamp >= 0) and not (
129+
isinstance(timestamp, str) and timestamp.isascii() and timestamp.isdecimal()
130+
):
131+
raise ValueError("Attribute timestamp must be non-negative Unix seconds.")
132+
extension.extend(
133+
[("deviceCustomDate1", str(int(timestamp) * 1000)), ("deviceCustomDate1Label", "MISP attribute timestamp")]
134+
)
135+
fields = [f"CEF:{cef_version}", *device, kind, name, severity]
136+
return "|".join(fields) + "|" + " ".join(f"{key}={_extension(value)}" for key, value in extension)
47137

48138

49139
def handler(q=False):
50140
if q is False:
51141
return False
52-
request = json.loads(q)
53-
if "config" in request:
54-
config = request["config"]
55-
else:
56-
config = {
57-
"Default_Severity": 1,
58-
"Device_Vendor": "MISP",
59-
"Device_Product": "MISP",
60-
"Device_Version": 1,
61-
}
62-
63-
data = request["data"]
64-
response = ""
65-
for ev in data:
66-
event = ev["Attribute"]
67-
for attr in event:
68-
if attr["type"] in cefmapping:
69-
response += "{} host CEF:0|{}|{}|{}|{}|{}|{}|{}={}\n".format(
70-
datetime.datetime.fromtimestamp(int(attr["timestamp"])).strftime("%b %d %H:%M:%S"),
71-
config["Device_Vendor"],
72-
config["Device_Product"],
73-
config["Device_Version"],
74-
attr["category"],
75-
attr["category"],
76-
config["Default_Severity"],
77-
cefmapping[attr["type"]],
78-
attr["value"],
79-
)
80-
81-
r = {
82-
"response": [],
83-
"data": str(base64.b64encode(bytes(response, "utf-8")), "utf-8"),
84-
}
85-
return r
142+
try:
143+
request = json.loads(q)
144+
if not isinstance(request, dict) or "data" not in request:
145+
raise ValueError("A request object with data is required.")
146+
supplied_config = request.get("config", {})
147+
if not isinstance(supplied_config, dict):
148+
raise ValueError("config must be an object.")
149+
config = {**DEFAULT_CONFIG, **supplied_config}
150+
severity = _severity(config["Default_Severity"])
151+
device = [
152+
_header(config[key], key, limit)
153+
for key, limit in (("Device_Vendor", 63), ("Device_Product", 63), ("Device_Version", 31))
154+
]
155+
lines = []
156+
for attr in _attributes(request["data"]):
157+
line = _record(attr, device, severity)
158+
if line is not None:
159+
lines.append(line + "\n")
160+
return {"response": [], "data": base64.b64encode("".join(lines).encode("utf-8")).decode("ascii")}
161+
except (TypeError, ValueError) as error:
162+
return {"error": f"Unable to export CEF: {error}"}
86163

87164

88165
def introspection():
89-
modulesetup = {}
90-
try:
91-
responseType
92-
modulesetup["responseType"] = responseType
93-
except NameError:
94-
pass
95-
try:
96-
userConfig
97-
modulesetup["userConfig"] = userConfig
98-
except NameError:
99-
pass
100-
try:
101-
outputFileExtension
102-
modulesetup["outputFileExtension"] = outputFileExtension
103-
except NameError:
104-
pass
105-
try:
106-
inputSource
107-
modulesetup["inputSource"] = inputSource
108-
except NameError:
109-
pass
110-
return modulesetup
166+
return {
167+
**mispattributes,
168+
"responseType": responseType,
169+
"outputFileExtension": outputFileExtension,
170+
"inputSource": inputSource,
171+
}
111172

112173

113174
def version():

0 commit comments

Comments
 (0)