sentinel-ci 0.1.0
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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +235 -0
- data/bin/gh-workflow-scanner +1 -0
- data/bin/sentinel +57 -0
- data/lib/auto_fix.rb +485 -0
- data/lib/cli/bot.rb +53 -0
- data/lib/cli/fix.rb +50 -0
- data/lib/cli/scan.rb +145 -0
- data/lib/clone_client.rb +64 -0
- data/lib/finding.rb +27 -0
- data/lib/formatter/json.rb +18 -0
- data/lib/formatter/terminal.rb +47 -0
- data/lib/github_client.rb +98 -0
- data/lib/local_client.rb +33 -0
- data/lib/rule_engine.rb +39 -0
- data/lib/rules/allow_forks_artifact.rb +22 -0
- data/lib/rules/base.rb +33 -0
- data/lib/rules/build_publish_same_job.rb +39 -0
- data/lib/rules/credential_window.rb +43 -0
- data/lib/rules/curl_pipe_shell.rb +29 -0
- data/lib/rules/dangerous_triggers.rb +43 -0
- data/lib/rules/docker_build_arg_secrets.rb +30 -0
- data/lib/rules/git_config_global.rb +25 -0
- data/lib/rules/missing_env_protection.rb +37 -0
- data/lib/rules/missing_frozen_lockfile.rb +28 -0
- data/lib/rules/missing_permissions.rb +18 -0
- data/lib/rules/missing_persist_creds.rb +51 -0
- data/lib/rules/missing_timeouts.rb +25 -0
- data/lib/rules/overly_broad_triggers.rb +31 -0
- data/lib/rules/shell_injection_expr.rb +57 -0
- data/lib/rules/shell_injection_jq.rb +59 -0
- data/lib/rules/static_aws_credentials.rb +33 -0
- data/lib/rules/unpinned_actions.rb +35 -0
- data/lib/rules/unpinned_docker_image.rb +25 -0
- data/lib/rules/unscoped_app_token.rb +31 -0
- data/lib/scanner.rb +95 -0
- data/lib/sha_resolver.rb +60 -0
- data/lib/version.rb +3 -0
- data/lib/workflow.rb +100 -0
- metadata +84 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 32b245849733b662cb30bc87855190f0f85529a1467ff63414c9200aa6293251
|
|
4
|
+
data.tar.gz: 3ff45c00a69a18e351a94fdeec5540350ca5dc48b7e38f45d89a72a84698b7ae
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 843c0f319cf5666311abc62e87b939d717fb1c0b2ef028b23d528c5ba5b3aa99d7ea9740c713cc79bd6463d4dd99e379d52b0016b975faeb2a43eb18f2e660a4
|
|
7
|
+
data.tar.gz: 264f40e687806d1eb6a3af350ab265160b8d9d67f26769dab3349549298ff532d44df8884377adaeebf16e483cf58749253fdcd6ef17979931ed26fd35893f54
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jordan Ritter
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Sentinel
|
|
2
|
+
|
|
3
|
+
**Deterministic security scanner for GitHub Actions workflows**
|
|
4
|
+
|
|
5
|
+
<!-- badges -->
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
Scan GitHub Actions workflows for 21 security vulnerabilities. No AI, no gems -- pure Ruby stdlib.
|
|
11
|
+
|
|
12
|
+
Documentation: https://sentinel.copilotkit.dev
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Zero-config for public repos — no GITHUB_TOKEN needed
|
|
18
|
+
gem install sentinel-ci
|
|
19
|
+
sentinel scan owner/repo
|
|
20
|
+
|
|
21
|
+
# One-shot (like npx)
|
|
22
|
+
gem exec sentinel-ci scan owner/repo
|
|
23
|
+
|
|
24
|
+
# For private repos or org scanning, set a token
|
|
25
|
+
export GITHUB_TOKEN=$(gh auth token)
|
|
26
|
+
sentinel scan --org my-org
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requires Ruby 3.2+ and `git`. Public repos are scanned via shallow clone -- no API token required.
|
|
30
|
+
For private repos or `--org` scanning, set `GITHUB_TOKEN`.
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Scan a single repo
|
|
36
|
+
bin/gh-workflow-scanner owner/repo
|
|
37
|
+
|
|
38
|
+
# Scan a local checkout
|
|
39
|
+
bin/gh-workflow-scanner --local /path/to/repo
|
|
40
|
+
|
|
41
|
+
# Scan an entire GitHub org
|
|
42
|
+
bin/gh-workflow-scanner --org my-org
|
|
43
|
+
|
|
44
|
+
# JSON output, filter to high+ severity
|
|
45
|
+
bin/gh-workflow-scanner --format json --severity high owner/repo
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## GitHub Action
|
|
49
|
+
|
|
50
|
+
Use as a GitHub Action to automatically scan workflows on every PR:
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
- uses: jpr5/gh-workflow-scanner-action@v1
|
|
54
|
+
with:
|
|
55
|
+
severity: high
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Full workflow example:
|
|
59
|
+
|
|
60
|
+
```yaml
|
|
61
|
+
name: Workflow Security Scan
|
|
62
|
+
on:
|
|
63
|
+
pull_request:
|
|
64
|
+
paths: ['.github/workflows/**']
|
|
65
|
+
permissions:
|
|
66
|
+
contents: read
|
|
67
|
+
jobs:
|
|
68
|
+
scan:
|
|
69
|
+
runs-on: ubuntu-latest
|
|
70
|
+
steps:
|
|
71
|
+
- uses: actions/checkout@v4
|
|
72
|
+
- uses: jpr5/gh-workflow-scanner-action@v1
|
|
73
|
+
id: scan
|
|
74
|
+
with:
|
|
75
|
+
severity: high
|
|
76
|
+
fail-on-findings: true
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Inputs:**
|
|
80
|
+
|
|
81
|
+
| Name | Default | Description |
|
|
82
|
+
|------|---------|-------------|
|
|
83
|
+
| `severity` | `high` | Minimum severity: `critical`, `high`, `medium`, `low` |
|
|
84
|
+
| `fail-on-findings` | `true` | Fail the check if findings above threshold exist |
|
|
85
|
+
|
|
86
|
+
**Outputs:**
|
|
87
|
+
|
|
88
|
+
| Name | Description |
|
|
89
|
+
|------|-------------|
|
|
90
|
+
| `findings-count` | Total findings at or above severity |
|
|
91
|
+
| `critical-count` | Critical findings count |
|
|
92
|
+
| `high-count` | High findings count |
|
|
93
|
+
|
|
94
|
+
Findings appear as inline annotations on the PR diff -- critical/high as errors,
|
|
95
|
+
medium as warnings, low as notices.
|
|
96
|
+
|
|
97
|
+
## What It Checks
|
|
98
|
+
|
|
99
|
+
| # | Rule | Severity | What |
|
|
100
|
+
|---|------|----------|------|
|
|
101
|
+
| 1 | `unpinned-actions` | critical/medium | Tag-pinned actions (critical for third-party, medium for `actions/*`) |
|
|
102
|
+
| 2 | `shell-injection-expr` | critical | Attacker-controllable `${{ }}` in `run:` blocks |
|
|
103
|
+
| 3 | `shell-injection-jq` | critical | `${VAR}` in double-quoted jq/curl strings |
|
|
104
|
+
| 4 | `dangerous-triggers` | critical | `pull_request_target` + fork code checkout |
|
|
105
|
+
| 5 | `missing-persist-credentials` | high | `actions/checkout` without `persist-credentials: false` |
|
|
106
|
+
| 6 | `credential-window` | high | Git credentials configured far from push step |
|
|
107
|
+
| 7 | `static-aws-credentials` | high | Static AWS keys instead of OIDC federation |
|
|
108
|
+
| 8 | `unscoped-app-token` | high | `create-github-app-token` without `permission-*` scoping |
|
|
109
|
+
| 9 | `docker-build-arg-secrets` | high | Secrets in Docker build-args (visible in image layers) |
|
|
110
|
+
| 10 | `build-publish-same-job` | high | Build + publish in same job with publish secrets |
|
|
111
|
+
| 11 | `curl-pipe-shell` | high | `curl \| sh` without integrity verification |
|
|
112
|
+
| 12 | `missing-permissions` | medium | No top-level permissions block |
|
|
113
|
+
| 13 | `git-config-global` | medium | `git config --global` with credentials |
|
|
114
|
+
| 14 | `missing-timeouts` | medium | Jobs without `timeout-minutes` |
|
|
115
|
+
| 15 | `missing-env-protection` | medium | Publish/deploy jobs without environment protection |
|
|
116
|
+
| 16 | `allow-forks-artifact` | medium | Fork-produced artifact download in privileged context |
|
|
117
|
+
| 17 | `missing-frozen-lockfile` | medium | Package install without `--frozen-lockfile` / `npm ci` |
|
|
118
|
+
| 18 | `unpinned-docker-image` | low | Docker images using `:latest` tag |
|
|
119
|
+
| 19 | `overly-broad-triggers` | low | Push/PR triggers without branch/path filters |
|
|
120
|
+
| 20 | `missing-dependabot` | low | No Dependabot config for github-actions ecosystem |
|
|
121
|
+
| 21 | `missing-zizmor` | low | No zizmor static analysis workflow |
|
|
122
|
+
|
|
123
|
+
## Auto-Fix
|
|
124
|
+
|
|
125
|
+
Sentinel can automatically generate fixes for three rule categories:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
bin/gh-workflow-scanner --fix owner/repo # future CLI flag
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Or use the Ruby API directly:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
require_relative "lib/auto_fix"
|
|
135
|
+
require_relative "lib/sha_resolver"
|
|
136
|
+
|
|
137
|
+
resolver = ShaResolver.new
|
|
138
|
+
patched = AutoFix.apply(finding, raw_yaml, sha_resolver: resolver)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Fixable rules:**
|
|
142
|
+
|
|
143
|
+
| Rule | Fix Strategy |
|
|
144
|
+
|------|-------------|
|
|
145
|
+
| `unpinned-actions` | Resolves tag to SHA via GitHub API |
|
|
146
|
+
| `shell-injection-expr` | Moves expression to step-level `env:` block |
|
|
147
|
+
| `missing-persist-credentials` | Adds `persist-credentials: false` to checkout |
|
|
148
|
+
|
|
149
|
+
## PR Bot
|
|
150
|
+
|
|
151
|
+
Proactively scan popular public repos and open fix PRs for critical findings.
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
ruby bot/scanner_bot.rb --pattern shell-injection --dry-run
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Features:**
|
|
158
|
+
|
|
159
|
+
- GitHub Code Search to find vulnerable repos
|
|
160
|
+
- Auto-generates fix PRs for mechanically-fixable rules
|
|
161
|
+
- Rate limited (50 PRs/day), stars threshold (>100)
|
|
162
|
+
- Opt-out support, clear bot identity
|
|
163
|
+
- Runs as daily cron via GitHub Actions
|
|
164
|
+
|
|
165
|
+
## Options
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
--format FORMAT terminal (default) or json
|
|
169
|
+
--severity LEVEL minimum severity: critical, high, medium, low (default: low)
|
|
170
|
+
--local PATH scan local directory
|
|
171
|
+
--org ORG scan all repos in a GitHub org (requires GITHUB_TOKEN)
|
|
172
|
+
--token TOKEN GitHub API token — only needed for private repos and --org scanning
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Exit Codes
|
|
176
|
+
|
|
177
|
+
- `0` -- no critical or high findings
|
|
178
|
+
- `1` -- critical or high findings present
|
|
179
|
+
- `2` -- usage error
|
|
180
|
+
|
|
181
|
+
## Architecture
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
bin/gh-workflow-scanner # CLI entry point (optparse)
|
|
185
|
+
action/
|
|
186
|
+
annotate.rb # GitHub Action annotation emitter
|
|
187
|
+
lib/
|
|
188
|
+
scanner.rb # orchestrator
|
|
189
|
+
rule_engine.rb # loads + runs all rules
|
|
190
|
+
workflow.rb # YAML parser + helpers
|
|
191
|
+
finding.rb # finding data struct
|
|
192
|
+
github_client.rb # GitHub API client
|
|
193
|
+
local_client.rb # filesystem client
|
|
194
|
+
auto_fix.rb # auto-fix engine
|
|
195
|
+
sha_resolver.rb # GitHub tag -> SHA resolver
|
|
196
|
+
formatter/
|
|
197
|
+
terminal.rb # colored terminal output
|
|
198
|
+
json.rb # JSON output
|
|
199
|
+
rules/
|
|
200
|
+
base.rb # abstract rule interface
|
|
201
|
+
*.rb # one file per rule (19 rules)
|
|
202
|
+
bot/
|
|
203
|
+
scanner_bot.rb # PR bot orchestrator
|
|
204
|
+
search.rb # GitHub Code Search client
|
|
205
|
+
state.rb # JSON-file state tracking
|
|
206
|
+
pr_writer.rb # cross-fork PR creation
|
|
207
|
+
config.rb # bot configuration
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Adding Rules
|
|
211
|
+
|
|
212
|
+
Create `lib/rules/my_rule.rb`:
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
module Rules
|
|
216
|
+
class MyRule < Base
|
|
217
|
+
def name = "my-rule"
|
|
218
|
+
def description = "What this detects"
|
|
219
|
+
def severity = :high # :critical, :high, :medium, :low
|
|
220
|
+
|
|
221
|
+
def check(workflow)
|
|
222
|
+
findings = []
|
|
223
|
+
# workflow.uses_actions, workflow.run_blocks, workflow.raw_lines, etc.
|
|
224
|
+
# Use finding() helper or construct Finding.new() directly
|
|
225
|
+
findings
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Rules are auto-discovered from `lib/rules/`.
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sentinel
|
data/bin/sentinel
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require_relative "../lib/version"
|
|
4
|
+
|
|
5
|
+
SUBCOMMANDS = %w[scan fix bot version help].freeze
|
|
6
|
+
|
|
7
|
+
HELP_TEXT = <<~HELP
|
|
8
|
+
Usage: sentinel <command> [options] [args]
|
|
9
|
+
|
|
10
|
+
Commands:
|
|
11
|
+
scan [REPO] Scan a repo, org, or local path for vulnerabilities
|
|
12
|
+
fix [REPO] Auto-fix findings (coming soon)
|
|
13
|
+
bot Run the PR bot
|
|
14
|
+
version Print version
|
|
15
|
+
help Show this help message
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
sentinel scan owner/repo
|
|
19
|
+
sentinel scan --local .
|
|
20
|
+
sentinel scan --org myorg --severity high
|
|
21
|
+
sentinel scan --format json owner/repo
|
|
22
|
+
sentinel owner/repo # implicit scan
|
|
23
|
+
|
|
24
|
+
Run 'sentinel <command> --help' for command-specific options.
|
|
25
|
+
HELP
|
|
26
|
+
|
|
27
|
+
subcmd = ARGV.first
|
|
28
|
+
|
|
29
|
+
if subcmd.nil?
|
|
30
|
+
$stderr.puts HELP_TEXT
|
|
31
|
+
exit 0
|
|
32
|
+
elsif subcmd == "-h" || subcmd == "--help"
|
|
33
|
+
puts HELP_TEXT
|
|
34
|
+
exit 0
|
|
35
|
+
elsif subcmd == "--version" || subcmd == "-v"
|
|
36
|
+
puts "sentinel #{Sentinel::VERSION}"
|
|
37
|
+
exit 0
|
|
38
|
+
elsif SUBCOMMANDS.include?(subcmd)
|
|
39
|
+
ARGV.shift
|
|
40
|
+
else
|
|
41
|
+
# Not a known subcommand — default to scan (backwards compatible).
|
|
42
|
+
# This handles both `sentinel owner/repo` and `sentinel --local .`
|
|
43
|
+
subcmd = "scan"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
case subcmd
|
|
47
|
+
when "scan"
|
|
48
|
+
require_relative "../lib/cli/scan"
|
|
49
|
+
when "fix"
|
|
50
|
+
require_relative "../lib/cli/fix"
|
|
51
|
+
when "bot"
|
|
52
|
+
require_relative "../lib/cli/bot"
|
|
53
|
+
when "version"
|
|
54
|
+
puts "sentinel #{Sentinel::VERSION}"
|
|
55
|
+
when "help"
|
|
56
|
+
puts HELP_TEXT
|
|
57
|
+
end
|