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.
@@ -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.
@@ -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
+ ![Python](https://img.shields.io/badge/python-3.9%2B-blue)
22
+ ![License: MIT](https://img.shields.io/badge/license-MIT-green)
23
+ ![Checks](https://img.shields.io/badge/checks-16-orange)
24
+ ![Mock-first](https://img.shields.io/badge/mock--first-no%20cloud%20access-brightgreen)
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
+ ![Python](https://img.shields.io/badge/python-3.9%2B-blue)
4
+ ![License: MIT](https://img.shields.io/badge/license-MIT-green)
5
+ ![Checks](https://img.shields.io/badge/checks-16-orange)
6
+ ![Mock-first](https://img.shields.io/badge/mock--first-no%20cloud%20access-brightgreen)
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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """gcphelpit — find security, IAM, cost, and reliability issues in Google Cloud."""
2
+
3
+ __version__ = "0.1.0"
@@ -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,7 @@
1
+ """Built-in check catalog.
2
+
3
+ Importing this package imports every check module, which runs the ``@check``
4
+ decorators and populates the registry in :mod:`gcphelpit.catalog`.
5
+ """
6
+
7
+ from . import cost, iam, reliability, security # noqa: F401
@@ -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
+ )