cloudcheck 8.4.0__tar.gz → 8.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/Cargo.lock +1 -1
  2. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/Cargo.toml +1 -1
  3. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/PKG-INFO +5 -5
  4. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/README.md +4 -4
  5. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck_update/__init__.py +0 -41
  6. cloudcheck-8.4.1/dist/cloudcheck-8.4.1-cp314-cp314-manylinux_2_38_x86_64.whl +0 -0
  7. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/pyproject.toml +1 -1
  8. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/pytest.ini +2 -2
  9. cloudcheck-8.4.1/src/lib.rs +281 -0
  10. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/uv.lock +1 -1
  11. cloudcheck-8.4.0/dist/cloudcheck-8.4.0-cp314-cp314-manylinux_2_38_x86_64.whl +0 -0
  12. cloudcheck-8.4.0/src/lib.rs +0 -215
  13. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/.github/dependabot.yml +0 -0
  14. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/.github/workflows/pipeline-tests.yml +0 -0
  15. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/.github/workflows/python-tests.yml +0 -0
  16. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/.github/workflows/rust-tests.yml +0 -0
  17. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/.gitignore +0 -0
  18. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloud_providers.json +0 -0
  19. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloud_providers_v2.json +0 -0
  20. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/__init__.py +0 -0
  21. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/helpers.py +0 -0
  22. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/akamai.py +0 -0
  23. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/alibaba.py +0 -0
  24. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/amazon.py +0 -0
  25. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/arvancloud.py +0 -0
  26. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/azure.py +0 -0
  27. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/backblaze.py +0 -0
  28. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/base.py +0 -0
  29. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/cisco.py +0 -0
  30. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/cloudflare.py +0 -0
  31. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/cloudfront.py +0 -0
  32. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/dell.py +0 -0
  33. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/digitalocean.py +0 -0
  34. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/fastly.py +0 -0
  35. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/github.py +0 -0
  36. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/google.py +0 -0
  37. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/heroku.py +0 -0
  38. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/hetzner.py +0 -0
  39. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/hpe.py +0 -0
  40. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/huawei.py +0 -0
  41. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/ibm.py +0 -0
  42. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/imperva.py +0 -0
  43. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/kamatera.py +0 -0
  44. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/oracle.py +0 -0
  45. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/ovh.py +0 -0
  46. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/rackspace.py +0 -0
  47. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/salesforce.py +0 -0
  48. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/scaleway.py +0 -0
  49. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/tencent.py +0 -0
  50. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/wasabi.py +0 -0
  51. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/zoho.py +0 -0
  52. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck_update/cli.py +0 -0
  53. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck_update/test_cloudcheck_update.py +0 -0
  54. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/src/main.rs +0 -0
  55. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/src/python.rs +0 -0
  56. {cloudcheck-8.4.0 → cloudcheck-8.4.1}/test_cloudcheck.py +0 -0
@@ -56,7 +56,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
56
56
 
57
57
  [[package]]
58
58
  name = "cloudcheck"
