1use std::collections::HashMap;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Arc;
9
10use async_trait::async_trait;
11use chrono::Utc;
12use http::StatusCode;
13use parking_lot::Mutex;
14use serde_json::{json, Value};
15use tokio::sync::Mutex as AsyncMutex;
16use uuid::Uuid;
17
18use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
19use fakecloud_lambda::runtime::ContainerRuntime;
20use fakecloud_lambda::SharedLambdaState;
21use fakecloud_persistence::SnapshotStore;
22
23use crate::persistence::save_config_snapshot;
24use crate::state::*;
25use crate::validate::{self, CrossServiceStates};
26
27pub const SUPPORTED_ACTIONS: &[&str] = &[
28 "AssociateResourceTypes",
29 "BatchGetAggregateResourceConfig",
30 "BatchGetResourceConfig",
31 "DeleteAggregationAuthorization",
32 "DeleteConfigRule",
33 "DeleteConfigurationAggregator",
34 "DeleteConfigurationRecorder",
35 "DeleteConformancePack",
36 "DeleteDeliveryChannel",
37 "DeleteEvaluationResults",
38 "DeleteOrganizationConfigRule",
39 "DeleteOrganizationConformancePack",
40 "DeletePendingAggregationRequest",
41 "DeleteRemediationConfiguration",
42 "DeleteRemediationExceptions",
43 "DeleteResourceConfig",
44 "DeleteRetentionConfiguration",
45 "DeleteServiceLinkedConfigurationRecorder",
46 "DeleteStoredQuery",
47 "DeliverConfigSnapshot",
48 "DescribeAggregateComplianceByConfigRules",
49 "DescribeAggregateComplianceByConformancePacks",
50 "DescribeAggregationAuthorizations",
51 "DescribeComplianceByConfigRule",
52 "DescribeComplianceByResource",
53 "DescribeConfigRuleEvaluationStatus",
54 "DescribeConfigRules",
55 "DescribeConfigurationAggregatorSourcesStatus",
56 "DescribeConfigurationAggregators",
57 "DescribeConfigurationRecorderStatus",
58 "DescribeConfigurationRecorders",
59 "DescribeConformancePackCompliance",
60 "DescribeConformancePackStatus",
61 "DescribeConformancePacks",
62 "DescribeDeliveryChannelStatus",
63 "DescribeDeliveryChannels",
64 "DescribeOrganizationConfigRuleStatuses",
65 "DescribeOrganizationConfigRules",
66 "DescribeOrganizationConformancePackStatuses",
67 "DescribeOrganizationConformancePacks",
68 "DescribePendingAggregationRequests",
69 "DescribeRemediationConfigurations",
70 "DescribeRemediationExceptions",
71 "DescribeRemediationExecutionStatus",
72 "DescribeRetentionConfigurations",
73 "DisassociateResourceTypes",
74 "GetAggregateComplianceDetailsByConfigRule",
75 "GetAggregateConfigRuleComplianceSummary",
76 "GetAggregateConformancePackComplianceSummary",
77 "GetAggregateDiscoveredResourceCounts",
78 "GetAggregateResourceConfig",
79 "GetComplianceDetailsByConfigRule",
80 "GetComplianceDetailsByResource",
81 "GetComplianceSummaryByConfigRule",
82 "GetComplianceSummaryByResourceType",
83 "GetConformancePackComplianceDetails",
84 "GetConformancePackComplianceSummary",
85 "GetCustomRulePolicy",
86 "GetDiscoveredResourceCounts",
87 "GetOrganizationConfigRuleDetailedStatus",
88 "GetOrganizationConformancePackDetailedStatus",
89 "GetOrganizationCustomRulePolicy",
90 "GetResourceConfigHistory",
91 "GetResourceEvaluationSummary",
92 "GetStoredQuery",
93 "ListAggregateDiscoveredResources",
94 "ListConfigurationRecorders",
95 "ListConformancePackComplianceScores",
96 "ListDiscoveredResources",
97 "ListResourceEvaluations",
98 "ListStoredQueries",
99 "ListTagsForResource",
100 "PutAggregationAuthorization",
101 "PutConfigRule",
102 "PutConfigurationAggregator",
103 "PutConfigurationRecorder",
104 "PutConformancePack",
105 "PutDeliveryChannel",
106 "PutEvaluations",
107 "PutExternalEvaluation",
108 "PutOrganizationConfigRule",
109 "PutOrganizationConformancePack",
110 "PutRemediationConfigurations",
111 "PutRemediationExceptions",
112 "PutResourceConfig",
113 "PutRetentionConfiguration",
114 "PutServiceLinkedConfigurationRecorder",
115 "PutStoredQuery",
116 "SelectAggregateResourceConfig",
117 "SelectResourceConfig",
118 "StartConfigRulesEvaluation",
119 "StartConfigurationRecorder",
120 "StartRemediationExecution",
121 "StartResourceEvaluation",
122 "StopConfigurationRecorder",
123 "TagResource",
124 "UntagResource",
125];
126
127const MUTATING_ACTIONS: &[&str] = &[
129 "AssociateResourceTypes",
130 "DeleteAggregationAuthorization",
131 "DeleteConfigRule",
132 "DeleteConfigurationAggregator",
133 "DeleteConfigurationRecorder",
134 "DeleteConformancePack",
135 "DeleteDeliveryChannel",
136 "DeleteEvaluationResults",
137 "DeleteOrganizationConfigRule",
138 "DeleteOrganizationConformancePack",
139 "DeletePendingAggregationRequest",
140 "DeleteRemediationConfiguration",
141 "DeleteRemediationExceptions",
142 "DeleteResourceConfig",
143 "DeleteRetentionConfiguration",
144 "DeleteServiceLinkedConfigurationRecorder",
145 "DeleteStoredQuery",
146 "DisassociateResourceTypes",
147 "PutAggregationAuthorization",
148 "PutConfigRule",
149 "PutConfigurationAggregator",
150 "PutConfigurationRecorder",
151 "PutConformancePack",
152 "PutDeliveryChannel",
153 "PutEvaluations",
154 "PutExternalEvaluation",
155 "PutOrganizationConfigRule",
156 "PutOrganizationConformancePack",
157 "PutRemediationConfigurations",
158 "PutRemediationExceptions",
159 "PutResourceConfig",
160 "PutRetentionConfiguration",
161 "PutServiceLinkedConfigurationRecorder",
162 "PutStoredQuery",
163 "StartConfigRulesEvaluation",
164 "StartConfigurationRecorder",
165 "StartRemediationExecution",
166 "StartResourceEvaluation",
167 "StopConfigurationRecorder",
168 "TagResource",
169 "UntagResource",
170];
171
172pub struct ConfigService {
173 state: SharedConfigState,
174 snapshot_store: Option<Arc<dyn SnapshotStore>>,
175 snapshot_lock: Arc<AsyncMutex<()>>,
176 cross: CrossServiceStates,
177 lambda_state: Option<SharedLambdaState>,
178 container_runtime: Option<Arc<ContainerRuntime>>,
179 state_version: Arc<AtomicU64>,
183 last_eval_version: Arc<Mutex<HashMap<String, u64>>>,
189}
190
191impl ConfigService {
192 pub fn new(state: SharedConfigState) -> Self {
193 Self {
194 state,
195 snapshot_store: None,
196 snapshot_lock: Arc::new(AsyncMutex::new(())),
197 cross: CrossServiceStates::default(),
198 lambda_state: None,
199 container_runtime: None,
200 state_version: Arc::new(AtomicU64::new(1)),
201 last_eval_version: Arc::new(Mutex::new(HashMap::new())),
202 }
203 }
204
205 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
206 self.snapshot_store = Some(store);
207 self
208 }
209
210 pub fn with_cross_service(mut self, cross: CrossServiceStates) -> Self {
211 self.cross = cross;
212 self
213 }
214
215 pub fn with_lambda(
216 mut self,
217 lambda_state: SharedLambdaState,
218 runtime: Option<Arc<ContainerRuntime>>,
219 ) -> Self {
220 self.lambda_state = Some(lambda_state);
221 self.container_runtime = runtime;
222 self
223 }
224
225 pub fn shared_state(&self) -> SharedConfigState {
226 Arc::clone(&self.state)
227 }
228
229 async fn save_snapshot(&self) {
230 save_config_snapshot(
231 &self.state,
232 self.snapshot_store.clone(),
233 &self.snapshot_lock,
234 )
235 .await;
236 }
237
238 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
239 let store = self.snapshot_store.clone()?;
240 let state = self.state.clone();
241 let lock = self.snapshot_lock.clone();
242 Some(Arc::new(move || {
243 let state = state.clone();
244 let store = store.clone();
245 let lock = lock.clone();
246 Box::pin(async move {
247 save_config_snapshot(&state, Some(store), &lock).await;
248 })
249 }))
250 }
251
252 fn is_recording(&self, account_id: &str) -> bool {
254 let st = self.state.read();
255 st.account(account_id)
256 .map(|a| a.recorders.values().any(|r| r.recording))
257 .unwrap_or(false)
258 }
259
260 fn bump_version(&self) {
262 self.state_version.fetch_add(1, Ordering::Relaxed);
263 }
264
265 fn sync_recording(&self, account_id: &str, region: &str) {
270 if !self.is_recording(account_id) {
271 return;
272 }
273 let discovered = validate::discover_all(&self.cross, account_id, region);
275 let needs = {
277 let st = self.state.read();
278 match st.account(account_id) {
279 Some(acc) => validate::sync_would_change(acc, &discovered),
280 None => !discovered.is_empty(),
281 }
282 };
283 if !needs {
284 return;
285 }
286 {
287 let mut st = self.state.write();
288 let account = st.account_mut(account_id);
289 validate::apply_recorded_items(account, discovered, account_id);
290 }
291 self.bump_version();
292 }
293
294 fn ensure_evaluated(&self, account_id: &str) {
299 let version = self.state_version.load(Ordering::Relaxed);
300 if self.last_eval_version.lock().get(account_id).copied() == Some(version) {
301 return;
302 }
303 let mut st = self.state.write();
304 let account = st.account_mut(account_id);
305 let rules: Vec<(String, String, Value, Value)> = account
306 .rules
307 .values()
308 .filter_map(|r| {
309 let owner = r.source.get("Owner").and_then(Value::as_str).unwrap_or("");
310 if owner != "AWS" {
311 return None;
312 }
313 let sid = r
314 .source
315 .get("SourceIdentifier")
316 .and_then(Value::as_str)?
317 .to_string();
318 let params: Value = r
319 .input_parameters
320 .as_deref()
321 .and_then(|p| serde_json::from_str(p).ok())
322 .unwrap_or(Value::Null);
323 let scope = r.scope.clone().unwrap_or(Value::Null);
324 Some((r.name.clone(), sid, params, scope))
325 })
326 .collect();
327 for (rule_name, sid, params, scope) in rules {
328 let outcomes = validate::evaluate_managed_rule(&sid, ¶ms, &scope, account);
329 let entry = account.evaluations.entry(rule_name.clone()).or_default();
330 entry.clear();
331 for o in &outcomes {
332 let key = resource_key(&o.resource_type, &o.resource_id);
333 entry.insert(key, validate::outcome_to_result(&rule_name, o));
334 }
335 if let Some(rule) = account.rules.get_mut(&rule_name) {
336 let now = Utc::now();
337 rule.last_successful_evaluation_time = Some(now);
338 rule.last_successful_invocation_time = Some(now);
339 if rule.first_activated_time.is_none() {
340 rule.first_activated_time = Some(now);
341 }
342 }
343 }
344 drop(st);
345 self.last_eval_version
346 .lock()
347 .insert(account_id.to_string(), version);
348 }
349}
350
351impl Default for ConfigService {
352 fn default() -> Self {
353 Self::new(Arc::new(parking_lot::RwLock::new(ConfigAccounts::new())))
354 }
355}
356
357#[async_trait]
358impl AwsService for ConfigService {
359 fn service_name(&self) -> &str {
360 "config"
361 }
362
363 fn supported_actions(&self) -> &[&str] {
364 SUPPORTED_ACTIONS
365 }
366
367 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
368 let account = account_id(&req);
369 let region = region(&req);
370 let body = req.json_body();
371 let mutates = MUTATING_ACTIONS.contains(&req.action.as_str());
372
373 if let Err(msg) = crate::model_validate::validate_input(&req.action, &body) {
376 return Err(AwsServiceError::aws_error(
377 StatusCode::BAD_REQUEST,
378 "ValidationException",
379 msg,
380 ));
381 }
382
383 const RECORDING_READS: &[&str] = &[
385 "GetResourceConfigHistory",
386 "BatchGetResourceConfig",
387 "BatchGetAggregateResourceConfig",
388 "ListDiscoveredResources",
389 "ListAggregateDiscoveredResources",
390 "GetDiscoveredResourceCounts",
391 "GetAggregateDiscoveredResourceCounts",
392 "GetResourceConfig",
393 "GetAggregateResourceConfig",
394 "SelectResourceConfig",
395 "SelectAggregateResourceConfig",
396 "StartConfigRulesEvaluation",
397 "DescribeComplianceByConfigRule",
398 "DescribeComplianceByResource",
399 "GetComplianceDetailsByConfigRule",
400 "GetComplianceDetailsByResource",
401 "GetComplianceSummaryByConfigRule",
402 "GetComplianceSummaryByResourceType",
403 "DescribeConfigRuleEvaluationStatus",
404 ];
405 if RECORDING_READS.contains(&req.action.as_str()) {
406 self.sync_recording(&account, ®ion);
407 }
408
409 let result = match req.action.as_str() {
410 "PutConfigurationRecorder" => self.put_configuration_recorder(&account, ®ion, &body),
412 "DescribeConfigurationRecorders" => {
413 self.describe_configuration_recorders(&account, &body)
414 }
415 "DescribeConfigurationRecorderStatus" => {
416 self.describe_configuration_recorder_status(&account, &body)
417 }
418 "DeleteConfigurationRecorder" => self.delete_configuration_recorder(&account, &body),
419 "StartConfigurationRecorder" => self.set_recording(&account, &body, true),
420 "StopConfigurationRecorder" => self.set_recording(&account, &body, false),
421 "ListConfigurationRecorders" => self.list_configuration_recorders(&account, &body),
422 "PutServiceLinkedConfigurationRecorder" => {
423 self.put_service_linked_recorder(&account, ®ion, &body)
424 }
425 "DeleteServiceLinkedConfigurationRecorder" => {
426 self.delete_service_linked_recorder(&account, &body)
427 }
428 "AssociateResourceTypes" => self.associate_resource_types(&account, &body, true),
429 "DisassociateResourceTypes" => self.associate_resource_types(&account, &body, false),
430 "PutDeliveryChannel" => self.put_delivery_channel(&account, &body),
432 "DescribeDeliveryChannels" => self.describe_delivery_channels(&account, &body),
433 "DescribeDeliveryChannelStatus" => {
434 self.describe_delivery_channel_status(&account, &body)
435 }
436 "DeleteDeliveryChannel" => self.delete_delivery_channel(&account, &body),
437 "DeliverConfigSnapshot" => self.deliver_config_snapshot(&account, &body),
438 "PutResourceConfig" => self.put_resource_config(&account, ®ion, &body),
440 "DeleteResourceConfig" => self.delete_resource_config(&account, &body),
441 "GetResourceConfigHistory" => self.get_resource_config_history(&account, &body),
442 "BatchGetResourceConfig" => self.batch_get_resource_config(&account, &body),
443 "ListDiscoveredResources" => self.list_discovered_resources(&account, &body),
444 "GetDiscoveredResourceCounts" => self.get_discovered_resource_counts(&account, &body),
445 "PutConfigRule" => self.put_config_rule(&account, ®ion, &body),
447 "DescribeConfigRules" => self.describe_config_rules(&account, &body),
448 "DeleteConfigRule" => self.delete_config_rule(&account, &body),
449 "DescribeConfigRuleEvaluationStatus" => {
450 self.ensure_evaluated(&account);
451 self.describe_config_rule_evaluation_status(&account, &body)
452 }
453 "StartConfigRulesEvaluation" => {
454 self.start_config_rules_evaluation(&account, ®ion, &body)
455 .await
456 }
457 "PutEvaluations" => self.put_evaluations(&account, &body),
458 "PutExternalEvaluation" => self.put_external_evaluation(&account, &body),
459 "DeleteEvaluationResults" => self.delete_evaluation_results(&account, &body),
460 "GetCustomRulePolicy" => self.get_custom_rule_policy(&account, &body),
461 "DescribeComplianceByConfigRule" => {
463 self.ensure_evaluated(&account);
464 self.describe_compliance_by_config_rule(&account, &body)
465 }
466 "DescribeComplianceByResource" => {
467 self.ensure_evaluated(&account);
468 self.describe_compliance_by_resource(&account, &body)
469 }
470 "GetComplianceDetailsByConfigRule" => {
471 self.ensure_evaluated(&account);
472 self.get_compliance_details_by_config_rule(&account, &body)
473 }
474 "GetComplianceDetailsByResource" => {
475 self.ensure_evaluated(&account);
476 self.get_compliance_details_by_resource(&account, &body)
477 }
478 "GetComplianceSummaryByConfigRule" => {
479 self.ensure_evaluated(&account);
480 self.get_compliance_summary_by_config_rule(&account)
481 }
482 "GetComplianceSummaryByResourceType" => {
483 self.ensure_evaluated(&account);
484 self.get_compliance_summary_by_resource_type(&account, &body)
485 }
486 "PutRemediationConfigurations" => {
488 self.put_remediation_configurations(&account, ®ion, &body)
489 }
490 "DescribeRemediationConfigurations" => {
491 self.describe_remediation_configurations(&account, &body)
492 }
493 "DeleteRemediationConfiguration" => {
494 self.delete_remediation_configuration(&account, &body)
495 }
496 "PutRemediationExceptions" => self.put_remediation_exceptions(&account, &body),
497 "DescribeRemediationExceptions" => {
498 self.describe_remediation_exceptions(&account, &body)
499 }
500 "DeleteRemediationExceptions" => self.delete_remediation_exceptions(&account, &body),
501 "StartRemediationExecution" => self.start_remediation_execution(&account, &body),
502 "DescribeRemediationExecutionStatus" => {
503 self.describe_remediation_execution_status(&account, &body)
504 }
505 "PutConformancePack" => self.put_conformance_pack(&account, ®ion, &body),
507 "DescribeConformancePacks" => self.describe_conformance_packs(&account, &body),
508 "DeleteConformancePack" => self.delete_conformance_pack(&account, &body),
509 "DescribeConformancePackStatus" => {
510 self.describe_conformance_pack_status(&account, &body)
511 }
512 "DescribeConformancePackCompliance" => {
513 self.ensure_evaluated(&account);
514 self.describe_conformance_pack_compliance(&account, &body)
515 }
516 "GetConformancePackComplianceSummary" => {
517 self.ensure_evaluated(&account);
518 self.get_conformance_pack_compliance_summary(&account, &body)
519 }
520 "GetConformancePackComplianceDetails" => {
521 self.ensure_evaluated(&account);
522 self.get_conformance_pack_compliance_details(&account, &body)
523 }
524 "ListConformancePackComplianceScores" => {
525 self.ensure_evaluated(&account);
526 self.list_conformance_pack_compliance_scores(&account)
527 }
528 "PutOrganizationConfigRule" => {
530 self.put_organization_config_rule(&account, ®ion, &body)
531 }
532 "DescribeOrganizationConfigRules" => {
533 self.describe_organization_config_rules(&account, &body)
534 }
535 "DeleteOrganizationConfigRule" => self.delete_organization_config_rule(&account, &body),
536 "DescribeOrganizationConfigRuleStatuses" => {
537 self.describe_organization_config_rule_statuses(&account, &body)
538 }
539 "GetOrganizationConfigRuleDetailedStatus" => {
540 self.get_organization_config_rule_detailed_status(&account, &body)
541 }
542 "GetOrganizationCustomRulePolicy" => {
543 self.get_organization_custom_rule_policy(&account, &body)
544 }
545 "PutOrganizationConformancePack" => {
546 self.put_organization_conformance_pack(&account, ®ion, &body)
547 }
548 "DescribeOrganizationConformancePacks" => {
549 self.describe_organization_conformance_packs(&account, &body)
550 }
551 "DeleteOrganizationConformancePack" => {
552 self.delete_organization_conformance_pack(&account, &body)
553 }
554 "DescribeOrganizationConformancePackStatuses" => {
555 self.describe_organization_conformance_pack_statuses(&account, &body)
556 }
557 "GetOrganizationConformancePackDetailedStatus" => {
558 self.get_organization_conformance_pack_detailed_status(&account, &body)
559 }
560 "PutConfigurationAggregator" => {
562 self.put_configuration_aggregator(&account, ®ion, &body)
563 }
564 "DescribeConfigurationAggregators" => {
565 self.describe_configuration_aggregators(&account, &body)
566 }
567 "DeleteConfigurationAggregator" => {
568 self.delete_configuration_aggregator(&account, &body)
569 }
570 "DescribeConfigurationAggregatorSourcesStatus" => {
571 self.describe_configuration_aggregator_sources_status(&account, &body)
572 }
573 "PutAggregationAuthorization" => {
574 self.put_aggregation_authorization(&account, ®ion, &body)
575 }
576 "DescribeAggregationAuthorizations" => {
577 self.describe_aggregation_authorizations(&account, &body)
578 }
579 "DeleteAggregationAuthorization" => {
580 self.delete_aggregation_authorization(&account, &body)
581 }
582 "DeletePendingAggregationRequest" => Ok(AwsResponse::ok_json(json!({}))),
583 "DescribePendingAggregationRequests" => Ok(AwsResponse::ok_json(
584 json!({ "PendingAggregationRequests": [] }),
585 )),
586 "BatchGetAggregateResourceConfig" => {
587 self.batch_get_aggregate_resource_config(&account, &body)
588 }
589 "GetAggregateResourceConfig" => self.get_aggregate_resource_config(&account, &body),
590 "ListAggregateDiscoveredResources" => {
591 self.list_aggregate_discovered_resources(&account, &body)
592 }
593 "GetAggregateDiscoveredResourceCounts" => {
594 self.get_aggregate_discovered_resource_counts(&account, &body)
595 }
596 "DescribeAggregateComplianceByConfigRules" => {
597 self.ensure_evaluated(&account);
598 self.describe_aggregate_compliance_by_config_rules(&account, &body)
599 }
600 "DescribeAggregateComplianceByConformancePacks" => {
601 self.describe_aggregate_compliance_by_conformance_packs(&account, &body)
602 }
603 "GetAggregateComplianceDetailsByConfigRule" => {
604 self.ensure_evaluated(&account);
605 self.get_aggregate_compliance_details_by_config_rule(&account, &body)
606 }
607 "GetAggregateConfigRuleComplianceSummary" => {
608 self.ensure_evaluated(&account);
609 self.get_aggregate_config_rule_compliance_summary(&account, &body)
610 }
611 "GetAggregateConformancePackComplianceSummary" => {
612 self.get_aggregate_conformance_pack_compliance_summary(&account, &body)
613 }
614 "SelectAggregateResourceConfig" => self.select_resource_config(&account, &body, true),
615 "PutRetentionConfiguration" => self.put_retention_configuration(&account, &body),
617 "DescribeRetentionConfigurations" => {
618 self.describe_retention_configurations(&account, &body)
619 }
620 "DeleteRetentionConfiguration" => self.delete_retention_configuration(&account, &body),
621 "PutStoredQuery" => self.put_stored_query(&account, ®ion, &body),
623 "GetStoredQuery" => self.get_stored_query(&account, &body),
624 "DeleteStoredQuery" => self.delete_stored_query(&account, &body),
625 "ListStoredQueries" => self.list_stored_queries(&account, &body),
626 "StartResourceEvaluation" => self.start_resource_evaluation(&account, &body),
628 "GetResourceEvaluationSummary" => self.get_resource_evaluation_summary(&account, &body),
629 "ListResourceEvaluations" => self.list_resource_evaluations(&account, &body),
630 "SelectResourceConfig" => self.select_resource_config(&account, &body, false),
632 "TagResource" => self.tag_resource(&account, &body),
634 "UntagResource" => self.untag_resource(&account, &body),
635 "ListTagsForResource" => self.list_tags_for_resource(&account, &body),
636 other => Err(AwsServiceError::action_not_implemented("config", other)),
637 };
638
639 if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
640 self.bump_version();
643 self.save_snapshot().await;
644 }
645 result
646 }
647}
648
649impl ConfigService {
652 fn put_configuration_recorder(
653 &self,
654 account: &str,
655 region: &str,
656 body: &Value,
657 ) -> Result<AwsResponse, AwsServiceError> {
658 let rec = body
659 .get("ConfigurationRecorder")
660 .filter(|v| v.is_object())
661 .ok_or_else(|| invalid("ConfigurationRecorder is required"))?;
662 let name = rec
663 .get("name")
664 .and_then(Value::as_str)
665 .unwrap_or("default")
666 .to_string();
667 let role_arn = rec
668 .get("roleARN")
669 .and_then(Value::as_str)
670 .unwrap_or("")
671 .to_string();
672 let mut st = self.state.write();
673 let acc = st.account_mut(account);
674 let existing = acc.recorders.get(&name);
675 let recorder = ConfigurationRecorder {
676 name: name.clone(),
677 role_arn,
678 recording_group: rec.get("recordingGroup").cloned(),
679 recording_mode: rec.get("recordingMode").cloned(),
680 arn: existing.and_then(|e| e.arn.clone()),
681 service_principal: existing.and_then(|e| e.service_principal.clone()),
682 recording: existing.map(|e| e.recording).unwrap_or(false),
683 last_start_time: existing.and_then(|e| e.last_start_time),
684 last_stop_time: existing.and_then(|e| e.last_stop_time),
685 last_status: existing
686 .map(|e| e.last_status.clone())
687 .unwrap_or_else(|| "PENDING".into()),
688 last_status_change_time: Some(Utc::now()),
689 };
690 let _ = region;
691 acc.recorders.insert(name, recorder);
692 Ok(AwsResponse::ok_json(json!({})))
693 }
694
695 fn recorder_json(r: &ConfigurationRecorder) -> Value {
696 let mut v = json!({
697 "name": r.name,
698 "roleARN": r.role_arn,
699 });
700 if let Some(g) = &r.recording_group {
701 v["recordingGroup"] = g.clone();
702 }
703 if let Some(m) = &r.recording_mode {
704 v["recordingMode"] = m.clone();
705 }
706 if let Some(a) = &r.arn {
707 v["arn"] = json!(a);
708 }
709 if let Some(sp) = &r.service_principal {
710 v["servicePrincipal"] = json!(sp);
711 }
712 v
713 }
714
715 fn describe_configuration_recorders(
716 &self,
717 account: &str,
718 body: &Value,
719 ) -> Result<AwsResponse, AwsServiceError> {
720 let names = string_list(body, "ConfigurationRecorderNames");
721 let st = self.state.read();
722 let list: Vec<Value> = st
723 .account(account)
724 .map(|a| {
725 a.recorders
726 .values()
727 .filter(|r| names.is_empty() || names.contains(&r.name))
728 .map(Self::recorder_json)
729 .collect()
730 })
731 .unwrap_or_default();
732 if !names.is_empty() {
733 for n in &names {
734 let found = st
735 .account(account)
736 .map(|a| a.recorders.contains_key(n))
737 .unwrap_or(false);
738 if !found {
739 return Err(no_such(
740 "NoSuchConfigurationRecorderException",
741 format!(
742 "Cannot find configuration recorder with the specified name '{n}'."
743 ),
744 ));
745 }
746 }
747 }
748 Ok(AwsResponse::ok_json(
749 json!({ "ConfigurationRecorders": list }),
750 ))
751 }
752
753 fn describe_configuration_recorder_status(
754 &self,
755 account: &str,
756 body: &Value,
757 ) -> Result<AwsResponse, AwsServiceError> {
758 let names = string_list(body, "ConfigurationRecorderNames");
759 let st = self.state.read();
760 let list: Vec<Value> = st
761 .account(account)
762 .map(|a| {
763 a.recorders
764 .values()
765 .filter(|r| names.is_empty() || names.contains(&r.name))
766 .map(|r| {
767 json!({
768 "name": r.name,
769 "lastStartTime": r.last_start_time.map(|t| t.timestamp() as f64),
770 "lastStopTime": r.last_stop_time.map(|t| t.timestamp() as f64),
771 "recording": r.recording,
772 "lastStatus": r.last_status,
773 "lastStatusChangeTime": r.last_status_change_time.map(|t| t.timestamp() as f64),
774 })
775 })
776 .collect()
777 })
778 .unwrap_or_default();
779 Ok(AwsResponse::ok_json(
780 json!({ "ConfigurationRecordersStatus": list }),
781 ))
782 }
783
784 fn delete_configuration_recorder(
785 &self,
786 account: &str,
787 body: &Value,
788 ) -> Result<AwsResponse, AwsServiceError> {
789 let name = require_str(body, "ConfigurationRecorderName")?;
790 let mut st = self.state.write();
791 let acc = st.account_mut(account);
792 if acc.recorders.remove(&name).is_none() {
793 return Err(no_such(
794 "NoSuchConfigurationRecorderException",
795 format!("Cannot find configuration recorder with the specified name '{name}'."),
796 ));
797 }
798 Ok(AwsResponse::ok_json(json!({})))
799 }
800
801 fn set_recording(
802 &self,
803 account: &str,
804 body: &Value,
805 recording: bool,
806 ) -> Result<AwsResponse, AwsServiceError> {
807 let name = require_str(body, "ConfigurationRecorderName")?;
808 let mut st = self.state.write();
809 let acc = st.account_mut(account);
810 let rec = acc.recorders.get_mut(&name).ok_or_else(|| {
811 no_such(
812 "NoSuchConfigurationRecorderException",
813 format!("Cannot find configuration recorder with the specified name '{name}'."),
814 )
815 })?;
816 rec.recording = recording;
817 let now = Utc::now();
818 if recording {
819 rec.last_start_time = Some(now);
820 rec.last_status = "SUCCESS".into();
821 } else {
822 rec.last_stop_time = Some(now);
823 rec.last_status = "PENDING".into();
824 }
825 rec.last_status_change_time = Some(now);
826 Ok(AwsResponse::ok_json(json!({})))
827 }
828
829 fn list_configuration_recorders(
830 &self,
831 account: &str,
832 body: &Value,
833 ) -> Result<AwsResponse, AwsServiceError> {
834 let st = self.state.read();
835 let list: Vec<Value> = st
836 .account(account)
837 .map(|a| {
838 a.recorders
839 .values()
840 .map(|r| {
841 let mut v = json!({ "name": r.name, "recordingScope": "INTERNAL" });
842 if let Some(arn) = &r.arn {
843 v["arn"] = json!(arn);
844 }
845 if let Some(sp) = &r.service_principal {
846 v["servicePrincipal"] = json!(sp);
847 }
848 v
849 })
850 .collect()
851 })
852 .unwrap_or_default();
853 Ok(paged_response(
854 "ConfigurationRecorderSummaries",
855 list,
856 body,
857 "NextToken",
858 ))
859 }
860
861 fn put_service_linked_recorder(
862 &self,
863 account: &str,
864 region: &str,
865 body: &Value,
866 ) -> Result<AwsResponse, AwsServiceError> {
867 let sp = body
868 .get("ServicePrincipal")
869 .and_then(Value::as_str)
870 .unwrap_or("config-conforms.amazonaws.com")
871 .to_string();
872 let name = format!(
873 "AWSConfigurationRecorderForServiceLinkedConfigurationRecorder-{}",
874 &Uuid::new_v4().to_string()[..8]
875 );
876 let arn = format!("arn:aws:config:{region}:{account}:configuration-recorder/{name}");
877 let mut st = self.state.write();
878 let acc = st.account_mut(account);
879 acc.recorders.insert(
880 name.clone(),
881 ConfigurationRecorder {
882 name: name.clone(),
883 role_arn: format!("arn:aws:iam::{account}:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig"),
884 recording_group: None,
885 recording_mode: None,
886 arn: Some(arn.clone()),
887 service_principal: Some(sp),
888 recording: true,
889 last_start_time: Some(Utc::now()),
890 last_stop_time: None,
891 last_status: "SUCCESS".into(),
892 last_status_change_time: Some(Utc::now()),
893 },
894 );
895 Ok(AwsResponse::ok_json(json!({ "Arn": arn, "Name": name })))
896 }
897
898 fn delete_service_linked_recorder(
899 &self,
900 account: &str,
901 body: &Value,
902 ) -> Result<AwsResponse, AwsServiceError> {
903 let sp = body
904 .get("ServicePrincipal")
905 .and_then(Value::as_str)
906 .unwrap_or_default()
907 .to_string();
908 let mut st = self.state.write();
909 let acc = st.account_mut(account);
910 let name = acc
911 .recorders
912 .values()
913 .find(|r| r.service_principal.as_deref() == Some(sp.as_str()))
914 .map(|r| r.name.clone());
915 let name = name.ok_or_else(|| {
916 no_such(
917 "NoSuchConfigurationRecorderException",
918 "No service-linked configuration recorder for that principal.",
919 )
920 })?;
921 let arn = acc
922 .recorders
923 .get(&name)
924 .and_then(|r| r.arn.clone())
925 .unwrap_or_default();
926 acc.recorders.remove(&name);
927 Ok(AwsResponse::ok_json(json!({ "Arn": arn, "Name": name })))
928 }
929
930 fn associate_resource_types(
931 &self,
932 account: &str,
933 body: &Value,
934 associate: bool,
935 ) -> Result<AwsResponse, AwsServiceError> {
936 let name = require_str(body, "ConfigurationRecorderArn")
937 .or_else(|_| require_str(body, "ConfigurationRecorderName"))?;
938 let types = string_list(body, "ResourceTypes");
939 let mut st = self.state.write();
940 let acc = st.account_mut(account);
941 let rec = acc
943 .recorders
944 .values_mut()
945 .find(|r| r.arn.as_deref() == Some(name.as_str()) || r.name == name)
946 .ok_or_else(|| {
947 no_such(
948 "NoSuchConfigurationRecorderException",
949 "Cannot find the specified configuration recorder.",
950 )
951 })?;
952 let mut group = rec.recording_group.clone().unwrap_or_else(|| json!({}));
953 let current = group
954 .get("resourceTypes")
955 .and_then(Value::as_array)
956 .cloned()
957 .unwrap_or_default();
958 let mut set: Vec<String> = current
959 .iter()
960 .filter_map(|v| v.as_str().map(String::from))
961 .collect();
962 if associate {
963 for t in types {
964 if !set.contains(&t) {
965 set.push(t);
966 }
967 }
968 } else {
969 set.retain(|t| !types.contains(t));
970 }
971 group["allSupported"] = json!(false);
972 group["resourceTypes"] = json!(set);
973 rec.recording_group = Some(group);
974 let out = Self::recorder_json(rec);
975 Ok(AwsResponse::ok_json(
976 json!({ "ConfigurationRecorder": out }),
977 ))
978 }
979}
980
981impl ConfigService {
984 fn put_delivery_channel(
985 &self,
986 account: &str,
987 body: &Value,
988 ) -> Result<AwsResponse, AwsServiceError> {
989 let ch = body
990 .get("DeliveryChannel")
991 .filter(|v| v.is_object())
992 .ok_or_else(|| invalid("DeliveryChannel is required"))?;
993 let name = ch
994 .get("name")
995 .and_then(Value::as_str)
996 .unwrap_or("default")
997 .to_string();
998 let bucket = ch
999 .get("s3BucketName")
1000 .and_then(Value::as_str)
1001 .map(String::from);
1002 match &bucket {
1003 None => {
1004 return Err(no_such(
1005 "NoSuchBucketException",
1006 "Cannot find a S3 bucket with an empty bucket name.",
1007 ));
1008 }
1009 Some(name) => {
1010 if validate::s3_bucket_exists(&self.cross, name) == Some(false) {
1014 return Err(no_such(
1015 "NoSuchBucketException",
1016 format!("Cannot find a S3 bucket with the bucket name '{name}'."),
1017 ));
1018 }
1019 }
1020 }
1021 let channel = DeliveryChannel {
1022 name: name.clone(),
1023 s3_bucket_name: bucket,
1024 s3_key_prefix: ch
1025 .get("s3KeyPrefix")
1026 .and_then(Value::as_str)
1027 .map(String::from),
1028 s3_kms_key_arn: ch
1029 .get("s3KmsKeyArn")
1030 .and_then(Value::as_str)
1031 .map(String::from),
1032 sns_topic_arn: ch
1033 .get("snsTopicARN")
1034 .and_then(Value::as_str)
1035 .map(String::from),
1036 config_snapshot_delivery_properties: ch
1037 .get("configSnapshotDeliveryProperties")
1038 .cloned(),
1039 last_status: "SUCCESS".into(),
1040 };
1041 let mut st = self.state.write();
1042 st.account_mut(account)
1043 .delivery_channels
1044 .insert(name, channel);
1045 Ok(AwsResponse::ok_json(json!({})))
1046 }
1047
1048 fn channel_json(c: &DeliveryChannel) -> Value {
1049 let mut v = json!({ "name": c.name });
1050 if let Some(b) = &c.s3_bucket_name {
1051 v["s3BucketName"] = json!(b);
1052 }
1053 if let Some(p) = &c.s3_key_prefix {
1054 v["s3KeyPrefix"] = json!(p);
1055 }
1056 if let Some(k) = &c.s3_kms_key_arn {
1057 v["s3KmsKeyArn"] = json!(k);
1058 }
1059 if let Some(s) = &c.sns_topic_arn {
1060 v["snsTopicARN"] = json!(s);
1061 }
1062 if let Some(p) = &c.config_snapshot_delivery_properties {
1063 v["configSnapshotDeliveryProperties"] = p.clone();
1064 }
1065 v
1066 }
1067
1068 fn describe_delivery_channels(
1069 &self,
1070 account: &str,
1071 body: &Value,
1072 ) -> Result<AwsResponse, AwsServiceError> {
1073 let names = string_list(body, "DeliveryChannelNames");
1074 let st = self.state.read();
1075 let list: Vec<Value> = st
1076 .account(account)
1077 .map(|a| {
1078 a.delivery_channels
1079 .values()
1080 .filter(|c| names.is_empty() || names.contains(&c.name))
1081 .map(Self::channel_json)
1082 .collect()
1083 })
1084 .unwrap_or_default();
1085 Ok(AwsResponse::ok_json(json!({ "DeliveryChannels": list })))
1086 }
1087
1088 fn describe_delivery_channel_status(
1089 &self,
1090 account: &str,
1091 body: &Value,
1092 ) -> Result<AwsResponse, AwsServiceError> {
1093 let names = string_list(body, "DeliveryChannelNames");
1094 let now = Utc::now().timestamp() as f64;
1095 let st = self.state.read();
1096 let list: Vec<Value> = st
1097 .account(account)
1098 .map(|a| {
1099 a.delivery_channels
1100 .values()
1101 .filter(|c| names.is_empty() || names.contains(&c.name))
1102 .map(|c| {
1103 json!({
1104 "name": c.name,
1105 "configSnapshotDeliveryInfo": { "lastStatus": "SUCCESS", "lastSuccessfulTime": now },
1106 "configHistoryDeliveryInfo": { "lastStatus": "SUCCESS", "lastSuccessfulTime": now },
1107 "configStreamDeliveryInfo": { "lastStatus": "SUCCESS", "lastStatusChangeTime": now },
1108 })
1109 })
1110 .collect()
1111 })
1112 .unwrap_or_default();
1113 Ok(AwsResponse::ok_json(
1114 json!({ "DeliveryChannelsStatus": list }),
1115 ))
1116 }
1117
1118 fn delete_delivery_channel(
1119 &self,
1120 account: &str,
1121 body: &Value,
1122 ) -> Result<AwsResponse, AwsServiceError> {
1123 let name = require_str(body, "DeliveryChannelName")?;
1124 let mut st = self.state.write();
1125 let acc = st.account_mut(account);
1126 if acc.delivery_channels.remove(&name).is_none() {
1127 return Err(no_such(
1128 "NoSuchDeliveryChannelException",
1129 format!("Cannot find delivery channel with specified name '{name}'."),
1130 ));
1131 }
1132 Ok(AwsResponse::ok_json(json!({})))
1133 }
1134
1135 fn deliver_config_snapshot(
1136 &self,
1137 account: &str,
1138 body: &Value,
1139 ) -> Result<AwsResponse, AwsServiceError> {
1140 let name = require_str(body, "deliveryChannelName")?;
1141 let st = self.state.read();
1142 let exists = st
1143 .account(account)
1144 .map(|a| a.delivery_channels.contains_key(&name))
1145 .unwrap_or(false);
1146 if !exists {
1147 return Err(no_such(
1148 "NoSuchDeliveryChannelException",
1149 format!("Cannot find delivery channel with specified name '{name}'."),
1150 ));
1151 }
1152 Ok(AwsResponse::ok_json(
1153 json!({ "configSnapshotId": Uuid::new_v4().to_string() }),
1154 ))
1155 }
1156}
1157
1158impl ConfigService {
1161 fn config_item_json(ci: &ConfigurationItem) -> Value {
1162 json!({
1163 "version": ci.version,
1164 "accountId": ci.account_id,
1165 "configurationItemCaptureTime": ci.configuration_item_capture_time.timestamp() as f64,
1166 "configurationItemStatus": ci.configuration_item_status,
1167 "configurationStateId": ci.configuration_state_id,
1168 "arn": ci.arn,
1169 "resourceType": ci.resource_type,
1170 "resourceId": ci.resource_id,
1171 "resourceName": ci.resource_name,
1172 "awsRegion": ci.aws_region,
1173 "availabilityZone": ci.availability_zone,
1174 "resourceCreationTime": ci.resource_creation_time.map(|t| t.timestamp() as f64),
1175 "tags": ci.tags,
1176 "relatedEvents": [],
1177 "relationships": [],
1178 "configuration": ci.configuration,
1179 "supplementaryConfiguration": ci.supplementary_configuration,
1180 })
1181 }
1182
1183 fn base_config_item_json(ci: &ConfigurationItem) -> Value {
1184 json!({
1185 "version": ci.version,
1186 "accountId": ci.account_id,
1187 "configurationItemCaptureTime": ci.configuration_item_capture_time.timestamp() as f64,
1188 "configurationItemStatus": ci.configuration_item_status,
1189 "configurationStateId": ci.configuration_state_id,
1190 "arn": ci.arn,
1191 "resourceType": ci.resource_type,
1192 "resourceId": ci.resource_id,
1193 "resourceName": ci.resource_name,
1194 "awsRegion": ci.aws_region,
1195 "availabilityZone": ci.availability_zone,
1196 "resourceCreationTime": ci.resource_creation_time.map(|t| t.timestamp() as f64),
1197 "configuration": ci.configuration,
1198 "supplementaryConfiguration": ci.supplementary_configuration,
1199 })
1200 }
1201
1202 fn put_resource_config(
1203 &self,
1204 account: &str,
1205 region: &str,
1206 body: &Value,
1207 ) -> Result<AwsResponse, AwsServiceError> {
1208 let resource_type = require_str(body, "ResourceType")?;
1209 let resource_id = require_str(body, "ResourceId")?;
1210 require_str(body, "SchemaVersionId")?;
1211 let configuration = require_str(body, "Configuration")?;
1212 let resource_name = body
1213 .get("ResourceName")
1214 .and_then(Value::as_str)
1215 .map(String::from);
1216 let tags: std::collections::BTreeMap<String, String> = body
1217 .get("Tags")
1218 .and_then(Value::as_object)
1219 .map(|m| {
1220 m.iter()
1221 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
1222 .collect()
1223 })
1224 .unwrap_or_default();
1225 let now = Utc::now();
1226 let mut st = self.state.write();
1227 let acc = st.account_mut(account);
1228 let key = resource_key(&resource_type, &resource_id);
1229 let history = acc.config_items.entry(key).or_default();
1230 let state_id = (history.len() as u64 + 1).to_string();
1231 history.push(ConfigurationItem {
1232 version: "1.3".into(),
1233 account_id: account.to_string(),
1234 configuration_item_capture_time: now,
1235 configuration_item_status: if history.is_empty() {
1236 "ResourceDiscovered".into()
1237 } else {
1238 "OK".into()
1239 },
1240 configuration_state_id: state_id,
1241 arn: format!(
1242 "arn:aws:config:{region}:{account}:resource/{resource_type}/{resource_id}"
1243 ),
1244 resource_type,
1245 resource_id,
1246 resource_name,
1247 aws_region: region.to_string(),
1248 availability_zone: "Not Applicable".into(),
1249 resource_creation_time: Some(now),
1250 tags,
1251 configuration,
1252 supplementary_configuration: Default::default(),
1253 externally_recorded: true,
1254 });
1255 Ok(AwsResponse::ok_json(json!({})))
1256 }
1257
1258 fn delete_resource_config(
1259 &self,
1260 account: &str,
1261 body: &Value,
1262 ) -> Result<AwsResponse, AwsServiceError> {
1263 let resource_type = require_str(body, "ResourceType")?;
1264 let resource_id = require_str(body, "ResourceId")?;
1265 let mut st = self.state.write();
1266 let acc = st.account_mut(account);
1267 let key = resource_key(&resource_type, &resource_id);
1268 if let Some(history) = acc.config_items.get_mut(&key) {
1269 if let Some(last) = history.last().cloned() {
1270 let state_id = (history.len() as u64 + 1).to_string();
1271 history.push(ConfigurationItem {
1272 configuration_item_capture_time: Utc::now(),
1273 configuration_item_status: "ResourceDeleted".into(),
1274 configuration_state_id: state_id,
1275 ..last
1276 });
1277 }
1278 }
1279 Ok(AwsResponse::ok_json(json!({})))
1280 }
1281
1282 fn get_resource_config_history(
1283 &self,
1284 account: &str,
1285 body: &Value,
1286 ) -> Result<AwsResponse, AwsServiceError> {
1287 let resource_type = require_str(body, "resourceType")?;
1288 let resource_id = require_str(body, "resourceId")?;
1289 let later = body.get("laterTime").and_then(Value::as_f64);
1292 let earlier = body.get("earlierTime").and_then(Value::as_f64);
1293 let forward = body
1295 .get("chronologicalOrder")
1296 .and_then(Value::as_str)
1297 .map(|o| o.eq_ignore_ascii_case("Forward"))
1298 .unwrap_or(false);
1299 let st = self.state.read();
1300 let key = resource_key(&resource_type, &resource_id);
1301 let history = st.account(account).and_then(|a| a.config_items.get(&key));
1302 let Some(history) = history else {
1303 return Err(no_such(
1304 "ResourceNotDiscoveredException",
1305 format!("Resource {resource_id} of type {resource_type} is not discovered."),
1306 ));
1307 };
1308 let mut selected: Vec<&ConfigurationItem> = history
1310 .iter()
1311 .filter(|ci| {
1312 let ts = ci.configuration_item_capture_time.timestamp() as f64;
1313 later.map(|l| ts <= l).unwrap_or(true) && earlier.map(|e| ts >= e).unwrap_or(true)
1314 })
1315 .collect();
1316 if !forward {
1317 selected.reverse();
1318 }
1319 let items: Vec<Value> = selected.into_iter().map(Self::config_item_json).collect();
1320 let (page, next) = paginate(items, body);
1321 let mut out = json!({ "configurationItems": page });
1322 if let Some(t) = next {
1323 out["nextToken"] = json!(t);
1324 }
1325 Ok(AwsResponse::ok_json(out))
1326 }
1327
1328 fn batch_get_resource_config(
1329 &self,
1330 account: &str,
1331 body: &Value,
1332 ) -> Result<AwsResponse, AwsServiceError> {
1333 let keys = body
1334 .get("resourceKeys")
1335 .and_then(Value::as_array)
1336 .cloned()
1337 .unwrap_or_default();
1338 let st = self.state.read();
1339 let acc = st.account(account);
1340 let mut items = Vec::new();
1341 let mut unprocessed = Vec::new();
1342 for k in keys {
1343 let rt = k
1344 .get("resourceType")
1345 .and_then(Value::as_str)
1346 .unwrap_or_default();
1347 let rid = k
1348 .get("resourceId")
1349 .and_then(Value::as_str)
1350 .unwrap_or_default();
1351 let key = resource_key(rt, rid);
1352 match acc.and_then(|a| a.config_items.get(&key)).and_then(|h| {
1353 h.iter()
1354 .rev()
1355 .find(|ci| !ci.configuration_item_status.starts_with("ResourceDeleted"))
1356 }) {
1357 Some(ci) => items.push(Self::base_config_item_json(ci)),
1358 None => unprocessed.push(k),
1359 }
1360 }
1361 Ok(AwsResponse::ok_json(
1362 json!({ "baseConfigurationItems": items, "unprocessedResourceKeys": unprocessed }),
1363 ))
1364 }
1365
1366 fn list_discovered_resources(
1367 &self,
1368 account: &str,
1369 body: &Value,
1370 ) -> Result<AwsResponse, AwsServiceError> {
1371 let resource_type = require_str(body, "resourceType")?;
1372 let ids = string_list(body, "resourceIds");
1373 let include_deleted = body
1374 .get("includeDeletedResources")
1375 .and_then(Value::as_bool)
1376 .unwrap_or(false);
1377 let st = self.state.read();
1378 let mut list = Vec::new();
1379 if let Some(acc) = st.account(account) {
1380 for (key, history) in &acc.config_items {
1381 let Some((rt, _)) = key.split_once('\u{1}') else {
1382 continue;
1383 };
1384 if rt != resource_type {
1385 continue;
1386 }
1387 let Some(ci) = history.last() else { continue };
1388 let deleted = ci.configuration_item_status.starts_with("ResourceDeleted");
1389 if deleted && !include_deleted {
1390 continue;
1391 }
1392 if !ids.is_empty() && !ids.contains(&ci.resource_id) {
1393 continue;
1394 }
1395 let mut ident = json!({
1396 "resourceType": ci.resource_type,
1397 "resourceId": ci.resource_id,
1398 "resourceName": ci.resource_name,
1399 });
1400 if deleted {
1401 ident["resourceDeletionTime"] =
1402 json!(ci.configuration_item_capture_time.timestamp() as f64);
1403 }
1404 list.push(ident);
1405 }
1406 }
1407 let (page, next) = paginate(list, body);
1408 let mut out = json!({ "resourceIdentifiers": page });
1409 if let Some(t) = next {
1410 out["nextToken"] = json!(t);
1411 }
1412 Ok(AwsResponse::ok_json(out))
1413 }
1414
1415 fn get_discovered_resource_counts(
1416 &self,
1417 account: &str,
1418 body: &Value,
1419 ) -> Result<AwsResponse, AwsServiceError> {
1420 let filter = string_list(body, "resourceTypes");
1421 let st = self.state.read();
1422 let mut counts: std::collections::BTreeMap<String, i64> = std::collections::BTreeMap::new();
1423 if let Some(acc) = st.account(account) {
1424 for (key, history) in &acc.config_items {
1425 let Some((rt, _)) = key.split_once('\u{1}') else {
1426 continue;
1427 };
1428 if !filter.is_empty() && !filter.contains(&rt.to_string()) {
1429 continue;
1430 }
1431 let deleted = history
1432 .last()
1433 .map(|ci| ci.configuration_item_status.starts_with("ResourceDeleted"))
1434 .unwrap_or(true);
1435 if deleted {
1436 continue;
1437 }
1438 *counts.entry(rt.to_string()).or_insert(0) += 1;
1439 }
1440 }
1441 let total: i64 = counts.values().sum();
1442 let list: Vec<Value> = counts
1443 .into_iter()
1444 .map(|(t, c)| json!({ "resourceType": t, "count": c }))
1445 .collect();
1446 let (page, next) = paginate(list, body);
1447 let mut out = json!({ "totalDiscoveredResources": total, "resourceCounts": page });
1448 if let Some(t) = next {
1449 out["nextToken"] = json!(t);
1450 }
1451 Ok(AwsResponse::ok_json(out))
1452 }
1453}
1454
1455impl ConfigService {
1458 fn put_config_rule(
1459 &self,
1460 account: &str,
1461 region: &str,
1462 body: &Value,
1463 ) -> Result<AwsResponse, AwsServiceError> {
1464 let rule = body
1465 .get("ConfigRule")
1466 .filter(|v| v.is_object())
1467 .ok_or_else(|| invalid("ConfigRule is required"))?;
1468 let name = rule
1469 .get("ConfigRuleName")
1470 .and_then(Value::as_str)
1471 .ok_or_else(|| invalid("ConfigRuleName is required"))?
1472 .to_string();
1473 let source = rule
1474 .get("Source")
1475 .cloned()
1476 .ok_or_else(|| invalid("Source is required"))?;
1477 let owner = source
1478 .get("Owner")
1479 .and_then(Value::as_str)
1480 .unwrap_or_default();
1481 if !["AWS", "CUSTOM_LAMBDA", "CUSTOM_POLICY"].contains(&owner) {
1482 return Err(invalid(format!("Invalid Source.Owner '{owner}'")));
1483 }
1484 if owner == "CUSTOM_LAMBDA"
1485 && source
1486 .get("SourceIdentifier")
1487 .and_then(Value::as_str)
1488 .unwrap_or_default()
1489 .is_empty()
1490 {
1491 return Err(invalid(
1492 "SourceIdentifier (Lambda ARN) is required for CUSTOM_LAMBDA rules",
1493 ));
1494 }
1495 let now = Utc::now();
1496 let mut st = self.state.write();
1497 let acc = st.account_mut(account);
1498 let existing = acc.rules.get(&name);
1499 let arn = existing.map(|r| r.arn.clone()).unwrap_or_else(|| {
1500 format!(
1501 "arn:aws:config:{region}:{account}:config-rule/config-rule-{}",
1502 short_id()
1503 )
1504 });
1505 let rule_id = existing
1506 .map(|r| r.rule_id.clone())
1507 .unwrap_or_else(|| format!("config-rule-{}", short_id()));
1508 let cfg_rule = ConfigRule {
1509 name: name.clone(),
1510 arn,
1511 rule_id,
1512 description: rule
1513 .get("Description")
1514 .and_then(Value::as_str)
1515 .map(String::from),
1516 scope: rule.get("Scope").cloned(),
1517 source,
1518 input_parameters: rule
1519 .get("InputParameters")
1520 .and_then(Value::as_str)
1521 .map(String::from),
1522 maximum_execution_frequency: rule
1523 .get("MaximumExecutionFrequency")
1524 .and_then(Value::as_str)
1525 .map(String::from),
1526 state: "ACTIVE".into(),
1527 created_by: rule
1528 .get("CreatedBy")
1529 .and_then(Value::as_str)
1530 .map(String::from),
1531 evaluation_modes: rule.get("EvaluationModes").cloned(),
1532 last_updated: now,
1533 first_activated_time: existing.and_then(|r| r.first_activated_time),
1534 last_successful_evaluation_time: existing
1535 .and_then(|r| r.last_successful_evaluation_time),
1536 last_successful_invocation_time: existing
1537 .and_then(|r| r.last_successful_invocation_time),
1538 };
1539 acc.rules.insert(name, cfg_rule);
1540 Ok(AwsResponse::ok_json(json!({})))
1541 }
1542
1543 fn rule_json(r: &ConfigRule) -> Value {
1544 let mut v = json!({
1545 "ConfigRuleName": r.name,
1546 "ConfigRuleArn": r.arn,
1547 "ConfigRuleId": r.rule_id,
1548 "Source": r.source,
1549 "ConfigRuleState": r.state,
1550 });
1551 if let Some(d) = &r.description {
1552 v["Description"] = json!(d);
1553 }
1554 if let Some(s) = &r.scope {
1555 v["Scope"] = s.clone();
1556 }
1557 if let Some(p) = &r.input_parameters {
1558 v["InputParameters"] = json!(p);
1559 }
1560 if let Some(f) = &r.maximum_execution_frequency {
1561 v["MaximumExecutionFrequency"] = json!(f);
1562 }
1563 if let Some(c) = &r.created_by {
1564 v["CreatedBy"] = json!(c);
1565 }
1566 if let Some(m) = &r.evaluation_modes {
1567 v["EvaluationModes"] = m.clone();
1568 }
1569 v
1570 }
1571
1572 fn describe_config_rules(
1573 &self,
1574 account: &str,
1575 body: &Value,
1576 ) -> Result<AwsResponse, AwsServiceError> {
1577 let names = string_list(body, "ConfigRuleNames");
1578 let st = self.state.read();
1579 let list: Vec<Value> = st
1580 .account(account)
1581 .map(|a| {
1582 a.rules
1583 .values()
1584 .filter(|r| names.is_empty() || names.contains(&r.name))
1585 .map(Self::rule_json)
1586 .collect()
1587 })
1588 .unwrap_or_default();
1589 if !names.is_empty() {
1590 for n in &names {
1591 if !st
1592 .account(account)
1593 .map(|a| a.rules.contains_key(n))
1594 .unwrap_or(false)
1595 {
1596 return Err(no_such(
1597 "NoSuchConfigRuleException",
1598 format!("The ConfigRule '{n}' provided in the request is invalid."),
1599 ));
1600 }
1601 }
1602 }
1603 Ok(paged_response("ConfigRules", list, body, "NextToken"))
1604 }
1605
1606 fn delete_config_rule(
1607 &self,
1608 account: &str,
1609 body: &Value,
1610 ) -> Result<AwsResponse, AwsServiceError> {
1611 let name = require_str(body, "ConfigRuleName")?;
1612 let mut st = self.state.write();
1613 let acc = st.account_mut(account);
1614 if acc.rules.remove(&name).is_none() {
1615 return Err(no_such(
1616 "NoSuchConfigRuleException",
1617 format!("The ConfigRule '{name}' provided in the request is invalid."),
1618 ));
1619 }
1620 acc.evaluations.remove(&name);
1621 acc.remediation_configs.remove(&name);
1622 acc.remediation_executions
1623 .retain(|_, e| e.config_rule_name != name);
1624 Ok(AwsResponse::ok_json(json!({})))
1625 }
1626
1627 fn describe_config_rule_evaluation_status(
1628 &self,
1629 account: &str,
1630 body: &Value,
1631 ) -> Result<AwsResponse, AwsServiceError> {
1632 let names = string_list(body, "ConfigRuleNames");
1633 let st = self.state.read();
1634 let list: Vec<Value> = st
1635 .account(account)
1636 .map(|a| {
1637 a.rules
1638 .values()
1639 .filter(|r| names.is_empty() || names.contains(&r.name))
1640 .map(|r| {
1641 json!({
1642 "ConfigRuleName": r.name,
1643 "ConfigRuleArn": r.arn,
1644 "ConfigRuleId": r.rule_id,
1645 "LastSuccessfulInvocationTime": r.last_successful_invocation_time.map(|t| t.timestamp() as f64),
1646 "LastSuccessfulEvaluationTime": r.last_successful_evaluation_time.map(|t| t.timestamp() as f64),
1647 "FirstActivatedTime": r.first_activated_time.map(|t| t.timestamp() as f64),
1648 "FirstEvaluationStarted": r.first_activated_time.is_some(),
1649 })
1650 })
1651 .collect()
1652 })
1653 .unwrap_or_default();
1654 Ok(paged_response(
1655 "ConfigRulesEvaluationStatus",
1656 list,
1657 body,
1658 "NextToken",
1659 ))
1660 }
1661
1662 async fn start_config_rules_evaluation(
1663 &self,
1664 account: &str,
1665 _region: &str,
1666 body: &Value,
1667 ) -> Result<AwsResponse, AwsServiceError> {
1668 self.ensure_evaluated(account);
1669 let names = string_list(body, "ConfigRuleNames");
1671 let custom_rules: Vec<(String, String, Option<String>)> = {
1672 let st = self.state.read();
1673 st.account(account)
1674 .map(|a| {
1675 a.rules
1676 .values()
1677 .filter(|r| names.is_empty() || names.contains(&r.name))
1678 .filter_map(|r| {
1679 let owner = r.source.get("Owner").and_then(Value::as_str).unwrap_or("");
1680 if owner != "CUSTOM_LAMBDA" {
1681 return None;
1682 }
1683 let lambda_arn = r
1684 .source
1685 .get("SourceIdentifier")
1686 .and_then(Value::as_str)?
1687 .to_string();
1688 Some((r.name.clone(), lambda_arn, r.input_parameters.clone()))
1689 })
1690 .collect()
1691 })
1692 .unwrap_or_default()
1693 };
1694 for (rule_name, lambda_arn, input_parameters) in custom_rules {
1695 self.invoke_custom_rule(
1696 account,
1697 &rule_name,
1698 &lambda_arn,
1699 input_parameters.as_deref(),
1700 )
1701 .await;
1702 }
1703 Ok(AwsResponse::ok_json(json!({})))
1704 }
1705
1706 async fn invoke_custom_rule(
1711 &self,
1712 account: &str,
1713 rule_name: &str,
1714 lambda_arn: &str,
1715 input_parameters: Option<&str>,
1716 ) {
1717 let (Some(lambda_state), Some(runtime)) =
1718 (self.lambda_state.clone(), self.container_runtime.clone())
1719 else {
1720 return;
1721 };
1722 let func_name = lambda_arn
1723 .rsplit(':')
1724 .next()
1725 .unwrap_or(lambda_arn)
1726 .to_string();
1727 let result_token = format!("{rule_name}#{}", Uuid::new_v4());
1728 let event = custom_rule_event(account, rule_name, input_parameters, &result_token);
1729 let resolved = {
1732 let accounts = lambda_state.read();
1733 accounts
1734 .get(account)
1735 .and_then(|state| state.functions.get(&func_name).cloned())
1736 };
1737 let Some(func) = resolved else {
1738 tracing::warn!(function = %func_name, account = %account, "Config custom rule Lambda not found");
1739 return;
1740 };
1741 let payload = event.to_string().into_bytes();
1742 match runtime.invoke(&func, &payload, &[]).await {
1743 Ok(resp) => {
1744 if let Ok(v) = serde_json::from_slice::<Value>(&resp) {
1746 if let Some(arr) = v
1747 .as_array()
1748 .or_else(|| v.get("evaluations").and_then(Value::as_array))
1749 {
1750 self.record_evaluations(account, rule_name, arr);
1751 }
1752 }
1753 }
1754 Err(e) => {
1755 tracing::warn!(function = %func_name, error = %e, "Config custom rule Lambda invocation failed")
1756 }
1757 }
1758 }
1759
1760 fn record_evaluations(&self, account: &str, rule_name: &str, evals: &[Value]) {
1761 let now = Utc::now();
1762 let mut st = self.state.write();
1763 let acc = st.account_mut(account);
1764 let entry = acc.evaluations.entry(rule_name.to_string()).or_default();
1765 for e in evals {
1766 let rt = e
1767 .get("ComplianceResourceType")
1768 .and_then(Value::as_str)
1769 .unwrap_or_default()
1770 .to_string();
1771 let rid = e
1772 .get("ComplianceResourceId")
1773 .and_then(Value::as_str)
1774 .unwrap_or_default()
1775 .to_string();
1776 let ct = e
1777 .get("ComplianceType")
1778 .and_then(Value::as_str)
1779 .unwrap_or("INSUFFICIENT_DATA")
1780 .to_string();
1781 let key = resource_key(&rt, &rid);
1782 entry.insert(
1783 key,
1784 EvaluationResult {
1785 resource_type: rt,
1786 resource_id: rid,
1787 rule_name: rule_name.to_string(),
1788 compliance_type: ct,
1789 annotation: e
1790 .get("Annotation")
1791 .and_then(Value::as_str)
1792 .map(String::from),
1793 result_recorded_time: now,
1794 config_rule_invoked_time: now,
1795 ordering_timestamp: now,
1796 },
1797 );
1798 }
1799 if let Some(rule) = acc.rules.get_mut(rule_name) {
1800 rule.last_successful_evaluation_time = Some(now);
1801 rule.last_successful_invocation_time = Some(now);
1802 if rule.first_activated_time.is_none() {
1803 rule.first_activated_time = Some(now);
1804 }
1805 }
1806 }
1807
1808 fn put_evaluations(&self, account: &str, body: &Value) -> Result<AwsResponse, AwsServiceError> {
1809 let token = require_str(body, "ResultToken")?;
1810 let rule_name = token.split('#').next().unwrap_or(&token).to_string();
1811 let evals = body
1812 .get("Evaluations")
1813 .and_then(Value::as_array)
1814 .cloned()
1815 .unwrap_or_default();
1816 let test_mode = body
1817 .get("TestMode")
1818 .and_then(Value::as_bool)
1819 .unwrap_or(false);
1820 if !test_mode {
1821 let now = Utc::now();
1822 let mut st = self.state.write();
1823 let acc = st.account_mut(account);
1824 let entry = acc.evaluations.entry(rule_name.clone()).or_default();
1825 for e in &evals {
1826 let rt = e
1827 .get("ComplianceResourceType")
1828 .and_then(Value::as_str)
1829 .unwrap_or_default()
1830 .to_string();
1831 let rid = e
1832 .get("ComplianceResourceId")
1833 .and_then(Value::as_str)
1834 .unwrap_or_default()
1835 .to_string();
1836 let ct = e
1837 .get("ComplianceType")
1838 .and_then(Value::as_str)
1839 .unwrap_or("INSUFFICIENT_DATA")
1840 .to_string();
1841 let key = resource_key(&rt, &rid);
1842 entry.insert(
1843 key,
1844 EvaluationResult {
1845 resource_type: rt,
1846 resource_id: rid,
1847 rule_name: rule_name.clone(),
1848 compliance_type: ct,
1849 annotation: e
1850 .get("Annotation")
1851 .and_then(Value::as_str)
1852 .map(String::from),
1853 result_recorded_time: now,
1854 config_rule_invoked_time: now,
1855 ordering_timestamp: now,
1856 },
1857 );
1858 }
1859 }
1860 Ok(AwsResponse::ok_json(json!({ "FailedEvaluations": [] })))
1861 }
1862
1863 fn put_external_evaluation(
1864 &self,
1865 account: &str,
1866 body: &Value,
1867 ) -> Result<AwsResponse, AwsServiceError> {
1868 let rule_name = require_str(body, "ConfigRuleName")?;
1869 let e = body
1870 .get("ExternalEvaluation")
1871 .cloned()
1872 .ok_or_else(|| invalid("ExternalEvaluation is required"))?;
1873 let rt = e
1874 .get("ComplianceResourceType")
1875 .and_then(Value::as_str)
1876 .unwrap_or_default()
1877 .to_string();
1878 let rid = e
1879 .get("ComplianceResourceId")
1880 .and_then(Value::as_str)
1881 .unwrap_or_default()
1882 .to_string();
1883 let ct = e
1884 .get("ComplianceType")
1885 .and_then(Value::as_str)
1886 .unwrap_or("INSUFFICIENT_DATA")
1887 .to_string();
1888 let now = Utc::now();
1889 let mut st = self.state.write();
1890 let acc = st.account_mut(account);
1891 let key = resource_key(&rt, &rid);
1892 acc.evaluations
1893 .entry(rule_name.clone())
1894 .or_default()
1895 .insert(
1896 key,
1897 EvaluationResult {
1898 resource_type: rt,
1899 resource_id: rid,
1900 rule_name,
1901 compliance_type: ct,
1902 annotation: e
1903 .get("Annotation")
1904 .and_then(Value::as_str)
1905 .map(String::from),
1906 result_recorded_time: now,
1907 config_rule_invoked_time: now,
1908 ordering_timestamp: now,
1909 },
1910 );
1911 Ok(AwsResponse::ok_json(json!({})))
1912 }
1913
1914 fn delete_evaluation_results(
1915 &self,
1916 account: &str,
1917 body: &Value,
1918 ) -> Result<AwsResponse, AwsServiceError> {
1919 let name = require_str(body, "ConfigRuleName")?;
1920 let mut st = self.state.write();
1921 st.account_mut(account).evaluations.remove(&name);
1922 Ok(AwsResponse::ok_json(json!({})))
1923 }
1924
1925 fn get_custom_rule_policy(
1926 &self,
1927 account: &str,
1928 body: &Value,
1929 ) -> Result<AwsResponse, AwsServiceError> {
1930 let name = require_str(body, "ConfigRuleName")?;
1931 let st = self.state.read();
1932 let policy = st
1933 .account(account)
1934 .and_then(|a| a.rules.get(&name))
1935 .and_then(|r| {
1936 r.source
1937 .pointer("/CustomPolicyDetails/PolicyText")
1938 .and_then(Value::as_str)
1939 .map(String::from)
1940 });
1941 Ok(AwsResponse::ok_json(json!({ "PolicyText": policy })))
1942 }
1943}
1944
1945fn fold_compliance<'a>(types: impl Iterator<Item = &'a str>) -> String {
1951 let mut any_compliant = false;
1952 let mut any_noncompliant = false;
1953 for t in types {
1954 match t {
1955 "NON_COMPLIANT" => any_noncompliant = true,
1956 "COMPLIANT" => any_compliant = true,
1957 _ => {}
1958 }
1959 }
1960 if any_noncompliant {
1961 "NON_COMPLIANT".into()
1962 } else if any_compliant {
1963 "COMPLIANT".into()
1964 } else {
1965 "INSUFFICIENT_DATA".into()
1966 }
1967}
1968
1969impl ConfigService {
1970 fn describe_compliance_by_config_rule(
1971 &self,
1972 account: &str,
1973 body: &Value,
1974 ) -> Result<AwsResponse, AwsServiceError> {
1975 let names = string_list(body, "ConfigRuleNames");
1976 let filter = string_list(body, "ComplianceTypes");
1977 let st = self.state.read();
1978 let mut list = Vec::new();
1979 if let Some(acc) = st.account(account) {
1980 for rule in acc.rules.values() {
1981 if !names.is_empty() && !names.contains(&rule.name) {
1982 continue;
1983 }
1984 let results = acc.evaluations.get(&rule.name);
1985 let compliance = results
1986 .map(|m| fold_compliance(m.values().map(|r| r.compliance_type.as_str())))
1987 .unwrap_or_else(|| "INSUFFICIENT_DATA".into());
1988 if !filter.is_empty() && !filter.contains(&compliance) {
1989 continue;
1990 }
1991 list.push(json!({
1992 "ConfigRuleName": rule.name,
1993 "Compliance": { "ComplianceType": compliance },
1994 }));
1995 }
1996 }
1997 Ok(paged_response(
1998 "ComplianceByConfigRules",
1999 list,
2000 body,
2001 "NextToken",
2002 ))
2003 }
2004
2005 fn describe_compliance_by_resource(
2006 &self,
2007 account: &str,
2008 body: &Value,
2009 ) -> Result<AwsResponse, AwsServiceError> {
2010 let rtype = body.get("ResourceType").and_then(Value::as_str);
2011 let rid = body.get("ResourceId").and_then(Value::as_str);
2012 let filter = string_list(body, "ComplianceTypes");
2013 let st = self.state.read();
2014 let mut per_resource: std::collections::BTreeMap<(String, String), Vec<String>> =
2016 std::collections::BTreeMap::new();
2017 if let Some(acc) = st.account(account) {
2018 for results in acc.evaluations.values() {
2019 for r in results.values() {
2020 if r.resource_type.is_empty() {
2021 continue;
2022 }
2023 if let Some(t) = rtype {
2024 if r.resource_type != t {
2025 continue;
2026 }
2027 }
2028 if let Some(i) = rid {
2029 if r.resource_id != i {
2030 continue;
2031 }
2032 }
2033 per_resource
2034 .entry((r.resource_type.clone(), r.resource_id.clone()))
2035 .or_default()
2036 .push(r.compliance_type.clone());
2037 }
2038 }
2039 }
2040 let mut list = Vec::new();
2041 for ((rt, ri), types) in per_resource {
2042 let compliance = fold_compliance(types.iter().map(String::as_str));
2043 if !filter.is_empty() && !filter.contains(&compliance) {
2044 continue;
2045 }
2046 list.push(json!({
2047 "ResourceType": rt,
2048 "ResourceId": ri,
2049 "Compliance": { "ComplianceType": compliance },
2050 }));
2051 }
2052 Ok(paged_response(
2053 "ComplianceByResources",
2054 list,
2055 body,
2056 "NextToken",
2057 ))
2058 }
2059
2060 fn get_compliance_details_by_config_rule(
2061 &self,
2062 account: &str,
2063 body: &Value,
2064 ) -> Result<AwsResponse, AwsServiceError> {
2065 let name = require_str(body, "ConfigRuleName")?;
2066 let filter = string_list(body, "ComplianceTypes");
2067 let st = self.state.read();
2068 let results: Vec<Value> = st
2069 .account(account)
2070 .and_then(|a| a.evaluations.get(&name))
2071 .map(|m| {
2072 m.values()
2073 .filter(|r| filter.is_empty() || filter.contains(&r.compliance_type))
2074 .map(|r| Self::evaluation_result_json(r, &name))
2075 .collect()
2076 })
2077 .unwrap_or_default();
2078 Ok(paged_response(
2079 "EvaluationResults",
2080 results,
2081 body,
2082 "NextToken",
2083 ))
2084 }
2085
2086 fn get_compliance_details_by_resource(
2087 &self,
2088 account: &str,
2089 body: &Value,
2090 ) -> Result<AwsResponse, AwsServiceError> {
2091 let rtype = body
2092 .get("ResourceType")
2093 .and_then(Value::as_str)
2094 .unwrap_or_default()
2095 .to_string();
2096 let rid = body
2097 .get("ResourceId")
2098 .and_then(Value::as_str)
2099 .unwrap_or_default()
2100 .to_string();
2101 let filter = string_list(body, "ComplianceTypes");
2102 let st = self.state.read();
2103 let mut results = Vec::new();
2104 if let Some(acc) = st.account(account) {
2105 for (rule_name, m) in &acc.evaluations {
2106 for r in m.values() {
2107 if r.resource_type == rtype
2108 && r.resource_id == rid
2109 && (filter.is_empty() || filter.contains(&r.compliance_type))
2110 {
2111 results.push(Self::evaluation_result_json(r, rule_name));
2112 }
2113 }
2114 }
2115 }
2116 Ok(paged_response(
2117 "EvaluationResults",
2118 results,
2119 body,
2120 "NextToken",
2121 ))
2122 }
2123
2124 fn evaluation_result_json(r: &EvaluationResult, rule_name: &str) -> Value {
2125 json!({
2126 "EvaluationResultIdentifier": {
2127 "EvaluationResultQualifier": {
2128 "ConfigRuleName": rule_name,
2129 "ResourceType": r.resource_type,
2130 "ResourceId": r.resource_id,
2131 },
2132 "OrderingTimestamp": r.ordering_timestamp.timestamp() as f64,
2133 },
2134 "ComplianceType": r.compliance_type,
2135 "ResultRecordedTime": r.result_recorded_time.timestamp() as f64,
2136 "ConfigRuleInvokedTime": r.config_rule_invoked_time.timestamp() as f64,
2137 "Annotation": r.annotation,
2138 })
2139 }
2140
2141 fn get_compliance_summary_by_config_rule(
2142 &self,
2143 account: &str,
2144 ) -> Result<AwsResponse, AwsServiceError> {
2145 let st = self.state.read();
2146 let mut compliant = 0;
2147 let mut noncompliant = 0;
2148 if let Some(acc) = st.account(account) {
2149 for rule in acc.rules.values() {
2150 let c = acc
2151 .evaluations
2152 .get(&rule.name)
2153 .map(|m| fold_compliance(m.values().map(|r| r.compliance_type.as_str())))
2154 .unwrap_or_else(|| "INSUFFICIENT_DATA".into());
2155 match c.as_str() {
2156 "COMPLIANT" => compliant += 1,
2157 "NON_COMPLIANT" => noncompliant += 1,
2158 _ => {}
2159 }
2160 }
2161 }
2162 Ok(AwsResponse::ok_json(json!({
2163 "ComplianceSummary": {
2164 "CompliantResourceCount": { "CappedCount": compliant, "CapExceeded": false },
2165 "NonCompliantResourceCount": { "CappedCount": noncompliant, "CapExceeded": false },
2166 "ComplianceSummaryTimestamp": Utc::now().timestamp() as f64,
2167 }
2168 })))
2169 }
2170
2171 fn get_compliance_summary_by_resource_type(
2172 &self,
2173 account: &str,
2174 body: &Value,
2175 ) -> Result<AwsResponse, AwsServiceError> {
2176 let types = string_list(body, "ResourceTypes");
2177 let st = self.state.read();
2178 let mut per_type_resources: std::collections::BTreeMap<
2182 String,
2183 std::collections::BTreeMap<String, Vec<String>>,
2184 > = std::collections::BTreeMap::new();
2185 if let Some(acc) = st.account(account) {
2186 for m in acc.evaluations.values() {
2187 for r in m.values() {
2188 if r.resource_type.is_empty() {
2189 continue;
2190 }
2191 if !types.is_empty() && !types.contains(&r.resource_type) {
2192 continue;
2193 }
2194 per_type_resources
2195 .entry(r.resource_type.clone())
2196 .or_default()
2197 .entry(r.resource_id.clone())
2198 .or_default()
2199 .push(r.compliance_type.clone());
2200 }
2201 }
2202 }
2203 let now = Utc::now().timestamp() as f64;
2204 let summary_json = |resource_type: &str,
2205 resources: &std::collections::BTreeMap<String, Vec<String>>|
2206 -> Value {
2207 let mut compliant = 0;
2208 let mut noncompliant = 0;
2209 for c in resources.values() {
2210 match fold_compliance(c.iter().map(String::as_str)).as_str() {
2211 "COMPLIANT" => compliant += 1,
2212 "NON_COMPLIANT" => noncompliant += 1,
2213 _ => {}
2214 }
2215 }
2216 let summary = json!({
2217 "CompliantResourceCount": { "CappedCount": compliant, "CapExceeded": false },
2218 "NonCompliantResourceCount": { "CappedCount": noncompliant, "CapExceeded": false },
2219 "ComplianceSummaryTimestamp": now,
2220 });
2221 if resource_type.is_empty() {
2222 json!({ "ComplianceSummary": summary })
2223 } else {
2224 json!({ "ResourceType": resource_type, "ComplianceSummary": summary })
2225 }
2226 };
2227 let list: Vec<Value> = if types.is_empty() {
2228 let mut all: std::collections::BTreeMap<String, Vec<String>> =
2231 std::collections::BTreeMap::new();
2232 for resources in per_type_resources.values() {
2233 for (rid, c) in resources {
2234 all.entry(rid.clone())
2235 .or_default()
2236 .extend(c.iter().cloned());
2237 }
2238 }
2239 vec![summary_json("", &all)]
2240 } else {
2241 types
2244 .iter()
2245 .map(|t| {
2246 let empty = std::collections::BTreeMap::new();
2247 let resources = per_type_resources.get(t).unwrap_or(&empty);
2248 summary_json(t, resources)
2249 })
2250 .collect()
2251 };
2252 Ok(AwsResponse::ok_json(json!({
2253 "ComplianceSummariesByResourceType": list
2254 })))
2255 }
2256}
2257
2258impl ConfigService {
2261 fn put_remediation_configurations(
2262 &self,
2263 account: &str,
2264 region: &str,
2265 body: &Value,
2266 ) -> Result<AwsResponse, AwsServiceError> {
2267 let configs = body
2268 .get("RemediationConfigurations")
2269 .and_then(Value::as_array)
2270 .cloned()
2271 .unwrap_or_default();
2272 let mut failed = Vec::new();
2273 let mut st = self.state.write();
2274 let acc = st.account_mut(account);
2275 for c in &configs {
2276 let rule_name = match c.get("ConfigRuleName").and_then(Value::as_str) {
2277 Some(n) => n.to_string(),
2278 None => {
2279 failed.push(json!({ "FailureMessage": "ConfigRuleName is required", "FailedItems": [c] }));
2280 continue;
2281 }
2282 };
2283 acc.remediation_configs.insert(
2284 rule_name.clone(),
2285 RemediationConfiguration {
2286 config_rule_name: rule_name.clone(),
2287 arn: format!("arn:aws:config:{region}:{account}:remediation-configuration/{rule_name}/{}", short_id()),
2288 target_type: c.get("TargetType").and_then(Value::as_str).unwrap_or("SSM_DOCUMENT").to_string(),
2289 target_id: c.get("TargetId").and_then(Value::as_str).unwrap_or_default().to_string(),
2290 target_version: c.get("TargetVersion").and_then(Value::as_str).map(String::from),
2291 parameters: c.get("Parameters").cloned(),
2292 resource_type: c.get("ResourceType").and_then(Value::as_str).map(String::from),
2293 automatic: c.get("Automatic").and_then(Value::as_bool).unwrap_or(false),
2294 execution_controls: c.get("ExecutionControls").cloned(),
2295 maximum_automatic_attempts: c.get("MaximumAutomaticAttempts").and_then(Value::as_i64),
2296 retry_attempt_seconds: c.get("RetryAttemptSeconds").and_then(Value::as_i64),
2297 created_by_service: None,
2298 },
2299 );
2300 }
2301 Ok(AwsResponse::ok_json(json!({ "FailedBatches": failed })))
2302 }
2303
2304 fn remediation_json(r: &RemediationConfiguration) -> Value {
2305 let mut v = json!({
2306 "ConfigRuleName": r.config_rule_name,
2307 "TargetType": r.target_type,
2308 "TargetId": r.target_id,
2309 "Arn": r.arn,
2310 "Automatic": r.automatic,
2311 });
2312 if let Some(t) = &r.target_version {
2313 v["TargetVersion"] = json!(t);
2314 }
2315 if let Some(p) = &r.parameters {
2316 v["Parameters"] = p.clone();
2317 }
2318 if let Some(rt) = &r.resource_type {
2319 v["ResourceType"] = json!(rt);
2320 }
2321 if let Some(e) = &r.execution_controls {
2322 v["ExecutionControls"] = e.clone();
2323 }
2324 if let Some(m) = r.maximum_automatic_attempts {
2325 v["MaximumAutomaticAttempts"] = json!(m);
2326 }
2327 if let Some(s) = r.retry_attempt_seconds {
2328 v["RetryAttemptSeconds"] = json!(s);
2329 }
2330 v
2331 }
2332
2333 fn describe_remediation_configurations(
2334 &self,
2335 account: &str,
2336 body: &Value,
2337 ) -> Result<AwsResponse, AwsServiceError> {
2338 let names = string_list(body, "ConfigRuleNames");
2339 let st = self.state.read();
2340 let list: Vec<Value> = st
2341 .account(account)
2342 .map(|a| {
2343 a.remediation_configs
2344 .values()
2345 .filter(|r| names.is_empty() || names.contains(&r.config_rule_name))
2346 .map(Self::remediation_json)
2347 .collect()
2348 })
2349 .unwrap_or_default();
2350 Ok(AwsResponse::ok_json(
2351 json!({ "RemediationConfigurations": list }),
2352 ))
2353 }
2354
2355 fn delete_remediation_configuration(
2356 &self,
2357 account: &str,
2358 body: &Value,
2359 ) -> Result<AwsResponse, AwsServiceError> {
2360 let name = require_str(body, "ConfigRuleName")?;
2361 let mut st = self.state.write();
2362 let acc = st.account_mut(account);
2363 if acc.remediation_configs.remove(&name).is_none() {
2364 return Err(no_such(
2365 "NoSuchRemediationConfigurationException",
2366 "No RemediationConfiguration for rule exists.",
2367 ));
2368 }
2369 acc.remediation_executions
2370 .retain(|_, e| e.config_rule_name != name);
2371 Ok(AwsResponse::ok_json(json!({})))
2372 }
2373
2374 fn put_remediation_exceptions(
2375 &self,
2376 account: &str,
2377 body: &Value,
2378 ) -> Result<AwsResponse, AwsServiceError> {
2379 let rule_name = require_str(body, "ConfigRuleName")?;
2380 let keys = body
2381 .get("ResourceKeys")
2382 .and_then(Value::as_array)
2383 .cloned()
2384 .unwrap_or_default();
2385 let message = body
2386 .get("Message")
2387 .and_then(Value::as_str)
2388 .map(String::from);
2389 let mut st = self.state.write();
2390 let acc = st.account_mut(account);
2391 for k in &keys {
2392 let rt = k
2393 .get("ResourceType")
2394 .and_then(Value::as_str)
2395 .unwrap_or_default()
2396 .to_string();
2397 let rid = k
2398 .get("ResourceId")
2399 .and_then(Value::as_str)
2400 .unwrap_or_default()
2401 .to_string();
2402 let key = format!("{rule_name}\u{1}{rt}\u{1}{rid}");
2403 acc.remediation_exceptions.insert(
2404 key,
2405 RemediationException {
2406 config_rule_name: rule_name.clone(),
2407 resource_type: rt,
2408 resource_id: rid,
2409 message: message.clone(),
2410 expiration_time: None,
2411 },
2412 );
2413 }
2414 Ok(AwsResponse::ok_json(json!({ "FailedBatches": [] })))
2415 }
2416
2417 fn describe_remediation_exceptions(
2418 &self,
2419 account: &str,
2420 body: &Value,
2421 ) -> Result<AwsResponse, AwsServiceError> {
2422 let rule_name = require_str(body, "ConfigRuleName")?;
2423 let st = self.state.read();
2424 let list: Vec<Value> = st
2425 .account(account)
2426 .map(|a| {
2427 a.remediation_exceptions
2428 .values()
2429 .filter(|e| e.config_rule_name == rule_name)
2430 .map(|e| {
2431 json!({
2432 "ConfigRuleName": e.config_rule_name,
2433 "ResourceType": e.resource_type,
2434 "ResourceId": e.resource_id,
2435 "Message": e.message,
2436 })
2437 })
2438 .collect()
2439 })
2440 .unwrap_or_default();
2441 Ok(paged_response(
2442 "RemediationExceptions",
2443 list,
2444 body,
2445 "NextToken",
2446 ))
2447 }
2448
2449 fn delete_remediation_exceptions(
2450 &self,
2451 account: &str,
2452 body: &Value,
2453 ) -> Result<AwsResponse, AwsServiceError> {
2454 let rule_name = require_str(body, "ConfigRuleName")?;
2455 let keys = body
2456 .get("ResourceKeys")
2457 .and_then(Value::as_array)
2458 .cloned()
2459 .unwrap_or_default();
2460 let mut st = self.state.write();
2461 let acc = st.account_mut(account);
2462 for k in &keys {
2463 let rt = k
2464 .get("ResourceType")
2465 .and_then(Value::as_str)
2466 .unwrap_or_default();
2467 let rid = k
2468 .get("ResourceId")
2469 .and_then(Value::as_str)
2470 .unwrap_or_default();
2471 acc.remediation_exceptions
2472 .remove(&format!("{rule_name}\u{1}{rt}\u{1}{rid}"));
2473 }
2474 Ok(AwsResponse::ok_json(json!({ "FailedBatches": [] })))
2475 }
2476
2477 fn start_remediation_execution(
2478 &self,
2479 account: &str,
2480 body: &Value,
2481 ) -> Result<AwsResponse, AwsServiceError> {
2482 let rule_name = require_str(body, "ConfigRuleName")?;
2483 let keys = body
2484 .get("ResourceKeys")
2485 .and_then(Value::as_array)
2486 .cloned()
2487 .unwrap_or_default();
2488 let now = Utc::now();
2489 let mut st = self.state.write();
2490 let acc = st.account_mut(account);
2491 if !acc.remediation_configs.contains_key(&rule_name) {
2492 return Err(no_such(
2493 "NoSuchRemediationConfigurationException",
2494 "No RemediationConfiguration for rule exists.",
2495 ));
2496 }
2497 let mut failed = Vec::new();
2502 for k in &keys {
2503 let (Some(rt), Some(rid)) = (
2504 k.get("resourceType")
2505 .or_else(|| k.get("ResourceType"))
2506 .and_then(Value::as_str),
2507 k.get("resourceId")
2508 .or_else(|| k.get("ResourceId"))
2509 .and_then(Value::as_str),
2510 ) else {
2511 failed.push(k.clone());
2512 continue;
2513 };
2514 let key = format!("{rule_name}\u{1}{rt}\u{1}{rid}");
2515 acc.remediation_executions.insert(
2516 key,
2517 RemediationExecutionStatus {
2518 config_rule_name: rule_name.clone(),
2519 resource_type: rt.to_string(),
2520 resource_id: rid.to_string(),
2521 state: "QUEUED".into(),
2522 invocation_time: now,
2523 last_updated_time: now,
2524 },
2525 );
2526 }
2527 Ok(AwsResponse::ok_json(json!({ "FailedItems": failed })))
2528 }
2529
2530 fn describe_remediation_execution_status(
2531 &self,
2532 account: &str,
2533 body: &Value,
2534 ) -> Result<AwsResponse, AwsServiceError> {
2535 let rule_name = require_str(body, "ConfigRuleName")?;
2536 let keys = body
2537 .get("ResourceKeys")
2538 .and_then(Value::as_array)
2539 .cloned()
2540 .unwrap_or_default();
2541 let wanted: Vec<(String, String)> = keys
2543 .iter()
2544 .filter_map(|k| {
2545 let rt = k
2546 .get("resourceType")
2547 .or_else(|| k.get("ResourceType"))
2548 .and_then(Value::as_str)?;
2549 let rid = k
2550 .get("resourceId")
2551 .or_else(|| k.get("ResourceId"))
2552 .and_then(Value::as_str)?;
2553 Some((rt.to_string(), rid.to_string()))
2554 })
2555 .collect();
2556 let st = self.state.read();
2557 let list: Vec<Value> = st
2558 .account(account)
2559 .map(|a| {
2560 a.remediation_executions
2561 .values()
2562 .filter(|e| e.config_rule_name == rule_name)
2563 .filter(|e| {
2564 wanted.is_empty()
2565 || wanted
2566 .iter()
2567 .any(|(rt, rid)| *rt == e.resource_type && *rid == e.resource_id)
2568 })
2569 .map(|e| {
2570 let inv = e.invocation_time.timestamp() as f64;
2571 let upd = e.last_updated_time.timestamp() as f64;
2572 json!({
2573 "ResourceKey": { "resourceType": e.resource_type, "resourceId": e.resource_id },
2574 "State": e.state,
2575 "StepDetails": [{ "Name": "remediate", "State": e.state, "StartTime": inv }],
2576 "InvocationTime": inv,
2577 "LastUpdatedTime": upd,
2578 })
2579 })
2580 .collect()
2581 })
2582 .unwrap_or_default();
2583 Ok(paged_response(
2584 "RemediationExecutionStatuses",
2585 list,
2586 body,
2587 "NextToken",
2588 ))
2589 }
2590}
2591
2592fn extract_pack_rule_names(template: Option<&str>) -> Vec<String> {
2600 let Some(t) = template else { return Vec::new() };
2601 let v: Value = serde_json::from_str(t)
2604 .or_else(|_| serde_yaml::from_str::<Value>(t))
2605 .unwrap_or(Value::Null);
2606 let Some(resources) = v.get("Resources").and_then(Value::as_object) else {
2607 return Vec::new();
2608 };
2609 resources
2610 .values()
2611 .filter(|r| r.get("Type").and_then(Value::as_str) == Some("AWS::Config::ConfigRule"))
2612 .filter_map(|r| {
2613 r.pointer("/Properties/ConfigRuleName")
2614 .and_then(Value::as_str)
2615 .map(String::from)
2616 })
2617 .collect()
2618}
2619
2620impl ConfigService {
2621 fn put_conformance_pack(
2622 &self,
2623 account: &str,
2624 region: &str,
2625 body: &Value,
2626 ) -> Result<AwsResponse, AwsServiceError> {
2627 let name = require_str(body, "ConformancePackName")?;
2628 let template_body = body
2629 .get("TemplateBody")
2630 .and_then(Value::as_str)
2631 .map(String::from);
2632 let template_s3_uri = body
2633 .get("TemplateS3Uri")
2634 .and_then(Value::as_str)
2635 .map(String::from);
2636 if template_body.is_none()
2637 && template_s3_uri.is_none()
2638 && body.get("TemplateSSMDocumentDetails").is_none()
2639 {
2640 return Err(invalid(
2641 "Either TemplateBody, TemplateS3Uri, or TemplateSSMDocumentDetails is required",
2642 ));
2643 }
2644 let now = Utc::now();
2645 let arn = format!(
2646 "arn:aws:config:{region}:{account}:conformance-pack/{name}-{}",
2647 short_id()
2648 );
2649 let id = format!("conformance-pack-{}", short_id());
2650 let rule_names = extract_pack_rule_names(template_body.as_deref());
2651 let mut st = self.state.write();
2652 let acc = st.account_mut(account);
2653 let existing = acc.conformance_packs.get(&name);
2654 let pack = ConformancePack {
2655 name: name.clone(),
2656 arn: existing.map(|p| p.arn.clone()).unwrap_or(arn.clone()),
2657 id: existing.map(|p| p.id.clone()).unwrap_or(id),
2658 delivery_s3_bucket: body
2659 .get("DeliveryS3Bucket")
2660 .and_then(Value::as_str)
2661 .map(String::from),
2662 delivery_s3_key_prefix: body
2663 .get("DeliveryS3KeyPrefix")
2664 .and_then(Value::as_str)
2665 .map(String::from),
2666 input_parameters: body
2667 .get("ConformancePackInputParameters")
2668 .and_then(Value::as_array)
2669 .cloned()
2670 .unwrap_or_default(),
2671 template_body,
2672 template_s3_uri,
2673 template_ssm_document_details: body.get("TemplateSSMDocumentDetails").cloned(),
2674 last_update_requested_time: now,
2675 created_by: None,
2676 rule_names,
2677 };
2678 let out_arn = pack.arn.clone();
2679 acc.conformance_packs.insert(name, pack);
2680 Ok(AwsResponse::ok_json(
2681 json!({ "ConformancePackArn": out_arn }),
2682 ))
2683 }
2684
2685 fn describe_conformance_packs(
2686 &self,
2687 account: &str,
2688 body: &Value,
2689 ) -> Result<AwsResponse, AwsServiceError> {
2690 let names = string_list(body, "ConformancePackNames");
2691 let st = self.state.read();
2692 let list: Vec<Value> = st
2693 .account(account)
2694 .map(|a| {
2695 a.conformance_packs
2696 .values()
2697 .filter(|p| names.is_empty() || names.contains(&p.name))
2698 .map(|p| {
2699 json!({
2700 "ConformancePackName": p.name,
2701 "ConformancePackArn": p.arn,
2702 "ConformancePackId": p.id,
2703 "DeliveryS3Bucket": p.delivery_s3_bucket,
2704 "DeliveryS3KeyPrefix": p.delivery_s3_key_prefix,
2705 "ConformancePackInputParameters": p.input_parameters,
2706 "LastUpdateRequestedTime": p.last_update_requested_time.timestamp() as f64,
2707 "TemplateSSMDocumentDetails": p.template_ssm_document_details,
2708 })
2709 })
2710 .collect()
2711 })
2712 .unwrap_or_default();
2713 if !names.is_empty() {
2714 for n in &names {
2715 if !st
2716 .account(account)
2717 .map(|a| a.conformance_packs.contains_key(n))
2718 .unwrap_or(false)
2719 {
2720 return Err(no_such(
2721 "NoSuchConformancePackException",
2722 format!("Cannot find conformance pack with name '{n}'."),
2723 ));
2724 }
2725 }
2726 }
2727 Ok(paged_response(
2728 "ConformancePackDetails",
2729 list,
2730 body,
2731 "NextToken",
2732 ))
2733 }
2734
2735 fn delete_conformance_pack(
2736 &self,
2737 account: &str,
2738 body: &Value,
2739 ) -> Result<AwsResponse, AwsServiceError> {
2740 let name = require_str(body, "ConformancePackName")?;
2741 let mut st = self.state.write();
2742 let acc = st.account_mut(account);
2743 if acc.conformance_packs.remove(&name).is_none() {
2744 return Err(no_such(
2745 "NoSuchConformancePackException",
2746 format!("Cannot find conformance pack with name '{name}'."),
2747 ));
2748 }
2749 Ok(AwsResponse::ok_json(json!({})))
2750 }
2751
2752 fn describe_conformance_pack_status(
2753 &self,
2754 account: &str,
2755 body: &Value,
2756 ) -> Result<AwsResponse, AwsServiceError> {
2757 let names = string_list(body, "ConformancePackNames");
2758 let now = Utc::now().timestamp() as f64;
2759 let st = self.state.read();
2760 let list: Vec<Value> = st
2761 .account(account)
2762 .map(|a| {
2763 a.conformance_packs
2764 .values()
2765 .filter(|p| names.is_empty() || names.contains(&p.name))
2766 .map(|p| {
2767 json!({
2768 "ConformancePackName": p.name,
2769 "ConformancePackId": p.id,
2770 "ConformancePackArn": p.arn,
2771 "ConformancePackState": "CREATE_COMPLETE",
2772 "StackArn": format!("arn:aws:cloudformation:us-east-1:{account}:stack/awsconfigconforms-{}/{}", p.name, short_id()),
2773 "LastUpdateRequestedTime": now,
2774 "LastUpdateCompletedTime": now,
2775 })
2776 })
2777 .collect()
2778 })
2779 .unwrap_or_default();
2780 Ok(paged_response(
2781 "ConformancePackStatusDetails",
2782 list,
2783 body,
2784 "NextToken",
2785 ))
2786 }
2787
2788 fn pack_rule_names(acc: &AccountState, pack: &ConformancePack) -> Vec<String> {
2791 if pack.rule_names.is_empty() {
2792 acc.rules.keys().cloned().collect()
2793 } else {
2794 pack.rule_names
2795 .iter()
2796 .filter(|n| acc.rules.contains_key(*n))
2797 .cloned()
2798 .collect()
2799 }
2800 }
2801
2802 fn describe_conformance_pack_compliance(
2803 &self,
2804 account: &str,
2805 body: &Value,
2806 ) -> Result<AwsResponse, AwsServiceError> {
2807 let name = require_str(body, "ConformancePackName")?;
2808 let st = self.state.read();
2809 let acc = st.account(account).ok_or_else(|| {
2810 no_such(
2811 "NoSuchConformancePackException",
2812 "No such conformance pack.",
2813 )
2814 })?;
2815 let pack = acc.conformance_packs.get(&name).ok_or_else(|| {
2816 no_such(
2817 "NoSuchConformancePackException",
2818 format!("Cannot find conformance pack with name '{name}'."),
2819 )
2820 })?;
2821 let list: Vec<Value> = Self::pack_rule_names(acc, pack)
2822 .iter()
2823 .map(|rule| {
2824 let c = acc
2825 .evaluations
2826 .get(rule)
2827 .map(|m| fold_compliance(m.values().map(|r| r.compliance_type.as_str())))
2828 .unwrap_or_else(|| "INSUFFICIENT_DATA".into());
2829 json!({ "ConfigRuleName": rule, "ComplianceType": c, "Controls": [] })
2830 })
2831 .collect();
2832 let (page, next) = paginate(list, body);
2833 let mut out =
2834 json!({ "ConformancePackName": name, "ConformancePackRuleComplianceList": page });
2835 if let Some(t) = next {
2836 out["NextToken"] = json!(t);
2837 }
2838 Ok(AwsResponse::ok_json(out))
2839 }
2840
2841 fn get_conformance_pack_compliance_summary(
2842 &self,
2843 account: &str,
2844 body: &Value,
2845 ) -> Result<AwsResponse, AwsServiceError> {
2846 let names = string_list(body, "ConformancePackNames");
2847 let st = self.state.read();
2848 let acc = st.account(account);
2849 let list: Vec<Value> = acc
2850 .map(|a| {
2851 a.conformance_packs
2852 .values()
2853 .filter(|p| names.is_empty() || names.contains(&p.name))
2854 .map(|p| {
2855 let rules = Self::pack_rule_names(a, p);
2856 let noncompliant = rules.iter().any(|r| {
2857 a.evaluations.get(r).map(|m| m.values().any(|e| e.compliance_type == "NON_COMPLIANT")).unwrap_or(false)
2858 });
2859 json!({
2860 "ConformancePackName": p.name,
2861 "ConformancePackComplianceStatus": if noncompliant { "NON_COMPLIANT" } else { "COMPLIANT" },
2862 })
2863 })
2864 .collect()
2865 })
2866 .unwrap_or_default();
2867 Ok(AwsResponse::ok_json(
2868 json!({ "ConformancePackComplianceSummaryList": list }),
2869 ))
2870 }
2871
2872 fn get_conformance_pack_compliance_details(
2873 &self,
2874 account: &str,
2875 body: &Value,
2876 ) -> Result<AwsResponse, AwsServiceError> {
2877 let name = require_str(body, "ConformancePackName")?;
2878 let st = self.state.read();
2879 let acc = st.account(account).ok_or_else(|| {
2880 no_such(
2881 "NoSuchConformancePackException",
2882 "No such conformance pack.",
2883 )
2884 })?;
2885 let pack = acc.conformance_packs.get(&name).ok_or_else(|| {
2886 no_such(
2887 "NoSuchConformancePackException",
2888 format!("Cannot find conformance pack with name '{name}'."),
2889 )
2890 })?;
2891 let mut results = Vec::new();
2892 for rule in Self::pack_rule_names(acc, pack) {
2893 if let Some(m) = acc.evaluations.get(&rule) {
2894 for r in m.values() {
2895 results.push(json!({
2896 "ConfigRuleInvokedTime": r.config_rule_invoked_time.timestamp() as f64,
2897 "ResultRecordedTime": r.result_recorded_time.timestamp() as f64,
2898 "ComplianceType": r.compliance_type,
2899 "EvaluationResultIdentifier": {
2900 "EvaluationResultQualifier": {
2901 "ConfigRuleName": rule,
2902 "ResourceType": r.resource_type,
2903 "ResourceId": r.resource_id,
2904 },
2905 "OrderingTimestamp": r.ordering_timestamp.timestamp() as f64,
2906 },
2907 }));
2908 }
2909 }
2910 }
2911 let (page, next) = paginate(results, body);
2912 let mut out =
2913 json!({ "ConformancePackName": name, "ConformancePackRuleEvaluationResults": page });
2914 if let Some(t) = next {
2915 out["NextToken"] = json!(t);
2916 }
2917 Ok(AwsResponse::ok_json(out))
2918 }
2919
2920 fn list_conformance_pack_compliance_scores(
2921 &self,
2922 account: &str,
2923 ) -> Result<AwsResponse, AwsServiceError> {
2924 let st = self.state.read();
2925 let list: Vec<Value> = st
2926 .account(account)
2927 .map(|a| {
2928 a.conformance_packs
2929 .values()
2930 .map(|p| {
2931 let rules = Self::pack_rule_names(a, p);
2932 let total = rules.len().max(1);
2933 let compliant = rules
2934 .iter()
2935 .filter(|r| {
2936 a.evaluations
2937 .get(*r)
2938 .map(|m| {
2939 !m.values().any(|e| e.compliance_type == "NON_COMPLIANT")
2940 })
2941 .unwrap_or(true)
2942 })
2943 .count();
2944 let score = (compliant as f64 / total as f64) * 100.0;
2945 json!({
2946 "Score": format!("{score:.2}"),
2947 "ConformancePackName": p.name,
2948 "LastUpdatedTime": p.last_update_requested_time.timestamp() as f64,
2949 })
2950 })
2951 .collect()
2952 })
2953 .unwrap_or_default();
2954 Ok(AwsResponse::ok_json(
2955 json!({ "ConformancePackComplianceScores": list }),
2956 ))
2957 }
2958}
2959
2960impl ConfigService {
2963 fn put_organization_config_rule(
2964 &self,
2965 account: &str,
2966 region: &str,
2967 body: &Value,
2968 ) -> Result<AwsResponse, AwsServiceError> {
2969 let name = require_str(body, "OrganizationConfigRuleName")?;
2970 let arn = format!(
2971 "arn:aws:config:{region}:{account}:organization-config-rule/{name}-{}",
2972 short_id()
2973 );
2974 let mut st = self.state.write();
2975 let acc = st.account_mut(account);
2976 let out_arn = acc
2977 .org_rules
2978 .get(&name)
2979 .map(|r| r.arn.clone())
2980 .unwrap_or(arn);
2981 acc.org_rules.insert(
2982 name.clone(),
2983 OrganizationConfigRule {
2984 name,
2985 arn: out_arn.clone(),
2986 managed_rule_metadata: body.get("OrganizationManagedRuleMetadata").cloned(),
2987 custom_rule_metadata: body.get("OrganizationCustomRuleMetadata").cloned(),
2988 custom_policy_rule_metadata: body
2989 .get("OrganizationCustomPolicyRuleMetadata")
2990 .cloned(),
2991 excluded_accounts: string_list(body, "ExcludedAccounts"),
2992 last_update_time: Utc::now(),
2993 },
2994 );
2995 Ok(AwsResponse::ok_json(
2996 json!({ "OrganizationConfigRuleArn": out_arn }),
2997 ))
2998 }
2999
3000 fn describe_organization_config_rules(
3001 &self,
3002 account: &str,
3003 body: &Value,
3004 ) -> Result<AwsResponse, AwsServiceError> {
3005 let names = string_list(body, "OrganizationConfigRuleNames");
3006 let st = self.state.read();
3007 let list: Vec<Value> = st
3008 .account(account)
3009 .map(|a| {
3010 a.org_rules
3011 .values()
3012 .filter(|r| names.is_empty() || names.contains(&r.name))
3013 .map(|r| {
3014 json!({
3015 "OrganizationConfigRuleName": r.name,
3016 "OrganizationConfigRuleArn": r.arn,
3017 "OrganizationManagedRuleMetadata": r.managed_rule_metadata,
3018 "OrganizationCustomRuleMetadata": r.custom_rule_metadata,
3019 "OrganizationCustomPolicyRuleMetadata": r.custom_policy_rule_metadata,
3020 "ExcludedAccounts": r.excluded_accounts,
3021 "LastUpdateTime": r.last_update_time.timestamp() as f64,
3022 })
3023 })
3024 .collect()
3025 })
3026 .unwrap_or_default();
3027 Ok(AwsResponse::ok_json(
3028 json!({ "OrganizationConfigRules": list }),
3029 ))
3030 }
3031
3032 fn delete_organization_config_rule(
3033 &self,
3034 account: &str,
3035 body: &Value,
3036 ) -> Result<AwsResponse, AwsServiceError> {
3037 let name = require_str(body, "OrganizationConfigRuleName")?;
3038 let mut st = self.state.write();
3039 let acc = st.account_mut(account);
3040 if acc.org_rules.remove(&name).is_none() {
3041 return Err(no_such(
3042 "NoSuchOrganizationConfigRuleException",
3043 format!("Cannot find organization config rule '{name}'."),
3044 ));
3045 }
3046 Ok(AwsResponse::ok_json(json!({})))
3047 }
3048
3049 fn describe_organization_config_rule_statuses(
3050 &self,
3051 account: &str,
3052 body: &Value,
3053 ) -> Result<AwsResponse, AwsServiceError> {
3054 let names = string_list(body, "OrganizationConfigRuleNames");
3055 let now = Utc::now().timestamp() as f64;
3056 let st = self.state.read();
3057 let list: Vec<Value> = st
3058 .account(account)
3059 .map(|a| {
3060 a.org_rules
3061 .values()
3062 .filter(|r| names.is_empty() || names.contains(&r.name))
3063 .map(|r| {
3064 json!({ "OrganizationConfigRuleName": r.name, "OrganizationRuleStatus": "CREATE_SUCCESSFUL", "LastUpdateTime": now })
3065 })
3066 .collect()
3067 })
3068 .unwrap_or_default();
3069 Ok(AwsResponse::ok_json(
3070 json!({ "OrganizationConfigRuleStatuses": list }),
3071 ))
3072 }
3073
3074 fn get_organization_config_rule_detailed_status(
3075 &self,
3076 account: &str,
3077 body: &Value,
3078 ) -> Result<AwsResponse, AwsServiceError> {
3079 let name = require_str(body, "OrganizationConfigRuleName")?;
3080 let st = self.state.read();
3081 if !st
3082 .account(account)
3083 .map(|a| a.org_rules.contains_key(&name))
3084 .unwrap_or(false)
3085 {
3086 return Err(no_such(
3087 "NoSuchOrganizationConfigRuleException",
3088 format!("Cannot find organization config rule '{name}'."),
3089 ));
3090 }
3091 Ok(AwsResponse::ok_json(
3092 json!({ "OrganizationConfigRuleDetailedStatus": [] }),
3093 ))
3094 }
3095
3096 fn get_organization_custom_rule_policy(
3097 &self,
3098 account: &str,
3099 body: &Value,
3100 ) -> Result<AwsResponse, AwsServiceError> {
3101 let name = require_str(body, "OrganizationConfigRuleName")?;
3102 let st = self.state.read();
3103 let policy = st
3104 .account(account)
3105 .and_then(|a| a.org_rules.get(&name))
3106 .and_then(|r| r.custom_policy_rule_metadata.as_ref())
3107 .and_then(|m| {
3108 m.get("PolicyText")
3109 .and_then(Value::as_str)
3110 .map(String::from)
3111 });
3112 Ok(AwsResponse::ok_json(json!({ "PolicyText": policy })))
3113 }
3114
3115 fn put_organization_conformance_pack(
3116 &self,
3117 account: &str,
3118 region: &str,
3119 body: &Value,
3120 ) -> Result<AwsResponse, AwsServiceError> {
3121 let name = require_str(body, "OrganizationConformancePackName")?;
3122 let arn = format!(
3123 "arn:aws:config:{region}:{account}:organization-conformance-pack/{name}-{}",
3124 short_id()
3125 );
3126 let mut st = self.state.write();
3127 let acc = st.account_mut(account);
3128 let out_arn = acc
3129 .org_conformance_packs
3130 .get(&name)
3131 .map(|p| p.arn.clone())
3132 .unwrap_or(arn);
3133 acc.org_conformance_packs.insert(
3134 name.clone(),
3135 OrganizationConformancePack {
3136 name,
3137 arn: out_arn.clone(),
3138 delivery_s3_bucket: body
3139 .get("DeliveryS3Bucket")
3140 .and_then(Value::as_str)
3141 .map(String::from),
3142 delivery_s3_key_prefix: body
3143 .get("DeliveryS3KeyPrefix")
3144 .and_then(Value::as_str)
3145 .map(String::from),
3146 input_parameters: body
3147 .get("ConformancePackInputParameters")
3148 .and_then(Value::as_array)
3149 .cloned()
3150 .unwrap_or_default(),
3151 template_body: body
3152 .get("TemplateBody")
3153 .and_then(Value::as_str)
3154 .map(String::from),
3155 template_s3_uri: body
3156 .get("TemplateS3Uri")
3157 .and_then(Value::as_str)
3158 .map(String::from),
3159 excluded_accounts: string_list(body, "ExcludedAccounts"),
3160 last_update_time: Utc::now(),
3161 },
3162 );
3163 Ok(AwsResponse::ok_json(
3164 json!({ "OrganizationConformancePackArn": out_arn }),
3165 ))
3166 }
3167
3168 fn describe_organization_conformance_packs(
3169 &self,
3170 account: &str,
3171 body: &Value,
3172 ) -> Result<AwsResponse, AwsServiceError> {
3173 let names = string_list(body, "OrganizationConformancePackNames");
3174 let st = self.state.read();
3175 let list: Vec<Value> = st
3176 .account(account)
3177 .map(|a| {
3178 a.org_conformance_packs
3179 .values()
3180 .filter(|p| names.is_empty() || names.contains(&p.name))
3181 .map(|p| {
3182 json!({
3183 "OrganizationConformancePackName": p.name,
3184 "OrganizationConformancePackArn": p.arn,
3185 "DeliveryS3Bucket": p.delivery_s3_bucket,
3186 "DeliveryS3KeyPrefix": p.delivery_s3_key_prefix,
3187 "ConformancePackInputParameters": p.input_parameters,
3188 "ExcludedAccounts": p.excluded_accounts,
3189 "LastUpdateTime": p.last_update_time.timestamp() as f64,
3190 })
3191 })
3192 .collect()
3193 })
3194 .unwrap_or_default();
3195 Ok(AwsResponse::ok_json(
3196 json!({ "OrganizationConformancePacks": list }),
3197 ))
3198 }
3199
3200 fn delete_organization_conformance_pack(
3201 &self,
3202 account: &str,
3203 body: &Value,
3204 ) -> Result<AwsResponse, AwsServiceError> {
3205 let name = require_str(body, "OrganizationConformancePackName")?;
3206 let mut st = self.state.write();
3207 let acc = st.account_mut(account);
3208 if acc.org_conformance_packs.remove(&name).is_none() {
3209 return Err(no_such(
3210 "NoSuchOrganizationConformancePackException",
3211 format!("Cannot find organization conformance pack '{name}'."),
3212 ));
3213 }
3214 Ok(AwsResponse::ok_json(json!({})))
3215 }
3216
3217 fn describe_organization_conformance_pack_statuses(
3218 &self,
3219 account: &str,
3220 body: &Value,
3221 ) -> Result<AwsResponse, AwsServiceError> {
3222 let names = string_list(body, "OrganizationConformancePackNames");
3223 let now = Utc::now().timestamp() as f64;
3224 let st = self.state.read();
3225 let list: Vec<Value> = st
3226 .account(account)
3227 .map(|a| {
3228 a.org_conformance_packs
3229 .values()
3230 .filter(|p| names.is_empty() || names.contains(&p.name))
3231 .map(|p| json!({ "OrganizationConformancePackName": p.name, "Status": "CREATE_SUCCESSFUL", "LastUpdateTime": now }))
3232 .collect()
3233 })
3234 .unwrap_or_default();
3235 Ok(AwsResponse::ok_json(
3236 json!({ "OrganizationConformancePackStatuses": list }),
3237 ))
3238 }
3239
3240 fn get_organization_conformance_pack_detailed_status(
3241 &self,
3242 account: &str,
3243 body: &Value,
3244 ) -> Result<AwsResponse, AwsServiceError> {
3245 let name = require_str(body, "OrganizationConformancePackName")?;
3246 let st = self.state.read();
3247 if !st
3248 .account(account)
3249 .map(|a| a.org_conformance_packs.contains_key(&name))
3250 .unwrap_or(false)
3251 {
3252 return Err(no_such(
3253 "NoSuchOrganizationConformancePackException",
3254 format!("Cannot find organization conformance pack '{name}'."),
3255 ));
3256 }
3257 Ok(AwsResponse::ok_json(
3258 json!({ "OrganizationConformancePackDetailedStatuses": [] }),
3259 ))
3260 }
3261}
3262
3263impl ConfigService {
3266 fn put_configuration_aggregator(
3267 &self,
3268 account: &str,
3269 region: &str,
3270 body: &Value,
3271 ) -> Result<AwsResponse, AwsServiceError> {
3272 let name = require_str(body, "ConfigurationAggregatorName")?;
3273 let now = Utc::now();
3274 let arn = format!(
3275 "arn:aws:config:{region}:{account}:config-aggregator/config-aggregator-{}",
3276 short_id()
3277 );
3278 let mut st = self.state.write();
3279 let acc = st.account_mut(account);
3280 let existing = acc.aggregators.get(&name);
3281 let agg = ConfigurationAggregator {
3282 name: name.clone(),
3283 arn: existing.map(|a| a.arn.clone()).unwrap_or(arn),
3284 account_aggregation_sources: body
3285 .get("AccountAggregationSources")
3286 .and_then(Value::as_array)
3287 .cloned()
3288 .unwrap_or_default(),
3289 organization_aggregation_source: body.get("OrganizationAggregationSource").cloned(),
3290 creation_time: existing.map(|a| a.creation_time).unwrap_or(now),
3291 last_updated_time: now,
3292 created_by: None,
3293 };
3294 let out = Self::aggregator_json(&agg);
3295 acc.aggregators.insert(name, agg);
3296 Ok(AwsResponse::ok_json(
3297 json!({ "ConfigurationAggregator": out }),
3298 ))
3299 }
3300
3301 fn aggregator_json(a: &ConfigurationAggregator) -> Value {
3302 json!({
3303 "ConfigurationAggregatorName": a.name,
3304 "ConfigurationAggregatorArn": a.arn,
3305 "AccountAggregationSources": a.account_aggregation_sources,
3306 "OrganizationAggregationSource": a.organization_aggregation_source,
3307 "CreationTime": a.creation_time.timestamp() as f64,
3308 "LastUpdatedTime": a.last_updated_time.timestamp() as f64,
3309 })
3310 }
3311
3312 fn describe_configuration_aggregators(
3313 &self,
3314 account: &str,
3315 body: &Value,
3316 ) -> Result<AwsResponse, AwsServiceError> {
3317 let names = string_list(body, "ConfigurationAggregatorNames");
3318 let st = self.state.read();
3319 let list: Vec<Value> = st
3320 .account(account)
3321 .map(|a| {
3322 a.aggregators
3323 .values()
3324 .filter(|g| names.is_empty() || names.contains(&g.name))
3325 .map(Self::aggregator_json)
3326 .collect()
3327 })
3328 .unwrap_or_default();
3329 if !names.is_empty() {
3330 for n in &names {
3331 if !st
3332 .account(account)
3333 .map(|a| a.aggregators.contains_key(n))
3334 .unwrap_or(false)
3335 {
3336 return Err(no_such(
3337 "NoSuchConfigurationAggregatorException",
3338 format!("The configuration aggregator '{n}' does not exist."),
3339 ));
3340 }
3341 }
3342 }
3343 Ok(paged_response(
3344 "ConfigurationAggregators",
3345 list,
3346 body,
3347 "NextToken",
3348 ))
3349 }
3350
3351 fn delete_configuration_aggregator(
3352 &self,
3353 account: &str,
3354 body: &Value,
3355 ) -> Result<AwsResponse, AwsServiceError> {
3356 let name = require_str(body, "ConfigurationAggregatorName")?;
3357 let mut st = self.state.write();
3358 let acc = st.account_mut(account);
3359 if acc.aggregators.remove(&name).is_none() {
3360 return Err(no_such(
3361 "NoSuchConfigurationAggregatorException",
3362 format!("The configuration aggregator '{name}' does not exist."),
3363 ));
3364 }
3365 Ok(AwsResponse::ok_json(json!({})))
3366 }
3367
3368 fn describe_configuration_aggregator_sources_status(
3369 &self,
3370 account: &str,
3371 body: &Value,
3372 ) -> Result<AwsResponse, AwsServiceError> {
3373 let name = require_str(body, "ConfigurationAggregatorName")?;
3374 let now = Utc::now().timestamp() as f64;
3375 let st = self.state.read();
3376 let agg = st.account(account).and_then(|a| a.aggregators.get(&name));
3377 let Some(agg) = agg else {
3378 return Err(no_such(
3379 "NoSuchConfigurationAggregatorException",
3380 format!("The configuration aggregator '{name}' does not exist."),
3381 ));
3382 };
3383 let mut list = Vec::new();
3384 for src in &agg.account_aggregation_sources {
3385 let region = src
3386 .get("AwsRegions")
3387 .and_then(Value::as_array)
3388 .and_then(|a| a.first())
3389 .and_then(Value::as_str)
3390 .unwrap_or("us-east-1");
3391 let accounts = src
3392 .get("AccountIds")
3393 .and_then(Value::as_array)
3394 .cloned()
3395 .unwrap_or_default();
3396 for acct in accounts {
3397 list.push(json!({
3398 "SourceId": acct,
3399 "SourceType": "ACCOUNT",
3400 "AwsRegion": region,
3401 "LastUpdateStatus": "SUCCEEDED",
3402 "LastUpdateTime": now,
3403 }));
3404 }
3405 }
3406 Ok(AwsResponse::ok_json(
3407 json!({ "AggregatedSourceStatusList": list }),
3408 ))
3409 }
3410
3411 fn put_aggregation_authorization(
3412 &self,
3413 account: &str,
3414 region: &str,
3415 body: &Value,
3416 ) -> Result<AwsResponse, AwsServiceError> {
3417 let authorized_account = require_str(body, "AuthorizedAccountId")?;
3418 let authorized_region = require_str(body, "AuthorizedAwsRegion")?;
3419 let now = Utc::now();
3420 let arn = format!("arn:aws:config:{region}:{account}:aggregation-authorization/{authorized_account}/{authorized_region}");
3421 let key = format!("{authorized_account}\u{1}{authorized_region}");
3422 let auth = AggregationAuthorization {
3423 arn: arn.clone(),
3424 authorized_account_id: authorized_account,
3425 authorized_aws_region: authorized_region,
3426 creation_time: now,
3427 };
3428 let out = Self::auth_json(&auth);
3429 let mut st = self.state.write();
3430 st.account_mut(account)
3431 .aggregation_authorizations
3432 .insert(key, auth);
3433 let _ = out;
3434 Ok(AwsResponse::ok_json(
3435 json!({ "AggregationAuthorization": Self::auth_json_by_arn(&arn) }),
3436 ))
3437 }
3438
3439 fn auth_json(a: &AggregationAuthorization) -> Value {
3440 json!({
3441 "AggregationAuthorizationArn": a.arn,
3442 "AuthorizedAccountId": a.authorized_account_id,
3443 "AuthorizedAwsRegion": a.authorized_aws_region,
3444 "CreationTime": a.creation_time.timestamp() as f64,
3445 })
3446 }
3447
3448 fn auth_json_by_arn(arn: &str) -> Value {
3449 json!({ "AggregationAuthorizationArn": arn })
3450 }
3451
3452 fn describe_aggregation_authorizations(
3453 &self,
3454 account: &str,
3455 body: &Value,
3456 ) -> Result<AwsResponse, AwsServiceError> {
3457 let st = self.state.read();
3458 let list: Vec<Value> = st
3459 .account(account)
3460 .map(|a| {
3461 a.aggregation_authorizations
3462 .values()
3463 .map(Self::auth_json)
3464 .collect()
3465 })
3466 .unwrap_or_default();
3467 Ok(paged_response(
3468 "AggregationAuthorizations",
3469 list,
3470 body,
3471 "NextToken",
3472 ))
3473 }
3474
3475 fn delete_aggregation_authorization(
3476 &self,
3477 account: &str,
3478 body: &Value,
3479 ) -> Result<AwsResponse, AwsServiceError> {
3480 let authorized_account = require_str(body, "AuthorizedAccountId")?;
3481 let authorized_region = require_str(body, "AuthorizedAwsRegion")?;
3482 let key = format!("{authorized_account}\u{1}{authorized_region}");
3483 let mut st = self.state.write();
3484 st.account_mut(account)
3485 .aggregation_authorizations
3486 .remove(&key);
3487 Ok(AwsResponse::ok_json(json!({})))
3488 }
3489
3490 fn batch_get_aggregate_resource_config(
3494 &self,
3495 account: &str,
3496 body: &Value,
3497 ) -> Result<AwsResponse, AwsServiceError> {
3498 let keys = body
3499 .get("ResourceIdentifiers")
3500 .and_then(Value::as_array)
3501 .cloned()
3502 .unwrap_or_default();
3503 let st = self.state.read();
3504 let acc = st.account(account);
3505 let mut items = Vec::new();
3506 let mut unprocessed = Vec::new();
3507 for k in keys {
3508 let rt = k
3509 .get("ResourceType")
3510 .and_then(Value::as_str)
3511 .unwrap_or_default();
3512 let rid = k
3513 .get("ResourceId")
3514 .and_then(Value::as_str)
3515 .unwrap_or_default();
3516 let key = resource_key(rt, rid);
3517 match acc
3518 .and_then(|a| a.config_items.get(&key))
3519 .and_then(|h| h.last())
3520 {
3521 Some(ci) => items.push(Self::config_item_json(ci)),
3522 None => unprocessed.push(k),
3523 }
3524 }
3525 Ok(AwsResponse::ok_json(
3526 json!({ "BaseConfigurationItems": items, "UnprocessedResourceIdentifiers": unprocessed }),
3527 ))
3528 }
3529
3530 fn get_aggregate_resource_config(
3531 &self,
3532 account: &str,
3533 body: &Value,
3534 ) -> Result<AwsResponse, AwsServiceError> {
3535 let ident = body
3536 .get("ResourceIdentifier")
3537 .cloned()
3538 .unwrap_or(Value::Null);
3539 let rt = ident
3540 .get("ResourceType")
3541 .and_then(Value::as_str)
3542 .unwrap_or_default();
3543 let rid = ident
3544 .get("ResourceId")
3545 .and_then(Value::as_str)
3546 .unwrap_or_default();
3547 let st = self.state.read();
3548 let key = resource_key(rt, rid);
3549 let ci = st
3550 .account(account)
3551 .and_then(|a| a.config_items.get(&key))
3552 .and_then(|h| h.last());
3553 match ci {
3554 Some(ci) => Ok(AwsResponse::ok_json(
3555 json!({ "ConfigurationItem": Self::config_item_json(ci) }),
3556 )),
3557 None => Err(no_such(
3558 "ResourceNotDiscoveredException",
3559 "Resource is not discovered by the aggregator.",
3560 )),
3561 }
3562 }
3563
3564 fn list_aggregate_discovered_resources(
3565 &self,
3566 account: &str,
3567 body: &Value,
3568 ) -> Result<AwsResponse, AwsServiceError> {
3569 let resource_type = require_str(body, "ResourceType")?;
3570 let st = self.state.read();
3571 let mut list = Vec::new();
3572 if let Some(acc) = st.account(account) {
3573 for (key, history) in &acc.config_items {
3574 let Some((rt, _)) = key.split_once('\u{1}') else {
3575 continue;
3576 };
3577 if rt != resource_type {
3578 continue;
3579 }
3580 let Some(ci) = history.last() else { continue };
3581 if ci.configuration_item_status.starts_with("ResourceDeleted") {
3582 continue;
3583 }
3584 list.push(json!({
3585 "SourceAccountId": account,
3586 "SourceRegion": ci.aws_region,
3587 "ResourceId": ci.resource_id,
3588 "ResourceType": ci.resource_type,
3589 "ResourceName": ci.resource_name,
3590 }));
3591 }
3592 }
3593 Ok(AwsResponse::ok_json(json!({ "ResourceIdentifiers": list })))
3594 }
3595
3596 fn get_aggregate_discovered_resource_counts(
3597 &self,
3598 account: &str,
3599 _body: &Value,
3600 ) -> Result<AwsResponse, AwsServiceError> {
3601 let st = self.state.read();
3602 let mut counts: std::collections::BTreeMap<String, i64> = std::collections::BTreeMap::new();
3603 if let Some(acc) = st.account(account) {
3604 for (key, history) in &acc.config_items {
3605 let Some((rt, _)) = key.split_once('\u{1}') else {
3606 continue;
3607 };
3608 if history
3609 .last()
3610 .map(|ci| ci.configuration_item_status.starts_with("ResourceDeleted"))
3611 .unwrap_or(true)
3612 {
3613 continue;
3614 }
3615 *counts.entry(rt.to_string()).or_insert(0) += 1;
3616 }
3617 }
3618 let total: i64 = counts.values().sum();
3619 let list: Vec<Value> = counts
3620 .into_iter()
3621 .map(|(t, c)| json!({ "ResourceType": t, "Count": c }))
3622 .collect();
3623 Ok(AwsResponse::ok_json(
3624 json!({ "TotalDiscoveredResources": total, "GroupedResourceCounts": list }),
3625 ))
3626 }
3627
3628 fn describe_aggregate_compliance_by_config_rules(
3629 &self,
3630 account: &str,
3631 _body: &Value,
3632 ) -> Result<AwsResponse, AwsServiceError> {
3633 let st = self.state.read();
3634 let mut list = Vec::new();
3635 if let Some(acc) = st.account(account) {
3636 for rule in acc.rules.values() {
3637 let c = acc
3638 .evaluations
3639 .get(&rule.name)
3640 .map(|m| fold_compliance(m.values().map(|r| r.compliance_type.as_str())))
3641 .unwrap_or_else(|| "INSUFFICIENT_DATA".into());
3642 list.push(json!({
3643 "ConfigRuleName": rule.name,
3644 "Compliance": { "ComplianceType": c },
3645 "AccountId": account,
3646 "AwsRegion": "us-east-1",
3647 }));
3648 }
3649 }
3650 Ok(AwsResponse::ok_json(
3651 json!({ "AggregateComplianceByConfigRules": list }),
3652 ))
3653 }
3654
3655 fn describe_aggregate_compliance_by_conformance_packs(
3656 &self,
3657 account: &str,
3658 _body: &Value,
3659 ) -> Result<AwsResponse, AwsServiceError> {
3660 let st = self.state.read();
3661 let list: Vec<Value> = st
3662 .account(account)
3663 .map(|a| {
3664 a.conformance_packs
3665 .values()
3666 .map(|p| {
3667 let rules = Self::pack_rule_names(a, p);
3668 let noncompliant = rules.iter().any(|r| a.evaluations.get(r).map(|m| m.values().any(|e| e.compliance_type == "NON_COMPLIANT")).unwrap_or(false));
3669 json!({
3670 "ConformancePackName": p.name,
3671 "Compliance": { "ComplianceType": if noncompliant { "NON_COMPLIANT" } else { "COMPLIANT" } },
3672 "AccountId": account,
3673 "AwsRegion": "us-east-1",
3674 })
3675 })
3676 .collect()
3677 })
3678 .unwrap_or_default();
3679 Ok(AwsResponse::ok_json(
3680 json!({ "AggregateComplianceByConformancePacks": list }),
3681 ))
3682 }
3683
3684 fn get_aggregate_compliance_details_by_config_rule(
3685 &self,
3686 account: &str,
3687 body: &Value,
3688 ) -> Result<AwsResponse, AwsServiceError> {
3689 let name = require_str(body, "ConfigRuleName")?;
3690 let st = self.state.read();
3691 let results: Vec<Value> = st
3692 .account(account)
3693 .and_then(|a| a.evaluations.get(&name))
3694 .map(|m| {
3695 m.values()
3696 .map(|r| {
3697 json!({
3698 "AccountId": account,
3699 "AwsRegion": "us-east-1",
3700 "ComplianceType": r.compliance_type,
3701 "ResultRecordedTime": r.result_recorded_time.timestamp() as f64,
3702 "ConfigRuleInvokedTime": r.config_rule_invoked_time.timestamp() as f64,
3703 "Annotation": r.annotation,
3704 "EvaluationResultIdentifier": {
3705 "EvaluationResultQualifier": {
3706 "ConfigRuleName": name,
3707 "ResourceType": r.resource_type,
3708 "ResourceId": r.resource_id,
3709 },
3710 "OrderingTimestamp": r.ordering_timestamp.timestamp() as f64,
3711 },
3712 })
3713 })
3714 .collect()
3715 })
3716 .unwrap_or_default();
3717 Ok(AwsResponse::ok_json(
3718 json!({ "AggregateEvaluationResults": results }),
3719 ))
3720 }
3721
3722 fn get_aggregate_config_rule_compliance_summary(
3723 &self,
3724 account: &str,
3725 _body: &Value,
3726 ) -> Result<AwsResponse, AwsServiceError> {
3727 let st = self.state.read();
3728 let mut compliant = 0;
3729 let mut noncompliant = 0;
3730 if let Some(acc) = st.account(account) {
3731 for rule in acc.rules.values() {
3732 match acc
3733 .evaluations
3734 .get(&rule.name)
3735 .map(|m| fold_compliance(m.values().map(|r| r.compliance_type.as_str())))
3736 .unwrap_or_else(|| "INSUFFICIENT_DATA".into())
3737 .as_str()
3738 {
3739 "COMPLIANT" => compliant += 1,
3740 "NON_COMPLIANT" => noncompliant += 1,
3741 _ => {}
3742 }
3743 }
3744 }
3745 Ok(AwsResponse::ok_json(json!({
3746 "GroupByKey": "ACCOUNT_ID",
3747 "AggregateComplianceCounts": [{
3748 "GroupName": account,
3749 "ComplianceSummary": {
3750 "CompliantResourceCount": { "CappedCount": compliant, "CapExceeded": false },
3751 "NonCompliantResourceCount": { "CappedCount": noncompliant, "CapExceeded": false },
3752 "ComplianceSummaryTimestamp": Utc::now().timestamp() as f64,
3753 }
3754 }]
3755 })))
3756 }
3757
3758 fn get_aggregate_conformance_pack_compliance_summary(
3759 &self,
3760 account: &str,
3761 _body: &Value,
3762 ) -> Result<AwsResponse, AwsServiceError> {
3763 let st = self.state.read();
3764 let mut compliant = 0;
3765 let mut noncompliant = 0;
3766 if let Some(acc) = st.account(account) {
3767 for p in acc.conformance_packs.values() {
3768 let rules = Self::pack_rule_names(acc, p);
3769 let nc = rules.iter().any(|r| {
3770 acc.evaluations
3771 .get(r)
3772 .map(|m| m.values().any(|e| e.compliance_type == "NON_COMPLIANT"))
3773 .unwrap_or(false)
3774 });
3775 if nc {
3776 noncompliant += 1;
3777 } else {
3778 compliant += 1;
3779 }
3780 }
3781 }
3782 Ok(AwsResponse::ok_json(json!({
3783 "GroupByKey": "ACCOUNT_ID",
3784 "AggregateConformancePackComplianceSummaries": [{
3785 "GroupName": account,
3786 "ComplianceSummary": {
3787 "CompliantConformancePackCount": compliant,
3788 "NonCompliantConformancePackCount": noncompliant,
3789 }
3790 }]
3791 })))
3792 }
3793}
3794
3795impl ConfigService {
3798 fn put_retention_configuration(
3799 &self,
3800 account: &str,
3801 body: &Value,
3802 ) -> Result<AwsResponse, AwsServiceError> {
3803 let days = body
3804 .get("RetentionPeriodInDays")
3805 .and_then(Value::as_i64)
3806 .ok_or_else(|| invalid("RetentionPeriodInDays is required"))?;
3807 if !(30..=2557).contains(&days) {
3808 return Err(invalid("RetentionPeriodInDays must be between 30 and 2557"));
3809 }
3810 let mut st = self.state.write();
3811 st.account_mut(account).retention_configurations.insert(
3812 "default".into(),
3813 RetentionConfiguration {
3814 name: "default".into(),
3815 retention_period_in_days: days,
3816 },
3817 );
3818 Ok(AwsResponse::ok_json(
3819 json!({ "RetentionConfiguration": { "Name": "default", "RetentionPeriodInDays": days } }),
3820 ))
3821 }
3822
3823 fn describe_retention_configurations(
3824 &self,
3825 account: &str,
3826 body: &Value,
3827 ) -> Result<AwsResponse, AwsServiceError> {
3828 let st = self.state.read();
3829 let list: Vec<Value> = st
3830 .account(account)
3831 .map(|a| a.retention_configurations.values().map(|r| json!({ "Name": r.name, "RetentionPeriodInDays": r.retention_period_in_days })).collect())
3832 .unwrap_or_default();
3833 Ok(paged_response(
3834 "RetentionConfigurations",
3835 list,
3836 body,
3837 "NextToken",
3838 ))
3839 }
3840
3841 fn delete_retention_configuration(
3842 &self,
3843 account: &str,
3844 body: &Value,
3845 ) -> Result<AwsResponse, AwsServiceError> {
3846 let name = require_str(body, "RetentionConfigurationName")?;
3847 let mut st = self.state.write();
3848 let acc = st.account_mut(account);
3849 if acc.retention_configurations.remove(&name).is_none() {
3850 return Err(no_such(
3851 "NoSuchRetentionConfigurationException",
3852 format!("Cannot find retention configuration with the specified name '{name}'."),
3853 ));
3854 }
3855 Ok(AwsResponse::ok_json(json!({})))
3856 }
3857
3858 fn put_stored_query(
3859 &self,
3860 account: &str,
3861 region: &str,
3862 body: &Value,
3863 ) -> Result<AwsResponse, AwsServiceError> {
3864 let q = body
3865 .get("StoredQuery")
3866 .cloned()
3867 .ok_or_else(|| invalid("StoredQuery is required"))?;
3868 let name = q
3869 .get("QueryName")
3870 .and_then(Value::as_str)
3871 .ok_or_else(|| invalid("QueryName is required"))?
3872 .to_string();
3873 let mut st = self.state.write();
3874 let acc = st.account_mut(account);
3875 let existing = acc.stored_queries.get(&name);
3876 let id = existing
3877 .map(|s| s.id.clone())
3878 .unwrap_or_else(|| Uuid::new_v4().to_string());
3879 let arn = existing.map(|s| s.arn.clone()).unwrap_or_else(|| {
3880 format!("arn:aws:config:{region}:{account}:stored-query/{name}/{id}")
3881 });
3882 acc.stored_queries.insert(
3883 name.clone(),
3884 StoredQuery {
3885 id: id.clone(),
3886 name,
3887 arn: arn.clone(),
3888 description: q
3889 .get("Description")
3890 .and_then(Value::as_str)
3891 .map(String::from),
3892 expression: q
3893 .get("Expression")
3894 .and_then(Value::as_str)
3895 .map(String::from),
3896 },
3897 );
3898 let _ = id;
3899 Ok(AwsResponse::ok_json(json!({ "QueryArn": arn })))
3900 }
3901
3902 fn get_stored_query(
3903 &self,
3904 account: &str,
3905 body: &Value,
3906 ) -> Result<AwsResponse, AwsServiceError> {
3907 let name = require_str(body, "QueryName")?;
3908 let st = self.state.read();
3909 let q = st
3910 .account(account)
3911 .and_then(|a| a.stored_queries.get(&name));
3912 let Some(q) = q else {
3913 return Err(no_such(
3914 "ResourceNotFoundException",
3915 format!("The stored query '{name}' does not exist."),
3916 ));
3917 };
3918 Ok(AwsResponse::ok_json(json!({
3919 "StoredQuery": {
3920 "QueryId": q.id,
3921 "QueryArn": q.arn,
3922 "QueryName": q.name,
3923 "Description": q.description,
3924 "Expression": q.expression,
3925 }
3926 })))
3927 }
3928
3929 fn delete_stored_query(
3930 &self,
3931 account: &str,
3932 body: &Value,
3933 ) -> Result<AwsResponse, AwsServiceError> {
3934 let name = require_str(body, "QueryName")?;
3935 let mut st = self.state.write();
3936 let acc = st.account_mut(account);
3937 if acc.stored_queries.remove(&name).is_none() {
3938 return Err(no_such(
3939 "ResourceNotFoundException",
3940 format!("The stored query '{name}' does not exist."),
3941 ));
3942 }
3943 Ok(AwsResponse::ok_json(json!({})))
3944 }
3945
3946 fn list_stored_queries(
3947 &self,
3948 account: &str,
3949 body: &Value,
3950 ) -> Result<AwsResponse, AwsServiceError> {
3951 let st = self.state.read();
3952 let list: Vec<Value> = st
3953 .account(account)
3954 .map(|a| a.stored_queries.values().map(|q| json!({ "QueryId": q.id, "QueryArn": q.arn, "QueryName": q.name, "Description": q.description })).collect())
3955 .unwrap_or_default();
3956 Ok(paged_response(
3957 "StoredQueryMetadata",
3958 list,
3959 body,
3960 "NextToken",
3961 ))
3962 }
3963
3964 fn start_resource_evaluation(
3965 &self,
3966 account: &str,
3967 body: &Value,
3968 ) -> Result<AwsResponse, AwsServiceError> {
3969 let mode = body
3970 .get("EvaluationMode")
3971 .and_then(Value::as_str)
3972 .unwrap_or("DETECTIVE")
3973 .to_string();
3974 let details = body.get("ResourceDetails").cloned();
3975 let (rtype, rid, config_str) = details
3978 .as_ref()
3979 .map(|d| {
3980 let rt = d
3981 .get("ResourceType")
3982 .and_then(Value::as_str)
3983 .unwrap_or_default()
3984 .to_string();
3985 let ri = d
3986 .get("ResourceId")
3987 .and_then(Value::as_str)
3988 .unwrap_or_default()
3989 .to_string();
3990 let cfg = match d.get("ResourceConfiguration") {
3993 Some(Value::String(s)) => s.clone(),
3994 Some(v) => v.to_string(),
3995 None => "null".to_string(),
3996 };
3997 (rt, ri, cfg)
3998 })
3999 .unwrap_or_default();
4000 let id = Uuid::new_v4().to_string();
4001 let mut st = self.state.write();
4002 let acc = st.account_mut(account);
4003 let evaluation_results = if rtype.is_empty() || rid.is_empty() {
4004 Vec::new()
4005 } else {
4006 validate::evaluate_proactive(acc, &rtype, &rid, &config_str)
4007 };
4008 acc.resource_evaluations.insert(
4009 id.clone(),
4010 ResourceEvaluation {
4011 resource_evaluation_id: id.clone(),
4012 evaluation_mode: mode,
4013 time_stamp: Utc::now(),
4014 status: "SUCCEEDED".into(),
4015 resource_details: details,
4016 evaluation_context: body.get("EvaluationContext").cloned(),
4017 evaluation_results,
4018 },
4019 );
4020 Ok(AwsResponse::ok_json(json!({ "ResourceEvaluationId": id })))
4021 }
4022
4023 fn get_resource_evaluation_summary(
4024 &self,
4025 account: &str,
4026 body: &Value,
4027 ) -> Result<AwsResponse, AwsServiceError> {
4028 let id = require_str(body, "ResourceEvaluationId")?;
4029 let st = self.state.read();
4030 let ev = st
4031 .account(account)
4032 .and_then(|a| a.resource_evaluations.get(&id));
4033 let Some(ev) = ev else {
4034 return Err(no_such(
4035 "ResourceNotFoundException",
4036 format!("ResourceEvaluationId '{id}' does not exist."),
4037 ));
4038 };
4039 let resource_type = ev
4040 .resource_details
4041 .as_ref()
4042 .and_then(|d| d.get("ResourceType"))
4043 .cloned()
4044 .unwrap_or(Value::Null);
4045 let resource_id = ev
4046 .resource_details
4047 .as_ref()
4048 .and_then(|d| d.get("ResourceId"))
4049 .cloned()
4050 .unwrap_or(Value::Null);
4051 let compliance = if ev.evaluation_results.is_empty() {
4054 "INSUFFICIENT_DATA".to_string()
4055 } else {
4056 fold_compliance(
4057 ev.evaluation_results
4058 .iter()
4059 .map(|r| r.compliance_type.as_str()),
4060 )
4061 };
4062 Ok(AwsResponse::ok_json(json!({
4063 "ResourceEvaluationId": ev.resource_evaluation_id,
4064 "EvaluationMode": ev.evaluation_mode,
4065 "EvaluationStatus": { "Status": ev.status },
4066 "EvaluationStartTimestamp": ev.time_stamp.timestamp() as f64,
4067 "Compliance": compliance,
4068 "ResourceDetails": { "ResourceId": resource_id, "ResourceType": resource_type },
4069 })))
4070 }
4071
4072 fn list_resource_evaluations(
4073 &self,
4074 account: &str,
4075 body: &Value,
4076 ) -> Result<AwsResponse, AwsServiceError> {
4077 let st = self.state.read();
4078 let list: Vec<Value> = st
4079 .account(account)
4080 .map(|a| {
4081 a.resource_evaluations
4082 .values()
4083 .map(|e| json!({ "ResourceEvaluationId": e.resource_evaluation_id, "EvaluationMode": e.evaluation_mode, "EvaluationStartTimestamp": e.time_stamp.timestamp() as f64 }))
4084 .collect()
4085 })
4086 .unwrap_or_default();
4087 Ok(paged_response(
4088 "ResourceEvaluations",
4089 list,
4090 body,
4091 "NextToken",
4092 ))
4093 }
4094
4095 fn select_resource_config(
4096 &self,
4097 account: &str,
4098 body: &Value,
4099 aggregate: bool,
4100 ) -> Result<AwsResponse, AwsServiceError> {
4101 let expr = require_str(body, "Expression")?;
4102 let st = self.state.read();
4103 let empty = AccountState::default();
4104 let acc = st.account(account).unwrap_or(&empty);
4105 let rows = validate::run_select(&expr, acc)
4106 .map_err(|e| invalid(format!("Invalid SelectResourceConfig expression: {e}")))?;
4107 let results: Vec<Value> = rows.iter().map(|r| json!(r.to_string())).collect();
4108 let (page, next) = paginate(results, body);
4109 let mut out = json!({
4110 "Results": page,
4111 "QueryInfo": { "SelectFields": select_fields(&expr) },
4112 });
4113 if let Some(t) = next {
4114 out["NextToken"] = json!(t);
4115 }
4116 if aggregate {
4117 let _ = out.as_object_mut();
4118 }
4119 Ok(AwsResponse::ok_json(out))
4120 }
4121
4122 fn tag_resource(&self, account: &str, body: &Value) -> Result<AwsResponse, AwsServiceError> {
4123 let arn = require_str(body, "ResourceArn")?;
4124 let tags = body
4125 .get("Tags")
4126 .and_then(Value::as_array)
4127 .cloned()
4128 .unwrap_or_default();
4129 let mut st = self.state.write();
4130 let acc = st.account_mut(account);
4131 let entry = acc.tags.entry(arn).or_default();
4132 for t in tags {
4133 if let (Some(k), Some(v)) = (
4134 t.get("Key").and_then(Value::as_str),
4135 t.get("Value").and_then(Value::as_str),
4136 ) {
4137 entry.insert(k.to_string(), v.to_string());
4138 }
4139 }
4140 Ok(AwsResponse::ok_json(json!({})))
4141 }
4142
4143 fn untag_resource(&self, account: &str, body: &Value) -> Result<AwsResponse, AwsServiceError> {
4144 let arn = require_str(body, "ResourceArn")?;
4145 let keys = string_list(body, "TagKeys");
4146 let mut st = self.state.write();
4147 let acc = st.account_mut(account);
4148 if let Some(entry) = acc.tags.get_mut(&arn) {
4149 for k in keys {
4150 entry.remove(&k);
4151 }
4152 }
4153 Ok(AwsResponse::ok_json(json!({})))
4154 }
4155
4156 fn list_tags_for_resource(
4157 &self,
4158 account: &str,
4159 body: &Value,
4160 ) -> Result<AwsResponse, AwsServiceError> {
4161 let arn = require_str(body, "ResourceArn")?;
4162 let st = self.state.read();
4163 let list: Vec<Value> = st
4164 .account(account)
4165 .and_then(|a| a.tags.get(&arn))
4166 .map(|m| {
4167 m.iter()
4168 .map(|(k, v)| json!({ "Key": k, "Value": v }))
4169 .collect()
4170 })
4171 .unwrap_or_default();
4172 Ok(AwsResponse::ok_json(json!({ "Tags": list })))
4173 }
4174}
4175
4176fn account_id(req: &AwsRequest) -> String {
4179 if req.account_id.is_empty() {
4180 "000000000000".to_string()
4181 } else {
4182 req.account_id.clone()
4183 }
4184}
4185
4186fn region(req: &AwsRequest) -> String {
4187 if req.region.is_empty() {
4188 "us-east-1".to_string()
4189 } else {
4190 req.region.clone()
4191 }
4192}
4193
4194fn short_id() -> String {
4195 fakecloud_core::ids::short_id(6)
4196}
4197
4198fn invalid(msg: impl Into<String>) -> AwsServiceError {
4199 AwsServiceError::aws_error(
4200 StatusCode::BAD_REQUEST,
4201 "InvalidParameterValueException",
4202 msg,
4203 )
4204}
4205
4206fn no_such(code: &str, msg: impl Into<String>) -> AwsServiceError {
4207 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, code, msg)
4208}
4209
4210fn require_str(body: &Value, field: &str) -> Result<String, AwsServiceError> {
4211 body.get(field)
4212 .and_then(Value::as_str)
4213 .filter(|s| !s.is_empty())
4214 .map(String::from)
4215 .ok_or_else(|| invalid(format!("{field} is required")))
4216}
4217
4218fn string_list(body: &Value, field: &str) -> Vec<String> {
4219 body.get(field)
4220 .and_then(Value::as_array)
4221 .map(|a| {
4222 a.iter()
4223 .filter_map(|v| v.as_str().map(String::from))
4224 .collect()
4225 })
4226 .unwrap_or_default()
4227}
4228
4229fn page_limit(body: &Value) -> Option<usize> {
4233 ["Limit", "MaxResults", "limit"]
4234 .iter()
4235 .find_map(|k| body.get(*k).and_then(Value::as_i64))
4236 .filter(|l| *l > 0)
4237 .map(|l| l as usize)
4238}
4239
4240fn page_token(body: &Value) -> Option<usize> {
4243 ["NextToken", "nextToken"]
4244 .iter()
4245 .find_map(|k| body.get(*k).and_then(Value::as_str))
4246 .filter(|s| !s.is_empty())
4247 .and_then(decode_page_token)
4248}
4249
4250fn encode_page_token(offset: usize) -> String {
4254 format!("cfg:{offset}")
4255}
4256
4257fn decode_page_token(token: &str) -> Option<usize> {
4258 token.strip_prefix("cfg:").and_then(|n| n.parse().ok())
4259}
4260
4261fn paginate(items: Vec<Value>, body: &Value) -> (Vec<Value>, Option<String>) {
4264 let total = items.len();
4265 let start = page_token(body).unwrap_or(0).min(total);
4266 let end = match page_limit(body) {
4267 Some(limit) => start.saturating_add(limit).min(total),
4268 None => total,
4269 };
4270 let next = if end < total {
4271 Some(encode_page_token(end))
4272 } else {
4273 None
4274 };
4275 (
4276 items.into_iter().skip(start).take(end - start).collect(),
4277 next,
4278 )
4279}
4280
4281fn paged_response(field: &str, items: Vec<Value>, body: &Value, token_field: &str) -> AwsResponse {
4285 let (page, next) = paginate(items, body);
4286 let mut out = json!({ field: page });
4287 if let Some(t) = next {
4288 out[token_field] = json!(t);
4289 }
4290 AwsResponse::ok_json(out)
4291}
4292
4293fn custom_rule_event(
4298 account: &str,
4299 rule_name: &str,
4300 input_parameters: Option<&str>,
4301 result_token: &str,
4302) -> Value {
4303 let invoking_event = json!({
4304 "awsAccountId": account,
4305 "configRuleName": rule_name,
4306 "messageType": "ScheduledNotification",
4307 "notificationCreationTime": Utc::now().to_rfc3339(),
4308 })
4309 .to_string();
4310 let rule_parameters = input_parameters
4311 .filter(|p| !p.is_empty())
4312 .map(String::from)
4313 .unwrap_or_else(|| "{}".to_string());
4314 json!({
4315 "invokingEvent": invoking_event,
4316 "ruleParameters": rule_parameters,
4317 "resultToken": result_token,
4318 "configRuleArn": format!("arn:aws:config:us-east-1:{account}:config-rule/{rule_name}"),
4319 "configRuleName": rule_name,
4320 "accountId": account,
4321 })
4322}
4323
4324fn select_fields(expr: &str) -> Vec<Value> {
4326 let lower = expr.to_ascii_lowercase();
4327 let Some(start) = lower.find("select ") else {
4328 return Vec::new();
4329 };
4330 let after = &expr[start + 7..];
4331 let end = after
4332 .to_ascii_lowercase()
4333 .find(" where ")
4334 .unwrap_or(after.len());
4335 after[..end]
4336 .split(',')
4337 .map(|s| json!({ "Name": s.trim() }))
4338 .collect()
4339}
4340
4341#[cfg(test)]
4342mod unit_tests {
4343 use super::*;
4344
4345 #[test]
4346 fn custom_rule_event_passes_stored_input_parameters() {
4347 let params = r#"{"maxAccessKeyAge":"90"}"#;
4348 let event = custom_rule_event("111122223333", "my-rule", Some(params), "my-rule#tok");
4349 assert_eq!(event["ruleParameters"], json!(params));
4352 assert_eq!(event["configRuleName"], json!("my-rule"));
4353 assert_eq!(event["accountId"], json!("111122223333"));
4354 assert_eq!(event["resultToken"], json!("my-rule#tok"));
4355 }
4356
4357 #[test]
4358 fn custom_rule_event_defaults_empty_parameters_to_empty_object() {
4359 let none = custom_rule_event("111122223333", "r", None, "r#t");
4360 assert_eq!(none["ruleParameters"], json!("{}"));
4361 let empty = custom_rule_event("111122223333", "r", Some(""), "r#t");
4362 assert_eq!(empty["ruleParameters"], json!("{}"));
4363 }
4364
4365 #[test]
4366 fn paginate_slices_and_emits_token() {
4367 let items: Vec<Value> = (0..5).map(|i| json!(i)).collect();
4368 let first_req = json!({ "Limit": 2 });
4369 let (page, next) = paginate(items.clone(), &first_req);
4370 assert_eq!(page, vec![json!(0), json!(1)]);
4371 let token = next.expect("more items remain -> token");
4372
4373 let second_req = json!({ "Limit": 2, "NextToken": token });
4374 let (page2, next2) = paginate(items.clone(), &second_req);
4375 assert_eq!(page2, vec![json!(2), json!(3)]);
4376 let token2 = next2.expect("still more");
4377
4378 let third_req = json!({ "Limit": 2, "NextToken": token2 });
4379 let (page3, next3) = paginate(items, &third_req);
4380 assert_eq!(page3, vec![json!(4)]);
4381 assert!(next3.is_none(), "last page has no token");
4382 }
4383
4384 #[test]
4385 fn paginate_no_limit_returns_all() {
4386 let items: Vec<Value> = (0..3).map(|i| json!(i)).collect();
4387 let (page, next) = paginate(items.clone(), &json!({}));
4388 assert_eq!(page, items);
4389 assert!(next.is_none());
4390 }
4391
4392 #[test]
4393 fn extract_pack_rule_names_parses_yaml_template() {
4394 let yaml = r#"
4395Resources:
4396 MyRule:
4397 Type: AWS::Config::ConfigRule
4398 Properties:
4399 ConfigRuleName: yaml-declared-rule
4400 Source:
4401 Owner: AWS
4402 SourceIdentifier: S3_BUCKET_VERSIONING_ENABLED
4403"#;
4404 let names = extract_pack_rule_names(Some(yaml));
4405 assert_eq!(names, vec!["yaml-declared-rule".to_string()]);
4406 }
4407}