code-guardian-mcp 0.2.0__tar.gz → 0.3.3__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 (59) hide show
  1. {code_guardian_mcp-0.2.0/code_guardian_mcp.egg-info → code_guardian_mcp-0.3.3}/PKG-INFO +221 -37
  2. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/README.md +218 -34
  3. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian/__init__.py +1 -1
  4. code_guardian_mcp-0.3.3/code_guardian/baseline.py +135 -0
  5. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian/catalogue.py +38 -0
  6. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian/cli.py +71 -6
  7. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian/finding.py +19 -13
  8. code_guardian_mcp-0.3.3/code_guardian/history.py +158 -0
  9. code_guardian_mcp-0.3.3/code_guardian/leftovers.py +266 -0
  10. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian/mcp_server.py +80 -20
  11. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian/orphans.py +7 -4
  12. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian/outputs.py +52 -10
  13. code_guardian_mcp-0.3.3/code_guardian/report.py +661 -0
  14. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian/scanners.py +181 -5
  15. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian/score.py +66 -17
  16. code_guardian_mcp-0.3.3/code_guardian/verify.py +247 -0
  17. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian/walk.py +3 -2
  18. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3/code_guardian_mcp.egg-info}/PKG-INFO +221 -37
  19. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian_mcp.egg-info/SOURCES.txt +11 -0
  20. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/pyproject.toml +3 -3
  21. code_guardian_mcp-0.3.3/tests/test_baseline.py +155 -0
  22. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/tests/test_complete.py +4 -1
  23. code_guardian_mcp-0.3.3/tests/test_coverage.py +142 -0
  24. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/tests/test_fixtures.py +6 -3
  25. code_guardian_mcp-0.3.3/tests/test_history.py +139 -0
  26. code_guardian_mcp-0.3.3/tests/test_leftovers.py +160 -0
  27. code_guardian_mcp-0.3.3/tests/test_locations_redact.py +91 -0
  28. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/tests/test_mcp.py +1 -1
  29. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/tests/test_registry_markers.py +8 -0
  30. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/tests/test_selftests.py +1 -1
  31. code_guardian_mcp-0.3.3/tests/test_status.py +139 -0
  32. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/tests/test_tool_annotations.py +2 -1
  33. code_guardian_mcp-0.3.3/tests/test_verify.py +201 -0
  34. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/tests/test_works.py +2 -1
  35. code_guardian_mcp-0.2.0/code_guardian/report.py +0 -383
  36. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/LICENSE +0 -0
  37. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian/__main__.py +0 -0
  38. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian/complete.py +0 -0
  39. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian/data/known_packages.txt.gz +0 -0
  40. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian/explain.py +0 -0
  41. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian/goal.py +0 -0
  42. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian/manifests.py +0 -0
  43. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian/supply.py +0 -0
  44. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian/works.py +0 -0
  45. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian_mcp.egg-info/dependency_links.txt +0 -0
  46. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian_mcp.egg-info/entry_points.txt +0 -0
  47. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/code_guardian_mcp.egg-info/top_level.txt +0 -0
  48. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/setup.cfg +0 -0
  49. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/tests/test_calibration.py +0 -0
  50. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/tests/test_explain.py +0 -0
  51. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/tests/test_outputs.py +0 -0
  52. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/tests/test_readme_sync.py +0 -0
  53. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/tests/test_safe_gaps.py +0 -0
  54. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/tests/test_safety.py +0 -0
  55. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/tests/test_scanners.py +0 -0
  56. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/tests/test_scoring.py +0 -0
  57. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/tests/test_self_scan.py +0 -0
  58. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/tests/test_spdx_license.py +0 -0
  59. {code_guardian_mcp-0.2.0 → code_guardian_mcp-0.3.3}/tests/test_supply.py +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-guardian-mcp
3
- Version: 0.2.0
4
- Summary: Is this codebase safe to run, will it work, and is it complete? Deterministic Safe, Works and Complete scores with an A-F grade, from static analysis only; the code is never run.
3
+ Version: 0.3.3
4
+ Summary: Is this codebase safe to run, will it work, and is it complete? Deterministic Safe, Works and Complete scores with an A-F grade, from static analysis only, plus a verify loop for AI-assisted changes: baseline, repair plan, before/after diff. The code is never run.
5
5
  Author: LuminariSoftwares
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/LuminariSoftwares/code-guardian
8
8
  Project-URL: Issues, https://github.com/LuminariSoftwares/code-guardian/issues
9
9
  Project-URL: Changelog, https://github.com/LuminariSoftwares/code-guardian/blob/main/CHANGELOG.md
10
- Keywords: security,static-analysis,dead-code,supply-chain,secrets,mcp,local-llm
10
+ Keywords: security,static-analysis,dead-code,supply-chain,secrets,mcp,local-llm,ai-code-review,baseline
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Operating System :: OS Independent
13
13
  Classifier: Environment :: Console
@@ -37,14 +37,28 @@ from deterministic checks with fixed weights: same repo, same version, same outp
37
37
  - **Orphan code**: functions, classes and modules nothing reachable from the project's entry points uses, judged
38
38
  against a small goal contract.
39
39
  - An optional **plain-English explanation** from a local model, which never adds, removes or rescores a finding.
