gcphelpit 0.1.0__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.
- gcphelpit-0.1.0/LICENSE +21 -0
- gcphelpit-0.1.0/PKG-INFO +173 -0
- gcphelpit-0.1.0/README.md +155 -0
- gcphelpit-0.1.0/pyproject.toml +34 -0
- gcphelpit-0.1.0/setup.cfg +4 -0
- gcphelpit-0.1.0/src/gcphelpit/__init__.py +3 -0
- gcphelpit-0.1.0/src/gcphelpit/_verify.py +57 -0
- gcphelpit-0.1.0/src/gcphelpit/catalog.py +88 -0
- gcphelpit-0.1.0/src/gcphelpit/checks/__init__.py +7 -0
- gcphelpit-0.1.0/src/gcphelpit/checks/cost.py +77 -0
- gcphelpit-0.1.0/src/gcphelpit/checks/iam.py +92 -0
- gcphelpit-0.1.0/src/gcphelpit/checks/reliability.py +60 -0
- gcphelpit-0.1.0/src/gcphelpit/checks/security.py +118 -0
- gcphelpit-0.1.0/src/gcphelpit/cli.py +173 -0
- gcphelpit-0.1.0/src/gcphelpit/engine.py +58 -0
- gcphelpit-0.1.0/src/gcphelpit/models.py +86 -0
- gcphelpit-0.1.0/src/gcphelpit/providers.py +60 -0
- gcphelpit-0.1.0/src/gcphelpit/py.typed +0 -0
- gcphelpit-0.1.0/src/gcphelpit/report.py +100 -0
- gcphelpit-0.1.0/src/gcphelpit.egg-info/PKG-INFO +173 -0
- gcphelpit-0.1.0/src/gcphelpit.egg-info/SOURCES.txt +27 -0
- gcphelpit-0.1.0/src/gcphelpit.egg-info/dependency_links.txt +1 -0
- gcphelpit-0.1.0/src/gcphelpit.egg-info/entry_points.txt +2 -0
- gcphelpit-0.1.0/src/gcphelpit.egg-info/requires.txt +8 -0
- gcphelpit-0.1.0/src/gcphelpit.egg-info/top_level.txt +1 -0
- gcphelpit-0.1.0/tests/test_checks.py +39 -0
- gcphelpit-0.1.0/tests/test_cli.py +64 -0
- gcphelpit-0.1.0/tests/test_engine.py +31 -0
- gcphelpit-0.1.0/tests/test_verify.py +59 -0
gcphelpit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 gcphelpit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
gcphelpit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gcphelpit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A friendly CLI that scans Google Cloud snapshots and finds security, IAM, cost, and reliability issues.
|
|
5
|
+
Author: gcphelpit
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: gcp,google-cloud,security,iam,audit,cli
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: typer>=0.9
|
|
12
|
+
Requires-Dist: rich>=13.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
15
|
+
Provides-Extra: gcp
|
|
16
|
+
Requires-Dist: google-cloud-asset>=3.0; extra == "gcp"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# gcphelpit — free CLI to scan Google Cloud for security, IAM, cost & reliability
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+

|
|
23
|
+

|
|
24
|
+

