1use std::collections::HashMap;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Arc;
9
10use async_trait::async_trait;
11use chrono::{DateTime, 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 expiration_time = body
2399 .get("ExpirationTime")
2400 .and_then(Value::as_f64)
2401 .and_then(|s| DateTime::from_timestamp(s as i64, 0));
2402 let mut st = self.state.write();
2403 let acc = st.account_mut(account);
2404 for k in &keys {
2405 let rt = k
2406 .get("ResourceType")
2407 .and_then(Value::as_str)
2408 .unwrap_or_default()
2409 .to_string();
2410 let rid = k
2411 .get("ResourceId")
2412 .and_then(Value::as_str)
2413 .unwrap_or_default()
2414 .to_string();
2415 let key = format!("{rule_name}\u{1}{rt}\u{1}{rid}");
2416 acc.remediation_exceptions.insert(
2417 key,
2418 RemediationException {
2419 config_rule_name: rule_name.clone(),
2420 resource_type: rt,
2421 resource_id: rid,
2422 message: message.clone(),
2423 expiration_time,
2424 },
2425 );
2426 }
2427 Ok(AwsResponse::ok_json(json!({ "FailedBatches": [] })))
2428 }
2429
2430 fn describe_remediation_exceptions(
2431 &self,
2432 account: &str,
2433 body: &Value,
2434 ) -> Result<AwsResponse, AwsServiceError> {
2435 let rule_name = require_str(body, "ConfigRuleName")?;
2436 let st = self.state.read();
2437 let list: Vec<Value> = st
2438 .account(account)
2439 .map(|a| {
2440 a.remediation_exceptions
2441 .values()
2442 .filter(|e| e.config_rule_name == rule_name)
2443 .map(|e| {
2444 let mut v = json!({
2445 "ConfigRuleName": e.config_rule_name,
2446 "ResourceType": e.resource_type,
2447 "ResourceId": e.resource_id,
2448 "Message": e.message,
2449 });
2450 if let Some(exp) = e.expiration_time {
2451 v["ExpirationTime"] = json!(exp.timestamp() as f64);
2452 }
2453 v
2454 })
2455 .collect()
2456 })
2457 .unwrap_or_default();
2458 Ok(paged_response(
2459 "RemediationExceptions",
2460 list,
2461 body,
2462 "NextToken",
2463 ))
2464 }
2465
2466 fn delete_remediation_exceptions(
2467 &self,
2468 account: &str,
2469 body: &Value,
2470 ) -> Result<AwsResponse, AwsServiceError> {
2471 let rule_name = require_str(body, "ConfigRuleName")?;
2472 let keys = body
2473 .get("ResourceKeys")
2474 .and_then(Value::as_array)
2475 .cloned()
2476 .unwrap_or_default();
2477 let mut st = self.state.write();
2478 let acc = st.account_mut(account);
2479 for k in &keys {
2480 let rt = k
2481 .get("ResourceType")
2482 .and_then(Value::as_str)
2483 .unwrap_or_default();
2484 let rid = k
2485 .get("ResourceId")
2486 .and_then(Value::as_str)
2487 .unwrap_or_default();
2488 acc.remediation_exceptions
2489 .remove(&format!("{rule_name}\u{1}{rt}\u{1}{rid}"));
2490 }
2491 Ok(AwsResponse::ok_json(json!({ "FailedBatches": [] })))
2492 }
2493
2494 fn start_remediation_execution(
2495 &self,
2496 account: &str,
2497 body: &Value,
2498 ) -> Result<AwsResponse, AwsServiceError> {
2499 let rule_name = require_str(body, "ConfigRuleName")?;
2500 let keys = body
2501 .get("ResourceKeys")
2502 .and_then(Value::as_array)
2503 .cloned()
2504 .unwrap_or_default();
2505 let now = Utc::now();
2506 let mut st = self.state.write();
2507 let acc = st.account_mut(account);
2508 if !acc.remediation_configs.contains_key(&rule_name) {
2509 return Err(no_such(
2510 "NoSuchRemediationConfigurationException",
2511 "No RemediationConfiguration for rule exists.",
2512 ));
2513 }
2514 let mut failed = Vec::new();
2519 for k in &keys {
2520 let (Some(rt), Some(rid)) = (
2521 k.get("resourceType")
2522 .or_else(|| k.get("ResourceType"))
2523 .and_then(Value::as_str),
2524 k.get("resourceId")
2525 .or_else(|| k.get("ResourceId"))
2526 .and_then(Value::as_str),
2527 ) else {
2528 failed.push(k.clone());
2529 continue;
2530 };
2531 let key = format!("{rule_name}\u{1}{rt}\u{1}{rid}");
2532 acc.remediation_executions.insert(
2533 key,
2534 RemediationExecutionStatus {
2535 config_rule_name: rule_name.clone(),
2536 resource_type: rt.to_string(),
2537 resource_id: rid.to_string(),
2538 state: "QUEUED".into(),
2539 invocation_time: now,
2540 last_updated_time: now,
2541 },
2542 );
2543 }
2544 Ok(AwsResponse::ok_json(json!({ "FailedItems": failed })))
2545 }
2546
2547 fn describe_remediation_execution_status(
2548 &self,
2549 account: &str,
2550 body: &Value,
2551 ) -> Result<AwsResponse, AwsServiceError> {
2552 let rule_name = require_str(body, "ConfigRuleName")?;
2553 let keys = body
2554 .get("ResourceKeys")
2555 .and_then(Value::as_array)
2556 .cloned()
2557 .unwrap_or_default();
2558 let wanted: Vec<(String, String)> = keys
2560 .iter()
2561 .filter_map(|k| {
2562 let rt = k
2563 .get("resourceType")
2564 .or_else(|| k.get("ResourceType"))
2565 .and_then(Value::as_str)?;
2566 let rid = k
2567 .get("resourceId")
2568 .or_else(|| k.get("ResourceId"))
2569 .and_then(Value::as_str)?;
2570 Some((rt.to_string(), rid.to_string()))
2571 })
2572 .collect();
2573 let st = self.state.read();
2574 let list: Vec<Value> = st
2575 .account(account)
2576 .map(|a| {
2577 a.remediation_executions
2578 .values()
2579 .filter(|e| e.config_rule_name == rule_name)
2580 .filter(|e| {
2581 wanted.is_empty()
2582 || wanted
2583 .iter()
2584 .any(|(rt, rid)| *rt == e.resource_type && *rid == e.resource_id)
2585 })
2586 .map(|e| {
2587 let inv = e.invocation_time.timestamp() as f64;
2588 let upd = e.last_updated_time.timestamp() as f64;
2589 json!({
2590 "ResourceKey": { "resourceType": e.resource_type, "resourceId": e.resource_id },
2591 "State": e.state,
2592 "StepDetails": [{ "Name": "remediate", "State": e.state, "StartTime": inv }],
2593 "InvocationTime": inv,
2594 "LastUpdatedTime": upd,
2595 })
2596 })
2597 .collect()
2598 })
2599 .unwrap_or_default();
2600 Ok(paged_response(
2601 "RemediationExecutionStatuses",
2602 list,
2603 body,
2604 "NextToken",
2605 ))
2606 }
2607}
2608
2609fn extract_pack_rule_names(template: Option<&str>) -> Vec<String> {
2617 let Some(t) = template else { return Vec::new() };
2618 let v: Value = serde_json::from_str(t)
2621 .or_else(|_| serde_yaml::from_str::<Value>(t))
2622 .unwrap_or(Value::Null);
2623 let Some(resources) = v.get("Resources").and_then(Value::as_object) else {
2624 return Vec::new();
2625 };
2626 resources
2627 .values()
2628 .filter(|r| r.get("Type").and_then(Value::as_str) == Some("AWS::Config::ConfigRule"))
2629 .filter_map(|r| {
2630 r.pointer("/Properties/ConfigRuleName")
2631 .and_then(Value::as_str)
2632 .map(String::from)
2633 })
2634 .collect()
2635}
2636
2637impl ConfigService {
2638 fn put_conformance_pack(
2639 &self,
2640 account: &str,
2641 region: &str,
2642 body: &Value,
2643 ) -> Result<AwsResponse, AwsServiceError> {
2644 let name = require_str(body, "ConformancePackName")?;
2645 let template_body = body
2646 .get("TemplateBody")
2647 .and_then(Value::as_str)
2648 .map(String::from);
2649 let template_s3_uri = body
2650 .get("TemplateS3Uri")
2651 .and_then(Value::as_str)
2652 .map(String::from);
2653 if template_body.is_none()
2654 && template_s3_uri.is_none()
2655 && body.get("TemplateSSMDocumentDetails").is_none()
2656 {
2657 return Err(invalid(
2658 "Either TemplateBody, TemplateS3Uri, or TemplateSSMDocumentDetails is required",
2659 ));
2660 }
2661 let now = Utc::now();
2662 let arn = format!(
2663 "arn:aws:config:{region}:{account}:conformance-pack/{name}-{}",
2664 short_id()
2665 );
2666 let id = format!("conformance-pack-{}", short_id());
2667 let rule_names = extract_pack_rule_names(template_body.as_deref());
2668 let mut st = self.state.write();
2669 let acc = st.account_mut(account);
2670 let existing = acc.conformance_packs.get(&name);
2671 let pack = ConformancePack {
2672 name: name.clone(),
2673 arn: existing.map(|p| p.arn.clone()).unwrap_or(arn.clone()),
2674 id: existing.map(|p| p.id.clone()).unwrap_or(id),
2675 delivery_s3_bucket: body
2676 .get("DeliveryS3Bucket")
2677 .and_then(Value::as_str)
2678 .map(String::from),
2679 delivery_s3_key_prefix: body
2680 .get("DeliveryS3KeyPrefix")
2681 .and_then(Value::as_str)
2682 .map(String::from),
2683 input_parameters: body
2684 .get("ConformancePackInputParameters")
2685 .and_then(Value::as_array)
2686 .cloned()
2687 .unwrap_or_default(),
2688 template_body,
2689 template_s3_uri,
2690 template_ssm_document_details: body.get("TemplateSSMDocumentDetails").cloned(),
2691 last_update_requested_time: now,
2692 created_by: None,
2693 rule_names,
2694 };
2695 let out_arn = pack.arn.clone();
2696 acc.conformance_packs.insert(name, pack);
2697 let tags = parse_create_tags(body);
2698 if !tags.is_empty() {
2699 let entry = acc.tags.entry(out_arn.clone()).or_default();
2700 for (k, v) in tags {
2701 entry.insert(k, v);
2702 }
2703 }
2704 Ok(AwsResponse::ok_json(
2705 json!({ "ConformancePackArn": out_arn }),
2706 ))
2707 }
2708
2709 fn describe_conformance_packs(
2710 &self,
2711 account: &str,
2712 body: &Value,
2713 ) -> Result<AwsResponse, AwsServiceError> {
2714 let names = string_list(body, "ConformancePackNames");
2715 let st = self.state.read();
2716 let list: Vec<Value> = st
2717 .account(account)
2718 .map(|a| {
2719 a.conformance_packs
2720 .values()
2721 .filter(|p| names.is_empty() || names.contains(&p.name))
2722 .map(|p| {
2723 json!({
2724 "ConformancePackName": p.name,
2725 "ConformancePackArn": p.arn,
2726 "ConformancePackId": p.id,
2727 "DeliveryS3Bucket": p.delivery_s3_bucket,
2728 "DeliveryS3KeyPrefix": p.delivery_s3_key_prefix,
2729 "ConformancePackInputParameters": p.input_parameters,
2730 "LastUpdateRequestedTime": p.last_update_requested_time.timestamp() as f64,
2731 "TemplateSSMDocumentDetails": p.template_ssm_document_details,
2732 })
2733 })
2734 .collect()
2735 })
2736 .unwrap_or_default();
2737 if !names.is_empty() {
2738 for n in &names {
2739 if !st
2740 .account(account)
2741 .map(|a| a.conformance_packs.contains_key(n))
2742 .unwrap_or(false)
2743 {
2744 return Err(no_such(
2745 "NoSuchConformancePackException",
2746 format!("Cannot find conformance pack with name '{n}'."),
2747 ));
2748 }
2749 }
2750 }
2751 Ok(paged_response(
2752 "ConformancePackDetails",
2753 list,
2754 body,
2755 "NextToken",
2756 ))
2757 }
2758
2759 fn delete_conformance_pack(
2760 &self,
2761 account: &str,
2762 body: &Value,
2763 ) -> Result<AwsResponse, AwsServiceError> {
2764 let name = require_str(body, "ConformancePackName")?;
2765 let mut st = self.state.write();
2766 let acc = st.account_mut(account);
2767 if acc.conformance_packs.remove(&name).is_none() {
2768 return Err(no_such(
2769 "NoSuchConformancePackException",
2770 format!("Cannot find conformance pack with name '{name}'."),
2771 ));
2772 }
2773 Ok(AwsResponse::ok_json(json!({})))
2774 }
2775
2776 fn describe_conformance_pack_status(
2777 &self,
2778 account: &str,
2779 body: &Value,
2780 ) -> Result<AwsResponse, AwsServiceError> {
2781 let names = string_list(body, "ConformancePackNames");
2782 let now = Utc::now().timestamp() as f64;
2783 let st = self.state.read();
2784 let list: Vec<Value> = st
2785 .account(account)
2786 .map(|a| {
2787 a.conformance_packs
2788 .values()
2789 .filter(|p| names.is_empty() || names.contains(&p.name))
2790 .map(|p| {
2791 json!({
2792 "ConformancePackName": p.name,
2793 "ConformancePackId": p.id,
2794 "ConformancePackArn": p.arn,
2795 "ConformancePackState": "CREATE_COMPLETE",
2796 "StackArn": format!("arn:aws:cloudformation:us-east-1:{account}:stack/awsconfigconforms-{}/{}", p.name, short_id()),
2797 "LastUpdateRequestedTime": now,
2798 "LastUpdateCompletedTime": now,
2799 })
2800 })
2801 .collect()
2802 })
2803 .unwrap_or_default();
2804 Ok(paged_response(
2805 "ConformancePackStatusDetails",
2806 list,
2807 body,
2808 "NextToken",
2809 ))
2810 }
2811
2812 fn pack_rule_names(acc: &AccountState, pack: &ConformancePack) -> Vec<String> {
2815 if pack.rule_names.is_empty() {
2816 acc.rules.keys().cloned().collect()
2817 } else {
2818 pack.rule_names
2819 .iter()
2820 .filter(|n| acc.rules.contains_key(*n))
2821 .cloned()
2822 .collect()
2823 }
2824 }
2825
2826 fn describe_conformance_pack_compliance(
2827 &self,
2828 account: &str,
2829 body: &Value,
2830 ) -> Result<AwsResponse, AwsServiceError> {
2831 let name = require_str(body, "ConformancePackName")?;
2832 let st = self.state.read();
2833 let acc = st.account(account).ok_or_else(|| {
2834 no_such(
2835 "NoSuchConformancePackException",
2836 "No such conformance pack.",
2837 )
2838 })?;
2839 let pack = acc.conformance_packs.get(&name).ok_or_else(|| {
2840 no_such(
2841 "NoSuchConformancePackException",
2842 format!("Cannot find conformance pack with name '{name}'."),
2843 )
2844 })?;
2845 let list: Vec<Value> = Self::pack_rule_names(acc, pack)
2846 .iter()
2847 .map(|rule| {
2848 let c = acc
2849 .evaluations
2850 .get(rule)
2851 .map(|m| fold_compliance(m.values().map(|r| r.compliance_type.as_str())))
2852 .unwrap_or_else(|| "INSUFFICIENT_DATA".into());
2853 json!({ "ConfigRuleName": rule, "ComplianceType": c, "Controls": [] })
2854 })
2855 .collect();
2856 let (page, next) = paginate(list, body);
2857 let mut out =
2858 json!({ "ConformancePackName": name, "ConformancePackRuleComplianceList": page });
2859 if let Some(t) = next {
2860 out["NextToken"] = json!(t);
2861 }
2862 Ok(AwsResponse::ok_json(out))
2863 }
2864
2865 fn get_conformance_pack_compliance_summary(
2866 &self,
2867 account: &str,
2868 body: &Value,
2869 ) -> Result<AwsResponse, AwsServiceError> {
2870 let names = string_list(body, "ConformancePackNames");
2871 let st = self.state.read();
2872 let acc = st.account(account);
2873 let list: Vec<Value> = acc
2874 .map(|a| {
2875 a.conformance_packs
2876 .values()
2877 .filter(|p| names.is_empty() || names.contains(&p.name))
2878 .map(|p| {
2879 let rules = Self::pack_rule_names(a, p);
2880 let noncompliant = rules.iter().any(|r| {
2881 a.evaluations.get(r).map(|m| m.values().any(|e| e.compliance_type == "NON_COMPLIANT")).unwrap_or(false)
2882 });
2883 json!({
2884 "ConformancePackName": p.name,
2885 "ConformancePackComplianceStatus": if noncompliant { "NON_COMPLIANT" } else { "COMPLIANT" },
2886 })
2887 })
2888 .collect()
2889 })
2890 .unwrap_or_default();
2891 Ok(AwsResponse::ok_json(
2892 json!({ "ConformancePackComplianceSummaryList": list }),
2893 ))
2894 }
2895
2896 fn get_conformance_pack_compliance_details(
2897 &self,
2898 account: &str,
2899 body: &Value,
2900 ) -> Result<AwsResponse, AwsServiceError> {
2901 let name = require_str(body, "ConformancePackName")?;
2902 let st = self.state.read();
2903 let acc = st.account(account).ok_or_else(|| {
2904 no_such(
2905 "NoSuchConformancePackException",
2906 "No such conformance pack.",
2907 )
2908 })?;
2909 let pack = acc.conformance_packs.get(&name).ok_or_else(|| {
2910 no_such(
2911 "NoSuchConformancePackException",
2912 format!("Cannot find conformance pack with name '{name}'."),
2913 )
2914 })?;
2915 let mut results = Vec::new();
2916 for rule in Self::pack_rule_names(acc, pack) {
2917 if let Some(m) = acc.evaluations.get(&rule) {
2918 for r in m.values() {
2919 results.push(json!({
2920 "ConfigRuleInvokedTime": r.config_rule_invoked_time.timestamp() as f64,
2921 "ResultRecordedTime": r.result_recorded_time.timestamp() as f64,
2922 "ComplianceType": r.compliance_type,
2923 "EvaluationResultIdentifier": {
2924 "EvaluationResultQualifier": {
2925 "ConfigRuleName": rule,
2926 "ResourceType": r.resource_type,
2927 "ResourceId": r.resource_id,
2928 },
2929 "OrderingTimestamp": r.ordering_timestamp.timestamp() as f64,
2930 },
2931 }));
2932 }
2933 }
2934 }
2935 let (page, next) = paginate(results, body);
2936 let mut out =
2937 json!({ "ConformancePackName": name, "ConformancePackRuleEvaluationResults": page });
2938 if let Some(t) = next {
2939 out["NextToken"] = json!(t);
2940 }
2941 Ok(AwsResponse::ok_json(out))
2942 }
2943
2944 fn list_conformance_pack_compliance_scores(
2945 &self,
2946 account: &str,
2947 ) -> Result<AwsResponse, AwsServiceError> {
2948 let st = self.state.read();
2949 let list: Vec<Value> = st
2950 .account(account)
2951 .map(|a| {
2952 a.conformance_packs
2953 .values()
2954 .map(|p| {
2955 let rules = Self::pack_rule_names(a, p);
2956 let total = rules.len().max(1);
2957 let compliant = rules
2958 .iter()
2959 .filter(|r| {
2960 a.evaluations
2961 .get(*r)
2962 .map(|m| {
2963 !m.values().any(|e| e.compliance_type == "NON_COMPLIANT")
2964 })
2965 .unwrap_or(true)
2966 })
2967 .count();
2968 let score = (compliant as f64 / total as f64) * 100.0;
2969 json!({
2970 "Score": format!("{score:.2}"),
2971 "ConformancePackName": p.name,
2972 "LastUpdatedTime": p.last_update_requested_time.timestamp() as f64,
2973 })
2974 })
2975 .collect()
2976 })
2977 .unwrap_or_default();
2978 Ok(AwsResponse::ok_json(
2979 json!({ "ConformancePackComplianceScores": list }),
2980 ))
2981 }
2982}
2983
2984impl ConfigService {
2987 fn put_organization_config_rule(
2988 &self,
2989 account: &str,
2990 region: &str,
2991 body: &Value,
2992 ) -> Result<AwsResponse, AwsServiceError> {
2993 let name = require_str(body, "OrganizationConfigRuleName")?;
2994 let arn = format!(
2995 "arn:aws:config:{region}:{account}:organization-config-rule/{name}-{}",
2996 short_id()
2997 );
2998 let mut st = self.state.write();
2999 let acc = st.account_mut(account);
3000 let out_arn = acc
3001 .org_rules
3002 .get(&name)
3003 .map(|r| r.arn.clone())
3004 .unwrap_or(arn);
3005 acc.org_rules.insert(
3006 name.clone(),
3007 OrganizationConfigRule {
3008 name,
3009 arn: out_arn.clone(),
3010 managed_rule_metadata: body.get("OrganizationManagedRuleMetadata").cloned(),
3011 custom_rule_metadata: body.get("OrganizationCustomRuleMetadata").cloned(),
3012 custom_policy_rule_metadata: body
3013 .get("OrganizationCustomPolicyRuleMetadata")
3014 .cloned(),
3015 excluded_accounts: string_list(body, "ExcludedAccounts"),
3016 last_update_time: Utc::now(),
3017 },
3018 );
3019 let tags = parse_create_tags(body);
3020 if !tags.is_empty() {
3021 let entry = acc.tags.entry(out_arn.clone()).or_default();
3022 for (k, v) in tags {
3023 entry.insert(k, v);
3024 }
3025 }
3026 Ok(AwsResponse::ok_json(
3027 json!({ "OrganizationConfigRuleArn": out_arn }),
3028 ))
3029 }
3030
3031 fn describe_organization_config_rules(
3032 &self,
3033 account: &str,
3034 body: &Value,
3035 ) -> Result<AwsResponse, AwsServiceError> {
3036 let names = string_list(body, "OrganizationConfigRuleNames");
3037 let st = self.state.read();
3038 let list: Vec<Value> = st
3039 .account(account)
3040 .map(|a| {
3041 a.org_rules
3042 .values()
3043 .filter(|r| names.is_empty() || names.contains(&r.name))
3044 .map(|r| {
3045 json!({
3046 "OrganizationConfigRuleName": r.name,
3047 "OrganizationConfigRuleArn": r.arn,
3048 "OrganizationManagedRuleMetadata": r.managed_rule_metadata,
3049 "OrganizationCustomRuleMetadata": r.custom_rule_metadata,
3050 "OrganizationCustomPolicyRuleMetadata": r.custom_policy_rule_metadata,
3051 "ExcludedAccounts": r.excluded_accounts,
3052 "LastUpdateTime": r.last_update_time.timestamp() as f64,
3053 })
3054 })
3055 .collect()
3056 })
3057 .unwrap_or_default();
3058 Ok(AwsResponse::ok_json(
3059 json!({ "OrganizationConfigRules": list }),
3060 ))
3061 }
3062
3063 fn delete_organization_config_rule(
3064 &self,
3065 account: &str,
3066 body: &Value,
3067 ) -> Result<AwsResponse, AwsServiceError> {
3068 let name = require_str(body, "OrganizationConfigRuleName")?;
3069 let mut st = self.state.write();
3070 let acc = st.account_mut(account);
3071 if acc.org_rules.remove(&name).is_none() {
3072 return Err(no_such(
3073 "NoSuchOrganizationConfigRuleException",
3074 format!("Cannot find organization config rule '{name}'."),
3075 ));
3076 }
3077 Ok(AwsResponse::ok_json(json!({})))
3078 }
3079
3080 fn describe_organization_config_rule_statuses(
3081 &self,
3082 account: &str,
3083 body: &Value,
3084 ) -> Result<AwsResponse, AwsServiceError> {
3085 let names = string_list(body, "OrganizationConfigRuleNames");
3086 let now = Utc::now().timestamp() as f64;
3087 let st = self.state.read();
3088 let list: Vec<Value> = st
3089 .account(account)
3090 .map(|a| {
3091 a.org_rules
3092 .values()
3093 .filter(|r| names.is_empty() || names.contains(&r.name))
3094 .map(|r| {
3095 json!({ "OrganizationConfigRuleName": r.name, "OrganizationRuleStatus": "CREATE_SUCCESSFUL", "LastUpdateTime": now })
3096 })
3097 .collect()
3098 })
3099 .unwrap_or_default();
3100 Ok(AwsResponse::ok_json(
3101 json!({ "OrganizationConfigRuleStatuses": list }),
3102 ))
3103 }
3104
3105 fn get_organization_config_rule_detailed_status(
3106 &self,
3107 account: &str,
3108 body: &Value,
3109 ) -> Result<AwsResponse, AwsServiceError> {
3110 let name = require_str(body, "OrganizationConfigRuleName")?;
3111 let st = self.state.read();
3112 if !st
3113 .account(account)
3114 .map(|a| a.org_rules.contains_key(&name))
3115 .unwrap_or(false)
3116 {
3117 return Err(no_such(
3118 "NoSuchOrganizationConfigRuleException",
3119 format!("Cannot find organization config rule '{name}'."),
3120 ));
3121 }
3122 Ok(AwsResponse::ok_json(
3123 json!({ "OrganizationConfigRuleDetailedStatus": [] }),
3124 ))
3125 }
3126
3127 fn get_organization_custom_rule_policy(
3128 &self,
3129 account: &str,
3130 body: &Value,
3131 ) -> Result<AwsResponse, AwsServiceError> {
3132 let name = require_str(body, "OrganizationConfigRuleName")?;
3133 let st = self.state.read();
3134 let policy = st
3135 .account(account)
3136 .and_then(|a| a.org_rules.get(&name))
3137 .and_then(|r| r.custom_policy_rule_metadata.as_ref())
3138 .and_then(|m| {
3139 m.get("PolicyText")
3140 .and_then(Value::as_str)
3141 .map(String::from)
3142 });
3143 Ok(AwsResponse::ok_json(json!({ "PolicyText": policy })))
3144 }
3145
3146 fn put_organization_conformance_pack(
3147 &self,
3148 account: &str,
3149 region: &str,
3150 body: &Value,
3151 ) -> Result<AwsResponse, AwsServiceError> {
3152 let name = require_str(body, "OrganizationConformancePackName")?;
3153 let arn = format!(
3154 "arn:aws:config:{region}:{account}:organization-conformance-pack/{name}-{}",
3155 short_id()
3156 );
3157 let mut st = self.state.write();
3158 let acc = st.account_mut(account);
3159 let out_arn = acc
3160 .org_conformance_packs
3161 .get(&name)
3162 .map(|p| p.arn.clone())
3163 .unwrap_or(arn);
3164 acc.org_conformance_packs.insert(
3165 name.clone(),
3166 OrganizationConformancePack {
3167 name,
3168 arn: out_arn.clone(),
3169 delivery_s3_bucket: body
3170 .get("DeliveryS3Bucket")
3171 .and_then(Value::as_str)
3172 .map(String::from),
3173 delivery_s3_key_prefix: body
3174 .get("DeliveryS3KeyPrefix")
3175 .and_then(Value::as_str)
3176 .map(String::from),
3177 input_parameters: body
3178 .get("ConformancePackInputParameters")
3179 .and_then(Value::as_array)
3180 .cloned()
3181 .unwrap_or_default(),
3182 template_body: body
3183 .get("TemplateBody")
3184 .and_then(Value::as_str)
3185 .map(String::from),
3186 template_s3_uri: body
3187 .get("TemplateS3Uri")
3188 .and_then(Value::as_str)
3189 .map(String::from),
3190 excluded_accounts: string_list(body, "ExcludedAccounts"),
3191 last_update_time: Utc::now(),
3192 },
3193 );
3194 let tags = parse_create_tags(body);
3195 if !tags.is_empty() {
3196 let entry = acc.tags.entry(out_arn.clone()).or_default();
3197 for (k, v) in tags {
3198 entry.insert(k, v);
3199 }
3200 }
3201 Ok(AwsResponse::ok_json(
3202 json!({ "OrganizationConformancePackArn": out_arn }),
3203 ))
3204 }
3205
3206 fn describe_organization_conformance_packs(
3207 &self,
3208 account: &str,
3209 body: &Value,
3210 ) -> Result<AwsResponse, AwsServiceError> {
3211 let names = string_list(body, "OrganizationConformancePackNames");
3212 let st = self.state.read();
3213 let list: Vec<Value> = st
3214 .account(account)
3215 .map(|a| {
3216 a.org_conformance_packs
3217 .values()
3218 .filter(|p| names.is_empty() || names.contains(&p.name))
3219 .map(|p| {
3220 json!({
3221 "OrganizationConformancePackName": p.name,
3222 "OrganizationConformancePackArn": p.arn,
3223 "DeliveryS3Bucket": p.delivery_s3_bucket,
3224 "DeliveryS3KeyPrefix": p.delivery_s3_key_prefix,
3225 "ConformancePackInputParameters": p.input_parameters,
3226 "ExcludedAccounts": p.excluded_accounts,
3227 "LastUpdateTime": p.last_update_time.timestamp() as f64,
3228 })
3229 })
3230 .collect()
3231 })
3232 .unwrap_or_default();
3233 Ok(AwsResponse::ok_json(
3234 json!({ "OrganizationConformancePacks": list }),
3235 ))
3236 }
3237
3238 fn delete_organization_conformance_pack(
3239 &self,
3240 account: &str,
3241 body: &Value,
3242 ) -> Result<AwsResponse, AwsServiceError> {
3243 let name = require_str(body, "OrganizationConformancePackName")?;
3244 let mut st = self.state.write();
3245 let acc = st.account_mut(account);
3246 if acc.org_conformance_packs.remove(&name).is_none() {
3247 return Err(no_such(
3248 "NoSuchOrganizationConformancePackException",
3249 format!("Cannot find organization conformance pack '{name}'."),
3250 ));
3251 }
3252 Ok(AwsResponse::ok_json(json!({})))
3253 }
3254
3255 fn describe_organization_conformance_pack_statuses(
3256 &self,
3257 account: &str,
3258 body: &Value,
3259 ) -> Result<AwsResponse, AwsServiceError> {
3260 let names = string_list(body, "OrganizationConformancePackNames");
3261 let now = Utc::now().timestamp() as f64;
3262 let st = self.state.read();
3263 let list: Vec<Value> = st
3264 .account(account)
3265 .map(|a| {
3266 a.org_conformance_packs
3267 .values()
3268 .filter(|p| names.is_empty() || names.contains(&p.name))
3269 .map(|p| json!({ "OrganizationConformancePackName": p.name, "Status": "CREATE_SUCCESSFUL", "LastUpdateTime": now }))
3270 .collect()
3271 })
3272 .unwrap_or_default();
3273 Ok(AwsResponse::ok_json(
3274 json!({ "OrganizationConformancePackStatuses": list }),
3275 ))
3276 }
3277
3278 fn get_organization_conformance_pack_detailed_status(
3279 &self,
3280 account: &str,
3281 body: &Value,
3282 ) -> Result<AwsResponse, AwsServiceError> {
3283 let name = require_str(body, "OrganizationConformancePackName")?;
3284 let st = self.state.read();
3285 if !st
3286 .account(account)
3287 .map(|a| a.org_conformance_packs.contains_key(&name))
3288 .unwrap_or(false)
3289 {
3290 return Err(no_such(
3291 "NoSuchOrganizationConformancePackException",
3292 format!("Cannot find organization conformance pack '{name}'."),
3293 ));
3294 }
3295 Ok(AwsResponse::ok_json(
3296 json!({ "OrganizationConformancePackDetailedStatuses": [] }),
3297 ))
3298 }
3299}
3300
3301impl ConfigService {
3304 fn put_configuration_aggregator(
3305 &self,
3306 account: &str,
3307 region: &str,
3308 body: &Value,
3309 ) -> Result<AwsResponse, AwsServiceError> {
3310 let name = require_str(body, "ConfigurationAggregatorName")?;
3311 let now = Utc::now();
3312 let arn = format!(
3313 "arn:aws:config:{region}:{account}:config-aggregator/config-aggregator-{}",
3314 short_id()
3315 );
3316 let mut st = self.state.write();
3317 let acc = st.account_mut(account);
3318 let existing = acc.aggregators.get(&name);
3319 let agg = ConfigurationAggregator {
3320 name: name.clone(),
3321 arn: existing.map(|a| a.arn.clone()).unwrap_or(arn),
3322 account_aggregation_sources: body
3323 .get("AccountAggregationSources")
3324 .and_then(Value::as_array)
3325 .cloned()
3326 .unwrap_or_default(),
3327 organization_aggregation_source: body.get("OrganizationAggregationSource").cloned(),
3328 creation_time: existing.map(|a| a.creation_time).unwrap_or(now),
3329 last_updated_time: now,
3330 created_by: None,
3331 };
3332 let out = Self::aggregator_json(&agg);
3333 let arn_for_tags = agg.arn.clone();
3334 acc.aggregators.insert(name, agg);
3335 let tags = parse_create_tags(body);
3336 if !tags.is_empty() {
3337 let entry = acc.tags.entry(arn_for_tags).or_default();
3338 for (k, v) in tags {
3339 entry.insert(k, v);
3340 }
3341 }
3342 Ok(AwsResponse::ok_json(
3343 json!({ "ConfigurationAggregator": out }),
3344 ))
3345 }
3346
3347 fn aggregator_json(a: &ConfigurationAggregator) -> Value {
3348 json!({
3349 "ConfigurationAggregatorName": a.name,
3350 "ConfigurationAggregatorArn": a.arn,
3351 "AccountAggregationSources": a.account_aggregation_sources,
3352 "OrganizationAggregationSource": a.organization_aggregation_source,
3353 "CreationTime": a.creation_time.timestamp() as f64,
3354 "LastUpdatedTime": a.last_updated_time.timestamp() as f64,
3355 })
3356 }
3357
3358 fn describe_configuration_aggregators(
3359 &self,
3360 account: &str,
3361 body: &Value,
3362 ) -> Result<AwsResponse, AwsServiceError> {
3363 let names = string_list(body, "ConfigurationAggregatorNames");
3364 let st = self.state.read();
3365 let list: Vec<Value> = st
3366 .account(account)
3367 .map(|a| {
3368 a.aggregators
3369 .values()
3370 .filter(|g| names.is_empty() || names.contains(&g.name))
3371 .map(Self::aggregator_json)
3372 .collect()
3373 })
3374 .unwrap_or_default();
3375 if !names.is_empty() {
3376 for n in &names {
3377 if !st
3378 .account(account)
3379 .map(|a| a.aggregators.contains_key(n))
3380 .unwrap_or(false)
3381 {
3382 return Err(no_such(
3383 "NoSuchConfigurationAggregatorException",
3384 format!("The configuration aggregator '{n}' does not exist."),
3385 ));
3386 }
3387 }
3388 }
3389 Ok(paged_response(
3390 "ConfigurationAggregators",
3391 list,
3392 body,
3393 "NextToken",
3394 ))
3395 }
3396
3397 fn delete_configuration_aggregator(
3398 &self,
3399 account: &str,
3400 body: &Value,
3401 ) -> Result<AwsResponse, AwsServiceError> {
3402 let name = require_str(body, "ConfigurationAggregatorName")?;
3403 let mut st = self.state.write();
3404 let acc = st.account_mut(account);
3405 if acc.aggregators.remove(&name).is_none() {
3406 return Err(no_such(
3407 "NoSuchConfigurationAggregatorException",
3408 format!("The configuration aggregator '{name}' does not exist."),
3409 ));
3410 }
3411 Ok(AwsResponse::ok_json(json!({})))
3412 }
3413
3414 fn describe_configuration_aggregator_sources_status(
3415 &self,
3416 account: &str,
3417 body: &Value,
3418 ) -> Result<AwsResponse, AwsServiceError> {
3419 let name = require_str(body, "ConfigurationAggregatorName")?;
3420 let now = Utc::now().timestamp() as f64;
3421 let st = self.state.read();
3422 let agg = st.account(account).and_then(|a| a.aggregators.get(&name));
3423 let Some(agg) = agg else {
3424 return Err(no_such(
3425 "NoSuchConfigurationAggregatorException",
3426 format!("The configuration aggregator '{name}' does not exist."),
3427 ));
3428 };
3429 let mut list = Vec::new();
3430 for src in &agg.account_aggregation_sources {
3431 let region = src
3432 .get("AwsRegions")
3433 .and_then(Value::as_array)
3434 .and_then(|a| a.first())
3435 .and_then(Value::as_str)
3436 .unwrap_or("us-east-1");
3437 let accounts = src
3438 .get("AccountIds")
3439 .and_then(Value::as_array)
3440 .cloned()
3441 .unwrap_or_default();
3442 for acct in accounts {
3443 list.push(json!({
3444 "SourceId": acct,
3445 "SourceType": "ACCOUNT",
3446 "AwsRegion": region,
3447 "LastUpdateStatus": "SUCCEEDED",
3448 "LastUpdateTime": now,
3449 }));
3450 }
3451 }
3452 Ok(AwsResponse::ok_json(
3453 json!({ "AggregatedSourceStatusList": list }),
3454 ))
3455 }
3456
3457 fn put_aggregation_authorization(
3458 &self,
3459 account: &str,
3460 region: &str,
3461 body: &Value,
3462 ) -> Result<AwsResponse, AwsServiceError> {
3463 let authorized_account = require_str(body, "AuthorizedAccountId")?;
3464 let authorized_region = require_str(body, "AuthorizedAwsRegion")?;
3465 let now = Utc::now();
3466 let arn = format!("arn:aws:config:{region}:{account}:aggregation-authorization/{authorized_account}/{authorized_region}");
3467 let key = format!("{authorized_account}\u{1}{authorized_region}");
3468 let auth = AggregationAuthorization {
3469 arn: arn.clone(),
3470 authorized_account_id: authorized_account,
3471 authorized_aws_region: authorized_region,
3472 creation_time: now,
3473 };
3474 let out = Self::auth_json(&auth);
3475 let mut st = self.state.write();
3476 let acc = st.account_mut(account);
3477 acc.aggregation_authorizations.insert(key, auth);
3478 let tags = parse_create_tags(body);
3479 if !tags.is_empty() {
3480 let entry = acc.tags.entry(arn.clone()).or_default();
3481 for (k, v) in tags {
3482 entry.insert(k, v);
3483 }
3484 }
3485 let _ = out;
3486 Ok(AwsResponse::ok_json(
3487 json!({ "AggregationAuthorization": Self::auth_json_by_arn(&arn) }),
3488 ))
3489 }
3490
3491 fn auth_json(a: &AggregationAuthorization) -> Value {
3492 json!({
3493 "AggregationAuthorizationArn": a.arn,
3494 "AuthorizedAccountId": a.authorized_account_id,
3495 "AuthorizedAwsRegion": a.authorized_aws_region,
3496 "CreationTime": a.creation_time.timestamp() as f64,
3497 })
3498 }
3499
3500 fn auth_json_by_arn(arn: &str) -> Value {
3501 json!({ "AggregationAuthorizationArn": arn })
3502 }
3503
3504 fn describe_aggregation_authorizations(
3505 &self,
3506 account: &str,
3507 body: &Value,
3508 ) -> Result<AwsResponse, AwsServiceError> {
3509 let st = self.state.read();
3510 let list: Vec<Value> = st
3511 .account(account)
3512 .map(|a| {
3513 a.aggregation_authorizations
3514 .values()
3515 .map(Self::auth_json)
3516 .collect()
3517 })
3518 .unwrap_or_default();
3519 Ok(paged_response(
3520 "AggregationAuthorizations",
3521 list,
3522 body,
3523 "NextToken",
3524 ))
3525 }
3526
3527 fn delete_aggregation_authorization(
3528 &self,
3529 account: &str,
3530 body: &Value,
3531 ) -> Result<AwsResponse, AwsServiceError> {
3532 let authorized_account = require_str(body, "AuthorizedAccountId")?;
3533 let authorized_region = require_str(body, "AuthorizedAwsRegion")?;
3534 let key = format!("{authorized_account}\u{1}{authorized_region}");
3535 let mut st = self.state.write();
3536 st.account_mut(account)
3537 .aggregation_authorizations
3538 .remove(&key);
3539 Ok(AwsResponse::ok_json(json!({})))
3540 }
3541
3542 fn batch_get_aggregate_resource_config(
3546 &self,
3547 account: &str,
3548 body: &Value,
3549 ) -> Result<AwsResponse, AwsServiceError> {
3550 let keys = body
3551 .get("ResourceIdentifiers")
3552 .and_then(Value::as_array)
3553 .cloned()
3554 .unwrap_or_default();
3555 let st = self.state.read();
3556 let acc = st.account(account);
3557 let mut items = Vec::new();
3558 let mut unprocessed = Vec::new();
3559 for k in keys {
3560 let rt = k
3561 .get("ResourceType")
3562 .and_then(Value::as_str)
3563 .unwrap_or_default();
3564 let rid = k
3565 .get("ResourceId")
3566 .and_then(Value::as_str)
3567 .unwrap_or_default();
3568 let key = resource_key(rt, rid);
3569 match acc
3570 .and_then(|a| a.config_items.get(&key))
3571 .and_then(|h| h.last())
3572 {
3573 Some(ci) => items.push(Self::config_item_json(ci)),
3574 None => unprocessed.push(k),
3575 }
3576 }
3577 Ok(AwsResponse::ok_json(
3578 json!({ "BaseConfigurationItems": items, "UnprocessedResourceIdentifiers": unprocessed }),
3579 ))
3580 }
3581
3582 fn get_aggregate_resource_config(
3583 &self,
3584 account: &str,
3585 body: &Value,
3586 ) -> Result<AwsResponse, AwsServiceError> {
3587 let ident = body
3588 .get("ResourceIdentifier")
3589 .cloned()
3590 .unwrap_or(Value::Null);
3591 let rt = ident
3592 .get("ResourceType")
3593 .and_then(Value::as_str)
3594 .unwrap_or_default();
3595 let rid = ident
3596 .get("ResourceId")
3597 .and_then(Value::as_str)
3598 .unwrap_or_default();
3599 let st = self.state.read();
3600 let key = resource_key(rt, rid);
3601 let ci = st
3602 .account(account)
3603 .and_then(|a| a.config_items.get(&key))
3604 .and_then(|h| h.last());
3605 match ci {
3606 Some(ci) => Ok(AwsResponse::ok_json(
3607 json!({ "ConfigurationItem": Self::config_item_json(ci) }),
3608 )),
3609 None => Err(no_such(
3610 "ResourceNotDiscoveredException",
3611 "Resource is not discovered by the aggregator.",
3612 )),
3613 }
3614 }
3615
3616 fn list_aggregate_discovered_resources(
3617 &self,
3618 account: &str,
3619 body: &Value,
3620 ) -> Result<AwsResponse, AwsServiceError> {
3621 let resource_type = require_str(body, "ResourceType")?;
3622 let st = self.state.read();
3623 let mut list = Vec::new();
3624 if let Some(acc) = st.account(account) {
3625 for (key, history) in &acc.config_items {
3626 let Some((rt, _)) = key.split_once('\u{1}') else {
3627 continue;
3628 };
3629 if rt != resource_type {
3630 continue;
3631 }
3632 let Some(ci) = history.last() else { continue };
3633 if ci.configuration_item_status.starts_with("ResourceDeleted") {
3634 continue;
3635 }
3636 list.push(json!({
3637 "SourceAccountId": account,
3638 "SourceRegion": ci.aws_region,
3639 "ResourceId": ci.resource_id,
3640 "ResourceType": ci.resource_type,
3641 "ResourceName": ci.resource_name,
3642 }));
3643 }
3644 }
3645 Ok(AwsResponse::ok_json(json!({ "ResourceIdentifiers": list })))
3646 }
3647
3648 fn get_aggregate_discovered_resource_counts(
3649 &self,
3650 account: &str,
3651 _body: &Value,
3652 ) -> Result<AwsResponse, AwsServiceError> {
3653 let st = self.state.read();
3654 let mut counts: std::collections::BTreeMap<String, i64> = std::collections::BTreeMap::new();
3655 if let Some(acc) = st.account(account) {
3656 for (key, history) in &acc.config_items {
3657 let Some((rt, _)) = key.split_once('\u{1}') else {
3658 continue;
3659 };
3660 if history
3661 .last()
3662 .map(|ci| ci.configuration_item_status.starts_with("ResourceDeleted"))
3663 .unwrap_or(true)
3664 {
3665 continue;
3666 }
3667 *counts.entry(rt.to_string()).or_insert(0) += 1;
3668 }
3669 }
3670 let total: i64 = counts.values().sum();
3671 let list: Vec<Value> = counts
3672 .into_iter()
3673 .map(|(t, c)| json!({ "ResourceType": t, "Count": c }))
3674 .collect();
3675 Ok(AwsResponse::ok_json(
3676 json!({ "TotalDiscoveredResources": total, "GroupedResourceCounts": list }),
3677 ))
3678 }
3679
3680 fn describe_aggregate_compliance_by_config_rules(
3681 &self,
3682 account: &str,
3683 _body: &Value,
3684 ) -> Result<AwsResponse, AwsServiceError> {
3685 let st = self.state.read();
3686 let mut list = Vec::new();
3687 if let Some(acc) = st.account(account) {
3688 for rule in acc.rules.values() {
3689 let c = acc
3690 .evaluations
3691 .get(&rule.name)
3692 .map(|m| fold_compliance(m.values().map(|r| r.compliance_type.as_str())))
3693 .unwrap_or_else(|| "INSUFFICIENT_DATA".into());
3694 list.push(json!({
3695 "ConfigRuleName": rule.name,
3696 "Compliance": { "ComplianceType": c },
3697 "AccountId": account,
3698 "AwsRegion": "us-east-1",
3699 }));
3700 }
3701 }
3702 Ok(AwsResponse::ok_json(
3703 json!({ "AggregateComplianceByConfigRules": list }),
3704 ))
3705 }
3706
3707 fn describe_aggregate_compliance_by_conformance_packs(
3708 &self,
3709 account: &str,
3710 _body: &Value,
3711 ) -> Result<AwsResponse, AwsServiceError> {
3712 let st = self.state.read();
3713 let list: Vec<Value> = st
3714 .account(account)
3715 .map(|a| {
3716 a.conformance_packs
3717 .values()
3718 .map(|p| {
3719 let rules = Self::pack_rule_names(a, p);
3720 let noncompliant = rules.iter().any(|r| a.evaluations.get(r).map(|m| m.values().any(|e| e.compliance_type == "NON_COMPLIANT")).unwrap_or(false));
3721 json!({
3722 "ConformancePackName": p.name,
3723 "Compliance": { "ComplianceType": if noncompliant { "NON_COMPLIANT" } else { "COMPLIANT" } },
3724 "AccountId": account,
3725 "AwsRegion": "us-east-1",
3726 })
3727 })
3728 .collect()
3729 })
3730 .unwrap_or_default();
3731 Ok(AwsResponse::ok_json(
3732 json!({ "AggregateComplianceByConformancePacks": list }),
3733 ))
3734 }
3735
3736 fn get_aggregate_compliance_details_by_config_rule(
3737 &self,
3738 account: &str,
3739 body: &Value,
3740 ) -> Result<AwsResponse, AwsServiceError> {
3741 let name = require_str(body, "ConfigRuleName")?;
3742 let st = self.state.read();
3743 let results: Vec<Value> = st
3744 .account(account)
3745 .and_then(|a| a.evaluations.get(&name))
3746 .map(|m| {
3747 m.values()
3748 .map(|r| {
3749 json!({
3750 "AccountId": account,
3751 "AwsRegion": "us-east-1",
3752 "ComplianceType": r.compliance_type,
3753 "ResultRecordedTime": r.result_recorded_time.timestamp() as f64,
3754 "ConfigRuleInvokedTime": r.config_rule_invoked_time.timestamp() as f64,
3755 "Annotation": r.annotation,
3756 "EvaluationResultIdentifier": {
3757 "EvaluationResultQualifier": {
3758 "ConfigRuleName": name,
3759 "ResourceType": r.resource_type,
3760 "ResourceId": r.resource_id,
3761 },
3762 "OrderingTimestamp": r.ordering_timestamp.timestamp() as f64,
3763 },
3764 })
3765 })
3766 .collect()
3767 })
3768 .unwrap_or_default();
3769 Ok(AwsResponse::ok_json(
3770 json!({ "AggregateEvaluationResults": results }),
3771 ))
3772 }
3773
3774 fn get_aggregate_config_rule_compliance_summary(
3775 &self,
3776 account: &str,
3777 _body: &Value,
3778 ) -> Result<AwsResponse, AwsServiceError> {
3779 let st = self.state.read();
3780 let mut compliant = 0;
3781 let mut noncompliant = 0;
3782 if let Some(acc) = st.account(account) {
3783 for rule in acc.rules.values() {
3784 match acc
3785 .evaluations
3786 .get(&rule.name)
3787 .map(|m| fold_compliance(m.values().map(|r| r.compliance_type.as_str())))
3788 .unwrap_or_else(|| "INSUFFICIENT_DATA".into())
3789 .as_str()
3790 {
3791 "COMPLIANT" => compliant += 1,
3792 "NON_COMPLIANT" => noncompliant += 1,
3793 _ => {}
3794 }
3795 }
3796 }
3797 Ok(AwsResponse::ok_json(json!({
3798 "GroupByKey": "ACCOUNT_ID",
3799 "AggregateComplianceCounts": [{
3800 "GroupName": account,
3801 "ComplianceSummary": {
3802 "CompliantResourceCount": { "CappedCount": compliant, "CapExceeded": false },
3803 "NonCompliantResourceCount": { "CappedCount": noncompliant, "CapExceeded": false },
3804 "ComplianceSummaryTimestamp": Utc::now().timestamp() as f64,
3805 }
3806 }]
3807 })))
3808 }
3809
3810 fn get_aggregate_conformance_pack_compliance_summary(
3811 &self,
3812 account: &str,
3813 _body: &Value,
3814 ) -> Result<AwsResponse, AwsServiceError> {
3815 let st = self.state.read();
3816 let mut compliant = 0;
3817 let mut noncompliant = 0;
3818 if let Some(acc) = st.account(account) {
3819 for p in acc.conformance_packs.values() {
3820 let rules = Self::pack_rule_names(acc, p);
3821 let nc = rules.iter().any(|r| {
3822 acc.evaluations
3823 .get(r)
3824 .map(|m| m.values().any(|e| e.compliance_type == "NON_COMPLIANT"))
3825 .unwrap_or(false)
3826 });
3827 if nc {
3828 noncompliant += 1;
3829 } else {
3830 compliant += 1;
3831 }
3832 }
3833 }
3834 Ok(AwsResponse::ok_json(json!({
3835 "GroupByKey": "ACCOUNT_ID",
3836 "AggregateConformancePackComplianceSummaries": [{
3837 "GroupName": account,
3838 "ComplianceSummary": {
3839 "CompliantConformancePackCount": compliant,
3840 "NonCompliantConformancePackCount": noncompliant,
3841 }
3842 }]
3843 })))
3844 }
3845}
3846
3847impl ConfigService {
3850 fn put_retention_configuration(
3851 &self,
3852 account: &str,
3853 body: &Value,
3854 ) -> Result<AwsResponse, AwsServiceError> {
3855 let days = body
3856 .get("RetentionPeriodInDays")
3857 .and_then(Value::as_i64)
3858 .ok_or_else(|| invalid("RetentionPeriodInDays is required"))?;
3859 if !(30..=2557).contains(&days) {
3860 return Err(invalid("RetentionPeriodInDays must be between 30 and 2557"));
3861 }
3862 let mut st = self.state.write();
3863 st.account_mut(account).retention_configurations.insert(
3864 "default".into(),
3865 RetentionConfiguration {
3866 name: "default".into(),
3867 retention_period_in_days: days,
3868 },
3869 );
3870 Ok(AwsResponse::ok_json(
3871 json!({ "RetentionConfiguration": { "Name": "default", "RetentionPeriodInDays": days } }),
3872 ))
3873 }
3874
3875 fn describe_retention_configurations(
3876 &self,
3877 account: &str,
3878 body: &Value,
3879 ) -> Result<AwsResponse, AwsServiceError> {
3880 let st = self.state.read();
3881 let list: Vec<Value> = st
3882 .account(account)
3883 .map(|a| a.retention_configurations.values().map(|r| json!({ "Name": r.name, "RetentionPeriodInDays": r.retention_period_in_days })).collect())
3884 .unwrap_or_default();
3885 Ok(paged_response(
3886 "RetentionConfigurations",
3887 list,
3888 body,
3889 "NextToken",
3890 ))
3891 }
3892
3893 fn delete_retention_configuration(
3894 &self,
3895 account: &str,
3896 body: &Value,
3897 ) -> Result<AwsResponse, AwsServiceError> {
3898 let name = require_str(body, "RetentionConfigurationName")?;
3899 let mut st = self.state.write();
3900 let acc = st.account_mut(account);
3901 if acc.retention_configurations.remove(&name).is_none() {
3902 return Err(no_such(
3903 "NoSuchRetentionConfigurationException",
3904 format!("Cannot find retention configuration with the specified name '{name}'."),
3905 ));
3906 }
3907 Ok(AwsResponse::ok_json(json!({})))
3908 }
3909
3910 fn put_stored_query(
3911 &self,
3912 account: &str,
3913 region: &str,
3914 body: &Value,
3915 ) -> Result<AwsResponse, AwsServiceError> {
3916 let q = body
3917 .get("StoredQuery")
3918 .cloned()
3919 .ok_or_else(|| invalid("StoredQuery is required"))?;
3920 let name = q
3921 .get("QueryName")
3922 .and_then(Value::as_str)
3923 .ok_or_else(|| invalid("QueryName is required"))?
3924 .to_string();
3925 let mut st = self.state.write();
3926 let acc = st.account_mut(account);
3927 let existing = acc.stored_queries.get(&name);
3928 let id = existing
3929 .map(|s| s.id.clone())
3930 .unwrap_or_else(|| Uuid::new_v4().to_string());
3931 let arn = existing.map(|s| s.arn.clone()).unwrap_or_else(|| {
3932 format!("arn:aws:config:{region}:{account}:stored-query/{name}/{id}")
3933 });
3934 acc.stored_queries.insert(
3935 name.clone(),
3936 StoredQuery {
3937 id: id.clone(),
3938 name,
3939 arn: arn.clone(),
3940 description: q
3941 .get("Description")
3942 .and_then(Value::as_str)
3943 .map(String::from),
3944 expression: q
3945 .get("Expression")
3946 .and_then(Value::as_str)
3947 .map(String::from),
3948 },
3949 );
3950 let _ = id;
3951 let tags = parse_create_tags(body);
3952 if !tags.is_empty() {
3953 let entry = acc.tags.entry(arn.clone()).or_default();
3954 for (k, v) in tags {
3955 entry.insert(k, v);
3956 }
3957 }
3958 Ok(AwsResponse::ok_json(json!({ "QueryArn": arn })))
3959 }
3960
3961 fn get_stored_query(
3962 &self,
3963 account: &str,
3964 body: &Value,
3965 ) -> Result<AwsResponse, AwsServiceError> {
3966 let name = require_str(body, "QueryName")?;
3967 let st = self.state.read();
3968 let q = st
3969 .account(account)
3970 .and_then(|a| a.stored_queries.get(&name));
3971 let Some(q) = q else {
3972 return Err(no_such(
3973 "ResourceNotFoundException",
3974 format!("The stored query '{name}' does not exist."),
3975 ));
3976 };
3977 Ok(AwsResponse::ok_json(json!({
3978 "StoredQuery": {
3979 "QueryId": q.id,
3980 "QueryArn": q.arn,
3981 "QueryName": q.name,
3982 "Description": q.description,
3983 "Expression": q.expression,
3984 }
3985 })))
3986 }
3987
3988 fn delete_stored_query(
3989 &self,
3990 account: &str,
3991 body: &Value,
3992 ) -> Result<AwsResponse, AwsServiceError> {
3993 let name = require_str(body, "QueryName")?;
3994 let mut st = self.state.write();
3995 let acc = st.account_mut(account);
3996 if acc.stored_queries.remove(&name).is_none() {
3997 return Err(no_such(
3998 "ResourceNotFoundException",
3999 format!("The stored query '{name}' does not exist."),
4000 ));
4001 }
4002 Ok(AwsResponse::ok_json(json!({})))
4003 }
4004
4005 fn list_stored_queries(
4006 &self,
4007 account: &str,
4008 body: &Value,
4009 ) -> Result<AwsResponse, AwsServiceError> {
4010 let st = self.state.read();
4011 let list: Vec<Value> = st
4012 .account(account)
4013 .map(|a| a.stored_queries.values().map(|q| json!({ "QueryId": q.id, "QueryArn": q.arn, "QueryName": q.name, "Description": q.description })).collect())
4014 .unwrap_or_default();
4015 Ok(paged_response(
4016 "StoredQueryMetadata",
4017 list,
4018 body,
4019 "NextToken",
4020 ))
4021 }
4022
4023 fn start_resource_evaluation(
4024 &self,
4025 account: &str,
4026 body: &Value,
4027 ) -> Result<AwsResponse, AwsServiceError> {
4028 let mode = body
4029 .get("EvaluationMode")
4030 .and_then(Value::as_str)
4031 .unwrap_or("DETECTIVE")
4032 .to_string();
4033 let details = body.get("ResourceDetails").cloned();
4034 let (rtype, rid, config_str) = details
4037 .as_ref()
4038 .map(|d| {
4039 let rt = d
4040 .get("ResourceType")
4041 .and_then(Value::as_str)
4042 .unwrap_or_default()
4043 .to_string();
4044 let ri = d
4045 .get("ResourceId")
4046 .and_then(Value::as_str)
4047 .unwrap_or_default()
4048 .to_string();
4049 let cfg = match d.get("ResourceConfiguration") {
4052 Some(Value::String(s)) => s.clone(),
4053 Some(v) => v.to_string(),
4054 None => "null".to_string(),
4055 };
4056 (rt, ri, cfg)
4057 })
4058 .unwrap_or_default();
4059 let id = Uuid::new_v4().to_string();
4060 let mut st = self.state.write();
4061 let acc = st.account_mut(account);
4062 let evaluation_results = if rtype.is_empty() || rid.is_empty() {
4063 Vec::new()
4064 } else {
4065 validate::evaluate_proactive(acc, &rtype, &rid, &config_str)
4066 };
4067 acc.resource_evaluations.insert(
4068 id.clone(),
4069 ResourceEvaluation {
4070 resource_evaluation_id: id.clone(),
4071 evaluation_mode: mode,
4072 time_stamp: Utc::now(),
4073 status: "SUCCEEDED".into(),
4074 resource_details: details,
4075 evaluation_context: body.get("EvaluationContext").cloned(),
4076 evaluation_results,
4077 },
4078 );
4079 Ok(AwsResponse::ok_json(json!({ "ResourceEvaluationId": id })))
4080 }
4081
4082 fn get_resource_evaluation_summary(
4083 &self,
4084 account: &str,
4085 body: &Value,
4086 ) -> Result<AwsResponse, AwsServiceError> {
4087 let id = require_str(body, "ResourceEvaluationId")?;
4088 let st = self.state.read();
4089 let ev = st
4090 .account(account)
4091 .and_then(|a| a.resource_evaluations.get(&id));
4092 let Some(ev) = ev else {
4093 return Err(no_such(
4094 "ResourceNotFoundException",
4095 format!("ResourceEvaluationId '{id}' does not exist."),
4096 ));
4097 };
4098 let resource_type = ev
4099 .resource_details
4100 .as_ref()
4101 .and_then(|d| d.get("ResourceType"))
4102 .cloned()
4103 .unwrap_or(Value::Null);
4104 let resource_id = ev
4105 .resource_details
4106 .as_ref()
4107 .and_then(|d| d.get("ResourceId"))
4108 .cloned()
4109 .unwrap_or(Value::Null);
4110 let compliance = if ev.evaluation_results.is_empty() {
4113 "INSUFFICIENT_DATA".to_string()
4114 } else {
4115 fold_compliance(
4116 ev.evaluation_results
4117 .iter()
4118 .map(|r| r.compliance_type.as_str()),
4119 )
4120 };
4121 Ok(AwsResponse::ok_json(json!({
4122 "ResourceEvaluationId": ev.resource_evaluation_id,
4123 "EvaluationMode": ev.evaluation_mode,
4124 "EvaluationStatus": { "Status": ev.status },
4125 "EvaluationStartTimestamp": ev.time_stamp.timestamp() as f64,
4126 "Compliance": compliance,
4127 "ResourceDetails": { "ResourceId": resource_id, "ResourceType": resource_type },
4128 })))
4129 }
4130
4131 fn list_resource_evaluations(
4132 &self,
4133 account: &str,
4134 body: &Value,
4135 ) -> Result<AwsResponse, AwsServiceError> {
4136 let st = self.state.read();
4137 let list: Vec<Value> = st
4138 .account(account)
4139 .map(|a| {
4140 a.resource_evaluations
4141 .values()
4142 .map(|e| json!({ "ResourceEvaluationId": e.resource_evaluation_id, "EvaluationMode": e.evaluation_mode, "EvaluationStartTimestamp": e.time_stamp.timestamp() as f64 }))
4143 .collect()
4144 })
4145 .unwrap_or_default();
4146 Ok(paged_response(
4147 "ResourceEvaluations",
4148 list,
4149 body,
4150 "NextToken",
4151 ))
4152 }
4153
4154 fn select_resource_config(
4155 &self,
4156 account: &str,
4157 body: &Value,
4158 aggregate: bool,
4159 ) -> Result<AwsResponse, AwsServiceError> {
4160 let expr = require_str(body, "Expression")?;
4161 let st = self.state.read();
4162 let empty = AccountState::default();
4163 let acc = st.account(account).unwrap_or(&empty);
4164 let rows = validate::run_select(&expr, acc)
4165 .map_err(|e| invalid(format!("Invalid SelectResourceConfig expression: {e}")))?;
4166 let results: Vec<Value> = rows.iter().map(|r| json!(r.to_string())).collect();
4167 let (page, next) = paginate(results, body);
4168 let mut out = json!({
4169 "Results": page,
4170 "QueryInfo": { "SelectFields": select_fields(&expr) },
4171 });
4172 if let Some(t) = next {
4173 out["NextToken"] = json!(t);
4174 }
4175 if aggregate {
4176 let _ = out.as_object_mut();
4177 }
4178 Ok(AwsResponse::ok_json(out))
4179 }
4180
4181 fn tag_resource(&self, account: &str, body: &Value) -> Result<AwsResponse, AwsServiceError> {
4182 let arn = require_str(body, "ResourceArn")?;
4183 let tags = body
4184 .get("Tags")
4185 .and_then(Value::as_array)
4186 .cloned()
4187 .unwrap_or_default();
4188 let mut st = self.state.write();
4189 let acc = st.account_mut(account);
4190 let entry = acc.tags.entry(arn).or_default();
4191 for t in tags {
4192 if let (Some(k), Some(v)) = (
4193 t.get("Key").and_then(Value::as_str),
4194 t.get("Value").and_then(Value::as_str),
4195 ) {
4196 entry.insert(k.to_string(), v.to_string());
4197 }
4198 }
4199 Ok(AwsResponse::ok_json(json!({})))
4200 }
4201
4202 fn untag_resource(&self, account: &str, body: &Value) -> Result<AwsResponse, AwsServiceError> {
4203 let arn = require_str(body, "ResourceArn")?;
4204 let keys = string_list(body, "TagKeys");
4205 let mut st = self.state.write();
4206 let acc = st.account_mut(account);
4207 if let Some(entry) = acc.tags.get_mut(&arn) {
4208 for k in keys {
4209 entry.remove(&k);
4210 }
4211 }
4212 Ok(AwsResponse::ok_json(json!({})))
4213 }
4214
4215 fn list_tags_for_resource(
4216 &self,
4217 account: &str,
4218 body: &Value,
4219 ) -> Result<AwsResponse, AwsServiceError> {
4220 let arn = require_str(body, "ResourceArn")?;
4221 let st = self.state.read();
4222 let list: Vec<Value> = st
4223 .account(account)
4224 .and_then(|a| a.tags.get(&arn))
4225 .map(|m| {
4226 m.iter()
4227 .map(|(k, v)| json!({ "Key": k, "Value": v }))
4228 .collect()
4229 })
4230 .unwrap_or_default();
4231 Ok(AwsResponse::ok_json(json!({ "Tags": list })))
4232 }
4233}
4234
4235fn parse_create_tags(body: &Value) -> Vec<(String, String)> {
4242 body.get("Tags")
4243 .and_then(Value::as_array)
4244 .map(|arr| {
4245 arr.iter()
4246 .filter_map(|t| {
4247 Some((
4248 t.get("Key").and_then(Value::as_str)?.to_string(),
4249 t.get("Value").and_then(Value::as_str)?.to_string(),
4250 ))
4251 })
4252 .collect()
4253 })
4254 .unwrap_or_default()
4255}
4256
4257fn account_id(req: &AwsRequest) -> String {
4258 if req.account_id.is_empty() {
4259 "000000000000".to_string()
4260 } else {
4261 req.account_id.clone()
4262 }
4263}
4264
4265fn region(req: &AwsRequest) -> String {
4266 if req.region.is_empty() {
4267 "us-east-1".to_string()
4268 } else {
4269 req.region.clone()
4270 }
4271}
4272
4273fn short_id() -> String {
4274 fakecloud_core::ids::short_id(6)
4275}
4276
4277fn invalid(msg: impl Into<String>) -> AwsServiceError {
4278 AwsServiceError::aws_error(
4279 StatusCode::BAD_REQUEST,
4280 "InvalidParameterValueException",
4281 msg,
4282 )
4283}
4284
4285fn no_such(code: &str, msg: impl Into<String>) -> AwsServiceError {
4286 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, code, msg)
4287}
4288
4289fn require_str(body: &Value, field: &str) -> Result<String, AwsServiceError> {
4290 body.get(field)
4291 .and_then(Value::as_str)
4292 .filter(|s| !s.is_empty())
4293 .map(String::from)
4294 .ok_or_else(|| invalid(format!("{field} is required")))
4295}
4296
4297fn string_list(body: &Value, field: &str) -> Vec<String> {
4298 body.get(field)
4299 .and_then(Value::as_array)
4300 .map(|a| {
4301 a.iter()
4302 .filter_map(|v| v.as_str().map(String::from))
4303 .collect()
4304 })
4305 .unwrap_or_default()
4306}
4307
4308fn page_limit(body: &Value) -> Option<usize> {
4312 ["Limit", "MaxResults", "limit"]
4313 .iter()
4314 .find_map(|k| body.get(*k).and_then(Value::as_i64))
4315 .filter(|l| *l > 0)
4316 .map(|l| l as usize)
4317}
4318
4319fn page_token(body: &Value) -> Option<usize> {
4322 ["NextToken", "nextToken"]
4323 .iter()
4324 .find_map(|k| body.get(*k).and_then(Value::as_str))
4325 .filter(|s| !s.is_empty())
4326 .and_then(decode_page_token)
4327}
4328
4329fn encode_page_token(offset: usize) -> String {
4333 format!("cfg:{offset}")
4334}
4335
4336fn decode_page_token(token: &str) -> Option<usize> {
4337 token.strip_prefix("cfg:").and_then(|n| n.parse().ok())
4338}
4339
4340fn paginate(items: Vec<Value>, body: &Value) -> (Vec<Value>, Option<String>) {
4343 let total = items.len();
4344 let start = page_token(body).unwrap_or(0).min(total);
4345 let end = match page_limit(body) {
4346 Some(limit) => start.saturating_add(limit).min(total),
4347 None => total,
4348 };
4349 let next = if end < total {
4350 Some(encode_page_token(end))
4351 } else {
4352 None
4353 };
4354 (
4355 items.into_iter().skip(start).take(end - start).collect(),
4356 next,
4357 )
4358}
4359
4360fn paged_response(field: &str, items: Vec<Value>, body: &Value, token_field: &str) -> AwsResponse {
4364 let (page, next) = paginate(items, body);
4365 let mut out = json!({ field: page });
4366 if let Some(t) = next {
4367 out[token_field] = json!(t);
4368 }
4369 AwsResponse::ok_json(out)
4370}
4371
4372fn custom_rule_event(
4377 account: &str,
4378 rule_name: &str,
4379 input_parameters: Option<&str>,
4380 result_token: &str,
4381) -> Value {
4382 let invoking_event = json!({
4383 "awsAccountId": account,
4384 "configRuleName": rule_name,
4385 "messageType": "ScheduledNotification",
4386 "notificationCreationTime": Utc::now().to_rfc3339(),
4387 })
4388 .to_string();
4389 let rule_parameters = input_parameters
4390 .filter(|p| !p.is_empty())
4391 .map(String::from)
4392 .unwrap_or_else(|| "{}".to_string());
4393 json!({
4394 "invokingEvent": invoking_event,
4395 "ruleParameters": rule_parameters,
4396 "resultToken": result_token,
4397 "configRuleArn": format!("arn:aws:config:us-east-1:{account}:config-rule/{rule_name}"),
4398 "configRuleName": rule_name,
4399 "accountId": account,
4400 })
4401}
4402
4403fn select_fields(expr: &str) -> Vec<Value> {
4405 let lower = expr.to_ascii_lowercase();
4406 let Some(start) = lower.find("select ") else {
4407 return Vec::new();
4408 };
4409 let after = &expr[start + 7..];
4410 let end = after
4411 .to_ascii_lowercase()
4412 .find(" where ")
4413 .unwrap_or(after.len());
4414 after[..end]
4415 .split(',')
4416 .map(|s| json!({ "Name": s.trim() }))
4417 .collect()
4418}
4419
4420#[cfg(test)]
4421mod unit_tests {
4422 use super::*;
4423
4424 #[test]
4425 fn custom_rule_event_passes_stored_input_parameters() {
4426 let params = r#"{"maxAccessKeyAge":"90"}"#;
4427 let event = custom_rule_event("111122223333", "my-rule", Some(params), "my-rule#tok");
4428 assert_eq!(event["ruleParameters"], json!(params));
4431 assert_eq!(event["configRuleName"], json!("my-rule"));
4432 assert_eq!(event["accountId"], json!("111122223333"));
4433 assert_eq!(event["resultToken"], json!("my-rule#tok"));
4434 }
4435
4436 #[test]
4437 fn custom_rule_event_defaults_empty_parameters_to_empty_object() {
4438 let none = custom_rule_event("111122223333", "r", None, "r#t");
4439 assert_eq!(none["ruleParameters"], json!("{}"));
4440 let empty = custom_rule_event("111122223333", "r", Some(""), "r#t");
4441 assert_eq!(empty["ruleParameters"], json!("{}"));
4442 }
4443
4444 #[test]
4445 fn paginate_slices_and_emits_token() {
4446 let items: Vec<Value> = (0..5).map(|i| json!(i)).collect();
4447 let first_req = json!({ "Limit": 2 });
4448 let (page, next) = paginate(items.clone(), &first_req);
4449 assert_eq!(page, vec![json!(0), json!(1)]);
4450 let token = next.expect("more items remain -> token");
4451
4452 let second_req = json!({ "Limit": 2, "NextToken": token });
4453 let (page2, next2) = paginate(items.clone(), &second_req);
4454 assert_eq!(page2, vec![json!(2), json!(3)]);
4455 let token2 = next2.expect("still more");
4456
4457 let third_req = json!({ "Limit": 2, "NextToken": token2 });
4458 let (page3, next3) = paginate(items, &third_req);
4459 assert_eq!(page3, vec![json!(4)]);
4460 assert!(next3.is_none(), "last page has no token");
4461 }
4462
4463 #[test]
4464 fn paginate_no_limit_returns_all() {
4465 let items: Vec<Value> = (0..3).map(|i| json!(i)).collect();
4466 let (page, next) = paginate(items.clone(), &json!({}));
4467 assert_eq!(page, items);
4468 assert!(next.is_none());
4469 }
4470
4471 #[test]
4472 fn extract_pack_rule_names_parses_yaml_template() {
4473 let yaml = r#"
4474Resources:
4475 MyRule:
4476 Type: AWS::Config::ConfigRule
4477 Properties:
4478 ConfigRuleName: yaml-declared-rule
4479 Source:
4480 Owner: AWS
4481 SourceIdentifier: S3_BUCKET_VERSIONING_ENABLED
4482"#;
4483 let names = extract_pack_rule_names(Some(yaml));
4484 assert_eq!(names, vec!["yaml-declared-rule".to_string()]);
4485 }
4486}