Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ jobs:
- run: npm test
- run: npm pack --dry-run

# Dogfood: run the bundled Action on this repo and write a sanitized summary.
# Dogfood: run the bundled Action on this repo and write a sanitized summary,
# plus a populated demo summary (scanning a fixture with findings) so the run
# Summary page shows a real, non-empty maintenance table.
self-check:
runs-on: ubuntu-latest
permissions:
Expand All @@ -32,3 +34,45 @@ jobs:
with:
node-version: "20"
- run: node "$GITHUB_WORKSPACE/src/index.js" . --github-summary --no-network
- name: Demo summary (populated, for docs/screenshot)
run: |
mkdir -p /tmp/demo/.github/workflows
printf 'on: push\npermissions: write-all\njobs:\n build:\n runs-on: ubuntu-latest\n steps:\n - uses: tj-actions/changed-files@v47\n' > /tmp/demo/.github/workflows/ci.yml
node "$GITHUB_WORKSPACE/src/index.js" /tmp/demo --github-summary --no-network

# Verify the packed npm artifact: MCP initializes from it, and the agent skill
# has a valid skills.sh discovery shape.
artifact-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: "20"
- run: npm ci --no-audit --no-fund
- name: MCP initialization from the packed artifact
run: |
set -euo pipefail
npm pack
TGZ="$(ls taskbounty-check-*.tgz)"
VER="$(node -p "require('$GITHUB_WORKSPACE/package.json').version")"
mkdir -p /tmp/consume && cd /tmp/consume && npm init -y >/dev/null 2>&1
npm install "$GITHUB_WORKSPACE/$TGZ" >/dev/null 2>&1
OUT="$(printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | npx --no-install taskbounty-check mcp)"
echo "$OUT"
echo "$OUT" | grep -q '"serverInfo"' || { echo "::error::MCP did not initialize from the packed artifact"; exit 1; }
echo "$OUT" | grep -q "\"version\":\"$VER\"" || { echo "::error::MCP serverInfo version != package version $VER"; exit 1; }
- name: Agent skill discovery shape (skills.sh)
run: |
set -euo pipefail
test -f skills/taskbounty-security/SKILL.md || { echo "::error::SKILL.md missing"; exit 1; }
node -e '
const fs = require("fs");
const s = fs.readFileSync("skills/taskbounty-security/SKILL.md", "utf8");
const m = s.match(/^---\n([\s\S]*?)\n---/);
if (!m) { console.error("missing frontmatter"); process.exit(1); }
const fm = "\n" + m[1];
if (!/\nname:\s*\S/.test(fm)) { console.error("missing name"); process.exit(1); }
if (!/\ndescription:\s*\S/.test(fm)) { console.error("missing description"); process.exit(1); }
console.log("SKILL.md frontmatter OK");
'
76 changes: 48 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,58 @@
# taskbounty-check

Pre-launch safety check for AI-built apps. Built it with **Lovable, Bolt, Replit, Cursor, or v0**?
This scans your **GitHub Actions + CI hygiene locally** before you ship. Your source code and
workflow contents **never leave your machine**. The default code path makes **no outbound
requests**; `fetch` is additionally blocked as defense in depth (this is not a complete network
sandbox). Only `--gh-org` intentionally uses the network.
**A local check for GitHub Actions and CI maintenance hygiene** (third-party action pinning,
workflow token permissions, and update automation), built for apps shipped with Lovable, Bolt,
Replit, Cursor, or v0.

> **Scope, honestly:** this checks GitHub Actions workflow + update-automation hygiene. It does
> **not** check exposed secrets, auth, payments, webhooks, or runtime behavior — those need a
> manual review. It is a maintenance check, not a full security audit.
**Local by default. No uploads. No telemetry.** It reads only your workflow files, on your machine.
The default code path makes no outbound network requests, writes its report locally, and sends
nothing anywhere. There is no analytics or phone-home of any kind. Only the opt-in `--gh-org` mode
uses the network (through your own `gh` session).

> **`--share` uploads nothing.** It writes a sanitized, counts-only local file for you to submit
> **manually**; network stays off under `--share`.
**Works with Cursor, Claude Code, and Codex** (local MCP server, below).

