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).await.unwrap_or_default();
56                
57                // CloudFront generic error when it doesn't exist usually is a DNS error or 403 Error from CloudFront.
58                // An active one returns something else.
59                // We'll report if it resolves and returns.
60                if body.contains("<Error><Code>NoSuchDistribution</Code>") {
61                    // Not found
62                } else {
63                    gossan_core::try_push_finding(crate::finding_builder(target, Severity::Low,
64                            format!("CloudFront Distribution found: {}", name),
65                            format!(
66                                "https://{}.cloudfront.net/ is resolving and returned HTTP {}. \
67                                 This indicates an active CloudFront distribution.",
68                                name, status
69                            ))
70                        .evidence(Evidence::HttpResponse {
71                            status,
72                            headers: vec![("url".into(), url.clone().into())],
73                            body_excerpt: Some(body.chars().take(300).collect::<String>().into()),
74                        })
75                        .tag("cloudfront").tag("cloud").tag("cdn"), &mut findings);
76                }
77            }
78            _ => {}
79        }
80
81        Ok(findings)
82    }
83}