Skip to content

Commit 669b0d4

Browse files
authored
Supporting "markdown" scan output format (#92)
1 parent aef7999 commit 669b0d4

5 files changed

Lines changed: 419 additions & 19 deletions

File tree

README.md

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Like `tfsec` for Terraform or `trivy` for containers — CleanCloud scans your c
1313
- **20 high-signal detection rules:** orphaned volumes, idle databases, empty load balancers, and more
1414
- **Estimated monthly waste:** per finding and aggregate
1515
- **CI-native enforcement (opt-in):** `--fail-on-confidence HIGH` or `--fail-on-cost 100` gates your pipeline
16+
- **Multiple output formats:** human-readable, JSON, CSV, and markdown (paste into GitHub PRs or Slack)
1617
- **Read-only by design:** no deletions, no tag changes, no mutations — ever
1718
- **No agents. No telemetry. No SaaS.** Runs in your environment, data never leaves
1819

@@ -21,8 +22,22 @@ Like `tfsec` for Terraform or `trivy` for containers — CleanCloud scans your c
2122
- Scheduled hygiene scans — cron job or weekly CI run to catch drift
2223
- CI/CD enforcement gates — fail builds when waste exceeds your threshold
2324

24-
> Bug reports and feedback very welcome via [issues](https://github.com/cleancloud-io/cleancloud/issues).
25+
```
26+
Found 6 hygiene issues:
2527
28+
1. [AWS] Unattached EBS Volume — $40/month
29+
2. [AWS] Idle NAT Gateway — $32.40/month
30+
3. [AWS] Unattached Elastic IP — $0/month
31+
...
32+
33+
Estimated monthly waste: ~$147
34+
Regions scanned: us-east-1, us-west-2, eu-west-1
35+
```
36+
37+
## What users say
38+
39+
> "Solid discovery tool that bubbles up potential savings. Easy to install and use!"
40+
> [Reddit user](https://www.reddit.com/r/AZURE/comments/1rm7an5/comment/o8zfv6a/)
2641
---
2742

2843
## Get Started
@@ -139,7 +154,40 @@ Regions scanned: us-east-1, us-west-2, eu-west-1 (auto-detected)
139154

140155
No cloud account yet? `cleancloud demo` shows sample output without any credentials.
141156

142-
For full output examples including `doctor`, JSON, and CSV: [`docs/example-outputs.md`](docs/example-outputs.md)
157+
### Shareable markdown report
158+
159+
```bash
160+
cleancloud scan --provider aws --all-regions --output markdown
161+
```
162+
163+
Prints a grouped summary you can paste directly into a GitHub PR comment, Slack message, or issue:
164+
165+
```markdown
166+
## CleanCloud Scan Results
167+
168+
**Provider:** AWS
169+
**Regions:** us-east-1, us-west-2, eu-west-1
170+
**Scanned:** 2026-03-07
171+
**Estimated monthly waste:** ~$147
172+
173+
**Total findings:** 6
174+
175+
| Finding | Count | Est. Monthly Cost |
176+
|---------|------:|------------------:|
177+
| Unattached EBS Volume | 2 | ~$115 |
178+
| Idle NAT Gateway | 1 | ~$32 |
179+
| Unattached Elastic IP | 1 | ~$0 |
180+
| Detached ENI | 1 ||
181+
| CloudWatch Log Group: Infinite Retention | 1 ||
182+
183+
**Confidence:** high: 3 · medium: 3
184+
185+
> Generated by [CleanCloud](https://github.com/cleancloud-io/cleancloud) — read-only cloud hygiene scanner for AWS and Azure.
186+
```
187+
188+
Save to a file with `--output-file results.md`. Without `--output-file`, it prints to stdout.
189+
190+
For full output examples including `doctor`, JSON, CSV, and markdown: [`docs/example-outputs.md`](docs/example-outputs.md)
143191

144192
---
145193

@@ -229,18 +277,6 @@ Setup guides: [AWS](docs/aws.md) · [Azure](docs/azure.md)
229277

230278
---
231279

232-
## What Users Are Saying
233-
234-
> "Solid discovery tool that bubbles up potential savings. Easy to install and use!
235-
>
236-
> The VPN gateway showed all of mine had no active connections, but 3 of 4 show Connected
237-
> and have recent data. I know why the 4th one is not connected and this helps remind me
238-
> to pester the network team about it :)"
239-
>
240-
> — [Reddit user](https://www.reddit.com/r/AZURE/comments/1rm7an5/comment/o8zfv6a/)
241-
242-
---
243-
244280
## Roadmap
245281

246282
- Additional AWS rules (S3 lifecycle, stopped EC2 instances)
@@ -262,8 +298,10 @@ Setup guides: [AWS](docs/aws.md) · [Azure](docs/azure.md)
262298

263299
---
264300

265-
**Found a bug?** [Open an issue](https://github.com/cleancloud-io/cleancloud/issues) ·
266-
**Feature request?** [Start a discussion](https://github.com/cleancloud-io/cleancloud/discussions) ·
301+
**Found a bug?** [Open an issue](https://github.com/cleancloud-io/cleancloud/issues)
302+
303+
**Feature request?** [Start a discussion](https://github.com/cleancloud-io/cleancloud/discussions)
304+
267305
**Questions?** suresh@getcleancloud.com
268306

269307
[MIT License](LICENSE)

cleancloud/output/markdown.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
from collections import defaultdict
2+
from pathlib import Path
3+
from typing import Dict, List, Optional
4+
5+
from cleancloud.core.finding import Finding
6+
7+
8+
def _group_findings(findings: List[Finding]) -> List[Dict]:
9+
"""Group findings by title, summing counts and costs."""
10+
groups: Dict[str, Dict] = defaultdict(lambda: {"count": 0, "cost": 0.0, "has_cost": False})
11+
12+
for f in findings:
13+
key = f.title
14+
groups[key]["count"] += 1
15+
if f.estimated_monthly_cost_usd is not None:
16+
groups[key]["cost"] += f.estimated_monthly_cost_usd
17+
groups[key]["has_cost"] = True
18+
19+
# Sort by cost descending, then count descending
20+
return sorted(
21+
[{"title": k, **v} for k, v in groups.items()],
22+
key=lambda x: (x["cost"], x["count"]),
23+
reverse=True,
24+
)
25+
26+
27+
def write_markdown(
28+
findings: List[Finding],
29+
summary: dict,
30+
output_path: Optional[Path] = None,
31+
) -> Optional[str]:
32+
lines = []
33+
34+
provider = summary.get("provider", "").upper()
35+
scanned_at = summary.get("scanned_at", "")[:10] # date only
36+
37+
# Header
38+
lines.append("## CleanCloud Scan Results")
39+
lines.append("")
40+
41+
# Metadata
42+
regions = summary.get("regions_scanned", [])
43+
if isinstance(regions, list):
44+
regions_str = ", ".join(regions) if regions else "—"
45+
else:
46+
regions_str = str(regions)
47+
48+
subscriptions = summary.get("subscriptions_scanned", [])
49+
if subscriptions:
50+
lines.append(f"**Provider:** {provider} ")
51+
lines.append(f"**Subscriptions:** {', '.join(subscriptions)} ")
52+
else:
53+
lines.append(f"**Provider:** {provider} ")
54+
lines.append(f"**Regions:** {regions_str} ")
55+
56+
lines.append(f"**Scanned:** {scanned_at} ")
57+
58+
waste = summary.get("minimum_estimated_monthly_waste_usd")
59+
if waste and waste > 0:
60+
lines.append(f"**Estimated monthly waste:** ~${waste:,.0f} ")
61+
62+
lines.append("")
63+
64+
# Findings table
65+
total = summary.get("total_findings", 0)
66+
if total == 0:
67+
lines.append("**No hygiene issues detected.**")
68+
else:
69+
lines.append(f"**Total findings:** {total}")
70+
lines.append("")
71+
lines.append("| Finding | Count | Est. Monthly Cost |")
72+
lines.append("|---------|------:|------------------:|")
73+
74+
for group in _group_findings(findings):
75+
cost_str = f"~${group['cost']:,.0f}" if group["has_cost"] else "—"
76+
lines.append(f"| {group['title']} | {group['count']} | {cost_str} |")
77+
78+
lines.append("")
79+
80+
# Confidence breakdown
81+
by_conf = summary.get("by_confidence", {})
82+
if by_conf:
83+
conf_parts = []
84+
for level in ["high", "medium", "low"]:
85+
for k, v in by_conf.items():
86+
label = k.value if hasattr(k, "value") else str(k)
87+
if label == level:
88+
conf_parts.append(f"{label}: {v}")
89+
if conf_parts:
90+
lines.append(f"**Confidence:** {' · '.join(conf_parts)}")
91+
lines.append("")
92+
93+
# Footer
94+
lines.append(
95+
"> Generated by [CleanCloud](https://github.com/cleancloud-io/cleancloud) — "
96+
"read-only cloud hygiene scanner for AWS and Azure."
97+
)
98+
99+
output = "\n".join(lines)
100+
101+
if output_path:
102+
with open(output_path, "w", encoding="utf-8") as f:
103+
f.write(output)
104+
return None
105+
else:
106+
return output

cleancloud/scan/command.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from cleancloud.output.csv import write_csv
2525
from cleancloud.output.human import print_human
2626
from cleancloud.output.json import write_json
27+
from cleancloud.output.markdown import write_markdown
2728
from cleancloud.output.summary import _print_summary, build_summary
2829
from cleancloud.policy.exit_policy import (
2930
CONFIDENCE_ORDER,
@@ -62,12 +63,12 @@
6263
@click.option(
6364
"--output",
6465
default="human",
65-
type=click.Choice(["human", "json", "csv"]),
66+
type=click.Choice(["human", "json", "csv", "markdown"]),
6667
)
6768
@click.option(
6869
"--output-file",
6970
default=None,
70-
help="Output file path (required for json/csv)",
71+
help="Output file path (required for json/csv; optional for markdown — prints to stdout if omitted)",
7172
)
7273
@click.option(
7374
"--fail-on-findings",
@@ -229,6 +230,14 @@ def scan(
229230
click.echo(f"CSV output written to {output_path}")
230231
click.echo()
231232

233+
elif output == "markdown":
234+
result = write_markdown(findings, summary, output_path)
235+
if result is not None:
236+
click.echo(result)
237+
else:
238+
click.echo(f"Markdown output written to {output_path}")
239+
click.echo()
240+
232241
else:
233242
print_human(findings)
234243
_print_summary(summary, region_selection_mode)

docs/example-outputs.md

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Example Outputs
22

3-
Complete output examples for CleanCloud across AWS and Azure — doctor validation, human-readable scan results, and JSON output for CI/CD integration.
3+
Complete output examples for CleanCloud across AWS and Azure — doctor validation, human-readable scan results, JSON output for CI/CD integration, and markdown for sharing in GitHub PRs or Slack.
44

55
---
66

@@ -12,6 +12,8 @@ Complete output examples for CleanCloud across AWS and Azure — doctor validati
1212
- [Scan — Azure (Human-Readable)](#scan--azure-human-readable)
1313
- [Scan — AWS (JSON)](#scan--aws-json)
1414
- [Scan — Azure (JSON)](#scan--azure-json)
15+
- [Scan — AWS (Markdown)](#scan--aws-markdown)
16+
- [Scan — Azure (Markdown)](#scan--azure-markdown)
1517
- [JSON Schema Reference](#json-schema-reference)
1618

1719
---
@@ -805,6 +807,71 @@ Scanned at: 2026-02-08T14:45:16+00:00
805807

806808
---
807809

810+
## Scan — AWS (Markdown)
811+
812+
`cleancloud scan --provider aws --all-regions --output markdown`
813+
814+
Produces a grouped, cost-sorted summary you can paste directly into a GitHub PR comment, Slack message, or issue. Use `--output-file results.md` to save to a file instead.
815+
816+
```markdown
817+
## CleanCloud Scan Results
818+
819+
**Provider:** AWS
820+
**Regions:** us-east-1, us-west-2, eu-west-1
821+
**Scanned:** 2026-02-08
822+
**Estimated monthly waste:** ~$147
823+
824+
**Total findings:** 6
825+
826+
| Finding | Count | Est. Monthly Cost |
827+
|---------|------:|------------------:|
828+
| Unattached EBS Volume | 2 | ~$115 |
829+
| Idle NAT Gateway | 1 | ~$32 |
830+
| Old AMI | 1 | ~$4 |
831+
| Unattached Elastic IP | 1 | ~$0 |
832+
| CloudWatch Log Group: Infinite Retention | 1 ||
833+
| Untagged Resource | 1 ||
834+
835+
**Confidence:** high: 2 · medium: 4
836+
837+
> Generated by [CleanCloud](https://github.com/cleancloud-io/cleancloud) — read-only cloud hygiene scanner for AWS and Azure.
838+
```
839+
840+
Findings are grouped by title (multiple instances of the same finding type are collapsed into one row with a count) and sorted by estimated cost descending.
841+
842+
---
843+
844+
## Scan — Azure (Markdown)
845+
846+
`cleancloud scan --provider azure --output markdown`
847+
848+
```markdown
849+
## CleanCloud Scan Results
850+
851+
**Provider:** AZURE
852+
**Subscriptions:** Production, Staging
853+
**Scanned:** 2026-02-08
854+
**Estimated monthly waste:** ~$72
855+
856+
**Total findings:** 5
857+
858+
| Finding | Count | Est. Monthly Cost |
859+
|---------|------:|------------------:|
860+
| Load Balancer with No Backends | 1 | ~$18 |
861+
| Empty App Service Plan | 1 | ~$146 |
862+
| Unused Public IP | 1 | ~$4 |
863+
| Unattached Managed Disk | 1 ||
864+
| Untagged Resource | 1 ||
865+
866+
**Confidence:** high: 3 · medium: 2
867+
868+
> Generated by [CleanCloud](https://github.com/cleancloud-io/cleancloud) — read-only cloud hygiene scanner for AWS and Azure.
869+
```
870+
871+
For Azure, the **Subscriptions** field is shown instead of **Regions**, reflecting how Azure scans are scoped.
872+
873+
---
874+
808875
## JSON Schema Reference
809876

810877
CleanCloud uses a versioned JSON schema (current: `1.0.0`). All JSON output includes a `schema_version` field for backward compatibility.

0 commit comments

Comments
 (0)