certifyclouds 1.2.0__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.
cc_scanner/security.py ADDED
@@ -0,0 +1,175 @@
1
+ """Security findings rules engine for CertifyClouds Scanner.
2
+
3
+ Analyzes scan results and generates actionable security findings.
4
+ Uses only data already present in scan results — no additional API calls.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from datetime import datetime, timezone
9
+ from typing import Any
10
+
11
+
12
+ SEVERITY_ORDER = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3, "INFO": 4}
13
+
14
+
15
+ def analyze(results: dict[str, Any]) -> list[dict[str, Any]]:
16
+ """Analyze scan results and return a list of security findings.
17
+
18
+ Args:
19
+ results: Scan results dict from scanner.scan()
20
+
21
+ Returns:
22
+ List of finding dicts, sorted by severity (most severe first)
23
+ """
24
+ findings: list[dict[str, Any]] = []
25
+ now = datetime.now(timezone.utc)
26
+
27
+ for sub in results.get("subscriptions", []):
28
+ sub_name = sub.get("name", sub.get("id", "unknown"))
29
+
30
+ for vault in sub.get("vaults", []):
31
+ vault_name = vault.get("name", "unknown")
32
+
33
+ # Vault-level findings
34
+ _check_vault_security(vault, vault_name, sub_name, findings)
35
+
36
+ # Asset-level findings
37
+ for secret in vault.get("secrets", []):
38
+ _check_asset_expiry(secret, "secret", vault_name, sub_name, now, findings)
39
+
40
+ for cert in vault.get("certificates", []):
41
+ _check_asset_expiry(cert, "certificate", vault_name, sub_name, now, findings)
42
+
43
+ # Sort by severity
44
+ findings.sort(key=lambda f: SEVERITY_ORDER.get(f["severity"], 99))
45
+ return findings
46
+
47
+
48
+ def _check_vault_security(
49
+ vault: dict[str, Any],
50
+ vault_name: str,
51
+ sub_name: str,
52
+ findings: list[dict[str, Any]],
53
+ ) -> None:
54
+ """Check vault-level security configuration."""
55
+ if vault.get("public_network_access") == "Enabled":
56
+ findings.append({
57
+ "id": "SEC-003",
58
+ "severity": "HIGH",
59
+ "title": "Public network access enabled",
60
+ "resource": vault_name,
61
+ "subscription": sub_name,
62
+ "recommendation": "Restrict access to specific VNets/IPs",
63
+ })
64
+
65
+ if vault.get("soft_delete_enabled") is False:
66
+ findings.append({
67
+ "id": "SEC-004",
68
+ "severity": "HIGH",
69
+ "title": "Soft delete disabled",
70
+ "resource": vault_name,
71
+ "subscription": sub_name,
72
+ "recommendation": "Enable soft delete for recovery protection",
73
+ })
74
+
75
+ if vault.get("purge_protection_enabled") is False:
76
+ findings.append({
77
+ "id": "SEC-005",
78
+ "severity": "HIGH",
79
+ "title": "Purge protection disabled",
80
+ "resource": vault_name,
81
+ "subscription": sub_name,
82
+ "recommendation": "Enable purge protection to prevent permanent deletion",
83
+ })
84
+
85
+ if vault.get("rbac_enabled") is False:
86
+ findings.append({
87
+ "id": "SEC-008",
88
+ "severity": "MEDIUM",
89
+ "title": "RBAC authorization not enabled",
90
+ "resource": vault_name,
91
+ "subscription": sub_name,
92
+ "recommendation": "Enable RBAC for fine-grained access control",
93
+ })
94
+
95
+ if vault.get("sku") == "standard":
96
+ findings.append({
97
+ "id": "SEC-009",
98
+ "severity": "LOW",
99
+ "title": "Standard SKU (no HSM backing)",
100
+ "resource": vault_name,
101
+ "subscription": sub_name,
102
+ "recommendation": "Consider Premium SKU for HSM-backed keys",
103
+ })
104
+
105
+
106
+ def _check_asset_expiry(
107
+ asset: dict[str, Any],
108
+ asset_type: str,
109
+ vault_name: str,
110
+ sub_name: str,
111
+ now: datetime,
112
+ findings: list[dict[str, Any]],
113
+ ) -> None:
114
+ """Check individual asset expiry status."""
115
+ name = asset.get("name", "unknown")
116
+ expires_on = asset.get("expiresOn")
117
+ enabled = asset.get("enabled", True)
118
+
119
+ if expires_on is None:
120
+ # No expiry set
121
+ findings.append({
122
+ "id": "SEC-006" if asset_type == "secret" else "SEC-007",
123
+ "severity": "MEDIUM",
124
+ "title": f"{asset_type.capitalize()} has no expiry date",
125
+ "resource": f"{vault_name}/{name}",
126
+ "subscription": sub_name,
127
+ "recommendation": "Set an expiry date to enforce rotation",
128
+ })
129
+ return
130
+
131
+ try:
132
+ expiry = datetime.fromisoformat(expires_on.replace("Z", "+00:00"))
133
+ if expiry.tzinfo is None:
134
+ expiry = expiry.replace(tzinfo=timezone.utc)
135
+ except (ValueError, TypeError):
136
+ return
137
+
138
+ if expiry < now and enabled:
139
+ # Expired but still enabled — critical
140
+ findings.append({
141
+ "id": "SEC-001" if asset_type == "secret" else "SEC-002",
142
+ "severity": "CRITICAL",
143
+ "title": f"Expired {asset_type} still enabled",
144
+ "resource": f"{vault_name}/{name}",
145
+ "subscription": sub_name,
146
+ "recommendation": f"Disable or rotate this {asset_type} immediately",
147
+ })
148
+ elif expiry < now and not enabled:
149
+ # Expired and disabled — no finding needed
150
+ pass
151
+ else:
152
+ days_left = (expiry - now).days
153
+ if days_left <= 30:
154
+ findings.append({
155
+ "id": "SEC-010" if asset_type == "secret" else "SEC-011",
156
+ "severity": "INFO",
157
+ "title": f"{asset_type.capitalize()} expiring in {days_left} days",
158
+ "resource": f"{vault_name}/{name}",
159
+ "subscription": sub_name,
160
+ "recommendation": f"Plan {'rotation' if asset_type == 'secret' else 'renewal'} before expiry",
161
+ })
162
+
163
+
164
+ def count_by_severity(findings: list[dict[str, Any]]) -> dict[str, int]:
165
+ """Count findings by severity level."""
166
+ counts: dict[str, int] = {}
167
+ for f in findings:
168
+ sev = f["severity"]
169
+ counts[sev] = counts.get(sev, 0) + 1
170
+ return counts
171
+
172
+
173
+ def has_critical_or_high(findings: list[dict[str, Any]]) -> bool:
174
+ """Check if any findings are CRITICAL or HIGH severity."""
175
+ return any(f["severity"] in ("CRITICAL", "HIGH") for f in findings)
@@ -0,0 +1,98 @@
1
+ """Thumbprint Normalization Utility
2
+
3
+ Handles certificate thumbprint normalization across different sources:
4
+ - Azure Key Vault SDK: Returns `bytes[20]` (raw SHA1 hash)
5
+ - Microsoft Graph API: Returns base64-encoded string
6
+ - Display format: Lowercase hex string (40 characters)
7
+
8
+ All thumbprints are normalized to lowercase hex for consistent matching.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import base64
13
+ import logging
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def normalize_thumbprint(thumbprint: str | bytes | None) -> str:
19
+ """Normalize a thumbprint to lowercase hex string format.
20
+
21
+ Handles multiple input formats:
22
+ 1. bytes (from Azure Key Vault SDK's x509_thumbprint)
23
+ 2. Base64 string (from Microsoft Graph API's customKeyIdentifier)
24
+ 3. Hex string (already formatted, possibly uppercase)
25
+ 4. None or empty (returns empty string)
26
+ """
27
+ if not thumbprint:
28
+ return ""
29
+
30
+ try:
31
+ # Case 1: bytes (from Azure Key Vault SDK)
32
+ if isinstance(thumbprint, bytes):
33
+ result = thumbprint.hex().lower()
34
+ logger.debug(
35
+ "[Thumbprint] Normalized from bytes: len=%d -> hex=%s...",
36
+ len(thumbprint), result[:16]
37
+ )
38
+ return result
39
+
40
+ # Case 2: String input - need to determine format
41
+ if isinstance(thumbprint, str):
42
+ thumbprint_str = thumbprint.strip()
43
+
44
+ if not thumbprint_str:
45
+ return ""
46
+
47
+ # Check if it's already a valid hex string (40 chars for SHA1, or 64 for SHA256)
48
+ if _is_hex_string(thumbprint_str):
49
+ result = thumbprint_str.lower()
50
+ logger.debug(
51
+ "[Thumbprint] Normalized from hex: %s... -> %s...",
52
+ thumbprint_str[:16], result[:16]
53
+ )
54
+ return result
55
+
56
+ # Try to decode as base64 (Graph API format)
57
+ try:
58
+ decoded = base64.b64decode(thumbprint_str)
59
+ result = decoded.hex().lower()
60
+ logger.debug(
61
+ "[Thumbprint] Normalized from base64: %s... -> %s...",
62
+ thumbprint_str[:16], result[:16]
63
+ )
64
+ return result
65
+ except Exception:
66
+ pass
67
+
68
+ # Last resort: just lowercase whatever we have
69
+ logger.warning(
70
+ "[Thumbprint] Unknown format, using as-is (lowercased): %s...",
71
+ thumbprint_str[:16]
72
+ )
73
+ return thumbprint_str.lower()
74
+
75
+ # Unknown type
76
+ logger.warning(
77
+ "[Thumbprint] Unexpected type %s, converting to string",
78
+ type(thumbprint).__name__
79
+ )
80
+ return str(thumbprint).lower()
81
+
82
+ except Exception as e:
83
+ logger.error(
84
+ "[Thumbprint] Failed to normalize thumbprint: %s - %s",
85
+ type(thumbprint).__name__, e
86
+ )
87
+ return ""
88
+
89
+
90
+ def _is_hex_string(s: str) -> bool:
91
+ """Check if a string is a valid hexadecimal string of expected thumbprint length."""
92
+ if len(s) not in (40, 64):
93
+ return False
94
+ try:
95
+ int(s, 16)
96
+ return True
97
+ except ValueError:
98
+ return False
@@ -0,0 +1,253 @@
1
+ Metadata-Version: 2.4
2
+ Name: certifyclouds
3
+ Version: 1.2.0
4
+ Summary: Scan Azure Key Vaults for expiring secrets, certificates, and keys
5
+ Project-URL: Homepage, https://certifyclouds.com/scanner
6
+ Project-URL: Documentation, https://docs.certifyclouds.com/scanner
7
+ Project-URL: Repository, https://codeberg.org/hus/cc
8
+ Author-email: CertifyClouds <hello@certifyclouds.com>
9
+ License: Apache-2.0
10
+ License-File: LICENSE
11
+ Keywords: azure,certificates,expiry,keyvault,rotation,scanner,secrets,security
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: License :: OSI Approved :: Apache Software License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Security
25
+ Classifier: Topic :: System :: Systems Administration
26
+ Requires-Python: >=3.9
27
+ Requires-Dist: azure-core>=1.30
28
+ Requires-Dist: azure-identity>=1.20
29
+ Requires-Dist: azure-keyvault-certificates>=4.9
30
+ Requires-Dist: azure-keyvault-keys>=4.10
31
+ Requires-Dist: azure-keyvault-secrets>=4.9
32
+ Requires-Dist: azure-mgmt-keyvault>=13.0
33
+ Requires-Dist: azure-mgmt-resource>=24.0
34
+ Requires-Dist: click>=8.0
35
+ Requires-Dist: httpx>=0.27
36
+ Requires-Dist: rich>=13.0
37
+ Requires-Dist: tenacity>=9.0
38
+ Provides-Extra: dev
39
+ Requires-Dist: pytest-mock>=3.10; extra == 'dev'
40
+ Requires-Dist: pytest>=7.0; extra == 'dev'
41
+ Description-Content-Type: text/markdown
42
+
43
+ # cc-scanner
44
+
45
+ **Do you know which Azure secrets expire this month?**
46
+
47
+ Scan all your Azure Key Vaults across every subscription in 30 seconds. Free, open source, works in Azure Cloud Shell.
48
+
49
+ ```
50
+ pip install cc-scanner
51
+ cc-scan
52
+ ```
53
+
54
+ ## What you get
55
+
56
+ - Cross-subscription inventory of every secret, certificate, and key
57
+ - Expiry warnings: expired, <7 days, <30 days, <90 days (color-coded)
58
+ - Security findings: public access, soft delete, RBAC, missing expiry dates
59
+ - Export to JSON, CSV, or standalone HTML report
60
+ - Works in Azure Cloud Shell with zero setup
61
+
62
+ ## Quick Start
63
+
64
+ ```bash
65
+ # 1. Install
66
+ pip install cc-scanner
67
+
68
+ # 2. Authenticate (skip in Azure Cloud Shell)
69
+ az login
70
+
71
+ # 3. Scan
72
+ cc-scan
73
+ ```
74
+
75
+ On first run, you'll be prompted to register with your email (free, takes 10 seconds). Your API key is saved locally to `~/.certifyclouds/config.json`.
76
+
77
+ ## Output
78
+
79
+ ```
80
+ CertifyClouds Scanner v1.2.0
81
+ Authenticated as: you@company.com
82
+
83
+ Scanning Azure subscriptions...
84
+
85
+ Production (sub-abc123)
86
+ vault-prod (eastus) - 45 secrets, 12 certificates, 3 keys
87
+ vault-shared (westeurope) - 23 secrets, 5 certificates, 1 key
88
+
89
+ SUMMARY
90
+ Subscriptions: 2 Vaults: 4 Total assets: 112
91
+ Secrets: 88 Certificates: 20 Keys: 4
92
+
93
+ EXPIRY WARNINGS
94
+ EXPIRED 3 secrets, 1 certificate
95
+ < 7 days 2 secrets
96
+ < 30 days 8 secrets, 2 certificates
97
+
98
+ SECURITY FINDINGS
99
+ CRITICAL: 3 HIGH: 2 MEDIUM: 47 LOW: 1
100
+
101
+ CRITICAL Expired secret still enabled vault-prod/db-password
102
+ HIGH Public network access enabled vault-legacy
103
+ MEDIUM Secret has no expiry date vault-prod/api-key
104
+ ```
105
+
106
+ ## Usage
107
+
108
+ ```bash
109
+ # Output formats
110
+ cc-scan --format table # Default: pretty terminal output
111
+ cc-scan --format json # Full JSON (pipe-friendly)
112
+ cc-scan --format csv # Flat CSV (for spreadsheets)
113
+ cc-scan --format html # Standalone HTML report
114
+
115
+ # Write to file
116
+ cc-scan --output report.html --format html
117
+ cc-scan --output scan.json --format json
118
+
119
+ # Filters
120
+ cc-scan --expiring 30 # Only items expiring within 30 days
121
+ cc-scan --expired # Only already-expired items
122
+ cc-scan --type secrets # Only secrets (or: certificates, keys)
123
+ cc-scan --vault vault-prod # Specific vault(s) (repeatable)
124
+ cc-scan --subscription sub-123 # Specific subscription(s) (repeatable)
125
+
126
+ # Security summary only
127
+ cc-scan --security # Show findings without per-item details
128
+
129
+ # Azure auth
130
+ cc-scan --auth cli # Azure CLI credential (default)
131
+ cc-scan --auth default # DefaultAzureCredential (managed identity/SP)
132
+
133
+ # Tuning
134
+ cc-scan --workers 10 # Parallel workers (default: 5, max: 20)
135
+ cc-scan --timeout 600 # Scan timeout in seconds (default: 300)
136
+
137
+ # Other
138
+ cc-scan --offline # Skip license server validation
139
+ cc-scan --register # Re-register / change email
140
+ cc-scan --key cc-scan-xxxxx # Use a specific API key
141
+ cc-scan --version # Show version
142
+ cc-scan --help # Show all options
143
+ ```
144
+
145
+ ## Environment Variables
146
+
147
+ All flags can be set via environment variables (flags take priority):
148
+
149
+ ```bash
150
+ CC_SCAN_KEY=cc-scan-xxxxx # API key
151
+ CC_SCAN_AUTH=cli # Auth method
152
+ CC_SCAN_FORMAT=json # Output format
153
+ CC_SCAN_WORKERS=10 # Workers
154
+ CC_SCAN_TIMEOUT=600 # Timeout
155
+ CC_SCAN_OFFLINE=true # Offline mode
156
+ ```
157
+
158
+ For service principal auth (used with `--auth default`):
159
+
160
+ ```bash
161
+ AZURE_CLIENT_ID=...
162
+ AZURE_CLIENT_SECRET=...
163
+ AZURE_TENANT_ID=...
164
+ ```
165
+
166
+ ## Exit Codes
167
+
168
+ | Code | Meaning |
169
+ |------|---------|
170
+ | 0 | Scan completed, no critical/high findings |
171
+ | 1 | Scan completed, critical or high findings detected |
172
+ | 2 | Scan failed (auth error, network error, timeout) |
173
+ | 3 | Registration required / invalid API key |
174
+
175
+ Non-zero exit codes make this CI/cron-friendly: `cc-scan || notify-team`.
176
+
177
+ ## Azure Permissions
178
+
179
+ Your identity needs these roles on each Key Vault:
180
+
181
+ | Role | Why |
182
+ |------|-----|
183
+ | Key Vault Secrets User | List and read secret properties |
184
+ | Key Vault Certificate User | List and read certificate properties |
185
+ | Key Vault Crypto User | List and read key properties |
186
+ | Reader | List Key Vaults across subscriptions |
187
+
188
+ Assign roles in Azure Portal: Key Vault > Access control (IAM) > Add role assignment.
189
+
190
+ If a vault has firewall restrictions, you'll see an inline error (the scan continues to the next vault).
191
+
192
+ ## Security Findings
193
+
194
+ The scanner checks for 11 security rules:
195
+
196
+ | ID | Severity | What it checks |
197
+ |----|----------|---------------|
198
+ | SEC-001 | CRITICAL | Expired secret still enabled |
199
+ | SEC-002 | CRITICAL | Expired certificate still enabled |
200
+ | SEC-003 | HIGH | Public network access enabled on vault |
201
+ | SEC-004 | HIGH | Soft delete disabled |
202
+ | SEC-005 | HIGH | Purge protection disabled |
203
+ | SEC-006 | MEDIUM | Secret has no expiry date set |
204
+ | SEC-007 | MEDIUM | Certificate has no expiry date set |
205
+ | SEC-008 | MEDIUM | RBAC authorization not enabled |
206
+ | SEC-009 | LOW | Standard SKU (no HSM backing) |
207
+ | SEC-010 | INFO | Secret expiring within 30 days |
208
+ | SEC-011 | INFO | Certificate expiring within 30 days |
209
+
210
+ ## Privacy & Telemetry
211
+
212
+ CertifyClouds Scanner sends **aggregate usage statistics** to improve the product. Here's exactly what is sent:
213
+
214
+ **Sent:** API key, CLI version, counts (subscriptions, vaults, secrets, certificates, keys, expired, expiring soon, security findings, vaults with errors), scan duration.
215
+
216
+ **Never sent:** Vault names or URIs, secret/certificate/key names, secret values, subscription IDs or names, Azure tenant IDs, resource group names, tags, certificate subjects or thumbprints.
217
+
218
+ Use `--offline` to disable all network calls to the license server. The scan works fully offline.
219
+
220
+ Source code is open and auditable: [codeberg.org/hus/cc/scanner](https://codeberg.org/hus/cc)
221
+
222
+ ## Troubleshooting
223
+
224
+ **"No Azure credentials found"**
225
+ Run `az login` to authenticate, or set `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_TENANT_ID` for service principal auth.
226
+
227
+ **Vault shows "Firewall restriction"**
228
+ Run cc-scan from an allowed network or add your IP in Azure Portal (Key Vault > Networking).
229
+
230
+ **Vault shows "Access denied"**
231
+ Your identity needs the roles listed in [Azure Permissions](#azure-permissions).
232
+
233
+ **"Could not reach license.certifyclouds.com"**
234
+ Check internet connection. Behind a proxy? Set `HTTPS_PROXY`. Or use `--offline` mode.
235
+
236
+ **Slow scans**
237
+ Increase workers: `cc-scan --workers 10`. Each subscription is scanned in parallel.
238
+
239
+ ## Requirements
240
+
241
+ - Python 3.9+
242
+ - Azure CLI (`az login`) or service principal credentials
243
+ - Network access to Azure management and Key Vault data plane APIs
244
+
245
+ ## License
246
+
247
+ Apache 2.0. See [LICENSE](LICENSE).
248
+
249
+ ## Links
250
+
251
+ - [CertifyClouds](https://certifyclouds.com) - Automate secret rotation & compliance
252
+ - [Documentation](https://docs.certifyclouds.com/scanner)
253
+ - [Source Code](https://codeberg.org/hus/cc)
@@ -0,0 +1,19 @@
1
+ cc_scanner/__init__.py,sha256=UvDKQpOLThCNfGAnT7jqr3SXEuVUZMGtUZfRbALIoaA,123
2
+ cc_scanner/auth.py,sha256=LEUPT1OM4Ut3-igSzvRP5LQu-fjnp8l7kv5VLJ_4FWo,3035
3
+ cc_scanner/cli.py,sha256=RubxCSnhQYj0jJNlHMmummx1jhv8lWn8wtg6G0YC1AI,12309
4
+ cc_scanner/config.py,sha256=2mWfMu_jWfF66SOcySYze4xGKuAO1hppcpZR5eOY1FY,2766
5
+ cc_scanner/filters.py,sha256=yauU88xWDkab_hhr2mni9f7xBWW3Tiyd2I4nNj23WE8,4905
6
+ cc_scanner/registration.py,sha256=2bbE7UQ_8zeBFqjXu9oosoR65FA3UMJUOfqSe4aKdWc,2928
7
+ cc_scanner/scanner.py,sha256=di82PqwGn47gxzQTT62p5IBjgMrTAXfPUCcPfR--6YE,30382
8
+ cc_scanner/security.py,sha256=X8OqghtRC6IQI7R2IQxK0JVXXazNbV5knwC4fL7K2TA,6025
9
+ cc_scanner/thumbprint.py,sha256=hwcg5qqy0p-f4W4uS8eIbVRKBppeF_PcxiLu_0ov7w0,3197
10
+ cc_scanner/formatters/__init__.py,sha256=ZpxcJqEBRtXJpfaM2k02i_I-m1kGOyFruqiZb2r04Ac,555
11
+ cc_scanner/formatters/csv_fmt.py,sha256=vtC_fhIgS_YEI6ZAIdwCTjml50KcYkN-tB1SYhpQi2M,2588
12
+ cc_scanner/formatters/html.py,sha256=IbWyvhP-zfDRAHVcv-j2rlB0uKg_ZTXDk3X8udpM5Vs,8840
13
+ cc_scanner/formatters/json_fmt.py,sha256=LVJK8pNyZiumF7zLKCiWr8Vw8QosgaIkwCrFtSn4-hU,793
14
+ cc_scanner/formatters/table.py,sha256=dcdR3X_7qx_oQ6n5CB3cbmy9ePCmQJEXenKFkiiZVno,6823
15
+ certifyclouds-1.2.0.dist-info/METADATA,sha256=7ECsp68t8s3lTqJLUm0keYZCYUCEHYFSWmNFrbNSKhQ,8673
16
+ certifyclouds-1.2.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
17
+ certifyclouds-1.2.0.dist-info/entry_points.txt,sha256=vxwSbE52iqCuQEHIx17VwPPdMIE6am0e46kmYAbCZ7g,48
18
+ certifyclouds-1.2.0.dist-info/licenses/LICENSE,sha256=LstlN_mRiSNm-hVYNdlu6sEDIRozNIXd-0wnYXoqbSA,10764
19
+ certifyclouds-1.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cc-scan = cc_scanner.cli:main