PluginWorld
Ci

civet

MCP

Enables auditing GitHub Actions workflows for security problems directly from an MCP client, returning a risk score and findings with file:line evidence, attack narratives, and fixes. Also exposes the full rule catalog and a one-call demo that writes and scans a deliberately broken workflow.

@xuange-hu · MIT License · updated today

SECURITY

B

SCORE

60

STARS

▲ 0

PLUG IN

git clone https://github.com/xuange-hu/civet.git

See the README to configure this MCP server

README

🐱 civet

A security auditor for GitHub Actions workflows that runs anywhere Python runs.

36 deterministic rules · zero dependencies · evidence-first · SARIF for CI · MCP server

CI tests rules Dependencies MCP Python License: MIT

English · 简体中文


Why another workflow scanner

zizmor (6.7k★, Rust) and actionlint (4.3k★, Go) are excellent. They are also compiled binaries you have to download, and actionlint does not do security audits while zizmor does not do general linting.

civet is the third shape:

zizmor actionlint civet
Language Rust binary Go binary pure Python, 0 deps
Install download a release download a release pip install civet-audit
Works offline / air-gapped needs a binary needs a binary yes — stdlib only
Parses YAML with serde_yaml custom its own reader, so it can keep line numbers
Evidence (file:line + source) yes yes yes, plus the raw line under every finding
Explains the attack yes partly civet explain CIV201 — a narrative per rule
SARIF 2.1.0 yes yes yes
MCP server no no 4 tools

The zero-dependency part is not austerity — it is the point. A scanner you cannot run on the slim container that is your CI is a scanner you will not run.

Install

pip install civet-audit
# or, without installing anything:
uvx --from civet-audit civet .

Three steps

# 1. see it work, on a workflow that is broken on purpose
civet demo

# 2. scan your repository
civet scan .

# 3. understand the worst finding
civet explain CIV201

Requires Python ≥ 3.10. Nothing else. No API keys, no network calls, no LLM in the loop — a scan is a pure function of the bytes on disk, so two runs on the same tree always produce the same report.

What the output looks like

Real output from civet demo (trimmed to high and above):

  scanning ./civet-demo  ·  2 workflow file(s)  ·  36 rules

  .github/workflows/civet-demo.yml
    line 13   HIGH     CIV205  Self-hosted runner reachable from a fork-triggered event
                    job: build
                    Job 'build' runs on a self-hosted runner under ['issue_comment', 'pull_request_target'].
                    › runs-on: self-hosted
    line 15   CRITICAL CIV201  pwn request: pull_request_target + checkout of PR head
                    job: build / step: actions/checkout@v4
                    pull_request_target checks out PR head code while holding a privileged token.
                    › - uses: actions/checkout@v4
    line 20   CRITICAL CIV101  Untrusted event data interpolated into run:
                    job: build / step: Greet the contributor
                    run: interpolates attacker-controlled `${{ github.event.pull_request.title }}`.
                    › - name: Greet the contributor
    line 23   HIGH     CIV501  Remote script piped into a shell
                    job: build / step: Install toolchain
                    Remote content is piped straight into an interpreter.
                    › - name: Install toolchain

  52 findings  ·  critical 3  high 10  medium 22  low 13
  Risk score: 100/100 → CRITICAL RISK

Every line carries: the line number, the job and step it belongs to, the reason in concrete terms, and the raw source line (›) so you can check the claim yourself instead of trusting it.

explain — the part that teaches

$ civet explain CIV201

CIV201  CRITICAL  [trigger]
  pwn request: pull_request_target + checkout of PR head

  WHY
    pull_request_target runs with the base repo's token, and checking out
    github.event.pull_request.head.sha executes the fork's code in that context.
    Any attacker PR gets a write-scoped token and access to secrets.

  FIX
    Use pull_request, or split into two workflows: an unprivileged build that
    uploads an artifact and a privileged workflow_run that consumes it.

  CWE-269

Rule map

Group Rules What it covers
injection 5 ${{ github.event.* }} into run:, github-script, action inputs, untrusted if:
trigger 6 pwn request, pull_request_target, workflow_run, comment triggers, self-hosted + fork
supply-chain 6 SHA pinning, branch pins, persist-credentials, mutable images, reusable-workflow pins
secrets 5 GITHUB_TOKEN in run:, secrets: inherit, third-party secret handoff, job-level secrets
execution 5 curl | bash, eval, unpinned npx/uvx, set-output, GITHUB_ENV writes
artifact 3 cache poisoning, artifact from privileged run, retention
hygiene 6 permissions:, timeout-minutes, environment protection, concurrency
civet rules                     # all 36 with severity
civet rules | grep injection    # one group

CI gate

- name: Audit workflows
  run: |
    pip install civet-audit
    civet scan . --fail-on high --min-severity medium

Exit codes: 0 clean, 1 findings at or above --fail-on, 2 usage error. Default is --fail-on high.

Upload to code scanning:

- run: civet scan . --format sarif -o civet.sarif --fail-on none
- uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: civet.sarif

Tuning, for when a finding is an accepted risk:

civet scan . --ignore-rule CIV301 --ignore-group hygiene

MCP server

pip install "civet-audit[mcp]"
civet-mcp

Claude Code / Cursor config:

{
  "mcpServers": {
    "civet": {
      "command": "uvx",
      "args": ["--from", "civet-audit[mcp]", "civet-mcp"]
    }
  }
}
Tool What it does
civet_scan scan a path; returns counts, risk score, findings with evidence
civet_explain the attack narrative and fix for one rule
civet_rules list rules, optionally by group
civet_demo write and scan a broken workflow — one-call proof of value

Design rules

  1. Deterministic. No model, no network, no clock. Same bytes → same report.
  2. Evidence first. file:line, the job, the step, and the raw source line.
  3. Zero dependencies in core. mcp lives in an extra; the CLI never imports it.
  4. No silent failures. Unparseable YAML is reported in errors, not swallowed.
  5. Say why. Every rule carries an attack narrative and a minimal fix.

Dogfood

civet audits the workflows of its author's other repositories. On first run it found 19 findings across vehsec (2 workflows), 19 in pcaplens, 22 in skillvet and 13 in agent-scan — including a real curl … | sh in a publish pipeline. They are being fixed, not suppressed.

Roadmap

  • --fix for mechanical remediations (SHA pinning, persist-credentials)
  • actions/* version→SHA resolution with a local cache
  • GitLab CI and Jenkinsfile readers behind the same rule engine
  • baseline file (.civet-baseline.json) for incremental adoption
  • environment protection checks against the repository API

Contributing

git clone https://github.com/xuange-hu/civet && cd civet
pip install -e ".[mcp]"
PYTHONPATH=src pytest -q     # 163 tests

Adding a rule means touching three places: rules.py (id, severity, why, fix), analyze.py (the check), and demo.py (a line that proves it fires). A test in tests/test_rules.py asserts the id; test_demo.py asserts the demo still covers 100% of the registry.

License

MIT

SIMILAR PLUGINS