## Quick start (60 seconds)
## Three ways to use it

```bash
# 1. Scan the current repo locally (no network, writes a local report)
npx taskbounty-check@latest .
**1. GitHub Action** — add a maintenance check to CI that writes a summary to the run (no PR
comments, no source upload):

# 2. SARIF for GitHub Code Scanning
npx taskbounty-check@latest . --format sarif --output taskbounty.sarif
```yaml
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- run: npx taskbounty-check@0.1.5 . --github-summary --no-network
```

# 3. Scaffold a least-privilege CI workflow (previews; never overwrites)
npx taskbounty-check@latest init
**2. Agent / MCP** — a local stdio server for Cursor, Claude Code, and Codex:

# 4. Local MCP server for Codex / Claude Code / Cursor
npx taskbounty-check@latest mcp
```bash
npx -y taskbounty-check@0.1.5 mcp
```

Reproducible, pinned invocation (recommended): `npx taskbounty-check@0.1.4 .`
**3. One-off CLI** — scan the current repo locally and write a report:

```bash
npx -y taskbounty-check@0.1.5 .
```

> Pin a version (`@0.1.5`) in committed config and CI for reproducibility. `@latest` is convenient
> for a quick one-off, but a pinned version is the reproducible choice.

### A real GitHub job summary

The Action writes a counts-only maintenance summary to the workflow run (categories and next steps,
no filenames, line numbers, or repo source). Example from this repo's own CI:

![TaskBounty check: GitHub Actions job summary](docs/job-summary.png)

See it live: the **self-check** job in [this repository's Actions runs](https://github.com/eliottreich/taskbounty-check/actions).

### Learn more

**Privacy:** the scan runs locally and **sends nothing by default**. Source code, workflow
contents, filenames, line numbers, and evidence never leave your machine. The only thing that can
ever be transmitted is a sanitized counts-only summary, and only when you explicitly choose to.
- [Methodology](https://www.task-bounty.com/github-actions-security-check/methodology) — exactly what it reviews and how findings are labeled.
- [Privacy and scope](https://www.task-bounty.com/ai-app-security-check) — local-by-default data handling.
- [Limitations](#supported-checks-and-honest-limitations) — what it does NOT check (below).

## Supported checks (and honest limitations)

Expand Down Expand Up @@ -118,7 +137,7 @@ permissions:
security-events: write
steps:
- uses: actions/checkout@v4
- run: npx taskbounty-check@latest . --format sarif --output taskbounty.sarif
- run: npx taskbounty-check@0.1.5 . --format sarif --output taskbounty.sarif
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: taskbounty.sarif
Expand Down Expand Up @@ -157,6 +176,7 @@ args = ["-y", "taskbounty-check@latest", "mcp"]

## Security

Zero runtime dependencies. Published with npm provenance; verify checksums. See the threat model
(`design-docs/security-cli-threat-model.md`) and external-review packet
(`design-docs/security-expansion/external-review-packet.md`) in the project repository.
Zero runtime dependencies. Published to npm with provenance (verify on the package's npm page). The
default run makes no outbound requests and uploads nothing; see the
[methodology](https://www.task-bounty.com/github-actions-security-check/methodology) for the full
data-handling and scope boundaries.
14 changes: 12 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "taskbounty-check",
"version": "0.1.4",
"version": "0.1.5",
"mcpName": "io.github.eliottreich/taskbounty-check",
"description": "Pre-launch safety check for AI-built apps (Lovable, Bolt, Replit, Cursor, v0). Scans your GitHub Actions + CI hygiene locally. Source code and workflow contents never leave your machine.",
"type": "module",
"bin": {
Expand Down Expand Up @@ -37,7 +38,16 @@
"v0",
"github-actions",
"security",
"local-scanner"
"local-scanner",
"mcp",
"mcp-server",
"claude-code",
"codex",
"devsecops",
"github-security",
"github-actions-security",
"ci-security",
"software-supply-chain"
],
"homepage": "https://www.task-bounty.com/ai-app-security-check",
"repository": {
Expand Down
27 changes: 27 additions & 0 deletions server.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.eliottreich/taskbounty-check",
"description": "Local GitHub Actions/CI maintenance check (action pinning, token perms). Not a full security audit.",
"repository": {
"url": "https://github.com/eliottreich/taskbounty-check",
"source": "github"
},
"version": "0.1.5",
"packages": [
{
"registryType": "npm",
"identifier": "taskbounty-check",
"version": "0.1.5",
"transport": {
"type": "stdio"
},
"packageArguments": [
{
"type": "positional",
"value": "mcp",
"valueHint": "mcp"
}
]
}
]
}
77 changes: 77 additions & 0 deletions skills/taskbounty-security/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
---
name: taskbounty-security
description: Run TaskBounty's local GitHub Actions / CI maintenance-hygiene check, interpret the findings, and draft a fix plan. Use when a user wants to review a repo's GitHub Actions workflows for third-party action pinning, workflow token permissions, or update-automation gaps before shipping. Scope is CI/workflow hygiene only, not a full application-security audit. Runs locally, uploads nothing, and never changes files without explicit approval.
---

# TaskBounty security check (GitHub Actions / CI hygiene)

`taskbounty-check` is a local, zero-dependency checker for **GitHub Actions and CI maintenance hygiene**:
third-party action pinning, workflow token permissions, and dependency-update automation. It reads
workflow files on disk. By default it makes **no outbound network requests** and **uploads nothing**.

**Scope, stated honestly.** This covers GitHub Actions, CI workflow permissions, action pinning, and
update automation. It is **not a complete application-security audit** - it does not check exposed
secrets, authentication, payments, webhooks, or runtime behavior. Say so when you report results.

## Hard rules for the agent

- **Never upload the user's source code or scan results anywhere.** The tool keeps everything local;
keep it that way.
- **Never modify files without explicit user approval.** Findings and fix plans are proposals.
- **Never commit, push, open pull requests, or post comments automatically.** The user does that.
- Do not add the `--gh-org` flag unless the user explicitly asks to scan an organization (it is the
only mode that uses the network, via the user's own `gh` session).

## 1. Run the checker locally (primary)

```bash
npx -y taskbounty-check@0.1.5 .
```

This writes a local report and prints a summary. No network, nothing uploaded. Run it from the repo
root (or pass a path). To see what it would do without writing files, add `--dry-run`.

## 2. Interpret the findings

Each finding has a rule id and a confidence label (`confirmed` vs `review`). Common rules:

- **unpinned-action** - a third-party action uses a movable tag/branch (e.g. `@v4`) instead of a
full commit SHA. Movable refs can be re-pointed upstream.
- **broad-permissions** / **no-permissions-block** - the workflow grants (or defaults to) broad
`GITHUB_TOKEN` permissions instead of least privilege.
- **prt-checkout-untrusted** / **secrets-in-prt** - a `pull_request_target` workflow runs untrusted
PR code and/or exposes secrets to it.
- **script-injection** - untrusted `${{ github.event.* }}` input is interpolated into a run step.

Explain each finding in plain language and why it matters. Do not overstate: a `review` item is a
candidate to check, not a confirmed vulnerability.

## 3. Propose a fix plan (do not apply it silently)

Draft concrete, minimal edits - for example, replacing `uses: owner/action@v4` with
`uses: owner/action@<full-commit-sha> # v4`, or adding a top-level `permissions: { contents: read }`
block. Present the plan and let the user approve before changing any file. Never commit or push.

