nuvu-scan 2.0.0__py3-none-any.whl → 2.0.1__py3-none-any.whl
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.
- nuvu_scan/cli/commands/scan.py +67 -43
- nuvu_scan/core/providers/aws/aws_scanner.py +9 -45
- nuvu_scan/core/providers/gcp/gcp_scanner.py +3 -0
- {nuvu_scan-2.0.0.dist-info → nuvu_scan-2.0.1.dist-info}/METADATA +7 -30
- {nuvu_scan-2.0.0.dist-info → nuvu_scan-2.0.1.dist-info}/RECORD +7 -7
- {nuvu_scan-2.0.0.dist-info → nuvu_scan-2.0.1.dist-info}/WHEEL +0 -0
- {nuvu_scan-2.0.0.dist-info → nuvu_scan-2.0.1.dist-info}/entry_points.txt +0 -0
nuvu_scan/cli/commands/scan.py
CHANGED
|
@@ -98,24 +98,18 @@ from ..formatters.json import JSONFormatter
|
|
|
98
98
|
@click.option(
|
|
99
99
|
"--push",
|
|
100
100
|
is_flag=True,
|
|
101
|
-
help="Push scan results to Nuvu Cloud (requires
|
|
102
|
-
)
|
|
103
|
-
@click.option(
|
|
104
|
-
"--nuvu-cloud-url",
|
|
105
|
-
envvar="NUVU_CLOUD_URL",
|
|
106
|
-
default="https://nuvu.dev",
|
|
107
|
-
show_default=True,
|
|
108
|
-
help="Nuvu Cloud base URL",
|
|
101
|
+
help="Push scan results to Nuvu Cloud (requires --api-key)",
|
|
109
102
|
)
|
|
110
103
|
@click.option(
|
|
111
104
|
"--api-key",
|
|
112
105
|
envvar="NUVU_API_KEY",
|
|
113
|
-
help="Nuvu Cloud API key (from
|
|
106
|
+
help="Nuvu Cloud API key for pushing results (default: from NUVU_API_KEY env var)",
|
|
114
107
|
)
|
|
115
108
|
@click.option(
|
|
116
|
-
"--
|
|
117
|
-
|
|
118
|
-
|
|
109
|
+
"--api-url",
|
|
110
|
+
envvar="NUVU_API_URL",
|
|
111
|
+
default="https://nuvu.dev",
|
|
112
|
+
help="Nuvu Cloud API URL (default: https://nuvu.dev)",
|
|
119
113
|
)
|
|
120
114
|
def scan_command(
|
|
121
115
|
provider: str,
|
|
@@ -134,8 +128,8 @@ def scan_command(
|
|
|
134
128
|
gcp_credentials: str | None,
|
|
135
129
|
gcp_project: str | None,
|
|
136
130
|
push: bool,
|
|
137
|
-
nuvu_cloud_url: str | None,
|
|
138
131
|
api_key: str | None,
|
|
132
|
+
api_url: str,
|
|
139
133
|
list_collectors: bool,
|
|
140
134
|
):
|
|
141
135
|
"""Scan cloud provider for data assets."""
|
|
@@ -311,41 +305,71 @@ def scan_command(
|
|
|
311
305
|
f.write(content)
|
|
312
306
|
click.echo(f"Report written to {output_file}", err=True)
|
|
313
307
|
|
|
308
|
+
# Push to Nuvu Cloud if requested
|
|
314
309
|
if push:
|
|
315
|
-
if not nuvu_cloud_url:
|
|
316
|
-
click.echo("Error: --nuvu-cloud-url or NUVU_CLOUD_URL is required for --push", err=True)
|
|
317
|
-
sys.exit(1)
|
|
318
310
|
if not api_key:
|
|
319
|
-
click.echo(
|
|
311
|
+
click.echo(
|
|
312
|
+
"Error: --api-key required for pushing results (or set NUVU_API_KEY env var)",
|
|
313
|
+
err=True,
|
|
314
|
+
)
|
|
320
315
|
sys.exit(1)
|
|
321
316
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
317
|
+
click.echo(f"Pushing results to Nuvu Cloud ({api_url})...", err=True)
|
|
318
|
+
try:
|
|
319
|
+
import httpx
|
|
325
320
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
data=json.dumps(payload).encode("utf-8"),
|
|
330
|
-
headers={
|
|
331
|
-
"Content-Type": "application/json",
|
|
332
|
-
"Authorization": f"Bearer {api_key}",
|
|
333
|
-
},
|
|
334
|
-
method="POST",
|
|
335
|
-
)
|
|
321
|
+
# Prepare payload matching ScanImport schema
|
|
322
|
+
# Extract unique regions from assets
|
|
323
|
+
scan_regions = list(set(asset.region for asset in result.assets if asset.region))
|
|
336
324
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
325
|
+
payload = {
|
|
326
|
+
"provider": provider,
|
|
327
|
+
"account_id": result.account_id or "unknown",
|
|
328
|
+
"scan_timestamp": result.scan_timestamp or datetime.utcnow().isoformat(),
|
|
329
|
+
"total_cost_estimate_usd": result.total_cost_estimate_usd,
|
|
330
|
+
"scan_regions": scan_regions if scan_regions else None,
|
|
331
|
+
"scan_all_regions": not bool(region),
|
|
332
|
+
"assets": [
|
|
333
|
+
{
|
|
334
|
+
"provider": asset.provider,
|
|
335
|
+
"asset_type": asset.asset_type,
|
|
336
|
+
"normalized_category": asset.normalized_category.value if asset.normalized_category else "unknown",
|
|
337
|
+
"service": asset.service or asset.asset_type.split("_")[0] if asset.asset_type else "unknown",
|
|
338
|
+
"region": asset.region,
|
|
339
|
+
"arn": asset.arn,
|
|
340
|
+
"name": asset.name,
|
|
341
|
+
"created_at": asset.created_at,
|
|
342
|
+
"last_activity_at": asset.last_activity_at,
|
|
343
|
+
"size_bytes": asset.size_bytes,
|
|
344
|
+
"tags": asset.tags,
|
|
345
|
+
"cost_estimate_usd": asset.cost_estimate_usd,
|
|
346
|
+
"risk_flags": asset.risk_flags,
|
|
347
|
+
"ownership_confidence": asset.ownership_confidence or "unknown",
|
|
348
|
+
"suggested_owner": asset.suggested_owner,
|
|
349
|
+
}
|
|
350
|
+
for asset in result.assets
|
|
351
|
+
],
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
# Push to API using the /api/scans/import endpoint
|
|
355
|
+
with httpx.Client(timeout=60) as client:
|
|
356
|
+
response = client.post(
|
|
357
|
+
f"{api_url.rstrip('/')}/api/scans/import",
|
|
358
|
+
json=payload,
|
|
359
|
+
headers={
|
|
360
|
+
"Authorization": f"Bearer {api_key}",
|
|
361
|
+
"Content-Type": "application/json",
|
|
362
|
+
},
|
|
363
|
+
)
|
|
364
|
+
response.raise_for_status()
|
|
365
|
+
result_data = response.json()
|
|
366
|
+
click.echo(
|
|
367
|
+
f"✓ Pushed {len(result.assets)} assets to Nuvu Cloud (scan_id: {result_data.get('id', 'N/A')})",
|
|
368
|
+
err=True,
|
|
369
|
+
)
|
|
370
|
+
except httpx.HTTPStatusError as e:
|
|
371
|
+
click.echo(f"Error pushing to Nuvu Cloud: {e.response.status_code} - {e.response.text}", err=True)
|
|
348
372
|
sys.exit(1)
|
|
349
|
-
except
|
|
350
|
-
click.echo(f"
|
|
373
|
+
except Exception as e:
|
|
374
|
+
click.echo(f"Error pushing to Nuvu Cloud: {e}", err=True)
|
|
351
375
|
sys.exit(1)
|
|
@@ -33,55 +33,19 @@ class AWSScanner(CloudProviderScan):
|
|
|
33
33
|
def __init__(self, config: ScanConfig):
|
|
34
34
|
super().__init__(config)
|
|
35
35
|
self.session = self._create_session()
|
|
36
|
-
if not self.config.regions:
|
|
37
|
-
self.config.regions = self._resolve_regions()
|
|
38
|
-
self.collectors = self._initialize_collectors()
|
|
39
|
-
self.cost_explorer = CostExplorerCollector(self.session, self.config.regions)
|
|
40
|
-
|
|
41
36
|
# Auto-detect account ID if not provided
|
|
42
37
|
if not self.config.account_id:
|
|
43
38
|
self.config.account_id = self._get_account_id()
|
|
39
|
+
self.collectors = self._initialize_collectors()
|
|
40
|
+
self.cost_explorer = CostExplorerCollector(self.session, self.config.regions)
|
|
44
41
|
|
|
45
|
-
def
|
|
46
|
-
"""
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
# Analyze each asset
|
|
53
|
-
total_estimated_cost = 0.0
|
|
54
|
-
actual_total_cost = None
|
|
55
|
-
actual_service_costs = None
|
|
56
|
-
|
|
57
|
-
for asset in assets:
|
|
58
|
-
if asset.asset_type == "cost_summary":
|
|
59
|
-
actual_total_cost = asset.usage_metrics.get("total_actual_cost_30d")
|
|
60
|
-
actual_service_costs = asset.usage_metrics.get("actual_costs_30d")
|
|
61
|
-
continue
|
|
62
|
-
|
|
63
|
-
asset.usage_metrics = self.get_usage_metrics(asset)
|
|
64
|
-
asset.cost_estimate_usd = self.get_cost_estimate(asset)
|
|
65
|
-
total_estimated_cost += asset.cost_estimate_usd or 0.0
|
|
66
|
-
|
|
67
|
-
# Build summary
|
|
68
|
-
summary = self._build_summary(assets)
|
|
69
|
-
if actual_total_cost is not None:
|
|
70
|
-
summary["total_actual_cost_30d"] = actual_total_cost
|
|
71
|
-
summary["actual_costs_30d"] = actual_service_costs or {}
|
|
72
|
-
summary["estimated_assets_cost_total"] = total_estimated_cost
|
|
73
|
-
|
|
74
|
-
# Use actual 30-day cost if available, otherwise fallback to estimates
|
|
75
|
-
total_cost = actual_total_cost if actual_total_cost is not None else total_estimated_cost
|
|
76
|
-
|
|
77
|
-
return ScanResult(
|
|
78
|
-
provider=self.provider,
|
|
79
|
-
account_id=self.config.account_id or "unknown",
|
|
80
|
-
scan_timestamp=datetime.utcnow().isoformat(),
|
|
81
|
-
assets=assets,
|
|
82
|
-
total_cost_estimate_usd=total_cost,
|
|
83
|
-
summary=summary,
|
|
84
|
-
)
|
|
42
|
+
def _get_account_id(self) -> str:
|
|
43
|
+
"""Get the AWS account ID using STS."""
|
|
44
|
+
try:
|
|
45
|
+
sts_client = self.session.client("sts")
|
|
46
|
+
return sts_client.get_caller_identity()["Account"]
|
|
47
|
+
except Exception:
|
|
48
|
+
return "unknown"
|
|
85
49
|
|
|
86
50
|
def _create_session(self) -> boto3.Session:
|
|
87
51
|
"""
|
|
@@ -27,6 +27,9 @@ class GCPScanner(CloudProviderScan):
|
|
|
27
27
|
super().__init__(config)
|
|
28
28
|
self.credentials = self._create_credentials()
|
|
29
29
|
self.project_id = self._get_project_id()
|
|
30
|
+
# Set account_id to project_id for consistency in ScanResult
|
|
31
|
+
if not self.config.account_id:
|
|
32
|
+
self.config.account_id = self.project_id
|
|
30
33
|
self.collectors = self._initialize_collectors()
|
|
31
34
|
|
|
32
35
|
def _create_credentials(self):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nuvu-scan
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: Multi-Cloud Data Asset Control - Discover, govern, and optimize your cloud data assets across AWS and GCP
|
|
5
5
|
Project-URL: Homepage, https://nuvu.dev
|
|
6
6
|
Project-URL: Documentation, https://github.com/nuvudev/nuvu-scan#readme
|
|
@@ -35,6 +35,7 @@ Requires-Dist: google-cloud-dataproc>=5.4.0
|
|
|
35
35
|
Requires-Dist: google-cloud-monitoring>=2.16.0
|
|
36
36
|
Requires-Dist: google-cloud-pubsub>=2.18.0
|
|
37
37
|
Requires-Dist: google-cloud-storage>=2.10.0
|
|
38
|
+
Requires-Dist: httpx>=0.25.0
|
|
38
39
|
Provides-Extra: dev
|
|
39
40
|
Requires-Dist: bandit>=1.7.0; extra == 'dev'
|
|
40
41
|
Requires-Dist: boto3-stubs[essential]>=1.28.0; extra == 'dev'
|
|
@@ -195,40 +196,16 @@ nuvu scan --provider gcp --gcp-credentials /path/to/service-account-key.json --g
|
|
|
195
196
|
nuvu scan --provider gcp --gcp-project your-project-id --output-format json --output-file gcp-report.json
|
|
196
197
|
```
|
|
197
198
|
|
|
198
|
-
###
|
|
199
|
+
### Push to Remote API (Optional)
|
|
199
200
|
|
|
200
|
-
|
|
201
|
+
You can optionally push scan results to a remote API for centralized tracking:
|
|
201
202
|
|
|
202
203
|
```bash
|
|
203
|
-
#
|
|
204
|
-
nuvu scan --provider aws --
|
|
205
|
-
# Output: athena, glue, iam, mwaa, redshift, s3
|
|
206
|
-
|
|
207
|
-
nuvu scan --provider gcp --list-collectors
|
|
208
|
-
# Output: bigquery, dataproc, gcs, gemini, iam, pubsub
|
|
209
|
-
|
|
210
|
-
# Scan only Redshift
|
|
211
|
-
nuvu scan --provider aws -c redshift --region us-west-2
|
|
212
|
-
|
|
213
|
-
# Scan multiple specific collectors
|
|
214
|
-
nuvu scan --provider aws -c redshift -c glue --region us-west-2
|
|
215
|
-
|
|
216
|
-
# Scan only S3 buckets
|
|
217
|
-
nuvu scan --provider aws -c s3 --output-format html
|
|
218
|
-
|
|
219
|
-
# Full scan (default - all collectors)
|
|
220
|
-
nuvu scan --provider aws # Runs all collectors
|
|
221
|
-
|
|
222
|
-
# GCP: Scan only BigQuery
|
|
223
|
-
nuvu scan --provider gcp -c bigquery --gcp-project your-project
|
|
204
|
+
# Push results to a remote endpoint
|
|
205
|
+
nuvu scan --provider aws --push --api-key your-api-key --api-url https://your-api.example.com
|
|
224
206
|
```
|
|
225
207
|
|
|
226
|
-
|
|
227
|
-
- **Faster scans** - Focus on services you care about
|
|
228
|
-
- **Reduced API calls** - Only query the services you need
|
|
229
|
-
- **Targeted reports** - Generate reports for specific areas
|
|
230
|
-
|
|
231
|
-
---
|
|
208
|
+
This is useful for integrating with your own data governance platforms or CI/CD pipelines.
|
|
232
209
|
|
|
233
210
|
## Features
|
|
234
211
|
|
|
@@ -2,7 +2,7 @@ nuvu_scan/__init__.py,sha256=tsQim3nEvAmZ_2-lY6FPQSC5a4bAcUwltNYAn6bpaY0,68
|
|
|
2
2
|
nuvu_scan/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
nuvu_scan/cli/main.py,sha256=CI8ahTFALjd-HcGPAphXX8ZgJUtz7hG9l45MELOMwn0,396
|
|
4
4
|
nuvu_scan/cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
-
nuvu_scan/cli/commands/scan.py,sha256=
|
|
5
|
+
nuvu_scan/cli/commands/scan.py,sha256=Tk_--eXo7aU8Lk7Sh6wcVJ63t1unpcjKG2nQ4uPhnuo,13116
|
|
6
6
|
nuvu_scan/cli/formatters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
nuvu_scan/cli/formatters/csv.py,sha256=xGC8Kl1SKOcflLsZqBNJEucZUxgacSmlU8vogWQpskg,1719
|
|
8
8
|
nuvu_scan/cli/formatters/html.py,sha256=1uhZL6utjnv5-iyLPo_4QqQFUi0fvhbrb4DHZbnl1nc,17894
|
|
@@ -13,7 +13,7 @@ nuvu_scan/core/analyzers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
|
|
|
13
13
|
nuvu_scan/core/models/__init__.py,sha256=osGjl3G0LOYqaJoG_60_i9ZhAeGJ_0stZuMoxRO77aw,188
|
|
14
14
|
nuvu_scan/core/providers/__init__.py,sha256=aipXSMSaFB7TbLdjma4naxzChDOHFCo_VZbG-lbrpKs,105
|
|
15
15
|
nuvu_scan/core/providers/aws/__init__.py,sha256=R5ZPCc-qybHZh4v5w_D0tZc8cYJNuhlfCPnLg3Zr28A,98
|
|
16
|
-
nuvu_scan/core/providers/aws/aws_scanner.py,sha256=
|
|
16
|
+
nuvu_scan/core/providers/aws/aws_scanner.py,sha256=5PXFJjvxd7WyFK9hzDbhn7oPDFSIX05wBR1C_6XqJwc,13879
|
|
17
17
|
nuvu_scan/core/providers/aws/collectors/__init__.py,sha256=uFPTKjtMFHCQwsZy9WmMSvKZpfuXU8SxCfQRhi6yUt0,373
|
|
18
18
|
nuvu_scan/core/providers/aws/collectors/athena.py,sha256=sHuFmZMZ1TM91DxmxrcCSZ6OiN9l7udlMQPnCg7nI70,5737
|
|
19
19
|
nuvu_scan/core/providers/aws/collectors/cost_explorer.py,sha256=qas79Cm_rCyYDzeL4F0cER90Ohyg9rN33fBHh4VA9og,6730
|
|
@@ -23,7 +23,7 @@ nuvu_scan/core/providers/aws/collectors/mwaa.py,sha256=WSXB2kPIxpy82-6JX6Jkqkzjg
|
|
|
23
23
|
nuvu_scan/core/providers/aws/collectors/redshift.py,sha256=b9dYIZ389xOtzmT-ml1-A_nkhCeHrJmjX0cmTLDLhYI,41612
|
|
24
24
|
nuvu_scan/core/providers/aws/collectors/s3.py,sha256=Ma8LMgEpUucN64uohQ2zlGIkUbxmRNkUbeSfZpPe3Pw,13093
|
|
25
25
|
nuvu_scan/core/providers/gcp/__init__.py,sha256=NZ8bStAq0Qt4IRcxaCBAkbJVUPQLQHRRTl0fUxVuTxg,98
|
|
26
|
-
nuvu_scan/core/providers/gcp/gcp_scanner.py,sha256=
|
|
26
|
+
nuvu_scan/core/providers/gcp/gcp_scanner.py,sha256=WLDGRgxagc-8p3qKez2D-K2D_j1X8TFw2c2cgF0irwg,7425
|
|
27
27
|
nuvu_scan/core/providers/gcp/collectors/__init__.py,sha256=HbtMXas5Vh6kYz6RS1d_q7WQgIvZ_N5-fIR-UKHSi9M,394
|
|
28
28
|
nuvu_scan/core/providers/gcp/collectors/bigquery.py,sha256=K1uNN-I_PvR-jdyonY8hZT9LWyEftMKMxksizmkFfd8,14849
|
|
29
29
|
nuvu_scan/core/providers/gcp/collectors/billing.py,sha256=pA2vbHdzQSQR4_yR6wPF8gWN1CoPjRCQ5LVQG8BKoYs,4735
|
|
@@ -32,7 +32,7 @@ nuvu_scan/core/providers/gcp/collectors/gcs.py,sha256=qf-QMW56dR8C0Qg5snF1w7JNDV
|
|
|
32
32
|
nuvu_scan/core/providers/gcp/collectors/gemini.py,sha256=NsYTELcvdSGwSOFEmE3Zmp1bzto99iUCCr0yY4j54zg,17968
|
|
33
33
|
nuvu_scan/core/providers/gcp/collectors/iam.py,sha256=lusayhIwPLjzzpBEE7UD61-oVmceR1cYHstHWZq4VPA,8332
|
|
34
34
|
nuvu_scan/core/providers/gcp/collectors/pubsub.py,sha256=OMBSeWi2Hjp1jgpEQ-a8ecLxV1MRtVVgy0QlVlWnS2k,4953
|
|
35
|
-
nuvu_scan-2.0.
|
|
36
|
-
nuvu_scan-2.0.
|
|
37
|
-
nuvu_scan-2.0.
|
|
38
|
-
nuvu_scan-2.0.
|
|
35
|
+
nuvu_scan-2.0.1.dist-info/METADATA,sha256=nacNuEZTBL_EJJXq9V_8Tz1sbjI04NAUm08ZjaeFMK0,20908
|
|
36
|
+
nuvu_scan-2.0.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
37
|
+
nuvu_scan-2.0.1.dist-info/entry_points.txt,sha256=dge4RP32Lsft0GA_oBPCwFAKZ_GgzD3eq-vD4LY-jPE,83
|
|
38
|
+
nuvu_scan-2.0.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|