modvet 0.2.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.
Files changed (37) hide show
  1. modvet-0.2.0/.github/workflows/modvet.yml +22 -0
  2. modvet-0.2.0/.gitignore +8 -0
  3. modvet-0.2.0/PKG-INFO +62 -0
  4. modvet-0.2.0/README.md +53 -0
  5. modvet-0.2.0/modvet/__init__.py +3 -0
  6. modvet-0.2.0/modvet/badge.py +69 -0
  7. modvet-0.2.0/modvet/cli.py +159 -0
  8. modvet-0.2.0/modvet/core/__init__.py +19 -0
  9. modvet-0.2.0/modvet/core/engine.py +2351 -0
  10. modvet-0.2.0/modvet/core/mapping-patterns.csv +237 -0
  11. modvet-0.2.0/modvet/detectors_extra.py +226 -0
  12. modvet-0.2.0/modvet/mods.py +550 -0
  13. modvet-0.2.0/modvet/sarif.py +68 -0
  14. modvet-0.2.0/pyproject.toml +22 -0
  15. modvet-0.2.0/specs/benchmark-results.md +48 -0
  16. modvet-0.2.0/specs/pattern-audit.md +61 -0
  17. modvet-0.2.0/specs/spec-modvet-v0.1.md +30 -0
  18. modvet-0.2.0/specs/spec-modvet-v0.2-optimierung.md +28 -0
  19. modvet-0.2.0/tests/fixtures/clean-mod/hooks/output.ts +5 -0
  20. modvet-0.2.0/tests/fixtures/clean-mod/plugin.json +6 -0
  21. modvet-0.2.0/tests/fixtures/malicious-mod/.mcp.json +5 -0
  22. modvet-0.2.0/tests/fixtures/malicious-mod/hooks/chat.ts +4 -0
  23. modvet-0.2.0/tests/fixtures/malicious-mod/hooks/install.ts +6 -0
  24. modvet-0.2.0/tests/fixtures/malicious-mod/plugin.json +6 -0
  25. modvet-0.2.0/tests/fixtures/malicious-mod/scripts/setup.sh +4 -0
  26. modvet-0.2.0/tests/fixtures/skill-demo/SKILL.md +12 -0
  27. modvet-0.2.0/tests/run_tests.py +87 -0
  28. modvet-0.2.0/tests/test_badge.py +69 -0
  29. modvet-0.2.0/tests/test_detectors_extra.py +148 -0
  30. modvet-0.2.0/tests/test_fp_refiner.py +127 -0
  31. modvet-0.2.0/tests/test_sarif.py +139 -0
  32. modvet-0.2.0/tools/benchmark.py +58 -0
  33. modvet-0.2.0/tools/fetch_corpus.py +66 -0
  34. modvet-0.2.0/tools/fp_analyse.py +48 -0
  35. modvet-0.2.0/tools/fp_analyse_slow.py +45 -0
  36. modvet-0.2.0/tools/perf_profile.py +27 -0
  37. modvet-0.2.0/tools/rescan_phase2.py +43 -0