## SARIF mode (GitHub Code Scanning)

```bash
npx -y taskbounty-check@0.1.5 . --format sarif --output taskbounty.sarif
```

Produces SARIF 2.1.0 the user can upload to **their own** repo's Code Scanning. Each rule links to
the public methodology for context.

## MCP mode (Cursor, Claude Code, Codex)

```bash
npx -y taskbounty-check@0.1.5 mcp
```

Starts a local stdio MCP server exposing `scan_repo`, `explain_finding`, and `generate_fix_plan`.
It is local-only, makes no outbound requests, and never modifies files - fix plans are returned as
text for the user to apply explicitly.

## A note on pinning

For reproducibility, prefer a pinned version (`taskbounty-check@0.1.5`) over `@latest` in committed
config and CI.
26 changes: 22 additions & 4 deletions src/mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,21 +75,39 @@ function textResult(text) {
return { content: [{ type: "text", text }] };
}

// Product-led CTA. STATIC by construction — never contains repo names, paths, findings, or counts.
// Shown at most ONCE per server process, and only when scan_repo actually surfaced something.
const REVIEW_CTA =
"Need a human second opinion or fix plan?\n" +
"https://www.task-bounty.com/ai-app-security-check/review?utm_source=mcp_registry&utm_medium=integration&utm_campaign=agent_distribution";
let ctaShown = false;
function maybeAppendCta(text, hasFindings) {
if (!hasFindings || ctaShown) return text;
ctaShown = true;
return `${text}\n\n${REVIEW_CTA}`;
}
// Test-only: reset the show-once latch so ordering-independent tests can assert the behavior.
export function __resetCtaForTest() { ctaShown = false; }
export { REVIEW_CTA };

