Skip to main content

gossan_cloud/
cloudfront.rs

1//! CloudFront distribution discovery via CNAME probing.
2
3use async_trait::async_trait;
4use gossan_core::Target;
5use secfinding::{Evidence, Finding, Severity};
6
7use crate::provider::CloudProvider;
8
9pub struct CloudFrontProvider;
10
11#[async_trait]
12impl CloudProvider for CloudFrontProvider {
13    fn name(&self) -> &'static str {
14        "cloudfront"
15    }
16
17    fn endpoint(&self, name: &str) -> String {
18        format!("https://{}.cloudfront.net/", name)
19    }
20
21    async fn probe(
22        &self,
23        client: &reqwest::Client,
24        name: &str,
25        target: &Target,
26    ) -> anyhow::Result<Vec<Finding>> {
27        // CloudFront distributions have a length of exactly 14 alphanumeric characters.
28        // E.g. d111111abcdef8.cloudfront.net
29        let dist: String = name
30            .chars()
31            .filter(|c| c.is_ascii_alphanumeric())
32            .collect::<String>()
33            .to_lowercase();
34
35        // CloudFront distributions ID length logic
36        // Though some org permutations might be checked, cloudfront domains usually look like d[0-9a-z]{13}
37        if dist.len() > 63 {
38            return Ok(vec![]);
39        }
40
41        let url = self.endpoint(name);
42        let mut findings = Vec::new();
43
44        let resp = match client.get(&url).send().await {
45            Ok(r) => r,
46            Err(_) => return Ok(vec![]),
47        };
48
49        let status = resp.status().as_u16();
50
51        // If it's anything but a 403 matching "Bad request", it might be an active distribution.
52        // Actually, we look for 200/403/404. Let's just flag 200 or 403 as existence.
53        match status {
54            200 | 401 | 403 => {
55                let body = gossan_core::net::bounded_text(resp, 4 * 1024 * 1024)
56                    .await
57                    .unwrap_or_default();
58
59                // CloudFront generic error when it doesn't exist usually is a DNS error or 403 Error from CloudFront.
60                // An active one returns something else.
61                // We'll report if it resolves and returns.
62                if body.contains("<Error><Code>NoSuchDistribution</Code>") {
63                    // Not found
64                } else {
65                    gossan_core::try_push_finding(
66                        crate::finding_builder(
67                            target,
68                            Severity::Low,
69                            format!("CloudFront Distribution found: {}", name),
70                            format!(
71                                "https://{}.cloudfront.net/ is resolving and returned HTTP {}. \
72                                 This indicates an active CloudFront distribution.",
73                                name, status
74                            ),
75                        )
76                        .evidence(Evidence::HttpResponse {
77                            status,
78                            headers: vec![("url".into(), url.clone().into())],
79                            body_excerpt: Some(body.chars().take(300).collect::<String>().into()),
80                        })
81                        .tag("cloudfront")
82                        .tag("cloud")
83                        .tag("cdn"),
84                        &mut findings,
85                    );
86                }
87            }
88            _ => {}
89        }
90
91        Ok(findings)
92    }
93}