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.
@@ -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 API key)",
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 dashboard account settings)",
103
+ help="Nuvu Cloud API key for pushing results (default: from NUVU_API_KEY env var)",
114
104
  )
115
105
  @click.option(
116
- "--list-collectors",
117
- is_flag=True,
118
- help="List available collectors for the specified provider and exit",
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("Error: --api-key or NUVU_API_KEY is required for --push", err=True)
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
- payload = json.loads(JSONFormatter().format(result))
323
- payload["scan_regions"] = list(region) if region else None
324
- payload["scan_all_regions"] = False if region else True
314
+ click.echo(f"Pushing results to Nuvu Cloud ({api_url})...", err=True)
315
+ try:
316
+ import httpx
325
317
 
326
- import_url = nuvu_cloud_url.rstrip("/") + "/api/scans/import"
327
- request = Request(
328
- import_url,
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
- try:
338
- with urlopen(request) as response:
339
- response_body = response.read().decode("utf-8")
340
- click.echo(f"Scan uploaded to Nuvu Cloud: {response.status}", err=True)
341
- if response_body:
342
- click.echo(response_body, err=True)
343
- except HTTPError as e:
344
- error_body = e.read().decode("utf-8")
345
- click.echo(f"Failed to upload scan: {e.code} {e.reason}", err=True)
346
- if error_body:
347
- click.echo(error_body, err=True)
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 URLError as e:
350
- click.echo(f"Failed to upload scan: {e.reason}", err=True)
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 scan(self):
46
- """Execute a full scan with AWS-specific cost handling."""
47
- from datetime import datetime
48
-
49
- # Discover assets
50
- assets = self.list_assets()
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.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
- ### Selective Scanning (Collectors)
199
+ ### Push to Remote API (Optional)
199
200
 
200
- Run focused scans on specific services instead of a full scan:
201
+ You can optionally push scan results to a remote API for centralized tracking:
201
202
 
202
203
  ```bash
203
- # List available collectors for a provider
204
- nuvu scan --provider aws --list-collectors
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
- **Benefits of selective scanning:**
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=1FTILZXhQ2apz0HuMw9MyT0gZS75EsS0fXJBxyKePws,11622
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=gBqzuKWVh6ZZ8uK5A7pt5QEiO1hPgqukrUoOicFv_i0,15296
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=Vt2kRCMyLRTJsQjIyGHnNnJEGlgAh6bEpctR0L-_y5A,7264
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.0.dist-info/METADATA,sha256=FO_K95EjcPKGYOnJ-canTLMoIDejXhQSRneRaEiC4bw,21526
36
- nuvu_scan-2.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
37
- nuvu_scan-2.0.0.dist-info/entry_points.txt,sha256=dge4RP32Lsft0GA_oBPCwFAKZ_GgzD3eq-vD4LY-jPE,83
38
- nuvu_scan-2.0.0.dist-info/RECORD,,
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,,