bluerayscan 0.3.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 (82) hide show
  1. bluerayscan-0.3.0/LICENSE +21 -0
  2. bluerayscan-0.3.0/PKG-INFO +550 -0
  3. bluerayscan-0.3.0/README.md +530 -0
  4. bluerayscan-0.3.0/pyproject.toml +45 -0
  5. bluerayscan-0.3.0/setup.cfg +4 -0
  6. bluerayscan-0.3.0/src/bluerayscan.egg-info/PKG-INFO +550 -0
  7. bluerayscan-0.3.0/src/bluerayscan.egg-info/SOURCES.txt +80 -0
  8. bluerayscan-0.3.0/src/bluerayscan.egg-info/dependency_links.txt +1 -0
  9. bluerayscan-0.3.0/src/bluerayscan.egg-info/entry_points.txt +3 -0
  10. bluerayscan-0.3.0/src/bluerayscan.egg-info/top_level.txt +1 -0
  11. bluerayscan-0.3.0/src/repo_sentinel/__init__.py +5 -0
  12. bluerayscan-0.3.0/src/repo_sentinel/__main__.py +6 -0
  13. bluerayscan-0.3.0/src/repo_sentinel/baseline.py +168 -0
  14. bluerayscan-0.3.0/src/repo_sentinel/cli.py +262 -0
  15. bluerayscan-0.3.0/src/repo_sentinel/commands.py +513 -0
  16. bluerayscan-0.3.0/src/repo_sentinel/config.py +169 -0
  17. bluerayscan-0.3.0/src/repo_sentinel/discovery.py +274 -0
  18. bluerayscan-0.3.0/src/repo_sentinel/engine.py +281 -0
  19. bluerayscan-0.3.0/src/repo_sentinel/findings.py +166 -0
  20. bluerayscan-0.3.0/src/repo_sentinel/gitignore.py +242 -0
  21. bluerayscan-0.3.0/src/repo_sentinel/hcl.py +245 -0
  22. bluerayscan-0.3.0/src/repo_sentinel/heuristics.py +320 -0
  23. bluerayscan-0.3.0/src/repo_sentinel/jsonish.py +193 -0
  24. bluerayscan-0.3.0/src/repo_sentinel/report.py +697 -0
  25. bluerayscan-0.3.0/src/repo_sentinel/rules.py +371 -0
  26. bluerayscan-0.3.0/src/repo_sentinel/scanners/__init__.py +47 -0
  27. bluerayscan-0.3.0/src/repo_sentinel/scanners/allowlist.py +117 -0
  28. bluerayscan-0.3.0/src/repo_sentinel/scanners/ansible.py +259 -0
  29. bluerayscan-0.3.0/src/repo_sentinel/scanners/appcode.py +379 -0
  30. bluerayscan-0.3.0/src/repo_sentinel/scanners/azure.py +250 -0
  31. bluerayscan-0.3.0/src/repo_sentinel/scanners/ci.py +78 -0
  32. bluerayscan-0.3.0/src/repo_sentinel/scanners/circleci.py +211 -0
  33. bluerayscan-0.3.0/src/repo_sentinel/scanners/cloudformation.py +334 -0
  34. bluerayscan-0.3.0/src/repo_sentinel/scanners/compose.py +298 -0
  35. bluerayscan-0.3.0/src/repo_sentinel/scanners/dependencies.py +535 -0
  36. bluerayscan-0.3.0/src/repo_sentinel/scanners/dockerfiles.py +276 -0
  37. bluerayscan-0.3.0/src/repo_sentinel/scanners/filenames.py +331 -0
  38. bluerayscan-0.3.0/src/repo_sentinel/scanners/gitlab.py +254 -0
  39. bluerayscan-0.3.0/src/repo_sentinel/scanners/jenkins.py +183 -0
  40. bluerayscan-0.3.0/src/repo_sentinel/scanners/kubernetes.py +935 -0
  41. bluerayscan-0.3.0/src/repo_sentinel/scanners/providers.py +672 -0
  42. bluerayscan-0.3.0/src/repo_sentinel/scanners/secrets.py +497 -0
  43. bluerayscan-0.3.0/src/repo_sentinel/scanners/shell.py +256 -0
  44. bluerayscan-0.3.0/src/repo_sentinel/scanners/terraform.py +609 -0
  45. bluerayscan-0.3.0/src/repo_sentinel/scanners/workflows.py +856 -0
  46. bluerayscan-0.3.0/src/repo_sentinel/suppression.py +214 -0
  47. bluerayscan-0.3.0/src/repo_sentinel/wellknown.py +218 -0
  48. bluerayscan-0.3.0/src/repo_sentinel/yamlish.py +310 -0
  49. bluerayscan-0.3.0/tests/test_allowlist.py +124 -0
  50. bluerayscan-0.3.0/tests/test_ansible.py +124 -0
  51. bluerayscan-0.3.0/tests/test_appcode.py +270 -0
  52. bluerayscan-0.3.0/tests/test_azure.py +135 -0
  53. bluerayscan-0.3.0/tests/test_baseline.py +114 -0
  54. bluerayscan-0.3.0/tests/test_circleci.py +98 -0
  55. bluerayscan-0.3.0/tests/test_cli.py +753 -0
  56. bluerayscan-0.3.0/tests/test_cloudformation.py +237 -0
  57. bluerayscan-0.3.0/tests/test_compose.py +131 -0
  58. bluerayscan-0.3.0/tests/test_config.py +212 -0
  59. bluerayscan-0.3.0/tests/test_dependencies.py +324 -0
  60. bluerayscan-0.3.0/tests/test_discovery.py +141 -0
  61. bluerayscan-0.3.0/tests/test_dockerfiles.py +151 -0
  62. bluerayscan-0.3.0/tests/test_engine.py +210 -0
  63. bluerayscan-0.3.0/tests/test_filenames.py +191 -0
  64. bluerayscan-0.3.0/tests/test_findings.py +86 -0
  65. bluerayscan-0.3.0/tests/test_gitignore.py +211 -0
  66. bluerayscan-0.3.0/tests/test_gitlab.py +135 -0
  67. bluerayscan-0.3.0/tests/test_hcl.py +92 -0
  68. bluerayscan-0.3.0/tests/test_heuristics.py +247 -0
  69. bluerayscan-0.3.0/tests/test_jenkins.py +100 -0
  70. bluerayscan-0.3.0/tests/test_jsonish.py +94 -0
  71. bluerayscan-0.3.0/tests/test_kubernetes.py +604 -0
  72. bluerayscan-0.3.0/tests/test_report.py +502 -0
  73. bluerayscan-0.3.0/tests/test_robustness.py +431 -0
  74. bluerayscan-0.3.0/tests/test_rules.py +273 -0
  75. bluerayscan-0.3.0/tests/test_scanner_contract.py +79 -0
  76. bluerayscan-0.3.0/tests/test_secrets.py +564 -0
  77. bluerayscan-0.3.0/tests/test_shell.py +182 -0
  78. bluerayscan-0.3.0/tests/test_suppression.py +217 -0
  79. bluerayscan-0.3.0/tests/test_terraform.py +431 -0
  80. bluerayscan-0.3.0/tests/test_tools.py +91 -0
  81. bluerayscan-0.3.0/tests/test_workflows.py +640 -0
  82. bluerayscan-0.3.0/tests/test_yamlish.py +156 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 KhanSaahib
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,550 @@
1
+ Metadata-Version: 2.4
2
+ Name: bluerayscan
3
+ Version: 0.3.0
4
+ Summary: Audit a repository for leaked secrets, risky CI workflows, and insecure container and infrastructure configuration.
5
+ Author: KhanSaahib
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/KhanSaahib/repo-sentinel
8
+ Project-URL: Issues, https://github.com/KhanSaahib/repo-sentinel/issues
9
+ Keywords: security,secrets,sast,github-actions,terraform,kubernetes,docker,static-analysis
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Security
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ # BlueRayScan
22
+
23
+ [![CI](https://github.com/KhanSaahib/repo-sentinel/actions/workflows/ci.yml/badge.svg)](https://github.com/KhanSaahib/repo-sentinel/actions/workflows/ci.yml)
24
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9%2B-blue)](https://www.python.org/downloads/)
25
+ [![No dependencies](https://img.shields.io/badge/dependencies-none-brightgreen)](pyproject.toml)
26
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
27
+
28
+ A small command line auditor that reads a repository the way a security
29
+ reviewer skims it: looking for credentials that should never have been
30
+ committed, and for the configuration that quietly hands out more access than
31
+ anyone intended -- a workflow an outside contributor can hijack, a container
32
+ that runs as root, a security group open to the internet, a Compose service
33
+ that publishes your database on every interface.
34
+
35
+ **No runtime dependencies.** Standard library only, on Python 3.9 and up. A tool
36
+ you run against your supply chain should not enlarge it.
37
+
38
+ ## Install
39
+
40
+ From PyPI with `pipx`:
41
+
42
+ ```bash
43
+ pipx install bluerayscan
44
+ ```
45
+
46
+ Or with `pip`:
47
+
48
+ ```bash
49
+ pip install bluerayscan
50
+ ```
51
+
52
+ The primary command is `bluerayscan`. The existing `repo-sentinel` command is
53
+ kept as a compatibility alias.
54
+
55
+ From a checkout:
56
+
57
+ ```bash
58
+ git clone https://github.com/KhanSaahib/repo-sentinel.git
59
+ cd repo-sentinel
60
+ pip install .
61
+ ```
62
+
63
+ Or from a tag, without a checkout:
64
+
65
+ ```bash
66
+ pip install git+https://github.com/KhanSaahib/repo-sentinel@v0.3.0
67
+ ```
68
+
69
+ Or run it straight from a checkout, with no install at all:
70
+
71
+ ```bash
72
+ PYTHONPATH=src python -m repo_sentinel scan .
73
+ ```
74
+
75
+ ## Use
76
+
77
+ Start here:
78
+
79
+ ```bash
80
+ bluerayscan init .
81
+ ```
82
+
83
+ That scans the repository, tells you what is in it -- including which three
84
+ rules are doing most of the talking, because a hundred findings that are all
85
+ one rule is a decision to make once -- records the findings at or above `high`
86
+ as a baseline so your first pipeline run is green, writes a
87
+ `.repo-sentinel.json`, and prints the CI snippet for whichever CI system the
88
+ repository already has -- GitHub Actions, GitLab, Azure Pipelines, CircleCI or
89
+ Jenkins, the last four wired to draw the report rather than print it. Nothing
90
+ is overwritten without `--force`.
91
+
92
+ Then, day to day:
93
+
94
+ ```bash
95
+ bluerayscan scan . # scan the working directory
96
+ bluerayscan scan ../other-project # scan somewhere else
97
+ bluerayscan rules # what does this thing check for?
98
+ bluerayscan rules kubernetes # ...or just that family
99
+ bluerayscan rules WF011 # one rule, explained in full
100
+ bluerayscan init . # set a repository up
101
+
102
+ bluerayscan scan . --format json # machine-readable output
103
+ bluerayscan scan . --format sarif --output results.sarif
104
+ bluerayscan scan . --format markdown # a pull request comment
105
+ bluerayscan scan . --format github # annotations on the diff
106
+ bluerayscan scan . --format junit # a test report, for other CIs
107
+ bluerayscan scan . --min-severity high # only show what matters most
108
+ bluerayscan scan . --min-confidence high # only show what it is sure of
109
+ bluerayscan scan . --fail-on critical # relax the CI gate
110
+ bluerayscan scan . --fail-on none # report, never fail
111
+ bluerayscan scan . --exclude 'fixtures' # skip a directory (repeatable)
112
+ bluerayscan scan . --max-file-size 8M # read the big ones too
113
+ bluerayscan scan . --no-gitignore # also scan git-ignored files
114
+ bluerayscan scan . --no-example-allowlist # include documented and invented keys
115
+ bluerayscan scan . --no-suppression # read past the ignore markers
116
+
117
+ bluerayscan scan . --write-baseline # accept what is already there
118
+ bluerayscan scan . --baseline # fail only on what is new
119
+ bluerayscan scan . --prune-baseline # drop entries that match nothing
120
+
121
+ bluerayscan scan . --disable K8S004 # switch off a rule or family
122
+ bluerayscan scan . --quiet # the summary, and what it is mostly
123
+ bluerayscan scan . --sort path # group by file, to read rather than triage
124
+ git diff --name-only origin/main | bluerayscan scan . --paths-from -
125
+ ```
126
+
127
+ That last line is the fast per-pull-request run: the scan is restricted to the
128
+ files the branch touched. A listed path that no longer exists is skipped, since
129
+ a diff lists deletions too, and a listed path that `.gitignore` covers is
130
+ scanned anyway -- you named it.
131
+
132
+ Exit codes: `0` clean, `1` findings at or above `--fail-on` (default `medium`),
133
+ `2` usage error, unreadable baseline, or unwritable output. `--fail-on none`
134
+ reports without ever returning `1`, which is what the job that uploads SARIF or
135
+ posts the comment wants -- a `2` still means the run itself went wrong. That makes it a
136
+ one-line CI gate:
137
+
138
+ ```yaml
139
+ - run: pipx run bluerayscan scan . --fail-on high
140
+ ```
141
+
142
+ Sample output:
143
+
144
+ ```
145
+ CRITICAL SEC001 terraform/main.tf:14
146
+ AWS access key id
147
+ evidence: AKIA************LM3D
148
+ fix: Deactivate the key in IAM, then rotate it. Deleting the commit is not enough.
149
+
150
+ HIGH SEC101 .env.staging:7 (medium confidence)
151
+ High-entropy value position assigned to 'DATABASE_PASSWORD'
152
+ evidence: Tv8n************Lz4T
153
+ fix: Move the value to an environment variable or secret store.
154
+
155
+ MEDIUM WF001 .github/workflows/release.yml:22
156
+ Action 'actions/checkout@v4' is pinned to a mutable tag
157
+ evidence: - uses: actions/checkout@v4
158
+ fix: Tags can be moved to point at new code. Pin to a full commit SHA and let Dependabot propose upgrades.
159
+
160
+ 3 finding(s): 1 critical, 1 high, 1 medium
161
+ Scanned 412 file(s) in 0.31s.
162
+ ```
163
+
164
+ That last line is not decoration. A run that scanned nothing looks exactly like
165
+ a clean repository, and "no findings" from a mistyped path is the most
166
+ dangerous answer this tool can give.
167
+
168
+ ## Severity and confidence
169
+
170
+ Every finding carries both, because they answer different questions and
171
+ collapsing them into one number loses both.
172
+
173
+ **Severity** is what it costs you if the finding is real: a live AWS key is
174
+ critical whether the rule was certain or guessing. **Confidence** is how sure
175
+ the rule is that it found a real instance. A documented token shape — a GitHub
176
+ PAT, a Stripe live key — is high confidence; a high-entropy string next to a
177
+ variable named `api_key` is a heuristic, and says so.
178
+
179
+ Confidence also moves with where a file sits. A credential in `testdata/` or a
180
+ README is usually invented, and a pipeline under `docs/` is a tutorial snippet
181
+ rather than something that runs, so both drop a step. Neither is silenced --
182
+ that is what `--min-confidence` is for, and a real key does get committed to a
183
+ fixture directory.
184
+
185
+ The split is what makes the tool tunable without making it useless. A pipeline
186
+ that wants a hard gate can run `--fail-on high --min-confidence high` and be
187
+ woken only for things the scanner can defend, while a human audit runs with
188
+ neither flag and reads everything.
189
+
190
+ ## What gets scanned
191
+
192
+ The walk skips binaries, files over 2 MB, and a built-in list of generated or
193
+ vendored directories (`.git`, `node_modules`, `.venv`, `dist`, `target`, …).
194
+ The size limit is reported rather than assumed: every run says how many files
195
+ it skipped and names the first, and `--max-file-size 8M` reads them. A binary
196
+ is the one silent skip, because its bytes are not text in any sense a rule
197
+ could read.
198
+
199
+ It also honours `.gitignore`, including nested ones, which each govern their own
200
+ subtree. The rules implemented are negation with `!`, anchoring with a leading or
201
+ embedded `/`, directory-only patterns ending in `/`, the `*`, `?` and `[...]`
202
+ wildcards, and `**` for arbitrary depth; across the ignore files in scope, the
203
+ last matching pattern wins. Not implemented: `.git/info/exclude`, the global
204
+ `core.excludesFile`, and git's rule that an already-tracked file stays tracked
205
+ however it is ignored — all three would mean shelling out to git.
206
+
207
+ This is a deliberate narrowing of scope, and it cuts both ways. A secret in an
208
+ ignored file was never committed, so reporting it is a false positive, and the
209
+ noise from a local `.env` is what makes people stop reading the output. But an
210
+ ignore rule is also the easiest way to hide something from this tool, whether by
211
+ accident or on purpose. Audit what the scanner was told not to look at:
212
+
213
+ ```bash
214
+ repo-sentinel scan . --no-gitignore
215
+ ```
216
+
217
+ ## What it checks
218
+
219
+ One hundred and forty-four rules across sixteen families. [docs/RULES.md](docs/RULES.md) is the
220
+ full list, with a paragraph on each family explaining what it is looking for
221
+ and why; `repo-sentinel rules` prints the same catalogue from the tool.
222
+
223
+ | Family | Rules | Looks at |
224
+ | --- | --- | --- |
225
+ | [Secrets](docs/RULES.md#secrets) | SEC001–SEC054, SEC100–SEC101 | Credentials in any text file, including inside base64 |
226
+ | [File names](docs/RULES.md#file-names) | FN001–FN004 | Key material and credential files, which have no text to read |
227
+ | [Shell scripts](docs/RULES.md#shell-scripts-and-makefiles) | SH001–SH004 | Where `curl \| sh` actually lives |
228
+ | [Application code](docs/RULES.md#application-code) | AP001–AP006 | Verification off, debug on, predictable tokens, unsafe loads |
229
+ | [Dependencies](docs/RULES.md#dependencies) | SC001–SC004 | Where the rest of the build comes from, in nine manifests |
230
+ | [GitHub Actions](docs/RULES.md#github-actions-workflows) | WF001–WF013 | Script injection, token scope, privileged triggers |
231
+ | [GitLab CI](docs/RULES.md#gitlab-ci) | GL001–GL004 | The same injection class, and debug tracing |
232
+ | [Azure Pipelines](docs/RULES.md#azure-pipelines) | AZ001–AZ004 | The same injection, a third time |
233
+ | [Jenkins](docs/RULES.md#jenkins) | JK001–JK003 | Groovy's quoting, which decides if it is a bug |
234
+ | [CircleCI](docs/RULES.md#circleci) | CC001–CC004 | The same injection, a fourth time, plus moving orbs |
235
+ | [Dockerfiles](docs/RULES.md#dockerfiles) | DK001–DK006 | Base images, root, pipe-to-shell, layer secrets |
236
+ | [Docker Compose](docs/RULES.md#docker-compose) | DC001–DC006 | Privilege, host mounts, ports on every interface |
237
+ | [Terraform](docs/RULES.md#terraform) | TF001–TF008 | Open ingress, public storage, wildcard policies |
238
+ | [Ansible](docs/RULES.md#ansible) | AN001–AN003 | Decisions applied to every host at once |
239
+ | [CloudFormation](docs/RULES.md#cloudformation) | CF001–CF006 | The same, in AWS's other vocabulary |
240
+ | [Kubernetes](docs/RULES.md#kubernetes) | K8S001–K8S012 | Container escape routes, secrets in manifests, chart values, overlays |
241
+
242
+ Three things are worth knowing before you read the list.
243
+
244
+ **Structure, not lines.** The Terraform, Kubernetes, Compose, CloudFormation
245
+ and pipeline rules read block structure, through three small standard-library
246
+ readers. It is the difference between `privileged: true` under
247
+ `securityContext`, which is critical, and the same line under `annotations`,
248
+ which is nothing. The workflow family is the exception and says so: it reads
249
+ GitHub Actions files the way a reviewer skims them, splitting jobs and steps by
250
+ indentation.
251
+
252
+ **Recognition by content.** Kubernetes manifests are found by `apiVersion` plus
253
+ `kind`, Compose files by their `services` map, GitLab pipelines by name or by
254
+ shape. Not by directory: a workflow file that happens to live in `k8s/` is not
255
+ a workload.
256
+
257
+ **Honest edges.** None of this evaluates Terraform, renders a chart, or runs a
258
+ pipeline. A value arriving through a variable is invisible, and the rules say
259
+ so rather than implying coverage they do not have.
260
+
261
+ ## Project defaults
262
+
263
+ Every project that adopts a scanner ends up with a preferred invocation. Putting
264
+ it in a `Makefile` means the pre-commit hook, the pipeline and whoever runs the
265
+ tool by hand all disagree. Put it in `.repo-sentinel.json` beside the tree
266
+ instead:
267
+
268
+ ```json
269
+ {
270
+ "fail_on": "high",
271
+ "min_confidence": "medium",
272
+ "exclude": ["vendor", "testdata"],
273
+ "disable": ["K8S004", "DC006"]
274
+ }
275
+ ```
276
+
277
+ The settings are `exclude`, `fail_on` (`"none"` included), `min_severity`,
278
+ `min_confidence`, `baseline`, `max_file_size`, `sort`, `disable`, `gitignore`
279
+ and `example_allowlist`. An unknown
280
+ key is an error rather than a shrug: a typo in a security tool's configuration
281
+ means a project believes it configured something it did not.
282
+
283
+ Everything here is a *default*. Anything on the command line wins, so a config
284
+ file can never stop someone auditing their own repository more strictly than the
285
+ project usually does.
286
+
287
+ Rules can also be switched off for one subtree rather than everywhere, which is
288
+ usually what is actually wanted -- a vendored chart, an examples directory, a
289
+ fixtures tree:
290
+
291
+ ```json
292
+ {
293
+ "paths": {
294
+ "examples/**": { "disable": ["K8S*"] },
295
+ "charts/vendor/**": { "disable": ["*"] }
296
+ }
297
+ }
298
+ ```
299
+
300
+ The globs are the `.gitignore` dialect, matched by the same code, so
301
+ `examples/`, `charts/vendor/**` and `*.tf` mean here exactly what they mean
302
+ there. Inventing a second glob dialect for one config key is how a tool ends up
303
+ with two subtly different answers to "does this path match".
304
+
305
+ `disable` takes rule ids or family prefixes (`DC*`), and `--disable` does the
306
+ same ad hoc. A disabled rule is still counted in the output -- *"3 finding(s)
307
+ hidden by disabled rules"* -- because silence nobody can see is the failure mode
308
+ this whole tool exists to avoid. An id that matches no rule is called out too:
309
+ that typo leaves the rule switched on, which is the safe direction but not the
310
+ one you meant.
311
+
312
+ ## Baselines
313
+
314
+ A scanner introduced to a repository that has been running for years reports its
315
+ entire history at once, and a build that has been red since Tuesday tells nobody
316
+ anything. Record what is already there, then fail only on what arrives after:
317
+
318
+ ```bash
319
+ repo-sentinel scan . --write-baseline # writes .repo-sentinel-baseline.json
320
+ git add .repo-sentinel-baseline.json
321
+ repo-sentinel scan . --baseline # exits 0; new findings still fail
322
+ ```
323
+
324
+ Two properties make the file safe to commit. It never contains a secret —
325
+ entries hold a hash of the already-redacted evidence, along with the rule and
326
+ the path, so there is nothing to recover. And it does not pin line numbers, so
327
+ reformatting a file does not resurrect its accepted findings, while moving a
328
+ secret to another file does not keep it accepted.
329
+
330
+ Every run reports how many findings the baseline is holding, and every entry
331
+ that matched nothing this time is reported as stale — usually because the
332
+ finding was genuinely fixed. That is how a baseline shrinks instead of
333
+ calcifying. It is a list of debts, not a list of exemptions.
334
+
335
+ An unreadable or corrupt baseline is an error, not an empty baseline. Failing
336
+ open would mean a truncated file silently accepts everything.
337
+
338
+ ## The JSON output
339
+
340
+ `--format json` is the one to build on. Alongside the findings it carries a
341
+ `scan` object -- how many files were read, how long it took, what was skipped
342
+ for being unreadable or too large, and how many lines carry a suppression
343
+ marker. A person reads that as a sentence under the report; a pipeline cannot,
344
+ and a pipeline that cannot tell "no findings" from "nothing was read" is
345
+ exactly what that sentence exists to prevent.
346
+
347
+ Each finding carries:
348
+
349
+ ```json
350
+ {
351
+ "rule_id": "SEC001",
352
+ "severity": "critical",
353
+ "confidence": "high",
354
+ "title": "AWS access key id",
355
+ "path": "terraform/main.tf",
356
+ "line": 14,
357
+ "evidence": "AKIA************LM3D",
358
+ "remediation": "Deactivate the key in IAM, then rotate it. ...",
359
+ "fingerprint": "8f120d646369be74",
360
+ "occurrences": 1
361
+ }
362
+ ```
363
+
364
+ `occurrences` is how many places in that file hold the same value. One
365
+ credential pasted six hundred times is one credential to rotate, so it is
366
+ reported once, at the first of them, with the count attached.
367
+
368
+ The `fingerprint` is the same identity a baseline uses: a hash of the rule, the
369
+ path and the already-redacted evidence, with no line number in it, so it
370
+ survives reformatting and changes when the value does. Paths always use forward
371
+ slashes, on every platform, so a report reads the same wherever it was
372
+ produced. `repo-sentinel rules --format json` describes the rules themselves,
373
+ including the CWE each one reports and, for each family in the listing, what
374
+ it reads and where it is written up -- so a consumer grouping by category need
375
+ not invent a label the documentation does not use.
376
+
377
+ ## Posting the result onto a pull request
378
+
379
+ `--format markdown` writes a table meant to be pasted into a comment, where the
380
+ people arguing about the change are already looking:
381
+
382
+ ```yaml
383
+ - id: scan
384
+ run: repo-sentinel scan . --format markdown --output report.md --fail-on none
385
+ - uses: actions/github-script@<sha>
386
+ with:
387
+ script: |
388
+ const body = require("fs").readFileSync("report.md", "utf8");
389
+ github.rest.issues.createComment({ ...context.repo, issue_number: context.issue.number, body });
390
+ ```
391
+
392
+ The table carries what triage needs -- how bad, which rule, where -- and the
393
+ fixes go underneath in a collapsed block, once per rule rather than once per
394
+ finding. Long reports are truncated with a count: a comment that needs scrolling
395
+ past four hundred rows is one nobody reads.
396
+
397
+ ### Annotations, without asking for a permission
398
+
399
+ `--format sarif` needs `security-events: write`, which a workflow triggered by
400
+ a fork's pull request does not have. `--format github` writes the same findings
401
+ as workflow commands, which the runner turns into annotations on the diff and
402
+ which need no permission at all:
403
+
404
+ ```yaml
405
+ - run: repo-sentinel scan . --format github
406
+ ```
407
+
408
+ Same findings, worse home, far fewer prerequisites.
409
+
410
+ ### Every other CI: a test report
411
+
412
+ GitLab, Azure Pipelines and Jenkins all render JUnit XML natively, as a list of
413
+ failures with a message and a body -- which is a finding with its remediation
414
+ attached. No plugin, no permission:
415
+
416
+ ```yaml
417
+ # .gitlab-ci.yml
418
+ scan:
419
+ script: repo-sentinel scan . --format junit --output report.xml
420
+ artifacts:
421
+ when: always
422
+ reports:
423
+ junit: report.xml
424
+ ```
425
+
426
+ One test case per finding, classed by family so the CI groups them the way the
427
+ catalogue does, and a clean run is a single passing case rather than an empty
428
+ suite -- an empty report renders as a broken job rather than a quiet one.
429
+
430
+ ## Reporting to the GitHub Security tab
431
+
432
+ `--format sarif` emits SARIF 2.1.0, which GitHub's code scanning ingests and
433
+ turns into annotations on the pull request that introduced the line:
434
+
435
+ ```yaml
436
+ - run: repo-sentinel scan . --format sarif --output repo-sentinel.sarif --fail-on none
437
+ - uses: github/codeql-action/upload-sarif@<sha>
438
+ with:
439
+ sarif_file: repo-sentinel.sarif
440
+ ```
441
+
442
+ The job needs `security-events: write`, and `--fail-on none` on the scan step so
443
+ that a finding does not stop the run before it has published anything — put the
444
+ actual gate in a separate job. This repository's own
445
+ [CI](.github/workflows/ci.yml) does exactly that.
446
+
447
+ Each result carries a `partialFingerprint`, so code scanning follows a finding
448
+ across the reformattings and line moves that would otherwise close it and
449
+ immediately reopen it as new.
450
+
451
+ The run also reports what it could not read, as SARIF `toolExecutionNotifications`.
452
+ A Security tab showing no alerts because nothing was scanned looks exactly like
453
+ one showing no alerts because everything is fine, and those notifications are
454
+ the difference.
455
+
456
+ ## Suppressing a false positive
457
+
458
+ Three scopes, in increasing blast radius. All three work in any file the scanner
459
+ reads, workflows and Dockerfiles included, and none of them cares what the
460
+ comment syntax is.
461
+
462
+ One line:
463
+
464
+ ```python
465
+ sample_token = "Xk92mQp7Lz4TvB8nRw1Y" # repo-sentinel: ignore
466
+ ```
467
+
468
+ A block, for a generated section or a fixture full of invented keys. Both
469
+ markers are themselves suppressed, along with everything between them:
470
+
471
+ ```python
472
+ # repo-sentinel: ignore-start
473
+ FAKE_KEYS = {"aws": "...", "stripe": "..."}
474
+ # repo-sentinel: ignore-end
475
+ ```
476
+
477
+ A whole file, with `repo-sentinel: ignore-file` — but **only in the first 20
478
+ lines**. Below that it is just a mention, which is why this README still gets
479
+ scanned despite the line you are reading. Without that rule, any file that
480
+ described the directive would silently stop being scanned, and a scanner a
481
+ sentence about it can switch off is worse than no scanner. Keeping the directive
482
+ in the header also means you can see that a file is unscanned without reading to
483
+ the bottom of it.
484
+
485
+ All three can name the rules they mean, in brackets:
486
+
487
+ ```yaml
488
+ image: nginx:latest # repo-sentinel: ignore[K8S008]
489
+ ```
490
+
491
+ Prefer this to the blunt form. A line exempted from everything stays exempt when
492
+ a later release adds a rule that would have caught something real there, and the
493
+ comment no longer records why the exemption exists. Family prefixes work too
494
+ (`ignore[K8S*]`), and so do lists (`ignore[SEC100, DK002]`); the syntax is the
495
+ same one `disable` uses in the config file.
496
+
497
+ Every run says how many lines carry a marker, whether or not it obeyed them,
498
+ and `--no-suppression` reads past all of them. A scanner that can be switched
499
+ off invisibly is worse than no scanner, which is the same reason
500
+ `--no-gitignore` exists.
501
+
502
+ A block that is opened and never closed silences everything after it, so it is
503
+ reported as SEC900 rather than trusted. Close the block, or say `ignore-file` and
504
+ mean it.
505
+
506
+ Reach for a baseline instead when the finding is real but not yet fixed. A
507
+ suppression marker says "this is not a problem"; a baseline says "this is a
508
+ problem I have not got to". Recording the second as the first is how a repository
509
+ forgets.
510
+
511
+ ## Development
512
+
513
+ ```bash
514
+ PYTHONPATH=src python -m unittest discover -s tests -v
515
+ ```
516
+
517
+ CI runs the suite on Python 3.9, 3.11 and 3.13, holds a line-coverage floor,
518
+ scans this repository with the tool itself, and publishes the result to the
519
+ Security tab.
520
+
521
+ ```bash
522
+ python3 tools/coverage.py --show-missing
523
+ ```
524
+
525
+ The coverage tool is standard library only, like everything else here. Writing
526
+ one is a strange thing to do when a good one exists; the reason is that the
527
+ promise "this pulls nothing into your environment" should hold for the tests
528
+ too, so a contributor with no network can still check the floor.
529
+
530
+ The test suite includes a corpus that trips **every** rule in the catalogue, and
531
+ asserts in five directions: no scanner may emit a rule the catalogue does not
532
+ describe, no catalogue entry may describe a rule nothing can emit, no rule may
533
+ be missing from [docs/RULES.md](docs/RULES.md), no rule may be more severe in
534
+ practice than the catalogue promises, and the severity in the documentation has
535
+ to be the severity in the code. Adding a rule without documenting it fails the
536
+ build, and so does leaving an entry behind after deleting one -- as does a
537
+ number quoted in the README that the catalogue has moved past.
538
+
539
+ [docs/DESIGN.md](docs/DESIGN.md) explains how the pieces fit and why they are
540
+ shaped that way; [CONTRIBUTING.md](CONTRIBUTING.md) covers how a new rule earns
541
+ its place.
542
+
543
+ ## Security
544
+
545
+ See [SECURITY.md](SECURITY.md). If you find a vulnerability, please report it
546
+ privately rather than opening a public issue.
547
+
548
+ ## License
549
+
550
+ MIT. See [LICENSE](LICENSE).