cloud_detect/providers/
aws.rs

1//! Amazon Web Services (AWS).
2
3use std::path::Path;
4use std::time::Duration;
5
6use async_trait::async_trait;
7use serde::{Deserialize, Serialize};
8use tokio::fs;
9use tokio::sync::mpsc::Sender;
10use tracing::{debug, error, info, instrument};
11
12use crate::{Provider, ProviderId};
13
14const METADATA_URI: &str = "http://169.254.169.254";
15const METADATA_PATH: &str = "/latest/dynamic/instance-identity/document";
16const METADATA_TOKEN_PATH: &str = "/latest/api/token";
17const PRODUCT_VERSION_FILE: &str = "/sys/class/dmi/id/product_version";
18const BIOS_VENDOR_FILE: &str = "/sys/class/dmi/id/bios_vendor";
19pub(crate) const IDENTIFIER: ProviderId = ProviderId::AWS;
20
21#[derive(Serialize, Deserialize)]
22struct MetadataResponse {
23    #[serde(rename = "imageId")]
24    image_id: String,
25    #[serde(rename = "instanceId")]
26    instance_id: String,
27}
28
29pub(crate) struct Aws;
30
31#[async_trait]
32impl Provider for Aws {
33    fn identifier(&self) -> ProviderId {
34        IDENTIFIER
35    }
36
37    /// Tries to identify AWS using all the implemented options.
38    #[instrument(skip_all)]
39    async fn identify(&self, tx: Sender<ProviderId>, timeout: Duration) {
40        info!("Checking Amazon Web Services");
41        if self.check_product_version_file(PRODUCT_VERSION_FILE).await
42            || self.check_bios_vendor_file(BIOS_VENDOR_FILE).await
43            || self
44                .check_metadata_server_imdsv2(METADATA_URI, timeout)
45                .await
46            || self
47                .check_metadata_server_imdsv1(METADATA_URI, timeout)
48                .await
49        {
50            info!("Identified Amazon Web Services");
51            let res = tx.send(IDENTIFIER).await;
52
53            if let Err(err) = res {
54                error!("Error sending message: {:?}", err);
55            }
56        }
57    }
58}
59
60impl Aws {
61    /// Tries to identify AWS via metadata server (using IMDSv2).
62    #[instrument(skip_all)]
63    async fn check_metadata_server_imdsv2(&self, metadata_uri: &str, timeout: Duration) -> bool {
64        let token_url = format!("{metadata_uri}{METADATA_TOKEN_PATH}");
65        debug!("Retrieving {} IMDSv2 token from: {}", IDENTIFIER, token_url);
66
67        let client = if let Ok(client) = reqwest::Client::builder().timeout(timeout).build() {
68            client
69        } else {
70            error!("Error creating client");
71            return false;
72        };
73
74        let token = match client
75            .put(token_url)
76            .header("X-aws-ec2-metadata-token-ttl-seconds", "60")
77            .send()
78            .await
79        {
80            Ok(resp) => resp.text().await.unwrap_or_else(|err| {
81                error!("Error reading token: {:?}", err);
82                String::new()
83            }),
84            Err(err) => {
85                error!("Error making request: {:?}", err);
86                return false;
87            }
88        };
89
90        if token.is_empty() {
91            error!("IMDSv2 token is empty");
92            return false;
93        }
94
95        // Request to use the token to get metadata
96        let metadata_url = format!("{metadata_uri}{METADATA_PATH}");
97        debug!(
98            "Checking {} metadata using url: {}",
99            IDENTIFIER, metadata_url
100        );
101
102        let resp = match client
103            .get(metadata_url)
104            .header("X-aws-ec2-metadata-token", token)
105            .send()
106            .await
107        {
108            Ok(resp) => resp.json::<MetadataResponse>().await,
109            Err(err) => {
110                error!("Error making request: {:?}", err);
111                return false;
112            }
113        };
114
115        match resp {
116            Ok(metadata) => {
117                metadata.image_id.starts_with("ami-") && metadata.instance_id.starts_with("i-")
118            }
119            Err(err) => {
120                error!("Error reading response: {:?}", err);
121                false
122            }
123        }
124    }
125
126    /// Tries to identify AWS via metadata server (using IMDSv1).
127    #[instrument(skip_all)]
128    async fn check_metadata_server_imdsv1(&self, metadata_uri: &str, timeout: Duration) -> bool {
129        let url = format!("{metadata_uri}{METADATA_PATH}");
130        debug!("Checking {} metadata using url: {}", IDENTIFIER, url);
131
132        let client = if let Ok(client) = reqwest::Client::builder().timeout(timeout).build() {
133            client
134        } else {
135            error!("Error creating client");
136            return false;
137        };
138
139        match client.get(url).send().await {
140            Ok(resp) => match resp.json::<MetadataResponse>().await {
141                Ok(resp) => resp.image_id.starts_with("ami-") && resp.instance_id.starts_with("i-"),
142                Err(err) => {
143                    error!("Error reading response: {:?}", err);
144                    false
145                }
146            },
147            Err(err) => {
148                error!("Error making request: {:?}", err);
149                false
150            }
151        }
152    }
153
154    /// Tries to identify AWS using the product version file.
155    #[instrument(skip_all)]
156    async fn check_product_version_file<P: AsRef<Path>>(&self, product_version_file: P) -> bool {
157        debug!(
158            "Checking {} product version file: {}",
159            IDENTIFIER,
160            product_version_file.as_ref().display()
161        );
162
163        if product_version_file.as_ref().is_file() {
164            return match fs::read_to_string(product_version_file).await {
165                Ok(content) => content.to_lowercase().contains("amazon"),
166                Err(err) => {
167                    error!("Error reading file: {:?}", err);
168                    false
169                }
170            };
171        }
172
173        false
174    }
175
176    /// Tries to identify AWS using the BIOS vendor file.
177    #[instrument(skip_all)]
178    async fn check_bios_vendor_file<P: AsRef<Path>>(&self, bios_vendor_file: P) -> bool {
179        debug!(
180            "Checking {} BIOS vendor file: {}",
181            IDENTIFIER,
182            bios_vendor_file.as_ref().display()
183        );
184
185        if bios_vendor_file.as_ref().is_file() {
186            return match fs::read_to_string(bios_vendor_file).await {
187                Ok(content) => content.to_lowercase().contains("amazon"),
188                Err(err) => {
189                    error!("Error reading file: {:?}", err);
190                    false
191                }
192            };
193        }
194
195        false
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use std::io::Write;
202
203    use anyhow::Result;
204    use tempfile::NamedTempFile;
205    use wiremock::matchers::{header, path};
206    use wiremock::{Mock, MockServer, ResponseTemplate};
207
208    use super::*;
209
210    #[tokio::test]
211    async fn test_check_metadata_server_imdsv2_success() {
212        let mock_server = MockServer::start().await;
213
214        Mock::given(path(METADATA_TOKEN_PATH))
215            .and(header("X-aws-ec2-metadata-token-ttl-seconds", "60"))
216            .respond_with(ResponseTemplate::new(200).set_body_string("123abc"))
217            .expect(1)
218            .mount(&mock_server)
219            .await;
220
221        Mock::given(path(METADATA_PATH))
222            .respond_with(ResponseTemplate::new(200).set_body_json(MetadataResponse {
223                image_id: "ami-123abc".to_string(),
224                instance_id: "i-123abc".to_string(),
225            }))
226            .expect(1)
227            .mount(&mock_server)
228            .await;
229
230        let provider = Aws;
231        let metadata_uri = mock_server.uri();
232        let result = provider
233            .check_metadata_server_imdsv2(&metadata_uri, Duration::from_secs(1))
234            .await;
235
236        assert!(result);
237    }
238
239    #[tokio::test]
240    async fn test_check_metadata_server_imdsv2_failure() {
241        let mock_server = MockServer::start().await;
242
243        Mock::given(path(METADATA_TOKEN_PATH))
244            .respond_with(ResponseTemplate::new(200).set_body_string("123abc"))
245            .expect(1)
246            .mount(&mock_server)
247            .await;
248
249        Mock::given(path(METADATA_PATH))
250            .respond_with(ResponseTemplate::new(200).set_body_json(MetadataResponse {
251                image_id: "abc".to_string(),
252                instance_id: "abc".to_string(),
253            }))
254            .expect(1)
255            .mount(&mock_server)
256            .await;
257
258        let provider = Aws;
259        let metadata_uri = mock_server.uri();
260        let result = provider
261            .check_metadata_server_imdsv2(&metadata_uri, Duration::from_secs(1))
262            .await;
263
264        assert!(!result);
265    }
266
267    #[tokio::test]
268    async fn test_check_metadata_server_imdsv1_success() {
269        let mock_server = MockServer::start().await;
270        Mock::given(path(METADATA_PATH))
271            .respond_with(ResponseTemplate::new(200).set_body_json(MetadataResponse {
272                image_id: "ami-123abc".to_string(),
273                instance_id: "i-123abc".to_string(),
274            }))
275            .expect(1)
276            .mount(&mock_server)
277            .await;
278
279        let provider = Aws;
280        let metadata_uri = mock_server.uri();
281        let result = provider
282            .check_metadata_server_imdsv1(&metadata_uri, Duration::from_secs(1))
283            .await;
284
285        assert!(result);
286    }
287
288    #[tokio::test]
289    async fn test_check_metadata_server_imdsv1_failure() {
290        let mock_server = MockServer::start().await;
291        Mock::given(path(METADATA_PATH))
292            .respond_with(ResponseTemplate::new(200).set_body_json(MetadataResponse {
293                image_id: "abc".to_string(),
294                instance_id: "abc".to_string(),
295            }))
296            .expect(1)
297            .mount(&mock_server)
298            .await;
299
300        let provider = Aws;
301        let metadata_uri = mock_server.uri();
302        let result = provider
303            .check_metadata_server_imdsv1(&metadata_uri, Duration::from_secs(1))
304            .await;
305
306        assert!(!result);
307    }
308
309    #[tokio::test]
310    async fn test_check_product_version_file_success() -> Result<()> {
311        let mut product_version_file = NamedTempFile::new()?;
312        product_version_file.write_all(b"amazon")?;
313
314        let provider = Aws;
315        let result = provider
316            .check_product_version_file(product_version_file.path())
317            .await;
318
319        assert!(result);
320
321        Ok(())
322    }
323
324    #[tokio::test]
325    async fn test_check_product_version_file_failure() -> Result<()> {
326        let product_version_file = NamedTempFile::new()?;
327
328        let provider = Aws;
329        let result = provider
330            .check_product_version_file(product_version_file.path())
331            .await;
332
333        assert!(!result);
334
335        Ok(())
336    }
337
338    #[tokio::test]
339    async fn test_check_bios_vendor_file_success() -> Result<()> {
340        let mut bios_vendor_file = NamedTempFile::new()?;
341        bios_vendor_file.write_all(b"amazon")?;
342
343        let provider = Aws;
344        let result = provider
345            .check_bios_vendor_file(bios_vendor_file.path())
346            .await;
347
348        assert!(result);
349
350        Ok(())
351    }
352
353    #[tokio::test]
354    async fn test_check_bios_vendor_file_failure() -> Result<()> {
355        let bios_vendor_file = NamedTempFile::new()?;
356
357        let provider = Aws;
358        let result = provider
359            .check_bios_vendor_file(bios_vendor_file.path())
360            .await;
361
362        assert!(!result);
363
364        Ok(())
365    }
366}