@@ -0,0 +1,22 @@
1
+ # ModVet CI-Gate — scannt einen Mod vor jedem Merge.
2
+ # Nutzung: Datei nach <dein-mod-repo>/.github/workflows/modvet.yml kopieren.
3
+ name: modvet-scan
4
+
5
+ on:
6
+ pull_request:
7
+ push:
8
+ branches: [main]
9
+
10
+ jobs:
11
+ modvet:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.12"
18
+ - name: Install ModVet
19
+ run: |
20
+ pip install git+https://github.com/<owner>/modvet.git
21
+ - name: Scan mod
22
+ run: modvet scan . --fail-on HIGH
@@ -0,0 +1,8 @@
1
+ .modvet-snapshot.json
2
+ __pycache__/
3
+ *.pyc
4
+ .venv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ demo-mods/
modvet-0.2.0/PKG-INFO ADDED
@@ -0,0 +1,62 @@
1
+ Metadata-Version: 2.5
2
+ Name: modvet
3
+ Version: 0.2.0
4
+ Summary: ModVet — deterministic pre-install security scanner for Claude Code mods (and later: skills, MCP servers).
5
+ Author: Joerg Michno
6
+ License: Apache-2.0
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+
10
+ # ModVet
11
+
12
+ **Vet your Claude Code mods before you install them — deterministic, offline, in milliseconds.**
13
+
14
+ Mods run unsandboxed inside the agent process with your full permissions. ModVet scans a mod directory for prompt-injection patterns, dangerous capabilities (shell exec, exfiltration, env/file access, permission-lift hooks, update trojans) and hardcoded secrets — no cloud, no LLM, no account. Findings with `file:line`, OWASP MCP Top 10 mapping, JSON/SARIF/human output, CI-ready exit codes.
15
+
16
+ ```
17
+ modvet scan ./my-mod # Pre-Install-Check (human output)
18
+ modvet scan ./my-mod --json # machine-readable report
19
+ modvet scan ./my-mod --format sarif # GitHub Code Scanning kompatibel
20
+ modvet badge --input report.json # Status-Badge als SVG
21
+ modvet snapshot ./my-mod # Baseline nach sauberer Installation
22
+ modvet diff ./my-mod --against .modvet-snapshot.json # "The update is the attack"
23
+ ```
24
+
25
+ Exit codes: `0` clean · `1` findings ≥ `--fail-on` (default HIGH) · `2` usage error.
26
+
27
+ ## Install (dev)
28
+
29
+ ```
30
+ uv pip install -e .
31
+ # oder: python -m modvet.cli scan ./some-mod
32
+ ```
33
+
34
+ ## What it checks
35
+
36
+ - **Prompt Injection / Instruction Override** (EN + DE, ~400 deterministische Pattern inkl. Homoglyph-/Leet-/Base64-/Zero-Width-/Reverse-Normalisierung)
37
+ - **Capability-Kombinationslogik:** Einzel-Capabilities (fetch, process.env, fs) sind normale Mod-Arbeit — erst KOMBINATIONEN im selben Ausdruck (Env-Read + POST, Exec + Fetch) werden gemeldet. Plus Angriffsprimitive: Shell-Pipe-Downloads, Base64-Exec, Reverse Shells, Hook-Permission-Lift, Config-Rewrite, Committed-Env-Files, npm-Lifecycle-Skripte.
38
+ - **Mod-spezifische Detektoren (v0.2):** Prompt-Compose-Rewrite, PreToolUse-Argument-Rewrite, Update-Delta-Trojan.
39
+ - **OWASP MCP Top 10 Mapping:** jedes Finding trägt seine MCP01–MCP10-Referenz (JSON-Feld `owasp_mcp`).
40
+ - **Update-Drift:** `snapshot` + `diff` — re-audits nur das Delta nach jedem Mod-Update. Der Update-Mechanismus selbst ist ein Angriffspfad: „the update is the attack".
41
+
42
+ ## Vergleichstabelle (Stand 10/2026)
43
+
44
+ | | **ModVet** | Snyk (MCP/Skills-Scan) | mcp-triage |
45
+ |---|---|---|---|
46
+ | Fokus | Claude-Code-Mods (+ Skills) | MCP-Server/Skills, breites DevSecOps | MCP-Server-Review |
47
+ | Betrieb | **lokal & offline**, 0 €, kein Konto | Cloud-Konto, SaaS-Pricing | lokal/Repos |
48
+ | Verfahren | deterministisch, keine LLM-Entscheidungen | Hybrid (inkl. proprietäre Analyse) | Review-basiert |
49
+ | Update-Delta | **`snapshot`/`diff` re-auditiert nur Deltas** | Neuer Voll-Scan je Version | — |
50
+ | OWASP-Mapping | MCP Top 10 im Finding-Feld | vorhanden | — |
51
+ | Output | JSON, SARIF, Badge-SVG | SaaS-UI/API | Markdown |
52
+ | Transparenz | jede Regel im Open Source einsehbar | teilweise proprietär | Open Source |
53
+
54
+ ## Benchmark
55
+
56
+ 12 echte Mod-Repos (awesome-claude-code-Katalog): FP-Reduktion von 1.815 auf <300 Findings (−85 %) seit der v0.1-Engine — Details in `specs/benchmark-results.md` und `specs/pattern-audit.md`.
57
+
58
+ ## Status
59
+
60
+ v0.2 — Phase 1 abgeschlossen (FP-Reduktion, Kontext-Engine), Phase 2 (Outputs/Verteilung) im Bau. PyPI-Publish folgt auf Jörg-GO.
61
+
62
+ (C) 2026 Jörg Michno. Engine portiert aus ClawGuard v0.5.0.
modvet-0.2.0/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # ModVet
2
+
3
+ **Vet your Claude Code mods before you install them — deterministic, offline, in milliseconds.**
4
+
5
+ Mods run unsandboxed inside the agent process with your full permissions. ModVet scans a mod directory for prompt-injection patterns, dangerous capabilities (shell exec, exfiltration, env/file access, permission-lift hooks, update trojans) and hardcoded secrets — no cloud, no LLM, no account. Findings with `file:line`, OWASP MCP Top 10 mapping, JSON/SARIF/human output, CI-ready exit codes.
6
+
7
+ ```
8
+ modvet scan ./my-mod # Pre-Install-Check (human output)
9
+ modvet scan ./my-mod --json # machine-readable report
10
+ modvet scan ./my-mod --format sarif # GitHub Code Scanning kompatibel
11
+ modvet badge --input report.json # Status-Badge als SVG
12
+ modvet snapshot ./my-mod # Baseline nach sauberer Installation
13
+ modvet diff ./my-mod --against .modvet-snapshot.json # "The update is the attack"
14
+ ```
15
+
16
+ Exit codes: `0` clean · `1` findings ≥ `--fail-on` (default HIGH) · `2` usage error.
17
+
18
+ ## Install (dev)
19
+
20
+ ```
21
+ uv pip install -e .
22
+ # oder: python -m modvet.cli scan ./some-mod
23
+ ```
24
+
25
+ ## What it checks
26
+
27
+ - **Prompt Injection / Instruction Override** (EN + DE, ~400 deterministische Pattern inkl. Homoglyph-/Leet-/Base64-/Zero-Width-/Reverse-Normalisierung)
28
+ - **Capability-Kombinationslogik:** Einzel-Capabilities (fetch, process.env, fs) sind normale Mod-Arbeit — erst KOMBINATIONEN im selben Ausdruck (Env-Read + POST, Exec + Fetch) werden gemeldet. Plus Angriffsprimitive: Shell-Pipe-Downloads, Base64-Exec, Reverse Shells, Hook-Permission-Lift, Config-Rewrite, Committed-Env-Files, npm-Lifecycle-Skripte.
29
+ - **Mod-spezifische Detektoren (v0.2):** Prompt-Compose-Rewrite, PreToolUse-Argument-Rewrite, Update-Delta-Trojan.
30
+ - **OWASP MCP Top 10 Mapping:** jedes Finding trägt seine MCP01–MCP10-Referenz (JSON-Feld `owasp_mcp`).
31
+ - **Update-Drift:** `snapshot` + `diff` — re-audits nur das Delta nach jedem Mod-Update. Der Update-Mechanismus selbst ist ein Angriffspfad: „the update is the attack".
32
+
33
+ ## Vergleichstabelle (Stand 10/2026)
34
+
35
+ | | **ModVet** | Snyk (MCP/Skills-Scan) | mcp-triage |
36
+ |---|---|---|---|
37
+ | Fokus | Claude-Code-Mods (+ Skills) | MCP-Server/Skills, breites DevSecOps | MCP-Server-Review |
38
+ | Betrieb | **lokal & offline**, 0 €, kein Konto | Cloud-Konto, SaaS-Pricing | lokal/Repos |
39
+ | Verfahren | deterministisch, keine LLM-Entscheidungen | Hybrid (inkl. proprietäre Analyse) | Review-basiert |
40
+ | Update-Delta | **`snapshot`/`diff` re-auditiert nur Deltas** | Neuer Voll-Scan je Version | — |
41
+ | OWASP-Mapping | MCP Top 10 im Finding-Feld | vorhanden | — |
42
+ | Output | JSON, SARIF, Badge-SVG | SaaS-UI/API | Markdown |
43
+ | Transparenz | jede Regel im Open Source einsehbar | teilweise proprietär | Open Source |
44
+
45
+ ## Benchmark
46
+
47
+ 12 echte Mod-Repos (awesome-claude-code-Katalog): FP-Reduktion von 1.815 auf <300 Findings (−85 %) seit der v0.1-Engine — Details in `specs/benchmark-results.md` und `specs/pattern-audit.md`.
48
+
49
+ ## Status
50
+
51
+ v0.2 — Phase 1 abgeschlossen (FP-Reduktion, Kontext-Engine), Phase 2 (Outputs/Verteilung) im Bau. PyPI-Publish folgt auf Jörg-GO.
52
+
53
+ (C) 2026 Jörg Michno. Engine portiert aus ClawGuard v0.5.0.
@@ -0,0 +1,3 @@
1
+ """ModVet — deterministic pre-install security scanner for Claude Code mods."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,69 @@
1
+ """ModVet — deterministischer SVG-Badge-Generator im Shields.io-Stil (stdlib only).
2
+
3
+ Ausführen: from modvet.badge import badge_svg
4
+ """
5
+
6
+ # risk_level -> (Statuswort, Farbe)
7
+ LEVELS = {
8
+ "CLEAN": ("clean", "#4caf50"),
9
+ "LOW": ("low", "#8bc34a"),
10
+ "MEDIUM": ("warn", "#ffc107"),
11
+ "HIGH": ("high", "#ff9800"),
12
+ "CRITICAL": ("critical", "#f44336"),
13
+ }
14
+ UNKNOWN = ("unknown", "#9e9e9e")
15
+
16
+ LABEL = "modvet"
17
+ LABEL_COLOR = "#555555"
18
+ TEXT_COLOR = "#ffffff"
19
+ FONT_SIZE = 11
20
+ CHAR_WIDTH = 7 # ~7px pro Zeichen bei Schriftgröße 11
21
+ PAD_X = 5 # Innenabstand je Segmentrand
22
+ HEIGHT = 20
23
+ RADIUS = 3
24
+
25
+
26
+ def _esc(text):
27
+ """Minimales XML-Escaping für Textinhalte."""
28
+ return str(text).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
29
+
30
+
31
+ def _seg_width(text):
32
+ return CHAR_WIDTH * len(str(text)) + 2 * PAD_X
33
+
34
+
35
+ def badge_svg(risk_level: str, findings: int, source: str = "mod") -> str:
36
+ """Erzeugt ein 2-Segment-SVG-Badge (Label 'modvet' + Status).
37
+
38
+ Deterministisch: gleiche Eingabe -> byte-identische Ausgabe.
39
+ `source` ist für künftige Nutzung reserviert und fließt nicht ins SVG ein.
40
+ """
41
+ key = str(risk_level).strip().upper()
42
+ status_word, status_color = LEVELS.get(key, UNKNOWN)
43
+ status_text = f"{status_word} · {findings}"
44
+
45
+ label_w = _seg_width(LABEL)
46
+ status_w = _seg_width(status_text)
47
+ total_w = label_w + status_w
48
+ label_cx = label_w / 2
49
+ status_cx = label_w + status_w / 2
50
+ baseline = HEIGHT - 6 # 14 bei Höhe 20
51
+
52
+ return (
53
+ f'<svg xmlns="http://www.w3.org/2000/svg" width="{total_w}" '
54
+ f'height="{HEIGHT}" role="img">\n'
55
+ f" <title>modvet: {_esc(status_text)}</title>\n"
56
+ f' <rect width="{total_w}" height="{HEIGHT}" rx="{RADIUS}" '
57
+ f'fill="{LABEL_COLOR}"/>\n'
58
+ f' <rect x="{label_w}" width="{status_w}" height="{HEIGHT}" '
59
+ f'rx="{RADIUS}" fill="{status_color}"/>\n'
60
+ f' <rect x="{label_w}" width="{RADIUS}" height="{HEIGHT}" '
61
+ f'fill="{status_color}"/>\n'
62
+ f' <g fill="{TEXT_COLOR}" text-anchor="middle" '
63
+ f'font-family="Verdana,Geneva,DejaVu Sans,sans-serif" '
64
+ f'font-size="{FONT_SIZE}">\n'
65
+ f' <text x="{label_cx}" y="{baseline}">{LABEL}</text>\n'
66
+ f' <text x="{status_cx}" y="{baseline}">{_esc(status_text)}</text>\n'
67
+ f" </g>\n"
68
+ "</svg>\n"
69
+ )
@@ -0,0 +1,159 @@
1
+ """ModVet CLI — `modvet scan` / `modvet snapshot` / `modvet diff`.
2
+
3
+ Exit-Codes: 0 = clean, 1 = Findings >= --fail-on, 2 = usage/error.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+ import hashlib
9
+ import json
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ from .core.engine import Severity, format_human, format_json
14
+ from .mods import discover_mod_files, scan_mod
15
+
16
+ SNAPSHOT_VERSION = 1
17
+
18
+
19
+ def _manifest(root: Path) -> dict:
20
+ out = {}
21
+ for p in discover_mod_files(root):
22
+ rel = str(p.relative_to(root)).replace("\\", "/")
23
+ try:
24
+ data = p.read_bytes()
25
+ except OSError:
26
+ continue
27
+ out[rel] = {"sha256": hashlib.sha256(data).hexdigest(), "size": len(data)}
28
+ return out
29
+
30
+
31
+ def cmd_scan(args: argparse.Namespace) -> int:
32
+ root = Path(args.path)
33
+ if not root.is_dir():
34
+ print(f"error: not a directory: {root}", file=sys.stderr)
35
+ return 2
36
+ report = scan_mod(root)
37
+ if getattr(args, "format", "human") == "sarif":
38
+ from .sarif import format_sarif
39
+ print(json.dumps(format_sarif(report), indent=2, ensure_ascii=False))
40
+ elif args.json:
41
+ print(format_json(report))
42
+ else:
43
+ print(format_human(report))
44
+ threshold = Severity[args.fail_on.upper()].score()
45
+ return 1 if any(f.severity.score() >= threshold for f in report.findings) else 0
46
+
47
+
48
+ def cmd_badge(args: argparse.Namespace) -> int:
49
+ from .badge import badge_svg
50
+ src = Path(args.input)
51
+ try:
52
+ rep = json.loads(src.read_text(encoding="utf-8"))
53
+ except (OSError, json.JSONDecodeError) as e:
54
+ print(f"error: report unreadable: {e}", file=sys.stderr)
55
+ return 2
56
+ level = rep.get("risk_level", "UNKNOWN")
57
+ findings = int(rep.get("total_findings", 0))
58
+ svg = badge_svg(level, findings)
59
+ out = Path(args.out) if args.out else Path("badge.svg")
60
+ out.write_text(svg, encoding="utf-8")
61
+ print(f"badge: {level} ({findings} findings) -> {out}")
62
+ return 0
63
+
64
+
65
+ def cmd_snapshot(args: argparse.Namespace) -> int:
66
+ root = Path(args.path)
67
+ if not root.is_dir():
68
+ print(f"error: not a directory: {root}", file=sys.stderr)
69
+ return 2
70
+ snap = {"version": SNAPSHOT_VERSION, "files": _manifest(root)}
71
+ out = Path(args.out) if args.out else Path(".modvet-snapshot.json")
72
+ out.write_text(json.dumps(snap, indent=2, sort_keys=True), encoding="utf-8")
73
+ print(f"snapshot: {len(snap['files'])} files -> {out}")
74
+ return 0
75
+
76
+
77
+ def cmd_diff(args: argparse.Namespace) -> int:
78
+ root = Path(args.path)
79
+ if not root.is_dir():
80
+ print(f"error: not a directory: {root}", file=sys.stderr)
81
+ return 2
82
+ try:
83
+ snap = json.loads(Path(args.against).read_text(encoding="utf-8"))
84
+ old = snap["files"]
85
+ except (OSError, KeyError, json.JSONDecodeError) as e:
86
+ print(f"error: snapshot unreadable: {e}", file=sys.stderr)
87
+ return 2
88
+ new = _manifest(root)
89
+ changed = sorted(set(new) - set(old))
90
+ changed += [f for f in sorted(set(new) & set(old)) if new[f]["sha256"] != old[f]["sha256"]]
91
+ removed = sorted(set(old) - set(new))
92
+ print("=== Update-Delta ===")
93
+ for f in changed:
94
+ print(f" CHANGED {f}")
95
+ for f in removed:
96
+ print(f" REMOVED {f}")
97
+ if not changed and not removed:
98
+ print(" (kein Delta — installierte Version identisch mit Snapshot)")
99
+ return 0
100
+ # Findings nur auf geänderten/neuen Dateien
101
+ findings = []
102
+ for rel in changed:
103
+ p = root / rel
104
+ if not p.is_file():
105
+ continue
106
+ try:
107
+ text = p.read_text(encoding="utf-8", errors="replace")
108
+ except OSError:
109
+ continue
110
+ findings.extend(scan_mod_report_on_single_file(rel, text))
111
+ if findings:
112
+ print(f"=== Delta-Findings ({len(findings)}) ===")
113
+ for f in findings:
114
+ print(f" [{f.severity.value}] {f.pattern_name} ({f.category}) {f.line_number}: {f.matched_text[:80]}")
115
+ else:
116
+ print(" Delta-Findings: keine")
117
+ threshold = Severity[args.fail_on.upper()].score()
118
+ return 1 if any(f.severity.score() >= threshold for f in findings) else 0
119
+
120
+
121
+ def scan_mod_report_on_single_file(rel: str, text: str):
122
+ """Engine-Scan + Capability-Kombinationslogik für genau eine Datei (Delta-Pfad)."""
123
+ from .mods import scan_single_file
124
+ return scan_single_file(rel, text)
125
+
126
+
127
+ def main(argv: list[str] | None = None) -> int:
128
+ parser = argparse.ArgumentParser(prog="modvet", description="ModVet — pre-install security scanner for Claude Code mods")
129
+ sub = parser.add_subparsers(dest="cmd", required=True)
130
+
131
+ p_scan = sub.add_parser("scan", help="Scan a mod directory")
132
+ p_scan.add_argument("path")
133
+ p_scan.add_argument("--json", action="store_true", help="JSON output")
134
+ p_scan.add_argument("--format", choices=["human", "sarif"], default="human", help="Output format (SARIF 2.1.0 for GitHub Code Scanning)")
135
+ p_scan.add_argument("--fail-on", default="HIGH", choices=["LOW", "MEDIUM", "HIGH", "CRITICAL"])
136
+ p_scan.set_defaults(fn=cmd_scan)
137
+
138
+ p_badge = sub.add_parser("badge", help="Generate a status badge SVG from a scan report")
139
+ p_badge.add_argument("--input", required=True, help="modvet scan --json report file")
140
+ p_badge.add_argument("--out", help="output .svg path (default: badge.svg)")
141
+ p_badge.set_defaults(fn=cmd_badge)
142
+
143
+ p_snap = sub.add_parser("snapshot", help="Baseline a clean mod install")
144
+ p_snap.add_argument("path")
145
+ p_snap.add_argument("--out", help="snapshot file (default: .modvet-snapshot.json)")
146
+ p_snap.set_defaults(fn=cmd_snapshot)
147
+
148
+ p_diff = sub.add_parser("diff", help="Diff installed mod against snapshot")
149
+ p_diff.add_argument("path")
150
+ p_diff.add_argument("--against", required=True, help="snapshot file")
151
+ p_diff.add_argument("--fail-on", default="HIGH", choices=["LOW", "MEDIUM", "HIGH", "CRITICAL"])
152
+ p_diff.set_defaults(fn=cmd_diff)
153
+
154
+ args = parser.parse_args(argv)
155
+ return args.fn(args)
156
+
157
+
158
+ if __name__ == "__main__":
159
+ sys.exit(main())
@@ -0,0 +1,19 @@
1
+ """Core engine (ported ClawGuard scanner: deterministic patterns, offline, no LLM)."""
2
+
3
+ from .engine import (
4
+ Finding,
5
+ ScanReport,
6
+ Severity,
7
+ scan_text,
8
+ format_human,
9
+ format_json,
10
+ )
11
+
12
+ __all__ = [
13
+ "Finding",
14
+ "ScanReport",
15
+ "Severity",
16
+ "scan_text",
17
+ "format_human",
18
+ "format_json",
19
+ ]