agentci-guard 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/rules.ts","../src/cli.ts","../src/config.ts","../src/detect.ts","../src/report.ts","../src/index.ts","../src/sarif.ts","../src/scanner.ts"],"sourcesContent":["import type { Severity } from \"./types.js\";\n\nexport type RuleDefinition = {\n id: string;\n title: string;\n severity: Severity;\n why: string;\n fix: string[];\n};\n\nexport const RULES: Record<string, RuleDefinition> = {\n \"agentci/untrusted-ai-write-token\": {\n id: \"agentci/untrusted-ai-write-token\",\n title:\n \"Untrusted event content can reach an AI agent with write permissions\",\n severity: \"critical\",\n why: \"An attacker can place prompt-injection text in a PR, issue, or comment. If that text reaches an AI agent with repository write permissions, the agent can be induced to modify code, comments, workflows, or releases.\",\n fix: [\n \"Do not run privileged AI agents on untrusted triggers.\",\n \"Use read-only GITHUB_TOKEN permissions for untrusted events.\",\n \"Require maintainer approval before running the agent.\",\n \"Sanitize and summarize untrusted content before passing it to an agent.\",\n ],\n },\n \"agentci/pull-request-target-ai\": {\n id: \"agentci/pull-request-target-ai\",\n title: \"AI agent runs on pull_request_target\",\n severity: \"critical\",\n why: \"pull_request_target runs in the base repository security context and can expose write tokens or secrets to workflows influenced by an untrusted pull request.\",\n fix: [\n \"Use pull_request with read-only permissions for untrusted code.\",\n \"Split analysis into a read-only job and a separate maintainer-approved write job.\",\n \"Avoid checking out untrusted PR head code in pull_request_target.\",\n ],\n },\n \"agentci/ai-with-secrets\": {\n id: \"agentci/ai-with-secrets\",\n title: \"AI agent job has access to secrets\",\n severity: \"medium\",\n why: \"Secrets mounted into an AI-agent job can be exfiltrated if untrusted prompt content influences tool use, shell commands, or generated output. Most AI actions require a provider key, so this is a baseline exposure to review rather than a vulnerability on its own — it becomes high-risk when combined with untrusted input or write permissions (see agentci/untrusted-ai-write-token).\",\n fix: [\n \"Do not expose secrets to agent jobs that process untrusted content.\",\n \"Use short-lived scoped tokens.\",\n \"Move secret-bearing actions behind manual approval.\",\n ],\n },\n \"agentci/untrusted-input-in-prompt\": {\n id: \"agentci/untrusted-input-in-prompt\",\n title:\n \"Untrusted GitHub event content is passed into an AI prompt or command\",\n severity: \"high\",\n why: \"PR bodies, issue bodies, comments, branch names, and commit messages are attacker-controlled in common workflows and can contain prompt-injection instructions.\",\n fix: [\n \"Avoid inserting raw GitHub event text into prompts.\",\n \"Use structured extraction and length limits.\",\n \"Add prompt-injection filtering before AI execution.\",\n \"Run the agent with read-only permissions.\",\n ],\n },\n \"agentci/ai-shell-access\": {\n id: \"agentci/ai-shell-access\",\n title: \"AI agent has shell or arbitrary command access\",\n severity: \"high\",\n why: \"Shell access allows a compromised agent prompt to inspect the workspace, call network endpoints, or alter build artifacts.\",\n fix: [\n \"Disable shell tools for untrusted events.\",\n \"Run in a sandbox with no secrets.\",\n \"Restrict network and filesystem access.\",\n ],\n },\n \"agentci/broad-write-permissions\": {\n id: \"agentci/broad-write-permissions\",\n title: \"Workflow grants broad write permissions near AI usage\",\n severity: \"medium\",\n why: \"Broad write scopes increase blast radius if an AI-agent step is influenced by untrusted input.\",\n fix: [\n \"Set default permissions to read-only.\",\n \"Grant write scopes only in narrowly scoped jobs.\",\n \"Prefer job-level permissions over workflow-level write permissions.\",\n ],\n },\n \"agentci/unpinned-ai-action\": {\n id: \"agentci/unpinned-ai-action\",\n title: \"AI-related action is not pinned to a commit SHA\",\n severity: \"medium\",\n why: \"Tag-pinned third-party actions can change over time. AI-agent actions often receive privileged context, so supply-chain drift matters.\",\n fix: [\n \"Pin third-party actions to full commit SHAs.\",\n \"Review updates explicitly.\",\n \"Prefer first-party or internally mirrored actions for privileged jobs.\",\n ],\n },\n \"agentci/unsafe-checkout\": {\n id: \"agentci/unsafe-checkout\",\n title:\n \"Workflow checks out untrusted pull request head in a privileged context\",\n severity: \"high\",\n why: \"Checking out attacker-controlled code in a privileged workflow can let malicious build scripts or configuration affect the agent job.\",\n fix: [\n \"Do not checkout PR head code inside pull_request_target.\",\n \"Use read-only analysis jobs.\",\n \"Disable install/build scripts before trust is established.\",\n ],\n },\n};\n\nexport const SEVERITY_ORDER: Severity[] = [\"low\", \"medium\", \"high\", \"critical\"];\n","#!/usr/bin/env node\nimport fs from \"node:fs/promises\";\nimport { Command } from \"commander\";\nimport pc from \"picocolors\";\nimport {\n formatGithubOutputs,\n renderMarkdownReport,\n renderTextReport,\n scanRepository,\n toSarif,\n hasFindingAtOrAbove,\n} from \"./index.js\";\nimport type { Severity } from \"./types.js\";\n\ntype ScanOptions = {\n json?: boolean;\n markdown?: string;\n sarif?: string;\n config?: string;\n failOn: \"none\" | Severity;\n};\n\nasync function main(): Promise<void> {\n const program = new Command()\n .name(\"agentci\")\n .description(\"Scan CI/CD workflows for unsafe AI coding-agent usage.\")\n .version(\"0.1.0\");\n\n program\n .command(\"scan\")\n .description(\n \"Scan a repository for unsafe AI-agent GitHub Actions patterns.\",\n )\n .argument(\"[path]\", \"Repository path.\", \".\")\n .option(\"--json\", \"Print JSON output.\", false)\n .option(\"--markdown <path>\", \"Write a Markdown report.\")\n .option(\"--sarif <path>\", \"Write SARIF output.\")\n .option(\n \"--config <path>\",\n \"Path to an agentci config JSON file (default: agentci.config.json in the scan path).\",\n )\n .option(\n \"--fail-on <severity>\",\n \"Fail at or above severity: none, low, medium, high, critical.\",\n \"high\",\n )\n .action(async (target: string, options: ScanOptions) => {\n const failOn = parseFailOn(options.failOn);\n const result = await scanRepository(target, { configPath: options.config });\n\n if (options.sarif)\n await fs.writeFile(\n options.sarif,\n `${JSON.stringify(toSarif(result.findings), null, 2)}\\n`,\n \"utf8\",\n );\n if (options.markdown)\n await fs.writeFile(\n options.markdown,\n renderMarkdownReport(result),\n \"utf8\",\n );\n\n if (options.json) console.log(JSON.stringify(result, null, 2));\n else console.log(renderTextReport(result));\n\n if (process.env.GITHUB_OUTPUT)\n await fs.appendFile(\n process.env.GITHUB_OUTPUT,\n formatGithubOutputs(result, options.sarif),\n \"utf8\",\n );\n\n if (failOn !== \"none\" && hasFindingAtOrAbove(result.findings, failOn)) {\n process.exitCode = 2;\n }\n });\n\n program\n .command(\"explain\")\n .description(\"Explain a rule by ID.\")\n .argument(\n \"<rule-id>\",\n \"Rule ID, for example agentci/untrusted-ai-write-token.\",\n )\n .action(async (ruleId: string) => {\n const { RULES } = await import(\"./rules.js\");\n const rule = RULES[ruleId];\n if (!rule) throw new Error(`Unknown rule: ${ruleId}`);\n console.log(pc.bold(rule.title));\n console.log(`Severity: ${rule.severity}`);\n console.log(\"\");\n console.log(rule.why);\n console.log(\"\");\n console.log(\"Fix:\");\n for (const fix of rule.fix) console.log(`- ${fix}`);\n });\n\n await program.parseAsync(process.argv);\n}\n\nfunction parseFailOn(value: string): \"none\" | Severity {\n if (\n value === \"none\" ||\n value === \"low\" ||\n value === \"medium\" ||\n value === \"high\" ||\n value === \"critical\"\n )\n return value;\n throw new Error(\n \"--fail-on must be one of none, low, medium, high, critical.\",\n );\n}\n\nmain().catch((error: unknown) => {\n console.error(pc.red(error instanceof Error ? error.message : String(error)));\n process.exitCode = 1;\n});\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nexport type AgentciConfig = {\n /** Rule ids to suppress everywhere, e.g. \"agentci/unpinned-ai-action\". */\n ignore: string[];\n /** Workflow path globs (relative to scan root) to exclude from reporting. */\n ignorePaths: string[];\n};\n\nconst EMPTY: AgentciConfig = { ignore: [], ignorePaths: [] };\n\nconst CONFIG_FILENAMES = [\"agentci.config.json\", \".agentcirc.json\"];\n\n/** Load config from an explicit path, or by discovery in the scan root. */\nexport async function loadConfig(\n root: string,\n explicitPath?: string,\n): Promise<AgentciConfig> {\n const candidates = explicitPath\n ? [path.resolve(explicitPath)]\n : CONFIG_FILENAMES.map((name) => path.join(root, name));\n\n for (const file of candidates) {\n let raw: string;\n try {\n raw = await fs.readFile(file, \"utf8\");\n } catch {\n continue;\n }\n const parsed = JSON.parse(raw) as Partial<AgentciConfig>;\n return {\n ignore: toStringArray(parsed.ignore),\n ignorePaths: toStringArray(parsed.ignorePaths),\n };\n }\n return EMPTY;\n}\n\nfunction toStringArray(value: unknown): string[] {\n return Array.isArray(value) ? value.map((item) => String(item)) : [];\n}\n\n/**\n * Inline, file-level suppression directives read from raw workflow text:\n * # agentci-ignore <rule-id> [<rule-id> ...] [-- reason]\n * # agentci-ignore-all [-- reason]\n *\n * Findings are reported at job/step granularity rather than per line, so\n * suppression is scoped to the whole file. The optional `-- reason` is for\n * humans and is ignored by the parser.\n */\nexport function parseInlineIgnores(raw: string): {\n all: boolean;\n rules: Set<string>;\n} {\n const rules = new Set<string>();\n let all = false;\n\n for (const line of raw.split(\"\\n\")) {\n if (/#\\s*agentci-ignore-all\\b/i.test(line)) {\n all = true;\n continue;\n }\n const match = /#\\s*agentci-ignore\\s+([^\\n]+)/i.exec(line);\n if (!match) continue;\n const spec = match[1].split(\"--\")[0]; // strip the optional \"-- reason\"\n for (const id of spec.split(/[\\s,]+/)) {\n if (id) rules.add(id);\n }\n }\n\n return { all, rules };\n}\n\n/** Minimal glob match: `*` matches within a path segment, `**` across segments. */\nexport function matchesPath(glob: string, target: string): boolean {\n const pattern = glob\n .split(\"**\")\n .map((part) =>\n part\n .split(\"*\")\n .map((segment) => segment.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\"))\n .join(\"[^/]*\"),\n )\n .join(\".*\");\n return new RegExp(`^${pattern}$`).test(target);\n}\n","// Precision is the whole game for a workflow linter: cry wolf on ordinary CI\n// and it gets uninstalled after the first run. We therefore detect AI coding-\n// agent usage only from *specific, load-bearing* signals — known agent actions,\n// agent CLI invocations, and provider credentials / model identifiers — and\n// never from generic words like \"agent\", \"ai\", \"node\", \"codex\", or \"mcp\" that\n// legitimately appear in self-hosted runner labels (\"build-agent\"), user-agent\n// headers, action slugs (\"datadog/agent-action\"), and ordinary tooling.\n//\n// To add support for a new agent, add a specific pattern here (PRs welcome).\nexport const AI_AGENT_PATTERNS = [\n // Known AI coding-agent GitHub Actions (matched in `uses:`)\n /anthropics\\/claude-code(?:-base)?-action/i,\n /\\banthropics\\/[\\w.-]*claude/i,\n /\\baider-ai\\/aider\\b/i,\n /\\bsweepai\\//i,\n /(?:all-hands-ai|opendevin)\\/(?:openhands|opendevin)/i,\n /\\bcontinuedev\\//i,\n /\\bblock\\/goose\\b|\\bgoose-ai\\//i,\n /\\bgithub\\/copilot[\\w-]*agent/i,\n /\\bopenai\\/codex[\\w-]*/i,\n\n // Agent CLIs / tools (product names specific enough to be low false-positive)\n /\\bclaude-code\\b/i,\n /@anthropic-ai\\/claude-code\\b/i,\n /\\bclaude\\b/i, // CLI binary + product name; rare as a literal token in non-AI CI\n /\\baider\\b/i,\n /\\bchatgpt\\b/i,\n /\\bcodex\\s+(?:exec|run|--)/i, // openai codex CLI (bare \"codex\" is too generic)\n /\\bollama\\s+run\\b/i,\n /\\bcursor-agent\\b/i,\n /\\bllm\\s+(?:-m|--model)\\b/i, // simonw llm CLI (bare \"llm\" is too generic)\n\n // Provider credentials / endpoints / SDKs / model identifiers\n /\\bANTHROPIC_API_KEY\\b/i,\n /\\bOPENAI_API_KEY\\b/i,\n /\\bGEMINI_API_KEY\\b/i,\n /api\\.(?:anthropic|openai)\\.com/i,\n /@anthropic-ai\\//i,\n /\\bclaude-(?:3|4|opus|sonnet|haiku)\\b/i,\n /\\bgpt-(?:4|4o|5)\\b/i,\n\n // Explicit agent phrasing\n /\\bai[ -]agents?\\b/i,\n /\\bcoding agents?\\b/i,\n /\\bautonomous agents?\\b/i,\n /\\bllm[ -]agents?\\b/i,\n /\\bmodel context protocol\\b/i,\n];\n\nconst UNTRUSTED_CONTEXT_PATTERNS = [\n /github\\.event\\.pull_request\\.(body|title|head\\.ref|head\\.sha)/i,\n /github\\.event\\.issue\\.(body|title)/i,\n /github\\.event\\.comment\\.body/i,\n /github\\.event\\.review\\.body/i,\n /github\\.event\\.head_commit\\.message/i,\n /github\\.head_ref/i,\n /github\\.ref_name/i,\n];\n\nconst SECRET_PATTERNS = [\n /secrets\\./i,\n /GITHUB_TOKEN/i,\n /\\b[A-Z0-9_]*TOKEN\\b/,\n /\\b[A-Z0-9_]*KEY\\b/,\n];\nconst SHELL_PATTERNS = [\n /\\bshell\\b/i,\n /\\bbash\\b/i,\n /\\bsh\\b/i,\n /\\bcurl\\b/i,\n /\\bwget\\b/i,\n /\\bnpx\\b/i,\n /\\bpython\\b/i,\n /\\bnode\\b/i,\n /\\bexec\\b/i,\n];\n\nexport function looksLikeAiUsage(value: string): boolean {\n return AI_AGENT_PATTERNS.some((pattern) => pattern.test(value));\n}\n\nexport function containsUntrustedGitHubContext(value: string): boolean {\n return UNTRUSTED_CONTEXT_PATTERNS.some((pattern) => pattern.test(value));\n}\n\nexport function containsSecretReference(value: string): boolean {\n return SECRET_PATTERNS.some((pattern) => pattern.test(value));\n}\n\nexport function containsShellAccess(value: string): boolean {\n return SHELL_PATTERNS.some((pattern) => pattern.test(value));\n}\n\nexport function isPinnedAction(uses: string): boolean {\n const ref = uses.split(\"@\")[1];\n return Boolean(ref && /^[a-f0-9]{40}$/i.test(ref));\n}\n","import pc from \"picocolors\";\nimport type { Finding, ScanResult } from \"./types.js\";\n\n/**\n * Render the GitHub Actions `$GITHUB_OUTPUT` lines for a scan, so downstream\n * steps can branch on the result (e.g. comment on a PR when critical > 0).\n */\nexport function formatGithubOutputs(\n result: ScanResult,\n sarifPath?: string,\n): string {\n return (\n [\n `findings=${result.findings.length}`,\n `critical=${result.summary.critical}`,\n `high=${result.summary.high}`,\n `medium=${result.summary.medium}`,\n `low=${result.summary.low}`,\n `sarif-path=${sarifPath ?? \"\"}`,\n ].join(\"\\n\") + \"\\n\"\n );\n}\n\nexport function renderTextReport(result: ScanResult): string {\n const lines = [\n \"AgentCI Guard scan\",\n `Workflows: ${result.workflow_count}`,\n `Findings: ${result.findings.length}`,\n `Summary: critical=${result.summary.critical} high=${result.summary.high} medium=${result.summary.medium} low=${result.summary.low}`,\n \"\",\n ];\n\n for (const finding of result.findings) {\n lines.push(`${label(finding.severity)} ${finding.rule_id}`);\n lines.push(\n `File: ${finding.file}${finding.job ? ` / job: ${finding.job}` : \"\"}${finding.step ? ` / step: ${finding.step}` : \"\"}`,\n );\n lines.push(`Evidence: ${finding.evidence}`);\n lines.push(`Why: ${finding.why}`);\n lines.push(\"Fix:\");\n for (const fix of finding.fix) lines.push(`- ${fix}`);\n lines.push(\"\");\n }\n\n return lines.join(\"\\n\");\n}\n\nexport function renderMarkdownReport(result: ScanResult): string {\n return [\n \"# AgentCI Guard Scan\",\n \"\",\n `- Workflows: ${result.workflow_count}`,\n `- Findings: ${result.findings.length}`,\n `- Critical: ${result.summary.critical}`,\n `- High: ${result.summary.high}`,\n `- Medium: ${result.summary.medium}`,\n `- Low: ${result.summary.low}`,\n \"\",\n ...result.findings.flatMap(renderFindingMarkdown),\n \"\",\n ].join(\"\\n\");\n}\n\nfunction renderFindingMarkdown(finding: Finding): string[] {\n return [\n `## ${finding.severity.toUpperCase()} ${finding.rule_id}`,\n \"\",\n `**File:** ${finding.file}`,\n finding.job ? `**Job:** ${finding.job}` : \"\",\n finding.step ? `**Step:** ${finding.step}` : \"\",\n `**Evidence:** \\`${finding.evidence.replace(/`/g, \"'\")}\\``,\n \"\",\n finding.why,\n \"\",\n \"**Fix:**\",\n \"\",\n ...finding.fix.map((fix) => `- ${fix}`),\n \"\",\n ].filter(Boolean);\n}\n\nfunction label(severity: string): string {\n if (severity === \"critical\") return pc.red(\"[CRITICAL]\");\n if (severity === \"high\") return pc.red(\"[HIGH]\");\n if (severity === \"medium\") return pc.yellow(\"[MEDIUM]\");\n return pc.cyan(\"[LOW]\");\n}\n","export * from \"./config.js\";\nexport * from \"./detect.js\";\nexport * from \"./report.js\";\nexport * from \"./rules.js\";\nexport * from \"./sarif.js\";\nexport * from \"./scanner.js\";\nexport * from \"./types.js\";\n","import { RULES } from \"./rules.js\";\nimport type { Finding, SarifLog } from \"./types.js\";\n\nexport function toSarif(findings: Finding[]): SarifLog {\n const usedRules = Object.values(RULES).filter((rule) =>\n findings.some((finding) => finding.rule_id === rule.id),\n );\n return {\n version: \"2.1.0\",\n $schema: \"https://json.schemastore.org/sarif-2.1.0.json\",\n runs: [\n {\n tool: {\n driver: {\n name: \"AgentCI Guard\",\n informationUri: \"https://github.com/David-Wu1119/agentci-guard\",\n rules: usedRules.map((rule) => ({\n id: rule.id,\n name: rule.title,\n shortDescription: { text: rule.title },\n fullDescription: { text: rule.why },\n help: {\n text: rule.fix.join(\" \"),\n markdown: rule.fix.map((fix) => `- ${fix}`).join(\"\\n\"),\n },\n defaultConfiguration: { level: sarifLevel(rule.severity) },\n })),\n },\n },\n results: findings.map((finding) => ({\n ruleId: finding.rule_id,\n level: sarifLevel(finding.severity),\n message: { text: `${finding.title}: ${finding.evidence}` },\n locations: [\n {\n physicalLocation: {\n artifactLocation: { uri: finding.file },\n region: { startLine: 1 },\n },\n },\n ],\n })),\n },\n ],\n };\n}\n\nfunction sarifLevel(severity: string): \"note\" | \"warning\" | \"error\" {\n if (severity === \"critical\" || severity === \"high\") return \"error\";\n if (severity === \"medium\") return \"warning\";\n return \"note\";\n}\n","import path from \"node:path\";\nimport fg from \"fast-glob\";\nimport YAML from \"yaml\";\nimport {\n containsSecretReference,\n containsShellAccess,\n containsUntrustedGitHubContext,\n isPinnedAction,\n looksLikeAiUsage,\n} from \"./detect.js\";\nimport { RULES, SEVERITY_ORDER } from \"./rules.js\";\nimport { loadConfig, matchesPath, parseInlineIgnores } from \"./config.js\";\nimport type {\n Finding,\n ScanOptions,\n ScanResult,\n Severity,\n WorkflowFile,\n} from \"./types.js\";\nimport fs from \"node:fs/promises\";\n\ntype WorkflowMap = Record<string, unknown>;\ntype FindingContext = {\n file: string;\n job?: string;\n step?: string;\n evidence: string;\n};\n\nexport async function scanRepository(\n root: string,\n options: Partial<ScanOptions> = {},\n): Promise<ScanResult> {\n const cwd = options.cwd ?? process.cwd();\n const scanRoot = path.resolve(cwd, root);\n const config = await loadConfig(scanRoot, options.configPath);\n const workflows = await loadWorkflowFiles(scanRoot);\n const findings = workflows\n .flatMap((workflow) => scanWorkflow(workflow, scanRoot))\n .filter(\n (finding) =>\n !config.ignore.includes(finding.rule_id) &&\n !config.ignorePaths.some((glob) => matchesPath(glob, finding.file)),\n );\n return {\n scanned_at: new Date().toISOString(),\n root: scanRoot,\n workflow_count: workflows.length,\n findings,\n summary: summarize(findings),\n };\n}\n\nexport async function loadWorkflowFiles(root: string): Promise<WorkflowFile[]> {\n const entries = await fg([\".github/workflows/*.{yml,yaml}\"], {\n cwd: root,\n dot: true,\n absolute: true,\n });\n const workflows: WorkflowFile[] = [];\n for (const file of entries.sort()) {\n const raw = await fs.readFile(file, \"utf8\");\n try {\n workflows.push({ path: file, raw, document: YAML.parse(raw) });\n } catch (error) {\n workflows.push({\n path: file,\n raw,\n document: {\n __parse_error: error instanceof Error ? error.message : String(error),\n },\n });\n }\n }\n return workflows;\n}\n\nexport function scanWorkflow(workflow: WorkflowFile, root: string): Finding[] {\n const doc = isRecord(workflow.document) ? workflow.document : {};\n const file = path.relative(root, workflow.path);\n const findings: Finding[] = [];\n\n if (\"__parse_error\" in doc) {\n findings.push(\n makeFinding(\"agentci/untrusted-input-in-prompt\", {\n file,\n evidence: `YAML parse error: ${String(doc.__parse_error)}`,\n }),\n );\n return findings;\n }\n\n const triggers = normalizeTriggers(doc.on ?? doc[\"on\"]);\n const jobs = isRecord(doc.jobs) ? doc.jobs : {};\n const workflowPermissions = normalizePermissions(doc.permissions);\n const workflowIsUntrusted = triggers.some(isUntrustedTrigger);\n const isPullRequestTarget = triggers.includes(\"pull_request_target\");\n\n for (const [jobName, rawJob] of Object.entries(jobs)) {\n if (!isRecord(rawJob)) continue;\n const jobPermissions = {\n ...workflowPermissions,\n ...normalizePermissions(rawJob.permissions),\n };\n const jobText = JSON.stringify(rawJob);\n const steps = Array.isArray(rawJob.steps) ? rawJob.steps : [];\n const jobUsesAi = looksLikeAiUsage(jobText);\n const jobHasWrite = hasWritePermission(jobPermissions);\n const jobHasSecrets = containsSecretReference(jobText);\n // Untrusted event content inside an `if:` condition is a guard (e.g.\n // `contains(github.event.comment.body, '@claude')`), not a value that\n // reaches the agent. Only treat it as a sink when it appears elsewhere.\n const jobHasUntrusted = containsUntrustedGitHubContext(\n JSON.stringify(stripGuards(rawJob)),\n );\n\n if (jobUsesAi && isPullRequestTarget) {\n findings.push(\n makeFinding(\"agentci/pull-request-target-ai\", {\n file,\n job: jobName,\n evidence: \"on: pull_request_target + AI usage\",\n }),\n );\n }\n if (jobUsesAi && workflowIsUntrusted && jobHasWrite && jobHasUntrusted) {\n findings.push(\n makeFinding(\"agentci/untrusted-ai-write-token\", {\n file,\n job: jobName,\n evidence:\n \"untrusted trigger + AI usage + write permissions + untrusted GitHub event context\",\n }),\n );\n }\n if (jobUsesAi && jobHasSecrets) {\n findings.push(\n makeFinding(\"agentci/ai-with-secrets\", {\n file,\n job: jobName,\n evidence:\n \"AI job references secrets or token-like environment variables\",\n }),\n );\n }\n if (jobUsesAi && jobHasWrite) {\n findings.push(\n makeFinding(\"agentci/broad-write-permissions\", {\n file,\n job: jobName,\n evidence: `permissions: ${JSON.stringify(jobPermissions)}`,\n }),\n );\n }\n\n for (const [index, rawStep] of steps.entries()) {\n if (!isRecord(rawStep)) continue;\n const stepName =\n typeof rawStep.name === \"string\" ? rawStep.name : `step ${index + 1}`;\n const stepText = JSON.stringify(rawStep);\n const stepUses = typeof rawStep.uses === \"string\" ? rawStep.uses : \"\";\n const stepRun = typeof rawStep.run === \"string\" ? rawStep.run : \"\";\n const stepUsesAi = looksLikeAiUsage(stepText);\n const stepUntrustedText = JSON.stringify(stripGuards(rawStep));\n\n if (stepUsesAi && containsUntrustedGitHubContext(stepUntrustedText)) {\n findings.push(\n makeFinding(\"agentci/untrusted-input-in-prompt\", {\n file,\n job: jobName,\n step: stepName,\n evidence: shrink(stepText),\n }),\n );\n }\n if (\n stepUsesAi &&\n (containsShellAccess(stepRun) || containsShellAccess(stepText))\n ) {\n findings.push(\n makeFinding(\"agentci/ai-shell-access\", {\n file,\n job: jobName,\n step: stepName,\n evidence: shrink(stepText),\n }),\n );\n }\n if (\n stepUsesAi &&\n stepUses &&\n !isPinnedAction(stepUses) &&\n !isLocalAction(stepUses)\n ) {\n findings.push(\n makeFinding(\"agentci/unpinned-ai-action\", {\n file,\n job: jobName,\n step: stepName,\n evidence: `uses: ${stepUses}`,\n }),\n );\n }\n if (\n isPullRequestTarget &&\n stepUses.includes(\"actions/checkout\") &&\n stepText.includes(\"github.event.pull_request.head\")\n ) {\n findings.push(\n makeFinding(\"agentci/unsafe-checkout\", {\n file,\n job: jobName,\n step: stepName,\n evidence: shrink(stepText),\n }),\n );\n }\n }\n }\n\n const ignores = parseInlineIgnores(workflow.raw);\n const visible = ignores.all\n ? []\n : findings.filter((finding) => !ignores.rules.has(finding.rule_id));\n return dedupe(visible);\n}\n\nexport function hasFindingAtOrAbove(\n findings: Finding[],\n severity: Severity,\n): boolean {\n return findings.some(\n (finding) =>\n SEVERITY_ORDER.indexOf(finding.severity) >=\n SEVERITY_ORDER.indexOf(severity),\n );\n}\n\nfunction makeFinding(ruleId: string, context: FindingContext): Finding {\n const rule = RULES[ruleId];\n if (!rule) throw new Error(`Unknown rule: ${ruleId}`);\n const id = `${ruleId}:${context.file}:${context.job ?? \"\"}:${context.step ?? \"\"}`;\n return {\n id,\n rule_id: rule.id,\n title: rule.title,\n severity: rule.severity,\n file: context.file,\n job: context.job,\n step: context.step,\n message: rule.title,\n why: rule.why,\n fix: rule.fix,\n evidence: context.evidence,\n };\n}\n\nfunction normalizeTriggers(raw: unknown): string[] {\n if (typeof raw === \"string\") return [raw];\n if (Array.isArray(raw))\n return raw.filter((item): item is string => typeof item === \"string\");\n if (isRecord(raw)) return Object.keys(raw);\n return [];\n}\n\nfunction isUntrustedTrigger(trigger: string): boolean {\n return [\n \"pull_request\",\n \"pull_request_target\",\n \"issue_comment\",\n \"issues\",\n \"discussion\",\n \"discussion_comment\",\n \"workflow_run\",\n ].includes(trigger);\n}\n\nfunction normalizePermissions(raw: unknown): Record<string, string> {\n if (typeof raw === \"string\") return { contents: raw };\n if (!isRecord(raw)) return {};\n return Object.fromEntries(\n Object.entries(raw).filter(\n (entry): entry is [string, string] => typeof entry[1] === \"string\",\n ),\n );\n}\n\n// Write scopes that actually let an agent alter the repository — code,\n// releases, PRs, issues, packages, deployments. Scopes like `id-token`\n// (OIDC), `actions`, `checks`, `statuses`, `pages`, and `security-events`\n// grant `write` but do not let a prompt-injected agent modify the repo, so\n// they must not trip the AI-write-token / broad-write rules.\nconst SENSITIVE_WRITE_SCOPES = new Set([\n \"contents\",\n \"pull-requests\",\n \"issues\",\n \"packages\",\n \"deployments\",\n]);\n\nfunction hasWritePermission(permissions: Record<string, string>): boolean {\n return Object.entries(permissions).some(([scope, level]) => {\n if (level !== \"write\" && level !== \"write-all\") return false;\n // `permissions: write-all` normalizes to contents:write-all (all scopes).\n return level === \"write-all\" || SENSITIVE_WRITE_SCOPES.has(scope);\n });\n}\n\n/**\n * Deep-clone a workflow node with every `if:` condition removed. Untrusted\n * event references inside an `if:` are guards, not values that reach the agent.\n */\nfunction stripGuards(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(stripGuards);\n if (isRecord(value)) {\n const out: Record<string, unknown> = {};\n for (const [key, val] of Object.entries(value)) {\n if (key === \"if\") continue;\n out[key] = stripGuards(val);\n }\n return out;\n }\n return value;\n}\n\nfunction isRecord(value: unknown): value is WorkflowMap {\n return Boolean(value) && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction isLocalAction(uses: string): boolean {\n return uses.startsWith(\"./\") || uses.startsWith(\"docker://\");\n}\n\nfunction shrink(value: string): string {\n return value.length > 500 ? `${value.slice(0, 500)}...` : value;\n}\n\nfunction dedupe(findings: Finding[]): Finding[] {\n const seen = new Set<string>();\n return findings.filter((finding) => {\n const key = `${finding.rule_id}:${finding.file}:${finding.job ?? \"\"}:${finding.step ?? \"\"}`;\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n });\n}\n\nfunction summarize(findings: Finding[]): Record<Severity, number> {\n return {\n low: findings.filter((finding) => finding.severity === \"low\").length,\n medium: findings.filter((finding) => finding.severity === \"medium\").length,\n high: findings.filter((finding) => finding.severity === \"high\").length,\n critical: findings.filter((finding) => finding.severity === \"critical\")\n .length,\n };\n}\n"],"mappings":"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","names":["fs","pc","path","fs","path","fs","RULES","pc"]}
@@ -0,0 +1,135 @@
1
+ type AgentciConfig = {
2
+ /** Rule ids to suppress everywhere, e.g. "agentci/unpinned-ai-action". */
3
+ ignore: string[];
4
+ /** Workflow path globs (relative to scan root) to exclude from reporting. */
5
+ ignorePaths: string[];
6
+ };
7
+ /** Load config from an explicit path, or by discovery in the scan root. */
8
+ declare function loadConfig(root: string, explicitPath?: string): Promise<AgentciConfig>;
9
+ /**
10
+ * Inline, file-level suppression directives read from raw workflow text:
11
+ * # agentci-ignore <rule-id> [<rule-id> ...] [-- reason]
12
+ * # agentci-ignore-all [-- reason]
13
+ *
14
+ * Findings are reported at job/step granularity rather than per line, so
15
+ * suppression is scoped to the whole file. The optional `-- reason` is for
16
+ * humans and is ignored by the parser.
17
+ */
18
+ declare function parseInlineIgnores(raw: string): {
19
+ all: boolean;
20
+ rules: Set<string>;
21
+ };
22
+ /** Minimal glob match: `*` matches within a path segment, `**` across segments. */
23
+ declare function matchesPath(glob: string, target: string): boolean;
24
+
25
+ declare const AI_AGENT_PATTERNS: RegExp[];
26
+ declare function looksLikeAiUsage(value: string): boolean;
27
+ declare function containsUntrustedGitHubContext(value: string): boolean;
28
+ declare function containsSecretReference(value: string): boolean;
29
+ declare function containsShellAccess(value: string): boolean;
30
+ declare function isPinnedAction(uses: string): boolean;
31
+
32
+ type Severity = "low" | "medium" | "high" | "critical";
33
+ type Finding = {
34
+ id: string;
35
+ rule_id: string;
36
+ title: string;
37
+ severity: Severity;
38
+ file: string;
39
+ job?: string;
40
+ step?: string;
41
+ message: string;
42
+ why: string;
43
+ fix: string[];
44
+ evidence: string;
45
+ };
46
+ type ScanOptions = {
47
+ cwd: string;
48
+ /** Explicit path to an agentci config JSON file (overrides discovery). */
49
+ configPath?: string;
50
+ };
51
+ type WorkflowFile = {
52
+ path: string;
53
+ document: unknown;
54
+ raw: string;
55
+ };
56
+ type ScanResult = {
57
+ scanned_at: string;
58
+ root: string;
59
+ workflow_count: number;
60
+ findings: Finding[];
61
+ summary: Record<Severity, number>;
62
+ };
63
+ type SarifLog = {
64
+ version: "2.1.0";
65
+ $schema: string;
66
+ runs: Array<{
67
+ tool: {
68
+ driver: {
69
+ name: string;
70
+ informationUri: string;
71
+ rules: Array<{
72
+ id: string;
73
+ name: string;
74
+ shortDescription: {
75
+ text: string;
76
+ };
77
+ fullDescription: {
78
+ text: string;
79
+ };
80
+ help: {
81
+ text: string;
82
+ markdown: string;
83
+ };
84
+ defaultConfiguration: {
85
+ level: "note" | "warning" | "error";
86
+ };
87
+ }>;
88
+ };
89
+ };
90
+ results: Array<{
91
+ ruleId: string;
92
+ level: "note" | "warning" | "error";
93
+ message: {
94
+ text: string;
95
+ };
96
+ locations: Array<{
97
+ physicalLocation: {
98
+ artifactLocation: {
99
+ uri: string;
100
+ };
101
+ region?: {
102
+ startLine?: number;
103
+ };
104
+ };
105
+ }>;
106
+ }>;
107
+ }>;
108
+ };
109
+
110
+ /**
111
+ * Render the GitHub Actions `$GITHUB_OUTPUT` lines for a scan, so downstream
112
+ * steps can branch on the result (e.g. comment on a PR when critical > 0).
113
+ */
114
+ declare function formatGithubOutputs(result: ScanResult, sarifPath?: string): string;
115
+ declare function renderTextReport(result: ScanResult): string;
116
+ declare function renderMarkdownReport(result: ScanResult): string;
117
+
118
+ type RuleDefinition = {
119
+ id: string;
120
+ title: string;
121
+ severity: Severity;
122
+ why: string;
123
+ fix: string[];
124
+ };
125
+ declare const RULES: Record<string, RuleDefinition>;
126
+ declare const SEVERITY_ORDER: Severity[];
127
+
128
+ declare function toSarif(findings: Finding[]): SarifLog;
129
+
130
+ declare function scanRepository(root: string, options?: Partial<ScanOptions>): Promise<ScanResult>;
131
+ declare function loadWorkflowFiles(root: string): Promise<WorkflowFile[]>;
132
+ declare function scanWorkflow(workflow: WorkflowFile, root: string): Finding[];
133
+ declare function hasFindingAtOrAbove(findings: Finding[], severity: Severity): boolean;
134
+
135
+ export { AI_AGENT_PATTERNS, type AgentciConfig, type Finding, RULES, type RuleDefinition, SEVERITY_ORDER, type SarifLog, type ScanOptions, type ScanResult, type Severity, type WorkflowFile, containsSecretReference, containsShellAccess, containsUntrustedGitHubContext, formatGithubOutputs, hasFindingAtOrAbove, isPinnedAction, loadConfig, loadWorkflowFiles, looksLikeAiUsage, matchesPath, parseInlineIgnores, renderMarkdownReport, renderTextReport, scanRepository, scanWorkflow, toSarif };