zenveil 1.0.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.
zenveil-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.1
2
+ Name: zenveil
3
+ Version: 1.0.0
4
+ Summary: AI-powered security scanner for repositories and APIs.
5
+ Project-URL: Homepage, https://zenveil.dev
6
+ Project-URL: Documentation, https://zenveil.dev/docs
7
+ Project-URL: Repository, https://github.com/oyugirachel/zen-guard
8
+ Requires-Python: >=3.8
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=42"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "zenveil"
7
+ version = "1.0.0"
8
+ description = "AI-powered security scanner for repositories and APIs."
9
+ requires-python = ">=3.8"
10
+ dependencies = []
11
+
12
+ [project.urls]
13
+ Homepage = "https://zenveil.dev"
14
+ Documentation = "https://zenveil.dev/docs"
15
+ Repository = "https://github.com/oyugirachel/zen-guard"
16
+
17
+ [project.scripts]
18
+ zenguard = "zenguard.cli:main"
19
+
20
+ [tool.setuptools.packages.find]
21
+ where = ["."]
22
+ include = ["zenguard*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ """ZenGuard client — AI-powered security scanner."""
@@ -0,0 +1,94 @@
1
+ """HTTP client that talks to the ZenGuard API server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import urllib.error
7
+ import urllib.request
8
+ from typing import Iterator
9
+
10
+ from zenguard import config
11
+
12
+
13
+ def _headers(api_key: str) -> dict[str, str]:
14
+ return {
15
+ "X-API-Key": api_key,
16
+ "Content-Type": "application/json",
17
+ "Accept": "application/json",
18
+ "User-Agent": "zenguard-client/1.0.0",
19
+ }
20
+
21
+
22
+ def _post(path: str, payload: dict, api_key: str, base_url: str) -> dict:
23
+ url = f"{base_url.rstrip('/')}{path}"
24
+ data = json.dumps(payload).encode("utf-8")
25
+ req = urllib.request.Request(url, data=data, headers=_headers(api_key), method="POST")
26
+ try:
27
+ with urllib.request.urlopen(req, timeout=120) as resp:
28
+ return json.loads(resp.read().decode("utf-8"))
29
+ except urllib.error.HTTPError as exc:
30
+ body = exc.read().decode("utf-8", errors="replace")
31
+ try:
32
+ detail = json.loads(body).get("detail", body)
33
+ except json.JSONDecodeError:
34
+ detail = body
35
+ raise RuntimeError(f"API error {exc.code}: {detail}") from exc
36
+ except urllib.error.URLError as exc:
37
+ raise RuntimeError(f"Could not reach ZenGuard API: {exc.reason}") from exc
38
+
39
+
40
+ def _stream(path: str, payload: dict, api_key: str, base_url: str) -> Iterator[str]:
41
+ url = f"{base_url.rstrip('/')}{path}"
42
+ data = json.dumps(payload).encode("utf-8")
43
+ req = urllib.request.Request(url, data=data, headers={
44
+ **_headers(api_key),
45
+ "Accept": "text/plain",
46
+ }, method="POST")
47
+ try:
48
+ with urllib.request.urlopen(req, timeout=120) as resp:
49
+ while True:
50
+ chunk = resp.read(256)
51
+ if not chunk:
52
+ break
53
+ yield chunk.decode("utf-8", errors="replace")
54
+ except urllib.error.HTTPError as exc:
55
+ body = exc.read().decode("utf-8", errors="replace")
56
+ raise RuntimeError(f"API error {exc.code}: {body}") from exc
57
+ except urllib.error.URLError as exc:
58
+ raise RuntimeError(f"Could not reach ZenGuard API: {exc.reason}") from exc
59
+
60
+
61
+ class ZenGuardClient:
62
+ def __init__(self, api_key: str | None = None, api_url: str | None = None) -> None:
63
+ self._api_key = api_key or config.get_api_key()
64
+ self._api_url = api_url or config.get_api_url()
65
+ if not self._api_key:
66
+ raise RuntimeError(
67
+ "No API key found. Run `zenguard login` or set ZENGUARD_API_KEY."
68
+ )
69
+
70
+ def scan_github(
71
+ self,
72
+ repository: str,
73
+ token: str | None = None,
74
+ ref: str | None = None,
75
+ check_cves: bool = False,
76
+ ) -> dict:
77
+ return _post("/v1/scan/github", {
78
+ "repository": repository,
79
+ "token": token,
80
+ "ref": ref,
81
+ "check_cves": check_cves,
82
+ }, self._api_key, self._api_url)
83
+
84
+ def scan_api(self, url: str) -> dict:
85
+ return _post("/v1/scan/api", {"url": url}, self._api_key, self._api_url)
86
+
87
+ def explain_stream(self, finding: dict) -> Iterator[str]:
88
+ yield from _stream("/v1/explain", {"finding": finding}, self._api_key, self._api_url)
89
+
90
+ def fix_stream(self, finding: dict) -> Iterator[str]:
91
+ yield from _stream("/v1/fix", {"finding": finding}, self._api_key, self._api_url)
92
+
93
+ def triage_stream(self, scan: dict) -> Iterator[str]:
94
+ yield from _stream("/v1/triage", {"scan": scan}, self._api_key, self._api_url)
@@ -0,0 +1,310 @@
1
+ """ZenGuard thin CLI client — talks to the ZenGuard API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Sequence
10
+
11
+ from zenguard import config
12
+ from zenguard.api_client import ZenGuardClient
13
+ from zenguard.render import render_list, render_scan, render_stats
14
+
15
+ _LAST_SCAN_FILE = Path(".zenguard-last-scan.json")
16
+
17
+
18
+ def _save_scan(result: dict) -> None:
19
+ _LAST_SCAN_FILE.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
20
+
21
+
22
+ def _load_scan() -> dict:
23
+ if not _LAST_SCAN_FILE.exists():
24
+ raise ValueError("No cached scan found. Run `zenguard scan` first.")
25
+ return json.loads(_LAST_SCAN_FILE.read_text(encoding="utf-8"))
26
+
27
+
28
+ def _client() -> ZenGuardClient:
29
+ return ZenGuardClient()
30
+
31
+
32
+ def build_parser() -> argparse.ArgumentParser:
33
+ parser = argparse.ArgumentParser(prog="zenguard", description="AI-powered security scanner.")
34
+ subparsers = parser.add_subparsers(dest="command", required=True)
35
+
36
+ # login
37
+ login_p = subparsers.add_parser("login", help="Save your ZenGuard API key.")
38
+ login_p.add_argument("api_key", help="Your ZenGuard API key.")
39
+ login_p.add_argument("--url", dest="api_url", help="Custom API URL (optional).")
40
+
41
+ # scan
42
+ scan_p = subparsers.add_parser("scan", help="Run a security scan.")
43
+ scan_sub = scan_p.add_subparsers(dest="target_type", required=True)
44
+
45
+ github_p = scan_sub.add_parser("github", help="Scan a GitHub repository.")
46
+ github_p.add_argument("repository", help="owner/repo or GitHub URL.")
47
+ github_p.add_argument("--token", dest="token", help="GitHub token for private repos.")
48
+ github_p.add_argument("--ref", dest="ref", help="Branch, tag, or commit.")
49
+ github_p.add_argument("--check-cves", action="store_true", dest="check_cves")
50
+
51
+ api_p = scan_sub.add_parser("api", help="Scan an API URL.")
52
+ api_p.add_argument("url", help="API base URL.")
53
+
54
+ # list
55
+ list_p = subparsers.add_parser("list", help="List findings from the last scan.")
56
+ list_p.add_argument("--severity", dest="severity_filter", help="e.g. high,critical")
57
+ list_p.add_argument("--scanner", dest="scanner_filter", help="e.g. secrets")
58
+
59
+ # stats
60
+ subparsers.add_parser("stats", help="Show statistics for the last scan.")
61
+
62
+ # explain
63
+ explain_p = subparsers.add_parser("explain", help="AI explanation of a finding.")
64
+ explain_p.add_argument("finding_id", help="Finding ID (e.g. ZG-ABC123).")
65
+
66
+ # fix
67
+ fix_p = subparsers.add_parser("fix", help="AI-generated fix for a finding.")
68
+ fix_p.add_argument("finding_id", help="Finding ID (e.g. ZG-ABC123).")
69
+
70
+ # triage
71
+ subparsers.add_parser("triage", help="AI-prioritized remediation plan for all findings.")
72
+
73
+ # ignore
74
+ ignore_p = subparsers.add_parser("ignore", help="Suppress a finding.")
75
+ ignore_p.add_argument("finding_id", help="Finding ID to ignore.")
76
+ ignore_p.add_argument("--reason", dest="reason", default="")
77
+
78
+ # report
79
+ report_p = subparsers.add_parser("report", help="Export the last scan.")
80
+ report_sub = report_p.add_subparsers(dest="report_type", required=True)
81
+ json_p = report_sub.add_parser("json", help="Write last scan to JSON.")
82
+ json_p.add_argument("output_file")
83
+ html_p = report_sub.add_parser("html", help="Write last scan to HTML.")
84
+ html_p.add_argument("output_file")
85
+
86
+ # help
87
+ subparsers.add_parser("help", help="List all commands.")
88
+
89
+ return parser
90
+
91
+
92
+ def main(argv: Sequence[str] | None = None) -> int:
93
+ parser = build_parser()
94
+ args = parser.parse_args(argv)
95
+
96
+ try:
97
+ if args.command == "login":
98
+ data = config.load()
99
+ data["api_key"] = args.api_key
100
+ if getattr(args, "api_url", None):
101
+ data["api_url"] = args.api_url
102
+ config.save(data)
103
+ print(f"API key saved to {config._CONFIG_FILE}")
104
+ return 0
105
+
106
+ if args.command == "scan":
107
+ c = _client()
108
+ if args.target_type == "github":
109
+ print(f"Scanning {args.repository}…")
110
+ result = c.scan_github(
111
+ args.repository,
112
+ token=getattr(args, "token", None),
113
+ ref=getattr(args, "ref", None),
114
+ check_cves=getattr(args, "check_cves", False),
115
+ )
116
+ else:
117
+ print(f"Scanning {args.url}…")
118
+ result = c.scan_api(args.url)
119
+ print(render_scan(result))
120
+ _save_scan(result)
121
+ findings = result.get("findings", [])
122
+ return 1 if any(f["severity"] in {"HIGH", "CRITICAL"} for f in findings) else 0
123
+
124
+ if args.command == "list":
125
+ result = _load_scan()
126
+ print(render_list(result.get("findings", []), {
127
+ "severity": getattr(args, "severity_filter", None),
128
+ "scanner": getattr(args, "scanner_filter", None),
129
+ }))
130
+ return 0
131
+
132
+ if args.command == "stats":
133
+ print(render_stats(_load_scan()))
134
+ return 0
135
+
136
+ if args.command == "explain":
137
+ result = _load_scan()
138
+ index = {f["id"]: f for f in result.get("findings", [])}
139
+ finding = index.get(args.finding_id.upper())
140
+ if not finding:
141
+ print(f"Finding {args.finding_id.upper()} not found.", file=sys.stderr)
142
+ return 2
143
+ print(f"\nExplaining {finding['id']}: {finding['title']}\n")
144
+ for chunk in _client().explain_stream(finding):
145
+ print(chunk, end="", flush=True)
146
+ print()
147
+ return 0
148
+
149
+ if args.command == "fix":
150
+ result = _load_scan()
151
+ index = {f["id"]: f for f in result.get("findings", [])}
152
+ finding = index.get(args.finding_id.upper())
153
+ if not finding:
154
+ print(f"Finding {args.finding_id.upper()} not found.", file=sys.stderr)
155
+ return 2
156
+ print(f"\nGenerating fix for {finding['id']}: {finding['title']}\n")
157
+ for chunk in _client().fix_stream(finding):
158
+ print(chunk, end="", flush=True)
159
+ print()
160
+ return 0
161
+
162
+ if args.command == "triage":
163
+ result = _load_scan()
164
+ if not result.get("findings"):
165
+ print("No findings to triage.")
166
+ return 0
167
+ print(f"\nTriaging {result['finding_count']} finding(s)…\n")
168
+ for chunk in _client().triage_stream(result):
169
+ print(chunk, end="", flush=True)
170
+ print()
171
+ return 0
172
+
173
+ if args.command == "ignore":
174
+ result = _load_scan()
175
+ index = {f["id"]: f for f in result.get("findings", [])}
176
+ finding = index.get(args.finding_id.upper())
177
+ if not finding:
178
+ print(f"Finding {args.finding_id.upper()} not found.", file=sys.stderr)
179
+ return 2
180
+ ignore_file = Path(".zenguard-ignore.json")
181
+ ignored = {}
182
+ if ignore_file.exists():
183
+ ignored = json.loads(ignore_file.read_text(encoding="utf-8"))
184
+ ignored[args.finding_id.upper()] = {
185
+ "title": finding["title"],
186
+ "scanner": finding["scanner_name"],
187
+ "severity": finding["severity"],
188
+ "reason": args.reason,
189
+ }
190
+ ignore_file.write_text(json.dumps(ignored, indent=2) + "\n", encoding="utf-8")
191
+ print(f"Suppressed {args.finding_id.upper()} ({finding['title']}).")
192
+ return 0
193
+
194
+ if args.command == "report":
195
+ result = _load_scan()
196
+ if args.report_type == "json":
197
+ Path(args.output_file).write_text(
198
+ json.dumps(result, indent=2) + "\n", encoding="utf-8"
199
+ )
200
+ print(f"Wrote JSON report to {args.output_file}")
201
+ elif args.report_type == "html":
202
+ _write_html_report(result, args.output_file)
203
+ print(f"Wrote HTML report to {args.output_file}")
204
+ return 0
205
+
206
+ if args.command == "help":
207
+ _print_help()
208
+ return 0
209
+
210
+ except (RuntimeError, ValueError) as exc:
211
+ print(f"Error: {exc}", file=sys.stderr)
212
+ return 2
213
+
214
+ return 0
215
+
216
+
217
+ def _write_html_report(result: dict, output_file: str) -> None:
218
+ import html as h
219
+ findings = result.get("findings", [])
220
+ _SEV_COLOR = {"CRITICAL": "#c0392b", "HIGH": "#e67e22", "MEDIUM": "#f1c40f", "LOW": "#2ecc71"}
221
+ _SEV_ORDER = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
222
+ sorted_findings = sorted(findings, key=lambda f: _SEV_ORDER.get(f["severity"].upper(), 99))
223
+ findings_html = ""
224
+ for f in sorted_findings:
225
+ color = _SEV_COLOR.get(f["severity"].upper(), "#888")
226
+ loc = f["location"]
227
+ loc_str = loc.get("target", "")
228
+ if loc.get("path"):
229
+ loc_str += f" → {loc['path']}"
230
+ if loc.get("line"):
231
+ loc_str += f" : line {loc['line']}"
232
+ findings_html += f"""
233
+ <div class="finding">
234
+ <div class="finding-header" style="border-left:6px solid {color}">
235
+ <span class="badge" style="background:{color}">{h.escape(f['severity'])}</span>
236
+ <span class="fid">{h.escape(f['id'])}</span>
237
+ <span class="ftitle">{h.escape(f['title'])}</span>
238
+ </div>
239
+ <div class="finding-body">
240
+ <table>
241
+ <tr><th>Scanner</th><td>{h.escape(f['scanner_name'])}</td></tr>
242
+ <tr><th>Location</th><td>{h.escape(loc_str)}</td></tr>
243
+ <tr><th>Evidence</th><td><code>{h.escape(f['evidence'])}</code></td></tr>
244
+ <tr><th>Description</th><td>{h.escape(f['description'])}</td></tr>
245
+ <tr><th>Remediation</th><td>{h.escape(f['remediation'])}</td></tr>
246
+ </table>
247
+ </div>
248
+ </div>"""
249
+ page = f"""<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"/>
250
+ <title>ZenGuard Report — {h.escape(result.get('target',''))}</title>
251
+ <style>body{{font-family:sans-serif;background:#f4f6f9;color:#222;padding:24px}}
252
+ .finding{{background:#fff;border-radius:6px;margin-bottom:16px;box-shadow:0 1px 4px rgba(0,0,0,.1);overflow:hidden}}
253
+ .finding-header{{display:flex;align-items:center;gap:10px;padding:12px 16px;background:#fafafa}}
254
+ .badge{{color:#fff;border-radius:4px;padding:2px 8px;font-weight:700;font-size:.78rem;text-transform:uppercase}}
255
+ .fid{{font-family:monospace;font-size:.85rem;color:#555}}.ftitle{{font-weight:600}}
256
+ .finding-body{{padding:12px 16px}}table{{border-collapse:collapse;width:100%;font-size:.88rem}}
257
+ th{{text-align:left;color:#555;font-weight:600;width:110px;padding:4px 8px 4px 0;vertical-align:top}}
258
+ code{{background:#f0f0f0;padding:1px 4px;border-radius:3px;font-size:.82rem;word-break:break-all}}
259
+ </style></head><body>
260
+ <h1>ZenGuard Security Report</h1>
261
+ <p>Target: <strong>{h.escape(result.get('target',''))}</strong> | Findings: {result.get('finding_count',0)}</p>
262
+ {findings_html if findings_html else '<p>No findings.</p>'}
263
+ </body></html>"""
264
+ Path(output_file).write_text(page, encoding="utf-8")
265
+
266
+
267
+ def _print_help() -> None:
268
+ print("""
269
+ ZenGuard — AI-powered security scanner
270
+ =======================================
271
+
272
+ ACCOUNT
273
+ zenguard login <api-key> Save your API key
274
+
275
+ SCANNING
276
+ zenguard scan github <owner/repo> Scan a GitHub repository
277
+ zenguard scan github <url> --token Use a GitHub token for private repos
278
+ zenguard scan api <url> Scan an API endpoint
279
+
280
+ FINDINGS
281
+ zenguard list List findings from the last scan
282
+ zenguard list --severity high,critical
283
+ zenguard list --scanner secrets
284
+ zenguard stats Scan statistics
285
+
286
+ AI ANALYSIS
287
+ zenguard explain <id> Explain a finding
288
+ zenguard fix <id> Generate a fix
289
+ zenguard triage Prioritized remediation plan
290
+
291
+ REPORTING
292
+ zenguard report json <file> Export to JSON
293
+ zenguard report html <file> Export to HTML
294
+
295
+ MANAGEMENT
296
+ zenguard ignore <id> Suppress a finding
297
+ zenguard ignore <id> --reason "text"
298
+ zenguard help Show this help
299
+
300
+ ENVIRONMENT VARIABLES
301
+ ZENGUARD_API_KEY Your ZenGuard API key
302
+ ZENGUARD_API_URL Custom API URL (default: https://api.zenveil.dev)
303
+ GITHUB_TOKEN GitHub token for private repo scans
304
+
305
+ Get your API key at https://zenveil.dev
306
+ """)
307
+
308
+
309
+ if __name__ == "__main__":
310
+ raise SystemExit(main())
@@ -0,0 +1,35 @@
1
+ """Stores ZenGuard API key and server URL locally."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+
9
+ _CONFIG_DIR = Path.home() / ".zenguard"
10
+ _CONFIG_FILE = _CONFIG_DIR / "config.json"
11
+
12
+ DEFAULT_API_URL = "https://api.zenveil.dev"
13
+
14
+
15
+ def load() -> dict:
16
+ if _CONFIG_FILE.exists():
17
+ try:
18
+ return json.loads(_CONFIG_FILE.read_text(encoding="utf-8"))
19
+ except (json.JSONDecodeError, OSError):
20
+ pass
21
+ return {}
22
+
23
+
24
+ def save(data: dict) -> None:
25
+ _CONFIG_DIR.mkdir(parents=True, exist_ok=True)
26
+ _CONFIG_FILE.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
27
+ _CONFIG_FILE.chmod(0o600)
28
+
29
+
30
+ def get_api_key() -> str | None:
31
+ return os.environ.get("ZENGUARD_API_KEY") or load().get("api_key")
32
+
33
+
34
+ def get_api_url() -> str:
35
+ return os.environ.get("ZENGUARD_API_URL") or load().get("api_url") or DEFAULT_API_URL
@@ -0,0 +1,97 @@
1
+ """Console rendering for scan results from the API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ _SEV_ORDER = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}
6
+ _SEV_PREFIX = {"CRITICAL": "[CRITICAL]", "HIGH": "[HIGH]", "MEDIUM": "[MEDIUM]", "LOW": "[LOW]"}
7
+
8
+
9
+ def render_scan(result: dict) -> str:
10
+ findings = result.get("findings", [])
11
+ counts: dict[str, int] = {}
12
+ for f in findings:
13
+ sev = f["severity"].upper()
14
+ counts[sev] = counts.get(sev, 0) + 1
15
+
16
+ sev_str = ", ".join(
17
+ f"{s}={counts.get(s, 0)}"
18
+ for s in ["LOW", "MEDIUM", "HIGH", "CRITICAL"]
19
+ )
20
+
21
+ lines = [
22
+ "",
23
+ "ZenGuard Scan Report",
24
+ f"Target: {result.get('target_type', '')} {result.get('target', '')}",
25
+ f"Findings: {result.get('finding_count', 0)}",
26
+ f"Severity: {sev_str}",
27
+ "",
28
+ ]
29
+
30
+ sorted_findings = sorted(findings, key=lambda f: _SEV_ORDER.get(f["severity"].upper(), 99))
31
+ for f in sorted_findings:
32
+ sev = f["severity"].upper()
33
+ prefix = _SEV_PREFIX.get(sev, f"[{sev}]")
34
+ loc = f["location"]
35
+ location_str = loc.get("path") or loc.get("url") or loc.get("target", "")
36
+ if loc.get("line"):
37
+ location_str = f"{location_str}:{loc['line']}"
38
+
39
+ lines += [
40
+ f"{prefix} {f['title']}",
41
+ f"ID: {f['id']}",
42
+ f"Category: {f['category']}",
43
+ f"Scanner: {f['scanner_name']}",
44
+ f"Location: {location_str}",
45
+ f"Evidence: {f['evidence']}",
46
+ f"Description: {f['description']}",
47
+ f"Remediation: {f['remediation']}",
48
+ "",
49
+ ]
50
+
51
+ return "\n".join(lines)
52
+
53
+
54
+ def render_list(findings: list[dict], filters: dict | None = None) -> str:
55
+ if filters:
56
+ sev = {s.strip().lower() for s in (filters.get("severity") or "").split(",") if s.strip()}
57
+ scanner = (filters.get("scanner") or "").lower()
58
+ if sev:
59
+ findings = [f for f in findings if f["severity"].lower() in sev]
60
+ if scanner:
61
+ findings = [f for f in findings if f["scanner_name"].lower() == scanner]
62
+
63
+ if not findings:
64
+ return "No findings match the given filters."
65
+
66
+ sorted_findings = sorted(findings, key=lambda f: (_SEV_ORDER.get(f["severity"].upper(), 99), f["title"]))
67
+ lines = [f"\n{'ID':<18} {'SEV':<9} {'SCANNER':<15} {'TITLE'}", "-" * 80]
68
+ for f in sorted_findings:
69
+ lines.append(f"{f['id']:<18} {f['severity']:<9} {f['scanner_name']:<15} {f['title']}")
70
+ lines.append(f"\n{len(sorted_findings)} finding(s) shown.\n")
71
+ return "\n".join(lines)
72
+
73
+
74
+ def render_stats(result: dict) -> str:
75
+ findings = result.get("findings", [])
76
+ sev_counts: dict[str, int] = {}
77
+ scanner_counts: dict[str, int] = {}
78
+ for f in findings:
79
+ sev_counts[f["severity"]] = sev_counts.get(f["severity"], 0) + 1
80
+ scanner_counts[f["scanner_name"]] = scanner_counts.get(f["scanner_name"], 0) + 1
81
+
82
+ lines = [
83
+ f"\nScan target : {result.get('target')} ({result.get('target_type')})",
84
+ f"Started : {result.get('started_at')}",
85
+ f"Completed : {result.get('completed_at')}",
86
+ f"Total findings: {result.get('finding_count', 0)}",
87
+ ]
88
+ if sev_counts:
89
+ lines.append("\nBy severity:")
90
+ for sev in sorted(sev_counts, key=lambda s: _SEV_ORDER.get(s.upper(), 99)):
91
+ lines.append(f" {sev:<10} {sev_counts[sev]}")
92
+ if scanner_counts:
93
+ lines.append("\nBy scanner:")
94
+ for sc, cnt in sorted(scanner_counts.items(), key=lambda x: -x[1]):
95
+ lines.append(f" {sc:<20} {cnt}")
96
+ lines.append("")
97
+ return "\n".join(lines)
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.1
2
+ Name: zenveil
3
+ Version: 1.0.0
4
+ Summary: AI-powered security scanner for repositories and APIs.
5
+ Project-URL: Homepage, https://zenveil.dev
6
+ Project-URL: Documentation, https://zenveil.dev/docs
7
+ Project-URL: Repository, https://github.com/oyugirachel/zen-guard
8
+ Requires-Python: >=3.8
@@ -0,0 +1,11 @@
1
+ pyproject.toml
2
+ zenguard/__init__.py
3
+ zenguard/api_client.py
4
+ zenguard/cli.py
5
+ zenguard/config.py
6
+ zenguard/render.py
7
+ zenveil.egg-info/PKG-INFO
8
+ zenveil.egg-info/SOURCES.txt
9
+ zenveil.egg-info/dependency_links.txt
10
+ zenveil.egg-info/entry_points.txt
11
+ zenveil.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ zenguard = zenguard.cli:main
@@ -0,0 +1 @@
1
+ zenguard