feat(check): security value, commit tables, MEDUSA install and kvb-think-choice
kvb-check-security gains security.py: it scans the changed files with MEDUSA, weights findings (critical -50, high -25, medium -10, low -2) and prints locations and counts only; without MEDUSA it asks for a review by hand passed as '~<n>'. kvb-check-score takes --security and --critical: the score multiplies by security/100, a critical finding blocks the commit (exit 4), security below 100 asks the operator like efficiency or speed over 100, and a 90/5 failure now lists every cause. --table and --commit-table print the values as markdown tables, shown before and after every commit. make install installs MEDUSA with pipx unless it is on the PATH. The new kvb-think-choice skill offers open decisions as multiple choice and checkboxes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0343a2f3ce
commit
62a0bcf7e9
12 files changed
+786
-123
No files matched your search
+167
-51
@@ -1,103 +1,219 @@
|
||||
"""Exercise score.py of kvb-check-score: formula, dash and new values, capping, every gate verdict and exit code."""
|
||||
"""Exercise score.py of kvb-check-score: formula, dash and new values, security, capping, gate verdicts, tables."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SCORE = REPO_ROOT / "skills" / "kvb-check-score" / "scripts" / "score.py"
|
||||
PASS = "✅ gate: 90/5, efficiency and speed <= 100"
|
||||
PASS = "✅ gate: 90/5, efficiency and speed <= 100, security 100 %"
|
||||
BASE = {"fix": "95", "break": "2", "efficiency": "100", "speed": "100", "security": "100", "critical": "0"}
|
||||
HEADER = "| 🎯 Fix | 💥 Break | 🌱 Efficiency | ⚡ Speed | 🛡️ Security | 🏆 Score | Gate |"
|
||||
ASK = ": ask the operator before the commit"
|
||||
ENV = {"PATH": os.environ["PATH"]}
|
||||
|
||||
|
||||
def flags(values: dict[str, str]) -> list[str]:
|
||||
return [item for key, value in values.items() for item in (f"--{key}", value)]
|
||||
|
||||
|
||||
class TestScore(unittest.TestCase):
|
||||
def _run(self, *args: str) -> subprocess.CompletedProcess:
|
||||
return subprocess.run([sys.executable, str(SCORE), *args], capture_output=True, text=True, check=False)
|
||||
def _run(self, *args: str, cwd: Path) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(
|
||||
[sys.executable, str(SCORE), *args], cwd=cwd, env=ENV, capture_output=True, text=True, check=False
|
||||
)
|
||||
|
||||
def _score(self, fix: str, risk: str, efficiency: str, speed: str) -> tuple[list[str], int]:
|
||||
result = self._run("--fix", fix, "--break", risk, "--efficiency", efficiency, "--speed", speed)
|
||||
def _score(self, **values: str) -> tuple[list[str], int]:
|
||||
result = self._run(*flags({**BASE, **values}), cwd=REPO_ROOT)
|
||||
self.assertEqual(result.stderr, "")
|
||||
return result.stdout.splitlines(), result.returncode
|
||||
|
||||
def test_formula_and_line_format(self):
|
||||
self.assertEqual(
|
||||
self._score("95", "4", "100", "100"),
|
||||
(["🎯 fix 95 % · 💥 break 4 % · 🌱 efficiency 100 % · ⚡ speed 100 % · 🏆 score 91", PASS], 0),
|
||||
self._score(fix="95", **{"break": "4"}),
|
||||
(["🎯 fix 95 % · 💥 break 4 % · 🌱 efficiency 100 % · ⚡ speed 100 % · 🛡️ security 100 % · 🏆 score 91",
|
||||
PASS], 0),
|
||||
)
|
||||
lines, code = self._score(efficiency="97", speed="-")
|
||||
self.assertEqual(
|
||||
lines[0], "🎯 fix 95 % · 💥 break 2 % · 🌱 efficiency 97 % · ⚡ speed – · 🛡️ security 100 % · 🏆 score 96"
|
||||
)
|
||||
lines, code = self._score("95", "2", "97", "-")
|
||||
self.assertEqual(lines[0], "🎯 fix 95 % · 💥 break 2 % · 🌱 efficiency 97 % · ⚡ speed – · 🏆 score 96")
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(self._score("92.5", "0", "100", "100")[0][0].split(" · ")[0], "🎯 fix 92.5 %")
|
||||
self.assertEqual(self._score(fix="92.5")[0][0].split(" · ")[0], "🎯 fix 92.5 %")
|
||||
|
||||
def test_dash_values_count_as_100(self):
|
||||
lines, code = self._score(fix="92", efficiency="-", speed="-", **{"break": "3"})
|
||||
self.assertEqual(
|
||||
self._score("92", "3", "-", "-"),
|
||||
(["🎯 fix 92 % · 💥 break 3 % · 🌱 efficiency – · ⚡ speed – · 🏆 score 89", PASS], 0),
|
||||
(lines, code),
|
||||
(["🎯 fix 92 % · 💥 break 3 % · 🌱 efficiency – · ⚡ speed – · 🛡️ security 100 % · 🏆 score 89", PASS], 0),
|
||||
)
|
||||
|
||||
def test_security_multiplies_and_asks_below_100(self):
|
||||
lines, code = self._score(fix="100", efficiency="-", speed="-", security="75", **{"break": "0"})
|
||||
self.assertEqual(
|
||||
(lines, code),
|
||||
(["🎯 fix 100 % · 💥 break 0 % · 🌱 efficiency – · ⚡ speed – · 🛡️ security 75 % · 🏆 score 75",
|
||||
f"⚠️ gate: security 75 % < 100{ASK}"], 3),
|
||||
)
|
||||
lines, code = self._score(security="~80")
|
||||
self.assertIn("🛡️ security ~80 % · 🏆 score 74", lines[0])
|
||||
self.assertEqual((lines[1], code), (f"⚠️ gate: security ~80 % < 100{ASK}", 3))
|
||||
self.assertEqual(self._score(security="~100")[1], 0)
|
||||
|
||||
def test_score_rounds_half_up(self):
|
||||
for args, total in (
|
||||
(("94.5", "0", "-", "-"), 95),
|
||||
(("90", "5", "100", "100"), 86),
|
||||
(("91", "5", "-", "-"), 86),
|
||||
(("97", "0", "97", "-"), 100),
|
||||
for values, total in (
|
||||
({"fix": "94.5", "break": "0"}, 95),
|
||||
({"fix": "90", "break": "5"}, 86),
|
||||
({"fix": "91", "break": "5"}, 86),
|
||||
({"fix": "97", "break": "0", "efficiency": "97"}, 100),
|
||||
):
|
||||
with self.subTest(args=args):
|
||||
self.assertTrue(self._score(*args)[0][0].endswith(f"🏆 score {total}"))
|
||||
with self.subTest(values=values):
|
||||
self.assertTrue(self._score(**values)[0][0].endswith(f"🏆 score {total}"))
|
||||
|
||||
def test_score_is_capped(self):
|
||||
lines, code = self._score("100", "0", "50", "50")
|
||||
lines, code = self._score(fix="100", efficiency="50", speed="50", **{"break": "0"})
|
||||
self.assertTrue(lines[0].endswith("🏆 score 100"))
|
||||
self.assertEqual((lines[1], code), (PASS, 0))
|
||||
lines, code = self._score("0", "100", "-", "-")
|
||||
lines, code = self._score(fix="0", **{"break": "100"})
|
||||
self.assertTrue(lines[0].endswith("🏆 score 0"))
|
||||
self.assertEqual(code, 1)
|
||||
|
||||
def test_boundaries_pass(self):
|
||||
self.assertEqual(self._score("90", "5", "100", "100")[1], 0)
|
||||
self.assertEqual(self._score(fix="90", **{"break": "5"})[1], 0)
|
||||
|
||||
def test_90_5_failures_exit_1(self):
|
||||
for args, verdict in (
|
||||
(("89.5", "1", "-", "-"), "⛔ gate: fix 89.5 % < 90"),
|
||||
(("95", "6", "-", "-"), "⛔ gate: break 6 % > 5"),
|
||||
(("80", "10", "-", "-"), "⛔ gate: fix 80 % < 90, break 10 % > 5"),
|
||||
(("85", "2", "150", "new"), "⛔ gate: fix 85 % < 90"),
|
||||
for values, verdict in (
|
||||
({"fix": "89.5", "break": "1"}, "⛔ gate: fix 89.5 % < 90"),
|
||||
({"break": "6"}, "⛔ gate: break 6 % > 5"),
|
||||
({"fix": "80", "break": "10"}, "⛔ gate: fix 80 % < 90, break 10 % > 5"),
|
||||
(
|
||||
{"fix": "85", "efficiency": "150", "speed": "new", "security": "50"},
|
||||
f"⛔ gate: fix 85 % < 90; efficiency 150 % > 100, speed new, security 50 % < 100{ASK}",
|
||||
),
|
||||
):
|
||||
with self.subTest(args=args):
|
||||
lines, code = self._score(*args)
|
||||
with self.subTest(values=values):
|
||||
lines, code = self._score(**values)
|
||||
self.assertEqual((lines[1], code), (verdict, 1))
|
||||
|
||||
def test_cost_over_100_asks_the_operator_exit_3(self):
|
||||
cases = {
|
||||
("120", "100"): "⚠️ gate: efficiency 120 % > 100: ask the operator before the commit",
|
||||
("-", "130"): "⚠️ gate: speed 130 % > 100: ask the operator before the commit",
|
||||
("120", "130"): "⚠️ gate: efficiency 120 % > 100, speed 130 % > 100: ask the operator before the commit",
|
||||
("100.5", "-"): "⚠️ gate: efficiency 100.5 % > 100: ask the operator before the commit",
|
||||
("new", "-"): "⚠️ gate: efficiency new: ask the operator before the commit",
|
||||
}
|
||||
for (efficiency, speed), verdict in cases.items():
|
||||
with self.subTest(efficiency=efficiency, speed=speed):
|
||||
lines, code = self._score("95", "2", efficiency, speed)
|
||||
def test_costs_ask_the_operator_exit_3(self):
|
||||
for values, verdict in (
|
||||
({"efficiency": "120"}, f"⚠️ gate: efficiency 120 % > 100{ASK}"),
|
||||
({"efficiency": "-", "speed": "130"}, f"⚠️ gate: speed 130 % > 100{ASK}"),
|
||||
({"efficiency": "100.5", "speed": "-"}, f"⚠️ gate: efficiency 100.5 % > 100{ASK}"),
|
||||
({"efficiency": "new", "speed": "-"}, f"⚠️ gate: efficiency new{ASK}"),
|
||||
(
|
||||
{"efficiency": "120", "speed": "130", "security": "90"},
|
||||
f"⚠️ gate: efficiency 120 % > 100, speed 130 % > 100, security 90 % < 100{ASK}",
|
||||
),
|
||||
):
|
||||
with self.subTest(values=values):
|
||||
lines, code = self._score(**values)
|
||||
self.assertEqual((lines[1], code), (verdict, 3))
|
||||
|
||||
def test_critical_finding_wins_exit_4(self):
|
||||
for values in (
|
||||
{"critical": "1", "security": "50"},
|
||||
{"critical": "2", "fix": "80", "break": "10", "efficiency": "150", "security": "0"},
|
||||
):
|
||||
with self.subTest(values=values):
|
||||
lines, code = self._score(**values)
|
||||
self.assertEqual((lines[1], code), ("⛔ gate: critical security finding: do not commit", 4))
|
||||
|
||||
def test_new_counts_as_100_in_the_score(self):
|
||||
lines, _ = self._score("95", "0", "new", "-")
|
||||
self.assertEqual(lines[0], "🎯 fix 95 % · 💥 break 0 % · 🌱 efficiency new · ⚡ speed – · 🏆 score 95")
|
||||
lines, _ = self._score(efficiency="new", speed="-", **{"break": "0"})
|
||||
self.assertEqual(
|
||||
lines[0], "🎯 fix 95 % · 💥 break 0 % · 🌱 efficiency new · ⚡ speed – · 🛡️ security 100 % · 🏆 score 95"
|
||||
)
|
||||
|
||||
def test_table(self):
|
||||
result = self._run(*flags({**BASE, "efficiency": "97", "speed": "-"}), "--table", cwd=REPO_ROOT)
|
||||
self.assertEqual(
|
||||
result.stdout.splitlines(),
|
||||
[
|
||||
HEADER,
|
||||
"| --- | --- | --- | --- | --- | --- | --- |",
|
||||
"| 95 % | 2 % | 97 % | – | 100 % | 96 | ✅ 90/5, efficiency and speed <= 100, security 100 % |",
|
||||
],
|
||||
)
|
||||
self.assertEqual(result.returncode, 0)
|
||||
result = self._run(*flags({**BASE, "critical": "1"}), "--table", cwd=REPO_ROOT)
|
||||
self.assertTrue(result.stdout.splitlines()[2].endswith("| ⛔ critical security finding: do not commit |"))
|
||||
self.assertEqual(result.returncode, 4)
|
||||
|
||||
def test_commit_table(self):
|
||||
tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmp.cleanup)
|
||||
repo = Path(tmp.name) / "repo"
|
||||
repo.mkdir()
|
||||
env = {
|
||||
"PATH": os.path.dirname(shutil.which("git")),
|
||||
"HOME": tmp.name,
|
||||
"GIT_CONFIG_NOSYSTEM": "1",
|
||||
"GIT_AUTHOR_NAME": "Test",
|
||||
"GIT_AUTHOR_EMAIL": "test@example.org",
|
||||
"GIT_COMMITTER_NAME": "Test",
|
||||
"GIT_COMMITTER_EMAIL": "test@example.org",
|
||||
}
|
||||
|
||||
def git(*args: str) -> str:
|
||||
return subprocess.run(
|
||||
["git", *args], cwd=repo, env=env, capture_output=True, text=True, check=True
|
||||
).stdout.strip()
|
||||
|
||||
git("init", "-q", "-b", "main")
|
||||
(repo / "a.txt").write_text("one\n", encoding="utf-8")
|
||||
git("add", "a.txt")
|
||||
git("commit", "-qm", "base")
|
||||
(repo / "a.txt").write_text("two\n", encoding="utf-8")
|
||||
(repo / "b.txt").write_text("three\nfour\n", encoding="utf-8")
|
||||
git("add", "a.txt", "b.txt")
|
||||
head = ["📁 Worktree", "🌿 Branch", "📊 Diff stat"]
|
||||
score = "| 95 % | 2 % | 100 % | 100 % | 100 % | 93 | ✅ 90/5, efficiency and speed <= 100, security 100 % |"
|
||||
|
||||
result = self._run(*flags(BASE), "--commit-table", str(repo), cwd=repo)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(
|
||||
result.stdout.splitlines(),
|
||||
[
|
||||
"| " + " | ".join(head) + " " + HEADER,
|
||||
"|" + " --- |" * 10,
|
||||
f"| {repo.resolve()} | main | 2 files +3/-1 " + score,
|
||||
],
|
||||
)
|
||||
git("commit", "-qm", "change")
|
||||
sha = git("rev-parse", "--short", "HEAD")
|
||||
result = self._run(*flags(BASE), "--commit-table", str(repo), "--commit", "HEAD", cwd=repo)
|
||||
self.assertEqual(result.stdout.splitlines()[0], "| " + " | ".join([*head, "🔖 Commit"]) + " " + HEADER)
|
||||
self.assertEqual(result.stdout.splitlines()[2], f"| {repo.resolve()} | main | 2 files +3/-1 | {sha} " + score)
|
||||
git("checkout", "-q", "--detach", "HEAD")
|
||||
result = self._run(*flags(BASE), "--commit-table", str(repo), cwd=repo)
|
||||
self.assertEqual(result.stdout.splitlines()[2], f"| {repo.resolve()} | detached at {sha} | 0 files +0/-0 " + score)
|
||||
self.assertEqual(self._run(*flags(BASE), "--commit-table", tmp.name, cwd=repo).returncode, 2)
|
||||
|
||||
def test_usage_errors_exit_2(self):
|
||||
good = {"--fix": "95", "--break": "2", "--efficiency": "100", "--speed": "100"}
|
||||
bad = (
|
||||
("--fix", "101"), ("--fix", "-1"), ("--fix", "nan"), ("--break", "x"), ("--break", "-"),
|
||||
("--efficiency", "0"), ("--efficiency", "-5"), ("--efficiency", "abc"), ("--speed", "inf"),
|
||||
("fix", "101"), ("fix", "-1"), ("fix", "nan"), ("break", "x"), ("break", "-"),
|
||||
("efficiency", "0"), ("efficiency", "-5"), ("efficiency", "abc"), ("speed", "inf"),
|
||||
("security", "101"), ("security", "~"), ("security", "~x"), ("security", "-"),
|
||||
("critical", "-1"), ("critical", "a"), ("critical", "1.5"),
|
||||
)
|
||||
for flag, value in bad:
|
||||
with self.subTest(flag=flag, value=value):
|
||||
args = [item for key, val in {**good, flag: value}.items() for item in (key, val)]
|
||||
result = self._run(*args)
|
||||
for key, value in bad:
|
||||
with self.subTest(flag=key, value=value):
|
||||
result = self._run(*flags({**BASE, key: value}), cwd=REPO_ROOT)
|
||||
self.assertEqual((result.returncode, result.stdout), (2, ""))
|
||||
self.assertEqual(self._run("--fix", "95", "--break", "2", "--efficiency", "100").returncode, 2)
|
||||
for missing in ("security", "critical"):
|
||||
with self.subTest(missing=missing):
|
||||
values = {k: v for k, v in BASE.items() if k != missing}
|
||||
self.assertEqual(self._run(*flags(values), cwd=REPO_ROOT).returncode, 2)
|
||||
for extra in (("--commit", "HEAD"), ("--table", "--commit-table", str(REPO_ROOT))):
|
||||
with self.subTest(extra=extra):
|
||||
self.assertEqual(self._run(*flags(BASE), *extra, cwd=REPO_ROOT).returncode, 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
"""Exercise security.py of kvb-check-security against a fake medusa: weights, locations, exit codes, hand review."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SECURITY = REPO_ROOT / "skills" / "kvb-check-security" / "scripts" / "security.py"
|
||||
GIT_DIR = os.path.dirname(shutil.which("git"))
|
||||
PLACEHOLDER = "placeholder-value-never-shown"
|
||||
FAKE = """#!{python}
|
||||
import json, os, pathlib, sys
|
||||
args = sys.argv[1:]
|
||||
pathlib.Path(__file__).with_name("argv.json").write_text(json.dumps(args))
|
||||
target, out = pathlib.Path(args[1]), pathlib.Path(args[args.index("-o") + 1])
|
||||
out.mkdir(parents=True)
|
||||
findings = []
|
||||
for path in sorted(p for p in target.rglob("*") if p.is_file()):
|
||||
for number, text in enumerate(path.read_text().splitlines(), 1):
|
||||
for severity in ("CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"):
|
||||
if "MARK-" + severity in text:
|
||||
findings.append({{"severity": severity, "file": str(path), "line": number, "code": text}})
|
||||
report = {{"issues": findings}} if os.environ.get("FAKE_MEDUSA_SHAPE") == "other" else {{"findings": findings}}
|
||||
(out / "medusa-scan-20261104-120000.json").write_text(json.dumps(report))
|
||||
"""
|
||||
HAND = "review trust boundaries, secrets and injection in the diff by hand, then pass --security '~<n>' to kvb-check-score"
|
||||
|
||||
|
||||
class TestSecurity(unittest.TestCase):
|
||||
def setUp(self):
|
||||
tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(tmp.cleanup)
|
||||
self.base = Path(tmp.name)
|
||||
self.bin = self.base / "bin"
|
||||
self.bin.mkdir()
|
||||
medusa = self.bin / "medusa"
|
||||
medusa.write_text(FAKE.format(python=sys.executable), encoding="utf-8")
|
||||
medusa.chmod(0o755)
|
||||
self.repo = self.base / "repo"
|
||||
self.repo.mkdir()
|
||||
self.env = {
|
||||
"PATH": f"{self.bin}:{GIT_DIR}",
|
||||
"HOME": str(self.base),
|
||||
"GIT_CONFIG_NOSYSTEM": "1",
|
||||
"GIT_AUTHOR_NAME": "Test",
|
||||
"GIT_AUTHOR_EMAIL": "test@example.org",
|
||||
"GIT_COMMITTER_NAME": "Test",
|
||||
"GIT_COMMITTER_EMAIL": "test@example.org",
|
||||
}
|
||||
self.git("init", "-q", "-b", "main")
|
||||
self.write("app.py", "print(1)\n")
|
||||
self.write("old.py", "x = 1 # MARK-CRITICAL\n")
|
||||
self.write("gone.py", "y = 2 # MARK-HIGH\n")
|
||||
self.git("add", "-A")
|
||||
self.git("commit", "-qm", "base")
|
||||
|
||||
def git(self, *args: str) -> None:
|
||||
subprocess.run(["git", *args], cwd=self.repo, env=self.env, capture_output=True, check=True)
|
||||
|
||||
def write(self, path: str, text: str) -> None:
|
||||
(self.repo / path).write_text(text, encoding="utf-8")
|
||||
|
||||
def scan(self, *extra: str, env: dict[str, str]) -> subprocess.CompletedProcess:
|
||||
return subprocess.run(
|
||||
[sys.executable, str(SECURITY), "--repo", str(self.repo), "--base", "HEAD", *extra],
|
||||
cwd=self.base, env=env, capture_output=True, text=True, check=False,
|
||||
)
|
||||
|
||||
def test_changed_and_untracked_files_are_weighted(self):
|
||||
self.write("app.py", f"print(1)\ntoken = '{PLACEHOLDER}' # MARK-HIGH\n")
|
||||
self.write("new.py", "a = 1 # MARK-LOW\nb = 2\nc = 3 # MARK-MEDIUM\nd = 4 # MARK-INFO\n")
|
||||
(self.repo / "gone.py").unlink()
|
||||
(self.repo / "link.py").symlink_to(self.repo / "old.py")
|
||||
result = self.scan(env=self.env)
|
||||
self.assertEqual(
|
||||
result.stdout,
|
||||
"🛡️ security 63 % (medusa: 4 findings in 2 files: high app.py:2, medium new.py:3, low new.py:1, "
|
||||
"info new.py:4)\n",
|
||||
)
|
||||
self.assertEqual(result.returncode, 3, result.stderr)
|
||||
argv = json.loads((self.bin / "argv.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual([argv[0], *argv[2:4], argv[4], argv[6]], ["scan", "--format", "json", "-o", "--no-cache"])
|
||||
result = self.scan("--json", env=self.env)
|
||||
self.assertNotIn(PLACEHOLDER, result.stdout + result.stderr)
|
||||
found = json.loads(result.stdout)
|
||||
self.assertEqual(
|
||||
{k: found[k] for k in ("security", "files", "critical", "high", "medium", "low", "info")},
|
||||
{"security": 63, "files": 2, "critical": 0, "high": 1, "medium": 1, "low": 1, "info": 1},
|
||||
)
|
||||
self.assertEqual(found["findings"][0], {"severity": "high", "path": "app.py", "line": 2})
|
||||
|
||||
def test_critical_exits_1_and_floors_at_0(self):
|
||||
self.write("app.py", "a # MARK-CRITICAL\nb # MARK-CRITICAL\nc # MARK-CRITICAL\n")
|
||||
result = self.scan(env=self.env)
|
||||
self.assertTrue(result.stdout.startswith("🛡️ security 0 % (medusa: 3 findings in 1 file: critical app.py:1, "))
|
||||
self.assertEqual(result.returncode, 1)
|
||||
|
||||
def test_more_than_five_locations_are_cut(self):
|
||||
self.write("app.py", "".join(f"v{n} # MARK-LOW\n" for n in range(7)))
|
||||
result = self.scan(env=self.env)
|
||||
self.assertEqual(
|
||||
result.stdout,
|
||||
"🛡️ security 86 % (medusa: 7 findings in 1 file: low app.py:1, low app.py:2, low app.py:3, "
|
||||
"low app.py:4, low app.py:5, +2 more)\n",
|
||||
)
|
||||
self.assertEqual(result.returncode, 3)
|
||||
|
||||
def test_clean_and_empty_changes_are_100(self):
|
||||
result = self.scan(env=self.env)
|
||||
self.assertEqual((result.stdout, result.returncode), ("🛡️ security 100 % (medusa: 0 findings in 0 files)\n", 0))
|
||||
self.assertFalse((self.bin / "argv.json").exists())
|
||||
self.write("app.py", "print(2)\n")
|
||||
result = self.scan(env=self.env)
|
||||
self.assertEqual((result.stdout, result.returncode), ("🛡️ security 100 % (medusa: 0 findings in 1 file)\n", 0))
|
||||
|
||||
def test_missing_medusa_asks_for_a_review_by_hand(self):
|
||||
if shutil.which("medusa", path=GIT_DIR):
|
||||
self.skipTest("a real medusa sits next to git")
|
||||
self.write("app.py", "print(2)\n")
|
||||
result = self.scan(env={**self.env, "PATH": GIT_DIR})
|
||||
self.assertEqual((result.stdout, result.returncode), (f"🛡️ security – (medusa missing: {HAND})\n", 4))
|
||||
result = self.scan("--json", env={**self.env, "PATH": GIT_DIR})
|
||||
self.assertEqual(json.loads(result.stdout), {"security": None, "problem": "medusa missing"})
|
||||
|
||||
def test_unreadable_report_asks_for_a_review_by_hand(self):
|
||||
self.write("app.py", "print(2)\n")
|
||||
result = self.scan(env={**self.env, "FAKE_MEDUSA_SHAPE": "other"})
|
||||
self.assertEqual(result.returncode, 4)
|
||||
self.assertRegex(result.stdout, r"^🛡️ security – \(medusa report unreadable: .*holds no findings list")
|
||||
|
||||
def test_usage_errors_exit_2(self):
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(SECURITY), "--repo", str(self.repo), "--base", "nope"],
|
||||
env=self.env, capture_output=True, text=True, check=False,
|
||||
)
|
||||
self.assertEqual(result.returncode, 2)
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(SECURITY), "--repo", str(self.base / "absent"), "--base", "HEAD"],
|
||||
env=self.env, capture_output=True, text=True, check=False,
|
||||
)
|
||||
self.assertEqual(result.returncode, 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user