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.
- bluerayscan-0.3.0/LICENSE +21 -0
- bluerayscan-0.3.0/PKG-INFO +550 -0
- bluerayscan-0.3.0/README.md +530 -0
- bluerayscan-0.3.0/pyproject.toml +45 -0
- bluerayscan-0.3.0/setup.cfg +4 -0
- bluerayscan-0.3.0/src/bluerayscan.egg-info/PKG-INFO +550 -0
- bluerayscan-0.3.0/src/bluerayscan.egg-info/SOURCES.txt +80 -0
- bluerayscan-0.3.0/src/bluerayscan.egg-info/dependency_links.txt +1 -0
- bluerayscan-0.3.0/src/bluerayscan.egg-info/entry_points.txt +3 -0
- bluerayscan-0.3.0/src/bluerayscan.egg-info/top_level.txt +1 -0
- bluerayscan-0.3.0/src/repo_sentinel/__init__.py +5 -0
- bluerayscan-0.3.0/src/repo_sentinel/__main__.py +6 -0
- bluerayscan-0.3.0/src/repo_sentinel/baseline.py +168 -0
- bluerayscan-0.3.0/src/repo_sentinel/cli.py +262 -0
- bluerayscan-0.3.0/src/repo_sentinel/commands.py +513 -0
- bluerayscan-0.3.0/src/repo_sentinel/config.py +169 -0
- bluerayscan-0.3.0/src/repo_sentinel/discovery.py +274 -0
- bluerayscan-0.3.0/src/repo_sentinel/engine.py +281 -0
- bluerayscan-0.3.0/src/repo_sentinel/findings.py +166 -0
- bluerayscan-0.3.0/src/repo_sentinel/gitignore.py +242 -0
- bluerayscan-0.3.0/src/repo_sentinel/hcl.py +245 -0
- bluerayscan-0.3.0/src/repo_sentinel/heuristics.py +320 -0
- bluerayscan-0.3.0/src/repo_sentinel/jsonish.py +193 -0
- bluerayscan-0.3.0/src/repo_sentinel/report.py +697 -0
- bluerayscan-0.3.0/src/repo_sentinel/rules.py +371 -0
- bluerayscan-0.3.0/src/repo_sentinel/scanners/__init__.py +47 -0
- bluerayscan-0.3.0/src/repo_sentinel/scanners/allowlist.py +117 -0
- bluerayscan-0.3.0/src/repo_sentinel/scanners/ansible.py +259 -0
- bluerayscan-0.3.0/src/repo_sentinel/scanners/appcode.py +379 -0
- bluerayscan-0.3.0/src/repo_sentinel/scanners/azure.py +250 -0
- bluerayscan-0.3.0/src/repo_sentinel/scanners/ci.py +78 -0
- bluerayscan-0.3.0/src/repo_sentinel/scanners/circleci.py +211 -0
- bluerayscan-0.3.0/src/repo_sentinel/scanners/cloudformation.py +334 -0
- bluerayscan-0.3.0/src/repo_sentinel/scanners/compose.py +298 -0
- bluerayscan-0.3.0/src/repo_sentinel/scanners/dependencies.py +535 -0
- bluerayscan-0.3.0/src/repo_sentinel/scanners/dockerfiles.py +276 -0
- bluerayscan-0.3.0/src/repo_sentinel/scanners/filenames.py +331 -0
- bluerayscan-0.3.0/src/repo_sentinel/scanners/gitlab.py +254 -0
- bluerayscan-0.3.0/src/repo_sentinel/scanners/jenkins.py +183 -0
- bluerayscan-0.3.0/src/repo_sentinel/scanners/kubernetes.py +935 -0
- bluerayscan-0.3.0/src/repo_sentinel/scanners/providers.py +672 -0
- bluerayscan-0.3.0/src/repo_sentinel/scanners/secrets.py +497 -0
- bluerayscan-0.3.0/src/repo_sentinel/scanners/shell.py +256 -0
- bluerayscan-0.3.0/src/repo_sentinel/scanners/terraform.py +609 -0
- bluerayscan-0.3.0/src/repo_sentinel/scanners/workflows.py +856 -0
- bluerayscan-0.3.0/src/repo_sentinel/suppression.py +214 -0
- bluerayscan-0.3.0/src/repo_sentinel/wellknown.py +218 -0
- bluerayscan-0.3.0/src/repo_sentinel/yamlish.py +310 -0
- bluerayscan-0.3.0/tests/test_allowlist.py +124 -0
- bluerayscan-0.3.0/tests/test_ansible.py +124 -0
- bluerayscan-0.3.0/tests/test_appcode.py +270 -0
- bluerayscan-0.3.0/tests/test_azure.py +135 -0
- bluerayscan-0.3.0/tests/test_baseline.py +114 -0
- bluerayscan-0.3.0/tests/test_circleci.py +98 -0
- bluerayscan-0.3.0/tests/test_cli.py +753 -0
- bluerayscan-0.3.0/tests/test_cloudformation.py +237 -0
- bluerayscan-0.3.0/tests/test_compose.py +131 -0
- bluerayscan-0.3.0/tests/test_config.py +212 -0
- bluerayscan-0.3.0/tests/test_dependencies.py +324 -0
- bluerayscan-0.3.0/tests/test_discovery.py +141 -0
- bluerayscan-0.3.0/tests/test_dockerfiles.py +151 -0
- bluerayscan-0.3.0/tests/test_engine.py +210 -0
- bluerayscan-0.3.0/tests/test_filenames.py +191 -0
- bluerayscan-0.3.0/tests/test_findings.py +86 -0
- bluerayscan-0.3.0/tests/test_gitignore.py +211 -0
- bluerayscan-0.3.0/tests/test_gitlab.py +135 -0
- bluerayscan-0.3.0/tests/test_hcl.py +92 -0
- bluerayscan-0.3.0/tests/test_heuristics.py +247 -0
- bluerayscan-0.3.0/tests/test_jenkins.py +100 -0
- bluerayscan-0.3.0/tests/test_jsonish.py +94 -0
- bluerayscan-0.3.0/tests/test_kubernetes.py +604 -0
- bluerayscan-0.3.0/tests/test_report.py +502 -0
- bluerayscan-0.3.0/tests/test_robustness.py +431 -0
- bluerayscan-0.3.0/tests/test_rules.py +273 -0
- bluerayscan-0.3.0/tests/test_scanner_contract.py +79 -0
- bluerayscan-0.3.0/tests/test_secrets.py +564 -0
- bluerayscan-0.3.0/tests/test_shell.py +182 -0
- bluerayscan-0.3.0/tests/test_suppression.py +217 -0
- bluerayscan-0.3.0/tests/test_terraform.py +431 -0
- bluerayscan-0.3.0/tests/test_tools.py +91 -0
- bluerayscan-0.3.0/tests/test_workflows.py +640 -0
- 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
|
+
[](https://github.com/KhanSaahib/repo-sentinel/actions/workflows/ci.yml)
|
|
24
|
+
[](https://www.python.org/downloads/)
|
|
25
|
+
[](pyproject.toml)
|
|
26
|
+
[](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).
|