Skip to main content

aws_lite_rs/api/
securityhub.rs

1//! AWS Security Hub API client.
2//!
3//! Thin wrapper over generated ops.
4//!
5//! Needed by AWS CIS benchmark checks:
6//!   - CIS 5.16 (aws_security_hub_enabled): verify Security Hub is enabled
7//!     per account/region. `describe_hub()` succeeds → enabled;
8//!     `AwsError::NotFound` → not enabled.
9
10use crate::{
11    AwsError, AwsHttpClient, Result, ops::securityhub::SecurityhubOps,
12    types::securityhub::DescribeHubResponse,
13};
14
15/// Client for the AWS Security Hub API.
16pub struct SecurityHubClient<'a> {
17    ops: SecurityhubOps<'a>,
18}
19
20impl<'a> SecurityHubClient<'a> {
21    /// Create a new Security Hub client.
22    pub(crate) fn new(client: &'a AwsHttpClient) -> Self {
23        Self {
24            ops: SecurityhubOps::new(client),
25        }
26    }
27
28    // ── Hub ───────────────────────────────────────────────────────────
29
30    /// Return details about the Security Hub resource in the current account/region.
31    ///
32    /// Returns `Err(AwsError::NotFound { .. })` if Security Hub is not enabled.
33    ///
34    /// CIS 5.16: Security Hub must be enabled in every active region.
35    pub async fn describe_hub(&self) -> Result<DescribeHubResponse> {
36        self.ops.describe_hub("").await
37    }
38
39    /// Return `true` if Security Hub is enabled in the current account/region.
40    ///
41    /// Treats both 404 (`ResourceNotFoundException`) and 403
42    /// (`InvalidAccessException` — "not subscribed") as "not enabled".
43    ///
44    /// CIS 5.16: all active regions should have Security Hub enabled.
45    pub async fn is_enabled(&self) -> Result<bool> {
46        match self.ops.describe_hub("").await {
47            Ok(_) => Ok(true),
48            Err(AwsError::NotFound { .. }) => Ok(false),
49            // InvalidAccessException (403): "Account is not subscribed to Security Hub"
50            Err(AwsError::ServiceError { status: 403, .. }) => Ok(false),
51            Err(e) => Err(e),
52        }
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use crate::AwsError;
59    use serde_json::json;
60
61    #[tokio::test]
62    async fn test_describe_hub_returns_hub_details() {
63        let mut mock = crate::MockClient::new();
64        mock.expect_get("/accounts")
65            .returning_json(json!({
66                "HubArn": "arn:aws:securityhub:us-east-1:123456789012:hub/default",
67                "SubscribedAt": "2024-01-15T10:00:00.000Z",
68                "AutoEnableControls": true,
69                "ControlFindingGenerator": "SECURITY_CONTROL"
70            }))
71            .times(1);
72
73        let client = crate::AwsHttpClient::from_mock(mock);
74        let sh = client.securityhub();
75        let hub = sh.describe_hub().await.unwrap();
76        assert_eq!(
77            hub.hub_arn.as_deref(),
78            Some("arn:aws:securityhub:us-east-1:123456789012:hub/default")
79        );
80        assert_eq!(hub.auto_enable_controls, Some(true));
81        assert_eq!(
82            hub.control_finding_generator.as_deref(),
83            Some("SECURITY_CONTROL")
84        );
85    }
86
87    #[tokio::test]
88    async fn test_is_enabled_true() {
89        let mut mock = crate::MockClient::new();
90        mock.expect_get("/accounts")
91            .returning_json(json!({
92                "HubArn": "arn:aws:securityhub:us-east-1:123456789012:hub/default",
93                "SubscribedAt": "2024-01-15T10:00:00.000Z"
94            }))
95            .times(1);
96
97        let client = crate::AwsHttpClient::from_mock(mock);
98        assert!(client.securityhub().is_enabled().await.unwrap());
99    }
100
101    #[tokio::test]
102    async fn test_is_enabled_false_when_not_found() {
103        let mut mock = crate::MockClient::new();
104        mock.expect_get("/accounts")
105            .returning_error(AwsError::NotFound {
106                resource: "Account is not subscribed to AWS Security Hub".into(),
107            })
108            .times(1);
109
110        let client = crate::AwsHttpClient::from_mock(mock);
111        assert!(!client.securityhub().is_enabled().await.unwrap());
112    }
113
114    #[tokio::test]
115    async fn test_is_enabled_false_when_invalid_access() {
116        // InvalidAccessException (403) = "Account is not subscribed" — real-world case
117        let mut mock = crate::MockClient::new();
118        mock.expect_get("/accounts")
119            .returning_error(AwsError::ServiceError {
120                code: "HttpError403".into(),
121                message: "The AWS Access Key Id needs a subscription for the service".into(),
122                status: 403,
123            })
124            .times(1);
125
126        let client = crate::AwsHttpClient::from_mock(mock);
127        assert!(!client.securityhub().is_enabled().await.unwrap());
128    }
129}