40
+ - A **verify loop** for AI-assisted changes: run it before and after every change to learn what the change fixed,
41
+ what it broke, what to fix next, and whether the scan itself can be trusted (see [The verify loop](#the-verify-loop)).
40
42
 
41
43
  It parses the code (`ast` for Python; `json`, `tomllib` and `configparser` for manifests; regular expressions for
42
44
  secrets) and never executes, imports or installs it. A scan makes
43
- no network calls. Python 3.11+, standard library only. Status: **0.2.0 is the current release.** The fourth guardian,
45
+ no network calls. Python 3.11+, standard library only. Status: **0.3.3 is the current release.** The fourth guardian,
44
46
  next to [stack-guardian](https://github.com/LuminariSoftwares/stack-guardian),
45
47
  [context-guardian](https://github.com/LuminariSoftwares/context-guardian) and
46
48
  [tool-guardian](https://github.com/LuminariSoftwares/tool-guardian).
47
49
 
50
+ ## What it analyses
51
+
52
+ **Python is analysed in depth**; every Safe, Works and Complete check runs on it. **Other languages** (JavaScript,
53
+ TypeScript, Go, Rust, shell and the rest) **get the secret scan and the repo-hygiene checks only**: key shapes,
54
+ manifest and install-hook reading (`package.json`, `setup.py`, `pyproject.toml`, ...) and the Complete checks
55
+ (licence, README, CI, `.env.example`). Every report prints a `coverage:` line saying which files were analysed, e.g.
56
+ `coverage: 40 Python analysed; 112 .ts, 9 .go secret-scan only`. When there is no Python source at all, Works is
57
+ **not scored** (`n/a (Python only)`, `null` in JSON), Safe is labelled **partial**, Overall comes from the scored
58
+ dimensions only, and the status is **PARTIAL**. A repo whose source is mostly another language is also PARTIAL with
59
+ the same scores (rule in [Scan status](#scan-status)). PARTIAL means "do not read this grade as covering the whole
60
+ repo"; it never fails `--fail-on` on its own.
61
+
48
62
  ## Install
49
63
 
50
64
  ```
@@ -78,8 +92,10 @@ code-guardian scan risky_repo
78
92
  Real output:
79
93
 
80
94
  ```
81
- code-guardian 0.2.0 scan of risky
95
+ code-guardian 0.3.3 scan of risky
82
96
  files: 7 Python, 3 manifests, 2 other (0 skipped, 0 excluded by the goal file)
97
+ status: COMPLETE
98
+ coverage: 7 Python analysed; 1 .js secret-scan only
83
99
 
84
100
  Safe 0/100 is it safe to run? -237
85
101
  Works 85/100 will it work? -15
@@ -87,11 +103,11 @@ files: 7 Python, 3 manifests, 2 other (0 skipped, 0 excluded by the goal file)
87
103
  Overall 33/100 grade F (a Safe check is FAIL (exec.shell, obfuscation, secret.key, tls.off): the grade is at most D)
88
104
 
89
105
  biggest deductions:
90
- -60 Safe secret.key FAIL x3 first at deploy/id_rsa:1
91
- -40 Safe obfuscation FAIL x1 first at risky/loader.py:7
92
- -30 Safe exec.shell FAIL x1 first at risky/runner.py:7
93
- -25 Safe tls.off FAIL x1 first at risky/network.py:11
94
- -23 Safe install.hook WARN x3 first at package.json:5
106
+ -60 Safe secret.key FAIL x3 at deploy/id_rsa:1, risky/settings.py:1, risky/settings.py:2
107
+ -40 Safe obfuscation FAIL x1 at risky/loader.py:7
108
+ -30 Safe exec.shell FAIL x1 at risky/runner.py:7
109
+ -25 Safe tls.off FAIL x1 at risky/network.py:11
110
+ -23 Safe install.hook WARN x3 at package.json:5, setup.py:5, setup.py:14
95
111
 
96
112
  findings (6 FAIL, 19 WARN, 7 INFO; egress listed below):
97
113
  Safe:
@@ -134,19 +150,17 @@ egress inventory (4):
134
150
 
135
151
  hygiene (inferred (library public API) entry points: 1): 0 dead, 0 test-only, 0 uncertain
136
152
 
137
- notes:
138
- - code-guardian analyses Python only: 1 file(s) in other languages (.js 1) got the secret scan only
139
-
140
153
  weights used (each check (code + level) deducts its weight once, plus a quarter of the weight per extra occurrence (rounded up), at most 2x the weight; INFO is never scored):
141
154
  Safe deps.unpinned INFO 0 · deser.unsafe WARN 10 · exec.dynamic FAIL 30 · exec.dynamic WARN 10 ·
142
155
  exec.shell FAIL 30 · exec.shell WARN 10 · fs.outside WARN 3 · install.hook WARN 15 ·
143
156
  net.bind_all WARN 5 · net.egress INFO 0 · net.egress WARN 2 · obfuscation FAIL 40 ·
144
- obfuscation WARN 10 · secret.key FAIL 40 · secret.key INFO 0 · supply.lookalike WARN 15 ·
145
- supply.unknown_package FAIL 40 · supply.unknown_package INFO 0 · supply.unknown_package WARN 10 ·
146
- tls.off FAIL 25 · tls.off WARN 10
147
- Works orphan.dead WARN 3 · works.entry_missing FAIL 30 · works.import_unresolved WARN 10 ·
148
- works.relative_missing FAIL 25 · works.syntax FAIL 30 · works.syntax INFO 0 ·
149
- works.test_no_assert WARN 5 · works.tests_missing WARN 15
157
+ obfuscation WARN 10 · privacy.pii_log WARN 5 · privacy.pii_prompt WARN 5 · secret.history FAIL 30 ·
158
+ secret.key FAIL 40 · secret.key INFO 0 · supply.lookalike WARN 15 · supply.unknown_package FAIL 40 ·
159
+ supply.unknown_package INFO 0 · supply.unknown_package WARN 10 · tls.off FAIL 25 · tls.off WARN 10
160
+ Works orphan.dead WARN 3 · works.commented_code INFO 0 · works.dead_flag INFO 0 ·
161
+ works.entry_missing FAIL 30 · works.import_unresolved WARN 10 · works.relative_missing FAIL 25 ·
162
+ works.syntax FAIL 30 · works.syntax INFO 0 · works.test_no_assert WARN 5 ·
163
+ works.tests_missing WARN 15 · works.todo_stub WARN 3
150
164
  Complete complete.ci_missing WARN 10 · complete.env_example WARN 10 · complete.license_missing WARN 20 ·
151
165
  complete.license_undeclared WARN 5 · complete.lockfile WARN 10 · complete.metadata WARN 10 ·
152
166
  complete.readme_missing WARN 20 · complete.readme_sections WARN 5 · complete.readme_short WARN 10 ·
@@ -160,13 +174,61 @@ Exit code 1: there are FAIL findings. `--json` prints the same report as JSON (s
160
174
 
161
175
  | exit | meaning |
162
176
  |---|---|
163
- | 0 | nothing at or above `--fail-on` (default `FAIL`) |
177
+ | 0 | nothing at or above `--fail-on` (default `FAIL`), and the scan is COMPLETE (or PARTIAL for coverage only) |
164
178
  | 1 | findings at or above it |
165
179
  | 2 | usage error, a missing PATH, or a bad goal file |
180
+ | 3 | incomplete scan: the status is PARTIAL or INVALID (see [Scan status](#scan-status)) and nothing is at or above `--fail-on`; a PARTIAL that is only about coverage (other languages) never gives 3 |
166
181
 
167
182
  `scan` gates on every scored finding (Safe, Works and Complete); `code-guardian orphans PATH` gates on the hygiene
168
183
  findings.
169
184
 
185
+ ## The verify loop
186
+
187
+ code-guardian answers four questions about a change, so a developer or a coding agent can run it before and after
188
+ every edit:
189
+
190
+ | question | command | MCP tool |
191
+ |---|---|---|
192
+ | Can I trust this scan? | every report's `status`: COMPLETE, PARTIAL (grade capped at B) or INVALID (no grade); exit 3 | `scan` |
193
+ | What should be fixed next? | `code-guardian plan PATH` (each check row's **estimated gain if fixed**, best first) | `plan` |
194
+ | What did the change fix, and what did it break? | `code-guardian diff BEFORE.json AFTER.json` (exit 1 when anything regressed) | `diff` |
195
+ | Is this finding new? | `code-guardian baseline PATH`, then `scan --baseline FILE` (gate on new findings only) | `scan` with `baseline` |
196
+
197
+ An agent's loop, from the shell:
198
+
199
+ ```
200
+ code-guardian scan . --json > before.json # 1. scan: is the scan COMPLETE? what is the grade?
201
+ code-guardian plan . --top 5 # 2. plan: the five fixes worth the most points, with locations
202
+ # 3. fix the top item (the plan never edits code)
203
+ code-guardian scan . --json > after.json # 4. scan again
204
+ code-guardian diff before.json after.json # 5. diff: verdict improved / regressed / mixed / unchanged
205
+ ```
206
+
207
+ ```
208
+ verdict: improved (1 fixed, 0 new, 31 unchanged)
209
+
210
+ before after delta
211
+ Safe 0 0 +0
212
+ Works 85 85 +0
213
+ Complete 35 55 +20
214
+ Overall 33 37 +4
215
+ grade F F
216
+ status COMPLETE -> COMPLETE
217
+
218
+ fixed (1):
219
+ WARN complete.license_missing . no licence file: nobody may legally reuse this code
220
+
221
+ new (0):
222
+ none
223
+
224
+ matched by fingerprint (code, level, file and the evidence line; line numbers ignored); INFO findings never decide the verdict
225
+ ```
226
+
227
+ The same loop over MCP is `scan` → `plan` → edit → `scan` → `diff(before, after)`, where `before` and `after` are the
228
+ two `scan` replies (JSON text) or paths to `scan --json` files. Findings are matched by fingerprint (code, level,
229
+ file and the evidence line, never the line number), so moving code does not count as a change. Everything in the
230
+ loop is deterministic, offline and read-only.
231
+
170
232
  ## How the scores work
171
233
 
172
234
  Every check is a small pure function with its own tests. Every finding has the guardian family's shape (`code`,
@@ -179,30 +241,69 @@ Complete.
179
241
  one minor issue cannot zero a score.
180
242
  - **Dimension score** = max(0, 100 − its deductions). Safe is 100 minus the v0.1 threat score; its weights are the
181
243
  v0.1 weights, unchanged.
182
- - **Overall** = 0.5 × Safe + 0.3 × Works + 0.2 × Complete, rounded half up.
244
+ - **Overall** = 0.5 × Safe + 0.3 × Works + 0.2 × Complete, rounded half up. A dimension that is not scored (Works
245
+ when there is no Python source) is left out and the others keep their relative weights: (5 × Safe + 2 × Complete)
246
+ / 7. The Overall line names the dimension left out.
183
247
  - **Grade**: **A** ≥ 90 · **B** ≥ 75 · **C** ≥ 60 · **D** ≥ 40 · **F** below 40.
184
248
  - **FAIL cap**: any Safe finding at FAIL caps the grade at **D**, whatever the average. One leaked key never hides
185
249
  behind a good README.
186
250
  - INFO findings are never scored. Every report prints the weight table it used.
251
+ - **Every deduction cites its evidence.** Each check row lists every distinct `file:line` it was counted at
252
+ (sorted, at most 20, plus `more`: how many were left out); the text report shows up to three per row.
253
+ - **Nothing secret is printed.** Every output (JSON, text, Markdown, SARIF, MCP) goes through one `redact()`: API-key
254
+ shapes and email addresses are masked as `<redacted key>` / `<redacted email>` wherever they appear (a URL literal,
255
+ a path), and the evidence of `secret.*` and `privacy.*` findings is also stripped of any long token-like value.
187
256
 
188
257
  The weight tables live in `code_guardian/score.py` (`TABLES`); `tests/test_readme_sync.py` fails if the tables in
189
258
  this README drift from them.
190
259
 
260
+ ### Scan status
261
+
262
+ Can I trust this scan? Every report carries a `status`, the reasons for it, and the outcome of every scanner,
263
+ check and the file walk (`ran`, `skipped` with a reason, or `crashed` with the error):
264
+
265
+ | status | when | effect |
266
+ |---|---|---|
267
+ | `COMPLETE` | everything ran | none |
268
+ | `PARTIAL` | a check crashed or was skipped, a Python file did not parse, or a Python file or manifest was too large (over 2 MB) or unreadable | the grade is capped at **B** and the report says why; exit 3 unless a finding already gives exit 1 |
269
+ | `PARTIAL` (coverage) | there is no Python source, or the source is mostly in other languages (rule below) | the grade is capped at **B** and the `coverage:` line says what was not analysed; **the exit code is unchanged** (0 or 1 from the findings, never 3) |
270
+ | `INVALID` | PATH is unreadable, or there are no Python files and no manifests to judge | no grade (`scores.grade` is `null`); exit 3 |
271
+
272
+ **Coverage rule.** Source files are counted by extension (`.js`, `.mjs`, `.ts`, `.tsx`, `.go`, `.rs`, `.java`,
273
+ `.rb`, `.php`, `.sh`, `.c`, `.cs` and other common languages; data, docs and config files are not source). With
274
+ **no Python source**, Works is not scored, Safe is partial (it can only see secrets, manifests and install hooks),
275
+ Overall is the weighted mean of Safe and Complete, and the scan is PARTIAL. With Python present, the scan is PARTIAL
276
+ when there are **at least 5 other-language source files and more than twice as many as Python files** (Python is
277
+ under a third of the source); the scores are computed as usual. A Python project with a few build, docs or shell
278
+ helpers stays COMPLETE: in [calibration](calibration.md) the most JS-heavy repo has 37 `.js`/`.mjs` files to 33
279
+ Python files and is COMPLETE. `tests/test_coverage.py` pins both sides of the line.
280
+
281
+ Binary files, symlinks and large non-source files (images, design files) are only ever secret-scanned; skipping them
282
+ is listed in the notes but does not make a scan PARTIAL.
283
+
191
284
  ### JSON shape (`--json`, and the MCP `scan` tool)
192
285
 
193
286
  ```
194
287
  {
195
- "tool", "version", "path", "goal_file",
288
+ "tool", "version", "schema_version": 2, "path", "goal_file",
289
+ "status": "COMPLETE" | "PARTIAL" | "INVALID", "status_reasons": [...],
290
+ "outcomes": [{"check", "outcome": "ran" | "skipped" | "crashed", "reason"}],
196
291
  "files": {"python", "manifests", "other", "skipped", "ignored", "excluded", "unparsed"},
197
292
  "other_languages": {".js": 1, ...},
293
+ "coverage": {"analysed": "python", "python", "other_source": {".js": 1, ...}, "other_source_files",
294
+ "works_scored", "safe_partial", "partial", "reason"},
198
295
  "anchor": "pyproject.toml", (the file repo-level findings are attached to in SARIF)
199
296
  "scores": {"safe", "works", "complete", "overall", "grade", "grade_uncapped", "capped", "cap_reason"},
200
- "dimensions": {"safe" | "works" | "complete": {"score", "deducted",
201
- "checks": [{"code", "level", "count", "weight", "deduction", "first"}]}},
297
+ (a dimension that is not scored is null)
298
+ "dimensions": {"safe" | "works" | "complete": {"score", "scored", "partial", "deducted",
299
+ "checks": [{"code", "level", "count", "weight", "deduction", "first",
300
+ "locations": [every distinct file:line, sorted, at most 20], "more": N}]}},
202
301
  "top": [the five biggest deductions, each a check row plus "dimension"],
203
302
  "weights": {"safe" | "works" | "complete": {"code LEVEL": weight}},
204
303
  "weights_rule": "...",
205
- "findings": [{"code", "severity", "level", "node", "summary", "evidence", "try", "confidence", "fix"}],
304
+ "findings": [{"code", "severity", "level", "node", "summary", "evidence", "try", "confidence", "fix",
305
+ "fingerprint", "new" (only with --baseline)}],
306
+ "baseline": {"file", "version", "new", "known"} (only with --baseline)
206
307
  "egress": [{"node", "level", "kind", "target"}],
207
308
  "hygiene": {"source", "goal", "entry_points", "keep", "findings", "counts", "notes"},
208
309
  "notes": [...],
@@ -210,7 +311,12 @@ this README drift from them.
210
311
  }
211
312
  ```
212
313
 
213
- The MCP `scan` reply adds `exit_code`. Nothing in the report depends on the clock.
314
+ The MCP `scan` reply adds `exit_code`. Nothing in the report depends on the clock. `schema_version` 2 only adds
315
+ keys to the v0.2 shape (which had no `schema_version`); every v0.2 key keeps its meaning. `scores.grade` (and
316
+ `grade_uncapped`) is `null` for an INVALID scan; `capped` and `cap_reason` also cover the PARTIAL cap. With no Python
317
+ source, `scores.works` is `null` and `dimensions.works` is `{"score": null, "scored": false, ...}`;
318
+ `dimensions.safe.partial` is `true`. SARIF carries `status`, `status_reasons` and `coverage` in the run properties;
319
+ the text and Markdown reports print the status and the coverage line above the scores.
214
320
 
215
321
  ## Safe checks
216
322
 
@@ -218,6 +324,7 @@ The MCP `scan` reply adds `exit_code`. Nothing in the report depends on the cloc
218
324
  |---|---|---|---|
219
325
  | `secret.key` | API-key shapes (the privacy workflow's built-in list: `sk-` (which covers Anthropic's `sk-ant-`), `sk-or-`, `tvly-`, `ghp_`, `github_pat_`, `nvapi-`, `gsk_`, `AIza`; plus AWS access key IDs `AKIA`/`ASIA` + 16, Stripe `sk_live_`/`rk_live_`, Slack `xox[abprs]-`, Hugging Face `hf_` + 30 letters), a 40-character AWS secret key assigned to a name containing `aws` and `secret`, private-key blocks, `password = "<literal>"` (also `api_key`, `secret`, `access_token`…), and any `*_KEY` / `*_TOKEN` name holding a long, high-entropy literal. One finding per line. Values are never printed; values that call themselves fake (`mock`, `dummy`, `example`…) are skipped | FAIL | 40 |
220
326
  | `secret.key` | a Stripe test-mode key (`sk_test_`) | INFO | 0 |
327
+ | `secret.history` | **opt-in, `scan --history`**: a key shape (the `secret.key` API-key shapes, AWS secret keys, private-key blocks) that a commit added and the file no longer has; it may still be valid. Names the short sha and the path, never the value | FAIL | 30 |
221
328
  | `exec.dynamic` | `eval`, `exec`, `compile` on non-literal input; `__import__` with a variable | WARN | 10 |
222
329
  | `exec.dynamic` | …when `eval` or `exec` runs text from `input()`, `sys.argv`, request data, `os.environ` or a network read (the `exec.shell` taint rule, one function deep; `exec(compile(...))` counts at the `exec`): "runs code typed by the user" | FAIL | 30 |
223
330
  | `exec.shell` | `os.system`, `os.popen`, `subprocess.*(..., shell=True)` | WARN | 10 |
@@ -232,6 +339,8 @@ The MCP `scan` reply adds `exit_code`. Nothing in the report depends on the cloc
232
339
  | `obfuscation` | `exec`/`eval` of `base64`/`zlib`/`marshal`/hex-decoded data | FAIL | 40 |
233
340
  | `obfuscation` | base64 or hex blobs of 200+ characters, high-entropy string literals | WARN | 10 |
234
341
  | `fs.outside` | writes, deletes and moves aimed at absolute paths, `~`, or `..` escapes | WARN | 3 |
342
+ | `privacy.pii_log` | a personal-data name (`email`, `phone`, `ssn`, `dob`/`birth`, a person's `address`, `passport`, `credit_card`/`card_number`, `iban`, `ip_address`; whole words, so `emails_sent` and `server_address` do not count) flows into `logging.*`, a logger method or `print` (the `exec.shell` taint rule, one function deep) | WARN | 5 |
343
+ | `privacy.pii_prompt` | the same names flow into an LLM prompt: a `messages=` or `prompt=` argument, or a `.chat(`, `.complete(` or `.generate(` call | WARN | 5 |
235
344
  | `deps.unpinned` | dependencies with no pin or upper bound (`pyproject.toml`, `setup.py`, `setup.cfg`, `requirements*.txt`, `package.json`) | INFO | 0 |
236
345
 
237
346
  `net.egress` targets that stay in the inventory at INFO: https to a named host, loopback, the local network
@@ -241,6 +350,21 @@ The MCP `scan` reply adds `exit_code`. Nothing in the report depends on the cloc
241
350
  The walk skips `.git`, `venv`/`.venv`, `node_modules`, `__pycache__`, `dist`, `build` and the tool caches, anything
242
351
  the root `.gitignore` matches (simple globs; `!` negation is ignored), binary files, files over 2 MB and symlinks.
243
352
 
353
+ ### Secrets in the git history (opt-in)
354
+
355
+ ```
356
+ code-guardian scan . --history [--history-depth 500]
357
+ ```
358
+
359
+ A key deleted from a file is still in every clone's history. `--history` runs one read-only git command (an argument
360
+ list, never a shell): `git log -p --no-color --no-ext-diff --no-textconv --relative --format=%H --max-count=N -- .`
361
+ with the scanned repo's fsmonitor and signature-checking config switched off, so the repo cannot make the scan run a
362
+ program. It checks only the lines commits **added**, with the `secret.key` key-shape rules, reads at most the last N
363
+ commits (default 500) and at most 64 MB of log, and never modifies the repository. A key still present in the current
364
+ file is left to `secret.key`; the oldest commit that added a value is the one reported. Without git, or when PATH is
365
+ not a repository, the history check is skipped with a note (and the scan is PARTIAL, since what was asked did not
366
+ run). The scan itself stays offline.
367
+
244
368
  ### Invented and lookalike packages (Safe)
245
369
 
246
370
  AI coding assistants sometimes suggest a package that does not exist. Anyone can register that name on PyPI and
@@ -280,6 +404,9 @@ Will it work? Read from the parse and the manifests, never by importing or runni
280
404
  | `works.test_no_assert` | test functions with no `assert`, no `pytest.raises`/`warns`, no `self.assert*` and no call to a local helper that asserts; once per file, with the count | WARN | 5 |
281
405
  | `works.entry_missing` | a `[project.scripts]` (or `setup.cfg` console script) target `module:function` whose module or function is not in the tree | FAIL | 30 |
282
406
  | `orphan.dead` | dead code from the hygiene section (below), folded in at a low weight, only when entry points exist | WARN | 3 |
407
+ | `works.todo_stub` | a function whose body is only `pass`, `...` or `raise NotImplementedError` (a docstring allowed) **and** an entry point reaches it; not abstract methods, `Protocol` classes, `@overload` or a method a subclass in the repo overrides | WARN | 3 |
408
+ | `works.commented_code` | 5+ consecutive comment lines that parse as Python (found with `tokenize` + `ast.parse`); once per file, with the count of blocks | INFO | 0 |
409
+ | `works.dead_flag` | a module-level ALL_CAPS boolean (`DEBUG = False`) that no file reads, imports or names | INFO | 0 |
283
410
 
284
411
  How `works.import_unresolved` decides:
285
412
 
@@ -332,6 +459,51 @@ that merely holds `os.environ` is not followed.
332
459
  declared but unused. A repo with no Python source and no manifest has nothing for Works or Complete to check: it
333
460
  scores 100 there, and the report says so.
334
461
 
462
+ ## Baseline: gate on new findings only
463
+
464
+ An existing project rarely starts clean. Write a baseline once, commit it, and the gate then fails only on findings
465
+ that are new since:
466
+
467
+ ```
468
+ code-guardian baseline . # writes ./code-guardian.baseline.json; commit it
469
+ code-guardian scan . --baseline code-guardian.baseline.json --fail-on WARN
470
+ ```
471
+
472
+ - Each finding gets a `fingerprint`: sha256 of `code | level | path | evidence line`, where the evidence line is the
473
+ source line the finding points at, stripped, whitespace collapsed and redacted. **Line numbers are left out**, so
474
+ moving code up or down does not make a finding new; editing the line does. The baseline file holds the sorted
475
+ fingerprints and the version that wrote them, nothing else.
476
+ - With `--baseline`, every finding carries `new: true|false`, the report says `N new, M known`, and `--fail-on`
477
+ counts **new findings only**. A finding fixed since the baseline simply disappears; `scan` never rewrites the
478
+ baseline file (run `code-guardian baseline` again to accept the current state).
479
+ - **A baseline never hides the real grade.** The scores, the grade and every finding are computed and shown exactly
480
+ as without one; only the exit code changes.
481
+ - Same line twice counts twice: a third copy of a known line is new.
482
+ - SARIF results carry the fingerprint as `partialFingerprints["codeGuardian/v1"]` and `new` in their properties.
483
+
484
+ ## Diff and repair plan
485
+
486
+ ```
487
+ code-guardian scan . --json > before.json
488
+ code-guardian plan . # what to fix next: Markdown (--json for JSON), --top N rows (default 10)
489
+ # ...edit...
490
+ code-guardian scan . --json > after.json
491
+ code-guardian diff before.json after.json # what the change fixed and broke; exit 1 when anything regressed
492
+ ```
493
+
494
+ **`diff BEFORE.json AFTER.json`** compares two `scan --json` reports (0.3 or newer) by fingerprint: `fixed`, `new`
495
+ and `unchanged` findings, per-dimension score deltas, and a one-line verdict: **improved**, **regressed**, **mixed**
496
+ or **unchanged**. A new WARN/FAIL finding, or a status that got worse (COMPLETE → PARTIAL → INVALID), is a
497
+ regression; INFO findings are listed but never decide the verdict. Exit 1 when anything regressed (regressed or
498
+ mixed), 2 when a report cannot be read.
499
+
500
+ **`plan PATH`** is the repair plan. For each check row it re-runs the scoring on the finding list without that row:
501
+ the exact Overall points the row is worth, labelled **estimated gain if fixed** (the plan never touches the code, so
502
+ fixing more or less than the whole row changes the real gain). Rows are ranked by that gain, ties by the fewest
503
+ locations, and each comes with the catalogue's "how to fix it" and every location. When a dimension is floored at
504
+ 0 (more than 100 points deducted), no single fix in it moves Overall yet: its rows show +0 and the plan says how
505
+ many points must go first.
506
+
335
507
  ## Outputs, GitHub Action and pre-commit
336
508
 
337
509
  | flag | prints |
@@ -345,8 +517,9 @@ Every format is deterministic: no timestamps, no absolute paths. `--fail-on` wor
345
517
  `scan` tool keeps returning JSON.
346
518
 
347
519
  **GitHub Action** (`action.yml` in this repo, a composite action). It installs code-guardian from the action's own
348
- checkout, writes `code-guardian.sarif`, adds the Markdown report to the job summary, uploads the SARIF to code
349
- scanning, and then fails the job when a finding is at or above `fail-on`:
520
+ checkout, writes `code-guardian.sarif`, adds the Markdown report to the job summary (it opens with the status and
521
+ the coverage line), uploads the SARIF to code scanning, and then fails the job when a finding is at or above
522
+ `fail-on`:
350
523
 
351
524
  ```yaml
352
525
  name: code-guardian
@@ -359,15 +532,15 @@ jobs:
359
532
  runs-on: ubuntu-latest
360
533
  steps:
361
534
  - uses: actions/checkout@v4
362
- - uses: LuminariSoftwares/code-guardian@v0.2.0
535
+ - uses: LuminariSoftwares/code-guardian@v0.3.3
363
536
  with:
364
537
  path: .
365
538
  fail-on: FAIL # or WARN
366
539
  online: "false" # "true" asks PyPI about unknown dependency names
367
540
  ```
368
541
 
369
- Inputs: `path`, `fail-on`, `online`, `sarif-file` (default `code-guardian.sarif`), `upload-sarif` (default `true`),
370
- `python-version` (default `3.11`). Outputs: `overall`, `grade`, `exit-code`. Inputs reach the shell only through
542
+ Inputs: `path`, `fail-on`, `online`, `baseline` (a committed baseline file: gate on new findings only),
543
+ `sarif-file` (default `code-guardian.sarif`), `upload-sarif` (default `true`), `python-version` (default `3.11`). Outputs: `overall`, `grade`, `status` (`COMPLETE`, `PARTIAL` or `INVALID`), `exit-code`. Inputs reach the shell only through
371
544
  environment variables, never by text substitution.
372
545
 
373
546
  **pre-commit** (`.pre-commit-hooks.yaml`). It scans the whole repository, not just the staged files:
@@ -375,10 +548,12 @@ environment variables, never by text substitution.
375
548
  ```yaml
376
549
  repos:
377
550
  - repo: https://github.com/LuminariSoftwares/code-guardian
378
- rev: v0.2.0
551
+ rev: v0.3.3
379
552
  hooks:
380
553
  - id: code-guardian
381
554
  # args: [--fail-on, WARN]
555
+ # or, with a committed baseline, fail only on new findings:
556
+ # - id: code-guardian-new
382
557
  ```
383
558
 
384
559
  ## Check catalogue
@@ -406,7 +581,10 @@ above. Two rules apply across Safe:
406
581
  | `net.bind_all` | Safe | A server binds 0.0.0.0 or ::, so other machines can reach it. | Bind to 127.0.0.1 unless other machines must connect. | Containers usually must bind 0.0.0.0; a test that asserts the bind is refused still matches. |
407
582
  | `net.egress` | Safe | The egress inventory: every URL and IP literal and every network call site. WARN for a public IP, or plain http/ws/ftp to a public name or to a host set at runtime. | Use https, and confirm each destination is expected. | Local-network and documentation hosts stay INFO; a URL in a help string is still inventoried. |
408
583
  | `obfuscation` | Safe | exec/eval of base64, zlib, marshal or hex-decoded data (FAIL); long encoded blobs or high-entropy literals (WARN). | Decode the payload offline and read what it does before running anything. | Embedded icons, test vectors and hashes can look like blobs. |
584
+ | `secret.history` | Safe | With scan --history: a key shape (the secret.key API-key shapes, AWS secret keys, private-key blocks) was added by a commit and is no longer in that file. Anyone with a clone can still read it, and it may still be valid. The finding names the short sha and the path, never the value. | Rotate the key now; deleting it does not remove it from history. Then consider git filter-repo. | Keys that were revoked long ago, and test keys committed on purpose; a key still in the file is secret.key. |
409
585
  | `secret.key` | Safe | An API-key shape (OpenAI-style sk-, GitHub, Google, AWS, Stripe live, Slack, Hugging Face and others), an AWS secret key, a private-key block, a password-like literal or a high-entropy literal in a name ending _KEY or _TOKEN is written into a file (a Stripe test key is INFO). | Move it to an environment variable or a secret store, and rotate it: it is in the history. | Test fixtures and documentation samples; values that call themselves fake (mock, dummy, example) are skipped. |
586
+ | `privacy.pii_log` | Safe | A name for personal data (email, phone, ssn, dob/birth, a person's address, passport, credit card or card number, iban, ip address) flows into logging.*, a logger method or print, directly or through a variable in the same function. | Log an id or a masked value instead; logs are copied and kept far longer than the data. | Names that only look personal (a count of emails is named emails_n, not email); taint is one function deep. |
587
+ | `privacy.pii_prompt` | Safe | The same personal-data names flow into an LLM prompt: a messages= or prompt= argument, or a .chat(, .complete( or .generate( call. | Send an id or a redacted value; check the provider's data terms before sending personal data. | A .complete( or .generate( method that is not a model call; a prompt built in another function is missed. |
410
588
  | `supply.lookalike` | Safe | The name is one or two edits away from a known package without being it: the typosquatting pattern. | Check you meant the well-known package. | Real packages with names close to a popular one. |
411
589
  | `supply.unknown_package` | Safe | Offline: the name is not in the bundled list of known packages (INFO). Online: PyPI has no such project (FAIL), or it is new or has a single release (WARN). | Check the name is the one you meant; remove names an assistant invented; run with --online. | Offline misses are common for less-known real packages, which is why they are INFO. Existing is not safe. |
412
590
  | `tls.off` | Safe | verify=False, ssl._create_unverified_context, CERT_NONE or check_hostname = False. | Remove it; point at a CA bundle if the certificate is private. | Rare: code that deliberately probes broken servers. |
@@ -416,6 +594,9 @@ above. Two rules apply across Safe:
416
594
  | `works.entry_missing` | Works | A [project.scripts] (or setup.cfg console script) target module:function is not in the tree. | Point the script at an existing module:function. | Modules generated at build time. |
417
595
  | `works.import_unresolved` | Works | An absolute import that is not in the standard library, not in the repo and not a declared dependency. | Declare the distribution that provides it, or guard an optional import with try/except ImportError. | Packages pulled in only by another dependency (they work by accident); import names the mapping table misses. |
418
596
  | `works.relative_missing` | Works | from .x import y points at a module (or a package name) the repo does not have. | Fix the module path or add the missing module. | Modules generated at build time. |
597
+ | `works.todo_stub` | Works | A function whose body is only pass, ..., or raise NotImplementedError (a docstring allowed), and the entry points reach it: it will be called and do nothing. Abstract methods, Protocol classes, @overload and methods a subclass overrides do not count. | Implement it, or delete it and its callers; mark it @abc.abstractmethod if subclasses must implement it. | Hook methods meant to be overridden by users of a library; no-op callbacks kept on purpose. |
598
+ | `works.commented_code` | Works | A run of 5 or more consecutive comment lines that parse as Python (once per file, with the count of blocks). | Delete it: version control keeps the old code. | Code samples in comments that explain an API; INFO, never scored. |
599
+ | `works.dead_flag` | Works | A module-level ALL_CAPS boolean constant (DEBUG = False) that no file reads, imports or names. | Delete it, or wire it to the code it was meant to switch. | Flags read only by another tool or by string name; INFO, never scored. |
419
600
  | `works.syntax` | Works | ast.parse rejects the file, so it cannot be imported or run (INFO when the project needs a newer Python than the one scanning). | Fix the syntax error; port Python 2 code to Python 3. | Files kept on purpose for another interpreter, or templates with a .py name. |
420
601
  | `works.test_no_assert` | Works | Test functions with no assert, no pytest.raises/warns, no self.assert* and no call to an asserting helper. | Assert on the result; use pytest.raises for expected errors. | Smoke tests that only check nothing raises; helpers defined in another file. |
421
602
  | `works.tests_missing` | Works | There are no test files at all. | Add a tests/ folder with at least one test per entry point. | Tests kept in another repository. |
@@ -489,12 +670,14 @@ Guarantees, each covered by a test against a stub model on loopback:
489
670
 
490
671
  ## MCP server
491
672
 
492
- `code-guardian-mcp` (or `code-guardian mcp`) speaks MCP over stdio with three read-only tools, each with a `title`
673
+ `code-guardian-mcp` (or `code-guardian mcp`) speaks MCP over stdio with five read-only tools, each with a `title`
493
674
  and `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true`:
494
675
 
495
676
  | tool | does | `openWorldHint` |
496
677
  |---|---|---|
497
- | `scan` | the full report for `path` (+ optional `goal`, `fail_on`; the reply carries `exit_code`) | false |
678
+ | `scan` | the full report for `path` (+ optional `goal`, `fail_on`, `baseline`; the reply carries `exit_code`) | false |
679
+ | `diff` | `before` and `after` (each a scan report as JSON text, or a path to a `scan --json` file): fixed, new, unchanged, score deltas, verdict and `exit_code` | false |
680
+ | `plan` | the repair plan for `path` (+ optional `top`, default 10, and `goal`) | false |
498
681
  | `orphans` | the hygiene section for `path` | false |
499
682
  | `explain` | `scan` plus the model explanation | true |
500
683
 
@@ -524,7 +707,8 @@ The `env` block is only needed for `explain`.
524
707
 
525
708
  - **Only Python is analysed.** Manifests (`package.json`, `setup.py`, `setup.cfg`, `pyproject.toml`,
526
709
  `requirements*.txt`) are read for install hooks and pins; every other language gets the secret scan only, and the
527
- report says which files.
710
+ `coverage:` line says which files (see [What it analyses](#what-it-analyses)). An `eval` of request input in a
711
+ `.js` file is not seen.
528
712
  - **Static analysis cannot see runtime behaviour.** A finding means the code is there, not that it runs; code that
529
713
  builds its behaviour at runtime (downloaded plugins, `exec` of fetched text) is flagged where the parse can see it
530
714
  and invisible where it cannot. A grade A is "nothing the scanners recognise", not "proven safe".
@@ -540,11 +724,11 @@ The `env` block is only needed for `explain`.
540
724
 
541
725
  ```
542
726
  python -m code_guardian.scanners --selftest
543
- scanners selftest: 18 checks, 18 passed, 0 failed
727
+ scanners selftest: 27 checks, 27 passed, 0 failed
544
728
  ```
545
729
 
546
730
  Each of `finding`, `walk`, `scanners`, `manifests`, `score`, `goal`, `orphans`, `works`, `complete`, `supply`,
547
- `explain`, `report`, `cli` and `mcp_server` has one; `tests/test_selftests.py` runs them all.
731
+ `explain`, `report`, `cli`, `mcp_server`, `baseline`, `verify`, `history` and `leftovers` has one; `tests/test_selftests.py` runs them all.
548
732
 
549
733
  ## License
550
734