diff --git a/skills/kvb-check-security/SKILL.md b/skills/kvb-check-security/SKILL.md index ddb9068..b739bca 100644 --- a/skills/kvb-check-security/SKILL.md +++ b/skills/kvb-check-security/SKILL.md @@ -47,12 +47,12 @@ value. Pass `` as `--security` and the critical count as `--critical` `--security '~'` (quoted: zsh expands a bare `~100` as a user's home) and the critical findings you found as `--critical`. - Exit 2: usage error. - **Accepted false positives**: `security.py accept --repo --rule - --path --sha1 --reason ""`, rule, path and - sha1 from `--json`, appends a row outside the repository: to the council - root, which the `kvb-org-organize` watch hook keeps in - `~/.local/state/agents-council/`. A match leaves the score and shows as - ` accepted`; editing the line brings it back. Accept only after reading - the rule's pattern, and name each acceptance in the commit message. + [--rule ...] --path --sha1 --reason ""` + (from `--json`) appends a row per rule to the council root, which the + `kvb-org-organize` watch hook mirrors to `~/.local/state/agents-council/`. + A match leaves the score as ` accepted` until the line changes. Accept + only after `kvb-think-criticize` judged it a false positive or by design; + name each acceptance in the commit message. ## Rules diff --git a/skills/kvb-check-security/scripts/security.py b/skills/kvb-check-security/scripts/security.py index 17e0630..42580a6 100755 --- a/skills/kvb-check-security/scripts/security.py +++ b/skills/kvb-check-security/scripts/security.py @@ -6,9 +6,9 @@ Args: --base: git ref the change starts from. --json: print the figures as JSON instead of the line. - accept --repo --rule --path --sha1 <40 hex> --reason : - record a reviewed false positive instead; --json of a scan prints rule, path - and sha1 of every finding. + accept --repo --rule [--rule ...] --path --sha1 <40 hex> --reason : + record reviewed false positives of one line instead, a row per rule; --json + of a scan prints rule, path and sha1 of every finding. Scan: The regular files `git diff ` names plus untracked ones, symlinks left @@ -151,25 +151,29 @@ def accepted(listings: tuple[Path, ...]) -> set[tuple[str, str, str]]: def accept(argv: list[str]) -> int: parser = argparse.ArgumentParser(prog="security.py accept", description="Record a reviewed false positive.") - for flag in ("--repo", "--rule", "--path", "--sha1", "--reason"): + for flag in ("--repo", "--path", "--sha1", "--reason"): parser.add_argument(flag, required=True) + parser.add_argument("--rule", action="append", required=True) args = parser.parse_args(argv) if not SHA1.fullmatch(args.sha1): parser.error("--sha1 must be 40 lowercase hex digits") if not args.reason.strip() or "\n" in args.reason: parser.error("--reason must be one non-empty line") root = git(Path(args.repo), "rev-parse", "--show-toplevel").strip() - key = f"{args.rule}\t{args.path}\t{args.sha1}" - row = { - "id": hashlib.sha1(key.encode()).hexdigest(), "rule": args.rule, "path": args.path, "sha1": args.sha1, - "reason": args.reason, "repo": root, "date": datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds"), - } + date = datetime.datetime.now(datetime.UTC).isoformat(timespec="seconds") + rows = [ + { + "id": hashlib.sha1(f"{rule}\t{args.path}\t{args.sha1}".encode()).hexdigest(), "rule": rule, + "path": args.path, "sha1": args.sha1, "reason": args.reason, "repo": root, "date": date, + } + for rule in args.rule + ] target = watch().ACCEPTED target.parent.mkdir(parents=True, exist_ok=True) with target.open("a", encoding="utf-8") as listing: fcntl.flock(listing, fcntl.LOCK_EX) - listing.write(json.dumps(row) + "\n") - print(f"accepted {args.rule} {args.path} ({args.sha1[:12]}) in {target}") + listing.write("".join(json.dumps(row) + "\n" for row in rows)) + print(f"accepted {', '.join(args.rule)} {args.path} ({args.sha1[:12]}) in {target}") return 0 diff --git a/tests/test_security.py b/tests/test_security.py index 30d0a87..1035370 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -163,6 +163,18 @@ class TestSecurity(unittest.TestCase): self.assertEqual(result.returncode, 2) self.assertIn("--sha1 must be 40 lowercase hex digits", result.stderr) + def test_one_accept_takes_every_rule_of_a_line(self): + line = "z = both() # MARK-HIGH MARK-LOW" + self.write("app.py", f"{line}\n") + sha1 = hashlib.sha1(line.encode()).hexdigest() + result = self.accept( + "--rule", "FAKE-HIGH", "--rule", "FAKE-LOW", "--path", "app.py", "--sha1", sha1, "--reason", "fixture" + ) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("accepted FAKE-HIGH, FAKE-LOW app.py", result.stdout) + result = self.scan(env=self.env) + self.assertEqual(result.stdout, "🛡️ security 100 % (medusa: 0 findings, 2 accepted in 1 file)\n") + 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)