Skip to content

Commit 8bb41ea

Browse files
feat: add workflow audit pack with real-case validation
1 parent f3cf1b1 commit 8bb41ea

126 files changed

Lines changed: 10192 additions & 159 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -120,21 +120,26 @@ See the [Configuration](#configuration) section for full settings and examples.
120120

121121
## `scan` Command Flags
122122

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

139144
Examples:
140145

@@ -148,9 +153,37 @@ codegate scan . --deep --force
148153
codegate scan . --remediate
149154
codegate scan . --fix-safe
150155
codegate scan . --remediate --dry-run --patch
156+
codegate scan . --workflow-audits --collect project --persona auditor --runtime-mode online
157+
codegate scan . --workflow-audits --strict-collection
151158
codegate scan . --reset-state
152159
```
153160

161+
## Workflow Audit Pack
162+
163+
CodeGate can audit GitHub Actions workflows when `--workflow-audits` is enabled.
164+
165+
Current checks include:
166+
167+
- Unpinned external action references (`uses: owner/repo@tag` instead of commit SHA)
168+
- High-risk triggers (`pull_request_target`, `workflow_run`)
169+
- Overly broad permissions (`write-all` and explicit write grants)
170+
- Template expression injection patterns in run steps and known sink inputs
171+
- Known vulnerable action references (online runtime mode)
172+
- Dependabot cooldown and execution-risk checks
173+
- Workflow hygiene checks (concurrency gates, obfuscation, unsafe conditional trust)
174+
175+
Track the current workflow-audit coverage and backlog in the [workflow audit parity checklist](docs/workflow-audit-parity-checklist.md).
176+
Real public validation fixtures and source provenance are documented in [workflow audit real-case corpus](docs/workflow-audit-real-cases.md).
177+
178+
Examples:
179+
180+
```bash
181+
codegate scan . --workflow-audits
182+
codegate scan . --workflow-audits --collect project --persona auditor
183+
codegate scan . --workflow-audits --runtime-mode online
184+
codegate scan . --workflow-audits --collect-kind dependabot
185+
```
186+
154187
## `scan-content` Command
155188

156189
`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.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# CodeGate Workflow Audit Parity Checklist
2+
3+
Use this checklist to track the workflow-audit detectors implemented in CodeGate and the backlog that remains.
4+
5+
## Wave A
6+
7+
- [x] `dangerous-triggers`
8+
- [x] `excessive-permissions`
9+
- [x] `known-vulnerable-actions`
10+
- [x] `template-injection`
11+
- [x] `unpinned-uses`
12+
- [x] `artipacked`
13+
- [x] `cache-poisoning`
14+
- [x] `github-env`
15+
- [x] `insecure-commands`
16+
- [x] `self-hosted-runner`
17+
- [x] `overprovisioned-secrets`
18+
- [x] `secrets-outside-env`
19+
- [x] `secrets-inherit`
20+
- [x] `use-trusted-publishing`
21+
- [x] `undocumented-permissions`
22+
23+
## Wave B
24+
25+
- [x] `archived-uses`
26+
- [x] `stale-action-refs`
27+
- [x] `forbidden-uses`
28+
- [x] `ref-confusion`
29+
- [x] `ref-version-mismatch`
30+
- [x] `impostor-commit`
31+
- [x] `unpinned-images`
32+
33+
## Wave C
34+
35+
- [x] `anonymous-definition`
36+
- [x] `concurrency-limits`
37+
- [x] `superfluous-actions`
38+
- [x] `misfeature`
39+
- [x] `obfuscation`
40+
- [x] `unsound-condition`
41+
- [x] `unsound-contains`
42+
43+
## Wave D
44+
45+
- [x] `dependabot-cooldown`
46+
- [x] `dependabot-execution`
47+
48+
## Wave E
49+
50+
- [x] `hardcoded-container-credentials`
51+
- [x] `unredacted-secrets`
52+
- [x] `bot-conditions`
53+
54+
## Notes
55+
56+
- Checked items are implemented in CodeGate.
57+
- Unchecked items remain in the backlog.
58+
- The checklist is intentionally limited to CodeGate workflow-audit terminology.

docs/workflow-audit-real-cases.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Workflow Audit Real-Case Corpus
2+
3+
This document tracks real public workflow/dependabot examples used to validate workflow-audit detections locally.
4+
5+
## Local Corpus
6+
7+
Root:
8+
9+
- `test-fixtures/workflow-audits/real-cases/`
10+
- `test-fixtures/workflow-audits/real-cases/index.json`
11+
12+
Each fixture is commit-pinned to keep source provenance stable.
13+
14+
## Cases
15+
16+
1. `RC-01-bot-conditions`
17+
18+
- Expected rule: `bot-conditions`
19+
- Source: <https://github.com/Significant-Gravitas/AutoGPT/blob/0f67e45d05c855077236f739ca3a02fa95fc7e96/.github/workflows/claude-dependabot.yml>
20+
- Local file: `test-fixtures/workflow-audits/real-cases/RC-01-bot-conditions/.github/workflows/claude-dependabot.yml`
21+
22+
2. `RC-02-obfuscation`
23+
24+
- Expected rule: `workflow-obfuscation`
25+
- Source: <https://github.com/electron/electron/blob/6df6ec5f094f1546b5510c47aa478b2e19187f88/.github/workflows/pipeline-electron-lint.yml>
26+
- Local file: `test-fixtures/workflow-audits/real-cases/RC-02-obfuscation/.github/workflows/pipeline-electron-lint.yml`
27+
28+
3. `RC-03-concurrency-limits`
29+
30+
- Expected rule: `workflow-concurrency-limits`
31+
- Source: <https://github.com/electricitymaps/electricitymaps-contrib/blob/7d22bea77bd73a9bcc8c7e6fe78a973713ba8637/.github/workflows/label.yml>
32+
- Local file: `test-fixtures/workflow-audits/real-cases/RC-03-concurrency-limits/.github/workflows/label.yml`
33+
34+
4. `RC-04-dependabot-execution`
35+
36+
- Expected rule: `dependabot-execution`
37+
- Source: <https://github.com/RoleModel/rolemodel_rails/blob/83f8c13518afd1137405b81fc4723e202f833368/lib/generators/rolemodel/github/templates/dependabot.yml>
38+
- Local file: `test-fixtures/workflow-audits/real-cases/RC-04-dependabot-execution/.github/dependabot.yml`
39+
40+
## Validation
41+
42+
Run targeted test:
43+
44+
```bash
45+
npm test -- tests/layer2/workflow-real-cases.test.ts
46+
```
47+
48+
Run CLI manually:
49+
50+
```bash
51+
codegate scan test-fixtures/workflow-audits/real-cases/RC-02-obfuscation --workflow-audits --no-tui --format json
52+
```

src/cli.ts

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import { Command, Option } from "commander";
1010
import {
1111
DEFAULT_CONFIG,
1212
OUTPUT_FORMATS,
13+
PERSONAS,
14+
RUNTIME_MODES,
15+
SCAN_COLLECTION_MODES,
16+
SCAN_COLLECTION_KINDS,
1317
resolveEffectiveConfig,
1418
type CliConfigOverrides,
1519
type CodeGateConfig,
@@ -286,6 +290,8 @@ const defaultCliDeps: CliDeps = {
286290
prepareScanDiscovery: (scanTarget, config, options) =>
287291
createScanDiscoveryContext(scanTarget, undefined, {
288292
includeUserScope: config?.scan_user_scope === true,
293+
collectModes: config?.scan_collection_modes,
294+
collectKinds: config?.scan_collection_kinds,
289295
parseSelected: true,
290296
explicitCandidates: options?.explicitCandidates,
291297
}),
@@ -318,6 +324,8 @@ const defaultCliDeps: CliDeps = {
318324
? discoverDeepScanResourcesFromContext(discoveryContext)
319325
: discoverDeepScanResources(scanTarget, undefined, {
320326
includeUserScope: config?.scan_user_scope === true,
327+
collectModes: config?.scan_collection_modes,
328+
collectKinds: config?.scan_collection_kinds,
321329
}),
322330
discoverLocalTextTargets: (_scanTarget, _config, discoveryContext) =>
323331
discoveryContext ? discoverLocalTextAnalysisTargetsFromContext(discoveryContext) : [],
@@ -362,6 +370,34 @@ function addScanCommand(program: Command, version: string, deps: CliDeps): void
362370
.option("--config <path>", "use a specific global config file")
363371
.option("--force", "skip interactive confirmations")
364372
.option("--include-user-scope", "include user/home AI tool config paths in scan")
373+
.addOption(
374+
new Option(
375+
"--collect <mode>",
376+
"collection mode (repeatable): default, project, user, explicit, all",
377+
)
378+
.choices([...SCAN_COLLECTION_MODES])
379+
.argParser((value: string, previous: string[] = []) => [...previous, value]),
380+
)
381+
.addOption(
382+
new Option(
383+
"--collect-kind <kind>",
384+
"collection kind (repeatable): workflows, actions, dependabot",
385+
)
386+
.choices([...SCAN_COLLECTION_KINDS])
387+
.argParser((value: string, previous: string[] = []) => [...previous, value]),
388+
)
389+
.option("--strict-collection", "treat parse failures in collected inputs as high severity")
390+
.addOption(
391+
new Option("--persona <type>", "audit sensitivity persona")
392+
.choices([...PERSONAS])
393+
.argParser((value) => value),
394+
)
395+
.addOption(
396+
new Option("--runtime-mode <mode>", "runtime network mode for optional online audits")
397+
.choices([...RUNTIME_MODES])
398+
.argParser((value) => value),
399+
)
400+
.option("--workflow-audits", "enable workflow security audit pack for .github/workflows")
365401
.option("--skill <name>", "select one skill directory when scanning a skills index repo URL")
366402
.option("--reset-state", "clear persisted scan-state history and exit")
367403
.addHelpText(
@@ -371,6 +407,7 @@ function addScanCommand(program: Command, version: string, deps: CliDeps): void
371407
"codegate scan ./skills/security-review/SKILL.md",
372408
"codegate scan https://github.com/owner/repo",
373409
"codegate scan https://github.com/owner/repo --skill security-review",
410+
"codegate scan . --workflow-audits --collect project --persona auditor --runtime-mode online",
374411
"codegate scan https://github.com/owner/repo/blob/main/skills/security-review/SKILL.md",
375412
"codegate scan https://example.com/security-review/SKILL.md --format json",
376413
]),
@@ -388,6 +425,7 @@ function addScanCommand(program: Command, version: string, deps: CliDeps): void
388425
let resolvedTarget: ResolvedScanTarget | undefined;
389426

390427
try {
428+
const scanOptions = options as ScanCommandOptions & { collectKind?: string[] };
391429
const resolveTarget =
392430
deps.resolveScanTarget ??
393431
((input: {
@@ -412,13 +450,30 @@ function addScanCommand(program: Command, version: string, deps: CliDeps): void
412450
scanTarget,
413451
cli: cliConfig,
414452
});
415-
const config =
416-
options.includeUserScope === true
417-
? {
418-
...baseConfig,
419-
scan_user_scope: true,
420-
}
421-
: baseConfig;
453+
const config = {
454+
...baseConfig,
455+
scan_collection_modes:
456+
options.collect && options.collect.length > 0
457+
? options.collect
458+
: baseConfig.scan_collection_modes,
459+
scan_collection_kinds: (scanOptions.collectKind && scanOptions.collectKind.length > 0
460+
? scanOptions.collectKind
461+
: baseConfig.scan_collection_kinds) as CodeGateConfig["scan_collection_kinds"],
462+
strict_collection:
463+
options.strictCollection === true
464+
? true
465+
: (baseConfig.strict_collection ?? DEFAULT_CONFIG.strict_collection),
466+
persona: options.persona ?? baseConfig.persona,
467+
runtime_mode: options.runtimeMode ?? baseConfig.runtime_mode,
468+
workflow_audits: {
469+
enabled:
470+
options.workflowAudits === true
471+
? true
472+
: (baseConfig.workflow_audits?.enabled ?? false),
473+
},
474+
scan_user_scope:
475+
options.includeUserScope === true ? true : (baseConfig.scan_user_scope ?? false),
476+
};
422477

423478
if (options.resetState) {
424479
const reset = deps.resetScanState ?? ((path?: string) => resetScanState(path));

src/commands/scan-command.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import { resolve } from "node:path";
2-
import { applyConfigPolicy, type CodeGateConfig, type OutputFormat } from "../config.js";
2+
import {
3+
applyConfigPolicy,
4+
type AuditPersona,
5+
type CodeGateConfig,
6+
type OutputFormat,
7+
type RuntimeMode,
8+
type ScanCollectionMode,
9+
} from "../config.js";
310
import {
411
buildMetaAgentCommand,
512
type MetaAgentCommand,
@@ -52,6 +59,11 @@ export interface ScanCommandOptions {
5259
resetState?: boolean;
5360
includeUserScope?: boolean;
5461
skill?: string;
62+
collect?: ScanCollectionMode[];
63+
strictCollection?: boolean;
64+
persona?: AuditPersona;
65+
runtimeMode?: RuntimeMode;
66+
workflowAudits?: boolean;
5567
}
5668

5769
export interface ScanRunnerInput {

src/commands/scan-content-command.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export async function executeScanContentCommand(
4343

4444
const kbVersion = loadKnowledgeBase().schemaVersion;
4545
const report = applyConfigPolicy(
46-
runStaticPipeline({
46+
await runStaticPipeline({
4747
version: input.version,
4848
kbVersion,
4949
scanTarget: `scan-content:${input.type}`,
@@ -71,6 +71,10 @@ export async function executeScanContentCommand(
7171
rulePackPaths: input.config.rule_pack_paths,
7272
allowedRules: input.config.allowed_rules,
7373
skipRules: input.config.skip_rules,
74+
persona: input.config.persona,
75+
runtimeMode: input.config.runtime_mode,
76+
workflowAuditsEnabled: input.config.workflow_audits?.enabled === true,
77+
rulePolicies: input.config.rules,
7478
},
7579
}),
7680
input.config,

0 commit comments

Comments
 (0)