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.
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/Cargo.lock +1 -1
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/Cargo.toml +1 -1
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/PKG-INFO +5 -5
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/README.md +4 -4
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck_update/__init__.py +0 -41
- cloudcheck-8.4.1/dist/cloudcheck-8.4.1-cp314-cp314-manylinux_2_38_x86_64.whl +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/pyproject.toml +1 -1
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/pytest.ini +2 -2
- cloudcheck-8.4.1/src/lib.rs +281 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/uv.lock +1 -1
- cloudcheck-8.4.0/dist/cloudcheck-8.4.0-cp314-cp314-manylinux_2_38_x86_64.whl +0 -0
- cloudcheck-8.4.0/src/lib.rs +0 -215
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/.github/dependabot.yml +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/.github/workflows/pipeline-tests.yml +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/.github/workflows/python-tests.yml +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/.github/workflows/rust-tests.yml +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/.gitignore +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloud_providers.json +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloud_providers_v2.json +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/__init__.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/helpers.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/akamai.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/alibaba.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/amazon.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/arvancloud.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/azure.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/backblaze.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/base.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/cisco.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/cloudflare.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/cloudfront.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/dell.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/digitalocean.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/fastly.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/github.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/google.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/heroku.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/hetzner.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/hpe.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/huawei.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/ibm.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/imperva.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/kamatera.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/oracle.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/ovh.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/rackspace.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/salesforce.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/scaleway.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/tencent.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/wasabi.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck/providers/zoho.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck_update/cli.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/cloudcheck_update/test_cloudcheck_update.py +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/src/main.rs +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/src/python.rs +0 -0
- {cloudcheck-8.4.0 → cloudcheck-8.4.1}/test_cloudcheck.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cloudcheck
|
|
3
|
-
Version: 8.4.
|
|
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
|
[](https://pypi.org/project/cloudcheck/)
|
|
12
12
|
[](https://www.rust-lang.org)
|
|
13
13
|
[](https://crates.io/crates/cloudcheck)
|
|
14
|
-
[](https://github.com/blacklanternsecurity/cloudcheck/blob/
|
|
14
|
+
[](https://github.com/blacklanternsecurity/cloudcheck/blob/stable/LICENSE)
|
|
15
15
|
[](https://github.com/astral-sh/ruff)
|
|
16
|
-
[](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/rust-tests.yml)
|
|
17
|
+
[](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/python-tests.yml)
|
|
18
|
+
[](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
|
[](https://pypi.org/project/cloudcheck/)
|
|
5
5
|
[](https://www.rust-lang.org)
|
|
6
6
|
[](https://crates.io/crates/cloudcheck)
|
|
7
|
-
[](https://github.com/blacklanternsecurity/cloudcheck/blob/
|
|
7
|
+
[](https://github.com/blacklanternsecurity/cloudcheck/blob/stable/LICENSE)
|
|
8
8
|
[](https://github.com/astral-sh/ruff)
|
|
9
|
-
[](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/rust-tests.yml)
|
|
10
|
+
[](https://github.com/blacklanternsecurity/cloudcheck/actions/workflows/python-tests.yml)
|
|
11
|
+
[](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"
|
|
@@ -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
|
+
}
|
|
Binary file
|
cloudcheck-8.4.0/src/lib.rs
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|