-
Notifications
You must be signed in to change notification settings - Fork 617
Crowdstrike.fdr: map CommandHistory events to ECS process fields #20285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3be10c8
d0257c7
e083da1
7d507a3
592866a
be29c93
54efc99
13397ac
f5cd816
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| {"event_simpleName":"CommandHistory","CommandHistory":"sc stop CSFalconService¶Stop-Service -Name CSFalconService -Force","FirstCommand":"0","CommandCountMax":"2","CommandCount":"2","CommandSequence":"1","LastDisplayed":"0","LastAdded":"0","TargetProcessId":"7292754410393","AuthenticationId":"2048391741","ConfigStateHash":"886310903","EffectiveTransmissionClass":"3","Entitlements":"15","EventOrigin":"1","LogonType":"10","UserName":"jdoe","aip":"81.2.69.144","aid":"b5c2f3e1d4a789102345678abcdef01","timestamp":"1752051389000","cid":"a1b2c3d4e5f67890abcdef1234567890","id":"a1b2c3d4e5f67890abcdef1234567890a1b2","event_platform":"Win","name":"CommandHistoryV5","ConfigBuild":"1007.3.0021003.10"} | ||
| {"event_simpleName":"CommandHistory","CommandHistory":"sc stop CSFalconService","FirstCommand":"0","CommandCountMax":"1","CommandCount":"1","CommandSequence":"1","LastDisplayed":"0","LastAdded":"0","TargetProcessId":"7292754410394","AuthenticationId":"2048391742","ConfigStateHash":"886310903","EffectiveTransmissionClass":"3","Entitlements":"15","EventOrigin":"1","LogonType":"2","UserName":"jdoe","aip":"81.2.69.144","aid":"b5c2f3e1d4a789102345678abcdef01","timestamp":"1752051390000","cid":"a1b2c3d4e5f67890abcdef1234567890","id":"b2c3d4e5f67890abcdef1234567890b2c3","event_platform":"Win","name":"CommandHistoryV5","ConfigBuild":"1007.3.0021003.10"} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| { | ||
| "expected": [ | ||
| { | ||
| "@timestamp": "2025-07-09T08:56:29.000Z", | ||
| "crowdstrike": { | ||
| "AuthenticationId": "2048391741", | ||
| "CommandCount": 2, | ||
| "CommandCountMax": 2, | ||
| "CommandHistory": [ | ||
| "sc stop CSFalconService", | ||
| "Stop-Service -Name CSFalconService -Force" | ||
| ], | ||
| "CommandSequence": "1", | ||
| "ConfigStateHash": "886310903", | ||
| "EffectiveTransmissionClass": "3", | ||
| "Entitlements": "15", | ||
| "EventOrigin": "1", | ||
| "FirstCommand": "0", | ||
| "LastAdded": "0", | ||
| "LastDisplayed": "0", | ||
| "LogonType": "10", | ||
| "cid": "a1b2c3d4e5f67890abcdef1234567890", | ||
| "event_simpleName": "CommandHistory", | ||
| "id": "a1b2c3d4e5f67890abcdef1234567890a1b2", | ||
| "name": "CommandHistoryV5" | ||
| }, | ||
| "device": { | ||
| "id": "b5c2f3e1d4a789102345678abcdef01" | ||
| }, | ||
| "event": { | ||
| "action": "CommandHistory", | ||
| "category": [ | ||
| "process" | ||
| ], | ||
| "created": "2025-07-09T08:56:29.000Z", | ||
| "id": "a1b2c3d4e5f67890abcdef1234567890a1b2|b5c2f3e1d4a789102345678abcdef01|a1b2c3d4e5f67890abcdef1234567890", | ||
| "kind": "event", | ||
| "original": "{\"event_simpleName\":\"CommandHistory\",\"CommandHistory\":\"sc stop CSFalconService¶Stop-Service -Name CSFalconService -Force\",\"FirstCommand\":\"0\",\"CommandCountMax\":\"2\",\"CommandCount\":\"2\",\"CommandSequence\":\"1\",\"LastDisplayed\":\"0\",\"LastAdded\":\"0\",\"TargetProcessId\":\"7292754410393\",\"AuthenticationId\":\"2048391741\",\"ConfigStateHash\":\"886310903\",\"EffectiveTransmissionClass\":\"3\",\"Entitlements\":\"15\",\"EventOrigin\":\"1\",\"LogonType\":\"10\",\"UserName\":\"jdoe\",\"aip\":\"81.2.69.144\",\"aid\":\"b5c2f3e1d4a789102345678abcdef01\",\"timestamp\":\"1752051389000\",\"cid\":\"a1b2c3d4e5f67890abcdef1234567890\",\"id\":\"a1b2c3d4e5f67890abcdef1234567890a1b2\",\"event_platform\":\"Win\",\"name\":\"CommandHistoryV5\",\"ConfigBuild\":\"1007.3.0021003.10\"}", | ||
| "outcome": "success", | ||
| "type": [ | ||
| "end", | ||
| "info" | ||
| ] | ||
| }, | ||
| "host": { | ||
| "id": "b5c2f3e1d4a789102345678abcdef01", | ||
| "os": { | ||
| "type": "windows" | ||
| } | ||
| }, | ||
| "observer": { | ||
| "address": [ | ||
| "81.2.69.144" | ||
| ], | ||
| "geo": { | ||
| "city_name": "London", | ||
| "continent_name": "Europe", | ||
| "country_iso_code": "GB", | ||
| "country_name": "United Kingdom", | ||
| "location": { | ||
| "lat": 51.5142, | ||
| "lon": -0.0931 | ||
| }, | ||
| "region_iso_code": "GB-ENG", | ||
| "region_name": "England" | ||
| }, | ||
| "ip": [ | ||
| "81.2.69.144" | ||
| ], | ||
| "serial_number": "b5c2f3e1d4a789102345678abcdef01", | ||
| "version": "1007.3.0021003.10" | ||
| }, | ||
| "process": { | ||
| "command_line": [ | ||
| "sc stop CSFalconService", | ||
| "Stop-Service -Name CSFalconService -Force" | ||
| ], | ||
| "entity_id": "7292754410393", | ||
| "interactive": true, | ||
| "user": { | ||
| "name": "jdoe" | ||
| } | ||
| }, | ||
| "related": { | ||
| "hash": [ | ||
| "886310903" | ||
| ], | ||
| "ip": [ | ||
| "81.2.69.144" | ||
| ], | ||
| "user": [ | ||
| "jdoe" | ||
| ] | ||
| }, | ||
| "tags": [ | ||
| "preserve_original_event" | ||
| ], | ||
| "user": { | ||
| "name": "jdoe" | ||
| } | ||
| }, | ||
| { | ||
| "@timestamp": "2025-07-09T08:56:30.000Z", | ||
| "crowdstrike": { | ||
| "AuthenticationId": "2048391742", | ||
| "CommandCount": 1, | ||
| "CommandCountMax": 1, | ||
| "CommandHistory": [ | ||
| "sc stop CSFalconService" | ||
| ], | ||
| "CommandSequence": "1", | ||
| "ConfigStateHash": "886310903", | ||
| "EffectiveTransmissionClass": "3", | ||
| "Entitlements": "15", | ||
| "EventOrigin": "1", | ||
| "FirstCommand": "0", | ||
| "LastAdded": "0", | ||
| "LastDisplayed": "0", | ||
| "LogonType": "2", | ||
| "cid": "a1b2c3d4e5f67890abcdef1234567890", | ||
| "event_simpleName": "CommandHistory", | ||
| "id": "b2c3d4e5f67890abcdef1234567890b2c3", | ||
| "name": "CommandHistoryV5" | ||
| }, | ||
| "device": { | ||
| "id": "b5c2f3e1d4a789102345678abcdef01" | ||
| }, | ||
| "event": { | ||
| "action": "CommandHistory", | ||
| "category": [ | ||
| "process" | ||
| ], | ||
| "created": "2025-07-09T08:56:30.000Z", | ||
| "id": "b2c3d4e5f67890abcdef1234567890b2c3|b5c2f3e1d4a789102345678abcdef01|a1b2c3d4e5f67890abcdef1234567890", | ||
| "kind": "event", | ||
| "original": "{\"event_simpleName\":\"CommandHistory\",\"CommandHistory\":\"sc stop CSFalconService\",\"FirstCommand\":\"0\",\"CommandCountMax\":\"1\",\"CommandCount\":\"1\",\"CommandSequence\":\"1\",\"LastDisplayed\":\"0\",\"LastAdded\":\"0\",\"TargetProcessId\":\"7292754410394\",\"AuthenticationId\":\"2048391742\",\"ConfigStateHash\":\"886310903\",\"EffectiveTransmissionClass\":\"3\",\"Entitlements\":\"15\",\"EventOrigin\":\"1\",\"LogonType\":\"2\",\"UserName\":\"jdoe\",\"aip\":\"81.2.69.144\",\"aid\":\"b5c2f3e1d4a789102345678abcdef01\",\"timestamp\":\"1752051390000\",\"cid\":\"a1b2c3d4e5f67890abcdef1234567890\",\"id\":\"b2c3d4e5f67890abcdef1234567890b2c3\",\"event_platform\":\"Win\",\"name\":\"CommandHistoryV5\",\"ConfigBuild\":\"1007.3.0021003.10\"}", | ||
| "outcome": "success", | ||
| "type": [ | ||
| "end", | ||
| "info" | ||
| ] | ||
| }, | ||
| "host": { | ||
| "id": "b5c2f3e1d4a789102345678abcdef01", | ||
| "os": { | ||
| "type": "windows" | ||
| } | ||
| }, | ||
| "observer": { | ||
| "address": [ | ||
| "81.2.69.144" | ||
| ], | ||
| "geo": { | ||
| "city_name": "London", | ||
| "continent_name": "Europe", | ||
| "country_iso_code": "GB", | ||
| "country_name": "United Kingdom", | ||
| "location": { | ||
| "lat": 51.5142, | ||
| "lon": -0.0931 | ||
| }, | ||
| "region_iso_code": "GB-ENG", | ||
| "region_name": "England" | ||
| }, | ||
| "ip": [ | ||
| "81.2.69.144" | ||
| ], | ||
| "serial_number": "b5c2f3e1d4a789102345678abcdef01", | ||
| "version": "1007.3.0021003.10" | ||
| }, | ||
| "process": { | ||
| "command_line": [ | ||
| "sc stop CSFalconService" | ||
| ], | ||
| "entity_id": "7292754410394", | ||
| "interactive": true, | ||
| "user": { | ||
| "name": "jdoe" | ||
| } | ||
| }, | ||
| "related": { | ||
| "hash": [ | ||
| "886310903" | ||
| ], | ||
| "ip": [ | ||
| "81.2.69.144" | ||
| ], | ||
| "user": [ | ||
| "jdoe" | ||
| ] | ||
| }, | ||
| "tags": [ | ||
| "preserve_original_event" | ||
| ], | ||
| "user": { | ||
| "name": "jdoe" | ||
| } | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1051,6 +1051,47 @@ processors: | |
|
|
||
| ctx.process.args = commandLineToArgv(ctx.process.command_line); | ||
| ctx.process.args_count = ctx.process.args.length; | ||
| - script: | ||
| description: >- | ||
| Split crowdstrike.CommandHistory on U+00B6 PILCROW SIGN into an array | ||
| and assign the result to both crowdstrike.CommandHistory and | ||
| process.command_line. Placed after split_command_line so that processor | ||
| only runs on string-valued command lines from crowdstrike.CommandLine. | ||
| tag: script_command_history_to_process_command_line_8c3e1f92 | ||
| if: |- | ||
| ctx.crowdstrike?.event_simpleName == 'CommandHistory' && | ||
| ctx.crowdstrike?.CommandHistory instanceof String && | ||
| ctx.crowdstrike.CommandHistory != '' | ||
| source: |- | ||
| def history = ctx.crowdstrike.CommandHistory; | ||
| def commands = new ArrayList(); | ||
| def remaining = history; | ||
| int idx = remaining.indexOf("¶"); | ||
| while (idx >= 0) { | ||
| def cmd = remaining.substring(0, idx).trim(); | ||
| if (!cmd.isEmpty()) { | ||
| commands.add(cmd); | ||
| } | ||
| remaining = remaining.substring(idx + 1); | ||
| idx = remaining.indexOf("¶"); | ||
| } | ||
| if (!remaining.trim().isEmpty()) { | ||
| commands.add(remaining.trim()); | ||
| } | ||
| ctx.crowdstrike.CommandHistory = commands; | ||
| ctx.process = ctx.process ?: [:]; | ||
| ctx.process.command_line = commands; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Severity: 🟡 Medium process.command_line is now assigned the whole command array, so the field is multi-valued on CommandHistory events but a scalar string everywhere else in the data stream; populate it only when the history holds exactly one command. DetailsECS defines process.command_line as the full command line that started the process identified by process.entity_id. On a CommandHistory event process.entity_id is TargetProcessId (the shell), and crowdstrike.CommandHistory is the buffer of commands typed inside that shell, not the command line that launched it. Assigning the array puts N unrelated command strings into a single-valued ECS field. This is visible in the committed fixtures: test-fdr-command-history.log-expected.json now carries process.command_line as a two-element array, and test-windows.log-expected.json turns a previously scalar value into a one-element array, while every other event in the same expected file (for example the LogonBehaviorCompositionDetectInfo document with "command_line": "System") keeps a scalar string. The field is therefore scalar on most documents and multi-valued on CommandHistory documents. In ES|QL most functions and aggregations return null and emit a warning on multi-valued rows, so queries and rules over process.command_line behave differently for these documents. The events also get process.command_line without the matching process.args / process.args_count that split_command_line produces for every other command line. This also moves away from the direction already agreed on this PR (populate process.command_line only when the history holds a single command); the previous revision's data-loss problem is better solved by leaving the field unset for multi-command buffers than by making it an array. Recommendation: Keep the full list in crowdstrike.CommandHistory and only promote it to process.command_line when it is unambiguous: if (!remaining.trim().isEmpty()) {
commands.add(remaining.trim());
}
ctx.crowdstrike.CommandHistory = commands;
if (commands.size() == 1) {
ctx.process = ctx.process ?: [:];
ctx.process.command_line = commands.get(0);
}Then regenerate test-fdr-command-history.log-expected.json and test-windows.log-expected.json so the two-command document has no process.command_line and the single-command documents carry a scalar string. 🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills
|
||
| on_failure: | ||
| - append: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Severity: 🔵 Low The on_failure append processors added by this PR have no tag and use an ad-hoc error message instead of the standard ingest.on_failure* template every other handler in this pipeline uses; align them with the existing convention. DetailsBoth new script processors (script_command_history_to_process_command_line_8c3e1f92 here, and script_process_interactive_logon_type_9b3d6c7a further down) attach an on_failure append that omits the tag field and writes a custom string carrying only _ingest.on_failure_message. Every other on_failure handler in this pipeline uses a tagged append with the full template that also records the failing processor type, tag and pipeline, for example append_error_message_4ef54c75 on the json processor. Without those fields an error.message from these handlers cannot be traced back to the processor that produced it, and the untagged processors will be flagged by elastic-package check once the package moves to format_version 3.6.0 or later. Recommendation: Use the pipeline's standard tagged handler on both new scripts: on_failure:
- append:
tag: append_error_message_8c3e1f92
field: error.message
value: 'Processor {{{_ingest.on_failure_processor_type}}} with tag {{{_ingest.on_failure_processor_tag}}} in pipeline {{{_ingest.on_failure_pipeline}}} failed with message: {{{_ingest.on_failure_message}}}'🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills
|
||
| field: error.message | ||
| value: "Failed to parse CommandHistory: {{{_ingest.on_failure_message}}}" | ||
| - set: | ||
| tag: set_process_user_name_command_history_4e2a8f1c | ||
| description: Map crowdstrike.UserName to process.user.name for CommandHistory events. | ||
| if: ctx.crowdstrike?.event_simpleName == 'CommandHistory' && ctx.crowdstrike?.UserName instanceof String && ctx.crowdstrike.UserName != '' | ||
| field: process.user.name | ||
| copy_from: crowdstrike.UserName | ||
| ignore_empty_value: true | ||
| - rename: | ||
| tag: rename_crowdstrike_ImageFileName_to_process_executable_1e9d3140 | ||
| if: ctx._temp?.isLibrary != true && ctx._temp?.isDriver != true | ||
|
|
@@ -3194,6 +3235,31 @@ processors: | |
| } | ||
| } | ||
|
|
||
| - script: | ||
| tag: script_process_interactive_logon_type_9b3d6c7a | ||
| description: >- | ||
| Set process.interactive from crowdstrike.LogonType whenever a process | ||
| object is present in the event. Placed after all other process | ||
| processors so that any processor that creates a process object | ||
| (TargetProcessId, ContextProcessId, etc.) has already run. Logon types | ||
| 2 (Interactive), 10 (RemoteInteractive), 11 (CachedInteractive), and 12 | ||
| (CachedRemoteInteractive) indicate an interactive shell session; all | ||
| other types are non-interactive. | ||
| if: >- | ||
| ctx.crowdstrike?.LogonType instanceof String && | ||
| ctx.crowdstrike.LogonType != '' && | ||
| ctx.process != null | ||
| source: |- | ||
| // Logon type reference: 2=Interactive, 3=Network, 4=Batch, 5=Service, | ||
| // 7=Unlock, 8=NetworkCleartext, 9=NewCredentials, 10=RemoteInteractive, | ||
| // 11=CachedInteractive, 12=CachedRemoteInteractive, 13=CachedUnlock | ||
| def logonType = ctx.crowdstrike.LogonType; | ||
| ctx.process.interactive = (logonType == '2' || logonType == '10' || logonType == '11' || logonType == '12'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Severity: 🟠 High process.interactive is derived from crowdstrike.LogonType on every event that has any process object, which labels processes that are not the session's shell — restrict the processor to CommandHistory events. DetailsLogonType is an attribute of the Windows logon session, but ECS defines process.interactive per-process ("inferred from the processes file descriptors... the character device for the controlling tty is the same as stdin and stderr") — the definition this PR adds to docs/README.md line 3197. Applying the session's LogonType to whatever process object happens to be on the event produces wrong values, and the PR's own fixtures show it:
The ctx.process != null gate added in this revision keeps the field off events with no process object at all, but it does not distinguish the process the logon session actually belongs to from an unrelated context/target process reference, so both cases above still fire. Recommendation: Scope the processor to the event type this PR is about (CommandHistory), where the recorded shell genuinely is the process the logon session belongs to: - script:
tag: script_process_interactive_logon_type_9b3d6c7a
description: >-
Set process.interactive from crowdstrike.LogonType for CommandHistory
events only. On other FDR events LogonType describes the logon session,
not the process referenced by ContextProcessId/TargetProcessId, so the
value would not describe that process. Logon types 2 (Interactive),
10 (RemoteInteractive), 11 (CachedInteractive) and 12
(CachedRemoteInteractive) indicate an interactive shell session.
if: >-
ctx.crowdstrike?.event_simpleName == 'CommandHistory' &&
ctx.crowdstrike?.LogonType instanceof String &&
ctx.crowdstrike.LogonType != '' &&
ctx.process != null
source: |-
def logonType = ctx.crowdstrike.LogonType;
ctx.process.interactive = (logonType == '2' || logonType == '10' || logonType == '11' || logonType == '12');
on_failure:
- append:
field: error.message
value: "Failed to set process.interactive from LogonType: {{{_ingest.on_failure_message}}}"This reverts the added "interactive" lines in test-fdr.log-expected.json and test-windows.log-expected.json and keeps only the two CommandHistory assertions in test-fdr-command-history.log-expected.json. 🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's intentional. We want to map |
||
| on_failure: | ||
| - append: | ||
| field: error.message | ||
| value: "Failed to set process.interactive from LogonType: {{{_ingest.on_failure_message}}}" | ||
|
|
||
| # Cleanup. | ||
| - remove: | ||
| tag: remove_crowdstrike_event_platform_f15993d1 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Severity: 🔵 Low
confidence: highpath: packages/crowdstrike/data_stream/fdr/elasticsearch/ingest_pipeline/default.yml:1058The rewritten description misstates why the processor is safe and drops the note that the pilcrow delimiter is undocumented by CrowdStrike; restore the delimiter rationale and describe the actual guard.
Details
The description now claims the processor is "Placed after split_command_line so that processor only runs on string-valued command lines from crowdstrike.CommandLine." That is not what the ordering does: split_command_line reads process.command_line (populated from crowdstrike.CommandLine), and it is the
ctx.crowdstrike?.CommandHistory instanceof Stringcondition, not the position, that keeps this processor off already-converted array values. The sentence is also grammatically broken.The previous revision recorded that U+00B6 PILCROW SIGN is not formally documented by CrowdStrike but is consistently observed as the delimiter in FDR CommandHistory events. That is the non-obvious reason a magic character appears in the source, and it is the one piece of context a future maintainer cannot recover from the code; the rewrite removed it.
Recommendation:
Restore the delimiter rationale and state the real guard:
🤖 AI-Generated Review | Vera Review Bot | 📚 Knowledge base: integration-skills