nuvu-scan 2.0.0__py3-none-any.whl → 2.0.2__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 +74 -46
- nuvu_scan/core/providers/aws/aws_scanner.py +9 -46
- nuvu_scan/core/providers/gcp/gcp_scanner.py +3 -0
- {nuvu_scan-2.0.0.dist-info → nuvu_scan-2.0.2.dist-info}/METADATA +7 -30
- {nuvu_scan-2.0.0.dist-info → nuvu_scan-2.0.2.dist-info}/RECORD +7 -7
- {nuvu_scan-2.0.0.dist-info → nuvu_scan-2.0.2.dist-info}/WHEEL +0 -0
- {nuvu_scan-2.0.0.dist-info → nuvu_scan-2.0.2.dist-info}/entry_points.txt +0 -0
nuvu_scan/cli/commands/scan.py
CHANGED
|
@@ -2,12 +2,9 @@
|
|
|
2
2
|
Scan command for Nuvu CLI.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
import json
|
|
6
5
|
import os
|
|
7
6
|
import sys
|
|
8
7
|
from datetime import datetime
|
|
9
|
-
from urllib.error import HTTPError, URLError
|
|
10
|
-
from urllib.request import Request, urlopen
|
|
11
8
|
|
|
12
9
|
import click
|
|
13
10
|
|
|
@@ -98,24 +95,18 @@ from ..formatters.json import JSONFormatter
|
|
|
98
95
|
@click.option(
|
|
99
96
|
"--push",
|
|
100
97
|
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",
|
|
98
|
+
help="Push scan results to Nuvu Cloud (requires --api-key)",
|
|
109
99
|
)
|
|
110
100
|
@click.option(
|
|
111
101
|
"--api-key",
|
|
112
102
|
envvar="NUVU_API_KEY",
|
|
113
|
-
help="Nuvu Cloud API key (from
|
|
103
|
+
help="Nuvu Cloud API key for pushing results (default: from NUVU_API_KEY env var)",
|
|
114
104
|
)
|
|
115
105
|
@click.option(
|
|
116
|
-
"--
|
|
117
|
-
|
|
118
|
-
|
|
106
|
+
"--api-url",
|
|
107
|
+
envvar="NUVU_API_URL",
|
|
108
|
+
default="https://nuvu.dev",
|
|
109
|
+
help="Nuvu Cloud API URL (default: https://nuvu.dev)",
|
|
119
110
|
)
|
|
120
111
|
def scan_command(
|
|
121
112
|
provider: str,
|
|
@@ -134,8 +125,8 @@ def scan_command(
|
|
|
134
125
|
gcp_credentials: str | None,
|
|
135
126
|
gcp_project: str | None,
|
|
136
127
|
push: bool,
|
|
137
|
-
nuvu_cloud_url: str | None,
|
|
138
128
|
api_key: str | None,
|
|
129
|
+
api_url: str,
|
|
139
130
|
list_collectors: bool,
|
|
140
131
|
):
|
|
141
132
|
"""Scan cloud provider for data assets."""
|
|
@@ -311,41 +302,78 @@ def scan_command(
|
|
|
311
302
|
f.write(content)
|
|
312
303
|
click.echo(f"Report written to {output_file}", err=True)
|
|
313
304
|
|
|
305
|
+
# Push to Nuvu Cloud if requested
|
|
314
306
|
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
307
|
if not api_key:
|
|
319
|
-
click.echo(
|
|
308
|
+
click.echo(
|
|
309
|
+
"Error: --api-key required for pushing results (or set NUVU_API_KEY env var)",
|
|
310
|
+
err=True,
|
|
311
|
+
)
|
|
320
312
|
sys.exit(1)
|
|
321
313
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
314
|
+
click.echo(f"Pushing results to Nuvu Cloud ({api_url})...", err=True)
|
|
315
|
+
try:
|
|
316
|
+
import httpx
|
|
325
317
|
|
|
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
|
-
)
|
|
318
|
+
# Prepare payload matching ScanImport schema
|
|
319
|
+
# Extract unique regions from assets
|
|
320
|
+
scan_regions = list(set(asset.region for asset in result.assets if asset.region))
|
|
336
321
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
322
|
+
payload = {
|
|
323
|
+
"provider": provider,
|
|
324
|
+
"account_id": result.account_id or "unknown",
|
|
325
|
+
"scan_timestamp": result.scan_timestamp or datetime.utcnow().isoformat(),
|
|
326
|
+
"total_cost_estimate_usd": result.total_cost_estimate_usd,
|
|
327
|
+
"scan_regions": scan_regions if scan_regions else None,
|
|
328
|
+
"scan_all_regions": not bool(region),
|
|
329
|
+
"assets": [
|
|
330
|
+
{
|
|
331
|
+
"provider": asset.provider,
|
|
332
|
+
"asset_type": asset.asset_type,
|
|
333
|
+
"normalized_category": asset.normalized_category.value
|
|
334
|
+
if asset.normalized_category
|
|
335
|
+
else "unknown",
|
|
336
|
+
"service": asset.service or asset.asset_type.split("_")[0]
|
|
337
|
+
if asset.asset_type
|
|
338
|
+
else "unknown",
|
|
339
|
+
"region": asset.region,
|
|
340
|
+
"arn": asset.arn,
|
|
341
|
+
"name": asset.name,
|
|
342
|
+
"created_at": asset.created_at,
|
|
343
|
+
"last_activity_at": asset.last_activity_at,
|
|
344
|
+
"size_bytes": asset.size_bytes,
|
|
345
|
+
"tags": asset.tags,
|
|
346
|
+
"cost_estimate_usd": asset.cost_estimate_usd,
|
|
347
|
+
"risk_flags": asset.risk_flags,
|
|
348
|
+
"ownership_confidence": asset.ownership_confidence or "unknown",
|
|
349
|
+
"suggested_owner": asset.suggested_owner,
|
|
350
|
+
}
|
|
351
|
+
for asset in result.assets
|
|
352
|
+
],
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
# Push to API using the /api/scans/import endpoint
|
|
356
|
+
with httpx.Client(timeout=60) as client:
|
|
357
|
+
response = client.post(
|
|
358
|
+
f"{api_url.rstrip('/')}/api/scans/import",
|
|
359
|
+
json=payload,
|
|
360
|
+
headers={
|
|
361
|
+
"Authorization": f"Bearer {api_key}",
|
|
362
|
+
"Content-Type": "application/json",
|
|
363
|
+
},
|
|
364
|
+
)
|
|
365
|
+
response.raise_for_status()
|
|
366
|
+
result_data = response.json()
|
|
367
|
+
click.echo(
|
|
368
|
+
f"✓ Pushed {len(result.assets)} assets to Nuvu Cloud (scan_id: {result_data.get('id', 'N/A')})",
|
|
369
|
+
err=True,
|
|
370
|
+
)
|
|
371
|
+
except httpx.HTTPStatusError as e:
|
|
372
|
+
click.echo(
|
|
373
|
+
f"Error pushing to Nuvu Cloud: {e.response.status_code} - {e.response.text}",
|
|
374
|
+
err=True,
|
|
375
|
+
)
|
|
348
376
|
sys.exit(1)
|
|
349
|
-
except
|
|
350
|
-
click.echo(f"
|
|
377
|
+
except Exception as e:
|
|
378
|
+
click.echo(f"Error pushing to Nuvu Cloud: {e}", err=True)
|
|
351
379
|
sys.exit(1)
|
|
@@ -13,7 +13,6 @@ from nuvu_scan.core.base import (
|
|
|
13
13
|
CloudProviderScan,
|
|
14
14
|
NormalizedCategory,
|
|
15
15
|
ScanConfig,
|
|
16
|
-
ScanResult,
|
|
17
16
|
)
|
|
18
17
|
|
|
19
18
|
from .collectors.athena import AthenaCollector
|
|
@@ -33,55 +32,19 @@ class AWSScanner(CloudProviderScan):
|
|
|
33
32
|
def __init__(self, config: ScanConfig):
|
|
34
33
|
super().__init__(config)
|
|
35
34
|
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
35
|
# Auto-detect account ID if not provided
|
|
42
36
|
if not self.config.account_id:
|
|
43
37
|
self.config.account_id = self._get_account_id()
|
|
38
|
+
self.collectors = self._initialize_collectors()
|
|
39
|
+
self.cost_explorer = CostExplorerCollector(self.session, self.config.regions)
|
|
44
40
|
|
|
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
|
-
)
|
|
41
|
+
def _get_account_id(self) -> str:
|
|
42
|
+
"""Get the AWS account ID using STS."""
|
|
43
|
+
try:
|
|
44
|
+
sts_client = self.session.client("sts")
|
|
45
|
+
return sts_client.get_caller_identity()["Account"]
|
|
46
|
+
except Exception:
|
|
47
|
+
return "unknown"
|
|
85
48
|
|
|
86
49
|
def _create_session(self) -> boto3.Session:
|
|
87
50
|
"""
|
|
@@ -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.2
|
|
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=MUgn-5DudrXx70_9c_Evi88jpn2pTVJABnBtoTmAWF0,13158
|
|
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=4ALaVbGYsH9nyQ_HMFkrjOnclJ6VN6B3xL6kEnHy63s,13863
|
|
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.2.dist-info/METADATA,sha256=1-sx6EH9e1nEGuSzP_9YVY9hrKHlLisWYbaPSZ1Iw64,20908
|
|
36
|
+
nuvu_scan-2.0.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
37
|
+
nuvu_scan-2.0.2.dist-info/entry_points.txt,sha256=dge4RP32Lsft0GA_oBPCwFAKZ_GgzD3eq-vD4LY-jPE,83
|
|
38
|
+
nuvu_scan-2.0.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|