@@ -15,88 +15,214 @@ inputs:
1515 dataset: crowdstrike.host
1616 interval: 24h
1717 program: |-
18- state.with(
19- (
20- state.?want_more.orValue(false) ?
21- state.start_time
22- :
23- state.?cursor.last_timestamp.orValue(
24- (now - duration(state.initial_interval)).format(time_layout.RFC3339)
25- )
26- ).as(start_time,
27- request(
28- "GET",
29- state.url.trim_right("/") + "/devices/combined/devices/v1?" + {
30- ?"offset": state.?next.page_token.optMap(v, [v]),
31- "limit": [string(state.batch_size)],
32- "sort": ["modified_timestamp|asc"],
33- "filter": [
34- [
35- "modified_timestamp:>'" + start_time + "'",
36- ?state.?query.optMap(q, "(" + q + ")"),
37- ].join("+"),
38- ],
39- ?"fields": state.?select_fields.optMap(v, [v.trim_right(",")+",modified_timestamp"]),
40- }.format_query()
41- ).do_request().as(resp, (resp.StatusCode == 200) ?
42- resp.Body.decode_json().as(body,
43- (size(body.?errors.orValue([])) > 0) ?
44- {
45- "events": body.errors.map(error,
18+ // This logic determines which CrowdStrike devices endpoint to call. The
19+ // "/devices/combined/devices/v1" endpoint is not supported in GovCloud
20+ // environments, so we must fall back to the "/devices/entities/devices/v2"
21+ // endpoint when running in GovCloud.
22+ //
23+ // The selection works in two stages:
24+ // 1. If the "gov_cloud" flag is explicitly enabled in the data stream manifest,
25+ // we always use the "/devices/entities/devices/v2" endpoint.
26+ //
27+ // 2. If the flag is disabled, the program inspects the base URL to infer
28+ // whether the integration is pointed at a GovCloud environment. If the
29+ // URL matches a known GovCloud domain, we avoid calling the unsupported
30+ // combined endpoint and instead use "/devices/entities/devices/v2".
31+ //
32+ // This dual-check mechanism ensures that GovCloud users do not encounter errors
33+ // even if they forget to enable the "gov_cloud" flag in the manifest.
34+ (
35+ state.gov_cloud || state.url.trim_right("/") in ["https://api.laggar.gcw.crowdstrike.com", "https://api.us-gov-2.crowdstrike.mil"] ?
36+ (
37+ state.want_more ?
38+ state.?page.start
39+ :
40+ optional.of(state.?cursor.last_timestamp.orValue((now - duration(state.initial_interval)).format(time_layout.RFC3339)))
41+ ).as(filter,
42+ state.with(
43+ get_request(
44+ state.url.trim_right("/") + "/devices/queries/devices/v1?" + {
45+ "sort": ["modified_timestamp.asc"],
46+ "offset": [string(state.offset)],
47+ "limit": [string(state.batch_size)],
48+ ?"filter": (filter.hasValue() || state.?query.orValue("") != "") ?
49+ optional.of(
50+ [
51+ [
52+ ?filter.optMap(f, "modified_timestamp:>\"" + f + "\""),
53+ ?state.?query.optMap(q, "(" + q + ")"),
54+ ].join("+"),
55+ ]
56+ )
57+ :
58+ optional.none(),
59+ }.format_query()
60+ ).do_request().as(get_resp, (get_resp.StatusCode == 200) ?
61+ bytes(get_resp.Body).decode_json().as(body,
62+ (int(state.offset) + body.resources.size() < body.meta.pagination.total).as(want_more,
4663 {
47- "error": {
48- "code": string(error.code),
49- "message": string(error.message),
64+ "next": {
65+ ?"resources": (body.resources.size() > 0) ? optional.of(body.resources) : optional.none(),
5066 },
67+ "filter": filter,
68+ "events": [],
69+ "offset": want_more ? (int(state.offset) + body.resources.size()) : 0,
70+ "want_more": want_more,
5171 }
52- ),
53- "next": {},
54- "want_more": false,
55- }
72+ )
73+ )
5674 :
5775 {
58- "events": has(body.resources) ?
59- body.resources.map(e,
76+ "events": {
77+ "error": {
78+ "code": string(get_resp.StatusCode),
79+ "id": string(get_resp.Status),
80+ "message": "GET:" + (
81+ (size(get_resp.Body) != 0) ?
82+ string(get_resp.Body)
83+ :
84+ string(get_resp.Status) + " (" + string(get_resp.StatusCode) + ")"
85+ ),
86+ },
87+ },
88+ "want_more": false,
89+ "next": {},
90+ }
91+ )
92+ ).as(state,
93+ state.with(
94+ !has(state.?next.resources) ? // Exit early due to GET failure or no resources to collect.
95+ state
96+ :
97+ post_request(
98+ state.url + "/devices/entities/devices/v2",
99+ "application/json",
100+ {"ids": state.next.resources}.encode_json()
101+ ).do_request().as(post_resp, (post_resp.StatusCode == 200) ?
102+ bytes(post_resp.Body).decode_json().as(inner_body,
60103 {
61- "message": e.encode_json(),
104+ "events": inner_body.resources.map(e,
105+ {
106+ "message": e.encode_json(),
107+ }
108+ ),
109+ "cursor": {
110+ ?"last_timestamp": (has(inner_body.resources) && inner_body.resources.size() > 0) ?
111+ optional.of(inner_body.resources.map(e, timestamp(e.modified_timestamp)).max().format(time_layout.RFC3339))
112+ :
113+ state.?cursor.last_timestamp,
114+ },
115+ "page": {
116+ "start": state.filter,
117+ },
118+ "next": {},
62119 }
63120 )
64121 :
65- [],
66- "start_time": start_time,
67- "next": {
68- ?"page_token": body.?meta.pagination.next,
69- },
70- "cursor": {
71- // The records are sorted in ascending order, based on value of modified_timestamp.
72- // In the next interval we start from the last event (newest) time.
73- ?"last_timestamp": (has(body.resources) && body.resources.size() > 0) ?
74- optional.of(timestamp(body.resources[size(body.resources) - 1].modified_timestamp).format(time_layout.RFC3339))
75- :
76- state.?cursor.last_timestamp,
122+ {
123+ "events": {
124+ "error": {
125+ "code": string(post_resp.StatusCode),
126+ "id": string(post_resp.Status),
127+ "message": "POST:" + (
128+ (size(post_resp.Body) != 0) ?
129+ string(post_resp.Body)
130+ :
131+ string(post_resp.Status) + " (" + string(post_resp.StatusCode) + ")"
132+ ),
133+ },
134+ },
135+ "want_more": false,
136+ "next": {},
137+ }
138+ )
139+ )
140+ )
141+ )
142+ :
143+ state.with(
144+ (
145+ state.?want_more.orValue(false) ?
146+ state.start_time
147+ :
148+ state.?cursor.last_timestamp.orValue(
149+ (now - duration(state.initial_interval)).format(time_layout.RFC3339)
150+ )
151+ ).as(start_time,
152+ request(
153+ "GET",
154+ state.url.trim_right("/") + "/devices/combined/devices/v1?" + {
155+ ?"offset": state.?next.page_token.optMap(v, [v]),
156+ "limit": [string(state.batch_size)],
157+ "sort": ["modified_timestamp|asc"],
158+ "filter": [
159+ [
160+ "modified_timestamp:>'" + start_time + "'",
161+ ?state.?query.optMap(q, "(" + q + ")"),
162+ ].join("+"),
163+ ],
164+ ?"fields": state.?select_fields.optMap(v, [v.trim_right(",")+",modified_timestamp"]),
165+ }.format_query()
166+ ).do_request().as(resp, (resp.StatusCode == 200) ?
167+ resp.Body.decode_json().as(body,
168+ (size(body.?errors.orValue([])) > 0) ?
169+ {
170+ "events": body.errors.map(error,
171+ {
172+ "error": {
173+ "code": string(error.code),
174+ "message": string(error.message),
175+ },
176+ }
177+ ),
178+ "next": {},
179+ "want_more": false,
180+ }
181+ :
182+ {
183+ "events": has(body.resources) ?
184+ body.resources.map(e,
185+ {
186+ "message": e.encode_json(),
187+ }
188+ )
189+ :
190+ [],
191+ "start_time": start_time,
192+ "next": {
193+ ?"page_token": body.?meta.pagination.next,
194+ },
195+ "cursor": {
196+ // The records are sorted in ascending order, based on value of modified_timestamp.
197+ // In the next interval we start from the last event (newest) time.
198+ ?"last_timestamp": (has(body.resources) && body.resources.size() > 0) ?
199+ optional.of(timestamp(body.resources[size(body.resources) - 1].modified_timestamp).format(time_layout.RFC3339))
200+ :
201+ state.?cursor.last_timestamp,
202+ },
203+ "want_more": has(body.?meta.pagination.next),
204+ }
205+ )
206+ :
207+ {
208+ "events": {
209+ "error": {
210+ "code": string(resp.StatusCode),
211+ "id": string(resp.Status),
212+ "message": "GET " + state.url.trim_right("/") + "/devices/combined/devices/v1:" + (
213+ (size(resp.Body) != 0) ?
214+ string(resp.Body)
215+ :
216+ string(resp.Status) + " (" + string(resp.StatusCode) + ")"
217+ ),
218+ },
77219 },
78- "want_more": has(body.?meta.pagination.next),
220+ "next": {},
221+ "want_more": false,
79222 }
223+ )
80224 )
81- :
82- {
83- "events": {
84- "error": {
85- "code": string(resp.StatusCode),
86- "id": string(resp.Status),
87- "message": "GET " + state.url.trim_right("/") + "/devices/combined/devices/v1:" + (
88- (size(resp.Body) != 0) ?
89- string(resp.Body)
90- :
91- string(resp.Status) + " (" + string(resp.StatusCode) + ")"
92- ),
93- },
94- },
95- "next": {},
96- "want_more": false,
97- }
98225 )
99- )
100226 )
101227 publisher_pipeline.disable_host: true
102228 redact:
@@ -109,9 +235,12 @@ inputs:
109235 maxbackups: 5
110236 resource.url: http://host.tld
111237 state:
112- batch_size: 10000
238+ batch_size: 5000
239+ gov_cloud: false
113240 initial_interval: 24h
241+ offset: 0
114242 select_fields: null
243+ want_more: false
115244 tags:
116245 - preserve_original_event
117246 - preserve_duplicate_custom_fields
0 commit comments