/** Pure tool dispatch — returns an MCP tool result. No network, no writes. */
export function callMcpTool(name, args = {}) {
if (name === "scan_repo") {
const path = typeof args.path === "string" && args.path.trim() ? args.path : ".";
const result = scanInput(path); // network already guarded off
const cats = (result.maintenanceCandidates || []).map((c) => `- ${c.category}: ${c.count} (${c.confidence})`).join("\n") || "- none";
const items = (result.localEvidence || []).slice(0, 50)
const evidence = result.localEvidence || [];
const items = evidence.slice(0, 50)
.map((e) => ` • [${e.confirmed ? "confirmed" : "review"}] ${e.rule} — ${e.file}${e.line ? ":" + e.line : ""}`).join("\n") || " • none";
return textResult(
const hasFindings = evidence.length > 0 || (result.privateReviewCount || 0) > 0 ||
(result.maintenanceCandidates || []).some((c) => (c.count || 0) > 0);
const body =
`Local scan of "${path}" (no network, nothing uploaded):\n` +
`Repos: ${result.repoCount} · workflow files: ${result.workflowFilesReviewed} · items for private review: ${result.privateReviewCount}\n\n` +
`Maintenance candidates by category:\n${cats}\n\nFindings (local detail; confirmed vs review):\n${items}\n\n` +
`Scope: GitHub Actions + update-automation hygiene only — not a full security audit (secrets/auth/payments/webhooks/runtime need manual review). ` +
`Use explain_finding and generate_fix_plan for next steps.`,
);
`Use explain_finding and generate_fix_plan for next steps.`;
return textResult(maybeAppendCta(body, hasFindings));
}
if (name === "explain_finding") {
const k = KB[String(args.rule || "").trim()];
Expand Down
16 changes: 14 additions & 2 deletions src/sarif.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,20 @@ const RULES = {
const RULE_PREFIX = "taskbounty";
const DEFAULT_LEVEL = "note";

// Rule-help destination. Links to the public methodology, with a per-rule anchor when the rule is
// a documented one. Channel-only utm — no repo name or finding data ever goes in the URL.
const METHODOLOGY_URL =
"https://www.task-bounty.com/github-actions-security-check/methodology?utm_source=github&utm_medium=sarif&utm_campaign=agent_distribution";

function ruleSlug(rule) {
return String(rule || "finding").replace(/[^a-z0-9_-]/gi, "-");
}
function ruleId(rule) {
return `${RULE_PREFIX}/${String(rule || "finding").replace(/[^a-z0-9_-]/gi, "-")}`;
return `${RULE_PREFIX}/${ruleSlug(rule)}`;
}
// Anchor to the specific rule section on the methodology page for documented rules.
function ruleHelpUri(rule) {
return RULES[rule] ? `${METHODOLOGY_URL}#rule-${ruleSlug(rule)}` : METHODOLOGY_URL;
}

// Map our severity to a SARIF level when the rule has no explicit one.
Expand All @@ -45,7 +57,7 @@ export function renderSarif(result) {
shortDescription: { text: meta.name },
fullDescription: { text: meta.help },
defaultConfiguration: { level: meta.level || DEFAULT_LEVEL },
helpUri: "https://www.task-bounty.com/github-actions-security-check/methodology",
helpUri: ruleHelpUri(e.rule),
properties: { category: e.category || "other" },
});
}
Expand Down
Loading