Skip to main content

gossan_cloud/
lambda.rs

1//! AWS Lambda function URL discovery.
2
3use async_trait::async_trait;
4use gossan_core::Target;
5use secfinding::{Evidence, Finding, Severity};
6
7use crate::provider::CloudProvider;
8
9pub struct LambdaProvider;
10
11#[async_trait]
12impl CloudProvider for LambdaProvider {
13    fn name(&self) -> &'static str {
14        "lambda"
15    }
16
17    fn endpoint(&self, name: &str) -> String {
18        // Lambda Function URLs generally take the form: https://{url_id}.lambda-url.{region}.on.aws/
19        format!("https://{}.lambda-url.us-east-1.on.aws/", name)
20    }
21
22    async fn probe(
23        &self,
24        client: &reqwest::Client,
25        name: &str,
26        target: &Target,
27    ) -> anyhow::Result<Vec<Finding>> {
28        // Lambda URL IDs are exactly 32 lowercase alphanumeric characters
29        let lambda_id: String = name
30            .chars()
31            .filter(|c| c.is_ascii_alphanumeric())
32            .collect::<String>()
33            .to_lowercase();
34
35        if lambda_id.len() != 32 && name.len() != 32 {
36            return Ok(vec![]);
37        }
38
39        let url = self.endpoint(name);
40        let mut findings = Vec::new();
41
42        let resp = match client.get(&url).send().await {
43            Ok(r) => r,
44            Err(_) => return Ok(vec![]),
45        };
46
47        let status = resp.status().as_u16();
48
49        // If it's a 403 Forbidden, it might be IAM authenticated.
50        // If it's 200, it's public.
51        // A non-existent lambda URL usually just doesn't resolve (DNS NXDOMAIN).
52        match status {
53            200 | 401 | 403 | 404 | 500 | 502 => {
54                let body = gossan_core::net::bounded_text(resp, 4 * 1024 * 1024)
55                    .await
56                    .unwrap_or_default();
57
58                gossan_core::try_push_finding(crate::finding_builder(target, Severity::Low,
59                        format!("Lambda Function URL found: {}", name),
60                        format!(
61                            "https://{}.lambda-url.us-east-1.on.aws/ is resolving and returned HTTP {}. \
62                             This indicates an active Lambda Function URL.",
63                            name, status
64                        ))
65                    .evidence(Evidence::HttpResponse {
66                        status,
67                        headers: vec![("url".into(), url.clone().into())],
68                        body_excerpt: Some(body.chars().take(300).collect::<String>().into()),
69                    })
70                    .tag("lambda").tag("cloud").tag("aws").tag("serverless"), &mut findings);
71            }
72            _ => {}
73        }
74
75        Ok(findings)
76    }
77}