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.
- modvet-0.2.0/.github/workflows/modvet.yml +22 -0
- modvet-0.2.0/.gitignore +8 -0
- modvet-0.2.0/PKG-INFO +62 -0
- modvet-0.2.0/README.md +53 -0
- modvet-0.2.0/modvet/__init__.py +3 -0
- modvet-0.2.0/modvet/badge.py +69 -0
- modvet-0.2.0/modvet/cli.py +159 -0
- modvet-0.2.0/modvet/core/__init__.py +19 -0
- modvet-0.2.0/modvet/core/engine.py +2351 -0
- modvet-0.2.0/modvet/core/mapping-patterns.csv +237 -0
- modvet-0.2.0/modvet/detectors_extra.py +226 -0
- modvet-0.2.0/modvet/mods.py +550 -0
- modvet-0.2.0/modvet/sarif.py +68 -0
- modvet-0.2.0/pyproject.toml +22 -0
- modvet-0.2.0/specs/benchmark-results.md +48 -0
- modvet-0.2.0/specs/pattern-audit.md +61 -0
- modvet-0.2.0/specs/spec-modvet-v0.1.md +30 -0
- modvet-0.2.0/specs/spec-modvet-v0.2-optimierung.md +28 -0
- modvet-0.2.0/tests/fixtures/clean-mod/hooks/output.ts +5 -0
- modvet-0.2.0/tests/fixtures/clean-mod/plugin.json +6 -0
- modvet-0.2.0/tests/fixtures/malicious-mod/.mcp.json +5 -0
- modvet-0.2.0/tests/fixtures/malicious-mod/hooks/chat.ts +4 -0
- modvet-0.2.0/tests/fixtures/malicious-mod/hooks/install.ts +6 -0
- modvet-0.2.0/tests/fixtures/malicious-mod/plugin.json +6 -0
- modvet-0.2.0/tests/fixtures/malicious-mod/scripts/setup.sh +4 -0
- modvet-0.2.0/tests/fixtures/skill-demo/SKILL.md +12 -0
- modvet-0.2.0/tests/run_tests.py +87 -0
- modvet-0.2.0/tests/test_badge.py +69 -0
- modvet-0.2.0/tests/test_detectors_extra.py +148 -0
- modvet-0.2.0/tests/test_fp_refiner.py +127 -0
- modvet-0.2.0/tests/test_sarif.py +139 -0
- modvet-0.2.0/tools/benchmark.py +58 -0
- modvet-0.2.0/tools/fetch_corpus.py +66 -0
- modvet-0.2.0/tools/fp_analyse.py +48 -0
- modvet-0.2.0/tools/fp_analyse_slow.py +45 -0
- modvet-0.2.0/tools/perf_profile.py +27 -0
- 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
|
modvet-0.2.0/.gitignore
ADDED
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,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("&", "&").replace("<", "<").replace(">", ">")
|
|
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
|
+
]
|