59
- version = "8.4.0"
59
+ version = "8.4.1"
60
60
  dependencies = [
61
61
  "openssl",
62
62
  "pyo3",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "cloudcheck"
3
- version = "8.4.0"
3
+ version = "8.4.1"
4
4
  edition = "2024"
5
5
  description = "CloudCheck is a simple Rust tool to check whether an IP address or hostname belongs to a cloud provider."
6
6
  license = "GPL-3.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudcheck
3
- Version: 8.4.0
3
+ Version: 8.4.1
4
4
  Summary: Detailed database of cloud providers. Instantly look up a domain or IP address
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
@@ -11,11 +11,11 @@ Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
11
11
  [![PyPI](https://img.shields.io/pypi/v/cloudcheck)](https://pypi.org/project/cloudcheck/)
12
12
  [![Rust Version](https://img.shields.io/badge/rust-1.70+-orange)](https://www.rust-lang.org)
13
13
  [![Crates.io](https://img.shields.io/crates/v/cloudcheck?color=orange)](https://crates.io/crates/cloudcheck)
14
- [![License](https://img.shields.io/badge/license-GPLv3-blue.svg)](https://github.com/blacklanternsecurity/cloudcheck/blob/master/LICENSE)
14
+ [![License](https://img.shields.io/badge/license-GPLv3-blue.svg)](https://github.com/blacklanternsecurity/cloudcheck/blob/stable/LICENSE)
15
15
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
16
- [![Rust Tests](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/rust-tests.yml/badge.svg?branch=master)](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/rust-tests.yml)
17
- [![Python Tests](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/python-tests.yml/badge.svg?branch=master)](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/python-tests.yml)
18
- [![Pipeline Tests](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/pipeline-tests.yml/badge.svg?branch=master)](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/pipeline-tests.yml)
16
+ [![Rust Tests](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/rust-tests.yml/badge.svg?branch=stable)](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/rust-tests.yml)
17
+ [![Python Tests](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/python-tests.yml/badge.svg?branch=stable)](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/python-tests.yml)
18
+ [![Pipeline Tests](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/pipeline-tests.yml/badge.svg?branch=stable)](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/pipeline-tests.yml)
19
19
 
20
20
  ### UPDATE: Now rewritten in Rust, with [34 supported cloud providers](#supported-cloud-providers)!
21
21
 
@@ -4,11 +4,11 @@
4
4
  [![PyPI](https://img.shields.io/pypi/v/cloudcheck)](https://pypi.org/project/cloudcheck/)
5
5
  [![Rust Version](https://img.shields.io/badge/rust-1.70+-orange)](https://www.rust-lang.org)
6
6
  [![Crates.io](https://img.shields.io/crates/v/cloudcheck?color=orange)](https://crates.io/crates/cloudcheck)
7
- [![License](https://img.shields.io/badge/license-GPLv3-blue.svg)](https://github.com/blacklanternsecurity/cloudcheck/blob/master/LICENSE)
7
+ [![License](https://img.shields.io/badge/license-GPLv3-blue.svg)](https://github.com/blacklanternsecurity/cloudcheck/blob/stable/LICENSE)
8
8
  [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
9
- [![Rust Tests](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/rust-tests.yml/badge.svg?branch=master)](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/rust-tests.yml)
10
- [![Python Tests](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/python-tests.yml/badge.svg?branch=master)](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/python-tests.yml)
11
- [![Pipeline Tests](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/pipeline-tests.yml/badge.svg?branch=master)](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/pipeline-tests.yml)
9
+ [![Rust Tests](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/rust-tests.yml/badge.svg?branch=stable)](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/rust-tests.yml)
10
+ [![Python Tests](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/python-tests.yml/badge.svg?branch=stable)](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/python-tests.yml)
11
+ [![Pipeline Tests](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/pipeline-tests.yml/badge.svg?branch=stable)](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/pipeline-tests.yml)
12
12
 
13
13
  ### UPDATE: Now rewritten in Rust, with [34 supported cloud providers](#supported-cloud-providers)!
14
14
 
@@ -11,47 +11,6 @@ from cloudcheck.providers.base import BaseProvider
11
11
  logging.basicConfig(level=logging.INFO)
12
12
  log = logging.getLogger("cloudcheck")
13
13
 
14
- # Dictionary to store loaded provider classes
15
- _provider_classes: Dict[str, Type[BaseProvider]] = {}
16
- # Dictionary to store instantiated providers
17
- providers: Dict[str, BaseProvider] = {}
18
-
19
-
20
- def load_provider_classes() -> Dict[str, Type[BaseProvider]]:
21
- """Dynamically load all cloud provider classes from the providers directory."""
22
- global _provider_classes
23
-
24
- if _provider_classes:
25
- return _provider_classes
26
-
27
- providers_path = Path(__file__).parent.parent / "cloudcheck" / "providers"
28
-
29
- for file in providers_path.glob("*.py"):
30
- if file.stem in ("base", "__init__"):
31
- continue
32
-
33
- try:
34
- import_path = f"cloudcheck.providers.{file.stem}"
35
- module = importlib.import_module(import_path)
36
-
37
- # Look for classes that inherit from BaseProvider
38
- for attr_name in dir(module):
39
- attr = getattr(module, attr_name)
40
- if (
41
- isinstance(attr, type)
42
- and issubclass(attr, BaseProvider)
43
- and attr != BaseProvider
44
- ):
45
- provider_name = attr.__name__.lower()
46
- _provider_classes[provider_name] = attr
47
- print(f"Loaded provider class: {attr.__name__}")
48
-
49
- except Exception as e:
50
- log.error(f"Failed to load provider from {file}: {e}")
51
- raise
52
-
53
- return _provider_classes
54
-
55
14
 
56
15
  project_root = Path(__file__).parent.parent
57
16
  json_path = project_root / "cloud_providers_v2.json"
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "cloudcheck"
7
- version = "8.4.0"
7
+ version = "8.4.1"
8
8
  description = "Detailed database of cloud providers. Instantly look up a domain or IP address"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -3,5 +3,5 @@ testpaths = .
3
3
  python_files = test_*.py
4
4
  python_classes = Test*
5
5
  python_functions = test_*
6
- norecursedirs = old
7
- addopts = --ignore=old
6
+ norecursedirs = old target
7
+ addopts = --ignore=old --ignore=target
@@ -0,0 +1,281 @@
1
+ use radixtarget::{RadixTarget, ScopeMode};
2
+ use serde::{Deserialize, Serialize};
3
+ use std::collections::HashMap;
4
+ use std::path::PathBuf;
5
+ use std::sync::Arc;
6
+ use std::time::{Duration, SystemTime};
7
+ use tokio::sync::{Mutex, RwLock};
8
+
9
+ #[cfg(feature = "py")]
10
+ mod python;
11
+
12
+ const CLOUDCHECK_SIGNATURE_URL: &str = "https://raw.githubusercontent.com/blacklanternsecurity/cloudcheck/refs/heads/stable/cloud_providers_v2.json";
13
+
14
+ #[derive(Debug, Clone, Serialize, Deserialize)]
15
+ pub struct CloudProvider {
16
+ pub name: String,
17
+ pub tags: Vec<String>,
18
+ }
19
+
20
+ #[derive(Debug, Deserialize)]
21
+ struct ProviderData {
22
+ name: String,
23
+ tags: Vec<String>,
24
+ cidrs: Vec<String>,
25
+ domains: Vec<String>,
26
+ }
27
+
28
+ type ProvidersMap = HashMap<String, Vec<CloudProvider>>;
29
+ type Error = Box<dyn std::error::Error + Send + Sync>;
30
+
31
+ #[derive(Clone)]
32
+ pub struct CloudCheck {
33
+ radix: Arc<RwLock<Option<RadixTarget>>>,
34
+ providers: Arc<RwLock<Option<ProvidersMap>>>,
35
+ last_fetch: Arc<Mutex<Option<SystemTime>>>,
36
+ }
37
+
38
+ impl Default for CloudCheck {
39
+ fn default() -> Self {
40
+ Self::new()
41
+ }
42
+ }
43
+
44
+ impl CloudCheck {
45
+ pub fn new() -> Self {
46
+ CloudCheck {
47
+ radix: Arc::new(RwLock::new(None)),
48
+ providers: Arc::new(RwLock::new(None)),
49
+ last_fetch: Arc::new(Mutex::new(None)),
50
+ }
51
+ }
52
+
53
+ fn get_signature_url() -> String {
54
+ std::env::var("CLOUDCHECK_SIGNATURE_URL")
55
+ .unwrap_or_else(|_| CLOUDCHECK_SIGNATURE_URL.to_string())
56
+ }
57
+
58
+ fn get_cache_path() -> Result<PathBuf, Error> {
59
+ let home = std::env::var("HOME")?;
60
+ let mut path = PathBuf::from(home);
61
+ path.push(".cache");
62
+ path.push("cloudcheck");
63
+ path.push("cloud_providers_v2.json");
64
+ Ok(path)
65
+ }
66
+
67
+ async fn fetch_and_cache(cache_path: &PathBuf) -> Result<String, Error> {
68
+ let url = Self::get_signature_url();
69
+ let response = reqwest::get(&url).await?;
70
+ let json_data = response.text().await?;
71
+
72
+ if let Some(parent) = cache_path.parent() {
73
+ tokio::fs::create_dir_all(parent).await?;
74
+ }
75
+ tokio::fs::write(cache_path, &json_data).await?;
76
+
77
+ Ok(json_data)
78
+ }
79
+
80
+ /// Gets the last fetch time, checking in-memory timestamp first.
81
+ /// If no in-memory timestamp exists (first run), falls back to checking
82
+ /// the cache file's modification time. Returns None if file doesn't exist.
83
+ async fn get_last_fetch_time(&self, cache_path: &PathBuf) -> Result<Option<SystemTime>, Error> {
84
+ let last_fetch = self.last_fetch.lock().await;
85
+ match *last_fetch {
86
+ Some(time) => Ok(Some(time)),
87
+ None => {
88
+ // No in-memory timestamp - check file modification time
89
+ drop(last_fetch);
90
+ match tokio::fs::metadata(cache_path).await {
91
+ Ok(metadata) => Ok(metadata.modified().ok()),
92
+ Err(_) => Ok(None),
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ /// Loads JSON data either from network (if refresh needed) or from cache file.
99
+ /// Returns (json_data, fetched_fresh) where fetched_fresh indicates if we
100
+ /// fetched from network. Sets last_fetch timestamp on first cache load to
101
+ /// track process runtime. Falls back to network fetch if cache read fails.
102
+ async fn load_json_data(
103
+ &self,
104
+ cache_path: &PathBuf,
105
+ needs_refresh: bool,
106
+ ) -> Result<(String, bool), Error> {
107
+ if needs_refresh {
108
+ let data = Self::fetch_and_cache(cache_path).await?;
109
+ Ok((data, true))
110
+ } else {
111
+ match tokio::fs::read_to_string(cache_path).await {
112
+ Ok(data) => {
113
+ // First load from cache - set timestamp to track process runtime
114
+ let now = SystemTime::now();
115
+ let mut last_fetch = self.last_fetch.lock().await;
116
+ if last_fetch.is_none() {
117
+ *last_fetch = Some(now);
118
+ }
119
+ Ok((data, false))
120
+ }
121
+ Err(_) => {
122
+ // Cache file was deleted between stat and read, fetch fresh
123
+ let data = Self::fetch_and_cache(cache_path).await?;
124
+ Ok((data, true))
125
+ }
126
+ }
127
+ }
128
+ }
129
+
130
+ /// Parses JSON and builds the radix tree and providers map.
131
+ /// For each provider, inserts all CIDRs and domains into the radix tree,
132
+ /// normalizing them in the process. Maps normalized values to provider lists.
133
+ fn build_data_structures(json_data: &str) -> Result<(RadixTarget, ProvidersMap), Error> {
134
+ let providers_data: HashMap<String, ProviderData> = serde_json::from_str(json_data)?;
135
+
136
+ let mut radix = RadixTarget::new(&[], ScopeMode::Normal)?;
137
+ let mut providers_map: ProvidersMap = HashMap::new();
138
+
139
+ for (_, provider) in providers_data {
140
+ let cloud_provider = CloudProvider {
141
+ name: provider.name.clone(),
142
+ tags: provider.tags.clone(),
143
+ };
144
+
145
+ // Insert all CIDRs for this provider
146
+ for cidr in provider.cidrs {
147
+ let normalized = match radix.get(&cidr) {
148
+ Some(n) => n,
149
+ None => match radix.insert(&cidr) {
150
+ Ok(Some(n)) => n,
151
+ Ok(None) => continue,
152
+ Err(e) => {
153
+ eprintln!("Error inserting CIDR '{}': {}", cidr, e);
154
+ continue;
155
+ }
156
+ },
157
+ };
158
+ providers_map
159
+ .entry(normalized.clone())
160
+ .or_default()
161
+ .push(cloud_provider.clone());
162
+ }
163
+
164
+ // Insert all domains for this provider
165
+ for domain in provider.domains {
166
+ let normalized = match radix.get(&domain) {
167
+ Some(n) => n,
168
+ None => match radix.insert(&domain) {
169
+ Ok(Some(n)) => n,
170
+ Ok(None) => continue,
171
+ Err(e) => {
172
+ eprintln!("Error inserting domain '{}': {}", domain, e);
173
+ continue;
174
+ }
175
+ },
176
+ };
177
+ providers_map
178
+ .entry(normalized.clone())
179
+ .or_default()
180
+ .push(cloud_provider.clone());
181
+ }
182
+ }
183
+
184
+ Ok((radix, providers_map))
185
+ }
186
+
187
+ /// Ensures data is loaded and fresh. Checks if refresh is needed based on
188
+ /// 24-hour process runtime. Returns early if data is already loaded and fresh.
189
+ /// Otherwise loads data (from network or cache), builds structures, and updates
190
+ /// the in-memory timestamp if we fetched fresh data.
191
+ async fn ensure_loaded(&self) -> Result<(), Error> {
192
+ let cache_valid_duration = Duration::from_secs(24 * 60 * 60);
193
+ let now = SystemTime::now();
194
+ let cache_path = Self::get_cache_path()?;
195
+
196
+ // Check if we need refresh (uses in-memory timestamp, falls back to file stat)
197
+ let last_fetch_time = self.get_last_fetch_time(&cache_path).await?;
198
+ let needs_refresh = match last_fetch_time {
199
+ Some(fetch_time) => now
200
+ .duration_since(fetch_time)
201
+ .map(|elapsed| elapsed >= cache_valid_duration)
202
+ .unwrap_or(true),
203
+ None => true,
204
+ };
205
+
206
+ // Early return if data is already loaded and fresh
207
+ {
208
+ let radix_guard = self.radix.read().await;
209
+ if radix_guard.is_some() && !needs_refresh {
210
+ return Ok(());
211
+ }
212
+ }
213
+
214
+ // Load JSON data and build structures
215
+ let (json_data, fetched_fresh) = self.load_json_data(&cache_path, needs_refresh).await?;
216
+ let (radix, providers_map) = Self::build_data_structures(&json_data)?;
217
+
218
+ // Update in-memory data structures
219
+ {
220
+ let mut radix_guard = self.radix.write().await;
221
+ *radix_guard = Some(radix);
222
+ }
223
+ {
224
+ let mut providers_guard = self.providers.write().await;
225
+ *providers_guard = Some(providers_map);
226
+ }
227
+
228
+ // Update timestamp if we fetched fresh data
229
+ if fetched_fresh {
230
+ let mut last_fetch = self.last_fetch.lock().await;
231
+ *last_fetch = Some(now);
232
+ }
233
+
234
+ Ok(())
235
+ }
236
+
237
+ pub async fn lookup(&self, target: &str) -> Result<Vec<CloudProvider>, Error> {
238
+ self.ensure_loaded().await?;
239
+
240
+ let radix_guard = self.radix.read().await;
241
+ let providers_guard = self.providers.read().await;
242
+
243
+ let radix = radix_guard.as_ref().unwrap();
244
+ let providers = providers_guard.as_ref().unwrap();
245
+
246
+ if let Some(normalized) = radix.get(target) {
247
+ Ok(providers.get(&normalized).cloned().unwrap_or_default())
248
+ } else {
249
+ Ok(Vec::new())
250
+ }
251
+ }
252
+ }
253
+
254
+ #[cfg(test)]
255
+ mod tests {
256
+ use super::*;
257
+
258
+ #[tokio::test]
259
+ async fn test_lookup_google_dns() {
260
+ let cloudcheck = CloudCheck::new();
261
+ let results = cloudcheck.lookup("8.8.8.8").await.unwrap();
262
+ let names: Vec<String> = results.iter().map(|p| p.name.clone()).collect();
263
+ assert!(
264
+ names.contains(&"Google".to_string()),
265
+ "Expected Google in results: {:?}",
266
+ names
267
+ );
268
+ }
269
+
270
+ #[tokio::test]
271
+ async fn test_lookup_amazon_domain() {
272
+ let cloudcheck = CloudCheck::new();
273
+ let results = cloudcheck.lookup("asdf.amazon.com").await.unwrap();
274
+ let names: Vec<String> = results.iter().map(|p| p.name.clone()).collect();
275
+ assert!(
276
+ names.contains(&"Amazon".to_string()),
277
+ "Expected Amazon in results: {:?}",
278
+ names
279
+ );
280
+ }
281
+ }
@@ -140,7 +140,7 @@ wheels = [
140
140
 
141
141
  [[package]]
142
142
  name = "cloudcheck"
143
- version = "8.4.0"
143
+ version = "8.4.1"
144
144
  source = { editable = "." }
145
145
 
146
146
  [package.dev-dependencies]
@@ -1,215 +0,0 @@
1
- use radixtarget::{RadixTarget, ScopeMode};
2
- use serde::{Deserialize, Serialize};
3
- use std::collections::HashMap;
4
- use std::path::PathBuf;
5
- use std::sync::Arc;
6
- use std::time::{Duration, SystemTime};
7
- use tokio::sync::OnceCell;
8
-
9
- #[cfg(feature = "py")]
10
- mod python;
11
-
12
- const CLOUDCHECK_SIGNATURE_URL: &str = "https://raw.githubusercontent.com/blacklanternsecurity/cloudcheck/refs/heads/stable/cloud_providers_v2.json";
13
-
14
- #[derive(Debug, Clone, Serialize, Deserialize)]
15
- pub struct CloudProvider {
16
- pub name: String,
17
- pub tags: Vec<String>,
18
- }
19
-
20
- #[derive(Debug, Deserialize)]
21
- struct ProviderData {
22
- name: String,
23
- tags: Vec<String>,
24
- cidrs: Vec<String>,
25
- domains: Vec<String>,
26
- }
27
-
28
- #[derive(Clone)]
29
- pub struct CloudCheck {
30
- radix: Arc<OnceCell<RadixTarget>>,
31
- providers: Arc<OnceCell<HashMap<String, Vec<CloudProvider>>>>,
32
- }
33
-
34
- impl Default for CloudCheck {
35
- fn default() -> Self {
36
- Self::new()
37
- }
38
- }
39
-
40
- impl CloudCheck {
41
- pub fn new() -> Self {
42
- CloudCheck {
43
- radix: Arc::new(OnceCell::new()),
44
- providers: Arc::new(OnceCell::new()),
45
- }
46
- }
47
-
48
- fn get_signature_url() -> String {
49
- std::env::var("CLOUDCHECK_SIGNATURE_URL")
50
- .unwrap_or_else(|_| CLOUDCHECK_SIGNATURE_URL.to_string())
51
- }
52
-
53
- fn get_cache_path() -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
54
- let home = std::env::var("HOME")?;
55
- let mut path = PathBuf::from(home);
56
- path.push(".cache");
57
- path.push("cloudcheck");
58
- path.push("cloud_providers_v2.json");
59
- Ok(path)
60
- }
61
-
62
- async fn fetch_and_cache(
63
- cache_path: &PathBuf,
64
- ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
65
- let url = Self::get_signature_url();
66
- let response = reqwest::get(&url).await?;
67
- let json_data = response.text().await?;
68
-
69
- if let Some(parent) = cache_path.parent() {
70
- tokio::fs::create_dir_all(parent).await?;
71
- }
72
- tokio::fs::write(cache_path, &json_data).await?;
73
-
74
- Ok(json_data)
75
- }
76
-
77
- async fn ensure_loaded(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
78
- let radix_cell = Arc::clone(&self.radix);
79
- let providers_cell = Arc::clone(&self.providers);
80
-
81
- radix_cell
82
- .get_or_try_init(|| async {
83
- let cache_path = Self::get_cache_path()?;
84
- let cache_valid_duration = Duration::from_secs(24 * 60 * 60);
85
-
86
- let json_data = match tokio::fs::metadata(&cache_path).await {
87
- Ok(metadata) => {
88
- let modified = metadata.modified()?;
89
- let now = SystemTime::now();
90
-
91
- if let Ok(elapsed) = now.duration_since(modified) {
92
- if elapsed < cache_valid_duration {
93
- tokio::fs::read_to_string(&cache_path).await?
94
- } else {
95
- Self::fetch_and_cache(&cache_path).await?
96
- }
97
- } else {
98
- Self::fetch_and_cache(&cache_path).await?
99
- }
100
- }
101
- Err(_) => Self::fetch_and_cache(&cache_path).await?,
102
- };
103
-
104
- let providers_data: HashMap<String, ProviderData> =
105
- serde_json::from_str(&json_data)?;
106
-
107
- let mut radix = RadixTarget::new(&[], ScopeMode::Normal)?;
108
- let mut providers_map: HashMap<String, Vec<CloudProvider>> = HashMap::new();
109
-
110
- for (_, provider) in providers_data {
111
- let cloud_provider = CloudProvider {
112
- name: provider.name.clone(),
113
- tags: provider.tags.clone(),
114
- };
115
-
116
- for cidr in provider.cidrs {
117
- let normalized = match radix.get(&cidr) {
118
- Some(n) => n,
119
- None => match radix.insert(&cidr) {
120
- Ok(Some(n)) => n,
121
- Ok(None) => continue,
122
- Err(e) => {
123
- eprintln!("Error inserting CIDR '{}': {}", cidr, e);
124
- continue;
125
- }
126
- },
127
- };
128
- providers_map
129
- .entry(normalized.clone())
130
- .or_default()
131
- .push(cloud_provider.clone());
132
- }
133
-
134
- for domain in provider.domains {
135
- // Clean domain: strip comments (everything after #) and trim whitespace
136
- let cleaned_domain = domain.split('#').next().unwrap_or(&domain).trim();
137
-
138
- if cleaned_domain.is_empty() {
139
- continue;
140
- }
141
-
142
- let normalized = match radix.get(cleaned_domain) {
143
- Some(n) => n,
144
- None => match radix.insert(cleaned_domain) {
145
- Ok(Some(n)) => n,
146
- Ok(None) => continue,
147
- Err(e) => {
148
- eprintln!("Error inserting domain '{}': {}", cleaned_domain, e);
149
- continue;
150
- }
151
- },
152
- };
153
- providers_map
154
- .entry(normalized.clone())
155
- .or_default()
156
- .push(cloud_provider.clone());
157
- }
158
- }
159
-
160
- providers_cell
161
- .set(providers_map)
162
- .map_err(|_| "Failed to set providers")?;
163
-
164
- Ok::<RadixTarget, Box<dyn std::error::Error + Send + Sync>>(radix)
165
- })
166
- .await?;
167
-
168
- Ok(())
169
- }
170
-
171
- pub async fn lookup(
172
- &self,
173
- target: &str,
174
- ) -> Result<Vec<CloudProvider>, Box<dyn std::error::Error + Send + Sync>> {
175
- self.ensure_loaded().await?;
176
-
177
- let radix = self.radix.get().unwrap();
178
- let providers = self.providers.get().unwrap();
179
-
180
- if let Some(normalized) = radix.get(target) {
181
- Ok(providers.get(&normalized).cloned().unwrap_or_default())
182
- } else {
183
- Ok(Vec::new())
184
- }
185
- }
186
- }
187
-
188
- #[cfg(test)]
189
- mod tests {
190
- use super::*;
191
-
192
- #[tokio::test]
193
- async fn test_lookup_google_dns() {
194
- let cloudcheck = CloudCheck::new();
195
- let results = cloudcheck.lookup("8.8.8.8").await.unwrap();
196
- let names: Vec<String> = results.iter().map(|p| p.name.clone()).collect();
197
- assert!(
198
- names.contains(&"Google".to_string()),
199
- "Expected Google in results: {:?}",
200
- names
201
- );
202
- }
203
-
204
- #[tokio::test]
205
- async fn test_lookup_amazon_domain() {
206
- let cloudcheck = CloudCheck::new();
207
- let results = cloudcheck.lookup("asdf.amazon.com").await.unwrap();
208
- let names: Vec<String> = results.iter().map(|p| p.name.clone()).collect();
209
- assert!(
210
- names.contains(&"Amazon".to_string()),
211
- "Expected Amazon in results: {:?}",
212
- names
213
- );
214
- }
215
- }
File without changes
File without changes
File without changes