sentinel-ci 1.0.1 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d80229adb1733c9bd79d6770c46d0b117702fc4ff1fe71e99d1afa5fe3b78ffe
4
- data.tar.gz: 2592ada1faa0fbf3917431c6baf4b341ab34a5d9d424a7caee20ec7a701e2779
3
+ metadata.gz: 93bcfc862a8cc368754114af437432420d70876e8496ecde629658886e888941
4
+ data.tar.gz: 1fa24fc8dbd87c0dc7ed329c4897213bf49f096ab1281b8a421b61cbef0268a9
5
5
  SHA512:
6
- metadata.gz: ccd23b049b0582c04b90baa5a9112197fb7c0407078edfd043c3ddcf93857a9adade4a9c83a1a46c6fe9dcc2ea1cd2c133664dc3c6f19a0a79617fe741dc163d
7
- data.tar.gz: da385d744d1897516f8422bc80701076c972a82014175ba1a6c0b4eac2b86852169b5635b7ad30a25008b45fe903dfcf5346a89e3f3f2f275fdca32454d51f00
6
+ metadata.gz: fff33f695f1031ac4f720b2e6ba034b50cb17001dcec2dad92f0448cc5c1709ea61fb0e39152cb104a9f7d7b264aa4110bc0478c1a620620d9d98579f4eca213
7
+ data.tar.gz: e102e7cb5281042fba55955eb55ca0f691adc1e391a3c5762bd18e53758422f53ba47394e7d08847872c2d7a05de35e07e2105dee6e49a6c96c028f943445ab0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.1.0 (2026-05-18)
4
+
5
+ ### Severity Re-ranking
6
+ - Severities re-evaluated based on actual exploitability
7
+ - Only actively exploitable rules (shell injection, dangerous triggers, hardcoded secrets) are critical
8
+ - Unpinned actions downgraded to medium (requires maintainer compromise first)
9
+ - First-party actions (actions/*) downgraded to low
10
+
11
+ ### Bot Improvements
12
+ - Consolidated PRs: one PR per repo instead of one per rule
13
+ - Rule explainer pages at sentinel-bot.copilotkit.dev/rules/*
14
+ - `--limit N` flag to cap repos scanned per run
15
+ - Skip repos that already use Sentinel
16
+ - Fix PR body includes "How this was detected" methodology disclosure
17
+ - Adopt + opt-out links with UUID tokens
18
+
19
+ ### Infrastructure
20
+ - Bot web handler live at sentinel-bot.copilotkit.dev (Sinatra on Railway)
21
+ - GitHub App created (sentinel-ci-scanner) for future bot identity
22
+ - GHCR-based deploy pipeline with env var trigger for Railway
23
+ - Rule explainer pages served from markdown with dark theme
24
+
25
+ ### Bug Fixes
26
+ - file_exists? returning true for 404s
27
+ - Sentinel skip check matching bare word instead of uses: reference
28
+ - Railway deploy: env var change triggers fresh image pull (serviceInstanceRedeploy doesn't)
29
+ - Sinatra host authorization in production mode
30
+
31
+
3
32
  ## 1.0.1 (2026-05-17)
4
33
 
5
34
  - Smart clone auth: try HTTPS, SSH, then gh token — no manual GITHUB_TOKEN needed for private repos
data/README.md CHANGED
@@ -97,6 +97,22 @@ jobs:
97
97
  Findings appear as inline annotations on the PR diff -- critical/high as errors,
98
98
  medium as warnings, low as notices.
99
99
 
100
+ **Fix mode inputs:**
101
+
102
+ | Name | Default | Description |
103
+ |------|---------|-------------|
104
+ | `fix` | `false` | Auto-fix findings. Pushes to PR branch, or creates fix PR on main. |
105
+ | `anthropic-key` | -- | Anthropic API key -- enables AI-powered fixes for all 28 rules |
106
+
107
+ **Fix mode outputs:**
108
+
109
+ | Name | Description |
110
+ |------|-------------|
111
+ | `fixes-applied` | Number of findings auto-fixed |
112
+
113
+ When `fix: true` on a **pull request**: fixes are pushed directly to the PR branch.
114
+ When `fix: true` on **main/push**: a new `sentinel/fix-*` PR is created.
115
+
100
116
  ## Pre-commit Hook
101
117
 
102
118
  Scan workflow files automatically before every commit:
@@ -125,34 +141,66 @@ pre-commit:
125
141
 
126
142
  The hook only runs when `.github/workflows/*.yml` files are staged, so it won't slow down unrelated commits.
127
143
 
144
+ ## Policy-as-Code
145
+
146
+ Define security standards in `.sentinel-ci.yml`:
147
+
148
+ ```yaml
149
+ severity: high
150
+
151
+ rules:
152
+ missing-timeouts: medium
153
+ overly-broad-triggers: "off"
154
+
155
+ ignore:
156
+ - ".github/workflows/dependabot-*.yml"
157
+
158
+ exceptions:
159
+ - rule: credential-window
160
+ file: publish-release.yml
161
+ reason: "Intentional late-injection pattern"
162
+ ```
163
+
164
+ The scanner and GitHub Action both read this file automatically.
165
+
166
+ ## Platform Support
167
+
168
+ Sentinel scans GitHub Actions (default), GitLab CI, and Bitbucket Pipelines:
169
+
170
+ ```bash
171
+ sentinel scan --local . --platform auto # detect automatically
172
+ sentinel scan --local . --platform gitlab # GitLab CI only
173
+ sentinel scan --local . --platform bitbucket # Bitbucket only
174
+ ```
175
+
128
176
  ## What It Checks
129
177
 
130
178
  | # | Rule | Severity | What |
131
179
  |---|------|----------|------|
132
- | 1 | `unpinned-actions` | critical/medium | Tag-pinned actions (critical for third-party, medium for `actions/*`) |
180
+ | 1 | `unpinned-actions` | medium/low | Tag-pinned actions (medium for third-party, low for `actions/*`) |
133
181
  | 2 | `shell-injection-expr` | critical | Attacker-controllable `${{ }}` in `run:` blocks |
134
182
  | 3 | `shell-injection-jq` | critical | `${VAR}` in double-quoted jq/curl strings |
135
183
  | 4 | `hardcoded-secrets` | critical | AWS keys, GitHub PATs, private keys, passwords in plain text |
136
- | 5 | `self-hosted-runner-fork` | critical | Self-hosted runner on fork PR triggers |
184
+ | 5 | `self-hosted-runner-fork` | high | Self-hosted runner on fork PR triggers |
137
185
  | 6 | `github-script-injection` | critical | Attacker-controllable `${{ }}` in github-script |
138
186
  | 7 | `dangerous-triggers` | critical | `pull_request_target` + fork code checkout |
139
187
  | 8 | `missing-persist-credentials` | high | `actions/checkout` without `persist-credentials: false` |
140
188
  | 9 | `credential-window` | high | Git credentials configured far from push step |
141
- | 10 | `static-aws-credentials` | high | Static AWS keys instead of OIDC federation |
142
- | 11 | `unscoped-app-token` | high | `create-github-app-token` without `permission-*` scoping |
143
- | 12 | `docker-build-arg-secrets` | high | Secrets in Docker build-args (visible in image layers) |
189
+ | 10 | `static-aws-credentials` | medium | Static AWS keys instead of OIDC federation |
190
+ | 11 | `unscoped-app-token` | medium | `create-github-app-token` without `permission-*` scoping |
191
+ | 12 | `docker-build-arg-secrets` | medium | Secrets in Docker build-args (visible in image layers) |
144
192
  | 13 | `build-publish-same-job` | high | Build + publish in same job with publish secrets |
145
193
  | 14 | `curl-pipe-shell` | high | `curl \| sh` without integrity verification |
146
194
  | 15 | `workflow-dispatch-injection` | high | `${{ inputs.* }}` in run blocks |
147
195
  | 16 | `missing-permissions` | medium | No top-level permissions block |
148
- | 17 | `git-config-global` | medium | `git config --global` with credentials |
149
- | 18 | `missing-timeouts` | medium | Jobs without `timeout-minutes` |
196
+ | 17 | `git-config-global` | low | `git config --global` with credentials |
197
+ | 18 | `missing-timeouts` | low | Jobs without `timeout-minutes` |
150
198
  | 19 | `missing-env-protection` | medium | Publish/deploy jobs without environment protection |
151
199
  | 20 | `allow-forks-artifact` | medium | Fork-produced artifact download in privileged context |
152
200
  | 21 | `missing-frozen-lockfile` | medium | Package install without `--frozen-lockfile` / `npm ci` |
153
201
  | 22 | `cache-poisoning` | medium | Cache keys with fork-controllable refs |
154
- | 23 | `excessive-permissions` | medium | Write permissions on jobs that only read |
155
- | 24 | `unpinned-artifact` | medium | download-artifact without specific name |
202
+ | 23 | `excessive-permissions` | low | Write permissions on jobs that only read |
203
+ | 24 | `unpinned-artifact` | low | download-artifact without specific name |
156
204
  | 25 | `unpinned-docker-image` | low | Docker images using `:latest` tag |
157
205
  | 26 | `overly-broad-triggers` | low | Push/PR triggers without branch/path filters |
158
206
  | 27 | `missing-dependabot` | low | No Dependabot config for github-actions ecosystem |
@@ -160,29 +208,32 @@ The hook only runs when `.github/workflows/*.yml` files are staged, so it won't
160
208
 
161
209
  ## Auto-Fix
162
210
 
163
- Sentinel can automatically generate fixes for three rule categories:
211
+ Sentinel can automatically fix findings -- 6 rules mechanically, all 28 with AI:
164
212
 
165
213
  ```bash
166
- sentinel scan --fix owner/repo # future CLI flag
167
- ```
214
+ # Mechanical fixes (free, deterministic)
215
+ sentinel fix --local .
216
+ sentinel fix --local . --dry-run # preview changes
168
217
 
169
- Or use the Ruby API directly:
218
+ # All rules via Claude Opus
219
+ sentinel fix --local . --ai
170
220
 
171
- ```ruby
172
- require_relative "lib/auto_fix"
173
- require_relative "lib/sha_resolver"
174
-
175
- resolver = ShaResolver.new
176
- patched = AutoFix.apply(finding, raw_yaml, sha_resolver: resolver)
221
+ # Fix a remote repo (creates a PR)
222
+ sentinel fix owner/repo
177
223
  ```
178
224
 
179
- **Fixable rules:**
225
+ **Mechanically fixable rules:**
180
226
 
181
227
  | Rule | Fix Strategy |
182
228
  |------|-------------|
183
229
  | `unpinned-actions` | Resolves tag to SHA via GitHub API |
184
230
  | `shell-injection-expr` | Moves expression to step-level `env:` block |
185
231
  | `missing-persist-credentials` | Adds `persist-credentials: false` to checkout |
232
+ | `workflow-dispatch-injection` | Moves `${{ inputs.* }}` to step-level `env:` block |
233
+ | `missing-permissions` | Adds `permissions: contents: read` at workflow level |
234
+ | `missing-timeouts` | Adds `timeout-minutes: 30` to jobs |
235
+
236
+ With `--ai`, Sentinel uses Claude Opus to fix all remaining rules that require understanding workflow intent.
186
237
 
187
238
  ## PR Bot
188
239
 
@@ -273,13 +324,18 @@ lib/
273
324
  bot.rb # sentinel bot subcommand
274
325
  hook.rb # sentinel hook install/uninstall
275
326
  deps.rb # sentinel deps subcommand
327
+ token_resolver.rb # GitHub token lookup chain
276
328
  formatter/
277
329
  terminal.rb # colored terminal output
278
330
  json.rb # JSON output
279
331
  sarif.rb # SARIF output for GitHub Security tab
332
+ platforms/
333
+ gitlab.rb # GitLab CI pipeline scanner
334
+ bitbucket.rb # Bitbucket Pipelines scanner
335
+ shared_patterns.rb # cross-platform rule patterns
280
336
  rules/
281
337
  base.rb # abstract rule interface
282
- *.rb # one file per rule (27 rules)
338
+ *.rb # one file per rule (26 rules)
283
339
  mcp/
284
340
  server.rb # MCP server for AI coding agents
285
341
  claude-code-config.json # example configuration for Claude Code
@@ -287,8 +343,10 @@ bot/
287
343
  scanner_bot.rb # PR bot orchestrator
288
344
  search.rb # GitHub Code Search client
289
345
  state.rb # JSON-file state tracking
346
+ state.json # persisted bot state
290
347
  pr_writer.rb # cross-fork PR creation
291
348
  config.rb # bot configuration
349
+ web.rb # bot web dashboard
292
350
  ```
293
351
 
294
352
  ## Adding Rules
data/lib/github_client.rb CHANGED
@@ -49,8 +49,7 @@ class GitHubClient
49
49
  end
50
50
 
51
51
  def file_exists?(repo, path)
52
- api_get("/repos/#{repo}/contents/#{path}")
53
- true
52
+ !api_get("/repos/#{repo}/contents/#{path}").nil?
54
53
  rescue StandardError
55
54
  false
56
55
  end
@@ -2,7 +2,7 @@ module Rules
2
2
  class DockerBuildArgSecrets < Base
3
3
  def name = "docker-build-arg-secrets"
4
4
  def description = "Secrets passed as Docker build-args (visible in image layers)"
5
- def severity = :high
5
+ def severity = :medium
6
6
 
7
7
  def check(workflow)
8
8
  findings = []
@@ -2,7 +2,7 @@ module Rules
2
2
  class ExcessivePermissions < Base
3
3
  def name = "excessive-permissions"
4
4
  def description = "Job has write permissions but no steps that appear to need them"
5
- def severity = :medium
5
+ def severity = :low
6
6
 
7
7
  # Actions that perform write operations
8
8
  WRITE_ACTIONS = [
@@ -2,7 +2,7 @@ module Rules
2
2
  class GitConfigGlobal < Base
3
3
  def name = "git-config-global"
4
4
  def description = "git config --global persists credentials beyond the repo clone"
5
- def severity = :medium
5
+ def severity = :low
6
6
 
7
7
  def check(workflow)
8
8
  findings = []
@@ -2,7 +2,7 @@ module Rules
2
2
  class MissingTimeouts < Base
3
3
  def name = "missing-timeouts"
4
4
  def description = "Job without timeout-minutes"
5
- def severity = :medium
5
+ def severity = :low
6
6
 
7
7
  def check(workflow)
8
8
  findings = []
@@ -2,7 +2,7 @@ module Rules
2
2
  class SelfHostedRunnerFork < Base
3
3
  def name = "self-hosted-runner-fork"
4
4
  def description = "Self-hosted runner exposed to fork PRs"
5
- def severity = :critical
5
+ def severity = :high
6
6
 
7
7
  FORK_TRIGGERS = %w[pull_request pull_request_target].freeze
8
8
 
@@ -2,7 +2,7 @@ module Rules
2
2
  class StaticAwsCredentials < Base
3
3
  def name = "static-aws-credentials"
4
4
  def description = "AWS credentials using static keys instead of OIDC"
5
- def severity = :high
5
+ def severity = :medium
6
6
 
7
7
  def check(workflow)
8
8
  findings = []
@@ -2,7 +2,7 @@ module Rules
2
2
  class UnpinnedActions < Base
3
3
  def name = "unpinned-actions"
4
4
  def description = "Action referenced by tag instead of SHA pin"
5
- def severity = :critical
5
+ def severity = :medium
6
6
 
7
7
  SHA_PATTERN = /@[0-9a-f]{40}\b/
8
8
  FIRST_PARTY = %w[actions/ github/].freeze
@@ -17,7 +17,7 @@ module Rules
17
17
  next if uses.match?(SHA_PATTERN)
18
18
 
19
19
  first_party = FIRST_PARTY.any? { |prefix| uses.start_with?(prefix) }
20
- sev = first_party ? :medium : :critical
20
+ sev = first_party ? :low : :medium
21
21
 
22
22
  findings << Finding.new(
23
23
  rule: name,
@@ -2,7 +2,7 @@ module Rules
2
2
  class UnpinnedArtifact < Base
3
3
  def name = "unpinned-artifact"
4
4
  def description = "download-artifact without specific artifact name"
5
- def severity = :medium
5
+ def severity = :low
6
6
 
7
7
  DOWNLOAD_ARTIFACT_PATTERN = /\bactions\/download-artifact\b/
8
8
 
@@ -2,7 +2,7 @@ module Rules
2
2
  class UnscopedAppToken < Base
3
3
  def name = "unscoped-app-token"
4
4
  def description = "GitHub App token without scoped permissions"
5
- def severity = :high
5
+ def severity = :medium
6
6
 
7
7
  def check(workflow)
8
8
  findings = []
data/lib/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Sentinel
2
- VERSION = "1.0.1"
2
+ VERSION = "1.1.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sentinel-ci
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Ritter
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-17 00:00:00.000000000 Z
11
+ date: 2026-05-18 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Scan GitHub Actions workflows for 28 security vulnerabilities. SHA pinning,
14
14
  shell injection, credential exposure, dangerous triggers. Optional AI-powered remediation