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
63 changes: 48 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,21 +120,26 @@ See the [Configuration](#configuration) section for full settings and examples.

## `scan` Command Flags

| Flag | Purpose |
| ---------------------- | --------------------------------------------------------------------------------------------------------- |
| `--deep` | Enable Layer 3 dynamic analysis. |
| `--remediate` | Enter remediation mode after scan. |
| `--fix-safe` | Auto-fix unambiguous critical findings. |
| `--dry-run` | Show proposed fixes but write nothing. |
| `--patch` | Generate a patch file for review workflows. |
| `--no-tui` | Disable TUI and interactive prompts. |
| `--format <type>` | Output format: `terminal`, `json`, `sarif`, `markdown`, `html`. |
| `--output <path>` | Write report to file instead of stdout. |
| `--verbose` | Show extended output in terminal format. |
| `--config <path>` | Use a specific global config file path. |
| `--force` | Skip interactive confirmations. |
| `--include-user-scope` | Force-enable user/home AI tool config paths for this run (useful if config disables user-scope scanning). |
| `--reset-state` | Clear persisted scan-state history and exit. |
| Flag | Purpose |
| ----------------------- | --------------------------------------------------------------------------------------------------------- |
| `--deep` | Enable Layer 3 dynamic analysis. |
| `--remediate` | Enter remediation mode after scan. |
| `--fix-safe` | Auto-fix unambiguous critical findings. |
| `--dry-run` | Show proposed fixes but write nothing. |
| `--patch` | Generate a patch file for review workflows. |
| `--no-tui` | Disable TUI and interactive prompts. |
| `--format <type>` | Output format: `terminal`, `json`, `sarif`, `markdown`, `html`. |
| `--output <path>` | Write report to file instead of stdout. |
| `--verbose` | Show extended output in terminal format. |
| `--config <path>` | Use a specific global config file path. |
| `--force` | Skip interactive confirmations. |
| `--include-user-scope` | Force-enable user/home AI tool config paths for this run (useful if config disables user-scope scanning). |
| `--collect <mode>` | Collection scope mode (`default`, `project`, `user`, `explicit`, `all`). Repeatable. |
| `--strict-collection` | Treat parse failures in collected inputs as high-severity findings. |
| `--persona <type>` | Audit sensitivity (`regular`, `pedantic`, `auditor`). |
| `--runtime-mode <mode>` | Runtime mode for optional online audits (`offline`, `online`, `online-no-audits`). |
| `--workflow-audits` | Enable CI/CD audit pack for GitHub workflow, action, and Dependabot inputs. |
| `--reset-state` | Clear persisted scan-state history and exit. |

Examples:

Expand All @@ -148,9 +153,37 @@ codegate scan . --deep --force
codegate scan . --remediate
codegate scan . --fix-safe
codegate scan . --remediate --dry-run --patch
codegate scan . --workflow-audits --collect project --persona auditor --runtime-mode online
codegate scan . --workflow-audits --strict-collection
codegate scan . --reset-state
```

## Workflow Audit Pack

CodeGate can audit GitHub Actions workflows when `--workflow-audits` is enabled.

Current checks include:

- Unpinned external action references (`uses: owner/repo@tag` instead of commit SHA)
- High-risk triggers (`pull_request_target`, `workflow_run`)
- Overly broad permissions (`write-all` and explicit write grants)
- Template expression injection patterns in run steps and known sink inputs
- Known vulnerable action references (online runtime mode)
- Dependabot cooldown and execution-risk checks
- Workflow hygiene checks (concurrency gates, obfuscation, unsafe conditional trust)

Track the current workflow-audit coverage and backlog in the [workflow audit parity checklist](docs/workflow-audit-parity-checklist.md).
Real public validation fixtures and source provenance are documented in [workflow audit real-case corpus](docs/workflow-audit-real-cases.md).

Examples:

```bash
codegate scan . --workflow-audits
codegate scan . --workflow-audits --collect project --persona auditor
codegate scan . --workflow-audits --runtime-mode online
codegate scan . --workflow-audits --collect-kind dependabot
```

## `scan-content` Command

`codegate scan-content <content...>` scans inline content directly from the command line. It is useful when you want to inspect JSON, YAML, TOML, Markdown, or plain text before writing it to disk or installing it into a tool configuration.
Expand Down
58 changes: 58 additions & 0 deletions docs/workflow-audit-parity-checklist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# CodeGate Workflow Audit Parity Checklist

Use this checklist to track the workflow-audit detectors implemented in CodeGate and the backlog that remains.

## Wave A

- [x] `dangerous-triggers`
- [x] `excessive-permissions`
- [x] `known-vulnerable-actions`
- [x] `template-injection`
- [x] `unpinned-uses`
- [x] `artipacked`
- [x] `cache-poisoning`
- [x] `github-env`
- [x] `insecure-commands`
- [x] `self-hosted-runner`
- [x] `overprovisioned-secrets`
- [x] `secrets-outside-env`
- [x] `secrets-inherit`
- [x] `use-trusted-publishing`
- [x] `undocumented-permissions`

## Wave B

- [x] `archived-uses`
- [x] `stale-action-refs`
- [x] `forbidden-uses`
- [x] `ref-confusion`
- [x] `ref-version-mismatch`
- [x] `impostor-commit`
- [x] `unpinned-images`

## Wave C

- [x] `anonymous-definition`
- [x] `concurrency-limits`
- [x] `superfluous-actions`
- [x] `misfeature`
- [x] `obfuscation`
- [x] `unsound-condition`
- [x] `unsound-contains`

## Wave D

- [x] `dependabot-cooldown`
- [x] `dependabot-execution`

## Wave E

- [x] `hardcoded-container-credentials`
- [x] `unredacted-secrets`
- [x] `bot-conditions`

## Notes

- Checked items are implemented in CodeGate.
- Unchecked items remain in the backlog.
- The checklist is intentionally limited to CodeGate workflow-audit terminology.
52 changes: 52 additions & 0 deletions docs/workflow-audit-real-cases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Workflow Audit Real-Case Corpus

This document tracks real public workflow/dependabot examples used to validate workflow-audit detections locally.

## Local Corpus

Root:

- `test-fixtures/workflow-audits/real-cases/`
- `test-fixtures/workflow-audits/real-cases/index.json`

Each fixture is commit-pinned to keep source provenance stable.

## Cases

1. `RC-01-bot-conditions`

- Expected rule: `bot-conditions`
- Source: <https://github.com/Significant-Gravitas/AutoGPT/blob/0f67e45d05c855077236f739ca3a02fa95fc7e96/.github/workflows/claude-dependabot.yml>
- Local file: `test-fixtures/workflow-audits/real-cases/RC-01-bot-conditions/.github/workflows/claude-dependabot.yml`

2. `RC-02-obfuscation`

- Expected rule: `workflow-obfuscation`
- Source: <https://github.com/electron/electron/blob/6df6ec5f094f1546b5510c47aa478b2e19187f88/.github/workflows/pipeline-electron-lint.yml>
- Local file: `test-fixtures/workflow-audits/real-cases/RC-02-obfuscation/.github/workflows/pipeline-electron-lint.yml`

3. `RC-03-concurrency-limits`

- Expected rule: `workflow-concurrency-limits`
- Source: <https://github.com/electricitymaps/electricitymaps-contrib/blob/7d22bea77bd73a9bcc8c7e6fe78a973713ba8637/.github/workflows/label.yml>
- Local file: `test-fixtures/workflow-audits/real-cases/RC-03-concurrency-limits/.github/workflows/label.yml`

4. `RC-04-dependabot-execution`

- Expected rule: `dependabot-execution`
- Source: <https://github.com/RoleModel/rolemodel_rails/blob/83f8c13518afd1137405b81fc4723e202f833368/lib/generators/rolemodel/github/templates/dependabot.yml>
- Local file: `test-fixtures/workflow-audits/real-cases/RC-04-dependabot-execution/.github/dependabot.yml`

## Validation

Run targeted test:

```bash
npm test -- tests/layer2/workflow-real-cases.test.ts
```

Run CLI manually:

```bash
codegate scan test-fixtures/workflow-audits/real-cases/RC-02-obfuscation --workflow-audits --no-tui --format json
```
69 changes: 62 additions & 7 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import { Command, Option } from "commander";
import {
DEFAULT_CONFIG,
OUTPUT_FORMATS,
PERSONAS,
RUNTIME_MODES,
SCAN_COLLECTION_MODES,
SCAN_COLLECTION_KINDS,
resolveEffectiveConfig,
type CliConfigOverrides,
type CodeGateConfig,
Expand Down Expand Up @@ -286,6 +290,8 @@ const defaultCliDeps: CliDeps = {
prepareScanDiscovery: (scanTarget, config, options) =>
createScanDiscoveryContext(scanTarget, undefined, {
includeUserScope: config?.scan_user_scope === true,
collectModes: config?.scan_collection_modes,
collectKinds: config?.scan_collection_kinds,
parseSelected: true,
explicitCandidates: options?.explicitCandidates,
}),
Expand Down Expand Up @@ -318,6 +324,8 @@ const defaultCliDeps: CliDeps = {
? discoverDeepScanResourcesFromContext(discoveryContext)
: discoverDeepScanResources(scanTarget, undefined, {
includeUserScope: config?.scan_user_scope === true,
collectModes: config?.scan_collection_modes,
collectKinds: config?.scan_collection_kinds,
}),
discoverLocalTextTargets: (_scanTarget, _config, discoveryContext) =>
discoveryContext ? discoverLocalTextAnalysisTargetsFromContext(discoveryContext) : [],
Expand Down Expand Up @@ -362,6 +370,34 @@ function addScanCommand(program: Command, version: string, deps: CliDeps): void
.option("--config <path>", "use a specific global config file")
.option("--force", "skip interactive confirmations")
.option("--include-user-scope", "include user/home AI tool config paths in scan")
.addOption(
new Option(
"--collect <mode>",
"collection mode (repeatable): default, project, user, explicit, all",
)
.choices([...SCAN_COLLECTION_MODES])
.argParser((value: string, previous: string[] = []) => [...previous, value]),
)
.addOption(
new Option(
"--collect-kind <kind>",
"collection kind (repeatable): workflows, actions, dependabot",
)
.choices([...SCAN_COLLECTION_KINDS])
.argParser((value: string, previous: string[] = []) => [...previous, value]),
)
.option("--strict-collection", "treat parse failures in collected inputs as high severity")
.addOption(
new Option("--persona <type>", "audit sensitivity persona")
.choices([...PERSONAS])
.argParser((value) => value),
)
.addOption(
new Option("--runtime-mode <mode>", "runtime network mode for optional online audits")
.choices([...RUNTIME_MODES])
.argParser((value) => value),
)
.option("--workflow-audits", "enable workflow security audit pack for .github/workflows")
.option("--skill <name>", "select one skill directory when scanning a skills index repo URL")
.option("--reset-state", "clear persisted scan-state history and exit")
.addHelpText(
Expand All @@ -371,6 +407,7 @@ function addScanCommand(program: Command, version: string, deps: CliDeps): void
"codegate scan ./skills/security-review/SKILL.md",
"codegate scan https://github.com/owner/repo",
"codegate scan https://github.com/owner/repo --skill security-review",
"codegate scan . --workflow-audits --collect project --persona auditor --runtime-mode online",
"codegate scan https://github.com/owner/repo/blob/main/skills/security-review/SKILL.md",
"codegate scan https://example.com/security-review/SKILL.md --format json",
]),
Expand All @@ -388,6 +425,7 @@ function addScanCommand(program: Command, version: string, deps: CliDeps): void
let resolvedTarget: ResolvedScanTarget | undefined;

try {
const scanOptions = options as ScanCommandOptions & { collectKind?: string[] };
const resolveTarget =
deps.resolveScanTarget ??
((input: {
Expand All @@ -412,13 +450,30 @@ function addScanCommand(program: Command, version: string, deps: CliDeps): void
scanTarget,
cli: cliConfig,
});
const config =
options.includeUserScope === true
? {
...baseConfig,
scan_user_scope: true,
}
: baseConfig;
const config = {
...baseConfig,
scan_collection_modes:
options.collect && options.collect.length > 0
? options.collect
: baseConfig.scan_collection_modes,
scan_collection_kinds: (scanOptions.collectKind && scanOptions.collectKind.length > 0
? scanOptions.collectKind
: baseConfig.scan_collection_kinds) as CodeGateConfig["scan_collection_kinds"],
strict_collection:
options.strictCollection === true
? true
: (baseConfig.strict_collection ?? DEFAULT_CONFIG.strict_collection),
persona: options.persona ?? baseConfig.persona,
runtime_mode: options.runtimeMode ?? baseConfig.runtime_mode,
workflow_audits: {
enabled:
options.workflowAudits === true
? true
: (baseConfig.workflow_audits?.enabled ?? false),
},
scan_user_scope:
options.includeUserScope === true ? true : (baseConfig.scan_user_scope ?? false),
};

if (options.resetState) {
const reset = deps.resetScanState ?? ((path?: string) => resetScanState(path));
Expand Down
14 changes: 13 additions & 1 deletion src/commands/scan-command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { resolve } from "node:path";
import { applyConfigPolicy, type CodeGateConfig, type OutputFormat } from "../config.js";
import {
applyConfigPolicy,
type AuditPersona,
type CodeGateConfig,
type OutputFormat,
type RuntimeMode,
type ScanCollectionMode,
} from "../config.js";
import {
buildMetaAgentCommand,
type MetaAgentCommand,
Expand Down Expand Up @@ -52,6 +59,11 @@ export interface ScanCommandOptions {
resetState?: boolean;
includeUserScope?: boolean;
skill?: string;
collect?: ScanCollectionMode[];
strictCollection?: boolean;
persona?: AuditPersona;
runtimeMode?: RuntimeMode;
workflowAudits?: boolean;
}

export interface ScanRunnerInput {
Expand Down
6 changes: 5 additions & 1 deletion src/commands/scan-content-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export async function executeScanContentCommand(

const kbVersion = loadKnowledgeBase().schemaVersion;
const report = applyConfigPolicy(
runStaticPipeline({
await runStaticPipeline({
version: input.version,
kbVersion,
scanTarget: `scan-content:${input.type}`,
Expand Down Expand Up @@ -71,6 +71,10 @@ export async function executeScanContentCommand(
rulePackPaths: input.config.rule_pack_paths,
allowedRules: input.config.allowed_rules,
skipRules: input.config.skip_rules,
persona: input.config.persona,
runtimeMode: input.config.runtime_mode,
workflowAuditsEnabled: input.config.workflow_audits?.enabled === true,
rulePolicies: input.config.rules,
},
}),
input.config,
Expand Down
Loading
Loading