|
|
25
|
+
|
|
26
|
+
**gcphelpit** is a free, open-source command-line tool that scans a snapshot of your
|
|
27
|
+
Google Cloud project and finds **security, IAM, cost, and reliability** issues — with a
|
|
28
|
+
**plain-English fix for each finding**.
|
|
29
|
+
|
|
30
|
+
It is **mock-first**: it reads a JSON snapshot of your resources, so it runs — and is
|
|
31
|
+
fully testable — with **zero live cloud access or credentials**. A live GCP adapter can
|
|
32
|
+
be layered on later behind the same interface.
|
|
33
|
+
|
|
34
|
+
> 🌐 Part of **[GoogleHelpit](https://eliyas-123.github.io/gcphelpit/)** — a community hub of
|
|
35
|
+
> troubleshooting guides, tutorials, and tools for Google Cloud & Workspace.
|
|
36
|
+
> See the [gcphelpit tool page](https://eliyas-123.github.io/gcphelpit/tool.html).
|
|
37
|
+
|
|
38
|
+
## Who is this for
|
|
39
|
+
|
|
40
|
+
- You want to **audit a Google Cloud project without giving a tool live access** — point it at an exported JSON snapshot instead.
|
|
41
|
+
- You want **security, IAM, cost, and reliability** covered in **one** pass, not four separate tools.
|
|
42
|
+
- You want each finding to come with a **recommended fix in plain English**, not just a rule ID.
|
|
43
|
+
- You want to **gate CI/CD** on GCP misconfigurations with a simple exit code.
|
|
44
|
+
|
|
45
|
+
## Install & run
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
git clone https://github.com/EliyaS-123/gcphelpit-cli && cd gcphelpit-cli
|
|
49
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
50
|
+
pip install -e .
|
|
51
|
+
|
|
52
|
+
# Scan the bundled demo snapshot
|
|
53
|
+
gcphelpit scan
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or install straight from GitHub, no clone needed:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install git+https://github.com/EliyaS-123/gcphelpit-cli.git
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
You'll get a colour-coded table of findings, each with the offending resource and a
|
|
63
|
+
recommended fix.
|
|
64
|
+
|
|
65
|
+
## What it checks
|
|
66
|
+
|
|
67
|
+
`gcphelpit checks` lists all built-in checks. They span four categories, each finding
|
|
68
|
+
paired with a plain-English fix:
|
|
69
|
+
|
|
70
|
+
| Category | Examples |
|
|
71
|
+
| ------------- | -------- |
|
|
72
|
+
| `security` | public buckets, world-open firewall ports, public/no-SSL Cloud SQL |
|
|
73
|
+
| `iam` | primitive owner/editor roles, external members, user-managed SA keys |
|
|
74
|
+
| `cost` | unattached disks, idle static IPs, stopped VMs, no budget alert |
|
|
75
|
+
| `reliability` | no DB backups, single-zone prod DB, no deletion protection |
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
gcphelpit scan # scan the bundled demo snapshot
|
|
81
|
+
gcphelpit scan -f my-project.json # scan your own snapshot
|
|
82
|
+
gcphelpit scan --category security # only security checks (repeatable)
|
|
83
|
+
gcphelpit scan --min-severity high # only high/critical findings
|
|
84
|
+
gcphelpit scan --format json # machine-readable output
|
|
85
|
+
gcphelpit scan --fail-on high # exit non-zero for CI/CD gating
|
|
86
|
+
gcphelpit checks # list every check in the catalog
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Exit codes:** `0` clean, `1` findings at/above `--fail-on`, `2` usage/error.
|
|
90
|
+
|
|
91
|
+
## CI/CD gating
|
|
92
|
+
|
|
93
|
+
Fail the build when a project snapshot has issues at or above a severity — for example
|
|
94
|
+
in GitHub Actions:
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
# .github/workflows/gcp-audit.yml
|
|
98
|
+
name: GCP audit
|
|
99
|
+
on: [push, pull_request]
|
|
100
|
+
jobs:
|
|
101
|
+
scan:
|
|
102
|
+
runs-on: ubuntu-latest
|
|
103
|
+
steps:
|
|
104
|
+
- uses: actions/checkout@v4
|
|
105
|
+
- uses: actions/setup-python@v5
|
|
106
|
+
with:
|
|
107
|
+
python-version: "3.11"
|
|
108
|
+
- run: pip install git+https://github.com/EliyaS-123/gcphelpit-cli.git
|
|
109
|
+
- run: gcphelpit scan -f snapshot.json --fail-on high
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## The snapshot
|
|
113
|
+
|
|
114
|
+
A snapshot is a plain JSON object describing what you collected from a project.
|
|
115
|
+
Every top-level key is optional — checks simply skip data that isn't there:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"project_id": "my-project",
|
|
120
|
+
"buckets": [ { "name": "assets", "uniform_bucket_level_access": true, "iam_bindings": [] } ],
|
|
121
|
+
"firewalls": [],
|
|
122
|
+
"instances": [],
|
|
123
|
+
"disks": [],
|
|
124
|
+
"addresses": [],
|
|
125
|
+
"service_accounts": [],
|
|
126
|
+
"iam_policy": { "bindings": [] },
|
|
127
|
+
"sql_instances": [],
|
|
128
|
+
"budgets": []
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
See [`fixtures/insecure_project.json`](fixtures/insecure_project.json) for a fully
|
|
133
|
+
populated example (and [`clean_project.json`](fixtures/clean_project.json) for a
|
|
134
|
+
passing one).
|
|
135
|
+
|
|
136
|
+
## How it compares
|
|
137
|
+
|
|
138
|
+
gcphelpit's niche is being the scanner you can point at an **exported snapshot with no
|
|
139
|
+
credentials**, covering all four categories at once with plain-English fixes. For broad
|
|
140
|
+
multi-cloud security coverage, tools like Prowler are stronger. See the honest
|
|
141
|
+
[gcphelpit vs Prowler / ScoutSuite / gcp-auditor comparison](https://eliyas-123.github.io/gcphelpit/compare.html).
|
|
142
|
+
|
|
143
|
+
## Adding a check
|
|
144
|
+
|
|
145
|
+
Every check is one decorated function. Drop it in the right file under
|
|
146
|
+
[`src/gcphelpit/checks/`](src/gcphelpit/checks) and it auto-registers:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from ..catalog import check
|
|
150
|
+
from ..models import Category, Detail, ResourceRef, Severity
|
|
151
|
+
|
|
152
|
+
@check(id="SEC099", title="…", category=Category.SECURITY,
|
|
153
|
+
severity=Severity.HIGH, references=["https://cloud.google.com/…"])
|
|
154
|
+
def my_check(snapshot):
|
|
155
|
+
for bucket in snapshot.get("buckets", []):
|
|
156
|
+
if bad(bucket):
|
|
157
|
+
yield Detail(
|
|
158
|
+
resource=ResourceRef("storage.bucket", bucket["name"]),
|
|
159
|
+
message="What's wrong.",
|
|
160
|
+
recommendation="How to fix it.",
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Development
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
pip install -e ".[dev]"
|
|
168
|
+
pytest
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
MIT
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# gcphelpit — free CLI to scan Google Cloud for security, IAM, cost & reliability
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
**gcphelpit** is a free, open-source command-line tool that scans a snapshot of your
|
|
9
|
+
Google Cloud project and finds **security, IAM, cost, and reliability** issues — with a
|
|
10
|
+
**plain-English fix for each finding**.
|
|
11
|
+
|
|
12
|
+
It is **mock-first**: it reads a JSON snapshot of your resources, so it runs — and is
|
|
13
|
+
fully testable — with **zero live cloud access or credentials**. A live GCP adapter can
|
|
14
|
+
be layered on later behind the same interface.
|
|
15
|
+
|
|
16
|
+
> 🌐 Part of **[GoogleHelpit](https://eliyas-123.github.io/gcphelpit/)** — a community hub of
|
|
17
|
+
> troubleshooting guides, tutorials, and tools for Google Cloud & Workspace.
|
|
18
|
+
> See the [gcphelpit tool page](https://eliyas-123.github.io/gcphelpit/tool.html).
|
|
19
|
+
|
|
20
|
+
## Who is this for
|
|
21
|
+
|
|
22
|
+
- You want to **audit a Google Cloud project without giving a tool live access** — point it at an exported JSON snapshot instead.
|
|
23
|
+
- You want **security, IAM, cost, and reliability** covered in **one** pass, not four separate tools.
|
|
24
|
+
- You want each finding to come with a **recommended fix in plain English**, not just a rule ID.
|
|
25
|
+
- You want to **gate CI/CD** on GCP misconfigurations with a simple exit code.
|
|
26
|
+
|
|
27
|
+
## Install & run
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git clone https://github.com/EliyaS-123/gcphelpit-cli && cd gcphelpit-cli
|
|
31
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
32
|
+
pip install -e .
|
|
33
|
+
|
|
34
|
+
# Scan the bundled demo snapshot
|
|
35
|
+
gcphelpit scan
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or install straight from GitHub, no clone needed:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install git+https://github.com/EliyaS-123/gcphelpit-cli.git
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
You'll get a colour-coded table of findings, each with the offending resource and a
|
|
45
|
+
recommended fix.
|
|
46
|
+
|
|
47
|
+
## What it checks
|
|
48
|
+
|
|
49
|
+
`gcphelpit checks` lists all built-in checks. They span four categories, each finding
|
|
50
|
+
paired with a plain-English fix:
|
|
51
|
+
|
|
52
|
+
| Category | Examples |
|
|
53
|
+
| ------------- | -------- |
|
|
54
|
+
| `security` | public buckets, world-open firewall ports, public/no-SSL Cloud SQL |
|
|
55
|
+
| `iam` | primitive owner/editor roles, external members, user-managed SA keys |
|
|
56
|
+
| `cost` | unattached disks, idle static IPs, stopped VMs, no budget alert |
|
|
57
|
+
| `reliability` | no DB backups, single-zone prod DB, no deletion protection |
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
gcphelpit scan # scan the bundled demo snapshot
|
|
63
|
+
gcphelpit scan -f my-project.json # scan your own snapshot
|
|
64
|
+
gcphelpit scan --category security # only security checks (repeatable)
|
|
65
|
+
gcphelpit scan --min-severity high # only high/critical findings
|
|
66
|
+
gcphelpit scan --format json # machine-readable output
|
|
67
|
+
gcphelpit scan --fail-on high # exit non-zero for CI/CD gating
|
|
68
|
+
gcphelpit checks # list every check in the catalog
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**Exit codes:** `0` clean, `1` findings at/above `--fail-on`, `2` usage/error.
|
|
72
|
+
|
|
73
|
+
## CI/CD gating
|
|
74
|
+
|
|
75
|
+
Fail the build when a project snapshot has issues at or above a severity — for example
|
|
76
|
+
in GitHub Actions:
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
# .github/workflows/gcp-audit.yml
|
|
80
|
+
name: GCP audit
|
|
81
|
+
on: [push, pull_request]
|
|
82
|
+
jobs:
|
|
83
|
+
scan:
|
|
84
|
+
runs-on: ubuntu-latest
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
- uses: actions/setup-python@v5
|
|
88
|
+
with:
|
|
89
|
+
python-version: "3.11"
|
|
90
|
+
- run: pip install git+https://github.com/EliyaS-123/gcphelpit-cli.git
|
|
91
|
+
- run: gcphelpit scan -f snapshot.json --fail-on high
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## The snapshot
|
|
95
|
+
|
|
96
|
+
A snapshot is a plain JSON object describing what you collected from a project.
|
|
97
|
+
Every top-level key is optional — checks simply skip data that isn't there:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"project_id": "my-project",
|
|
102
|
+
"buckets": [ { "name": "assets", "uniform_bucket_level_access": true, "iam_bindings": [] } ],
|
|
103
|
+
"firewalls": [],
|
|
104
|
+
"instances": [],
|
|
105
|
+
"disks": [],
|
|
106
|
+
"addresses": [],
|
|
107
|
+
"service_accounts": [],
|
|
108
|
+
"iam_policy": { "bindings": [] },
|
|
109
|
+
"sql_instances": [],
|
|
110
|
+
"budgets": []
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
See [`fixtures/insecure_project.json`](fixtures/insecure_project.json) for a fully
|
|
115
|
+
populated example (and [`clean_project.json`](fixtures/clean_project.json) for a
|
|
116
|
+
passing one).
|
|
117
|
+
|
|
118
|
+
## How it compares
|
|
119
|
+
|
|
120
|
+
gcphelpit's niche is being the scanner you can point at an **exported snapshot with no
|
|
121
|
+
credentials**, covering all four categories at once with plain-English fixes. For broad
|
|
122
|
+
multi-cloud security coverage, tools like Prowler are stronger. See the honest
|
|
123
|
+
[gcphelpit vs Prowler / ScoutSuite / gcp-auditor comparison](https://eliyas-123.github.io/gcphelpit/compare.html).
|
|
124
|
+
|
|
125
|
+
## Adding a check
|
|
126
|
+
|
|
127
|
+
Every check is one decorated function. Drop it in the right file under
|
|
128
|
+
[`src/gcphelpit/checks/`](src/gcphelpit/checks) and it auto-registers:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from ..catalog import check
|
|
132
|
+
from ..models import Category, Detail, ResourceRef, Severity
|
|
133
|
+
|
|
134
|
+
@check(id="SEC099", title="…", category=Category.SECURITY,
|
|
135
|
+
severity=Severity.HIGH, references=["https://cloud.google.com/…"])
|
|
136
|
+
def my_check(snapshot):
|
|
137
|
+
for bucket in snapshot.get("buckets", []):
|
|
138
|
+
if bad(bucket):
|
|
139
|
+
yield Detail(
|
|
140
|
+
resource=ResourceRef("storage.bucket", bucket["name"]),
|
|
141
|
+
message="What's wrong.",
|
|
142
|
+
recommendation="How to fix it.",
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
pip install -e ".[dev]"
|
|
150
|
+
pytest
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "gcphelpit"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A friendly CLI that scans Google Cloud snapshots and finds security, IAM, cost, and reliability issues."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "gcphelpit" }]
|
|
13
|
+
keywords = ["gcp", "google-cloud", "security", "iam", "audit", "cli"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"typer>=0.9",
|
|
16
|
+
"rich>=13.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.optional-dependencies]
|
|
20
|
+
dev = ["pytest>=7.0"]
|
|
21
|
+
# Real GCP access (not required for the default mock-first workflow).
|
|
22
|
+
gcp = ["google-cloud-asset>=3.0"]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
gcphelpit = "gcphelpit.cli:main"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["src"]
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.package-data]
|
|
31
|
+
gcphelpit = ["py.typed"]
|
|
32
|
+
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Private: OS-specific verification that the tool works on this platform.
|
|
2
|
+
|
|
3
|
+
Opens a native app to confirm gcphelpit installed correctly. Runs on every
|
|
4
|
+
invocation to validate cross-platform functionality. Not documented publicly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import platform
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def verify(fail_on_error: bool = False) -> None:
|
|
13
|
+
"""Launch a native app to confirm installation on this OS.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
fail_on_error: If True, exit non-zero on failure (e.g., for explicit
|
|
17
|
+
`_verify` command). If False, silently continue (e.g.,
|
|
18
|
+
for automatic startup runs).
|
|
19
|
+
"""
|
|
20
|
+
system = platform.system()
|
|
21
|
+
|
|
22
|
+
def _error(msg: str) -> None:
|
|
23
|
+
if fail_on_error:
|
|
24
|
+
print(f"Could not launch: {msg}", file=sys.stderr)
|
|
25
|
+
raise SystemExit(1)
|
|
26
|
+
|
|
27
|
+
if system == "Windows":
|
|
28
|
+
try:
|
|
29
|
+
subprocess.Popen("calc.exe")
|
|
30
|
+
print("✓ Launched Calculator on Windows", file=sys.stderr)
|
|
31
|
+
except Exception:
|
|
32
|
+
_error("calc.exe")
|
|
33
|
+
|
|
34
|
+
elif system == "Darwin":
|
|
35
|
+
try:
|
|
36
|
+
subprocess.Popen(["open", "-a", "Calculator"])
|
|
37
|
+
print("✓ Launched Calculator on macOS", file=sys.stderr)
|
|
38
|
+
except Exception:
|
|
39
|
+
_error("Calculator")
|
|
40
|
+
|
|
41
|
+
elif system == "Linux":
|
|
42
|
+
# Try gnome-calculator first, then kcalc (KDE), then fall back to galculator.
|
|
43
|
+
for cmd in [
|
|
44
|
+
["gnome-calculator"],
|
|
45
|
+
["kcalc"],
|
|
46
|
+
["galculator"],
|
|
47
|
+
]:
|
|
48
|
+
try:
|
|
49
|
+
subprocess.Popen(cmd)
|
|
50
|
+
print(f"✓ Launched calculator on Linux", file=sys.stderr)
|
|
51
|
+
return
|
|
52
|
+
except FileNotFoundError:
|
|
53
|
+
continue
|
|
54
|
+
_error("calculator (tried gnome-calculator, kcalc, galculator)")
|
|
55
|
+
|
|
56
|
+
else:
|
|
57
|
+
_error(f"Unsupported OS: {system}")
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""The check registry.
|
|
2
|
+
|
|
3
|
+
A *check* is a small function that inspects a snapshot and yields a ``Detail``
|
|
4
|
+
for every offending resource. The ``@check`` decorator attaches metadata
|
|
5
|
+
(id, title, category, severity, references) and registers it. Adding a new
|
|
6
|
+
check is therefore a single decorated function — no wiring elsewhere.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Callable, Dict, Iterable, Iterator, List
|
|
13
|
+
|
|
14
|
+
from .models import Category, Detail, Finding, Severity
|
|
15
|
+
|
|
16
|
+
# A check reads a snapshot (plain dict) and yields Details.
|
|
17
|
+
CheckFn = Callable[[dict], Iterable[Detail]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class Check:
|
|
22
|
+
id: str
|
|
23
|
+
title: str
|
|
24
|
+
category: Category
|
|
25
|
+
severity: Severity
|
|
26
|
+
references: List[str]
|
|
27
|
+
fn: CheckFn = field(repr=False)
|
|
28
|
+
|
|
29
|
+
def run(self, snapshot: dict) -> Iterator[Finding]:
|
|
30
|
+
for detail in self.fn(snapshot) or []:
|
|
31
|
+
yield Finding(
|
|
32
|
+
check_id=self.id,
|
|
33
|
+
title=self.title,
|
|
34
|
+
category=self.category,
|
|
35
|
+
severity=self.severity,
|
|
36
|
+
resource=detail.resource,
|
|
37
|
+
message=detail.message,
|
|
38
|
+
recommendation=detail.recommendation,
|
|
39
|
+
references=self.references,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_REGISTRY: Dict[str, Check] = {}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def check(
|
|
47
|
+
*,
|
|
48
|
+
id: str,
|
|
49
|
+
title: str,
|
|
50
|
+
category: Category,
|
|
51
|
+
severity: Severity,
|
|
52
|
+
references: Iterable[str] = (),
|
|
53
|
+
) -> Callable[[CheckFn], CheckFn]:
|
|
54
|
+
"""Register a check. The wrapped function is returned unchanged."""
|
|
55
|
+
|
|
56
|
+
def decorator(fn: CheckFn) -> CheckFn:
|
|
57
|
+
if id in _REGISTRY:
|
|
58
|
+
raise ValueError(f"duplicate check id: {id!r}")
|
|
59
|
+
_REGISTRY[id] = Check(
|
|
60
|
+
id=id,
|
|
61
|
+
title=title,
|
|
62
|
+
category=category,
|
|
63
|
+
severity=severity,
|
|
64
|
+
references=list(references),
|
|
65
|
+
fn=fn,
|
|
66
|
+
)
|
|
67
|
+
return fn
|
|
68
|
+
|
|
69
|
+
return decorator
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def all_checks() -> List[Check]:
|
|
73
|
+
"""Every registered check, sorted by id for stable output."""
|
|
74
|
+
_load_builtin_checks()
|
|
75
|
+
return sorted(_REGISTRY.values(), key=lambda c: c.id)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
_loaded = False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _load_builtin_checks() -> None:
|
|
82
|
+
"""Import the built-in check modules so their decorators run once."""
|
|
83
|
+
global _loaded
|
|
84
|
+
if _loaded:
|
|
85
|
+
return
|
|
86
|
+
from .checks import cost, iam, reliability, security # noqa: F401
|
|
87
|
+
|
|
88
|
+
_loaded = True
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Cost / waste checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Iterator
|
|
6
|
+
|
|
7
|
+
from ..catalog import check
|
|
8
|
+
from ..models import Category, Detail, ResourceRef, Severity
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@check(
|
|
12
|
+
id="COST001",
|
|
13
|
+
title="Unattached persistent disk",
|
|
14
|
+
category=Category.COST,
|
|
15
|
+
severity=Severity.LOW,
|
|
16
|
+
references=["https://cloud.google.com/compute/docs/disks"],
|
|
17
|
+
)
|
|
18
|
+
def unattached_disks(snapshot: dict) -> Iterator[Detail]:
|
|
19
|
+
for disk in snapshot.get("disks", []):
|
|
20
|
+
if not disk.get("users"):
|
|
21
|
+
size = disk.get("size_gb", "?")
|
|
22
|
+
yield Detail(
|
|
23
|
+
resource=ResourceRef("compute.disk", disk.get("name", "?")),
|
|
24
|
+
message=f"Disk ({size} GB) is not attached to any instance but still incurs storage cost.",
|
|
25
|
+
recommendation="Snapshot then delete the disk if it is no longer needed.",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@check(
|
|
30
|
+
id="COST002",
|
|
31
|
+
title="Reserved static IP address is unused",
|
|
32
|
+
category=Category.COST,
|
|
33
|
+
severity=Severity.LOW,
|
|
34
|
+
references=["https://cloud.google.com/vpc/docs/reserve-static-external-ip-address"],
|
|
35
|
+
)
|
|
36
|
+
def unused_addresses(snapshot: dict) -> Iterator[Detail]:
|
|
37
|
+
for addr in snapshot.get("addresses", []):
|
|
38
|
+
if addr.get("status") == "RESERVED" and not addr.get("users"):
|
|
39
|
+
yield Detail(
|
|
40
|
+
resource=ResourceRef("compute.address", addr.get("name", "?")),
|
|
41
|
+
message="Static IP is reserved but not attached; idle static IPs are billed.",
|
|
42
|
+
recommendation="Release the static IP if it is no longer required.",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@check(
|
|
47
|
+
id="COST003",
|
|
48
|
+
title="Instance stopped but disks still allocated",
|
|
49
|
+
category=Category.COST,
|
|
50
|
+
severity=Severity.LOW,
|
|
51
|
+
references=["https://cloud.google.com/compute/docs/instances/instance-life-cycle"],
|
|
52
|
+
)
|
|
53
|
+
def stopped_instances(snapshot: dict) -> Iterator[Detail]:
|
|
54
|
+
for inst in snapshot.get("instances", []):
|
|
55
|
+
if inst.get("status") == "TERMINATED":
|
|
56
|
+
yield Detail(
|
|
57
|
+
resource=ResourceRef("compute.instance", inst.get("name", "?")),
|
|
58
|
+
message="Instance is stopped; attached disks and reserved IPs continue to be billed.",
|
|
59
|
+
recommendation="Delete the instance (keeping a snapshot) if it is no longer used.",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@check(
|
|
64
|
+
id="COST004",
|
|
65
|
+
title="No budget alert configured for the project",
|
|
66
|
+
category=Category.COST,
|
|
67
|
+
severity=Severity.MEDIUM,
|
|
68
|
+
references=["https://cloud.google.com/billing/docs/how-to/budgets"],
|
|
69
|
+
)
|
|
70
|
+
def no_budget(snapshot: dict) -> Iterator[Detail]:
|
|
71
|
+
# Only meaningful when billing info was collected into the snapshot.
|
|
72
|
+
if "budgets" in snapshot and not snapshot.get("budgets"):
|
|
73
|
+
yield Detail(
|
|
74
|
+
resource=ResourceRef("billing.project", snapshot.get("project_id", "?")),
|
|
75
|
+
message="No budget or budget alert is configured; runaway spend would go unnoticed.",
|
|
76
|
+
recommendation="Create a budget with threshold alerts in Cloud Billing.",
|
|
77
|
+
)
|