{
  "markdown": "# ci-sentinel 🛡️\n\n**Security auditor for SEVEN CI ecosystems — GitHub Actions, GitLab CI/CD,\nJenkins, CircleCI, Azure Pipelines, Bitbucket Pipelines and Travis CI** — finds\nthe supply-chain and injection flaws a YAML linter can't see, *before* an\nattacker's pull/merge request runs code with your repository secrets.\n\nAvailable as an **MCP server** (for Claude / Cursor / any agent) and a\n**pay-per-call HTTP API** (x402 USDC or a prepaid card key).\n\nGive it your `.github/workflows/*.yml`, `.gitlab-ci.yml`, `Jenkinsfile`,\n`.circleci/config.yml`, `azure-pipelines.yml`, `bitbucket-pipelines.yml` and/or\n`.travis.yml` (each file is auto-routed to the right analyzer) and get a\n**CRITICAL / VULNERABLE / RISKY / HARDENED** verdict with the exact flaws.\n\n## What it catches that a linter can't\n\n| Class | What ci-sentinel does |\n|---|---|\n| 💉 **Expression injection (inter-step / inter-job taint)** | Taints untrusted `${{ github.event.* }}` (issue/PR title, body, comment, review, branch name, commit message, **label name, fork repo identity, edited-from values**) to a `run:` shell **or** `actions/github-script` sink — and **follows it across `steps.<id>.outputs.*`, `needs.<job>.outputs.*`, `env:` vars, `matrix` values and reusable-workflow `inputs.*`**. An attacker who hides the untrusted value behind an intermediate output is still caught; a per-file grep is not. |\n| 🎭 **Pwn requests** | `pull_request_target` / `workflow_run` that **check out untrusted fork code** in the privileged, secret-bearing context = arbitrary code execution with your secrets. |\n| 🔑 **Token permissions** | Excessive `GITHUB_TOKEN` scopes (`write-all`, `contents:write`, `id-token:write`), missing `permissions:` blocks, and **secrets exposed to untrusted triggers**. |\n| 🔁 **Reusable workflows** | `workflow_call` callers passing untrusted data across the boundary as `inputs.*`, and **`secrets: inherit`** handing the full secret set to a reusable workflow on an untrusted trigger. |\n| 📌 **Action pinning** | Third-party actions (and **reusable-workflow `uses:`**) on **mutable tags/branches** instead of a commit SHA (the `tj-actions/changed-files`-style hijack), resolved through the **transitive action graph**. |\n| 🖥️ **Self-hosted RCE** | Self-hosted runners reachable from **public-repo PRs** (RCE on your own infra), and **OIDC `id-token` trust** misuse. |\n\n### …and the same depth for **GitLab CI/CD** (`.gitlab-ci.yml`)\n\n| Class | What ci-sentinel does |\n|---|---|\n| 💉 **CI-variable injection** | Taints untrusted GitLab variables (`CI_COMMIT_REF_NAME/BRANCH/TAG`, `CI_MERGE_REQUEST_TITLE/DESCRIPTION/SOURCE_BRANCH_NAME`, commit message/author, triggering-user fields) interpolated into `script:` — **following the taint through `variables:` and `extends:` templates**. Knows the SAFE vars (`CI_COMMIT_SHA`, `CI_MERGE_REQUEST_IID`, `CI_PROJECT_ID`, …) so it doesn't flag every `$CI_`. |\n| 🎭 **Fork merge-request pwn** | Jobs reachable from **fork MR pipelines** (`merge_request_event`, `only: merge_requests`) that carry **secrets / a broad `CI_JOB_TOKEN` / `id_tokens` (OIDC)**, or that run **privileged work (deploy/publish/`kubectl`/`terraform`) with no `when: manual` gate**. |\n| 📦 **`include:` supply-chain** | `include:` from a **remote URL** or another **project/component on a moving (non-SHA) ref** — foreign pipeline code merged into yours. |\n| ☣️ **Artifact / cache poisoning** | An **untrusted job** writes an artifact/cache that a **privileged downstream job consumes and executes** (`needs:`/`dependencies:` or a shared cache path) — a cross-job **and cross-pipeline** escalation. |\n\n### …and five more CI systems, same taint model\n\n| Ecosystem | What ci-sentinel catches |\n|---|---|\n| 🔧 **Jenkins** (`Jenkinsfile`) | Command injection from `params.*` / `env.CHANGE_*` / `ghprb*` / SCM data into `sh`/`bat`/`powershell` (through `environment{}` bindings); credential-leak (`credentials()`/`withCredentials` printed or baked into a GString); Groovy `evaluate()`/`load` RCE; missing `input()` approval gate; unsafe `agent any`. |\n| ⭕ **CircleCI** (`.circleci/config.yml`) | Shell injection from `<< pipeline.git.branch/tag >>` / pipeline parameters into `run:`; unpinned **orbs** (`@volatile` / bare major); fork-PR **context secret** exposure with no `type: approval` gate; privileged deploy with no approval. |\n| 🟦 **Azure Pipelines** (`azure-pipelines.yml`) | Macro injection from `$(Build.SourceBranch)` / `$(System.PullRequest.SourceBranch)` / commit message into `script:`/`bash:`/`pwsh:`; untrusted **templates** from a foreign repo resource; fork **variable-group** secret exposure; unpinned repository resources. |\n| 🪣 **Bitbucket Pipelines** (`bitbucket-pipelines.yml`) | Shell injection from `$BITBUCKET_BRANCH` / `$BITBUCKET_TAG` / `$BITBUCKET_PR_DESTINATION_BRANCH` / commit message expanded unquoted into `script:`; **secured/deployment-variable** exposure on externally-triggerable PR pipelines; unpinned **pipes** (`:latest`); deployments with no `trigger: manual` gate. |\n| 🚦 **Travis CI** (`.travis.yml`) | Shell injection from `$TRAVIS_BRANCH` / `$TRAVIS_PULL_REQUEST_BRANCH` / `$TRAVIS_COMMIT_MESSAGE` expanded unquoted into a lifecycle hook; **secure-env** PR/fork secret exposure; `deploy:` stages with no `on:` branch/condition gate. |\n\n### …plus two deep cross-domain detectors a one-shot agent can't replicate\n\n| Detector | What ci-sentinel catches |\n|---|---|\n| ☁️ **OIDC cloud-trust misconfiguration** (Terraform / CloudFormation / GCP workload-identity / Azure federated-credential) | Models the **cloud half** of CI OIDC: the trust policy of the IAM role / WIF pool / federated app. Flags a `sub` condition with a broad wildcard (`repo:org/*`, `repo:*`), **no `sub` condition at all** (any workflow on the issuer assumes the role), a repo pinned but ref/environment **un**pinned (any branch assumes), the bare `pull_request` subject (fork-reachable), or an unpinned `aud`. Then **correlates** the IaC trust with the CI side (a workflow that mints `id-token` reachable from an untrusted trigger) and **escalates to critical** when the chain is reachable end-to-end. No single-file linter catches this — it spans the CI claim and the cloud trust policy. |\n| 🔧 **Jenkins shared libraries** (`@Library` → `vars/*.groovy`) | Taints an untrusted pipeline value (PR title / branch / build parameter) passed to a shared-library global-var step **through the library's `call()` interior** to an internal `sh`/`bat` sink — the Jenkins parity of orb/template/composite-action cross-file taint, **invisible when reading only the `Jenkinsfile`**. Also flags `@Library` imports on a **mutable ref** (a branch / default version) as supply-chain risk. |\n\nEach analyzer knows the **SAFE** variables (SHAs, numeric ids, trusted slugs —\n`BITBUCKET_COMMIT`, `TRAVIS_PULL_REQUEST`, `pipeline.git.revision`, …) and the\nSAFE OIDC trusts (`sub` pinned to `repo:org/repo:ref:refs/heads/main` /\n`:environment:prod` with `aud` set) so it doesn't flag every `$VAR` or every\ntrust. Zero false positives on hardened configs.\n\nIt is a real static analyzer: a line-aware workflow YAML parser → a model of\ntriggers/jobs/steps/permissions/actions/outputs/needs → an inter-step/inter-job\ntaint resolver + permission + supply-chain detectors → a scored verdict with\nper-finding **file:line**, the **full taint path**, concrete **remediation**, and a\n**SARIF 2.1.0** report (with `codeFlows`) you can upload to **GitHub code scanning**.\n\n### …and it doesn't just flag — it FIXES, and it GRADES\n\n| Premium | What you get |\n|---|---|\n| 🔧 **Auto-remediation (the exact fix)** | For each finding ci-sentinel produces the **concrete change**: the corrected YAML/Groovy snippet **and a unified diff** you can apply — pin the action/orb/pipe to an immutable SHA/version, env-bind + quote the injected expression, drop write-all to least privilege, insert the manual/approval gate, pin the OIDC `sub`/`aud`, move the hardcoded secret to the store (and rotate it). Not \"you have a bug\" but **\"change THIS to THIS\"**, with a confidence label (`exact` / `template` / `guided`) so you know what's drop-in vs. what needs a value filled. Findings carry a stable `fingerprint` for de-duplication, and the SARIF results carry inline `fixes`. |\n| 📋 **Compliance scorecard (A–F)** | A CIS-like CI security benchmark scored **A–F** with a **PASS / FAIL / WARN / N/A breakdown per control** (least privilege, component pinning, no secrets to forks, no self-hosted on public PRs, OIDC pinned, no hardcoded secrets, no pwn checkout, gated deploys, no injection, no cross-job poisoning). Each control lists the **finding ids** that drove its verdict — defensible to a security team. A directly-exploitable **critical** flaw caps the grade (one open RCE/secret-exfil path dominates the posture). |\n| 🛰️ **Live threat-intelligence feed (the data moat)** | Every `uses:` / orb / pipe in your config is checked against a **live, server-side feed of KNOWN-compromised CI components**, **ingested on a cron from public sources — [OSV.dev](https://osv.dev) and the [GitHub Security Advisory](https://github.com/advisories) database (`ecosystem=actions`)** — and merged onto a curated seed of the flagship incidents (`tj-actions/changed-files` CVE-2025-30066, `reviewdog/action-setup` CVE-2025-30154 and its transitive dependents, the `aquasecurity/trivy-action` tag force-push, Ultralytics). It knows that `tj-actions/changed-files@v45` is **poison** and that the same action **SHA-pinned to a clean commit** or **upgraded to `@v46`** is **safe** — a distinction a static YAML scan or a local linter **cannot** make, because it has no feed. The feed lives **only on the hosted server** (the thin client never carries it); `GET /feed/status` surfaces its freshness. A **live reputation signal** (stars / last-commit / archived / does-the-repo-still-resolve) additionally flags **archived/abandoned** components and **dangling** references that are takeover-bait. **This is the access-moat that justifies the hosted, paid model:** the value is the live DATA, not the compute. |\n| 🪤 **Tag-rewrite & imposter detection (live ref→commit)** | Give ci-sentinel the commit SHA you *expected* a mutable ref to be (from your lockfile/SBOM) and it resolves what the tag **currently** points at upstream — flagging a **tag-rewrite / force-push** (`@v2` now runs a *different* commit than you reviewed — the exact tj-actions attack), a **deleted/dangling ref**, or a **suspiciously recent move**. It also pairs **typosquat detection with the live reputation signal** to confirm **impersonation** — `actons/checkout` (a near-miss of `actions`) whose repo is a freshly-stood-up, low-star or dangling imposter — vs. a legitimately-named small fork (no false positive). And because the feed is keyed by **component identity**, a compromised action is caught **no matter which CI system references it** (cross-CI) — the piece a mono-CI scanner structurally can't have. |\n\n## MCP server (free)\n\n```json\n{\n  \"mcpServers\": {\n    \"ci-sentinel\": { \"command\": \"npx\", \"args\": [\"-y\", \"ci-sentinel-mcp\"] }\n  }\n}\n```\n\nTool: **`audit_ci_security`**. Pass `files` (a map of workflow filename → YAML) or\n`source` (one workflow). The free tier returns the verdict and **how many** issues\nof each kind were found.\n\n## Deep audit (`deep: true`) — two ways to pay\n\nThe deep audit returns **every finding with file:line, the full inter-step/\ninter-job injection taint path, the transitive action supply-chain graph, a\nconcrete fix (corrected snippet + unified diff) per finding, an A–F compliance\nscorecard with a per-control breakdown, and a SARIF 2.1.0 document with inline\n`fixes`** (uploadable to GitHub code scanning via `github/codeql-action/upload-sarif`).\n\n- 💳 **Card (Stripe)** — buy a prepaid key at `https://ci-sentinel.vercel.app/pro/checkout`,\n  then set `\"env\": { \"CI_SENTINEL_KEY\": \"<key>\" }` in your MCP config.\n- 🪙 **x402 (USDC)** — AI agents pay per call automatically (USDC on Base), no signup.\n\n## HTTP API\n\n```\nPOST /audit          # free, rate-limited — verdict + per-severity counts\nPOST /pro/audit      # deep — pay-per-call (x402) or Authorization: Bearer <key>\nPOST /mcp            # MCP-over-HTTP (free)\nGET  /presence       # live active sessions (last 5 min)\nGET  /                # this landing page\n```\n\n```bash\ncurl -X POST https://ci-sentinel.vercel.app/audit \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"files\":{\".github/workflows/ci.yml\":\"name: CI\\non: pull_request_target\\n...\"}}'\n```\n\n## Why hosted? (the moat)\n\nThe npm package is a **thin client**: it runs no analysis locally. The parser,\ntaint engine, action-graph resolver and detector knowledge base run **server-side\nonly**, behind the paywall — so the premium material (findings, evidence, taint\npaths, remediation) never lands on the caller's machine. A `npm pack` ships only\n`dist/mcp.js`, `dist/mcpServer.js` and `dist/types.js`; the engine is never in the\ntarball (verified automatically by `npm run test:moat`).\n\n## How the live feed stays live (the update pipeline)\n\nThe threat-intelligence feed is **ingested, not hard-coded**. A periodic job runs\nserver-side:\n\n1. **Fetch** advisories from two public sources (no paid keys):\n   - **GitHub Security Advisory DB** — `GET /advisories?ecosystem=actions` (public;\n     `GITHUB_TOKEN` only raises the rate limit, it is **not required**).\n   - **OSV.dev** — the `GitHub Actions` ecosystem, queried for the components we\n     already track plus everything GHSA just surfaced (so OSV enriches them).\n2. **Normalize** each advisory into a `ThreatRecord` — owner/name split, incident\n   class inferred from the advisory text (tag-rewrite / maintainer-compromise /\n   malicious / CVE…), severity bucketed, the affected version range parsed into a\n   selector, the **fixed version** captured (so an upgraded pin does **not** fire).\n3. **Merge** the rows onto the curated seed (`ingestFeed`: union refs, keep the\n   worst severity, widen the affected range, de-dup by identity).\n4. **Store** the snapshot in Cloudflare KV. Every `/pro/audit` reads it back via\n   `kvFeedProvider`, **falling back to the compiled seed** if KV is cold — so the\n   detector is never blocked and never worse than the static baseline.\n\n```bash\n# run the refresh once (live: hits the real public APIs)\nnode scripts/refreshFeed.mjs\n# dry run with no network (produces the seed-only snapshot)\nCI_SENTINEL_NO_NET=1 node scripts/refreshFeed.mjs\n```\n\nSchedule it with **Vercel Cron** (`{ \"path\": \"/internal/refresh-feed\",\n\"schedule\": \"0 */6 * * *\" }`, guarded by `FEED_REFRESH_TOKEN`) or a scheduled\nGitHub Action that runs `node scripts/refreshFeed.mjs`. `GET /feed/status` reports\nthe live snapshot's version, `generatedAt` and record count — the public freshness\nproof. None of this ships in the npm client.\n\n## Development\n\n```bash\nnpm install\nnpm run build          # tsc\nnpm test               # engine + parser + taint + SARIF + autofix + compliance + corpus tests (851 checks)\nnpm run test:moat      # lock-proof: tarball ships no engine, deep degrades w/o pay\nnpm run dev:http       # local HTTP server (FORCE_LISTEN)\n```\n\n---\n\nHeuristic static analysis of the workflow YAML you provide — it cannot see\nrepo-level default token permissions, branch protections or org policy, and does\nnot execute the pipeline. Treat findings as leads to verify.\n\nSource & docs: https://github.com/Baneado98/ci-sentinel · MIT\n",
  "bytes": 15388,
  "sha": "2b65a8b68fc382b6e7d8f4768c23c51327665e51a55676f26e44f733e7989148",
  "repo_slug": "baneado98/ci-sentinel",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_baneado98_ci_sentinel_af4e3951/readme"
}