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.clone(), cfg_rule);
1540 let arn_for_tags = acc.rules[&name].arn.clone();
1541 let tags = parse_create_tags(body);
1542 if !tags.is_empty() {
1543 let entry = acc.tags.entry(arn_for_tags).or_default();
1544 for (k, v) in tags {
1545 entry.insert(k, v);
1546 }
1547 }
1548 Ok(AwsResponse::ok_json(json!({})))
1549 }
1550
1551 fn rule_json(r: &ConfigRule) -> Value {
1552 let mut v = json!({
1553 "ConfigRuleName": r.name,
1554 "ConfigRuleArn": r.arn,
1555 "ConfigRuleId": r.rule_id,
1556 "Source": r.source,
1557 "ConfigRuleState": r.state,
1558 });
1559 if let Some(d) = &r.description {
1560 v["Description"] = json!(d);
1561 }
1562 if let Some(s) = &r.scope {
1563 v["Scope"] = s.clone();
1564 }
1565 if let Some(p) = &r.input_parameters {
1566 v["InputParameters"] = json!(p);
1567 }
1568 if let Some(f) = &r.maximum_execution_frequency {
1569 v["MaximumExecutionFrequency"] = json!(f);
1570 }
1571 if let Some(c) = &r.created_by {
1572 v["CreatedBy"] = json!(c);
1573 }
1574 if let Some(m) = &r.evaluation_modes {
1575 v["EvaluationModes"] = m.clone();
1576 }
1577 v
1578 }
1579
1580 fn describe_config_rules(
1581 &self,
1582 account: &str,
1583 body: &Value,
1584 ) -> Result<AwsResponse, AwsServiceError> {
1585 let names = string_list(body, "ConfigRuleNames");
1586 let st = self.state.read();
1587 let list: Vec<Value> = st
1588 .account(account)
1589 .map(|a| {
1590 a.rules
1591 .values()
1592 .filter(|r| names.is_empty() || names.contains(&r.name))
1593 .map(Self::rule_json)
1594 .collect()
1595 })
1596 .unwrap_or_default();
1597 if !names.is_empty() {
1598 for n in &names {
1599 if !st
1600 .account(account)
1601 .map(|a| a.rules.contains_key(n))
1602 .unwrap_or(false)
1603 {
1604 return Err(no_such(
1605 "NoSuchConfigRuleException",
1606 format!("The ConfigRule '{n}' provided in the request is invalid."),
1607 ));
1608 }
1609 }
1610 }
1611 Ok(paged_response("ConfigRules", list, body, "NextToken"))
1612 }
1613
1614 fn delete_config_rule(
1615 &self,
1616 account: &str,
1617 body: &Value,
1618 ) -> Result<AwsResponse, AwsServiceError> {
1619 let name = require_str(body, "ConfigRuleName")?;
1620 let mut st = self.state.write();
1621 let acc = st.account_mut(account);
1622 if acc.rules.remove(&name).is_none() {
1623 return Err(no_such(
1624 "NoSuchConfigRuleException",
1625 format!("The ConfigRule '{name}' provided in the request is invalid."),
1626 ));
1627 }
1628 acc.evaluations.remove(&name);
1629 acc.remediation_configs.remove(&name);
1630 acc.remediation_executions
1631 .retain(|_, e| e.config_rule_name != name);
1632 Ok(AwsResponse::ok_json(json!({})))
1633 }
1634
1635 fn describe_config_rule_evaluation_status(
1636 &self,
1637 account: &str,
1638 body: &Value,
1639 ) -> Result<AwsResponse, AwsServiceError> {
1640 let names = string_list(body, "ConfigRuleNames");
1641 let st = self.state.read();
1642 let list: Vec<Value> = st
1643 .account(account)
1644 .map(|a| {
1645 a.rules
1646 .values()
1647 .filter(|r| names.is_empty() || names.contains(&r.name))
1648 .map(|r| {
1649 json!({
1650 "ConfigRuleName": r.name,
1651 "ConfigRuleArn": r.arn,
1652 "ConfigRuleId": r.rule_id,
1653 "LastSuccessfulInvocationTime": r.last_successful_invocation_time.map(|t| t.timestamp() as f64),
1654 "LastSuccessfulEvaluationTime": r.last_successful_evaluation_time.map(|t| t.timestamp() as f64),
1655 "FirstActivatedTime": r.first_activated_time.map(|t| t.timestamp() as f64),
1656 "FirstEvaluationStarted": r.first_activated_time.is_some(),
1657 })
1658 })
1659 .collect()
1660 })
1661 .unwrap_or_default();
1662 Ok(paged_response(
1663 "ConfigRulesEvaluationStatus",
1664 list,
1665 body,
1666 "NextToken",
1667 ))
1668 }
1669
1670 async fn start_config_rules_evaluation(
1671 &self,
1672 account: &str,
1673 _region: &str,
1674 body: &Value,
1675 ) -> Result<AwsResponse, AwsServiceError> {
1676 self.ensure_evaluated(account);
1677 let names = string_list(body, "ConfigRuleNames");
1679 let custom_rules: Vec<(String, String, Option<String>)> = {
1680 let st = self.state.read();
1681 st.account(account)
1682 .map(|a| {
1683 a.rules
1684 .values()
1685 .filter(|r| names.is_empty() || names.contains(&r.name))
1686 .filter_map(|r| {
1687 let owner = r.source.get("Owner").and_then(Value::as_str).unwrap_or("");
1688 if owner != "CUSTOM_LAMBDA" {
1689 return None;
1690 }
1691 let lambda_arn = r
1692 .source
1693 .get("SourceIdentifier")
1694 .and_then(Value::as_str)?
1695 .to_string();
1696 Some((r.name.clone(), lambda_arn, r.input_parameters.clone()))
1697 })
1698 .collect()
1699 })
1700 .unwrap_or_default()
1701 };
1702 for (rule_name, lambda_arn, input_parameters) in custom_rules {
1703 self.invoke_custom_rule(
1704 account,
1705 &rule_name,
1706 &lambda_arn,
1707 input_parameters.as_deref(),
1708 )
1709 .await;
1710 }
1711 Ok(AwsResponse::ok_json(json!({})))
1712 }
1713
1714 async fn invoke_custom_rule(
1719 &self,
1720 account: &str,
1721 rule_name: &str,
1722 lambda_arn: &str,
1723 input_parameters: Option<&str>,
1724 ) {
1725 let (Some(lambda_state), Some(runtime)) =
1726 (self.lambda_state.clone(), self.container_runtime.clone())
1727 else {
1728 return;
1729 };
1730 let func_name = lambda_arn
1731 .rsplit(':')
1732 .next()
1733 .unwrap_or(lambda_arn)
1734 .to_string();
1735 let result_token = format!("{rule_name}#{}", Uuid::new_v4());
1736 let event = custom_rule_event(account, rule_name, input_parameters, &result_token);
1737 let resolved = {
1740 let accounts = lambda_state.read();
1741 accounts
1742 .get(account)
1743 .and_then(|state| state.functions.get(&func_name).cloned())
1744 };
1745 let Some(func) = resolved else {
1746 tracing::warn!(function = %func_name, account = %account, "Config custom rule Lambda not found");
1747 return;
1748 };
1749 let payload = event.to_string().into_bytes();
1750 match runtime.invoke(&func, &payload, &[]).await {
1751 Ok(resp) => {
1752 if let Ok(v) = serde_json::from_slice::<Value>(&resp) {
1754 if let Some(arr) = v
1755 .as_array()
1756 .or_else(|| v.get("evaluations").and_then(Value::as_array))
1757 {
1758 self.record_evaluations(account, rule_name, arr);
1759 }
1760 }
1761 }
1762 Err(e) => {
1763 tracing::warn!(function = %func_name, error = %e, "Config custom rule Lambda invocation failed")
1764 }
1765 }
1766 }
1767
1768 fn record_evaluations(&self, account: &str, rule_name: &str, evals: &[Value]) {
1769 let now = Utc::now();
1770 let mut st = self.state.write();
1771 let acc = st.account_mut(account);
1772 let entry = acc.evaluations.entry(rule_name.to_string()).or_default();
1773 for e in evals {
1774 let rt = e
1775 .get("ComplianceResourceType")
1776 .and_then(Value::as_str)
1777 .unwrap_or_default()
1778 .to_string();
1779 let rid = e
1780 .get("ComplianceResourceId")
1781 .and_then(Value::as_str)
1782 .unwrap_or_default()
1783 .to_string();
1784 let ct = e
1785 .get("ComplianceType")
1786 .and_then(Value::as_str)
1787 .unwrap_or("INSUFFICIENT_DATA")
1788 .to_string();
1789 let key = resource_key(&rt, &rid);
1790 entry.insert(
1791 key,
1792 EvaluationResult {
1793 resource_type: rt,
1794 resource_id: rid,
1795 rule_name: rule_name.to_string(),
1796 compliance_type: ct,
1797 annotation: e
1798 .get("Annotation")
1799 .and_then(Value::as_str)
1800 .map(String::from),
1801 result_recorded_time: now,
1802 config_rule_invoked_time: now,
1803 ordering_timestamp: now,
1804 },
1805 );
1806 }
1807 if let Some(rule) = acc.rules.get_mut(rule_name) {
1808 rule.last_successful_evaluation_time = Some(now);
1809 rule.last_successful_invocation_time = Some(now);
1810 if rule.first_activated_time.is_none() {
1811 rule.first_activated_time = Some(now);
1812 }
1813 }
1814 }
1815
1816 fn put_evaluations(&self, account: &str, body: &Value) -> Result<AwsResponse, AwsServiceError> {
1817 let token = require_str(body, "ResultToken")?;
1818 let rule_name = token.split('#').next().unwrap_or(&token).to_string();
1819 let evals = body
1820 .get("Evaluations")
1821 .and_then(Value::as_array)
1822 .cloned()
1823 .unwrap_or_default();
1824 let test_mode = body
1825 .get("TestMode")
1826 .and_then(Value::as_bool)
1827 .unwrap_or(false);
1828 if !test_mode {
1829 let now = Utc::now();
1830 let mut st = self.state.write();
1831 let acc = st.account_mut(account);
1832 let entry = acc.evaluations.entry(rule_name.clone()).or_default();
1833 for e in &evals {
1834 let rt = e
1835 .get("ComplianceResourceType")
1836 .and_then(Value::as_str)
1837 .unwrap_or_default()
1838 .to_string();
1839 let rid = e
1840 .get("ComplianceResourceId")
1841 .and_then(Value::as_str)
1842 .unwrap_or_default()
1843 .to_string();
1844 let ct = e
1845 .get("ComplianceType")
1846 .and_then(Value::as_str)
1847 .unwrap_or("INSUFFICIENT_DATA")
1848 .to_string();
1849 let key = resource_key(&rt, &rid);
1850 entry.insert(
1851 key,
1852 EvaluationResult {
1853 resource_type: rt,
1854 resource_id: rid,
1855 rule_name: rule_name.clone(),
1856 compliance_type: ct,
1857 annotation: e
1858 .get("Annotation")
1859 .and_then(Value::as_str)
1860 .map(String::from),
1861 result_recorded_time: now,
1862 config_rule_invoked_time: now,
1863 ordering_timestamp: now,
1864 },
1865 );
1866 }
1867 }
1868 Ok(AwsResponse::ok_json(json!({ "FailedEvaluations": [] })))
1869 }
1870
1871 fn put_external_evaluation(
1872 &self,
1873 account: &str,
1874 body: &Value,
1875 ) -> Result<AwsResponse, AwsServiceError> {
1876 let rule_name = require_str(body, "ConfigRuleName")?;
1877 let e = body
1878 .get("ExternalEvaluation")
1879 .cloned()
1880 .ok_or_else(|| invalid("ExternalEvaluation is required"))?;
1881 let rt = e
1882 .get("ComplianceResourceType")
1883 .and_then(Value::as_str)
1884 .unwrap_or_default()
1885 .to_string();
1886 let rid = e
1887 .get("ComplianceResourceId")
1888 .and_then(Value::as_str)
1889 .unwrap_or_default()
1890 .to_string();
1891 let ct = e
1892 .get("ComplianceType")
1893 .and_then(Value::as_str)
1894 .unwrap_or("INSUFFICIENT_DATA")
1895 .to_string();
1896 let now = Utc::now();
1897 let mut st = self.state.write();
1898 let acc = st.account_mut(account);
1899 let key = resource_key(&rt, &rid);
1900 acc.evaluations
1901 .entry(rule_name.clone())
1902 .or_default()
1903 .insert(
1904 key,
1905 EvaluationResult {
1906 resource_type: rt,
1907 resource_id: rid,
1908 rule_name,
1909 compliance_type: ct,
1910 annotation: e
1911 .get("Annotation")
1912 .and_then(Value::as_str)
1913 .map(String::from),
1914 result_recorded_time: now,
1915 config_rule_invoked_time: now,
1916 ordering_timestamp: now,
1917 },
1918 );
1919 Ok(AwsResponse::ok_json(json!({})))
1920 }
1921
1922 fn delete_evaluation_results(
1923 &self,
1924 account: &str,
1925 body: &Value,
1926 ) -> Result<AwsResponse, AwsServiceError> {
1927 let name = require_str(body, "ConfigRuleName")?;
1928 let mut st = self.state.write();
1929 st.account_mut(account).evaluations.remove(&name);
1930 Ok(AwsResponse::ok_json(json!({})))
1931 }
1932
1933 fn get_custom_rule_policy(
1934 &self,
1935 account: &str,
1936 body: &Value,
1937 ) -> Result<AwsResponse, AwsServiceError> {
1938 let name = require_str(body, "ConfigRuleName")?;
1939 let st = self.state.read();
1940 let policy = st
1941 .account(account)
1942 .and_then(|a| a.rules.get(&name))
1943 .and_then(|r| {
1944 r.source
1945 .pointer("/CustomPolicyDetails/PolicyText")
1946 .and_then(Value::as_str)
1947 .map(String::from)
1948 });
1949 Ok(AwsResponse::ok_json(json!({ "PolicyText": policy })))
1950 }
1951}
1952
1953fn fold_compliance<'a>(types: impl Iterator<Item = &'a str>) -> String {
1959 let mut any_compliant = false;
1960 let mut any_noncompliant = false;
1961 for t in types {
1962 match t {
1963 "NON_COMPLIANT" => any_noncompliant = true,
1964 "COMPLIANT" => any_compliant = true,
1965 _ => {}
1966 }
1967 }
1968 if any_noncompliant {
1969 "NON_COMPLIANT".into()
1970 } else if any_compliant {
1971 "COMPLIANT".into()
1972 } else {
1973 "INSUFFICIENT_DATA".into()
1974 }
1975}
1976
1977impl ConfigService {
1978 fn describe_compliance_by_config_rule(
1979 &self,
1980 account: &str,
1981 body: &Value,
1982 ) -> Result<AwsResponse, AwsServiceError> {
1983 let names = string_list(body, "ConfigRuleNames");
1984 let filter = string_list(body, "ComplianceTypes");
1985 let st = self.state.read();
1986 let mut list = Vec::new();
1987 if let Some(acc) = st.account(account) {
1988 for rule in acc.rules.values() {
1989 if !names.is_empty() && !names.contains(&rule.name) {
1990 continue;
1991 }
1992 let results = acc.evaluations.get(&rule.name);
1993 let compliance = results
1994 .map(|m| fold_compliance(m.values().map(|r| r.compliance_type.as_str())))
1995 .unwrap_or_else(|| "INSUFFICIENT_DATA".into());
1996 if !filter.is_empty() && !filter.contains(&compliance) {
1997 continue;
1998 }
1999 list.push(json!({
2000 "ConfigRuleName": rule.name,
2001 "Compliance": { "ComplianceType": compliance },
2002 }));
2003 }
2004 }
2005 Ok(paged_response(
2006 "ComplianceByConfigRules",
2007 list,
2008 body,
2009 "NextToken",
2010 ))
2011 }
2012
2013 fn describe_compliance_by_resource(
2014 &self,
2015 account: &str,
2016 body: &Value,
2017 ) -> Result<AwsResponse, AwsServiceError> {
2018 let rtype = body.get("ResourceType").and_then(Value::as_str);
2019 let rid = body.get("ResourceId").and_then(Value::as_str);
2020 let filter = string_list(body, "ComplianceTypes");
2021 let st = self.state.read();
2022 let mut per_resource: std::collections::BTreeMap<(String, String), Vec<String>> =
2024 std::collections::BTreeMap::new();
2025 if let Some(acc) = st.account(account) {
2026 for results in acc.evaluations.values() {
2027 for r in results.values() {
2028 if r.resource_type.is_empty() {
2029 continue;
2030 }
2031 if let Some(t) = rtype {
2032 if r.resource_type != t {
2033 continue;
2034 }
2035 }
2036 if let Some(i) = rid {
2037 if r.resource_id != i {
2038 continue;
2039 }
2040 }
2041 per_resource
2042 .entry((r.resource_type.clone(), r.resource_id.clone()))
2043 .or_default()
2044 .push(r.compliance_type.clone());
2045 }
2046 }
2047 }
2048 let mut list = Vec::new();
2049 for ((rt, ri), types) in per_resource {
2050 let compliance = fold_compliance(types.iter().map(String::as_str));
2051 if !filter.is_empty() && !filter.contains(&compliance) {
2052 continue;
2053 }
2054 list.push(json!({
2055 "ResourceType": rt,
2056 "ResourceId": ri,
2057 "Compliance": { "ComplianceType": compliance },
2058 }));
2059 }
2060 Ok(paged_response(
2061 "ComplianceByResources",
2062 list,
2063 body,
2064 "NextToken",
2065 ))
2066 }
2067
2068 fn get_compliance_details_by_config_rule(
2069 &self,
2070 account: &str,
2071 body: &Value,
2072 ) -> Result<AwsResponse, AwsServiceError> {
2073 let name = require_str(body, "ConfigRuleName")?;
2074 let filter = string_list(body, "ComplianceTypes");
2075 let st = self.state.read();
2076 let results: Vec<Value> = st
2077 .account(account)
2078 .and_then(|a| a.evaluations.get(&name))
2079 .map(|m| {
2080 m.values()
2081 .filter(|r| filter.is_empty() || filter.contains(&r.compliance_type))
2082 .map(|r| Self::evaluation_result_json(r, &name))
2083 .collect()
2084 })
2085 .unwrap_or_default();
2086 Ok(paged_response(
2087 "EvaluationResults",
2088 results,
2089 body,
2090 "NextToken",
2091 ))
2092 }
2093
2094 fn get_compliance_details_by_resource(
2095 &self,
2096 account: &str,
2097 body: &Value,
2098 ) -> Result<AwsResponse, AwsServiceError> {
2099 let rtype = body
2100 .get("ResourceType")
2101 .and_then(Value::as_str)
2102 .unwrap_or_default()
2103 .to_string();
2104 let rid = body
2105 .get("ResourceId")
2106 .and_then(Value::as_str)
2107 .unwrap_or_default()
2108 .to_string();
2109 let filter = string_list(body, "ComplianceTypes");
2110 let st = self.state.read();
2111 let mut results = Vec::new();
2112 if let Some(acc) = st.account(account) {
2113 for (rule_name, m) in &acc.evaluations {
2114 for r in m.values() {
2115 if r.resource_type == rtype
2116 && r.resource_id == rid
2117 && (filter.is_empty() || filter.contains(&r.compliance_type))
2118 {
2119 results.push(Self::evaluation_result_json(r, rule_name));
2120 }
2121 }
2122 }
2123 }
2124 Ok(paged_response(
2125 "EvaluationResults",
2126 results,
2127 body,
2128 "NextToken",
2129 ))
2130 }
2131
2132 fn evaluation_result_json(r: &EvaluationResult, rule_name: &str) -> Value {
2133 json!({
2134 "EvaluationResultIdentifier": {
2135 "EvaluationResultQualifier": {
2136 "ConfigRuleName": rule_name,
2137 "ResourceType": r.resource_type,
2138 "ResourceId": r.resource_id,
2139 },
2140 "OrderingTimestamp": r.ordering_timestamp.timestamp() as f64,
2141 },
2142 "ComplianceType": r.compliance_type,
2143 "ResultRecordedTime": r.result_recorded_time.timestamp() as f64,
2144 "ConfigRuleInvokedTime": r.config_rule_invoked_time.timestamp() as f64,
2145 "Annotation": r.annotation,
2146 })
2147 }
2148
2149 fn get_compliance_summary_by_config_rule(
2150 &self,
2151 account: &str,
2152 ) -> Result<AwsResponse, AwsServiceError> {
2153 let st = self.state.read();
2154 let mut compliant = 0;
2155 let mut noncompliant = 0;
2156 if let Some(acc) = st.account(account) {
2157 for rule in acc.rules.values() {
2158 let c = acc
2159 .evaluations
2160 .get(&rule.name)
2161 .map(|m| fold_compliance(m.values().map(|r| r.compliance_type.as_str())))
2162 .unwrap_or_else(|| "INSUFFICIENT_DATA".into());
2163 match c.as_str() {
2164 "COMPLIANT" => compliant += 1,
2165 "NON_COMPLIANT" => noncompliant += 1,
2166 _ => {}
2167 }
2168 }
2169 }
2170 Ok(AwsResponse::ok_json(json!({
2171 "ComplianceSummary": {
2172 "CompliantResourceCount": { "CappedCount": compliant, "CapExceeded": false },
2173 "NonCompliantResourceCount": { "CappedCount": noncompliant, "CapExceeded": false },
2174 "ComplianceSummaryTimestamp": Utc::now().timestamp() as f64,
2175 }
2176 })))
2177 }
2178
2179 fn get_compliance_summary_by_resource_type(
2180 &self,
2181 account: &str,
2182 body: &Value,
2183 ) -> Result<AwsResponse, AwsServiceError> {
2184 let types = string_list(body, "ResourceTypes");
2185 let st = self.state.read();
2186 let mut per_type_resources: std::collections::BTreeMap<
2190 String,
2191 std::collections::BTreeMap<String, Vec<String>>,
2192 > = std::collections::BTreeMap::new();
2193 if let Some(acc) = st.account(account) {
2194 for m in acc.evaluations.values() {
2195 for r in m.values() {
2196 if r.resource_type.is_empty() {
2197 continue;
2198 }
2199 if !types.is_empty() && !types.contains(&r.resource_type) {
2200 continue;
2201 }
2202 per_type_resources
2203 .entry(r.resource_type.clone())
2204 .or_default()
2205 .entry(r.resource_id.clone())
2206 .or_default()
2207 .push(r.compliance_type.clone());
2208 }
2209 }
2210 }
2211 let now = Utc::now().timestamp() as f64;
2212 let summary_json = |resource_type: &str,
2213 resources: &std::collections::BTreeMap<String, Vec<String>>|
2214 -> Value {
2215 let mut compliant = 0;
2216 let mut noncompliant = 0;
2217 for c in resources.values() {
2218 match fold_compliance(c.iter().map(String::as_str)).as_str() {
2219 "COMPLIANT" => compliant += 1,
2220 "NON_COMPLIANT" => noncompliant += 1,
2221 _ => {}
2222 }
2223 }
2224 let summary = json!({
2225 "CompliantResourceCount": { "CappedCount": compliant, "CapExceeded": false },
2226 "NonCompliantResourceCount": { "CappedCount": noncompliant, "CapExceeded": false },
2227 "ComplianceSummaryTimestamp": now,
2228 });
2229 if resource_type.is_empty() {
2230 json!({ "ComplianceSummary": summary })
2231 } else {
2232 json!({ "ResourceType": resource_type, "ComplianceSummary": summary })
2233 }
2234 };
2235 let list: Vec<Value> = if types.is_empty() {
2236 let mut all: std::collections::BTreeMap<String, Vec<String>> =
2239 std::collections::BTreeMap::new();
2240 for resources in per_type_resources.values() {
2241 for (rid, c) in resources {
2242 all.entry(rid.clone())
2243 .or_default()
2244 .extend(c.iter().cloned());
2245 }
2246 }
2247 vec![summary_json("", &all)]
2248 } else {
2249 types
2252 .iter()
2253 .map(|t| {
2254 let empty = std::collections::BTreeMap::new();
2255 let resources = per_type_resources.get(t).unwrap_or(&empty);
2256 summary_json(t, resources)
2257 })
2258 .collect()
2259 };
2260 Ok(AwsResponse::ok_json(json!({
2261 "ComplianceSummariesByResourceType": list
2262 })))
2263 }
2264}
2265
2266impl ConfigService {
2269 fn put_remediation_configurations(
2270 &self,
2271 account: &str,
2272 region: &str,
2273 body: &Value,
2274 ) -> Result<AwsResponse, AwsServiceError> {
2275 let configs = body
2276 .get("RemediationConfigurations")
2277 .and_then(Value::as_array)
2278 .cloned()
2279 .unwrap_or_default();
2280 let mut failed = Vec::new();
2281 let mut st = self.state.write();
2282 let acc = st.account_mut(account);
2283 for c in &configs {
2284 let rule_name = match c.get("ConfigRuleName").and_then(Value::as_str) {
2285 Some(n) => n.to_string(),
2286 None => {
2287 failed.push(json!({ "FailureMessage": "ConfigRuleName is required", "FailedItems": [c] }));
2288 continue;
2289 }
2290 };
2291 acc.remediation_configs.insert(
2292 rule_name.clone(),
2293 RemediationConfiguration {
2294 config_rule_name: rule_name.clone(),
2295 arn: format!("arn:aws:config:{region}:{account}:remediation-configuration/{rule_name}/{}", short_id()),
2296 target_type: c.get("TargetType").and_then(Value::as_str).unwrap_or("SSM_DOCUMENT").to_string(),
2297 target_id: c.get("TargetId").and_then(Value::as_str).unwrap_or_default().to_string(),
2298 target_version: c.get("TargetVersion").and_then(Value::as_str).map(String::from),
2299 parameters: c.get("Parameters").cloned(),
2300 resource_type: c.get("ResourceType").and_then(Value::as_str).map(String::from),
2301 automatic: c.get("Automatic").and_then(Value::as_bool).unwrap_or(false),
2302 execution_controls: c.get("ExecutionControls").cloned(),
2303 maximum_automatic_attempts: c.get("MaximumAutomaticAttempts").and_then(Value::as_i64),
2304 retry_attempt_seconds: c.get("RetryAttemptSeconds").and_then(Value::as_i64),
2305 created_by_service: None,
2306 },
2307 );
2308 }
2309 Ok(AwsResponse::ok_json(json!({ "FailedBatches": failed })))
2310 }
2311
2312 fn remediation_json(r: &RemediationConfiguration) -> Value {
2313 let mut v = json!({
2314 "ConfigRuleName": r.config_rule_name,
2315 "TargetType": r.target_type,
2316 "TargetId": r.target_id,
2317 "Arn": r.arn,
2318 "Automatic": r.automatic,
2319 });
2320 if let Some(t) = &r.target_version {
2321 v["TargetVersion"] = json!(t);
2322 }
2323 if let Some(p) = &r.parameters {
2324 v["Parameters"] = p.clone();
2325 }
2326 if let Some(rt) = &r.resource_type {
2327 v["ResourceType"] = json!(rt);
2328 }
2329 if let Some(e) = &r.execution_controls {
2330 v["ExecutionControls"] = e.clone();
2331 }
2332 if let Some(m) = r.maximum_automatic_attempts {
2333 v["MaximumAutomaticAttempts"] = json!(m);
2334 }
2335 if let Some(s) = r.retry_attempt_seconds {
2336 v["RetryAttemptSeconds"] = json!(s);
2337 }
2338 v
2339 }
2340
2341 fn describe_remediation_configurations(
2342 &self,
2343 account: &str,
2344 body: &Value,
2345 ) -> Result<AwsResponse, AwsServiceError> {
2346 let names = string_list(body, "ConfigRuleNames");
2347 let st = self.state.read();
2348 let list: Vec<Value> = st
2349 .account(account)
2350 .map(|a| {
2351 a.remediation_configs
2352 .values()
2353 .filter(|r| names.is_empty() || names.contains(&r.config_rule_name))
2354 .map(Self::remediation_json)
2355 .collect()
2356 })
2357 .unwrap_or_default();
2358 Ok(AwsResponse::ok_json(
2359 json!({ "RemediationConfigurations": list }),
2360 ))
2361 }
2362
2363 fn delete_remediation_configuration(
2364 &self,
2365 account: &str,
2366 body: &Value,
2367 ) -> Result<AwsResponse, AwsServiceError> {
2368 let name = require_str(body, "ConfigRuleName")?;
2369 let mut st = self.state.write();
2370 let acc = st.account_mut(account);
2371 if acc.remediation_configs.remove(&name).is_none() {
2372 return Err(no_such(
2373 "NoSuchRemediationConfigurationException",
2374 "No RemediationConfiguration for rule exists.",
2375 ));
2376 }
2377 acc.remediation_executions
2378 .retain(|_, e| e.config_rule_name != name);
2379 Ok(AwsResponse::ok_json(json!({})))
2380 }
2381
2382 fn put_remediation_exceptions(
2383 &self,
2384 account: &str,
2385 body: &Value,
2386 ) -> Result<AwsResponse, AwsServiceError> {
2387 let rule_name = require_str(body, "ConfigRuleName")?;
2388 let keys = body
2389 .get("ResourceKeys")
2390 .and_then(Value::as_array)
2391 .cloned()
2392 .unwrap_or_default();
2393 let message = body
2394 .get("Message")
2395 .and_then(Value::as_str)
2396 .map(String::from);
2397 let mut st = self.state.write();
2398 let acc = st.account_mut(account);
2399 for k in &keys {
2400 let rt = k
2401 .get("ResourceType")
2402 .and_then(Value::as_str)
2403 .unwrap_or_default()
2404 .to_string();
2405 let rid = k
2406 .get("ResourceId")
2407 .and_then(Value::as_str)
2408 .unwrap_or_default()
2409 .to_string();
2410 let key = format!("{rule_name}\u{1}{rt}\u{1}{rid}");
2411 acc.remediation_exceptions.insert(
2412 key,
2413 RemediationException {
2414 config_rule_name: rule_name.clone(),
2415 resource_type: rt,
2416 resource_id: rid,
2417 message: message.clone(),
2418 expiration_time: None,
2419 },
2420 );
2421 }
2422 Ok(AwsResponse::ok_json(json!({ "FailedBatches": [] })))
2423 }
2424
2425 fn describe_remediation_exceptions(
2426 &self,
2427 account: &str,
2428 body: &Value,
2429 ) -> Result<AwsResponse, AwsServiceError> {
2430 let rule_name = require_str(body, "ConfigRuleName")?;
2431 let st = self.state.read();
2432 let list: Vec<Value> = st
2433 .account(account)
2434 .map(|a| {
2435 a.remediation_exceptions
2436 .values()
2437 .filter(|e| e.config_rule_name == rule_name)
2438 .map(|e| {
2439 json!({
2440 "ConfigRuleName": e.config_rule_name,
2441 "ResourceType": e.resource_type,
2442 "ResourceId": e.resource_id,
2443 "Message": e.message,
2444 })
2445 })
2446 .collect()
2447 })
2448 .unwrap_or_default();
2449 Ok(paged_response(
2450 "RemediationExceptions",
2451 list,
2452 body,
2453 "NextToken",
2454 ))
2455 }
2456
2457 fn delete_remediation_exceptions(
2458 &self,
2459 account: &str,
2460 body: &Value,
2461 ) -> Result<AwsResponse, AwsServiceError> {
2462 let rule_name = require_str(body, "ConfigRuleName")?;
2463 let keys = body
2464 .get("ResourceKeys")
2465 .and_then(Value::as_array)
2466 .cloned()
2467 .unwrap_or_default();
2468 let mut st = self.state.write();
2469 let acc = st.account_mut(account);
2470 for k in &keys {
2471 let rt = k
2472 .get("ResourceType")
2473 .and_then(Value::as_str)
2474 .unwrap_or_default();
2475 let rid = k
2476 .get("ResourceId")
2477 .and_then(Value::as_str)
2478 .unwrap_or_default();
2479 acc.remediation_exceptions
2480 .remove(&format!("{rule_name}\u{1}{rt}\u{1}{rid}"));
2481 }
2482 Ok(AwsResponse::ok_json(json!({ "FailedBatches": [] })))
2483 }
2484
2485 fn start_remediation_execution(
2486 &self,
2487 account: &str,
2488 body: &Value,
2489 ) -> Result<AwsResponse, AwsServiceError> {
2490 let rule_name = require_str(body, "ConfigRuleName")?;
2491 let keys = body
2492 .get("ResourceKeys")
2493 .and_then(Value::as_array)
2494 .cloned()
2495 .unwrap_or_default();
2496 let now = Utc::now();
2497 let mut st = self.state.write();
2498 let acc = st.account_mut(account);
2499 if !acc.remediation_configs.contains_key(&rule_name) {
2500 return Err(no_such(
2501 "NoSuchRemediationConfigurationException",
2502 "No RemediationConfiguration for rule exists.",
2503 ));
2504 }
2505 let mut failed = Vec::new();
2510 for k in &keys {
2511 let (Some(rt), Some(rid)) = (
2512 k.get("resourceType")
2513 .or_else(|| k.get("ResourceType"))
2514 .and_then(Value::as_str),
2515 k.get("resourceId")
2516 .or_else(|| k.get("ResourceId"))
2517 .and_then(Value::as_str),
2518 ) else {
2519 failed.push(k.clone());
2520 continue;
2521 };
2522 let key = format!("{rule_name}\u{1}{rt}\u{1}{rid}");
2523 acc.remediation_executions.insert(
2524 key,
2525 RemediationExecutionStatus {
2526 config_rule_name: rule_name.clone(),
2527 resource_type: rt.to_string(),
2528 resource_id: rid.to_string(),
2529 state: "QUEUED".into(),
2530 invocation_time: now,
2531 last_updated_time: now,
2532 },
2533 );
2534 }
2535 Ok(AwsResponse::ok_json(json!({ "FailedItems": failed })))
2536 }
2537
2538 fn describe_remediation_execution_status(
2539 &self,
2540 account: &str,
2541 body: &Value,
2542 ) -> Result<AwsResponse, AwsServiceError> {
2543 let rule_name = require_str(body, "ConfigRuleName")?;
2544 let keys = body
2545 .get("ResourceKeys")
2546 .and_then(Value::as_array)
2547 .cloned()
2548 .unwrap_or_default();
2549 let wanted: Vec<(String, String)> = keys
2551 .iter()
2552 .filter_map(|k| {
2553 let rt = k
2554 .get("resourceType")
2555 .or_else(|| k.get("ResourceType"))
2556 .and_then(Value::as_str)?;
2557 let rid = k
2558 .get("resourceId")
2559 .or_else(|| k.get("ResourceId"))
2560 .and_then(Value::as_str)?;
2561 Some((rt.to_string(), rid.to_string()))
2562 })
2563 .collect();
2564 let st = self.state.read();
2565 let list: Vec<Value> = st
2566 .account(account)
2567 .map(|a| {
2568 a.remediation_executions
2569 .values()
2570 .filter(|e| e.config_rule_name == rule_name)
2571 .filter(|e| {
2572 wanted.is_empty()
2573 || wanted
2574 .iter()
2575 .any(|(rt, rid)| *rt == e.resource_type && *rid == e.resource_id)
2576 })
2577 .map(|e| {
2578 let inv = e.invocation_time.timestamp() as f64;
2579 let upd = e.last_updated_time.timestamp() as f64;
2580 json!({
2581 "ResourceKey": { "resourceType": e.resource_type, "resourceId": e.resource_id },
2582 "State": e.state,
2583 "StepDetails": [{ "Name": "remediate", "State": e.state, "StartTime": inv }],
2584 "InvocationTime": inv,
2585 "LastUpdatedTime": upd,
2586 })
2587 })
2588 .collect()
2589 })
2590 .unwrap_or_default();
2591 Ok(paged_response(
2592 "RemediationExecutionStatuses",
2593 list,
2594 body,
2595 "NextToken",
2596 ))
2597 }
2598}
2599
2600fn extract_pack_rule_names(template: Option<&str>) -> Vec<String> {
2608 let Some(t) = template else { return Vec::new() };
2609 let v: Value = serde_json::from_str(t)
2612 .or_else(|_| serde_yaml::from_str::<Value>(t))
2613 .unwrap_or(Value::Null);
2614 let Some(resources) = v.get("Resources").and_then(Value::as_object) else {
2615 return Vec::new();
2616 };
2617 resources
2618 .values()
2619 .filter(|r| r.get("Type").and_then(Value::as_str) == Some("AWS::Config::ConfigRule"))
2620 .filter_map(|r| {
2621 r.pointer("/Properties/ConfigRuleName")
2622 .and_then(Value::as_str)
2623 .map(String::from)
2624 })
2625 .collect()
2626}
2627
2628impl ConfigService {
2629 fn put_conformance_pack(
2630 &self,
2631 account: &str,
2632 region: &str,
2633 body: &Value,
2634 ) -> Result<AwsResponse, AwsServiceError> {
2635 let name = require_str(body, "ConformancePackName")?;
2636 let template_body = body
2637 .get("TemplateBody")
2638 .and_then(Value::as_str)
2639 .map(String::from);
2640 let template_s3_uri = body
2641 .get("TemplateS3Uri")
2642 .and_then(Value::as_str)
2643 .map(String::from);
2644 if template_body.is_none()
2645 && template_s3_uri.is_none()
2646 && body.get("TemplateSSMDocumentDetails").is_none()
2647 {
2648 return Err(invalid(
2649 "Either TemplateBody, TemplateS3Uri, or TemplateSSMDocumentDetails is required",
2650 ));
2651 }
2652 let now = Utc::now();
2653 let arn = format!(
2654 "arn:aws:config:{region}:{account}:conformance-pack/{name}-{}",
2655 short_id()
2656 );
2657 let id = format!("conformance-pack-{}", short_id());
2658 let rule_names = extract_pack_rule_names(template_body.as_deref());
2659 let mut st = self.state.write();
2660 let acc = st.account_mut(account);
2661 let existing = acc.conformance_packs.get(&name);
2662 let pack = ConformancePack {
2663 name: name.clone(),
2664 arn: existing.map(|p| p.arn.clone()).unwrap_or(arn.clone()),
2665 id: existing.map(|p| p.id.clone()).unwrap_or(id),
2666 delivery_s3_bucket: body
2667 .get("DeliveryS3Bucket")
2668 .and_then(Value::as_str)
2669 .map(String::from),
2670 delivery_s3_key_prefix: body
2671 .get("DeliveryS3KeyPrefix")
2672 .and_then(Value::as_str)
2673 .map(String::from),
2674 input_parameters: body
2675 .get("ConformancePackInputParameters")
2676 .and_then(Value::as_array)
2677 .cloned()
2678 .unwrap_or_default(),
2679 template_body,
2680 template_s3_uri,
2681 template_ssm_document_details: body.get("TemplateSSMDocumentDetails").cloned(),
2682 last_update_requested_time: now,
2683 created_by: None,
2684 rule_names,
2685 };
2686 let out_arn = pack.arn.clone();
2687 acc.conformance_packs.insert(name, pack);
2688 let tags = parse_create_tags(body);
2689 if !tags.is_empty() {
2690 let entry = acc.tags.entry(out_arn.clone()).or_default();
2691 for (k, v) in tags {
2692 entry.insert(k, v);
2693 }
2694 }
2695 Ok(AwsResponse::ok_json(
2696 json!({ "ConformancePackArn": out_arn }),
2697 ))
2698 }
2699
2700 fn describe_conformance_packs(
2701 &self,
2702 account: &str,
2703 body: &Value,
2704 ) -> Result<AwsResponse, AwsServiceError> {
2705 let names = string_list(body, "ConformancePackNames");
2706 let st = self.state.read();
2707 let list: Vec<Value> = st
2708 .account(account)
2709 .map(|a| {
2710 a.conformance_packs
2711 .values()
2712 .filter(|p| names.is_empty() || names.contains(&p.name))
2713 .map(|p| {
2714 json!({
2715 "ConformancePackName": p.name,
2716 "ConformancePackArn": p.arn,
2717 "ConformancePackId": p.id,
2718 "DeliveryS3Bucket": p.delivery_s3_bucket,
2719 "DeliveryS3KeyPrefix": p.delivery_s3_key_prefix,
2720 "ConformancePackInputParameters": p.input_parameters,
2721 "LastUpdateRequestedTime": p.last_update_requested_time.timestamp() as f64,
2722 "TemplateSSMDocumentDetails": p.template_ssm_document_details,
2723 })
2724 })
2725 .collect()
2726 })
2727 .unwrap_or_default();
2728 if !names.is_empty() {
2729 for n in &names {
2730 if !st
2731 .account(account)
2732 .map(|a| a.conformance_packs.contains_key(n))
2733 .unwrap_or(false)
2734 {
2735 return Err(no_such(
2736 "NoSuchConformancePackException",
2737 format!("Cannot find conformance pack with name '{n}'."),
2738 ));
2739 }
2740 }
2741 }
2742 Ok(paged_response(
2743 "ConformancePackDetails",
2744 list,
2745 body,
2746 "NextToken",
2747 ))
2748 }
2749
2750 fn delete_conformance_pack(
2751 &self,
2752 account: &str,
2753 body: &Value,
2754 ) -> Result<AwsResponse, AwsServiceError> {
2755 let name = require_str(body, "ConformancePackName")?;
2756 let mut st = self.state.write();
2757 let acc = st.account_mut(account);
2758 if acc.conformance_packs.remove(&name).is_none() {
2759 return Err(no_such(
2760 "NoSuchConformancePackException",
2761 format!("Cannot find conformance pack with name '{name}'."),
2762 ));
2763 }
2764 Ok(AwsResponse::ok_json(json!({})))
2765 }
2766
2767 fn describe_conformance_pack_status(
2768 &self,
2769 account: &str,
2770 body: &Value,
2771 ) -> Result<AwsResponse, AwsServiceError> {
2772 let names = string_list(body, "ConformancePackNames");
2773 let now = Utc::now().timestamp() as f64;
2774 let st = self.state.read();
2775 let list: Vec<Value> = st
2776 .account(account)
2777 .map(|a| {
2778 a.conformance_packs
2779 .values()
2780 .filter(|p| names.is_empty() || names.contains(&p.name))
2781 .map(|p| {
2782 json!({
2783 "ConformancePackName": p.name,
2784 "ConformancePackId": p.id,
2785 "ConformancePackArn": p.arn,
2786 "ConformancePackState": "CREATE_COMPLETE",
2787 "StackArn": format!("arn:aws:cloudformation:us-east-1:{account}:stack/awsconfigconforms-{}/{}", p.name, short_id()),
2788 "LastUpdateRequestedTime": now,
2789 "LastUpdateCompletedTime": now,
2790 })
2791 })
2792 .collect()
2793 })
2794 .unwrap_or_default();
2795 Ok(paged_response(
2796 "ConformancePackStatusDetails",
2797 list,
2798 body,
2799 "NextToken",
2800 ))
2801 }
2802
2803 fn pack_rule_names(acc: &AccountState, pack: &ConformancePack) -> Vec<String> {
2806 if pack.rule_names.is_empty() {
2807 acc.rules.keys().cloned().collect()
2808 } else {
2809 pack.rule_names
2810 .iter()
2811 .filter(|n| acc.rules.contains_key(*n))
2812 .cloned()
2813 .collect()
2814 }
2815 }
2816
2817 fn describe_conformance_pack_compliance(
2818 &self,
2819 account: &str,
2820 body: &Value,
2821 ) -> Result<AwsResponse, AwsServiceError> {
2822 let name = require_str(body, "ConformancePackName")?;
2823 let st = self.state.read();
2824 let acc = st.account(account).ok_or_else(|| {
2825 no_such(
2826 "NoSuchConformancePackException",
2827 "No such conformance pack.",
2828 )
2829 })?;
2830 let pack = acc.conformance_packs.get(&name).ok_or_else(|| {
2831 no_such(
2832 "NoSuchConformancePackException",
2833 format!("Cannot find conformance pack with name '{name}'."),
2834 )
2835 })?;
2836 let list: Vec<Value> = Self::pack_rule_names(acc, pack)
2837 .iter()
2838 .map(|rule| {
2839 let c = acc
2840 .evaluations
2841 .get(rule)
2842 .map(|m| fold_compliance(m.values().map(|r| r.compliance_type.as_str())))
2843 .unwrap_or_else(|| "INSUFFICIENT_DATA".into());
2844 json!({ "ConfigRuleName": rule, "ComplianceType": c, "Controls": [] })
2845 })
2846 .collect();
2847 let (page, next) = paginate(list, body);
2848 let mut out =
2849 json!({ "ConformancePackName": name, "ConformancePackRuleComplianceList": page });
2850 if let Some(t) = next {
2851 out["NextToken"] = json!(t);
2852 }
2853 Ok(AwsResponse::ok_json(out))
2854 }
2855
2856 fn get_conformance_pack_compliance_summary(
2857 &self,
2858 account: &str,
2859 body: &Value,
2860 ) -> Result<AwsResponse, AwsServiceError> {
2861 let names = string_list(body, "ConformancePackNames");
2862 let st = self.state.read();
2863 let acc = st.account(account);
2864 let list: Vec<Value> = acc
2865 .map(|a| {
2866 a.conformance_packs
2867 .values()
2868 .filter(|p| names.is_empty() || names.contains(&p.name))
2869 .map(|p| {
2870 let rules = Self::pack_rule_names(a, p);
2871 let noncompliant = rules.iter().any(|r| {
2872 a.evaluations.get(r).map(|m| m.values().any(|e| e.compliance_type == "NON_COMPLIANT")).unwrap_or(false)
2873 });
2874 json!({
2875 "ConformancePackName": p.name,
2876 "ConformancePackComplianceStatus": if noncompliant { "NON_COMPLIANT" } else { "COMPLIANT" },
2877 })
2878 })
2879 .collect()
2880 })
2881 .unwrap_or_default();
2882 Ok(AwsResponse::ok_json(
2883 json!({ "ConformancePackComplianceSummaryList": list }),
2884 ))
2885 }
2886
2887 fn get_conformance_pack_compliance_details(
2888 &self,
2889 account: &str,
2890 body: &Value,
2891 ) -> Result<AwsResponse, AwsServiceError> {
2892 let name = require_str(body, "ConformancePackName")?;
2893 let st = self.state.read();
2894 let acc = st.account(account).ok_or_else(|| {
2895 no_such(
2896 "NoSuchConformancePackException",
2897 "No such conformance pack.",
2898 )
2899 })?;
2900 let pack = acc.conformance_packs.get(&name).ok_or_else(|| {
2901 no_such(
2902 "NoSuchConformancePackException",
2903 format!("Cannot find conformance pack with name '{name}'."),
2904 )
2905 })?;
2906 let mut results = Vec::new();
2907 for rule in Self::pack_rule_names(acc, pack) {
2908 if let Some(m) = acc.evaluations.get(&rule) {
2909 for r in m.values() {
2910 results.push(json!({
2911 "ConfigRuleInvokedTime": r.config_rule_invoked_time.timestamp() as f64,
2912 "ResultRecordedTime": r.result_recorded_time.timestamp() as f64,
2913 "ComplianceType": r.compliance_type,
2914 "EvaluationResultIdentifier": {
2915 "EvaluationResultQualifier": {
2916 "ConfigRuleName": rule,
2917 "ResourceType": r.resource_type,
2918 "ResourceId": r.resource_id,
2919 },
2920 "OrderingTimestamp": r.ordering_timestamp.timestamp() as f64,
2921 },
2922 }));
2923 }
2924 }
2925 }
2926 let (page, next) = paginate(results, body);
2927 let mut out =
2928 json!({ "ConformancePackName": name, "ConformancePackRuleEvaluationResults": page });
2929 if let Some(t) = next {
2930 out["NextToken"] = json!(t);
2931 }
2932 Ok(AwsResponse::ok_json(out))
2933 }
2934
2935 fn list_conformance_pack_compliance_scores(
2936 &self,
2937 account: &str,
2938 ) -> Result<AwsResponse, AwsServiceError> {
2939 let st = self.state.read();
2940 let list: Vec<Value> = st
2941 .account(account)
2942 .map(|a| {
2943 a.conformance_packs
2944 .values()
2945 .map(|p| {
2946 let rules = Self::pack_rule_names(a, p);
2947 let total = rules.len().max(1);
2948 let compliant = rules
2949 .iter()
2950 .filter(|r| {
2951 a.evaluations
2952 .get(*r)
2953 .map(|m| {
2954 !m.values().any(|e| e.compliance_type == "NON_COMPLIANT")
2955 })
2956 .unwrap_or(true)
2957 })
2958 .count();
2959 let score = (compliant as f64 / total as f64) * 100.0;
2960 json!({
2961 "Score": format!("{score:.2}"),
2962 "ConformancePackName": p.name,
2963 "LastUpdatedTime": p.last_update_requested_time.timestamp() as f64,
2964 })
2965 })
2966 .collect()
2967 })
2968 .unwrap_or_default();
2969 Ok(AwsResponse::ok_json(
2970 json!({ "ConformancePackComplianceScores": list }),
2971 ))
2972 }
2973}
2974
2975impl ConfigService {
2978 fn put_organization_config_rule(
2979 &self,
2980 account: &str,
2981 region: &str,
2982 body: &Value,
2983 ) -> Result<AwsResponse, AwsServiceError> {
2984 let name = require_str(body, "OrganizationConfigRuleName")?;
2985 let arn = format!(
2986 "arn:aws:config:{region}:{account}:organization-config-rule/{name}-{}",
2987 short_id()
2988 );
2989 let mut st = self.state.write();
2990 let acc = st.account_mut(account);
2991 let out_arn = acc
2992 .org_rules
2993 .get(&name)
2994 .map(|r| r.arn.clone())
2995 .unwrap_or(arn);
2996 acc.org_rules.insert(
2997 name.clone(),
2998 OrganizationConfigRule {
2999 name,
3000 arn: out_arn.clone(),
3001 managed_rule_metadata: body.get("OrganizationManagedRuleMetadata").cloned(),
3002 custom_rule_metadata: body.get("OrganizationCustomRuleMetadata").cloned(),
3003 custom_policy_rule_metadata: body
3004 .get("OrganizationCustomPolicyRuleMetadata")
3005 .cloned(),
3006 excluded_accounts: string_list(body, "ExcludedAccounts"),
3007 last_update_time: Utc::now(),
3008 },
3009 );
3010 let tags = parse_create_tags(body);
3011 if !tags.is_empty() {
3012 let entry = acc.tags.entry(out_arn.clone()).or_default();
3013 for (k, v) in tags {
3014 entry.insert(k, v);
3015 }
3016 }
3017 Ok(AwsResponse::ok_json(
3018 json!({ "OrganizationConfigRuleArn": out_arn }),
3019 ))
3020 }
3021
3022 fn describe_organization_config_rules(
3023 &self,
3024 account: &str,
3025 body: &Value,
3026 ) -> Result<AwsResponse, AwsServiceError> {
3027 let names = string_list(body, "OrganizationConfigRuleNames");
3028 let st = self.state.read();
3029 let list: Vec<Value> = st
3030 .account(account)
3031 .map(|a| {
3032 a.org_rules
3033 .values()
3034 .filter(|r| names.is_empty() || names.contains(&r.name))
3035 .map(|r| {
3036 json!({
3037 "OrganizationConfigRuleName": r.name,
3038 "OrganizationConfigRuleArn": r.arn,
3039 "OrganizationManagedRuleMetadata": r.managed_rule_metadata,
3040 "OrganizationCustomRuleMetadata": r.custom_rule_metadata,
3041 "OrganizationCustomPolicyRuleMetadata": r.custom_policy_rule_metadata,
3042 "ExcludedAccounts": r.excluded_accounts,
3043 "LastUpdateTime": r.last_update_time.timestamp() as f64,
3044 })
3045 })
3046 .collect()
3047 })
3048 .unwrap_or_default();
3049 Ok(AwsResponse::ok_json(
3050 json!({ "OrganizationConfigRules": list }),
3051 ))
3052 }
3053
3054 fn delete_organization_config_rule(
3055 &self,
3056 account: &str,
3057 body: &Value,
3058 ) -> Result<AwsResponse, AwsServiceError> {
3059 let name = require_str(body, "OrganizationConfigRuleName")?;
3060 let mut st = self.state.write();
3061 let acc = st.account_mut(account);
3062 if acc.org_rules.remove(&name).is_none() {
3063 return Err(no_such(
3064 "NoSuchOrganizationConfigRuleException",
3065 format!("Cannot find organization config rule '{name}'."),
3066 ));
3067 }
3068 Ok(AwsResponse::ok_json(json!({})))
3069 }
3070
3071 fn describe_organization_config_rule_statuses(
3072 &self,
3073 account: &str,
3074 body: &Value,
3075 ) -> Result<AwsResponse, AwsServiceError> {
3076 let names = string_list(body, "OrganizationConfigRuleNames");
3077 let now = Utc::now().timestamp() as f64;
3078 let st = self.state.read();
3079 let list: Vec<Value> = st
3080 .account(account)
3081 .map(|a| {
3082 a.org_rules
3083 .values()
3084 .filter(|r| names.is_empty() || names.contains(&r.name))
3085 .map(|r| {
3086 json!({ "OrganizationConfigRuleName": r.name, "OrganizationRuleStatus": "CREATE_SUCCESSFUL", "LastUpdateTime": now })
3087 })
3088 .collect()
3089 })
3090 .unwrap_or_default();
3091 Ok(AwsResponse::ok_json(
3092 json!({ "OrganizationConfigRuleStatuses": list }),
3093 ))
3094 }
3095
3096 fn get_organization_config_rule_detailed_status(
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 if !st
3104 .account(account)
3105 .map(|a| a.org_rules.contains_key(&name))
3106 .unwrap_or(false)
3107 {
3108 return Err(no_such(
3109 "NoSuchOrganizationConfigRuleException",
3110 format!("Cannot find organization config rule '{name}'."),
3111 ));
3112 }
3113 Ok(AwsResponse::ok_json(
3114 json!({ "OrganizationConfigRuleDetailedStatus": [] }),
3115 ))
3116 }
3117
3118 fn get_organization_custom_rule_policy(
3119 &self,
3120 account: &str,
3121 body: &Value,
3122 ) -> Result<AwsResponse, AwsServiceError> {
3123 let name = require_str(body, "OrganizationConfigRuleName")?;
3124 let st = self.state.read();
3125 let policy = st
3126 .account(account)
3127 .and_then(|a| a.org_rules.get(&name))
3128 .and_then(|r| r.custom_policy_rule_metadata.as_ref())
3129 .and_then(|m| {
3130 m.get("PolicyText")
3131 .and_then(Value::as_str)
3132 .map(String::from)
3133 });
3134 Ok(AwsResponse::ok_json(json!({ "PolicyText": policy })))
3135 }
3136
3137 fn put_organization_conformance_pack(
3138 &self,
3139 account: &str,
3140 region: &str,
3141 body: &Value,
3142 ) -> Result<AwsResponse, AwsServiceError> {
3143 let name = require_str(body, "OrganizationConformancePackName")?;
3144 let arn = format!(
3145 "arn:aws:config:{region}:{account}:organization-conformance-pack/{name}-{}",
3146 short_id()
3147 );
3148 let mut st = self.state.write();
3149 let acc = st.account_mut(account);
3150 let out_arn = acc
3151 .org_conformance_packs
3152 .get(&name)
3153 .map(|p| p.arn.clone())
3154 .unwrap_or(arn);
3155 acc.org_conformance_packs.insert(
3156 name.clone(),
3157 OrganizationConformancePack {
3158 name,
3159 arn: out_arn.clone(),
3160 delivery_s3_bucket: body
3161 .get("DeliveryS3Bucket")
3162 .and_then(Value::as_str)
3163 .map(String::from),
3164 delivery_s3_key_prefix: body
3165 .get("DeliveryS3KeyPrefix")
3166 .and_then(Value::as_str)
3167 .map(String::from),
3168 input_parameters: body
3169 .get("ConformancePackInputParameters")
3170 .and_then(Value::as_array)
3171 .cloned()
3172 .unwrap_or_default(),
3173 template_body: body
3174 .get("TemplateBody")
3175 .and_then(Value::as_str)
3176 .map(String::from),
3177 template_s3_uri: body
3178 .get("TemplateS3Uri")
3179 .and_then(Value::as_str)
3180 .map(String::from),
3181 excluded_accounts: string_list(body, "ExcludedAccounts"),
3182 last_update_time: Utc::now(),
3183 },
3184 );
3185 let tags = parse_create_tags(body);
3186 if !tags.is_empty() {
3187 let entry = acc.tags.entry(out_arn.clone()).or_default();
3188 for (k, v) in tags {
3189 entry.insert(k, v);
3190 }
3191 }
3192 Ok(AwsResponse::ok_json(
3193 json!({ "OrganizationConformancePackArn": out_arn }),
3194 ))
3195 }
3196
3197 fn describe_organization_conformance_packs(
3198 &self,
3199 account: &str,
3200 body: &Value,
3201 ) -> Result<AwsResponse, AwsServiceError> {
3202 let names = string_list(body, "OrganizationConformancePackNames");
3203 let st = self.state.read();
3204 let list: Vec<Value> = st
3205 .account(account)
3206 .map(|a| {
3207 a.org_conformance_packs
3208 .values()
3209 .filter(|p| names.is_empty() || names.contains(&p.name))
3210 .map(|p| {
3211 json!({
3212 "OrganizationConformancePackName": p.name,
3213 "OrganizationConformancePackArn": p.arn,
3214 "DeliveryS3Bucket": p.delivery_s3_bucket,
3215 "DeliveryS3KeyPrefix": p.delivery_s3_key_prefix,
3216 "ConformancePackInputParameters": p.input_parameters,
3217 "ExcludedAccounts": p.excluded_accounts,
3218 "LastUpdateTime": p.last_update_time.timestamp() as f64,
3219 })
3220 })
3221 .collect()
3222 })
3223 .unwrap_or_default();
3224 Ok(AwsResponse::ok_json(
3225 json!({ "OrganizationConformancePacks": list }),
3226 ))
3227 }
3228
3229 fn delete_organization_conformance_pack(
3230 &self,
3231 account: &str,
3232 body: &Value,
3233 ) -> Result<AwsResponse, AwsServiceError> {
3234 let name = require_str(body, "OrganizationConformancePackName")?;
3235 let mut st = self.state.write();
3236 let acc = st.account_mut(account);
3237 if acc.org_conformance_packs.remove(&name).is_none() {
3238 return Err(no_such(
3239 "NoSuchOrganizationConformancePackException",
3240 format!("Cannot find organization conformance pack '{name}'."),
3241 ));
3242 }
3243 Ok(AwsResponse::ok_json(json!({})))
3244 }
3245
3246 fn describe_organization_conformance_pack_statuses(
3247 &self,
3248 account: &str,
3249 body: &Value,
3250 ) -> Result<AwsResponse, AwsServiceError> {
3251 let names = string_list(body, "OrganizationConformancePackNames");
3252 let now = Utc::now().timestamp() as f64;
3253 let st = self.state.read();
3254 let list: Vec<Value> = st
3255 .account(account)
3256 .map(|a| {
3257 a.org_conformance_packs
3258 .values()
3259 .filter(|p| names.is_empty() || names.contains(&p.name))
3260 .map(|p| json!({ "OrganizationConformancePackName": p.name, "Status": "CREATE_SUCCESSFUL", "LastUpdateTime": now }))
3261 .collect()
3262 })
3263 .unwrap_or_default();
3264 Ok(AwsResponse::ok_json(
3265 json!({ "OrganizationConformancePackStatuses": list }),
3266 ))
3267 }
3268
3269 fn get_organization_conformance_pack_detailed_status(
3270 &self,
3271 account: &str,
3272 body: &Value,
3273 ) -> Result<AwsResponse, AwsServiceError> {
3274 let name = require_str(body, "OrganizationConformancePackName")?;
3275 let st = self.state.read();
3276 if !st
3277 .account(account)
3278 .map(|a| a.org_conformance_packs.contains_key(&name))
3279 .unwrap_or(false)
3280 {
3281 return Err(no_such(
3282 "NoSuchOrganizationConformancePackException",
3283 format!("Cannot find organization conformance pack '{name}'."),
3284 ));
3285 }
3286 Ok(AwsResponse::ok_json(
3287 json!({ "OrganizationConformancePackDetailedStatuses": [] }),
3288 ))
3289 }
3290}
3291
3292impl ConfigService {
3295 fn put_configuration_aggregator(
3296 &self,
3297 account: &str,
3298 region: &str,
3299 body: &Value,
3300 ) -> Result<AwsResponse, AwsServiceError> {
3301 let name = require_str(body, "ConfigurationAggregatorName")?;
3302 let now = Utc::now();
3303 let arn = format!(
3304 "arn:aws:config:{region}:{account}:config-aggregator/config-aggregator-{}",
3305 short_id()
3306 );
3307 let mut st = self.state.write();
3308 let acc = st.account_mut(account);
3309 let existing = acc.aggregators.get(&name);
3310 let agg = ConfigurationAggregator {
3311 name: name.clone(),
3312 arn: existing.map(|a| a.arn.clone()).unwrap_or(arn),
3313 account_aggregation_sources: body
3314 .get("AccountAggregationSources")
3315 .and_then(Value::as_array)
3316 .cloned()
3317 .unwrap_or_default(),
3318 organization_aggregation_source: body.get("OrganizationAggregationSource").cloned(),
3319 creation_time: existing.map(|a| a.creation_time).unwrap_or(now),
3320 last_updated_time: now,
3321 created_by: None,
3322 };
3323 let out = Self::aggregator_json(&agg);
3324 let arn_for_tags = agg.arn.clone();
3325 acc.aggregators.insert(name, agg);
3326 let tags = parse_create_tags(body);
3327 if !tags.is_empty() {
3328 let entry = acc.tags.entry(arn_for_tags).or_default();
3329 for (k, v) in tags {
3330 entry.insert(k, v);
3331 }
3332 }
3333 Ok(AwsResponse::ok_json(
3334 json!({ "ConfigurationAggregator": out }),
3335 ))
3336 }
3337
3338 fn aggregator_json(a: &ConfigurationAggregator) -> Value {
3339 json!({
3340 "ConfigurationAggregatorName": a.name,
3341 "ConfigurationAggregatorArn": a.arn,
3342 "AccountAggregationSources": a.account_aggregation_sources,
3343 "OrganizationAggregationSource": a.organization_aggregation_source,
3344 "CreationTime": a.creation_time.timestamp() as f64,
3345 "LastUpdatedTime": a.last_updated_time.timestamp() as f64,
3346 })
3347 }
3348
3349 fn describe_configuration_aggregators(
3350 &self,
3351 account: &str,
3352 body: &Value,
3353 ) -> Result<AwsResponse, AwsServiceError> {
3354 let names = string_list(body, "ConfigurationAggregatorNames");
3355 let st = self.state.read();
3356 let list: Vec<Value> = st
3357 .account(account)
3358 .map(|a| {
3359 a.aggregators
3360 .values()
3361 .filter(|g| names.is_empty() || names.contains(&g.name))
3362 .map(Self::aggregator_json)
3363 .collect()
3364 })
3365 .unwrap_or_default();
3366 if !names.is_empty() {
3367 for n in &names {
3368 if !st
3369 .account(account)
3370 .map(|a| a.aggregators.contains_key(n))
3371 .unwrap_or(false)
3372 {
3373 return Err(no_such(
3374 "NoSuchConfigurationAggregatorException",
3375 format!("The configuration aggregator '{n}' does not exist."),
3376 ));
3377 }
3378 }
3379 }
3380 Ok(paged_response(
3381 "ConfigurationAggregators",
3382 list,
3383 body,
3384 "NextToken",
3385 ))
3386 }
3387
3388 fn delete_configuration_aggregator(
3389 &self,
3390 account: &str,
3391 body: &Value,
3392 ) -> Result<AwsResponse, AwsServiceError> {
3393 let name = require_str(body, "ConfigurationAggregatorName")?;
3394 let mut st = self.state.write();
3395 let acc = st.account_mut(account);
3396 if acc.aggregators.remove(&name).is_none() {
3397 return Err(no_such(
3398 "NoSuchConfigurationAggregatorException",
3399 format!("The configuration aggregator '{name}' does not exist."),
3400 ));
3401 }
3402 Ok(AwsResponse::ok_json(json!({})))
3403 }
3404
3405 fn describe_configuration_aggregator_sources_status(
3406 &self,
3407 account: &str,
3408 body: &Value,
3409 ) -> Result<AwsResponse, AwsServiceError> {
3410 let name = require_str(body, "ConfigurationAggregatorName")?;
3411 let now = Utc::now().timestamp() as f64;
3412 let st = self.state.read();
3413 let agg = st.account(account).and_then(|a| a.aggregators.get(&name));
3414 let Some(agg) = agg else {
3415 return Err(no_such(
3416 "NoSuchConfigurationAggregatorException",
3417 format!("The configuration aggregator '{name}' does not exist."),
3418 ));
3419 };
3420 let mut list = Vec::new();
3421 for src in &agg.account_aggregation_sources {
3422 let region = src
3423 .get("AwsRegions")
3424 .and_then(Value::as_array)
3425 .and_then(|a| a.first())
3426 .and_then(Value::as_str)
3427 .unwrap_or("us-east-1");
3428 let accounts = src
3429 .get("AccountIds")
3430 .and_then(Value::as_array)
3431 .cloned()
3432 .unwrap_or_default();
3433 for acct in accounts {
3434 list.push(json!({
3435 "SourceId": acct,
3436 "SourceType": "ACCOUNT",
3437 "AwsRegion": region,
3438 "LastUpdateStatus": "SUCCEEDED",
3439 "LastUpdateTime": now,
3440 }));
3441 }
3442 }
3443 Ok(AwsResponse::ok_json(
3444 json!({ "AggregatedSourceStatusList": list }),
3445 ))
3446 }
3447
3448 fn put_aggregation_authorization(
3449 &self,
3450 account: &str,
3451 region: &str,
3452 body: &Value,
3453 ) -> Result<AwsResponse, AwsServiceError> {
3454 let authorized_account = require_str(body, "AuthorizedAccountId")?;
3455 let authorized_region = require_str(body, "AuthorizedAwsRegion")?;
3456 let now = Utc::now();
3457 let arn = format!("arn:aws:config:{region}:{account}:aggregation-authorization/{authorized_account}/{authorized_region}");
3458 let key = format!("{authorized_account}\u{1}{authorized_region}");
3459 let auth = AggregationAuthorization {
3460 arn: arn.clone(),
3461 authorized_account_id: authorized_account,
3462 authorized_aws_region: authorized_region,
3463 creation_time: now,
3464 };
3465 let out = Self::auth_json(&auth);
3466 let mut st = self.state.write();
3467 let acc = st.account_mut(account);
3468 acc.aggregation_authorizations.insert(key, auth);
3469 let tags = parse_create_tags(body);
3470 if !tags.is_empty() {
3471 let entry = acc.tags.entry(arn.clone()).or_default();
3472 for (k, v) in tags {
3473 entry.insert(k, v);
3474 }
3475 }
3476 let _ = out;
3477 Ok(AwsResponse::ok_json(
3478 json!({ "AggregationAuthorization": Self::auth_json_by_arn(&arn) }),
3479 ))
3480 }
3481
3482 fn auth_json(a: &AggregationAuthorization) -> Value {
3483 json!({
3484 "AggregationAuthorizationArn": a.arn,
3485 "AuthorizedAccountId": a.authorized_account_id,
3486 "AuthorizedAwsRegion": a.authorized_aws_region,
3487 "CreationTime": a.creation_time.timestamp() as f64,
3488 })
3489 }
3490
3491 fn auth_json_by_arn(arn: &str) -> Value {
3492 json!({ "AggregationAuthorizationArn": arn })
3493 }
3494
3495 fn describe_aggregation_authorizations(
3496 &self,
3497 account: &str,
3498 body: &Value,
3499 ) -> Result<AwsResponse, AwsServiceError> {
3500 let st = self.state.read();
3501 let list: Vec<Value> = st
3502 .account(account)
3503 .map(|a| {
3504 a.aggregation_authorizations
3505 .values()
3506 .map(Self::auth_json)
3507 .collect()
3508 })
3509 .unwrap_or_default();
3510 Ok(paged_response(
3511 "AggregationAuthorizations",
3512 list,
3513 body,
3514 "NextToken",
3515 ))
3516 }
3517
3518 fn delete_aggregation_authorization(
3519 &self,
3520 account: &str,
3521 body: &Value,
3522 ) -> Result<AwsResponse, AwsServiceError> {
3523 let authorized_account = require_str(body, "AuthorizedAccountId")?;
3524 let authorized_region = require_str(body, "AuthorizedAwsRegion")?;
3525 let key = format!("{authorized_account}\u{1}{authorized_region}");
3526 let mut st = self.state.write();
3527 st.account_mut(account)
3528 .aggregation_authorizations
3529 .remove(&key);
3530 Ok(AwsResponse::ok_json(json!({})))
3531 }
3532
3533 fn batch_get_aggregate_resource_config(
3537 &self,
3538 account: &str,
3539 body: &Value,
3540 ) -> Result<AwsResponse, AwsServiceError> {
3541 let keys = body
3542 .get("ResourceIdentifiers")
3543 .and_then(Value::as_array)
3544 .cloned()
3545 .unwrap_or_default();
3546 let st = self.state.read();
3547 let acc = st.account(account);
3548 let mut items = Vec::new();
3549 let mut unprocessed = Vec::new();
3550 for k in keys {
3551 let rt = k
3552 .get("ResourceType")
3553 .and_then(Value::as_str)
3554 .unwrap_or_default();
3555 let rid = k
3556 .get("ResourceId")
3557 .and_then(Value::as_str)
3558 .unwrap_or_default();
3559 let key = resource_key(rt, rid);
3560 match acc
3561 .and_then(|a| a.config_items.get(&key))
3562 .and_then(|h| h.last())
3563 {
3564 Some(ci) => items.push(Self::config_item_json(ci)),
3565 None => unprocessed.push(k),
3566 }
3567 }
3568 Ok(AwsResponse::ok_json(
3569 json!({ "BaseConfigurationItems": items, "UnprocessedResourceIdentifiers": unprocessed }),
3570 ))
3571 }
3572
3573 fn get_aggregate_resource_config(
3574 &self,
3575 account: &str,
3576 body: &Value,
3577 ) -> Result<AwsResponse, AwsServiceError> {
3578 let ident = body
3579 .get("ResourceIdentifier")
3580 .cloned()
3581 .unwrap_or(Value::Null);
3582 let rt = ident
3583 .get("ResourceType")
3584 .and_then(Value::as_str)
3585 .unwrap_or_default();
3586 let rid = ident
3587 .get("ResourceId")
3588 .and_then(Value::as_str)
3589 .unwrap_or_default();
3590 let st = self.state.read();
3591 let key = resource_key(rt, rid);
3592 let ci = st
3593 .account(account)
3594 .and_then(|a| a.config_items.get(&key))
3595 .and_then(|h| h.last());
3596 match ci {
3597 Some(ci) => Ok(AwsResponse::ok_json(
3598 json!({ "ConfigurationItem": Self::config_item_json(ci) }),
3599 )),
3600 None => Err(no_such(
3601 "ResourceNotDiscoveredException",
3602 "Resource is not discovered by the aggregator.",
3603 )),
3604 }
3605 }
3606
3607 fn list_aggregate_discovered_resources(
3608 &self,
3609 account: &str,
3610 body: &Value,
3611 ) -> Result<AwsResponse, AwsServiceError> {
3612 let resource_type = require_str(body, "ResourceType")?;
3613 let st = self.state.read();
3614 let mut list = Vec::new();
3615 if let Some(acc) = st.account(account) {
3616 for (key, history) in &acc.config_items {
3617 let Some((rt, _)) = key.split_once('\u{1}') else {
3618 continue;
3619 };
3620 if rt != resource_type {
3621 continue;
3622 }
3623 let Some(ci) = history.last() else { continue };
3624 if ci.configuration_item_status.starts_with("ResourceDeleted") {
3625 continue;
3626 }
3627 list.push(json!({
3628 "SourceAccountId": account,
3629 "SourceRegion": ci.aws_region,
3630 "ResourceId": ci.resource_id,
3631 "ResourceType": ci.resource_type,
3632 "ResourceName": ci.resource_name,
3633 }));
3634 }
3635 }
3636 Ok(AwsResponse::ok_json(json!({ "ResourceIdentifiers": list })))
3637 }
3638
3639 fn get_aggregate_discovered_resource_counts(
3640 &self,
3641 account: &str,
3642 _body: &Value,
3643 ) -> Result<AwsResponse, AwsServiceError> {
3644 let st = self.state.read();
3645 let mut counts: std::collections::BTreeMap<String, i64> = std::collections::BTreeMap::new();
3646 if let Some(acc) = st.account(account) {
3647 for (key, history) in &acc.config_items {
3648 let Some((rt, _)) = key.split_once('\u{1}') else {
3649 continue;
3650 };
3651 if history
3652 .last()
3653 .map(|ci| ci.configuration_item_status.starts_with("ResourceDeleted"))
3654 .unwrap_or(true)
3655 {
3656 continue;
3657 }
3658 *counts.entry(rt.to_string()).or_insert(0) += 1;
3659 }
3660 }
3661 let total: i64 = counts.values().sum();
3662 let list: Vec<Value> = counts
3663 .into_iter()
3664 .map(|(t, c)| json!({ "ResourceType": t, "Count": c }))
3665 .collect();
3666 Ok(AwsResponse::ok_json(
3667 json!({ "TotalDiscoveredResources": total, "GroupedResourceCounts": list }),
3668 ))
3669 }
3670
3671 fn describe_aggregate_compliance_by_config_rules(
3672 &self,
3673 account: &str,
3674 _body: &Value,
3675 ) -> Result<AwsResponse, AwsServiceError> {
3676 let st = self.state.read();
3677 let mut list = Vec::new();
3678 if let Some(acc) = st.account(account) {
3679 for rule in acc.rules.values() {
3680 let c = acc
3681 .evaluations
3682 .get(&rule.name)
3683 .map(|m| fold_compliance(m.values().map(|r| r.compliance_type.as_str())))
3684 .unwrap_or_else(|| "INSUFFICIENT_DATA".into());
3685 list.push(json!({
3686 "ConfigRuleName": rule.name,
3687 "Compliance": { "ComplianceType": c },
3688 "AccountId": account,
3689 "AwsRegion": "us-east-1",
3690 }));
3691 }
3692 }
3693 Ok(AwsResponse::ok_json(
3694 json!({ "AggregateComplianceByConfigRules": list }),
3695 ))
3696 }
3697
3698 fn describe_aggregate_compliance_by_conformance_packs(
3699 &self,
3700 account: &str,
3701 _body: &Value,
3702 ) -> Result<AwsResponse, AwsServiceError> {
3703 let st = self.state.read();
3704 let list: Vec<Value> = st
3705 .account(account)
3706 .map(|a| {
3707 a.conformance_packs
3708 .values()
3709 .map(|p| {
3710 let rules = Self::pack_rule_names(a, p);
3711 let noncompliant = rules.iter().any(|r| a.evaluations.get(r).map(|m| m.values().any(|e| e.compliance_type == "NON_COMPLIANT")).unwrap_or(false));
3712 json!({
3713 "ConformancePackName": p.name,
3714 "Compliance": { "ComplianceType": if noncompliant { "NON_COMPLIANT" } else { "COMPLIANT" } },
3715 "AccountId": account,
3716 "AwsRegion": "us-east-1",
3717 })
3718 })
3719 .collect()
3720 })
3721 .unwrap_or_default();
3722 Ok(AwsResponse::ok_json(
3723 json!({ "AggregateComplianceByConformancePacks": list }),
3724 ))
3725 }
3726
3727 fn get_aggregate_compliance_details_by_config_rule(
3728 &self,
3729 account: &str,
3730 body: &Value,
3731 ) -> Result<AwsResponse, AwsServiceError> {
3732 let name = require_str(body, "ConfigRuleName")?;
3733 let st = self.state.read();
3734 let results: Vec<Value> = st
3735 .account(account)
3736 .and_then(|a| a.evaluations.get(&name))
3737 .map(|m| {
3738 m.values()
3739 .map(|r| {
3740 json!({
3741 "AccountId": account,
3742 "AwsRegion": "us-east-1",
3743 "ComplianceType": r.compliance_type,
3744 "ResultRecordedTime": r.result_recorded_time.timestamp() as f64,
3745 "ConfigRuleInvokedTime": r.config_rule_invoked_time.timestamp() as f64,
3746 "Annotation": r.annotation,
3747 "EvaluationResultIdentifier": {
3748 "EvaluationResultQualifier": {
3749 "ConfigRuleName": name,
3750 "ResourceType": r.resource_type,
3751 "ResourceId": r.resource_id,
3752 },
3753 "OrderingTimestamp": r.ordering_timestamp.timestamp() as f64,
3754 },
3755 })
3756 })
3757 .collect()
3758 })
3759 .unwrap_or_default();
3760 Ok(AwsResponse::ok_json(
3761 json!({ "AggregateEvaluationResults": results }),
3762 ))
3763 }
3764
3765 fn get_aggregate_config_rule_compliance_summary(
3766 &self,
3767 account: &str,
3768 _body: &Value,
3769 ) -> Result<AwsResponse, AwsServiceError> {
3770 let st = self.state.read();
3771 let mut compliant = 0;
3772 let mut noncompliant = 0;
3773 if let Some(acc) = st.account(account) {
3774 for rule in acc.rules.values() {
3775 match acc
3776 .evaluations
3777 .get(&rule.name)
3778 .map(|m| fold_compliance(m.values().map(|r| r.compliance_type.as_str())))
3779 .unwrap_or_else(|| "INSUFFICIENT_DATA".into())
3780 .as_str()
3781 {
3782 "COMPLIANT" => compliant += 1,
3783 "NON_COMPLIANT" => noncompliant += 1,
3784 _ => {}
3785 }
3786 }
3787 }
3788 Ok(AwsResponse::ok_json(json!({
3789 "GroupByKey": "ACCOUNT_ID",
3790 "AggregateComplianceCounts": [{
3791 "GroupName": account,
3792 "ComplianceSummary": {
3793 "CompliantResourceCount": { "CappedCount": compliant, "CapExceeded": false },
3794 "NonCompliantResourceCount": { "CappedCount": noncompliant, "CapExceeded": false },
3795 "ComplianceSummaryTimestamp": Utc::now().timestamp() as f64,
3796 }
3797 }]
3798 })))
3799 }
3800
3801 fn get_aggregate_conformance_pack_compliance_summary(
3802 &self,
3803 account: &str,
3804 _body: &Value,
3805 ) -> Result<AwsResponse, AwsServiceError> {
3806 let st = self.state.read();
3807 let mut compliant = 0;
3808 let mut noncompliant = 0;
3809 if let Some(acc) = st.account(account) {
3810 for p in acc.conformance_packs.values() {
3811 let rules = Self::pack_rule_names(acc, p);
3812 let nc = rules.iter().any(|r| {
3813 acc.evaluations
3814 .get(r)
3815 .map(|m| m.values().any(|e| e.compliance_type == "NON_COMPLIANT"))
3816 .unwrap_or(false)
3817 });
3818 if nc {
3819 noncompliant += 1;
3820 } else {
3821 compliant += 1;
3822 }
3823 }
3824 }
3825 Ok(AwsResponse::ok_json(json!({
3826 "GroupByKey": "ACCOUNT_ID",
3827 "AggregateConformancePackComplianceSummaries": [{
3828 "GroupName": account,
3829 "ComplianceSummary": {
3830 "CompliantConformancePackCount": compliant,
3831 "NonCompliantConformancePackCount": noncompliant,
3832 }
3833 }]
3834 })))
3835 }
3836}
3837
3838impl ConfigService {
3841 fn put_retention_configuration(
3842 &self,
3843 account: &str,
3844 body: &Value,
3845 ) -> Result<AwsResponse, AwsServiceError> {
3846 let days = body
3847 .get("RetentionPeriodInDays")
3848 .and_then(Value::as_i64)
3849 .ok_or_else(|| invalid("RetentionPeriodInDays is required"))?;
3850 if !(30..=2557).contains(&days) {
3851 return Err(invalid("RetentionPeriodInDays must be between 30 and 2557"));
3852 }
3853 let mut st = self.state.write();
3854 st.account_mut(account).retention_configurations.insert(
3855 "default".into(),
3856 RetentionConfiguration {
3857 name: "default".into(),
3858 retention_period_in_days: days,
3859 },
3860 );
3861 Ok(AwsResponse::ok_json(
3862 json!({ "RetentionConfiguration": { "Name": "default", "RetentionPeriodInDays": days } }),
3863 ))
3864 }
3865
3866 fn describe_retention_configurations(
3867 &self,
3868 account: &str,
3869 body: &Value,
3870 ) -> Result<AwsResponse, AwsServiceError> {
3871 let st = self.state.read();
3872 let list: Vec<Value> = st
3873 .account(account)
3874 .map(|a| a.retention_configurations.values().map(|r| json!({ "Name": r.name, "RetentionPeriodInDays": r.retention_period_in_days })).collect())
3875 .unwrap_or_default();
3876 Ok(paged_response(
3877 "RetentionConfigurations",
3878 list,
3879 body,
3880 "NextToken",
3881 ))
3882 }
3883
3884 fn delete_retention_configuration(
3885 &self,
3886 account: &str,
3887 body: &Value,
3888 ) -> Result<AwsResponse, AwsServiceError> {
3889 let name = require_str(body, "RetentionConfigurationName")?;
3890 let mut st = self.state.write();
3891 let acc = st.account_mut(account);
3892 if acc.retention_configurations.remove(&name).is_none() {
3893 return Err(no_such(
3894 "NoSuchRetentionConfigurationException",
3895 format!("Cannot find retention configuration with the specified name '{name}'."),
3896 ));
3897 }
3898 Ok(AwsResponse::ok_json(json!({})))
3899 }
3900
3901 fn put_stored_query(
3902 &self,
3903 account: &str,
3904 region: &str,
3905 body: &Value,
3906 ) -> Result<AwsResponse, AwsServiceError> {
3907 let q = body
3908 .get("StoredQuery")
3909 .cloned()
3910 .ok_or_else(|| invalid("StoredQuery is required"))?;
3911 let name = q
3912 .get("QueryName")
3913 .and_then(Value::as_str)
3914 .ok_or_else(|| invalid("QueryName is required"))?
3915 .to_string();
3916 let mut st = self.state.write();
3917 let acc = st.account_mut(account);
3918 let existing = acc.stored_queries.get(&name);
3919 let id = existing
3920 .map(|s| s.id.clone())
3921 .unwrap_or_else(|| Uuid::new_v4().to_string());
3922 let arn = existing.map(|s| s.arn.clone()).unwrap_or_else(|| {
3923 format!("arn:aws:config:{region}:{account}:stored-query/{name}/{id}")
3924 });
3925 acc.stored_queries.insert(
3926 name.clone(),
3927 StoredQuery {
3928 id: id.clone(),
3929 name,
3930 arn: arn.clone(),
3931 description: q
3932 .get("Description")
3933 .and_then(Value::as_str)
3934 .map(String::from),
3935 expression: q
3936 .get("Expression")
3937 .and_then(Value::as_str)
3938 .map(String::from),
3939 },
3940 );
3941 let _ = id;
3942 let tags = parse_create_tags(body);
3943 if !tags.is_empty() {
3944 let entry = acc.tags.entry(arn.clone()).or_default();
3945 for (k, v) in tags {
3946 entry.insert(k, v);
3947 }
3948 }
3949 Ok(AwsResponse::ok_json(json!({ "QueryArn": arn })))
3950 }
3951
3952 fn get_stored_query(
3953 &self,
3954 account: &str,
3955 body: &Value,
3956 ) -> Result<AwsResponse, AwsServiceError> {
3957 let name = require_str(body, "QueryName")?;
3958 let st = self.state.read();
3959 let q = st
3960 .account(account)
3961 .and_then(|a| a.stored_queries.get(&name));
3962 let Some(q) = q else {
3963 return Err(no_such(
3964 "ResourceNotFoundException",
3965 format!("The stored query '{name}' does not exist."),
3966 ));
3967 };
3968 Ok(AwsResponse::ok_json(json!({
3969 "StoredQuery": {
3970 "QueryId": q.id,
3971 "QueryArn": q.arn,
3972 "QueryName": q.name,
3973 "Description": q.description,
3974 "Expression": q.expression,
3975 }
3976 })))
3977 }
3978
3979 fn delete_stored_query(
3980 &self,
3981 account: &str,
3982 body: &Value,
3983 ) -> Result<AwsResponse, AwsServiceError> {
3984 let name = require_str(body, "QueryName")?;
3985 let mut st = self.state.write();
3986 let acc = st.account_mut(account);
3987 if acc.stored_queries.remove(&name).is_none() {
3988 return Err(no_such(
3989 "ResourceNotFoundException",
3990 format!("The stored query '{name}' does not exist."),
3991 ));
3992 }
3993 Ok(AwsResponse::ok_json(json!({})))
3994 }
3995
3996 fn list_stored_queries(
3997 &self,
3998 account: &str,
3999 body: &Value,
4000 ) -> Result<AwsResponse, AwsServiceError> {
4001 let st = self.state.read();
4002 let list: Vec<Value> = st
4003 .account(account)
4004 .map(|a| a.stored_queries.values().map(|q| json!({ "QueryId": q.id, "QueryArn": q.arn, "QueryName": q.name, "Description": q.description })).collect())
4005 .unwrap_or_default();
4006 Ok(paged_response(
4007 "StoredQueryMetadata",
4008 list,
4009 body,
4010 "NextToken",
4011 ))
4012 }
4013
4014 fn start_resource_evaluation(
4015 &self,
4016 account: &str,
4017 body: &Value,
4018 ) -> Result<AwsResponse, AwsServiceError> {
4019 let mode = body
4020 .get("EvaluationMode")
4021 .and_then(Value::as_str)
4022 .unwrap_or("DETECTIVE")
4023 .to_string();
4024 let details = body.get("ResourceDetails").cloned();
4025 let (rtype, rid, config_str) = details
4028 .as_ref()
4029 .map(|d| {
4030 let rt = d
4031 .get("ResourceType")
4032 .and_then(Value::as_str)
4033 .unwrap_or_default()
4034 .to_string();
4035 let ri = d
4036 .get("ResourceId")
4037 .and_then(Value::as_str)
4038 .unwrap_or_default()
4039 .to_string();
4040 let cfg = match d.get("ResourceConfiguration") {
4043 Some(Value::String(s)) => s.clone(),
4044 Some(v) => v.to_string(),
4045 None => "null".to_string(),
4046 };
4047 (rt, ri, cfg)
4048 })
4049 .unwrap_or_default();
4050 let id = Uuid::new_v4().to_string();
4051 let mut st = self.state.write();
4052 let acc = st.account_mut(account);
4053 let evaluation_results = if rtype.is_empty() || rid.is_empty() {
4054 Vec::new()
4055 } else {
4056 validate::evaluate_proactive(acc, &rtype, &rid, &config_str)
4057 };
4058 acc.resource_evaluations.insert(
4059 id.clone(),
4060 ResourceEvaluation {
4061 resource_evaluation_id: id.clone(),
4062 evaluation_mode: mode,
4063 time_stamp: Utc::now(),
4064 status: "SUCCEEDED".into(),
4065 resource_details: details,
4066 evaluation_context: body.get("EvaluationContext").cloned(),
4067 evaluation_results,
4068 },
4069 );
4070 Ok(AwsResponse::ok_json(json!({ "ResourceEvaluationId": id })))
4071 }
4072
4073 fn get_resource_evaluation_summary(
4074 &self,
4075 account: &str,
4076 body: &Value,
4077 ) -> Result<AwsResponse, AwsServiceError> {
4078 let id = require_str(body, "ResourceEvaluationId")?;
4079 let st = self.state.read();
4080 let ev = st
4081 .account(account)
4082 .and_then(|a| a.resource_evaluations.get(&id));
4083 let Some(ev) = ev else {
4084 return Err(no_such(
4085 "ResourceNotFoundException",
4086 format!("ResourceEvaluationId '{id}' does not exist."),
4087 ));
4088 };
4089 let resource_type = ev
4090 .resource_details
4091 .as_ref()
4092 .and_then(|d| d.get("ResourceType"))
4093 .cloned()
4094 .unwrap_or(Value::Null);
4095 let resource_id = ev
4096 .resource_details
4097 .as_ref()
4098 .and_then(|d| d.get("ResourceId"))
4099 .cloned()
4100 .unwrap_or(Value::Null);
4101 let compliance = if ev.evaluation_results.is_empty() {
4104 "INSUFFICIENT_DATA".to_string()
4105 } else {
4106 fold_compliance(
4107 ev.evaluation_results
4108 .iter()
4109 .map(|r| r.compliance_type.as_str()),
4110 )
4111 };
4112 Ok(AwsResponse::ok_json(json!({
4113 "ResourceEvaluationId": ev.resource_evaluation_id,
4114 "EvaluationMode": ev.evaluation_mode,
4115 "EvaluationStatus": { "Status": ev.status },
4116 "EvaluationStartTimestamp": ev.time_stamp.timestamp() as f64,
4117 "Compliance": compliance,
4118 "ResourceDetails": { "ResourceId": resource_id, "ResourceType": resource_type },
4119 })))
4120 }
4121
4122 fn list_resource_evaluations(
4123 &self,
4124 account: &str,
4125 body: &Value,
4126 ) -> Result<AwsResponse, AwsServiceError> {
4127 let st = self.state.read();
4128 let list: Vec<Value> = st
4129 .account(account)
4130 .map(|a| {
4131 a.resource_evaluations
4132 .values()
4133 .map(|e| json!({ "ResourceEvaluationId": e.resource_evaluation_id, "EvaluationMode": e.evaluation_mode, "EvaluationStartTimestamp": e.time_stamp.timestamp() as f64 }))
4134 .collect()
4135 })
4136 .unwrap_or_default();
4137 Ok(paged_response(
4138 "ResourceEvaluations",
4139 list,
4140 body,
4141 "NextToken",
4142 ))
4143 }
4144
4145 fn select_resource_config(
4146 &self,
4147 account: &str,
4148 body: &Value,
4149 aggregate: bool,
4150 ) -> Result<AwsResponse, AwsServiceError> {
4151 let expr = require_str(body, "Expression")?;
4152 let st = self.state.read();
4153 let empty = AccountState::default();
4154 let acc = st.account(account).unwrap_or(&empty);
4155 let rows = validate::run_select(&expr, acc)
4156 .map_err(|e| invalid(format!("Invalid SelectResourceConfig expression: {e}")))?;
4157 let results: Vec<Value> = rows.iter().map(|r| json!(r.to_string())).collect();
4158 let (page, next) = paginate(results, body);
4159 let mut out = json!({
4160 "Results": page,
4161 "QueryInfo": { "SelectFields": select_fields(&expr) },
4162 });
4163 if let Some(t) = next {
4164 out["NextToken"] = json!(t);
4165 }
4166 if aggregate {
4167 let _ = out.as_object_mut();
4168 }
4169 Ok(AwsResponse::ok_json(out))
4170 }
4171
4172 fn tag_resource(&self, account: &str, body: &Value) -> Result<AwsResponse, AwsServiceError> {
4173 let arn = require_str(body, "ResourceArn")?;
4174 let tags = body
4175 .get("Tags")
4176 .and_then(Value::as_array)
4177 .cloned()
4178 .unwrap_or_default();
4179 let mut st = self.state.write();
4180 let acc = st.account_mut(account);
4181 let entry = acc.tags.entry(arn).or_default();
4182 for t in tags {
4183 if let (Some(k), Some(v)) = (
4184 t.get("Key").and_then(Value::as_str),
4185 t.get("Value").and_then(Value::as_str),
4186 ) {
4187 entry.insert(k.to_string(), v.to_string());
4188 }
4189 }
4190 Ok(AwsResponse::ok_json(json!({})))
4191 }
4192
4193 fn untag_resource(&self, account: &str, body: &Value) -> Result<AwsResponse, AwsServiceError> {
4194 let arn = require_str(body, "ResourceArn")?;
4195 let keys = string_list(body, "TagKeys");
4196 let mut st = self.state.write();
4197 let acc = st.account_mut(account);
4198 if let Some(entry) = acc.tags.get_mut(&arn) {
4199 for k in keys {
4200 entry.remove(&k);
4201 }
4202 }
4203 Ok(AwsResponse::ok_json(json!({})))
4204 }
4205
4206 fn list_tags_for_resource(
4207 &self,
4208 account: &str,
4209 body: &Value,
4210 ) -> Result<AwsResponse, AwsServiceError> {
4211 let arn = require_str(body, "ResourceArn")?;
4212 let st = self.state.read();
4213 let list: Vec<Value> = st
4214 .account(account)
4215 .and_then(|a| a.tags.get(&arn))
4216 .map(|m| {
4217 m.iter()
4218 .map(|(k, v)| json!({ "Key": k, "Value": v }))
4219 .collect()
4220 })
4221 .unwrap_or_default();
4222 Ok(AwsResponse::ok_json(json!({ "Tags": list })))
4223 }
4224}
4225
4226fn parse_create_tags(body: &Value) -> Vec<(String, String)> {
4233 body.get("Tags")
4234 .and_then(Value::as_array)
4235 .map(|arr| {
4236 arr.iter()
4237 .filter_map(|t| {
4238 Some((
4239 t.get("Key").and_then(Value::as_str)?.to_string(),
4240 t.get("Value").and_then(Value::as_str)?.to_string(),
4241 ))
4242 })
4243 .collect()
4244 })
4245 .unwrap_or_default()
4246}
4247
4248fn account_id(req: &AwsRequest) -> String {
4249 if req.account_id.is_empty() {
4250 "000000000000".to_string()
4251 } else {
4252 req.account_id.clone()
4253 }
4254}
4255
4256fn region(req: &AwsRequest) -> String {
4257 if req.region.is_empty() {
4258 "us-east-1".to_string()
4259 } else {
4260 req.region.clone()
4261 }
4262}
4263
4264fn short_id() -> String {
4265 fakecloud_core::ids::short_id(6)
4266}
4267
4268fn invalid(msg: impl Into<String>) -> AwsServiceError {
4269 AwsServiceError::aws_error(
4270 StatusCode::BAD_REQUEST,
4271 "InvalidParameterValueException",
4272 msg,
4273 )
4274}
4275
4276fn no_such(code: &str, msg: impl Into<String>) -> AwsServiceError {
4277 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, code, msg)
4278}
4279
4280fn require_str(body: &Value, field: &str) -> Result<String, AwsServiceError> {
4281 body.get(field)
4282 .and_then(Value::as_str)
4283 .filter(|s| !s.is_empty())
4284 .map(String::from)
4285 .ok_or_else(|| invalid(format!("{field} is required")))
4286}
4287
4288fn string_list(body: &Value, field: &str) -> Vec<String> {
4289 body.get(field)
4290 .and_then(Value::as_array)
4291 .map(|a| {
4292 a.iter()
4293 .filter_map(|v| v.as_str().map(String::from))
4294 .collect()
4295 })
4296 .unwrap_or_default()
4297}
4298
4299fn page_limit(body: &Value) -> Option<usize> {
4303 ["Limit", "MaxResults", "limit"]
4304 .iter()
4305 .find_map(|k| body.get(*k).and_then(Value::as_i64))
4306 .filter(|l| *l > 0)
4307 .map(|l| l as usize)
4308}
4309
4310fn page_token(body: &Value) -> Option<usize> {
4313 ["NextToken", "nextToken"]
4314 .iter()
4315 .find_map(|k| body.get(*k).and_then(Value::as_str))
4316 .filter(|s| !s.is_empty())
4317 .and_then(decode_page_token)
4318}
4319
4320fn encode_page_token(offset: usize) -> String {
4324 format!("cfg:{offset}")
4325}
4326
4327fn decode_page_token(token: &str) -> Option<usize> {
4328 token.strip_prefix("cfg:").and_then(|n| n.parse().ok())
4329}
4330
4331fn paginate(items: Vec<Value>, body: &Value) -> (Vec<Value>, Option<String>) {
4334 let total = items.len();
4335 let start = page_token(body).unwrap_or(0).min(total);
4336 let end = match page_limit(body) {
4337 Some(limit) => start.saturating_add(limit).min(total),
4338 None => total,
4339 };
4340 let next = if end < total {
4341 Some(encode_page_token(end))
4342 } else {
4343 None
4344 };
4345 (
4346 items.into_iter().skip(start).take(end - start).collect(),
4347 next,
4348 )
4349}
4350
4351fn paged_response(field: &str, items: Vec<Value>, body: &Value, token_field: &str) -> AwsResponse {
4355 let (page, next) = paginate(items, body);
4356 let mut out = json!({ field: page });
4357 if let Some(t) = next {
4358 out[token_field] = json!(t);
4359 }
4360 AwsResponse::ok_json(out)
4361}
4362
4363fn custom_rule_event(
4368 account: &str,
4369 rule_name: &str,
4370 input_parameters: Option<&str>,
4371 result_token: &str,
4372) -> Value {
4373 let invoking_event = json!({
4374 "awsAccountId": account,
4375 "configRuleName": rule_name,
4376 "messageType": "ScheduledNotification",
4377 "notificationCreationTime": Utc::now().to_rfc3339(),
4378 })
4379 .to_string();
4380 let rule_parameters = input_parameters
4381 .filter(|p| !p.is_empty())
4382 .map(String::from)
4383 .unwrap_or_else(|| "{}".to_string());
4384 json!({
4385 "invokingEvent": invoking_event,
4386 "ruleParameters": rule_parameters,
4387 "resultToken": result_token,
4388 "configRuleArn": format!("arn:aws:config:us-east-1:{account}:config-rule/{rule_name}"),
4389 "configRuleName": rule_name,
4390 "accountId": account,
4391 })
4392}
4393
4394fn select_fields(expr: &str) -> Vec<Value> {
4396 let lower = expr.to_ascii_lowercase();
4397 let Some(start) = lower.find("select ") else {
4398 return Vec::new();
4399 };
4400 let after = &expr[start + 7..];
4401 let end = after
4402 .to_ascii_lowercase()
4403 .find(" where ")
4404 .unwrap_or(after.len());
4405 after[..end]
4406 .split(',')
4407 .map(|s| json!({ "Name": s.trim() }))
4408 .collect()
4409}
4410
4411#[cfg(test)]
4412mod unit_tests {
4413 use super::*;
4414
4415 #[test]
4416 fn custom_rule_event_passes_stored_input_parameters() {
4417 let params = r#"{"maxAccessKeyAge":"90"}"#;
4418 let event = custom_rule_event("111122223333", "my-rule", Some(params), "my-rule#tok");
4419 assert_eq!(event["ruleParameters"], json!(params));
4422 assert_eq!(event["configRuleName"], json!("my-rule"));
4423 assert_eq!(event["accountId"], json!("111122223333"));
4424 assert_eq!(event["resultToken"], json!("my-rule#tok"));
4425 }
4426
4427 #[test]
4428 fn custom_rule_event_defaults_empty_parameters_to_empty_object() {
4429 let none = custom_rule_event("111122223333", "r", None, "r#t");
4430 assert_eq!(none["ruleParameters"], json!("{}"));
4431 let empty = custom_rule_event("111122223333", "r", Some(""), "r#t");
4432 assert_eq!(empty["ruleParameters"], json!("{}"));
4433 }
4434
4435 #[test]
4436 fn paginate_slices_and_emits_token() {
4437 let items: Vec<Value> = (0..5).map(|i| json!(i)).collect();
4438 let first_req = json!({ "Limit": 2 });
4439 let (page, next) = paginate(items.clone(), &first_req);
4440 assert_eq!(page, vec![json!(0), json!(1)]);
4441 let token = next.expect("more items remain -> token");
4442
4443 let second_req = json!({ "Limit": 2, "NextToken": token });
4444 let (page2, next2) = paginate(items.clone(), &second_req);
4445 assert_eq!(page2, vec![json!(2), json!(3)]);
4446 let token2 = next2.expect("still more");
4447
4448 let third_req = json!({ "Limit": 2, "NextToken": token2 });
4449 let (page3, next3) = paginate(items, &third_req);
4450 assert_eq!(page3, vec![json!(4)]);
4451 assert!(next3.is_none(), "last page has no token");
4452 }
4453
4454 #[test]
4455 fn paginate_no_limit_returns_all() {
4456 let items: Vec<Value> = (0..3).map(|i| json!(i)).collect();
4457 let (page, next) = paginate(items.clone(), &json!({}));
4458 assert_eq!(page, items);
4459 assert!(next.is_none());
4460 }
4461
4462 #[test]
4463 fn extract_pack_rule_names_parses_yaml_template() {
4464 let yaml = r#"
4465Resources:
4466 MyRule:
4467 Type: AWS::Config::ConfigRule
4468 Properties:
4469 ConfigRuleName: yaml-declared-rule
4470 Source:
4471 Owner: AWS
4472 SourceIdentifier: S3_BUCKET_VERSIONING_ENABLED
4473"#;
4474 let names = extract_pack_rule_names(Some(yaml));
4475 assert_eq!(names, vec!["yaml-declared-rule".to_string()]);
4476 }
4477}