Metadata-Version: 2.4
Name: agentpipe-scan
Version: 0.6.0
Summary: Find CI agents that eat untrusted GitHub events and hold secrets
Author: Agentpipe
License-Expression: MIT
Project-URL: Homepage, https://github.com/cyberbobas/agentpipe
Project-URL: Issues, https://github.com/cyberbobas/agentpipe/issues
Keywords: ai-agent,github-actions,prompt-injection,ci,security,clinejection,supply-chain,sast,agent-security
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.11
Classifier: Topic :: Security
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: PyYAML>=6.0
Dynamic: license-file

# agentpipe

[![PyPI](https://img.shields.io/pypi/v/agentpipe-scan?color=8b7cf6)](https://pypi.org/project/agentpipe-scan/)
[![tests](https://github.com/cyberbobas/agentpipe/actions/workflows/tests.yml/badge.svg)](https://github.com/cyberbobas/agentpipe/actions/workflows/tests.yml)
[![Python](https://img.shields.io/pypi/pyversions/agentpipe-scan)](https://pypi.org/project/agentpipe-scan/)
[![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)

Find **CI agents that eat untrusted GitHub events and hold keys**. A repo lets a
stranger's issue title drive an AI agent step that carries an npm token, and now
an issue is a publish. agentpipe finds those chains statically, before anyone
opens the issue.

Not an Actions linter. Not gitleaks. It reports preconditions and never exploits.

![agentpipe finds an untrusted issue -> AI agent -> npm token publish chain](docs/agentpipe-scan.gif)

Part of [Agentoffense](https://agentoffense.com). The recon front of the line:
find the exposure here, close it at runtime with
[Airlock](https://github.com/cyberbobas/airlock) and
[Countersign](https://github.com/cyberbobas/countersign).

```
pip install agentpipe-scan
agentpipe scan .                          # local clone, no network
agentpipe scan github.com/org/repo        # public workflows via GitHub API
agentpipe scan github.com/org             # whole org, aggregated report
agentpipe prove .                         # canary kit: prove each finding harmlessly
                                          # (token: GITHUB_TOKEN env or --token)
```

## What problems it solves

AI agents moved into two places that hold real power, and both are easy to leave
open. agentpipe finds the exposure in each, statically, before an attacker does.

**1. A stranger's text becomes your CI's actions ("clinejection").**
You wire an AI agent into GitHub Actions to triage issues or review PRs. The
workflow triggers on `issues` or `pull_request_target`, drops the issue title
straight into the agent's prompt, and the job holds an `NPM_TOKEN` and
`packages: write`. Now anyone who opens an issue is handing instructions to a
process that can publish your package or exfiltrate through a bot comment. No
one clicked anything. agentpipe traces the whole chain (untrusted trigger to
agent step to secret or publish sink) and points at the exact `file:line`, so
you see it as code review, not as an incident.

**2. Your own laptop's agent config is wide open.**
The agent you run locally trusts a config you set up once and forgot.
Confirmations are off (`bypassPermissions`), a filesystem MCP is rooted at `/`
so the agent can read every secret you own, a hook pipes `curl | sh`, an MCP
server installs from an unpinned `npx -y pkg` (a rug-pull waiting to happen),
and a token sits in plaintext in the config. `agentpipe local` reads only,
touches no network, masks secret values, and tells you exactly what to tighten.

**Why static and why now.** Runtime tools (Airlock, Countersign) stop a bad call
in the moment. agentpipe answers the earlier question: *where are you even
exposed?* It runs in seconds on a repo, a whole org, or a machine, needs no
agent running and no exploit, and every finding carries what it proves, what it
does not, and the one change that closes it. Each report ends with what the scan
cannot see, which is where a real pentest begins.

## The killer bit: `prove`

A finding says "preconditions exist". `agentpipe prove` turns each high/medium
finding into a **harmless canary kit**: a unique token, the exact issue/PR text
to paste into a fork you own, the three places to watch (comment, step summary,
job log), what a positive result proves — and what it still doesn't (that part
is the pentest). Every payload is echo/read-only by construction.

The report also groups flat findings into **attack chains**
(entry → amplify → impact, one per job, severity-tagged).

## `agentpipe local` — the laptop half

CI is only half the surface. `agentpipe local` audits agent configs on the
machine itself (`~/.claude.json`, `.mcp.json`, cursor/gemini/windsurf configs):

![agentpipe local finds a whole-filesystem MCP, a curl-pipe-sh hook, and a plaintext token](docs/agentpipe-local.gif)

| id | finding |
|---|---|
| LOC-01 | confirmations weakened/off (`bypassPermissions`, unscoped tool grants) |
| LOC-02 | MCP server via unpinned `npx -y pkg` (rug-pull channel) |
| LOC-03 | filesystem MCP rooted at `/` or `~` |
| LOC-04 | remote MCP over plaintext `http://` or with no auth header |
| LOC-05 | hook piping the network into a shell (`curl … \| sh`) |
| LOC-06 | plaintext credential in an MCP env block (values are masked in the report) |
| LOC-07 | CLAUDE.md/.cursorrules ordering the agent to never ask |

Read-only, no network, secret values never printed.

Clinejection-class demo (issue title → Claude in Actions → npm token):

```
agentpipe scan examples/clinejection-repo
```

## Run it in your CI (GitHub Action)

Drop agentpipe into a workflow. On a pull request it scans the diff's repo, posts
**one updatable comment** with the chains the PR introduces, and uploads **SARIF**
to the Security tab.

```yaml
# .github/workflows/agentpipe.yml
name: agentpipe
on: [pull_request]
permissions:
  contents: read
  pull-requests: write      # the sticky PR comment
  security-events: write    # Code Scanning (SARIF)
jobs:
  agentpipe:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: cyberbobas/agentpipe@v0
        with:
          fail-on: high
          comment: true
```

- **One sticky comment.** The bot edits its own comment instead of posting a new
  one per push. Only `reachable` + `high` chains go in the headline; everything
  else folds into a details block. Noise is uninstall, so the default is quiet.
- **SARIF to Code Scanning.** Every finding shows in the Security tab with real
  `file:line`, severity, and a fingerprint GitHub tracks across runs.
- **Baseline / diff.** On a repo that already carries debt, commit a baseline
  once and the Action goes silent on it, speaking up only for chains a PR adds:

```
agentpipe scan . --write-baseline .github/agentpipe-baseline.json   # once, commit it
#  then the Action runs with:  baseline: .github/agentpipe-baseline.json
```

The same outputs are in the CLI: `agentpipe scan . --sarif out.sarif` writes
SARIF anywhere, and `agentpipe comment .` posts the sticky comment from any CI
that sets `GITHUB_TOKEN` and `GITHUB_REPOSITORY`.

## What the scanner flags

| id | chain | since |
|---|---|---|
| AWI-01 | untrusted trigger (`issues`, `issue_comment`, `pull_request_target`, …) + agent step + secret/write/publish | 0.1 |
| AWI-02 | `${{ github.event.issue.title }}` (or body/comment/PR) interpolated into the agent step | 0.1 |
| AWI-03 | `pull_request_target` + agent + checkout of PR head | 0.1 |
| AWI-04 | agent job can `issues: write` / `gh issue comment` (GitLost exfil) | 0.1 |
| AWI-05 | `--dangerously-skip-permissions` / YOLO on that agent | 0.1 |
| AWI-06 | `actions/cache` written on an untrusted-trigger workflow next to an agent/release | 0.3 |
| AWI-07 | agent/release job downloads artifacts across workflow boundaries (`workflow_run`) | 0.3 |
| AWI-08 | agent + `gh` CLI + `contents`/`actions: write` (workflow persistence) | 0.2 |
| AWI-09 | OIDC / cloud login on the untrusted-prompt job | 0.2 |
| AWI-10 | agent + untrusted trigger on a self-hosted runner | 0.2 |
| AWI-11 | agent hidden in a local composite action or reusable workflow (recursed, caller triggers inherited) | 0.2 |
| AWI-12 | GitHub Agentic Workflows: markdown `engine:` + untrusted `on:` + write surface | 0.3 |
| AWI-13 | Dependabot/Renovate in repo + agent merges PRs with no `dependabot[bot]` actor guard | 0.3 |
| AWI-14 | publish after/alongside an agent job with no `environment:` gate | 0.2 |
| AWI-15 | agent step handed `mcp_config` / MCP servers (CI trifecta) | 0.3 |

Findings carry real `file:line` of the agent step. A workflow that does not parse is a loud warning, not a silent skip.

Not yet: INS-01..04 (instruction-file hygiene in repos), GitHub Action wrapper, `--follow-remote`.

## What you will not see

A 0–100 score. A badge. Telemetry. A working exploit. An Airlock pitch.

The report ends with **what this scan cannot see** — that list *is* the pentest offer.

## Verdicts

`reachable` (trigger + agent + sink) · `plausible` (untrusted field in the prompt) · `adjacent` (one piece of the chain). Severity only on the first two.

MIT. Python ≥ 3.11. PyYAML.

```
python3 tests/run.py           # regression suite (41 checks)
python3 tests/adversarial.py   # parser fuzz, FP/FN matrix, recursion, ReDoS, CLI
python3 tests/matrix.py         # negative matrix per detector, GitHub semantics, live corpus
```
