Skip to content

Commit 5fa62a7

Browse files
authored
[aws/config]: Skip service-linked rules and tolerate per-rule errors (#19803)
aws/config: skip service-linked rules and tolerate per-rule errors The AWS Config data stream listed every Config rule and then called GetComplianceDetailsByConfigRule for each one. AWS owns service-linked rules and blocks that call for them with an AccessDeniedException, which aborted the whole collection cycle and degraded the input so no rules were ingested. Filter out service-linked rules in the DescribeConfigRules response by dropping any rule with a populated CreatedBy field, which AWS sets only for service-linked rules, so the blocked call is never attempted. As a defence-in-depth measure, treat a per-rule GetComplianceDetailsByConfigRule failure as non-fatal: emit the error as an advancing event and continue with the remaining rules instead of stopping the cycle. Thread the resource url through every output state branch so the program stays self-contained across evaluations. Add a service-linked rule and a matching AccessDeniedException fixture to the system-test mock to confirm the rule is filtered and never queried, and bump the package to 6.20.2.
1 parent df03b65 commit 5fa62a7

4 files changed

Lines changed: 131 additions & 27 deletions

File tree

packages/aws/changelog.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
# newer versions go on top
2+
- version: "6.20.2"
3+
changes:
4+
- description: Skip AWS service-linked Config rules when collecting compliance details so the AWS Config data stream no longer degrades on `AccessDeniedException`, and treat per-rule compliance errors as non-fatal so a single failing rule does not stop ingestion of the rest.
5+
type: bugfix
6+
link: https://github.com/elastic/integrations/pull/19803
27
- version: "6.20.1"
38
changes:
49
- description: Revert addition of assignment of host.id from CloudTrail target hosts.

packages/aws/data_stream/config/_dev/deploy/docker/files/config.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,23 @@ rules:
8888
"SourceIdentifier": "ACCESS_KEYS_ROTATED"
8989
}
9090
},
91+
{
92+
"ConfigRuleArn": "arn:aws:config:us-east-1:11223344556:config-rule/config-rule-slr",
93+
"ConfigRuleId": "config-rule-slr",
94+
"ConfigRuleName": "service-linked-config-rule",
95+
"ConfigRuleState": "ACTIVE",
96+
"CreatedBy": "securityhub.amazonaws.com",
97+
"Description": "AWS service-linked rule owned by AWS Security Hub. Querying its compliance details is blocked by AWS with AccessDeniedException, so the integration must skip it.",
98+
"EvaluationModes": [
99+
{
100+
"Mode": "DETECTIVE"
101+
}
102+
],
103+
"Source": {
104+
"Owner": "AWS",
105+
"SourceIdentifier": "SECURITYHUB_SERVICE_LINKED"
106+
}
107+
},
91108
{
92109
"ConfigRuleArn": "arn:aws:config:us-east-1:11223344556:config-rule/config-rule-id2",
93110
"ConfigRuleId": "config-rule-id2",
@@ -234,3 +251,25 @@ rules:
234251
]
235252
}
236253
`}}
254+
# Safety net: AWS blocks GetComplianceDetailsByConfigRule for service-linked
255+
# rules. The integration must skip them via the CreatedBy filter and never
256+
# reach this response. If the filter regresses and this rule is queried, AWS
257+
# returns this 400 AccessDeniedException, which the program must treat as
258+
# non-fatal (advance past it) rather than aborting the whole collection cycle.
259+
- path: /
260+
methods: ["POST"]
261+
request_headers:
262+
Content-Type:
263+
- "application/x-amz-json-1.1"
264+
X-Amz-Target:
265+
- "StarlingDoveService.GetComplianceDetailsByConfigRule"
266+
request_body: '{"ConfigRuleName":"service-linked-config-rule","Limit":2}'
267+
responses:
268+
- status_code: 400
269+
body: |-
270+
{{ minify_json `
271+
{
272+
"__type": "AccessDeniedException",
273+
"Message": "An AWS service owns ServiceLinkedConfigRule. You do not have permissions to take action on this rule."
274+
}
275+
`}}

packages/aws/data_stream/config/agent/stream/cel.yml.hbs

Lines changed: 86 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ program: |
6565
{
6666
// State management: preserve existing state from previous iteration
6767
"worklist": state.worklist,
68+
"url": state.url,
6869
"batch_size": state.batch_size,
6970
"has_next": state.has_next,
7071
"next": state.next,
@@ -188,10 +189,21 @@ program: |
188189
}
189190
).do_request().as(resp, (resp.StatusCode == 200) ?
190191
// Response Processing: Successfully got config rules list
191-
resp.Body.decode_json().as(body,
192+
resp.Body.decode_json().as(raw_body,
193+
// Skip AWS service-linked rules. AWS owns these rules and blocks
194+
// GetComplianceDetailsByConfigRule for them with an AccessDeniedException
195+
// ("An AWS service owns ServiceLinkedConfigRule"). They are identified by a
196+
// populated CreatedBy field, which AWS sets only for service-linked rules.
197+
// Filtering them out of the worklist here avoids the blocked call entirely.
198+
raw_body.with({
199+
"ConfigRules": raw_body.?ConfigRules.orValue([]).filter(r,
200+
r.?CreatedBy.orValue("") == ""
201+
),
202+
}).as(body,
192203
{
193204
// State Update: Store the fetched config rules as worklist
194205
"worklist": body,
206+
"url": state.url,
195207
"next": 0,
196208
"has_next": has(body.NextToken),
197209
"batch_size": state.batch_size,
@@ -238,11 +250,12 @@ program: |
238250
].join("\n").sha256().hex()
239251
].join("\n"),
240252
?"session_token": state.?session_token,
253+
"url": state.url,
241254
"access_key": state.access_key,
242255
"secret_key": state.secret_key,
243256
"aws_region": state.aws_region,
244257
"tld": state.tld,
245-
}
258+
})
246259
)
247260
:
248261
// Error Handling: DescribeConfigRules request failed
@@ -263,6 +276,7 @@ program: |
263276
"want_more": false,
264277
"batch_size": state.batch_size,
265278
?"session_token": state.?session_token,
279+
"url": state.url,
266280
"access_key": state.access_key,
267281
"secret_key": state.secret_key,
268282
"aws_region": state.aws_region,
@@ -344,49 +358,95 @@ program: |
344358
"has_next": config_rules.has_next,
345359
"batch_size": config_rules.batch_size,
346360
?"session_token": config_rules.?session_token,
361+
"url": config_rules.url,
347362
"access_key": config_rules.access_key,
348363
"secret_key": config_rules.secret_key,
349364
"aws_region": config_rules.aws_region,
350365
"tld": config_rules.tld,
351366
}
352367
)
353368
:
354-
// Error Handling: GetComplianceDetailsByConfigRule request failed
369+
// Error Handling: GetComplianceDetailsByConfigRule request failed for this
370+
// rule. Treat it as non-fatal: emit the error as an advancing (array) event
371+
// and continue with the remaining rules instead of aborting the whole cycle.
372+
// This prevents a single per-rule access error (for example a service-linked
373+
// rule that slipped past the CreatedBy filter) from degrading the entire input
374+
// and stopping all ingestion. The array form advances the cursor; the
375+
// ingest pipeline's terminate processor handles the error event.
355376
{
356-
"events": {
357-
"error": {
358-
"code": string(resp.StatusCode),
359-
"id": string(resp.Status),
360-
"message": "GetComplianceDetailsByConfigRule: POST " + state.url.trim_right("/") + " " +
361-
(
362-
(size(resp.Body) != 0) ?
363-
string(resp.Body)
364-
:
365-
string(resp.Status) + " (" + string(resp.StatusCode) + ")"
366-
),
377+
"events": [
378+
{
379+
"error": {
380+
"code": string(resp.StatusCode),
381+
"id": string(resp.Status),
382+
"message": "GetComplianceDetailsByConfigRule: POST " + state.url.trim_right("/") + " " +
383+
(
384+
(size(resp.Body) != 0) ?
385+
string(resp.Body)
386+
:
387+
string(resp.Status) + " (" + string(resp.StatusCode) + ")"
388+
),
389+
},
367390
},
391+
],
392+
"next_page": {
393+
?"rule_token": config_rules.?next_page.rule_token,
368394
},
369-
"want_more": false,
395+
"want_more": config_rules.has_next || (int(config_rules.next) + 1 < size(config_rules.worklist.ConfigRules)),
396+
"next": (int(config_rules.next) + 1 < size(config_rules.worklist.ConfigRules)) ?
397+
(int(config_rules.next) + 1)
398+
:
399+
0,
400+
"worklist": (int(config_rules.next) + 1 < size(config_rules.worklist.ConfigRules)) ?
401+
config_rules.worklist
402+
:
403+
{},
404+
"has_next": config_rules.has_next,
370405
"batch_size": config_rules.batch_size,
371406
?"session_token": config_rules.?session_token,
407+
"url": config_rules.url,
372408
"access_key": config_rules.access_key,
373409
"secret_key": config_rules.secret_key,
374410
"aws_region": config_rules.aws_region,
375411
"tld": config_rules.tld,
376412
}
377413
)
378414
:
379-
// Final State: No config rules found, return empty result
380-
{
381-
"events": [],
382-
"want_more": false,
383-
"batch_size": config_rules.batch_size,
384-
?"session_token": config_rules.?session_token,
385-
"access_key": config_rules.access_key,
386-
"secret_key": config_rules.secret_key,
387-
"aws_region": config_rules.aws_region,
388-
"tld": config_rules.tld,
389-
}
415+
// Final State: No config rules to process in this page. If DescribeConfigRules
416+
// paginated (rule_token present) or another rule page is pending (has_next) —
417+
// for example because every rule on this page was a filtered service-linked
418+
// rule — emit a placeholder event so the cursor persists and continue fetching;
419+
// otherwise return an empty result and stop.
420+
(config_rules.?has_next.orValue(false) || has(config_rules.?next_page.rule_token)) ?
421+
{
422+
"events": [{"message": "retry"}],
423+
"next_page": {
424+
?"rule_token": config_rules.?next_page.rule_token,
425+
},
426+
"want_more": true,
427+
"next": 0,
428+
"worklist": {},
429+
"has_next": config_rules.?has_next.orValue(false),
430+
"batch_size": config_rules.batch_size,
431+
?"session_token": config_rules.?session_token,
432+
"url": config_rules.url,
433+
"access_key": config_rules.access_key,
434+
"secret_key": config_rules.secret_key,
435+
"aws_region": config_rules.aws_region,
436+
"tld": config_rules.tld,
437+
}
438+
:
439+
{
440+
"events": [],
441+
"want_more": false,
442+
"batch_size": config_rules.batch_size,
443+
?"session_token": config_rules.?session_token,
444+
"url": config_rules.url,
445+
"access_key": config_rules.access_key,
446+
"secret_key": config_rules.secret_key,
447+
"aws_region": config_rules.aws_region,
448+
"tld": config_rules.tld,
449+
}
390450
)
391451
tags:
392452
{{#if preserve_original_event}}

packages/aws/manifest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
format_version: 3.4.0
22
name: aws
33
title: AWS
4-
version: 6.20.1
4+
version: 6.20.2
55
description: Collect logs and metrics from Amazon Web Services (AWS) with Elastic Agent.
66
type: integration
77
categories:

0 commit comments

Comments
 (0)