cloud_detect/providers/
aws.rs1use 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 #[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 #[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 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 #[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 #[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 #[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}