Sentinel
Deterministic security scanner for GitHub Actions workflows
Scan GitHub Actions workflows for 32 security vulnerabilities. Optional AI-powered remediation via Claude. Pure Ruby stdlib.
Documentation: https://sentinel.copilotkit.dev
Install
# Zero-config for public repos — no GITHUB_TOKEN needed
gem install sentinel-ci
sentinel scan owner/repo
# One-shot (like npx)
gem exec sentinel-ci scan owner/repo
# For private repos or org scanning, set a token
export GITHUB_TOKEN=$(gh auth token)
sentinel scan --org my-org
Requires Ruby 3.2+ and git. Public repos are scanned via shallow clone -- no API token required.
For private repos or --org scanning, set GITHUB_TOKEN.
Usage
# Scan a single repo
sentinel scan owner/repo
# Scan a local checkout
sentinel scan --local /path/to/repo
# Scan an entire GitHub org
sentinel scan --org my-org
# JSON output, filter to high+ severity
sentinel scan --format json --severity high owner/repo
# SARIF output for GitHub Security tab
sentinel scan --format sarif owner/repo > results.sarif
GitHub Action
Use as a GitHub Action to automatically scan workflows on every PR:
- uses: jpr5/sentinel@v1
with:
severity: high
Full workflow example:
name: Workflow Security Scan
on:
pull_request:
paths: ['.github/workflows/**']
permissions:
contents: read
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: jpr5/sentinel@v1
id: scan
with:
severity: high
fail-on-findings: true
Inputs:
| Name | Default | Description |
|---|---|---|
severity |
high |
Minimum severity: critical, high, medium, low |
fail-on-findings |
true |
Fail the check if findings above threshold exist |
Outputs:
| Name | Description |
|---|---|
findings-count |
Total findings at or above severity |
critical-count |
Critical findings count |
high-count |
High findings count |
Findings appear as inline annotations on the PR diff -- critical/high as errors, medium as warnings, low as notices.
Fix mode inputs:
| Name | Default | Description |
|---|---|---|
fix |
false |
Auto-fix findings. Pushes to PR branch, or creates fix PR on main. |
anthropic-key |
-- | Anthropic API key -- enables AI-powered fixes for all 32 rules |
Fix mode outputs:
| Name | Description |
|---|---|
fixes-applied |
Number of findings auto-fixed |
When fix: true on a pull request: fixes are pushed directly to the PR branch.
When fix: true on main/push: a new sentinel/fix-* PR is created.
Pre-commit Hook
Scan workflow files automatically before every commit:
# Auto-install
sentinel hook install
# Manual removal
sentinel hook uninstall
Works with hook managers too:
# husky
echo 'sentinel hook run' >> .husky/pre-commit
# lefthook (lefthook.yml)
pre-commit:
commands:
sentinel:
glob: ".github/workflows/*.{yml,yaml}"
run: sentinel hook run
The hook only runs when .github/workflows/*.yml files are staged, so it won't slow down unrelated commits.
Policy-as-Code
Define security standards in .sentinel-ci.yml:
severity: high
rules:
missing-timeouts: medium
overly-broad-triggers: "off"
ignore:
- ".github/workflows/dependabot-*.yml"
exceptions:
- rule: credential-window
file: publish-release.yml
reason: "Intentional late-injection pattern"
The scanner and GitHub Action both read this file automatically.
Platform Support
Sentinel scans GitHub Actions (default), GitLab CI, and Bitbucket Pipelines:
sentinel scan --local . --platform auto # detect automatically
sentinel scan --local . --platform gitlab # GitLab CI only
sentinel scan --local . --platform bitbucket # Bitbucket only
What It Checks
| # | Rule | Severity | What |
|---|---|---|---|
| 1 | unpinned-actions |
medium/low | Tag-pinned actions (medium for third-party, low for actions/*) |
| 2 | shell-injection-expr |
critical | Attacker-controllable ${{ }} in run: blocks |
| 3 | shell-injection-jq |
critical | ${VAR} in double-quoted jq/curl strings |
| 4 | hardcoded-secrets |
critical | AWS keys, GitHub PATs, private keys, passwords in plain text |
| 5 | self-hosted-runner-fork |
high | Self-hosted runner on fork PR triggers |
| 6 | github-script-injection |
critical | Attacker-controllable ${{ }} in github-script |
| 7 | dangerous-triggers |
critical | pull_request_target + fork code checkout |
| 8 | missing-persist-credentials |
high | actions/checkout without persist-credentials: false |
| 9 | credential-window |
high | Git credentials configured far from push step |
| 10 | static-aws-credentials |
medium | Static AWS keys instead of OIDC federation |
| 11 | unscoped-app-token |
medium | create-github-app-token without permission-* scoping |
| 12 | docker-build-arg-secrets |
medium | Secrets in Docker build-args (visible in image layers) |
| 13 | build-publish-same-job |
high | Build + publish in same job with publish secrets |
| 14 | curl-pipe-shell |
high | `curl \ |
| 15 | workflow-dispatch-injection |
high | ${{ inputs.* }} in run blocks |
| 16 | missing-permissions |
medium | No top-level permissions block |
| 17 | git-config-global |
low | git config --global with credentials |
| 18 | missing-timeouts |
low | Jobs without timeout-minutes |
| 19 | missing-env-protection |
medium | Publish/deploy jobs without environment protection |
| 20 | allow-forks-artifact |
medium | Fork-produced artifact download in privileged context |
| 21 | missing-frozen-lockfile |
medium | Package install without --frozen-lockfile / npm ci |
| 22 | cache-poisoning |
medium | Cache keys with fork-controllable refs |
| 23 | excessive-permissions |
low | Write permissions on jobs that only read |
| 24 | unpinned-artifact |
low | download-artifact without specific name |
| 25 | unpinned-docker-image |
low | Docker images using :latest tag |
| 26 | overly-broad-triggers |
low | Push/PR triggers without branch/path filters |
| 27 | missing-dependabot |
low | No Dependabot config for github-actions ecosystem |
| 28 | missing-zizmor |
low | No zizmor static analysis workflow |
| 29 | ide-config-injection |
critical | Workflow writes to IDE/AI config files (.claude/, .vscode/tasks.json) |
| 30 | dangerous-lifecycle-scripts |
medium | Package install without --ignore-scripts in workflows with secrets |
| 31 | github-dependency-refs |
medium | Direct GitHub commit/branch ref in package install |
| 32 | jq-arg-escape-sequences |
medium | jq --arg value contains backslash escape sequences that won't be interpreted |
Auto-Fix
Sentinel can automatically fix findings -- 6 rules mechanically, all 32 with AI:
# Mechanical fixes (free, deterministic)
sentinel fix --local .
sentinel fix --local . --dry-run # preview changes
# All rules via Claude Opus
sentinel fix --local . --ai
# Fix a remote repo (creates a PR)
sentinel fix owner/repo
Mechanically fixable rules:
| Rule | Fix Strategy |
|---|---|
unpinned-actions |
Resolves tag to SHA via GitHub API |
shell-injection-expr |
Moves expression to step-level env: block |
missing-persist-credentials |
Adds persist-credentials: false to checkout |
workflow-dispatch-injection |
Moves ${{ inputs.* }} to step-level env: block |
missing-permissions |
Adds permissions: contents: read at workflow level |
missing-timeouts |
Adds timeout-minutes: 30 to jobs |
With --ai, Sentinel uses Claude Opus to fix all remaining rules that require understanding workflow intent.
PR Bot
Proactively scan popular public repos and open fix PRs for critical findings.
ruby bot/scanner_bot.rb --pattern shell-injection --dry-run
Features:
- GitHub Code Search to find vulnerable repos
- Auto-generates fix PRs for mechanically-fixable rules
- Rate limited (50 PRs/day), stars threshold (>100)
- Opt-out support, clear bot identity
- Runs as daily cron via GitHub Actions
MCP Server
Use Sentinel as a tool in AI coding agents (Claude Code, Copilot, Cursor):
# Start the MCP server
sentinel mcp
# Configure in Claude Code (~/.claude.json)
{
"mcpServers": {
"sentinel": {
"command": "sentinel",
"args": ["mcp"]
}
}
}
Three tools available: sentinel_scan, sentinel_deps, sentinel_fix.
Supply Chain Analysis
Map third-party action dependencies with risk scoring:
sentinel deps --local .
sentinel deps owner/repo
sentinel deps --org my-org --format json
Options
--format FORMAT terminal (default), json, or sarif
--severity LEVEL minimum severity: critical, high, medium, low (default: low)
--local PATH scan local directory
--org ORG scan all repos in a GitHub org (requires GITHUB_TOKEN)
--token TOKEN GitHub API token — only needed for private repos and --org scanning
--platform PLAT github (default), gitlab, bitbucket, or auto
--dry-run preview changes without writing files (fix mode)
--ai enable AI-powered fixes via Claude
--model MODEL AI model to use (default: claude-opus-4-20250514)
--ai-key KEY Anthropic API key for AI fixes
Exit Codes
0-- no critical or high findings1-- critical or high findings present2-- usage error
Architecture
bin/sentinel # CLI entry point (subcommand dispatcher)
action/
annotate.rb # GitHub Action annotation emitter
lib/
scanner.rb # orchestrator
rule_engine.rb # loads + runs all rules
workflow.rb # YAML parser + helpers
finding.rb # finding data struct
github_client.rb # GitHub API client
local_client.rb # filesystem client
clone_client.rb # git-clone client for public repos
auto_fix.rb # mechanical auto-fix engine
ai_fix.rb # AI-powered fix via Claude
sha_resolver.rb # GitHub tag -> SHA resolver
policy.rb # policy-as-code engine (.sentinel-ci.yml)
supply_chain.rb # action dependency graph + risk scoring
version.rb # gem version constant
cli/
scan.rb # sentinel scan subcommand
fix.rb # sentinel fix subcommand
bot.rb # sentinel bot subcommand
hook.rb # sentinel hook install/uninstall
deps.rb # sentinel deps subcommand
token_resolver.rb # GitHub token lookup chain
formatter/
terminal.rb # colored terminal output
json.rb # JSON output
sarif.rb # SARIF output for GitHub Security tab
platforms/
gitlab.rb # GitLab CI pipeline scanner
bitbucket.rb # Bitbucket Pipelines scanner
shared_patterns.rb # cross-platform rule patterns
rules/
base.rb # abstract rule interface
*.rb # one file per rule (29 rules)
mcp/
server.rb # MCP server for AI coding agents
claude-code-config.json # example configuration for Claude Code
bot/
scanner_bot.rb # PR bot orchestrator
search.rb # GitHub Code Search client
state.rb # JSON-file state tracking
state.json # persisted bot state
pr_writer.rb # cross-fork PR creation
config.rb # bot configuration
github_app_auth.rb # GitHub App JWT + installation token auth
web.rb # bot web dashboard
Adding Rules
Create lib/rules/my_rule.rb:
module Rules
class MyRule < Base
def name = "my-rule"
def description = "What this detects"
def severity = :high # :critical, :high, :medium, :low
def check(workflow)
findings = []
# workflow.uses_actions, workflow.run_blocks, workflow.raw_lines, etc.
# Use finding() helper or construct Finding.new() directly
findings
end
end
end
Rules are auto-discovered from lib/rules/.
License
MIT