|
1 | | -# macOS normalization and rule review |
| 1 | +# macOS normalization and correlation review |
2 | 2 |
|
3 | | -Scope Endpoint Security history and grouping to the documented agent dataSource; reject missing/unknown host identity. |
| 3 | +This draft targets UTMStack `v11` and changes both macOS filter configurations |
| 4 | +and all 36 macOS rules. The pinned `go-sdk v1.1.31` Event/Side protobuf is the |
| 5 | +schema authority. The local dictionary is supporting material. |
4 | 6 |
|
5 | | -This draft targets UTMStack `v11`. It contains 0 filter changes |
6 | | -and 1 rule changes for this technology only. Review covered |
7 | | -2 filter configurations and 36 matching shipped rule files. |
8 | | -Unchanged rules are listed in the regression manifest; they are not duplicated in the diff. |
| 7 | +## Producer and consumer corrections |
9 | 8 |
|
10 | | -## Contract and validation |
| 9 | +The 30 sampled native raw records contain JSON with `message`, `process`, |
| 10 | +`class_name`, `level`, `timestamp`, and identifiers whose names contain underscores. |
| 11 | +The Go agent source supplies the dataType/dataSource envelope and forwards lines |
| 12 | +from an external collector; the collector implementation was not available in |
| 13 | +the inspected source tree. Its JSON shape is established by observed records, |
| 14 | +while Apple documentation establishes class and level semantics. SDK JSON field |
| 15 | +sanitization removes underscores and preserves letter case. The existing filter |
| 16 | +already produces `log.message`; 35 rules instead read `log.eventMessage`. Those |
| 17 | +rules now read the emitted message. The filter copies the process name to |
| 18 | +`origin.process`, the raw timestamp to `deviceTime`, and meaningful agent |
| 19 | +`dataSource` to `origin.host`. Every native record gets `dataSource` from the |
| 20 | +agent hostname; the native producer uses `unknown` if hostname lookup fails. |
11 | 21 |
|
12 | | -- Compared exact standard names/types with go-sdk v1.1.31 and the supplied UTMStack dictionaries. |
13 | | -- Checked documented pipeline ordering, rename/move behavior, open vendor log fields, |
14 | | - event-side versus alert-side fields, and surviving fields used by affected rule predicates/history/grouping. |
15 | | -- Strict SDK configuration decoding and actual CEL compilation pass for this scope. |
16 | | -- 2 synthetic normalization cases pass, including SDK Event conversion and any |
17 | | - trigger predicate assertions recorded in the manifest. |
18 | | -- The scoped alerts module tests and `git diff --check` pass with the shared contract runner applied. |
| 22 | +Legacy `eventMessage` and `messageType` remain available while supplying |
| 23 | +`log.message` and `log.level` when the native fields are absent. Native values |
| 24 | +win if both forms exist. `class_name` becomes the compatible `log.className` |
| 25 | +alias; the original sanitized `log.classname` remains available. The four |
| 26 | +existing activity/process/store/thread identifier aliases remain unchanged. |
| 27 | +There is no PID field in the SDK Side message, so the numeric process identifier |
| 28 | +stays in `log.processIdentifier`. No non-schema `origin.pid` is introduced. |
19 | 29 |
|
20 | | -The shared alert-contract PR supplies the reusable Go runner for the manifest in |
21 | | -`plugins/alerts/testdata/filter-contracts/macos.json`. Apply that support before running `go test ./...` in `plugins/alerts`. |
| 30 | +Apple documents `OSLogEntryLog` as an entry generated by the logging API. Three |
| 31 | +class-gated rules accept this native class alongside their legacy `eventType` |
| 32 | +checks; the filter does not invent `eventType`. Keychain and Endpoint Security |
| 33 | +consume `log.level`. Apple debug, info/notice, error, and fault map respectively |
| 34 | +to standard severity debug, info, error, and critical; undefined remains unmapped. |
| 35 | +Vendor levels remain intact. A log level does not establish operation success, |
| 36 | +so the filter does not synthesize `actionResult`. |
22 | 37 |
|
23 | | -The changed rules also require the shared alert-grouping fix to resolve `lastEvent.*` values correctly at runtime. |
| 38 | +A native process string identifies the process name, not an executable path. |
| 39 | +Only an explicitly supplied legacy `processImagePath` is copied to `origin.path`, |
| 40 | +with its vendor alias preserved. Neither process nor sender is fabricated into |
| 41 | +a path. All 36 rules retain `adversary: origin`; the host/process promotions feed |
| 42 | +those existing actor roles. Host grouping now has an actual source identity; |
| 43 | +process grouping uses `adversary.process`. Optional user grouping remains empty |
| 44 | +when the producer has no user. Existing message grouping uses `lastEvent.log.message`. |
24 | 45 |
|
25 | | -The model starts from synthetic extraction results. It does not run complex grok, |
26 | | -JSON/KV/XML/CSV extraction, time conversion, dynamic plugins, historical OpenSearch |
27 | | -queries, or the closed EventProcessor. Raw vendor logs and resulting alerts must |
28 | | -still be checked in staging before rollout. No customer false-positive reduction |
29 | | -has been measured and no production rollout is included. |
| 46 | +## Wrappers and history |
30 | 47 |
|
31 | | -The shipped macOS filters do not write origin.host. The history query and grouping use the input dataSource instead; agent ingestion must supply its documented per-host identity. Missing/unknown dataSource is rejected. The normalization test supplies this input metadata explicitly. |
| 48 | +The optional forwarding prefix is anchored at the start of raw input. It first |
| 49 | +captures `log.datasourceoverride`, then promotes only a nonempty, whitespace-free |
| 50 | +value other than `unknown` to `dataSource`. Invalid overrides stay in their |
| 51 | +vendor field without replacing ingress identity. JSON `datasource_override` |
| 52 | +uses the same sanitized spelling and validation. The native filter handles the |
| 53 | +wrapper itself and both filter execution orders are tested. Wrapper and legacy |
| 54 | +message copies preserve newlines. The compatibility filter skips extraction |
| 55 | +when a message is already available, avoiding duplicate capture. The original |
| 56 | +noise-drop policy runs after JSON extraction and legacy level normalization, |
| 57 | +before the remaining normalization work. |
| 58 | + |
| 59 | +Endpoint Security and ransomware history searches use the meaningful per-agent |
| 60 | +`dataSource` and an exact candidate marker computed by the filter. Without that |
| 61 | +marker, any earlier macOS event from the same source could satisfy a security |
| 62 | +threshold. Marker conditions repeat the complete rule trigger; tests check their |
| 63 | +parity. Untrusted incoming markers are removed and recomputed. The rules retain |
| 64 | +their existing counts/windows and reject empty/unknown identities. Filter and |
| 65 | +rules must deploy together; old indexed events do not have candidate markers, |
| 66 | +so each history window must warm up after deployment. |
| 67 | + |
| 68 | +The kernel-extension rule no longer treats a missing or non-system executable |
| 69 | +path as evidence that the loaded extension is untrusted. Its first branch |
| 70 | +requires explicit unsigned or invalid-signature message evidence. Process |
| 71 | +exclusions in kernel-extension, XProtect and TCC checks require a present process |
| 72 | +name. The other negative predicates were checked for positive same-field gates. |
| 73 | + |
| 74 | +## Validation and bounded evidence |
| 75 | + |
| 76 | +- Standalone `macos_contract_test.go` tests 99 synthetic raw fixtures in three |
| 77 | + configurations: native filter alone, compatibility filter first, and |
| 78 | + compatibility filter last. This includes a native positive and benign negative |
| 79 | + for each of the 36 rules, wrapper edge cases, multiline/legacy messages, |
| 80 | + severity/identity mappings, noise drops and absent-field regressions. |
| 81 | +- All 36 real SDK CEL predicates are compiled/evaluated. Forty-two positive |
| 82 | + fixtures assert intended matches, source grouping identities, and available |
| 83 | + history placeholders. The two candidate-marker predicates must agree with |
| 84 | + their consumers. Negative fixtures assert that none of the 36 rules matches. |
| 85 | +- Real SDK historical requests execute against a local mock OpenSearch service. |
| 86 | + Both history rules are checked below/at count thresholds, against another |
| 87 | + source, benign non-candidate history, and expired events. The mock test runs |
| 88 | + in a subprocess because the SDK OpenSearch client is a process-wide singleton. |
| 89 | +- The shared contract manifest covers 90 raw JSON cases with opt-in decoding and recursive |
| 90 | + history-placeholder preflight supplied by draft #2590. The standalone macOS |
| 91 | + tests also cover wrappers, which the shared harness deliberately does not model. |
| 92 | +- Thirty distinct raw/normalized records from three instances were inspected |
| 93 | + read-only. All use native JSON; all lack `log.eventMessage`, `origin.host` and |
| 94 | + `origin.process`; all carry a raw timestamp differing from stored `deviceTime`. |
| 95 | + The relevant deployed repository filters match the baseline mappings. Private |
| 96 | + evidence retains instance/document anchors, raw records and configuration |
| 97 | + hashes without publishing customer payloads. |
| 98 | +- Replaying those 30 raw records through the offline parser model and actual SDK |
| 99 | + CEL yields zero candidates. No wrapper override was observed in bounded |
| 100 | + 30-day searches on those instances. Wrapper defects are supported by source, |
| 101 | + deployed filter configuration and synthetic tests, not observed wrapper events. |
| 102 | + |
| 103 | +Filter extraction and transformations are an offline model using SDK field |
| 104 | +sanitization and documented steps. Configuration decoding, CEL, Event conversion, |
| 105 | +placeholder expansion, query construction and count decisions use the actual |
| 106 | +SDK. This does not execute the closed EventProcessor or validate live alert |
| 107 | +creation/delivery. No customer writes or production rollout were performed. |
| 108 | + |
| 109 | +## Activation and remaining validation |
| 110 | + |
| 111 | +Correcting 35 message consumers can materially increase alert candidates. |
| 112 | +Populating host grouping can also consolidate events that previously had no |
| 113 | +usable grouping identity. The bounded sample had no candidates; it does not |
| 114 | +measure false-positive rates or establish detection coverage. Before rollout, |
| 115 | +staging must compare actual raw/output/alert records and per-rule volumes, |
| 116 | +identity grouping, time/count behavior, and expected benign traffic. |
| 117 | + |
| 118 | +Native samples do not include a process image path. Legacy branches comparing |
| 119 | +that executable path to a TCC database or Gatekeeper resource path remain |
| 120 | +semantically uncertain and unexercised by the native positive fixtures. No |
| 121 | +resource path was invented as a process image to make those branches pass; |
| 122 | +message-based branches supply the tested matches. Further producer evidence is |
| 123 | +required before changing those resource-specific heuristics. |
| 124 | + |
| 125 | +The shared alert-grouping correction in draft #2590 is required for runtime |
| 126 | +resolution of the remaining `lastEvent.*` grouping keys. Review and deploy the |
| 127 | +filter, its consumers and that grouping support together. This draft does not |
| 128 | +claim that existing heuristic matches prove malware or compromise. |
32 | 129 |
|
33 | 130 | ## References |
34 | 131 |
|
35 | | -- [SDK schema](https://github.com/threatwinds/go-sdk/blob/v1.1.31/plugins/plugins.proto) |
| 132 | +- [SDK schema v1.1.31](https://github.com/threatwinds/go-sdk/blob/v1.1.31/plugins/plugins.proto) |
| 133 | +- [SDK sanitization](https://github.com/threatwinds/go-sdk/blob/v1.1.31/utils/fields.go) |
| 134 | +- [Native macOS producer](https://github.com/utmstack/UTMStack/blob/v11/agent/collector/platform/darwin.go) |
36 | 135 | - [Filter steps](https://github.com/threatwinds/go-sdk/wiki/Filter-Steps-Reference) |
37 | 136 | - [Standard event schema](https://github.com/threatwinds/go-sdk/wiki/Standard-Event-Schema) |
38 | | -- [Rule implementation](https://github.com/threatwinds/go-sdk/wiki/Implementing-Rules) |
39 | | - |
40 | | -`afterEvents`, empty noncapturing grok names, supported numeric strings, and custom |
41 | | -`log.*` fields are accepted. Existing textual protocol casing and vendor action names |
42 | | -are preserved unless a concrete consumer mismatch requires correction. |
| 137 | +- [Correlation rules](https://github.com/threatwinds/go-sdk/wiki/Implementing-Rules) |
| 138 | +- [Apple OSLogEntryLog](https://developer.apple.com/documentation/oslog/oslogentrylog) |
| 139 | +- [Apple log entry levels](https://developer.apple.com/documentation/oslog/oslogentrylog/level-swift.enum) |
0 commit comments